cdk/lib/status-app.ts (136 lines of code) (raw):
import { GuEc2App } from '@guardian/cdk';
import { AccessScope } from '@guardian/cdk/lib/constants';
import type { GuStackProps } from '@guardian/cdk/lib/constructs/core';
import {
GuParameter,
GuStack,
GuStringParameter,
} from '@guardian/cdk/lib/constructs/core';
import { GuSecurityGroup } from '@guardian/cdk/lib/constructs/ec2';
import { GuAllowPolicy } from '@guardian/cdk/lib/constructs/iam';
import { type App, Duration, Tags } from 'aws-cdk-lib';
import {
InstanceClass,
InstanceSize,
InstanceType,
Peer,
Port,
UserData,
} from 'aws-cdk-lib/aws-ec2';
import { CfnRecordSet, RecordType } from 'aws-cdk-lib/aws-route53';
export class StatusApp extends GuStack {
constructor(scope: App, id: string, props: GuStackProps) {
super(scope, id, props);
const { stage, stack } = props;
const app = 'status-app';
const region = 'eu-west-1';
new GuParameter(this, 'OAuthHost', {
description: 'Host domain for the Status App',
default: `/status-app/oauth/host`,
type: 'AWS::SSM::Parameter::Value<String>',
});
new GuParameter(this, 'OAuthProtocol', {
description: 'Protocol for the Status App',
default: `/status-app/oauth/protocol`,
type: 'AWS::SSM::Parameter::Value<String>',
});
new GuParameter(this, 'OAuthClientId', {
description: 'Google OAuth client ID for authentication',
default: `/status-app/oauth/clientId`,
type: 'AWS::SSM::Parameter::Value<String>',
});
new GuParameter(this, 'OAuthClientSecret', {
description: 'Google OAuth client secret for authentication',
default: `/status-app/oauth/clientSecret`,
type: 'AWS::SSM::Parameter::Value<String>',
});
new GuParameter(this, 'OAuthAllowedDomain', {
description: 'Allowed domain for Google OAuth authentication',
default: `/status-app/oauth/allowedDomain`,
type: 'AWS::SSM::Parameter::Value<String>',
});
const hostedZoneName = new GuStringParameter(this, 'hosted-zone-name', {
description:
"DNS hosted zone for which A CNAME will be created. e.g. example.com (note, no trailing full-stop) for status.example.com. Leave empty if you don't want to add a CNAME to the status app",
});
const hostedZoneId = new GuStringParameter(this, 'hosted-zone-id', {
description: 'ID for the hosted zone',
});
const userData = UserData.custom(`#!/bin/bash -ev
aws --region ${region} s3 cp s3://ophan-dist/ophan/${stage}/status-app/status-app_1.0_all.deb .
dpkg -i status-app_1.0_all.deb
/opt/cloudwatch-logs/configure-logs application ${stack} ${stage} status-app /var/log/status-app/status-app.log
`);
const domainName = `status.ophan.co.uk`;
const ec2 = new GuEc2App(this, {
app,
access: {
scope: AccessScope.PUBLIC,
},
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL),
applicationPort: 9000,
monitoringConfiguration: { noMonitoring: true },
scaling: { minimumInstances: 1, maximumInstances: 2 },
certificateProps: {
domainName: domainName,
hostedZoneId: hostedZoneId.valueAsString,
},
userData,
imageRecipe: 'ophan-ubuntu-jammy-ARM-CDK',
roleConfiguration: {
additionalPolicies: [
new GuAllowPolicy(this, 'read-metadata', {
resources: ['*'],
actions: [
'ec2:describe*',
'autoscaling:Describe*',
'elasticloadbalancing:Describe*',
'cloudwatch:Get*',
'sqs:ListQueues',
],
}),
new GuAllowPolicy(this, 'status-app-parameter-store-access', {
resources: [
`arn:aws:ssm:${region}:${this.account}:parameter/status-app/*`,
],
actions: ['ssm:GetParameter', 'ssm:GetParameters'],
}),
],
},
});
ec2.targetGroup.healthCheck = {
...ec2.targetGroup.healthCheck,
path: '/management/healthcheck',
healthyThresholdCount: 2,
unhealthyThresholdCount: 2,
interval: Duration.seconds(10),
timeout: Duration.seconds(5),
};
Tags.of(ec2.autoScalingGroup).add('SystemdUnit', `${app}.service`);
ec2.autoScalingGroup.instanceLaunchTemplate.addSecurityGroup(
new GuSecurityGroup(this, `ElasticSearchEgressSecurityGroup`, {
app,
vpc: ec2.vpc,
description: 'Allow outbound traffic to Elasticsearch',
allowAllOutbound: false,
egresses: [
{
range: Peer.anyIpv4(),
port: Port.tcp(9200),
description: 'Allow outbound traffic to Elasticsearch on port 9200',
},
],
}),
);
if (hostedZoneName.valueAsString) {
new CfnRecordSet(this, 'cname-record', {
name: `status.${hostedZoneName.valueAsString}`,
comment: 'CNAME for status app',
type: RecordType.CNAME,
hostedZoneName: `${hostedZoneName.valueAsString}.`,
ttl: '900',
resourceRecords: [ec2.loadBalancer.loadBalancerDnsName],
});
}
}
}