Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Perform EC2 deployments via CloudFormation #948

Merged
merged 2 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ jobs:
cdk.out:
- cdk/cdk.out
prism:
- target/prism.deb
- dist
2 changes: 2 additions & 0 deletions cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand All @@ -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');
Expand Down
120 changes: 116 additions & 4 deletions cdk/lib/__snapshots__/prism.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = `
"GuVpcParameter",
"GuSubnetListParameter",
"GuSubnetListParameter",
"GuPlayApp",
"GuEc2AppExperimental",
"GuDistributionBucketParameter",
"GuCertificate",
"GuInstanceRole",
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1238,13 +1299,60 @@ exports[`The PrismEc2App stack matches the snapshot 1`] = `
"",
[
"#!/bin/bash
mkdir -p $(dirname '/prism/prism.deb')
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.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
# 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",
],
],
},
Expand All @@ -1258,6 +1366,10 @@ dpkg -i /prism/prism.deb",
"Key": "App",
"Value": "prism",
},
{
"Key": "gu:build-identifier",
"Value": "TEST",
},
{
"Key": "gu:cdk:version",
"Value": "TEST",
Expand Down
1 change: 1 addition & 0 deletions cdk/lib/prism.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
20 changes: 16 additions & 4 deletions cdk/lib/prism.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -25,6 +25,12 @@ import {
interface PrismProps extends Omit<GuStackProps, 'description' | 'stack'> {
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 {
Expand All @@ -37,7 +43,13 @@ export class Prism extends GuStack {
app,
});

const pattern = new GuPlayApp(this, {
const { buildIdentifier } = props;

const filename = `${app}-${buildIdentifier}.deb`;
Copy link

@adamnfish adamnfish Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most minor thing in the world, but I get a niggle that we could use a different separator character here? Hyphens are commonly to separate words in the artefact name, making the version pop out will help at a glance, and if someone wants to parse it out for any reason later. It looks like sbt-native-packager is using _ as the version delimiter, could we do the same?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do. Will raise a follow-up PR. Would you think we can formalise the use of _? I've been struggling to think how we'd address this TODO - https://github.com/guardian/cdk/blob/890c89e07d6715277ef9db4f01baeafddc7696e8/src/experimental/patterns/ec2-app.ts#L265.


const pattern = new GuEc2AppExperimental(this, {
buildIdentifier,
applicationPort: 9000,
app,
applicationLogging: {
enabled: true,
Expand All @@ -46,8 +58,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: {
Expand Down
4 changes: 2 additions & 2 deletions scripts/ci
Original file line number Diff line number Diff line change
Expand Up @@ -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"