@guardian/cdk 59.3.5 → 59.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"asg.d.ts","sourceRoot":"","sources":["../../../src/constructs/autoscaling/asg.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,gBAAgB,EAA6B,MAAM,6BAA6B,CAAC;AAC1F,OAAO,KAAK,EAAE,qBAAqB,EAAuB,MAAM,6BAA6B,CAAC;AAE9F,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAsB,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACtG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAQpD,MAAM,WAAW,uBACf,SAAQ,IAAI,CACR,qBAAqB,EACnB,SAAS,GACT,QAAQ,GACR,cAAc,GACd,UAAU,GACV,aAAa,GACb,aAAa,GACb,iBAAiB,GACjB,eAAe,GACf,eAAe,CAClB,EACD,WAAW,EACX,aAAa;IACf;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAClC,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,wBAAwB,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5C,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iCAAiC,CAAC,EAAE,OAAO,CAAC;IAC5C,qBAAqB,CAAC,EAAE,QAAQ,CAAC;CAClC;;;;;;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,kBAAmB,SAAQ,uBAAqC;IAC3E,SAAgB,GAAG,EAAE,MAAM,CAAC;IAC5B,SAAgB,YAAY,EAAE,cAAc,CAAC;IAC7C,SAAgB,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;gBAEtC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,uBAAuB;CAgGvE"}
1
+ {"version":3,"file":"asg.d.ts","sourceRoot":"","sources":["../../../src/constructs/autoscaling/asg.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,gBAAgB,EAAgB,MAAM,6BAA6B,CAAC;AAC7E,OAAO,KAAK,EAAE,qBAAqB,EAAuB,MAAM,6BAA6B,CAAC;AAE9F,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAsB,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACtG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAQpD,MAAM,WAAW,uBACf,SAAQ,IAAI,CACR,qBAAqB,EACnB,SAAS,GACT,QAAQ,GACR,cAAc,GACd,UAAU,GACV,aAAa,GACb,aAAa,GACb,iBAAiB,GACjB,eAAe,GACf,eAAe,CAClB,EACD,WAAW,EACX,aAAa;IACf;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAClC,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,wBAAwB,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5C,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iCAAiC,CAAC,EAAE,OAAO,CAAC;IAC5C,qBAAqB,CAAC,EAAE,QAAQ,CAAC;CAClC;;;;;;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,kBAAmB,SAAQ,uBAAqC;IAC3E,SAAgB,GAAG,EAAE,MAAM,CAAC;IAC5B,SAAgB,YAAY,EAAE,cAAc,CAAC;IAC7C,SAAgB,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;gBAEtC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,uBAAuB;CAgGvE"}
@@ -30,7 +30,7 @@ const iam_1 = require("../iam");
30
30
  */
31
31
  class GuAutoScalingGroup extends (0, app_aware_construct_1.GuAppAwareConstruct)(aws_autoscaling_1.AutoScalingGroup) {
32
32
  constructor(scope, id, props) {
33
- const { app, additionalSecurityGroups = [], blockDevices, imageId = new core_1.GuAmiParameter(scope, { app }), imageRecipe, instanceType, groupMetrics = [new aws_autoscaling_1.GroupMetrics(aws_autoscaling_1.GroupMetric.TOTAL_INSTANCES, aws_autoscaling_1.GroupMetric.IN_SERVICE_INSTANCES)], minimumInstances, maximumInstances, role = new iam_1.GuInstanceRole(scope, { app }), targetGroup, userData, vpc, withoutImdsv2 = false, httpPutResponseHopLimit, updatePolicy, enabledDetailedInstanceMonitoring, defaultInstanceWarmup, } = props;
33
+ const { app, additionalSecurityGroups = [], blockDevices, imageId = new core_1.GuAmiParameter(scope, { app }), imageRecipe, instanceType, groupMetrics = [aws_autoscaling_1.GroupMetrics.all()], minimumInstances, maximumInstances, role = new iam_1.GuInstanceRole(scope, { app }), targetGroup, userData, vpc, withoutImdsv2 = false, httpPutResponseHopLimit, updatePolicy, enabledDetailedInstanceMonitoring, defaultInstanceWarmup, } = props;
34
34
  // Ensure min and max are defined in the same way. Throwing an `Error` when necessary. For example when min is defined via a Mapping, but max is not.
35
35
  if (aws_cdk_lib_1.Token.isUnresolved(minimumInstances) && !aws_cdk_lib_1.Token.isUnresolved(maximumInstances)) {
36
36
  throw new Error("minimumInstances is defined via a Mapping, but maximumInstances is not. Create maximumInstances via a Mapping too.");
@@ -1 +1 @@
1
- {"version":3,"file":"asg.js","sourceRoot":"","sources":["../../../src/constructs/autoscaling/asg.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Duration } from \"aws-cdk-lib\";\nimport { Tags, Token } from \"aws-cdk-lib\";\nimport { AutoScalingGroup, GroupMetric, GroupMetrics } from \"aws-cdk-lib/aws-autoscaling\";\nimport type { AutoScalingGroupProps, CfnAutoScalingGroup } from \"aws-cdk-lib/aws-autoscaling\";\nimport { LaunchTemplate, OperatingSystemType } from \"aws-cdk-lib/aws-ec2\";\nimport type { InstanceType, ISecurityGroup, MachineImageConfig, UserData } from \"aws-cdk-lib/aws-ec2\";\nimport type { ApplicationTargetGroup } from \"aws-cdk-lib/aws-elasticloadbalancingv2\";\nimport type { GuAsgCapacity } from \"../../types\";\nimport type { AmigoProps } from \"../../types/amigo\";\nimport { GuAppAwareConstruct } from \"../../utils/mixin/app-aware-construct\";\nimport { GuAmiParameter } from \"../core\";\nimport type { AppIdentity, GuStack } from \"../core\";\nimport { GuHttpsEgressSecurityGroup, GuWazuhAccess } from \"../ec2\";\nimport { GuInstanceRole } from \"../iam\";\n\n// Since we want to override the types of what gets passed in for the below props,\n// we need to use Omit<T, U> to remove them from the interface this extends\n// https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys\n// TODO avoid prop nesting to improve discoverability. See https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md#flat.\nexport interface GuAutoScalingGroupProps\n extends Omit<\n AutoScalingGroupProps,\n | \"imageId\"\n | \"osType\"\n | \"machineImage\"\n | \"userData\"\n | \"minCapacity\"\n | \"maxCapacity\"\n | \"desiredCapacity\"\n | \"requireImdsv2\"\n | \"securityGroup\"\n >,\n AppIdentity,\n GuAsgCapacity {\n /**\n * @deprecated\n * It shouldn't be necessary to specify a value here.\n * GuCDK will create an AMI parameter by default. Riff-Raff can use this parameter to inject the latest AMI ID for\n * your AMIgo recipe.\n */\n imageId?: GuAmiParameter;\n /**\n * If you are using GuCDK to generate your riff-raff.yaml file, specify AMIgo props here.\n * If you are using a hardcoded riff-raff.yaml file (usually found in project root) then providing a value here has no\n * effect.\n */\n imageRecipe?: string | AmigoProps;\n instanceType: InstanceType;\n userData: UserData;\n additionalSecurityGroups?: ISecurityGroup[];\n targetGroup?: ApplicationTargetGroup;\n withoutImdsv2?: boolean;\n httpPutResponseHopLimit?: number;\n enabledDetailedInstanceMonitoring?: boolean;\n defaultInstanceWarmup?: Duration;\n}\n\n/**\n * Construct which creates an Auto Scaling group.\n *\n * By default, all EC2 instances in this group will use [[`GuInstanceRole`]],\n * which provides common permissions (e.g. the ability to download an artifact and write logs to the account's logging Kinesis stream).\n *\n * If additional IAM permissions are required, a custom role can be provided via the `role` prop.\n * You may wish to instantiate [[`GuInstanceRole`]] yourself as a basis for this custom role, as it allows custom permissions\n * to be passed in.\n *\n * All EC2 instances in this group will be automatically associated with two security groups:\n * 1. [[`GuHttpsEgressSecurityGroup`]], which allows outbound traffic over HTTPS.\n * 2. [[`GuWazuhAccess`]], which allows instances to communicate with Wazuh (for security monitoring).\n *\n * If additional ingress or egress rules are required, define custom security groups and pass them in via the\n * `additionalSecurityGroups` prop.\n *\n * All EC2 instances provisioned via this construct will use\n * [IMDSv2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html).\n */\nexport class GuAutoScalingGroup extends GuAppAwareConstruct(AutoScalingGroup) {\n public readonly app: string;\n public readonly amiParameter: GuAmiParameter;\n public readonly imageRecipe?: string | AmigoProps;\n\n constructor(scope: GuStack, id: string, props: GuAutoScalingGroupProps) {\n const {\n app,\n additionalSecurityGroups = [],\n blockDevices,\n imageId = new GuAmiParameter(scope, { app }),\n imageRecipe,\n instanceType,\n groupMetrics = [new GroupMetrics(GroupMetric.TOTAL_INSTANCES, GroupMetric.IN_SERVICE_INSTANCES)],\n minimumInstances,\n maximumInstances,\n role = new GuInstanceRole(scope, { app }),\n targetGroup,\n userData,\n vpc,\n withoutImdsv2 = false,\n httpPutResponseHopLimit,\n updatePolicy,\n enabledDetailedInstanceMonitoring,\n defaultInstanceWarmup,\n } = props;\n\n // Ensure min and max are defined in the same way. Throwing an `Error` when necessary. For example when min is defined via a Mapping, but max is not.\n if (Token.isUnresolved(minimumInstances) && !Token.isUnresolved(maximumInstances)) {\n throw new Error(\n \"minimumInstances is defined via a Mapping, but maximumInstances is not. Create maximumInstances via a Mapping too.\",\n );\n }\n\n // Generate an ID unique to this app\n const launchTemplateId = `${scope.stack}-${scope.stage}-${app}`;\n const launchTemplate = new LaunchTemplate(scope, launchTemplateId, {\n blockDevices,\n detailedMonitoring: enabledDetailedInstanceMonitoring,\n instanceType,\n machineImage: {\n getImage: (): MachineImageConfig => {\n return {\n osType: OperatingSystemType.LINUX,\n userData,\n imageId: imageId.valueAsString,\n };\n },\n },\n // Do not use the default AWS security group which allows egress on any port.\n // Favour HTTPS only egress rules by default.\n securityGroup: GuHttpsEgressSecurityGroup.forVpc(scope, { app, vpc }),\n requireImdsv2: !withoutImdsv2,\n instanceMetadataTags: true,\n userData,\n role,\n httpPutResponseHopLimit,\n });\n\n // Add Wazuh & additional consumer specified Security Groups\n // Note: Launch templates via CDK allow specifying only one SG, so use connections\n // https://github.com/aws/aws-cdk/issues/18712\n [GuWazuhAccess.getInstance(scope, vpc), ...additionalSecurityGroups].forEach((sg) =>\n launchTemplate.connections.addSecurityGroup(sg),\n );\n\n const asgProps: AutoScalingGroupProps = {\n ...props,\n launchTemplate,\n maxCapacity: maximumInstances ?? minimumInstances * 2,\n minCapacity: minimumInstances,\n groupMetrics: groupMetrics,\n\n // Omit userData, instanceType, blockDevices & role from asgProps\n // As this are specified by the LaunchTemplate and must not be duplicated\n blockDevices: undefined,\n instanceType: undefined,\n role: undefined,\n userData: undefined,\n defaultInstanceWarmup,\n };\n\n super(scope, id, asgProps);\n\n this.app = app;\n this.amiParameter = imageId;\n this.imageRecipe = imageRecipe;\n\n targetGroup && this.attachToApplicationTargetGroup(targetGroup);\n\n const cfnAsg = this.node.defaultChild as CfnAutoScalingGroup;\n\n // A CDK AutoScalingGroup comes with this update policy, whereas the CFN autscaling group\n // leaves it to the default value, which is actually false.\n // { UpdatePolicy: { autoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true }}\n if (!updatePolicy) {\n cfnAsg.addDeletionOverride(\"UpdatePolicy\");\n }\n\n Tags.of(launchTemplate).add(\"App\", app);\n }\n}\n"]}
1
+ {"version":3,"file":"asg.js","sourceRoot":"","sources":["../../../src/constructs/autoscaling/asg.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Duration } from \"aws-cdk-lib\";\nimport { Tags, Token } from \"aws-cdk-lib\";\nimport { AutoScalingGroup, GroupMetrics } from \"aws-cdk-lib/aws-autoscaling\";\nimport type { AutoScalingGroupProps, CfnAutoScalingGroup } from \"aws-cdk-lib/aws-autoscaling\";\nimport { LaunchTemplate, OperatingSystemType } from \"aws-cdk-lib/aws-ec2\";\nimport type { InstanceType, ISecurityGroup, MachineImageConfig, UserData } from \"aws-cdk-lib/aws-ec2\";\nimport type { ApplicationTargetGroup } from \"aws-cdk-lib/aws-elasticloadbalancingv2\";\nimport type { GuAsgCapacity } from \"../../types\";\nimport type { AmigoProps } from \"../../types/amigo\";\nimport { GuAppAwareConstruct } from \"../../utils/mixin/app-aware-construct\";\nimport { GuAmiParameter } from \"../core\";\nimport type { AppIdentity, GuStack } from \"../core\";\nimport { GuHttpsEgressSecurityGroup, GuWazuhAccess } from \"../ec2\";\nimport { GuInstanceRole } from \"../iam\";\n\n// Since we want to override the types of what gets passed in for the below props,\n// we need to use Omit<T, U> to remove them from the interface this extends\n// https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys\n// TODO avoid prop nesting to improve discoverability. See https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md#flat.\nexport interface GuAutoScalingGroupProps\n extends Omit<\n AutoScalingGroupProps,\n | \"imageId\"\n | \"osType\"\n | \"machineImage\"\n | \"userData\"\n | \"minCapacity\"\n | \"maxCapacity\"\n | \"desiredCapacity\"\n | \"requireImdsv2\"\n | \"securityGroup\"\n >,\n AppIdentity,\n GuAsgCapacity {\n /**\n * @deprecated\n * It shouldn't be necessary to specify a value here.\n * GuCDK will create an AMI parameter by default. Riff-Raff can use this parameter to inject the latest AMI ID for\n * your AMIgo recipe.\n */\n imageId?: GuAmiParameter;\n /**\n * If you are using GuCDK to generate your riff-raff.yaml file, specify AMIgo props here.\n * If you are using a hardcoded riff-raff.yaml file (usually found in project root) then providing a value here has no\n * effect.\n */\n imageRecipe?: string | AmigoProps;\n instanceType: InstanceType;\n userData: UserData;\n additionalSecurityGroups?: ISecurityGroup[];\n targetGroup?: ApplicationTargetGroup;\n withoutImdsv2?: boolean;\n httpPutResponseHopLimit?: number;\n enabledDetailedInstanceMonitoring?: boolean;\n defaultInstanceWarmup?: Duration;\n}\n\n/**\n * Construct which creates an Auto Scaling group.\n *\n * By default, all EC2 instances in this group will use [[`GuInstanceRole`]],\n * which provides common permissions (e.g. the ability to download an artifact and write logs to the account's logging Kinesis stream).\n *\n * If additional IAM permissions are required, a custom role can be provided via the `role` prop.\n * You may wish to instantiate [[`GuInstanceRole`]] yourself as a basis for this custom role, as it allows custom permissions\n * to be passed in.\n *\n * All EC2 instances in this group will be automatically associated with two security groups:\n * 1. [[`GuHttpsEgressSecurityGroup`]], which allows outbound traffic over HTTPS.\n * 2. [[`GuWazuhAccess`]], which allows instances to communicate with Wazuh (for security monitoring).\n *\n * If additional ingress or egress rules are required, define custom security groups and pass them in via the\n * `additionalSecurityGroups` prop.\n *\n * All EC2 instances provisioned via this construct will use\n * [IMDSv2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html).\n */\nexport class GuAutoScalingGroup extends GuAppAwareConstruct(AutoScalingGroup) {\n public readonly app: string;\n public readonly amiParameter: GuAmiParameter;\n public readonly imageRecipe?: string | AmigoProps;\n\n constructor(scope: GuStack, id: string, props: GuAutoScalingGroupProps) {\n const {\n app,\n additionalSecurityGroups = [],\n blockDevices,\n imageId = new GuAmiParameter(scope, { app }),\n imageRecipe,\n instanceType,\n groupMetrics = [GroupMetrics.all()],\n minimumInstances,\n maximumInstances,\n role = new GuInstanceRole(scope, { app }),\n targetGroup,\n userData,\n vpc,\n withoutImdsv2 = false,\n httpPutResponseHopLimit,\n updatePolicy,\n enabledDetailedInstanceMonitoring,\n defaultInstanceWarmup,\n } = props;\n\n // Ensure min and max are defined in the same way. Throwing an `Error` when necessary. For example when min is defined via a Mapping, but max is not.\n if (Token.isUnresolved(minimumInstances) && !Token.isUnresolved(maximumInstances)) {\n throw new Error(\n \"minimumInstances is defined via a Mapping, but maximumInstances is not. Create maximumInstances via a Mapping too.\",\n );\n }\n\n // Generate an ID unique to this app\n const launchTemplateId = `${scope.stack}-${scope.stage}-${app}`;\n const launchTemplate = new LaunchTemplate(scope, launchTemplateId, {\n blockDevices,\n detailedMonitoring: enabledDetailedInstanceMonitoring,\n instanceType,\n machineImage: {\n getImage: (): MachineImageConfig => {\n return {\n osType: OperatingSystemType.LINUX,\n userData,\n imageId: imageId.valueAsString,\n };\n },\n },\n // Do not use the default AWS security group which allows egress on any port.\n // Favour HTTPS only egress rules by default.\n securityGroup: GuHttpsEgressSecurityGroup.forVpc(scope, { app, vpc }),\n requireImdsv2: !withoutImdsv2,\n instanceMetadataTags: true,\n userData,\n role,\n httpPutResponseHopLimit,\n });\n\n // Add Wazuh & additional consumer specified Security Groups\n // Note: Launch templates via CDK allow specifying only one SG, so use connections\n // https://github.com/aws/aws-cdk/issues/18712\n [GuWazuhAccess.getInstance(scope, vpc), ...additionalSecurityGroups].forEach((sg) =>\n launchTemplate.connections.addSecurityGroup(sg),\n );\n\n const asgProps: AutoScalingGroupProps = {\n ...props,\n launchTemplate,\n maxCapacity: maximumInstances ?? minimumInstances * 2,\n minCapacity: minimumInstances,\n groupMetrics: groupMetrics,\n\n // Omit userData, instanceType, blockDevices & role from asgProps\n // As this are specified by the LaunchTemplate and must not be duplicated\n blockDevices: undefined,\n instanceType: undefined,\n role: undefined,\n userData: undefined,\n defaultInstanceWarmup,\n };\n\n super(scope, id, asgProps);\n\n this.app = app;\n this.amiParameter = imageId;\n this.imageRecipe = imageRecipe;\n\n targetGroup && this.attachToApplicationTargetGroup(targetGroup);\n\n const cfnAsg = this.node.defaultChild as CfnAutoScalingGroup;\n\n // A CDK AutoScalingGroup comes with this update policy, whereas the CFN autscaling group\n // leaves it to the default value, which is actually false.\n // { UpdatePolicy: { autoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true }}\n if (!updatePolicy) {\n cfnAsg.addDeletionOverride(\"UpdatePolicy\");\n }\n\n Tags.of(launchTemplate).add(\"App\", app);\n }\n}\n"]}
@@ -0,0 +1,43 @@
1
+ import type { GuStack } from "../../constructs/core";
2
+ import type { GuEc2AppProps } from "../../patterns";
3
+ import { GuEc2App } from "../../patterns";
4
+ export interface GuEc2AppExperimentalProps extends Omit<GuEc2AppProps, "updatePolicy"> {
5
+ }
6
+ /**
7
+ * An experimental pattern to instantiate an EC2 application that is updated entirely via CloudFormation.
8
+ * It sets the update policy of the `AWS::AutoScaling::AutoScalingGroup` to `AutoScalingRollingUpdate`.
9
+ * When a CloudFormation update is applied, the current instances in the ASG will be replaced.
10
+ *
11
+ * This pattern also updates the User Data, running some commands AFTER yours.
12
+ * These changes are wrapped in start and end marking comments.
13
+ *
14
+ * This pattern does not enable for scaling events to trigger _during_ a deployment
15
+ * as during testing it was found that scale-in events during deployment led to unpredictable service availability.
16
+ *
17
+ * If a deployment fails, for example when the healthcheck fails, CloudFormation will automatically rollback the changes.
18
+ * This improves on the behaviour of Riff-Raff's `autoscaling` deployment which requires manual intervention on deployment failure.
19
+ *
20
+ * To update the application's code, a CloudFormation update must be triggered.
21
+ * The best way to do this is to include the build number in the application artifact.
22
+ * TODO test User Data includes a build number.
23
+ *
24
+ * @remarks
25
+ * - The rate at which instances are replaced are conditional based on how scaled up the service is.
26
+ * The more an application is scaled, the longer it'll take to deploy.
27
+ * - It is not necessary to use the `autoscaling` Riff-Raff deployment type in conjunction with this.
28
+ * Doing so won't break anything, however it'll lengthen the deployment time as instances will be rotated twice.
29
+ * Please remove it from any manually created `riff-raff.yaml` file.
30
+ * - Your application should include a build number in its filename.
31
+ * This value will change across builds, and therefore create a CloudFormation template difference to be deployed.
32
+ * - Requires the AWS CLI and `cfn-signal` binaries to be available on the instance, and on the `PATH`.
33
+ * AMIgo adds these via the `aws-tools` role.
34
+ *
35
+ * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-rollingupdate
36
+ * @see https://github.com/guardian/testing-asg-rolling-update
37
+ *
38
+ * @experimental
39
+ */
40
+ export declare class GuEc2AppExperimental extends GuEc2App {
41
+ constructor(scope: GuStack, props: GuEc2AppExperimentalProps);
42
+ }
43
+ //# sourceMappingURL=ec2-app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ec2-app.d.ts","sourceRoot":"","sources":["../../../src/experimental/patterns/ec2-app.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAmN1C,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,aAAa,EAAE,cAAc,CAAC;CAAG;AAEzF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,oBAAqB,SAAQ,QAAQ;gBACpC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,yBAAyB;CAoF7D"}
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GuEc2AppExperimental = void 0;
4
+ const aws_cdk_lib_1 = require("aws-cdk-lib");
5
+ const aws_autoscaling_1 = require("aws-cdk-lib/aws-autoscaling");
6
+ const aws_iam_1 = require("aws-cdk-lib/aws-iam");
7
+ const autoscaling_1 = require("../../constructs/autoscaling");
8
+ const patterns_1 = require("../../patterns");
9
+ const singleton_1 = require("../../utils/singleton");
10
+ /**
11
+ * Ensures the `AutoScalingRollingUpdate` of an AutoScaling Group has a `PauseTime` matching the healthcheck grace period.
12
+ * It also ensures the `CreationPolicy` resource signal `Timeout` matches the healthcheck grace period.
13
+ *
14
+ * @privateRemarks
15
+ * The ASG healthcheck grace period is hard-coded by {@link GuEc2App}.
16
+ * Customisation of this value is performed via an escape hatch.
17
+ * An `Aspect` is the only way to observe any customisation.
18
+ *
19
+ * TODO Expose the healthcheck grace period as a property on {@link GuEc2App} and remove this `Aspect`.
20
+ */
21
+ class AutoScalingRollingUpdateTimeout {
22
+ constructor(scope) {
23
+ this.stack = scope;
24
+ }
25
+ static getInstance(stack) {
26
+ if (!this.instance || !(0, singleton_1.isSingletonPresentInStack)(stack, this.instance)) {
27
+ this.instance = new AutoScalingRollingUpdateTimeout(stack);
28
+ }
29
+ return this.instance;
30
+ }
31
+ visit(construct) {
32
+ if (construct instanceof aws_autoscaling_1.CfnAutoScalingGroup) {
33
+ const currentRollingUpdate = construct.cfnOptions.updatePolicy?.autoScalingRollingUpdate;
34
+ const currentCreationPolicy = construct.cfnOptions.creationPolicy;
35
+ /**
36
+ * The type of `healthCheckGracePeriod` is `number | undefined`.
37
+ * In reality, it will always be set as we set it in {@link GuEc2App}.
38
+ * The right-hand side to appease the compiler; 5 minutes is the default value used by AWS.
39
+ *
40
+ * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-rollingupdate-pausetime
41
+ */
42
+ const signalTimeoutSeconds = construct.healthCheckGracePeriod ?? aws_cdk_lib_1.Duration.minutes(5).toSeconds();
43
+ if (currentRollingUpdate) {
44
+ construct.cfnOptions.updatePolicy = {
45
+ autoScalingRollingUpdate: {
46
+ ...currentRollingUpdate,
47
+ pauseTime: aws_cdk_lib_1.Duration.seconds(signalTimeoutSeconds).toIsoString(),
48
+ },
49
+ };
50
+ }
51
+ if (currentCreationPolicy) {
52
+ construct.cfnOptions.creationPolicy = {
53
+ ...currentCreationPolicy,
54
+ resourceSignal: {
55
+ ...currentCreationPolicy.resourceSignal,
56
+ timeout: aws_cdk_lib_1.Duration.seconds(signalTimeoutSeconds).toIsoString(),
57
+ },
58
+ };
59
+ }
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * An `Aspect` that adjusts the properties of an AutoScaling Group using an `AutoScalingRollingUpdate` update policy.
65
+ *
66
+ * It'll unset the `DesiredCapacity` of the ASG as a rolling update sets `Desired` back to the template version,
67
+ * undoing any changes that a scale-out event may have done.
68
+ * Having `DesiredCapacity` unset ensures the service remains at-capacity at all times.
69
+ *
70
+ * It'll also make the `MinInstancesInService` property dynamic via a CFN Parameter that Riff-Raff will set.
71
+ * The value depends on the current capacity of the ASG:
72
+ * - If the service is running normally, it'll be set to the `MinSize` capacity
73
+ * - If the service is partially scaled, it'll be set to the current `DesiredCapacity`
74
+ * - If the service is fully scaled, it'll be set to (at least) `MaxSize` - 1
75
+ *
76
+ * @privateRemarks
77
+ * - Temporarily implemented as a singleton to ensure only one instance is added to a stack,
78
+ * else multiple attempts to add the same CFN Parameter will be made and fail.
79
+ * - Once out of experimental, instantiate this `Aspect` directly in {@link GuStack}.
80
+ *
81
+ * @see https://github.com/guardian/testing-asg-rolling-update
82
+ */
83
+ class HorizontallyScalingDeploymentProperties {
84
+ constructor(scope) {
85
+ this.stack = scope;
86
+ this.asgToParamMap = new Map();
87
+ }
88
+ static getInstance(stack) {
89
+ if (!this.instance || !(0, singleton_1.isSingletonPresentInStack)(stack, this.instance)) {
90
+ this.instance = new HorizontallyScalingDeploymentProperties(stack);
91
+ }
92
+ return this.instance;
93
+ }
94
+ visit(construct) {
95
+ if (construct instanceof aws_autoscaling_1.CfnScalingPolicy) {
96
+ const { node } = construct;
97
+ const { scopes, path } = node;
98
+ /*
99
+ Requiring a `CfnScalingPolicy` to be created in the scope of a `GuAutoScalingGroup`
100
+ is the most reliable way to associate the two together.
101
+
102
+ Though the `autoScalingGroupName` property is passed to a `CfnScalingPolicy` when instantiating,
103
+ this does not create a concrete link as AWS CDK sets the value to be the ASG's `Ref`.
104
+ That is, it is a `Token` until it is synthesised.
105
+ This is even if the ASG has an explicit name set.
106
+
107
+ See also:
108
+ - https://docs.aws.amazon.com/cdk/v2/guide/tokens.html
109
+ - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-autoscalinggroup.html#aws-resource-autoscaling-autoscalinggroup-return-values
110
+ - https://github.com/aws/aws-cdk/blob/f6b649d47f8bc30ca741fbb7a4852d51e8275002/packages/aws-cdk-lib/aws-autoscaling/lib/auto-scaling-group.ts#L1560
111
+ */
112
+ const autoScalingGroup = scopes.find((_) => _ instanceof autoscaling_1.GuAutoScalingGroup);
113
+ if (!autoScalingGroup) {
114
+ throw new Error(`Failed to detect the autoscaling group relating to the scaling policy on path ${path}. Was it created in the scope of a GuAutoScalingGroup?`);
115
+ }
116
+ const cfnAutoScalingGroup = autoScalingGroup.node.defaultChild;
117
+ const currentRollingUpdate = cfnAutoScalingGroup.cfnOptions.updatePolicy?.autoScalingRollingUpdate;
118
+ if (currentRollingUpdate) {
119
+ /*
120
+ An autoscaling group that horizontally scales should not explicitly set `Desired`,
121
+ as a rolling update will set the current `Desired` back to the template version,
122
+ undoing any changes that a scale-out event may have done.
123
+ */
124
+ cfnAutoScalingGroup.desiredCapacity = undefined;
125
+ const asgNodeId = autoScalingGroup.node.id;
126
+ if (!this.asgToParamMap.has(asgNodeId)) {
127
+ this.asgToParamMap.set(asgNodeId, new aws_cdk_lib_1.CfnParameter(this.stack, `MinInstancesInServiceFor${autoScalingGroup.app}`, {
128
+ type: "Number",
129
+ default: parseInt(cfnAutoScalingGroup.minSize),
130
+ maxValue: parseInt(cfnAutoScalingGroup.maxSize) - 1,
131
+ }));
132
+ }
133
+ const minInstancesInService = this.asgToParamMap.get(asgNodeId);
134
+ cfnAutoScalingGroup.cfnOptions.updatePolicy = {
135
+ autoScalingRollingUpdate: {
136
+ ...currentRollingUpdate,
137
+ minInstancesInService: minInstancesInService.valueAsNumber,
138
+ },
139
+ };
140
+ }
141
+ }
142
+ }
143
+ }
144
+ /**
145
+ * An IAM Policy allowing the sending of a CloudFormation signal.
146
+ *
147
+ * @privateRemarks
148
+ * Implemented as a singleton as the resources can only be tightened at most to the CloudFormation stack.
149
+ */
150
+ class AsgRollingUpdatePolicy extends aws_iam_1.Policy {
151
+ constructor(scope) {
152
+ const { stackId } = scope;
153
+ super(scope, "AsgRollingUpdatePolicy", {
154
+ statements: [
155
+ // Allow usage of command `cfn-signal`.
156
+ new aws_iam_1.PolicyStatement({
157
+ actions: ["cloudformation:SignalResource"],
158
+ effect: aws_iam_1.Effect.ALLOW,
159
+ resources: [stackId],
160
+ }),
161
+ /*
162
+ Allow usage of command `aws elbv2 describe-target-health`.
163
+ AWS Elastic Load Balancing does not support resource based policies, so the resource has to be `*` (any) here.
164
+ See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html
165
+ */
166
+ new aws_iam_1.PolicyStatement({
167
+ actions: ["elasticloadbalancing:DescribeTargetHealth"],
168
+ effect: aws_iam_1.Effect.ALLOW,
169
+ resources: ["*"],
170
+ }),
171
+ ],
172
+ });
173
+ }
174
+ static getInstance(stack) {
175
+ if (!this.instance || !(0, singleton_1.isSingletonPresentInStack)(stack, this.instance)) {
176
+ this.instance = new AsgRollingUpdatePolicy(stack);
177
+ }
178
+ return this.instance;
179
+ }
180
+ }
181
+ /**
182
+ * An experimental pattern to instantiate an EC2 application that is updated entirely via CloudFormation.
183
+ * It sets the update policy of the `AWS::AutoScaling::AutoScalingGroup` to `AutoScalingRollingUpdate`.
184
+ * When a CloudFormation update is applied, the current instances in the ASG will be replaced.
185
+ *
186
+ * This pattern also updates the User Data, running some commands AFTER yours.
187
+ * These changes are wrapped in start and end marking comments.
188
+ *
189
+ * This pattern does not enable for scaling events to trigger _during_ a deployment
190
+ * as during testing it was found that scale-in events during deployment led to unpredictable service availability.
191
+ *
192
+ * If a deployment fails, for example when the healthcheck fails, CloudFormation will automatically rollback the changes.
193
+ * This improves on the behaviour of Riff-Raff's `autoscaling` deployment which requires manual intervention on deployment failure.
194
+ *
195
+ * To update the application's code, a CloudFormation update must be triggered.
196
+ * The best way to do this is to include the build number in the application artifact.
197
+ * TODO test User Data includes a build number.
198
+ *
199
+ * @remarks
200
+ * - The rate at which instances are replaced are conditional based on how scaled up the service is.
201
+ * The more an application is scaled, the longer it'll take to deploy.
202
+ * - It is not necessary to use the `autoscaling` Riff-Raff deployment type in conjunction with this.
203
+ * Doing so won't break anything, however it'll lengthen the deployment time as instances will be rotated twice.
204
+ * Please remove it from any manually created `riff-raff.yaml` file.
205
+ * - Your application should include a build number in its filename.
206
+ * This value will change across builds, and therefore create a CloudFormation template difference to be deployed.
207
+ * - Requires the AWS CLI and `cfn-signal` binaries to be available on the instance, and on the `PATH`.
208
+ * AMIgo adds these via the `aws-tools` role.
209
+ *
210
+ * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-rollingupdate
211
+ * @see https://github.com/guardian/testing-asg-rolling-update
212
+ *
213
+ * @experimental
214
+ */
215
+ class GuEc2AppExperimental extends patterns_1.GuEc2App {
216
+ constructor(scope, props) {
217
+ const { minimumInstances, maximumInstances = minimumInstances * 2 } = props.scaling;
218
+ const { applicationPort } = props;
219
+ const { region, stackId } = scope;
220
+ super(scope, {
221
+ ...props,
222
+ updatePolicy: aws_autoscaling_1.UpdatePolicy.rollingUpdate({
223
+ maxBatchSize: maximumInstances,
224
+ minInstancesInService: minimumInstances,
225
+ minSuccessPercentage: 100,
226
+ waitOnResourceSignals: true,
227
+ /*
228
+ If a scale-in event fires during an `AutoScalingRollingUpdate` operation, the update could fail and rollback.
229
+ For this reason, we suspend the `AlarmNotification` process, else availability of a service cannot be guaranteed.
230
+ Consequently, services cannot scale-out during deployments.
231
+ If AWS ever supports suspending scale-out and scale-in independently, we should allow scale-out.
232
+ */
233
+ suspendProcesses: [aws_autoscaling_1.ScalingProcess.ALARM_NOTIFICATION],
234
+ }),
235
+ });
236
+ const { autoScalingGroup, targetGroup } = this;
237
+ const { userData, role } = autoScalingGroup;
238
+ const cfnAutoScalingGroup = autoScalingGroup.node.defaultChild;
239
+ cfnAutoScalingGroup.desiredCapacity = minimumInstances.toString();
240
+ cfnAutoScalingGroup.cfnOptions.creationPolicy = {
241
+ autoScalingCreationPolicy: {
242
+ minSuccessfulInstancesPercent: 100,
243
+ },
244
+ resourceSignal: {
245
+ count: minimumInstances,
246
+ },
247
+ };
248
+ AsgRollingUpdatePolicy.getInstance(scope).attachToRole(role);
249
+ /*
250
+ `aws` is available via AMIgo baked AMIs.
251
+ See https://github.com/guardian/amigo/tree/main/roles/aws-tools.
252
+ */
253
+ userData.addCommands(`# ${GuEc2AppExperimental.name} UserData Start`, `
254
+ TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
255
+ INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" "http://169.254.169.254/latest/meta-data/instance-id")
256
+
257
+ STATE=$(aws elbv2 describe-target-health \
258
+ --target-group-arn ${targetGroup.targetGroupArn} \
259
+ --region ${region} \
260
+ --targets Id=$INSTANCE_ID,Port=${applicationPort} \
261
+ --query "TargetHealthDescriptions[0].TargetHealth.State")
262
+
263
+ until [ "$STATE" == "\\"healthy\\"" ]; do
264
+ echo "Instance not yet healthy within target group. Current state $STATE. Sleeping..."
265
+ sleep 5
266
+ STATE=$(aws elbv2 describe-target-health \
267
+ --target-group-arn ${targetGroup.targetGroupArn} \
268
+ --region ${region} \
269
+ --targets Id=$INSTANCE_ID,Port=${applicationPort} \
270
+ --query "TargetHealthDescriptions[0].TargetHealth.State")
271
+ done
272
+
273
+ echo "Instance is healthy in target group."
274
+ `, `# ${GuEc2AppExperimental.name} UserData End`);
275
+ userData.addOnExitCommands(`
276
+ cfn-signal --stack ${stackId} \
277
+ --resource ${cfnAutoScalingGroup.logicalId} \
278
+ --region ${region} \
279
+ --exit-code $exitCode || echo 'Failed to send Cloudformation Signal'
280
+ `);
281
+ // TODO Once out of experimental, instantiate these `Aspect`s directly in `GuStack`.
282
+ aws_cdk_lib_1.Aspects.of(scope).add(AutoScalingRollingUpdateTimeout.getInstance(scope));
283
+ aws_cdk_lib_1.Aspects.of(scope).add(HorizontallyScalingDeploymentProperties.getInstance(scope));
284
+ }
285
+ }
286
+ exports.GuEc2AppExperimental = GuEc2AppExperimental;
287
+ //# sourceMappingURL=ec2-app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ec2-app.js","sourceRoot":"","sources":["../../../src/experimental/patterns/ec2-app.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IAspect } from \"aws-cdk-lib\";\nimport { Aspects, CfnParameter, Duration } from \"aws-cdk-lib\";\nimport { CfnAutoScalingGroup, CfnScalingPolicy, ScalingProcess, UpdatePolicy } from \"aws-cdk-lib/aws-autoscaling\";\nimport { Effect, Policy, PolicyStatement } from \"aws-cdk-lib/aws-iam\";\nimport type { IConstruct } from \"constructs\";\nimport { GuAutoScalingGroup } from \"../../constructs/autoscaling\";\nimport type { GuStack } from \"../../constructs/core\";\nimport type { GuEc2AppProps } from \"../../patterns\";\nimport { GuEc2App } from \"../../patterns\";\nimport { isSingletonPresentInStack } from \"../../utils/singleton\";\n\n/**\n * Ensures the `AutoScalingRollingUpdate` of an AutoScaling Group has a `PauseTime` matching the healthcheck grace period.\n * It also ensures the `CreationPolicy` resource signal `Timeout` matches the healthcheck grace period.\n *\n * @privateRemarks\n * The ASG healthcheck grace period is hard-coded by {@link GuEc2App}.\n * Customisation of this value is performed via an escape hatch.\n * An `Aspect` is the only way to observe any customisation.\n *\n * TODO Expose the healthcheck grace period as a property on {@link GuEc2App} and remove this `Aspect`.\n */\nclass AutoScalingRollingUpdateTimeout implements IAspect {\n public readonly stack: GuStack;\n private static instance: AutoScalingRollingUpdateTimeout | undefined;\n\n private constructor(scope: GuStack) {\n this.stack = scope;\n }\n\n public static getInstance(stack: GuStack): AutoScalingRollingUpdateTimeout {\n if (!this.instance || !isSingletonPresentInStack(stack, this.instance)) {\n this.instance = new AutoScalingRollingUpdateTimeout(stack);\n }\n return this.instance;\n }\n\n public visit(construct: IConstruct) {\n if (construct instanceof CfnAutoScalingGroup) {\n const currentRollingUpdate = construct.cfnOptions.updatePolicy?.autoScalingRollingUpdate;\n const currentCreationPolicy = construct.cfnOptions.creationPolicy;\n\n /**\n * The type of `healthCheckGracePeriod` is `number | undefined`.\n * In reality, it will always be set as we set it in {@link GuEc2App}.\n * The right-hand side to appease the compiler; 5 minutes is the default value used by AWS.\n *\n * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-rollingupdate-pausetime\n */\n const signalTimeoutSeconds = construct.healthCheckGracePeriod ?? Duration.minutes(5).toSeconds();\n\n if (currentRollingUpdate) {\n construct.cfnOptions.updatePolicy = {\n autoScalingRollingUpdate: {\n ...currentRollingUpdate,\n pauseTime: Duration.seconds(signalTimeoutSeconds).toIsoString(),\n },\n };\n }\n\n if (currentCreationPolicy) {\n construct.cfnOptions.creationPolicy = {\n ...currentCreationPolicy,\n resourceSignal: {\n ...currentCreationPolicy.resourceSignal,\n timeout: Duration.seconds(signalTimeoutSeconds).toIsoString(),\n },\n };\n }\n }\n }\n}\n\n/**\n * An `Aspect` that adjusts the properties of an AutoScaling Group using an `AutoScalingRollingUpdate` update policy.\n *\n * It'll unset the `DesiredCapacity` of the ASG as a rolling update sets `Desired` back to the template version,\n * undoing any changes that a scale-out event may have done.\n * Having `DesiredCapacity` unset ensures the service remains at-capacity at all times.\n *\n * It'll also make the `MinInstancesInService` property dynamic via a CFN Parameter that Riff-Raff will set.\n * The value depends on the current capacity of the ASG:\n * - If the service is running normally, it'll be set to the `MinSize` capacity\n * - If the service is partially scaled, it'll be set to the current `DesiredCapacity`\n * - If the service is fully scaled, it'll be set to (at least) `MaxSize` - 1\n *\n * @privateRemarks\n * - Temporarily implemented as a singleton to ensure only one instance is added to a stack,\n * else multiple attempts to add the same CFN Parameter will be made and fail.\n * - Once out of experimental, instantiate this `Aspect` directly in {@link GuStack}.\n *\n * @see https://github.com/guardian/testing-asg-rolling-update\n */\nclass HorizontallyScalingDeploymentProperties implements IAspect {\n public readonly stack: GuStack;\n private readonly asgToParamMap: Map<string, CfnParameter>;\n private static instance: HorizontallyScalingDeploymentProperties | undefined;\n\n private constructor(scope: GuStack) {\n this.stack = scope;\n this.asgToParamMap = new Map();\n }\n\n public static getInstance(stack: GuStack): HorizontallyScalingDeploymentProperties {\n if (!this.instance || !isSingletonPresentInStack(stack, this.instance)) {\n this.instance = new HorizontallyScalingDeploymentProperties(stack);\n }\n\n return this.instance;\n }\n\n public visit(construct: IConstruct) {\n if (construct instanceof CfnScalingPolicy) {\n const { node } = construct;\n const { scopes, path } = node;\n\n /*\n Requiring a `CfnScalingPolicy` to be created in the scope of a `GuAutoScalingGroup`\n is the most reliable way to associate the two together.\n\n Though the `autoScalingGroupName` property is passed to a `CfnScalingPolicy` when instantiating,\n this does not create a concrete link as AWS CDK sets the value to be the ASG's `Ref`.\n That is, it is a `Token` until it is synthesised.\n This is even if the ASG has an explicit name set.\n\n See also:\n - https://docs.aws.amazon.com/cdk/v2/guide/tokens.html\n - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-autoscalinggroup.html#aws-resource-autoscaling-autoscalinggroup-return-values\n - https://github.com/aws/aws-cdk/blob/f6b649d47f8bc30ca741fbb7a4852d51e8275002/packages/aws-cdk-lib/aws-autoscaling/lib/auto-scaling-group.ts#L1560\n */\n const autoScalingGroup = scopes.find((_): _ is GuAutoScalingGroup => _ instanceof GuAutoScalingGroup);\n\n if (!autoScalingGroup) {\n throw new Error(\n `Failed to detect the autoscaling group relating to the scaling policy on path ${path}. Was it created in the scope of a GuAutoScalingGroup?`,\n );\n }\n\n const cfnAutoScalingGroup = autoScalingGroup.node.defaultChild as CfnAutoScalingGroup;\n const currentRollingUpdate = cfnAutoScalingGroup.cfnOptions.updatePolicy?.autoScalingRollingUpdate;\n\n if (currentRollingUpdate) {\n /*\n An autoscaling group that horizontally scales should not explicitly set `Desired`,\n as a rolling update will set the current `Desired` back to the template version,\n undoing any changes that a scale-out event may have done.\n */\n cfnAutoScalingGroup.desiredCapacity = undefined;\n\n const asgNodeId = autoScalingGroup.node.id;\n\n if (!this.asgToParamMap.has(asgNodeId)) {\n this.asgToParamMap.set(\n asgNodeId,\n new CfnParameter(this.stack, `MinInstancesInServiceFor${autoScalingGroup.app}`, {\n type: \"Number\",\n default: parseInt(cfnAutoScalingGroup.minSize),\n maxValue: parseInt(cfnAutoScalingGroup.maxSize) - 1,\n }),\n );\n }\n\n const minInstancesInService = this.asgToParamMap.get(asgNodeId)!;\n\n cfnAutoScalingGroup.cfnOptions.updatePolicy = {\n autoScalingRollingUpdate: {\n ...currentRollingUpdate,\n minInstancesInService: minInstancesInService.valueAsNumber,\n },\n };\n }\n }\n }\n}\n\n/**\n * An IAM Policy allowing the sending of a CloudFormation signal.\n *\n * @privateRemarks\n * Implemented as a singleton as the resources can only be tightened at most to the CloudFormation stack.\n */\nclass AsgRollingUpdatePolicy extends Policy {\n private static instance: AsgRollingUpdatePolicy | undefined;\n\n private constructor(scope: GuStack) {\n const { stackId } = scope;\n\n super(scope, \"AsgRollingUpdatePolicy\", {\n statements: [\n // Allow usage of command `cfn-signal`.\n new PolicyStatement({\n actions: [\"cloudformation:SignalResource\"],\n effect: Effect.ALLOW,\n resources: [stackId],\n }),\n\n /*\n Allow usage of command `aws elbv2 describe-target-health`.\n AWS Elastic Load Balancing does not support resource based policies, so the resource has to be `*` (any) here.\n See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html\n */\n new PolicyStatement({\n actions: [\"elasticloadbalancing:DescribeTargetHealth\"],\n effect: Effect.ALLOW,\n resources: [\"*\"],\n }),\n ],\n });\n }\n\n public static getInstance(stack: GuStack): AsgRollingUpdatePolicy {\n if (!this.instance || !isSingletonPresentInStack(stack, this.instance)) {\n this.instance = new AsgRollingUpdatePolicy(stack);\n }\n\n return this.instance;\n }\n}\n\nexport interface GuEc2AppExperimentalProps extends Omit<GuEc2AppProps, \"updatePolicy\"> {}\n\n/**\n * An experimental pattern to instantiate an EC2 application that is updated entirely via CloudFormation.\n * It sets the update policy of the `AWS::AutoScaling::AutoScalingGroup` to `AutoScalingRollingUpdate`.\n * When a CloudFormation update is applied, the current instances in the ASG will be replaced.\n *\n * This pattern also updates the User Data, running some commands AFTER yours.\n * These changes are wrapped in start and end marking comments.\n *\n * This pattern does not enable for scaling events to trigger _during_ a deployment\n * as during testing it was found that scale-in events during deployment led to unpredictable service availability.\n *\n * If a deployment fails, for example when the healthcheck fails, CloudFormation will automatically rollback the changes.\n * This improves on the behaviour of Riff-Raff's `autoscaling` deployment which requires manual intervention on deployment failure.\n *\n * To update the application's code, a CloudFormation update must be triggered.\n * The best way to do this is to include the build number in the application artifact.\n * TODO test User Data includes a build number.\n *\n * @remarks\n * - The rate at which instances are replaced are conditional based on how scaled up the service is.\n * The more an application is scaled, the longer it'll take to deploy.\n * - It is not necessary to use the `autoscaling` Riff-Raff deployment type in conjunction with this.\n * Doing so won't break anything, however it'll lengthen the deployment time as instances will be rotated twice.\n * Please remove it from any manually created `riff-raff.yaml` file.\n * - Your application should include a build number in its filename.\n * This value will change across builds, and therefore create a CloudFormation template difference to be deployed.\n * - Requires the AWS CLI and `cfn-signal` binaries to be available on the instance, and on the `PATH`.\n * AMIgo adds these via the `aws-tools` role.\n *\n * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-rollingupdate\n * @see https://github.com/guardian/testing-asg-rolling-update\n *\n * @experimental\n */\nexport class GuEc2AppExperimental extends GuEc2App {\n constructor(scope: GuStack, props: GuEc2AppExperimentalProps) {\n const { minimumInstances, maximumInstances = minimumInstances * 2 } = props.scaling;\n const { applicationPort } = props;\n const { region, stackId } = scope;\n\n super(scope, {\n ...props,\n updatePolicy: UpdatePolicy.rollingUpdate({\n maxBatchSize: maximumInstances,\n minInstancesInService: minimumInstances,\n minSuccessPercentage: 100,\n waitOnResourceSignals: true,\n\n /*\n If a scale-in event fires during an `AutoScalingRollingUpdate` operation, the update could fail and rollback.\n For this reason, we suspend the `AlarmNotification` process, else availability of a service cannot be guaranteed.\n Consequently, services cannot scale-out during deployments.\n If AWS ever supports suspending scale-out and scale-in independently, we should allow scale-out.\n */\n suspendProcesses: [ScalingProcess.ALARM_NOTIFICATION],\n }),\n });\n\n const { autoScalingGroup, targetGroup } = this;\n const { userData, role } = autoScalingGroup;\n const cfnAutoScalingGroup = autoScalingGroup.node.defaultChild as CfnAutoScalingGroup;\n\n cfnAutoScalingGroup.desiredCapacity = minimumInstances.toString();\n\n cfnAutoScalingGroup.cfnOptions.creationPolicy = {\n autoScalingCreationPolicy: {\n minSuccessfulInstancesPercent: 100,\n },\n resourceSignal: {\n count: minimumInstances,\n },\n };\n\n AsgRollingUpdatePolicy.getInstance(scope).attachToRole(role);\n\n /*\n `aws` is available via AMIgo baked AMIs.\n See https://github.com/guardian/amigo/tree/main/roles/aws-tools.\n */\n userData.addCommands(\n `# ${GuEc2AppExperimental.name} UserData Start`,\n `\n TOKEN=$(curl -X PUT \"http://169.254.169.254/latest/api/token\" -H \"X-aws-ec2-metadata-token-ttl-seconds: 21600\")\n INSTANCE_ID=$(curl -H \"X-aws-ec2-metadata-token: $TOKEN\" \"http://169.254.169.254/latest/meta-data/instance-id\")\n\n STATE=$(aws elbv2 describe-target-health \\\n --target-group-arn ${targetGroup.targetGroupArn} \\\n --region ${region} \\\n --targets Id=$INSTANCE_ID,Port=${applicationPort} \\\n --query \"TargetHealthDescriptions[0].TargetHealth.State\")\n\n until [ \"$STATE\" == \"\\\\\"healthy\\\\\"\" ]; do\n echo \"Instance not yet healthy within target group. Current state $STATE. Sleeping...\"\n sleep 5\n STATE=$(aws elbv2 describe-target-health \\\n --target-group-arn ${targetGroup.targetGroupArn} \\\n --region ${region} \\\n --targets Id=$INSTANCE_ID,Port=${applicationPort} \\\n --query \"TargetHealthDescriptions[0].TargetHealth.State\")\n done\n\n echo \"Instance is healthy in target group.\"\n `,\n `# ${GuEc2AppExperimental.name} UserData End`,\n );\n\n userData.addOnExitCommands(\n `\n cfn-signal --stack ${stackId} \\\n --resource ${cfnAutoScalingGroup.logicalId} \\\n --region ${region} \\\n --exit-code $exitCode || echo 'Failed to send Cloudformation Signal'\n `,\n );\n\n // TODO Once out of experimental, instantiate these `Aspect`s directly in `GuStack`.\n Aspects.of(scope).add(AutoScalingRollingUpdateTimeout.getInstance(scope));\n Aspects.of(scope).add(HorizontallyScalingDeploymentProperties.getInstance(scope));\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/riff-raff-yaml-file/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAUvC,OAAO,KAAK,EAMV,YAAY,EAGb,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAY;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAa;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAa;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAW;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;;;;;;;;;OAUG;IACH,SAAgB,YAAY,EAAE,YAAY,CAAC;IAE3C,OAAO,CAAC,iBAAiB;IASzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqDG;IACH,OAAO,CAAC,mBAAmB;IAiC3B,OAAO,CAAC,6BAA6B;IAQrC,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,sBAAsB;gBAKlB,GAAG,EAAE,GAAG;IAuEpB;;;OAGG;IACH,MAAM,IAAI,MAAM;IAiBhB;;;OAGG;IACH,KAAK,IAAI,IAAI;CAId"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/riff-raff-yaml-file/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAWvC,OAAO,KAAK,EAMV,YAAY,EAGb,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAY;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAa;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAa;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAW;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;;;;;;;;;OAUG;IACH,SAAgB,YAAY,EAAE,YAAY,CAAC;IAE3C,OAAO,CAAC,iBAAiB;IASzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqDG;IACH,OAAO,CAAC,mBAAmB;IAiC3B,OAAO,CAAC,6BAA6B;IAQrC,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,sBAAsB;gBAKlB,GAAG,EAAE,GAAG;IAiFpB;;;OAGG;IACH,MAAM,IAAI,MAAM;IAiBhB;;;OAGG;IACH,KAAK,IAAI,IAAI;CAId"}
@@ -196,7 +196,16 @@ class RiffRaffYamlFile {
196
196
  const lambdaDeployment = (0, lambda_2.updateLambdaDeployment)(lambda, cfnDeployment);
197
197
  deployments.set(lambdaDeployment.name, lambdaDeployment.props);
198
198
  });
199
- autoscalingGroups.forEach((asg) => {
199
+ /*
200
+ Instances in an ASG with an `AutoScalingRollingUpdate` update policy are rotated via CloudFormation.
201
+ Therefore, they do not need to also perform an `autoscaling` deployment via Riff-Raff.
202
+ */
203
+ const legacyAutoscalingGroups = autoscalingGroups.filter((asg) => {
204
+ const { cfnOptions } = asg.node.defaultChild;
205
+ const { updatePolicy } = cfnOptions;
206
+ return updatePolicy?.autoScalingRollingUpdate === undefined;
207
+ });
208
+ legacyAutoscalingGroups.forEach((asg) => {
200
209
  const asgDeployment = (0, autoscaling_2.autoscalingDeployment)(asg, cfnDeployment);
201
210
  deployments.set(asgDeployment.name, asgDeployment.props);
202
211
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/riff-raff-yaml-file/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { writeFileSync } from \"fs\";\nimport path from \"path\";\nimport type { App } from \"aws-cdk-lib\";\nimport { Token } from \"aws-cdk-lib\";\nimport { dump } from \"js-yaml\";\nimport { GuAutoScalingGroup } from \"../constructs/autoscaling\";\nimport { GuStack } from \"../constructs/core\";\nimport { GuLambdaFunction } from \"../constructs/lambda\";\nimport { autoscalingDeployment, uploadAutoscalingArtifact } from \"./deployments/autoscaling\";\nimport { addAmiParametersToCloudFormationDeployment, cloudFormationDeployment } from \"./deployments/cloudformation\";\nimport { updateLambdaDeployment, uploadLambdaArtifact } from \"./deployments/lambda\";\nimport { groupByClassNameStackRegionStage } from \"./group-by\";\nimport type {\n GroupedCdkStacks,\n Region,\n RiffRaffDeployment,\n RiffRaffDeploymentName,\n RiffRaffDeploymentProps,\n RiffRaffYaml,\n StackTag,\n StageTag,\n} from \"./types\";\n\n/**\n * A class that creates a `riff-raff.yaml` file.\n *\n * Rather than using this directly, prefer to use [[`GuRoot`]] instead.\n *\n * Supports:\n * - Multiple CloudFormation stacks\n * - Multiple regions\n * - Lambda applications\n * - EC2 (autoscaling) applications\n *\n * For lambda applications, 3 deployments will be defined:\n * 1. Lambda upload (`aws-lambda`, `action: [uploadLambda]`)\n * 2. CloudFormation deploy (`cloud-formation`)\n * 3. Lambda update (`aws-lambda`, `action: [updateLambda]`)\n *\n * For EC2 applications, 3 deployments will be defined:\n * 1. Artifact upload (`autoscaling`, `action: [uploadArtifacts]`)\n * 2. CloudFormation deploy (`cloud-formation`)\n * 3. Autoscaling deploy (`autoscaling`, `action: [deploy]`)\n *\n * It assumes a Riff-Raff bundle structure as follows:\n *\n * ```\n * .\n * ├── cdk.out\n * │ └── MyApplication.template.json\n * ├── my-application\n * │ └── my-application.deb\n * └── my-lambda\n * └── my-lambda.zip\n * ```\n *\n * That is, all CloudFormation templates are in a `cdk.out` directory, and there is a directory per app.\n * Application artifact(s) are in the app directory.\n *\n * NOTE: The file extension is decided by you, the above file tree is used for illustration purposes.\n *\n * NOTE: Resources will be looked up by tags (Stack, Stage, App). Ensure your CFN stack is tagged appropriately!\n *\n * @see https://riffraff.gutools.co.uk/docs/reference/riff-raff.yaml.md\n * @see https://riffraff.gutools.co.uk/docs/magenta-lib/types\n */\nexport class RiffRaffYamlFile {\n private readonly allCdkStacks: GuStack[];\n private readonly allStackTags: StackTag[];\n private readonly allStageTags: StageTag[];\n private readonly allRegions: Region[];\n private readonly outdir: string;\n\n /**\n * The `riff-raff.yaml` file as an object.\n *\n * It is useful for specifying additional deployment types that GuCDK does not support.\n * Consider raising an issue or pull request if you think it should be supported.\n *\n * In most cases, you shouldn't need to access this.\n * No validation is performed on the parameters you provide, so you might get deployment errors.\n *\n * @see https://riffraff.gutools.co.uk/docs/magenta-lib/types\n */\n public readonly riffRaffYaml: RiffRaffYaml;\n\n private isCdkStackPresent(expectedStack: StackTag, expectedStage: StageTag): boolean {\n const matches = this.allCdkStacks.find((cdkStack) => {\n const { stack, stage } = cdkStack;\n return stack === expectedStack && stage === expectedStage;\n });\n\n return !!matches;\n }\n\n /**\n * Check there are the appropriate number of `GuStack`s.\n * Expect to find an instance for each combination of `stack`, and `stage`.\n *\n * If not valid, a message is logged describing what is missing to aid debugging.\n *\n * Given the following:\n *\n * ```ts\n * const app = new App();\n *\n * class MyApplicationStack extends GuStack { }\n *\n * new MyApplicationStack(app, \"App-CODE-deploy\", {\n * env: {\n * region: \"eu-west-1\",\n * },\n * stack: \"deploy\",\n * stage: \"CODE\"\n * });\n *\n * new MyApplicationStack(app, \"App-PROD-media-service\", {\n * env: {\n * region: \"eu-west-1\",\n * },\n * stack: \"media-service\",\n * stage: \"PROD\",\n * });\n *\n * new MyApplicationStack(app, \"App-PROD-deploy\", {\n * env: {\n * region: \"eu-west-1\",\n * },\n * stack: \"deploy\",\n * stage: \"PROD\"\n * });\n * ```\n *\n * This will log a message like this, where ❌ denotes something missing,\n * specifically there is no `CODE` template for `media-service`.\n *\n * ```log\n * Unable to produce a working riff-raff.yaml file; missing 1 definitions (details below)\n *\n * ┌───────────────┬──────┬──────┐\n * │ (index) │ CODE │ PROD │\n * ├───────────────┼──────┼──────┤\n * │ deploy │ '✅' │ '✅' │\n * │ media-service │ '❌' │ '✅' │\n * └───────────────┴──────┴──────┘\n * ```\n *\n * @private\n */\n private validateStacksInApp(): void {\n type Found = \"✅\";\n type NotFound = \"❌\";\n type AppValidation = Record<StackTag, Record<StageTag, Found | NotFound>>;\n\n const { allStackTags, allStageTags } = this;\n\n const checks: AppValidation = allStackTags.reduce((accStackTag, stackTag) => {\n return {\n ...accStackTag,\n [stackTag]: allStageTags.reduce((accStageTag, stageTag) => {\n return {\n ...accStageTag,\n [stageTag]: this.isCdkStackPresent(stackTag, stageTag) ? \"✅\" : \"❌\",\n };\n }, {}),\n };\n }, {});\n\n const missingDefinitions: Array<Found | NotFound> = Object.values(checks).flatMap((groupedByStackTag) => {\n return Object.values(groupedByStackTag).filter((_) => _ === \"❌\");\n });\n\n if (missingDefinitions.length > 0) {\n const message = `Unable to produce a working riff-raff.yaml file; missing ${missingDefinitions.length} definitions`;\n\n console.log(`${message} (details below)`);\n console.table(checks);\n\n throw new Error(message);\n }\n }\n\n private validateAllRegionsAreResolved(): void {\n const unresolved = this.allRegions.filter((region) => Token.isUnresolved(region));\n\n if (unresolved.length !== 0) {\n throw new Error(`Unable to produce a working riff-raff.yaml file; all stacks must have an explicit region set`);\n }\n }\n\n private getLambdas(cdkStack: GuStack): GuLambdaFunction[] {\n return cdkStack.node.findAll().filter((_) => _ instanceof GuLambdaFunction) as GuLambdaFunction[];\n }\n\n private getAutoScalingGroups(cdkStack: GuStack): GuAutoScalingGroup[] {\n return cdkStack.node.findAll().filter((_) => _ instanceof GuAutoScalingGroup) as GuAutoScalingGroup[];\n }\n\n private getGuStackDependencies(cdkStack: GuStack): GuStack[] {\n return cdkStack.dependencies.filter((_) => _ instanceof GuStack) as GuStack[];\n }\n\n // eslint-disable-next-line custom-rules/valid-constructors -- this needs to sit above GuStack on the cdk tree\n constructor(app: App) {\n this.allCdkStacks = app.node.findAll().filter((_) => _ instanceof GuStack) as GuStack[];\n const allowedStages = new Set(this.allCdkStacks.map(({ stage }) => stage));\n this.allStageTags = Array.from(allowedStages);\n this.allStackTags = Array.from(new Set(this.allCdkStacks.map(({ stack }) => stack)));\n this.allRegions = Array.from(new Set(this.allCdkStacks.map(({ region }) => region)));\n\n this.validateStacksInApp();\n this.validateAllRegionsAreResolved();\n\n this.outdir = app.outdir;\n\n const deployments = new Map<RiffRaffDeploymentName, RiffRaffDeploymentProps>();\n\n const groupedStacks: GroupedCdkStacks = groupByClassNameStackRegionStage(this.allCdkStacks);\n\n Object.values(groupedStacks).forEach((stackTagGroup) => {\n Object.values(stackTagGroup).forEach((regionGroup) => {\n Object.values(regionGroup).forEach((stageGroup) => {\n const stacks: GuStack[] = Object.values(stageGroup).flat();\n\n // The items in `stacks` only differ by stage, so we can just use the first item in the list.\n const [stack] = stacks;\n\n if (!stack) {\n throw new Error(\"Unable to produce a working riff-raff.yaml file; there are no stacks!\");\n }\n\n const lambdas = this.getLambdas(stack);\n const autoscalingGroups = this.getAutoScalingGroups(stack);\n\n const artifactUploads: RiffRaffDeployment[] = [\n lambdas.filter((_) => !_.withoutArtifactUpload).map(uploadLambdaArtifact),\n autoscalingGroups.map(uploadAutoscalingArtifact),\n ].flat();\n artifactUploads.forEach(({ name, props }) => deployments.set(name, props));\n\n const parentStacks: RiffRaffDeployment[] = this.getGuStackDependencies(stack).map((x) =>\n cloudFormationDeployment([x], [], this.outdir),\n );\n\n const cfnDeployment = cloudFormationDeployment(stacks, [...artifactUploads, ...parentStacks], this.outdir);\n deployments.set(cfnDeployment.name, cfnDeployment.props);\n\n const lambdasWithoutAnAlias = lambdas.filter((lambda) => lambda.alias === undefined);\n // If the Lambda has an alias it is using versioning. When using versioning, there is no need for Riff-Raff\n // to modify the unpublished version of the function\n lambdasWithoutAnAlias.forEach((lambda) => {\n const lambdaDeployment = updateLambdaDeployment(lambda, cfnDeployment);\n deployments.set(lambdaDeployment.name, lambdaDeployment.props);\n });\n\n autoscalingGroups.forEach((asg) => {\n const asgDeployment = autoscalingDeployment(asg, cfnDeployment);\n deployments.set(asgDeployment.name, asgDeployment.props);\n });\n\n deployments.set(\n cfnDeployment.name,\n addAmiParametersToCloudFormationDeployment(cfnDeployment, autoscalingGroups),\n );\n });\n });\n });\n\n this.riffRaffYaml = {\n allowedStages,\n deployments,\n };\n }\n\n /**\n * The `riff-raff.yaml` file as a string.\n * Useful for testing.\n */\n toYAML(): string {\n // Add support for ES6 Set and Map. See https://github.com/nodeca/js-yaml/issues/436.\n const replacer = (_key: string, value: unknown) => {\n if (value instanceof Set) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- this is how `js-yaml` is typed\n return Array.from(value);\n }\n if (value instanceof Map) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- this is how `js-yaml` is typed\n return Object.fromEntries(value);\n }\n return value;\n };\n\n return dump(this.riffRaffYaml, { replacer });\n }\n\n /**\n * Write the `riff-raff.yaml` file to disk.\n * It'll be located with the CFN JSON templates generated by `cdk synth`.\n */\n synth(): void {\n const outPath = path.join(this.outdir, \"riff-raff.yaml\");\n writeFileSync(outPath, this.toYAML());\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/riff-raff-yaml-file/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { writeFileSync } from \"fs\";\nimport path from \"path\";\nimport type { App } from \"aws-cdk-lib\";\nimport { Token } from \"aws-cdk-lib\";\nimport type { CfnAutoScalingGroup } from \"aws-cdk-lib/aws-autoscaling\";\nimport { dump } from \"js-yaml\";\nimport { GuAutoScalingGroup } from \"../constructs/autoscaling\";\nimport { GuStack } from \"../constructs/core\";\nimport { GuLambdaFunction } from \"../constructs/lambda\";\nimport { autoscalingDeployment, uploadAutoscalingArtifact } from \"./deployments/autoscaling\";\nimport { addAmiParametersToCloudFormationDeployment, cloudFormationDeployment } from \"./deployments/cloudformation\";\nimport { updateLambdaDeployment, uploadLambdaArtifact } from \"./deployments/lambda\";\nimport { groupByClassNameStackRegionStage } from \"./group-by\";\nimport type {\n GroupedCdkStacks,\n Region,\n RiffRaffDeployment,\n RiffRaffDeploymentName,\n RiffRaffDeploymentProps,\n RiffRaffYaml,\n StackTag,\n StageTag,\n} from \"./types\";\n\n/**\n * A class that creates a `riff-raff.yaml` file.\n *\n * Rather than using this directly, prefer to use [[`GuRoot`]] instead.\n *\n * Supports:\n * - Multiple CloudFormation stacks\n * - Multiple regions\n * - Lambda applications\n * - EC2 (autoscaling) applications\n *\n * For lambda applications, 3 deployments will be defined:\n * 1. Lambda upload (`aws-lambda`, `action: [uploadLambda]`)\n * 2. CloudFormation deploy (`cloud-formation`)\n * 3. Lambda update (`aws-lambda`, `action: [updateLambda]`)\n *\n * For EC2 applications, 3 deployments will be defined:\n * 1. Artifact upload (`autoscaling`, `action: [uploadArtifacts]`)\n * 2. CloudFormation deploy (`cloud-formation`)\n * 3. Autoscaling deploy (`autoscaling`, `action: [deploy]`)\n *\n * It assumes a Riff-Raff bundle structure as follows:\n *\n * ```\n * .\n * ├── cdk.out\n * │ └── MyApplication.template.json\n * ├── my-application\n * │ └── my-application.deb\n * └── my-lambda\n * └── my-lambda.zip\n * ```\n *\n * That is, all CloudFormation templates are in a `cdk.out` directory, and there is a directory per app.\n * Application artifact(s) are in the app directory.\n *\n * NOTE: The file extension is decided by you, the above file tree is used for illustration purposes.\n *\n * NOTE: Resources will be looked up by tags (Stack, Stage, App). Ensure your CFN stack is tagged appropriately!\n *\n * @see https://riffraff.gutools.co.uk/docs/reference/riff-raff.yaml.md\n * @see https://riffraff.gutools.co.uk/docs/magenta-lib/types\n */\nexport class RiffRaffYamlFile {\n private readonly allCdkStacks: GuStack[];\n private readonly allStackTags: StackTag[];\n private readonly allStageTags: StageTag[];\n private readonly allRegions: Region[];\n private readonly outdir: string;\n\n /**\n * The `riff-raff.yaml` file as an object.\n *\n * It is useful for specifying additional deployment types that GuCDK does not support.\n * Consider raising an issue or pull request if you think it should be supported.\n *\n * In most cases, you shouldn't need to access this.\n * No validation is performed on the parameters you provide, so you might get deployment errors.\n *\n * @see https://riffraff.gutools.co.uk/docs/magenta-lib/types\n */\n public readonly riffRaffYaml: RiffRaffYaml;\n\n private isCdkStackPresent(expectedStack: StackTag, expectedStage: StageTag): boolean {\n const matches = this.allCdkStacks.find((cdkStack) => {\n const { stack, stage } = cdkStack;\n return stack === expectedStack && stage === expectedStage;\n });\n\n return !!matches;\n }\n\n /**\n * Check there are the appropriate number of `GuStack`s.\n * Expect to find an instance for each combination of `stack`, and `stage`.\n *\n * If not valid, a message is logged describing what is missing to aid debugging.\n *\n * Given the following:\n *\n * ```ts\n * const app = new App();\n *\n * class MyApplicationStack extends GuStack { }\n *\n * new MyApplicationStack(app, \"App-CODE-deploy\", {\n * env: {\n * region: \"eu-west-1\",\n * },\n * stack: \"deploy\",\n * stage: \"CODE\"\n * });\n *\n * new MyApplicationStack(app, \"App-PROD-media-service\", {\n * env: {\n * region: \"eu-west-1\",\n * },\n * stack: \"media-service\",\n * stage: \"PROD\",\n * });\n *\n * new MyApplicationStack(app, \"App-PROD-deploy\", {\n * env: {\n * region: \"eu-west-1\",\n * },\n * stack: \"deploy\",\n * stage: \"PROD\"\n * });\n * ```\n *\n * This will log a message like this, where ❌ denotes something missing,\n * specifically there is no `CODE` template for `media-service`.\n *\n * ```log\n * Unable to produce a working riff-raff.yaml file; missing 1 definitions (details below)\n *\n * ┌───────────────┬──────┬──────┐\n * │ (index) │ CODE │ PROD │\n * ├───────────────┼──────┼──────┤\n * │ deploy │ '✅' │ '✅' │\n * │ media-service │ '❌' │ '✅' │\n * └───────────────┴──────┴──────┘\n * ```\n *\n * @private\n */\n private validateStacksInApp(): void {\n type Found = \"✅\";\n type NotFound = \"❌\";\n type AppValidation = Record<StackTag, Record<StageTag, Found | NotFound>>;\n\n const { allStackTags, allStageTags } = this;\n\n const checks: AppValidation = allStackTags.reduce((accStackTag, stackTag) => {\n return {\n ...accStackTag,\n [stackTag]: allStageTags.reduce((accStageTag, stageTag) => {\n return {\n ...accStageTag,\n [stageTag]: this.isCdkStackPresent(stackTag, stageTag) ? \"✅\" : \"❌\",\n };\n }, {}),\n };\n }, {});\n\n const missingDefinitions: Array<Found | NotFound> = Object.values(checks).flatMap((groupedByStackTag) => {\n return Object.values(groupedByStackTag).filter((_) => _ === \"❌\");\n });\n\n if (missingDefinitions.length > 0) {\n const message = `Unable to produce a working riff-raff.yaml file; missing ${missingDefinitions.length} definitions`;\n\n console.log(`${message} (details below)`);\n console.table(checks);\n\n throw new Error(message);\n }\n }\n\n private validateAllRegionsAreResolved(): void {\n const unresolved = this.allRegions.filter((region) => Token.isUnresolved(region));\n\n if (unresolved.length !== 0) {\n throw new Error(`Unable to produce a working riff-raff.yaml file; all stacks must have an explicit region set`);\n }\n }\n\n private getLambdas(cdkStack: GuStack): GuLambdaFunction[] {\n return cdkStack.node.findAll().filter((_) => _ instanceof GuLambdaFunction) as GuLambdaFunction[];\n }\n\n private getAutoScalingGroups(cdkStack: GuStack): GuAutoScalingGroup[] {\n return cdkStack.node.findAll().filter((_) => _ instanceof GuAutoScalingGroup) as GuAutoScalingGroup[];\n }\n\n private getGuStackDependencies(cdkStack: GuStack): GuStack[] {\n return cdkStack.dependencies.filter((_) => _ instanceof GuStack) as GuStack[];\n }\n\n // eslint-disable-next-line custom-rules/valid-constructors -- this needs to sit above GuStack on the cdk tree\n constructor(app: App) {\n this.allCdkStacks = app.node.findAll().filter((_) => _ instanceof GuStack) as GuStack[];\n const allowedStages = new Set(this.allCdkStacks.map(({ stage }) => stage));\n this.allStageTags = Array.from(allowedStages);\n this.allStackTags = Array.from(new Set(this.allCdkStacks.map(({ stack }) => stack)));\n this.allRegions = Array.from(new Set(this.allCdkStacks.map(({ region }) => region)));\n\n this.validateStacksInApp();\n this.validateAllRegionsAreResolved();\n\n this.outdir = app.outdir;\n\n const deployments = new Map<RiffRaffDeploymentName, RiffRaffDeploymentProps>();\n\n const groupedStacks: GroupedCdkStacks = groupByClassNameStackRegionStage(this.allCdkStacks);\n\n Object.values(groupedStacks).forEach((stackTagGroup) => {\n Object.values(stackTagGroup).forEach((regionGroup) => {\n Object.values(regionGroup).forEach((stageGroup) => {\n const stacks: GuStack[] = Object.values(stageGroup).flat();\n\n // The items in `stacks` only differ by stage, so we can just use the first item in the list.\n const [stack] = stacks;\n\n if (!stack) {\n throw new Error(\"Unable to produce a working riff-raff.yaml file; there are no stacks!\");\n }\n\n const lambdas = this.getLambdas(stack);\n const autoscalingGroups = this.getAutoScalingGroups(stack);\n\n const artifactUploads: RiffRaffDeployment[] = [\n lambdas.filter((_) => !_.withoutArtifactUpload).map(uploadLambdaArtifact),\n autoscalingGroups.map(uploadAutoscalingArtifact),\n ].flat();\n artifactUploads.forEach(({ name, props }) => deployments.set(name, props));\n\n const parentStacks: RiffRaffDeployment[] = this.getGuStackDependencies(stack).map((x) =>\n cloudFormationDeployment([x], [], this.outdir),\n );\n\n const cfnDeployment = cloudFormationDeployment(stacks, [...artifactUploads, ...parentStacks], this.outdir);\n deployments.set(cfnDeployment.name, cfnDeployment.props);\n\n const lambdasWithoutAnAlias = lambdas.filter((lambda) => lambda.alias === undefined);\n // If the Lambda has an alias it is using versioning. When using versioning, there is no need for Riff-Raff\n // to modify the unpublished version of the function\n lambdasWithoutAnAlias.forEach((lambda) => {\n const lambdaDeployment = updateLambdaDeployment(lambda, cfnDeployment);\n deployments.set(lambdaDeployment.name, lambdaDeployment.props);\n });\n\n /*\n Instances in an ASG with an `AutoScalingRollingUpdate` update policy are rotated via CloudFormation.\n Therefore, they do not need to also perform an `autoscaling` deployment via Riff-Raff.\n */\n const legacyAutoscalingGroups = autoscalingGroups.filter((asg) => {\n const { cfnOptions } = asg.node.defaultChild as CfnAutoScalingGroup;\n const { updatePolicy } = cfnOptions;\n return updatePolicy?.autoScalingRollingUpdate === undefined;\n });\n\n legacyAutoscalingGroups.forEach((asg) => {\n const asgDeployment = autoscalingDeployment(asg, cfnDeployment);\n deployments.set(asgDeployment.name, asgDeployment.props);\n });\n\n deployments.set(\n cfnDeployment.name,\n addAmiParametersToCloudFormationDeployment(cfnDeployment, autoscalingGroups),\n );\n });\n });\n });\n\n this.riffRaffYaml = {\n allowedStages,\n deployments,\n };\n }\n\n /**\n * The `riff-raff.yaml` file as a string.\n * Useful for testing.\n */\n toYAML(): string {\n // Add support for ES6 Set and Map. See https://github.com/nodeca/js-yaml/issues/436.\n const replacer = (_key: string, value: unknown) => {\n if (value instanceof Set) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- this is how `js-yaml` is typed\n return Array.from(value);\n }\n if (value instanceof Map) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- this is how `js-yaml` is typed\n return Object.fromEntries(value);\n }\n return value;\n };\n\n return dump(this.riffRaffYaml, { replacer });\n }\n\n /**\n * Write the `riff-raff.yaml` file to disk.\n * It'll be located with the CFN JSON templates generated by `cdk synth`.\n */\n synth(): void {\n const outPath = path.join(this.outdir, \"riff-raff.yaml\");\n writeFileSync(outPath, this.toYAML());\n }\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@guardian/cdk",
3
3
  "description": "Generic Guardian flavoured AWS CDK components",
4
- "version": "59.3.5",
4
+ "version": "59.5.0",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "files": [
@@ -38,7 +38,7 @@
38
38
  "@types/lodash.camelcase": "^4.3.9",
39
39
  "@types/lodash.kebabcase": "^4.1.9",
40
40
  "@types/lodash.upperfirst": "^4.3.9",
41
- "@types/node": "22.5.3",
41
+ "@types/node": "22.5.4",
42
42
  "@types/yargs": "^17.0.33",
43
43
  "aws-cdk": "2.157.0",
44
44
  "aws-cdk-lib": "2.157.0",
@@ -51,7 +51,7 @@
51
51
  "serve": "^14.2.3",
52
52
  "ts-jest": "^29.2.5",
53
53
  "ts-node": "^10.9.2",
54
- "typedoc": "^0.26.6",
54
+ "typedoc": "^0.26.7",
55
55
  "typescript": "5.1.6"
56
56
  },
57
57
  "dependencies": {