diff --git a/.changeset/grumpy-experts-divide.md b/.changeset/grumpy-experts-divide.md new file mode 100644 index 0000000000..2f8bdf6f02 --- /dev/null +++ b/.changeset/grumpy-experts-divide.md @@ -0,0 +1,5 @@ +--- +"@guardian/cdk": minor +--- + +feat(asg): Allow setting the UpdatePolicy on ASGs provisioned by our EC2 patterns diff --git a/src/constructs/autoscaling/asg.ts b/src/constructs/autoscaling/asg.ts index 0c6b0d6300..bb8a02c969 100644 --- a/src/constructs/autoscaling/asg.ts +++ b/src/constructs/autoscaling/asg.ts @@ -94,6 +94,7 @@ export class GuAutoScalingGroup extends GuAppAwareConstruct(AutoScalingGroup) { vpc, withoutImdsv2 = false, httpPutResponseHopLimit, + updatePolicy, } = props; // 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. @@ -163,7 +164,9 @@ export class GuAutoScalingGroup extends GuAppAwareConstruct(AutoScalingGroup) { // A CDK AutoScalingGroup comes with this update policy, whereas the CFN autscaling group // leaves it to the default value, which is actually false. // { UpdatePolicy: { autoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true }} - cfnAsg.addDeletionOverride("UpdatePolicy"); + if (!updatePolicy) { + cfnAsg.addDeletionOverride("UpdatePolicy"); + } Tags.of(launchTemplate).add("App", app); } diff --git a/src/patterns/ec2-app/base.test.ts b/src/patterns/ec2-app/base.test.ts index 5850c7af21..71fd009a49 100644 --- a/src/patterns/ec2-app/base.test.ts +++ b/src/patterns/ec2-app/base.test.ts @@ -1,5 +1,5 @@ import { Match, Template } from "aws-cdk-lib/assertions"; -import { BlockDeviceVolume, EbsDeviceVolumeType } from "aws-cdk-lib/aws-autoscaling"; +import { BlockDeviceVolume, EbsDeviceVolumeType, UpdatePolicy } from "aws-cdk-lib/aws-autoscaling"; import { InstanceClass, InstanceSize, InstanceType, Peer, Port, Vpc } from "aws-cdk-lib/aws-ec2"; import { type CfnLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2"; import { AccessScope, MetadataKeys } from "../../constants"; @@ -1095,4 +1095,32 @@ describe("the GuEC2App pattern", function () { SslPolicy: "ELBSecurityPolicy-TLS13-1-2-2021-06", }); }); + + it("has a defined UpdatePolicy when provided with one", function () { + const stack = simpleGuStackForTesting(); + new GuEc2App(stack, { + applicationPort: 3000, + app: "test-gu-ec2-app", + access: { scope: AccessScope.PUBLIC }, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM), + monitoringConfiguration: { noMonitoring: true }, + userData: "#!/bin/dev foobarbaz", + certificateProps: { + domainName: "domain-name-for-your-application.example", + }, + scaling: { + minimumInstances: 1, + }, + instanceMetadataHopLimit: 2, + updatePolicy: UpdatePolicy.replacingUpdate(), + }); + + Template.fromStack(stack).hasResource("AWS::AutoScaling::AutoScalingGroup", { + UpdatePolicy: { + AutoScalingReplacingUpdate: { + WillReplace: true, + }, + }, + }); + }); }); diff --git a/src/patterns/ec2-app/base.ts b/src/patterns/ec2-app/base.ts index 308c8e5664..3d36a6c26c 100644 --- a/src/patterns/ec2-app/base.ts +++ b/src/patterns/ec2-app/base.ts @@ -1,6 +1,6 @@ /* eslint "@guardian/tsdoc-required/tsdoc-required": 2 -- to begin rolling this out for public APIs. */ import { Duration, SecretValue, Tags } from "aws-cdk-lib"; -import type { BlockDevice } from "aws-cdk-lib/aws-autoscaling"; +import type { BlockDevice, UpdatePolicy } from "aws-cdk-lib/aws-autoscaling"; import { HealthCheck } from "aws-cdk-lib/aws-autoscaling"; import { ProviderAttribute, @@ -298,6 +298,16 @@ export interface GuEc2AppProps extends AppIdentity { * for example when sharing the instance profile with a docker container running on the instance. */ instanceMetadataHopLimit?: number; + + /** + * Specify an update policy for the ASG created by this pattern. + * + * @see https://docs.aws.amazon.com/cdk/api/latest/docs/aws-autoscaling-readme.html#update-policy + * + * @defaultValue UpdatePolicy.none() - Cloudformation does not attempt to rotate instances in the ASG + * and must rely on riffraff to do so. + */ + updatePolicy?: UpdatePolicy; } function restrictedCidrRanges(ranges: IPeer[]) { @@ -352,6 +362,7 @@ export class GuEc2App extends Construct { privateSubnets = GuVpc.subnetsFromParameter(scope, { type: SubnetType.PRIVATE, app }), publicSubnets = GuVpc.subnetsFromParameter(scope, { type: SubnetType.PUBLIC, app }), instanceMetadataHopLimit, + updatePolicy, } = props; super(scope, app); // The assumption is `app` is unique @@ -400,6 +411,7 @@ export class GuEc2App extends Construct { ...(blockDevices && { blockDevices }), imageRecipe, httpPutResponseHopLimit: instanceMetadataHopLimit, + updatePolicy, }); // This allows automatic shipping of instance Cloud Init logs when using the