cdk/lib/floodgate.ts (141 lines of code) (raw):
import type {GuStackProps} from "@guardian/cdk/lib/constructs/core";
import {GuParameter, GuStack} from "@guardian/cdk/lib/constructs/core";
import type {App} from "aws-cdk-lib";
import {aws_ssm, Stack} from "aws-cdk-lib";
import {GuEc2App} from "@guardian/cdk";
import {AccessScope} from "@guardian/cdk/lib/constants";
import {InstanceClass, InstanceSize, InstanceType, Peer, Port, Vpc} from "aws-cdk-lib/aws-ec2";
import fs from "fs";
import {GuSecurityGroup, GuVpc} from "@guardian/cdk/lib/constructs/ec2";
import {GuPolicy} from "@guardian/cdk/lib/constructs/iam";
import {Effect, PolicyStatement} from "aws-cdk-lib/aws-iam";
import {Datastore} from "./datastore";
const useArm = true;
export class Floodgate extends GuStack {
constructor(scope: App, id: string, props: GuStackProps) {
super(scope, id, props);
const vpcId = aws_ssm.StringParameter.valueForStringParameter(this, this.getVpcIdPath());
const vpc = Vpc.fromVpcAttributes(this, "vpc", {
vpcId: vpcId,
availabilityZones: ["eu-west-1a","eu-west-1b" ,"eu-west-1c"]
});
const subnetsList = new GuParameter(this, "subnets", {
description: "Subnets to deploy into",
default: this.getDeploymentSubnetsPath(),
fromSSM: true,
type: "List<String>"
});
const deploymentSubnets = GuVpc.subnets(this, subnetsList.valueAsList);
const datastore = new Datastore(this, "Datastore");
const dnsZone = aws_ssm.StringParameter.valueForStringParameter(this, `/account/services/capi.gutools/${this.stage}/hostedzoneid`);
const prometheusRemoteWriteUrl = new GuParameter(this, "PromRemoteWrite", {
description: "SSM path pointing to the parameter which gives an Amazon Managed Prometheus endpoint to push metrics to",
fromSSM: true,
type: "String",
default: "/account/content-api-common/metrics/prometheus_remote_write_url"
});
const userDataRaw = fs.readFileSync("instance-startup.sh").toString('utf-8');
const userData = userDataRaw
.replace(/\$\{Stage}/g, this.stage)
.replace(/\$\{Stack}/g, this.stack)
.replace(/\$\{AWS::Region}/g, Stack.of(this).region)
.replace(/\$\{PrometheusRemoteWriteUrl}/g, prometheusRemoteWriteUrl.valueAsString)
.replace(/\$\{BuiltVersion}/g, "1.0");
const app = new GuEc2App(this, {
access: {
scope: AccessScope.PUBLIC,
},
app: "content-api-floodgate",
applicationLogging: {
enabled: true,
systemdUnitName: "content-api-floodgate"
},
roleConfiguration: {
additionalPolicies: [
new GuPolicy(this, "FloodgatePolicy", {
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["s3:GetObject"],
//we should already have access to content-api-dist "for free" because that's the default distribution bucket
resources: [
"arn:aws:s3:::content-api-config/*"
]
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["dynamodb:*"],
resources: [
datastore.jobHistoryTable.tableArn,
datastore.contentSourceTable.tableArn,
datastore.runningJobTable.tableArn,
]
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"ec2:DescribeInstances",
"ec2:DescribeTags",
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances"
],
resources: ["*"]
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"aps:RemoteWrite"
],
resources: ["*"]
})
]
})
]
},
applicationPort: 9000,
certificateProps: {
domainName: this.stage==="CODE" ? "floodgate.capi.code.dev-gutools.co.uk" : "floodgate.capi.gutools.co.uk",
hostedZoneId: dnsZone,
},
instanceType: useArm ? InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL) : InstanceType.of(InstanceClass.T3, InstanceSize.SMALL),
monitoringConfiguration: {
snsTopicName: `floogate-monitoring-${this.stage}`,
http5xxAlarm: {
tolerated5xxPercentage: 0,
numberOfMinutesAboveThresholdBeforeAlarm: 2,
},
unhealthyInstancesAlarm: true,
},
privateSubnets: deploymentSubnets,
publicSubnets: deploymentSubnets,
scaling: {
minimumInstances: 1,
maximumInstances: 2,
},
userData: userData,
vpc,
});
app.autoScalingGroup.connections.addSecurityGroup(new GuSecurityGroup(this, "InstanceOutboundSG", {
app: "content-api-floodgate",
allowAllOutbound: false,
allowAllIpv6Outbound: false,
egresses: [
{
range: Peer.ipv4("10.248.0.0/16"),
port: Port.tcp(8080),
description: "Outgoing to port 8080 on internal infrastructure"
}
],
vpc,
}))
}
getAccountPath(elementName: string) {
const basePath = "/account/vpc";
return `${basePath}/${this.stage}-generic/${elementName}`;
}
getVpcIdPath() {
return this.getAccountPath("id");
}
getDeploymentSubnetsPath() {
return this.getAccountPath("subnets")
}
}