cdk/lib/sanity-tests.ts (114 lines of code) (raw):

import type {GuStackProps} from "@guardian/cdk/lib/constructs/core"; import {GuLoggingStreamNameParameter, GuParameter, GuStack} from "@guardian/cdk/lib/constructs/core"; import type {App} from "aws-cdk-lib"; import {aws_ssm, Duration, Stack} from "aws-cdk-lib"; import {GuEc2App} from "@guardian/cdk"; import {InstanceClass, InstanceSize, InstanceType, Peer, Port, Vpc} from "aws-cdk-lib/aws-ec2"; import {AccessScope} from "@guardian/cdk/lib/constants"; import fs from "fs"; import {GuSecurityGroup, GuVpc} from "@guardian/cdk/lib/constructs/ec2"; import {cloudwatchMetricNamespace, useArmInstance} from "./constants"; import {Policies} from "./policies"; import {Alarm, ComparisonOperator, Metric, Statistic} from "aws-cdk-lib/aws-cloudwatch"; import {SnsAction} from "aws-cdk-lib/aws-cloudwatch-actions"; import {Topic} from "aws-cdk-lib/aws-sns"; export class SanityTests extends GuStack { constructor(scope: App, id: string, props: GuStackProps) { super(scope, id, props); const urgentAlarmTopicArn = aws_ssm.StringParameter.fromStringParameterName(this, "urgent-alarm-arn", "/account/content-api-common/alarms/urgent-alarm-topic"); const nonUrgentAlarmTopicArn = aws_ssm.StringParameter.fromStringParameterName(this, "non-urgent-alarm-arn", "/account/content-api-common/alarms/non-urgent-alarm-topic"); 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 hostedZone = aws_ssm.StringParameter.valueForStringParameter(this, `/account/services/capi.gutools/${this.stage}/hostedzoneid`); const userDataRaw = fs.readFileSync("./instance-startup.sh").toString('utf-8'); const userData = userDataRaw .replace(/\$\{Stage}/g, this.stage) .replace(/\$\{Stack}/g, this.stack) .replace(/\$\{LoggingStreamName}/g, GuLoggingStreamNameParameter.getInstance(this).valueAsString) .replace(/\$\{AWS::Region}/g, Stack.of(this).region); const app = new GuEc2App(this, { access: { scope: AccessScope.INTERNAL, cidrRanges: [Peer.ipv4("10.0.0.0/8")], }, app: "sanity-tests", applicationLogging: { enabled: true, }, roleConfiguration: { additionalPolicies: Policies(this), }, applicationPort: 9000, certificateProps: { domainName: this.stage=="CODE" ? "sanity-tests.capi.code.dev-gutools.co.uk" : "sanity-tests.capi.gutools.co.uk", hostedZoneId: hostedZone, }, instanceType: InstanceType.of(useArmInstance ? InstanceClass.T4G : InstanceClass.T3, InstanceSize.MICRO), monitoringConfiguration: { snsTopicName: urgentAlarmTopicArn.stringValue, http5xxAlarm: false, unhealthyInstancesAlarm: true, }, privateSubnets: deploymentSubnets, publicSubnets: deploymentSubnets, scaling: { minimumInstances: 1, maximumInstances: 2, }, userData: userData, vpc, }); const allowingOutgoingSG = new GuSecurityGroup(this, "OutgoingSG", { egresses: [ { range: Peer.anyIpv4(), port: Port.tcp(80), description: "Allow outgoing HTTP" } ], vpc, app: "sanity-tests" }) app.autoScalingGroup.addSecurityGroup(allowingOutgoingSG); const alarm = new Alarm(this, "NotEnoughSuccessfulTestsAlarm", { actionsEnabled: true, alarmDescription: 'Fewer than 100 tests in 5 minutes (we expect to run at least 14 tests every 30 seconds)', alarmName: `content-api-sanity-tests-${this.stage}-alarm-not-enough-successful-tests`, evaluationPeriods: 5, comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD, metric: new Metric({ period: Duration.minutes(5), //the CDK is quite prescriptive about the allowed durations metricName: "SuccessfulTests", namespace: cloudwatchMetricNamespace, statistic: Statistic.SUM, }), threshold: 100 }); const alarmTopic = Topic.fromTopicArn(this, "AlarmTopic", nonUrgentAlarmTopicArn.stringValue); if(this.stage=="PROD") alarm.addAlarmAction(new SnsAction(alarmTopic)); } getAccountPath(elementName: string) { const basePath = "/account/vpc"; if(this.stack.includes("preview")) { return this.stage=="CODE" ? `${basePath}/CODE-preview/${elementName}` : `${basePath}/PROD-preview/${elementName}`; } else { return this.stage=="CODE" ? `${basePath}/CODE-live/${elementName}` : `${basePath}/PROD-live/${elementName}`; } } getVpcIdPath() { return this.getAccountPath("id"); } getDeploymentSubnetsPath() { return this.getAccountPath("subnets") } }