From 2720f687f2cd0bb3ef3f4261ef5f863b458c4143 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 19 Sep 2024 16:17:18 +0100 Subject: [PATCH 1/2] feat: Add build number to application artifact This is a requirement for using `GuEc2AppExperimental`. --- .github/workflows/ci.yml | 2 +- cdk/bin/cdk.ts | 2 ++ cdk/lib/__snapshots__/prism.test.ts.snap | 6 +++--- cdk/lib/prism.test.ts | 1 + cdk/lib/prism.ts | 14 ++++++++++++-- scripts/ci | 4 ++-- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51e4a6e1..71d9ef5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,4 +44,4 @@ jobs: cdk.out: - cdk/cdk.out prism: - - target/prism.deb + - dist diff --git a/cdk/bin/cdk.ts b/cdk/bin/cdk.ts index dfa875cb..fde10cdb 100644 --- a/cdk/bin/cdk.ts +++ b/cdk/bin/cdk.ts @@ -13,6 +13,7 @@ new Prism(app, 'Prism-CODE', { minimumInstances: 1, cloudFormationStackName: 'prism-CODE', env: { region: 'eu-west-1' }, + buildIdentifier: process.env.GITHUB_RUN_NUMBER ?? 'DEV', }); new Prism(app, 'Prism-PROD', { @@ -21,6 +22,7 @@ new Prism(app, 'Prism-PROD', { minimumInstances: 2, cloudFormationStackName: 'prism-PROD', env: { region: 'eu-west-1' }, + buildIdentifier: process.env.GITHUB_RUN_NUMBER ?? 'DEV', }); new PrismAccess(app, 'PrismAccessStackSet'); diff --git a/cdk/lib/__snapshots__/prism.test.ts.snap b/cdk/lib/__snapshots__/prism.test.ts.snap index 0f17da1c..105c687f 100644 --- a/cdk/lib/__snapshots__/prism.test.ts.snap +++ b/cdk/lib/__snapshots__/prism.test.ts.snap @@ -1238,13 +1238,13 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = ` "", [ "#!/bin/bash -mkdir -p $(dirname '/prism/prism.deb') +mkdir -p $(dirname '/prism/prism-TEST.deb') aws s3 cp 's3://", { "Ref": "DistributionBucketName", }, - "/deploy/PROD/prism/prism.deb' '/prism/prism.deb' -dpkg -i /prism/prism.deb", + "/deploy/PROD/prism/prism-TEST.deb' '/prism/prism-TEST.deb' +dpkg -i /prism/prism-TEST.deb", ], ], }, diff --git a/cdk/lib/prism.test.ts b/cdk/lib/prism.test.ts index 3f56f1bc..8a213c9e 100644 --- a/cdk/lib/prism.test.ts +++ b/cdk/lib/prism.test.ts @@ -9,6 +9,7 @@ describe('The PrismEc2App stack', () => { stage: 'PROD', domainName: 'prism.gutools.co.uk', minimumInstances: 2, + buildIdentifier: 'TEST', }); expect(Template.fromStack(stack).toJSON()).toMatchSnapshot(); }); diff --git a/cdk/lib/prism.ts b/cdk/lib/prism.ts index 75f969d7..691601b8 100644 --- a/cdk/lib/prism.ts +++ b/cdk/lib/prism.ts @@ -25,6 +25,12 @@ import { interface PrismProps extends Omit { domainName: string; minimumInstances: number; + + /** + * Which application build to run. + * This will typically match the build number provided by CI. + */ + buildIdentifier: string; } export class Prism extends GuStack { @@ -37,6 +43,10 @@ export class Prism extends GuStack { app, }); + const { buildIdentifier } = props; + + const filename = `${app}-${buildIdentifier}.deb`; + const pattern = new GuPlayApp(this, { app, applicationLogging: { @@ -46,8 +56,8 @@ export class Prism extends GuStack { instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM), userData: { distributable: { - fileName: `${app}.deb`, - executionStatement: `dpkg -i /${app}/${app}.deb`, + fileName: filename, + executionStatement: `dpkg -i /${app}/${filename}`, }, }, certificateProps: { diff --git a/scripts/ci b/scripts/ci index f9b7dc27..866f6894 100755 --- a/scripts/ci +++ b/scripts/ci @@ -13,5 +13,5 @@ set -e sbt clean scalafmtSbtCheck scalafmtCheckAll compile test debian:packageBin -# `sbt debian:packageBin` produces `target/prism_1.0-SNAPSHOT_all.deb`. Rename it to something easier. -mv target/prism_1.0-SNAPSHOT_all.deb target/prism.deb +mkdir -p dist +mv target/prism_1.0-SNAPSHOT_all.deb "dist/prism-$GITHUB_RUN_NUMBER.deb" From 69e6f18f88a2a3a20211f9ef55f3d2ae57631233 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 19 Sep 2024 16:09:33 +0100 Subject: [PATCH 2/2] feat: Perform EC2 deployments via CloudFormation --- cdk/lib/__snapshots__/prism.test.ts.snap | 116 ++++++++++++++++++++++- cdk/lib/prism.ts | 6 +- 2 files changed, 118 insertions(+), 4 deletions(-) diff --git a/cdk/lib/__snapshots__/prism.test.ts.snap b/cdk/lib/__snapshots__/prism.test.ts.snap index 105c687f..bc35718e 100644 --- a/cdk/lib/__snapshots__/prism.test.ts.snap +++ b/cdk/lib/__snapshots__/prism.test.ts.snap @@ -12,7 +12,7 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = ` "GuVpcParameter", "GuSubnetListParameter", "GuSubnetListParameter", - "GuPlayApp", + "GuEc2AppExperimental", "GuDistributionBucketParameter", "GuCertificate", "GuInstanceRole", @@ -77,8 +77,49 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = ` }, }, "Resources": { + "AsgRollingUpdatePolicy2A1DDC6F": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "cloudformation:SignalResource", + "Effect": "Allow", + "Resource": { + "Ref": "AWS::StackId", + }, + }, + { + "Action": "elasticloadbalancing:DescribeTargetHealth", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AsgRollingUpdatePolicy2A1DDC6F", + "Roles": [ + { + "Ref": "InstanceRolePrism96D154B7", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, "AutoScalingGroupPrismASG36691601": { + "CreationPolicy": { + "AutoScalingCreationPolicy": { + "MinSuccessfulInstancesPercent": 100, + }, + "ResourceSignal": { + "Count": 2, + "Timeout": "PT16M", + }, + }, + "DependsOn": [ + "AsgRollingUpdatePolicy2A1DDC6F", + ], "Properties": { + "DesiredCapacity": "2", "HealthCheckGracePeriod": 900, "HealthCheckType": "ELB", "LaunchTemplate": { @@ -148,6 +189,18 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = ` }, }, "Type": "AWS::AutoScaling::AutoScalingGroup", + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "MaxBatchSize": 4, + "MinInstancesInService": 2, + "MinSuccessfulInstancesPercent": 100, + "PauseTime": "PT16M", + "SuspendProcesses": [ + "AlarmNotification", + ], + "WaitOnResourceSignals": true, + }, + }, }, "CertificatePrism0841D21D": { "DeletionPolicy": "Retain", @@ -1180,6 +1233,10 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = ` "Key": "App", "Value": "prism", }, + { + "Key": "gu:build-identifier", + "Value": "TEST", + }, { "Key": "gu:cdk:version", "Value": "TEST", @@ -1209,6 +1266,10 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = ` "Key": "App", "Value": "prism", }, + { + "Key": "gu:build-identifier", + "Value": "TEST", + }, { "Key": "gu:cdk:version", "Value": "TEST", @@ -1238,13 +1299,60 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = ` "", [ "#!/bin/bash +function exitTrap(){ +exitCode=$? + + cfn-signal --stack ", + { + "Ref": "AWS::StackId", + }, + " --resource AutoScalingGroupPrismASG36691601 --region ", + { + "Ref": "AWS::Region", + }, + " --exit-code $exitCode || echo 'Failed to send Cloudformation Signal' + +} +trap exitTrap EXIT mkdir -p $(dirname '/prism/prism-TEST.deb') aws s3 cp 's3://", { "Ref": "DistributionBucketName", }, "/deploy/PROD/prism/prism-TEST.deb' '/prism/prism-TEST.deb' -dpkg -i /prism/prism-TEST.deb", +dpkg -i /prism/prism-TEST.deb +# GuEc2AppExperimental UserData Start + + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" "http://169.254.169.254/latest/meta-data/instance-id") + + STATE=$(aws elbv2 describe-target-health --target-group-arn ", + { + "Ref": "TargetGroupPrismC8B388A7", + }, + " --region ", + { + "Ref": "AWS::Region", + }, + " --targets Id=$INSTANCE_ID,Port=9000 --query "TargetHealthDescriptions[0].TargetHealth.State") + + until [ "$STATE" == "\\"healthy\\"" ]; do + echo "Instance running build TEST not yet healthy within target group. Current state $STATE. Sleeping..." + sleep 5 + STATE=$(aws elbv2 describe-target-health --target-group-arn ", + { + "Ref": "TargetGroupPrismC8B388A7", + }, + " --region ", + { + "Ref": "AWS::Region", + }, + " --targets Id=$INSTANCE_ID,Port=9000 --query "TargetHealthDescriptions[0].TargetHealth.State") + done + + echo "Instance running build TEST is healthy in target group." + +# GuEc2AppExperimental UserData End", ], ], }, @@ -1258,6 +1366,10 @@ dpkg -i /prism/prism-TEST.deb", "Key": "App", "Value": "prism", }, + { + "Key": "gu:build-identifier", + "Value": "TEST", + }, { "Key": "gu:cdk:version", "Value": "TEST", diff --git a/cdk/lib/prism.ts b/cdk/lib/prism.ts index 691601b8..7012d8ec 100644 --- a/cdk/lib/prism.ts +++ b/cdk/lib/prism.ts @@ -1,4 +1,3 @@ -import { GuPlayApp } from '@guardian/cdk'; import { AccessScope } from '@guardian/cdk/lib/constants'; import type { GuStackProps } from '@guardian/cdk/lib/constructs/core/stack'; import { GuStack } from '@guardian/cdk/lib/constructs/core/stack'; @@ -8,6 +7,7 @@ import { GuDynamoDBReadPolicy, GuGetS3ObjectsPolicy, } from '@guardian/cdk/lib/constructs/iam'; +import { GuEc2AppExperimental } from '@guardian/cdk/lib/experimental/patterns/ec2-app'; import type { App } from 'aws-cdk-lib'; import { Duration } from 'aws-cdk-lib'; import type { CfnAutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; @@ -47,7 +47,9 @@ export class Prism extends GuStack { const filename = `${app}-${buildIdentifier}.deb`; - const pattern = new GuPlayApp(this, { + const pattern = new GuEc2AppExperimental(this, { + buildIdentifier, + applicationPort: 9000, app, applicationLogging: { enabled: true,