cdk/lib/podcasts-rss.ts (128 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, Duration} from "aws-cdk-lib"; import {GuPlayApp} from "@guardian/cdk"; import {AccessScope} from "@guardian/cdk/lib/constants"; import {InstanceClass, InstanceSize, InstanceType, Vpc} from "aws-cdk-lib/aws-ec2"; import {policies} from "./policies"; import {GuVpc} from "@guardian/cdk/lib/constructs/ec2"; import {AutoScalingAction} from "aws-cdk-lib/aws-cloudwatch-actions"; import {AdjustmentType, StepScalingAction} from "aws-cdk-lib/aws-autoscaling"; import {Alarm, ComparisonOperator, Metric, TreatMissingData} from "aws-cdk-lib/aws-cloudwatch"; import {ApplicationProtocol, ListenerAction} from "aws-cdk-lib/aws-elasticloadbalancingv2"; export class PodcastsRss 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/content-aws.guardianapis/${this.stage}/hostedzoneid`); const app = new GuPlayApp(this, { access: { scope: AccessScope.PUBLIC, }, app: "podcasts-rss", applicationLogging: { enabled: true, }, certificateProps: { domainName: this.stage==="CODE" ? "podcast-feed.content.code.dev-guardianapis.com" : "podcast-feed.content-aws.guardianapis.com", hostedZoneId: hostedZone, }, instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), monitoringConfiguration: { snsTopicName: urgentAlarmTopicArn.stringValue, http5xxAlarm: { tolerated5xxPercentage: 20, numberOfMinutesAboveThresholdBeforeAlarm: 2, }, unhealthyInstancesAlarm: true, }, privateSubnets: deploymentSubnets, publicSubnets: deploymentSubnets, roleConfiguration: { additionalPolicies: [policies(this)], }, scaling: { minimumInstances: 2, maximumInstances: 20, }, userData: { distributable: { fileName: "podcasts-rss_1.0_all.deb", executionStatement: "dpkg -i /podcasts-rss/podcasts-rss_1.0_all.deb" } }, vpc, }); app.loadBalancer.addListener("HttpToHttps", { protocol: ApplicationProtocol.HTTP, port: 80, defaultAction: ListenerAction.forward([app.targetGroup]) }); const cpuHighAlarm = new Alarm(this, "HighCPU", { actionsEnabled: true, alarmDescription: "CPU utilization alarm for autoscaling", comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, datapointsToAlarm: 5, evaluationPeriods: 5, metric: new Metric({ dimensionsMap: { AutoScalingGroupName: app.autoScalingGroup.autoScalingGroupName, }, metricName: "CPUUtilization", namespace: "AWS/EC2", period: Duration.minutes(1), statistic: "Average", }), threshold: 50, treatMissingData: TreatMissingData.MISSING, }); const scaleUpStep = new StepScalingAction(this, "ScaleUp", { adjustmentType: AdjustmentType.PERCENT_CHANGE_IN_CAPACITY, autoScalingGroup: app.autoScalingGroup, cooldown: Duration.minutes(5), }); scaleUpStep.addAdjustment({ lowerBound: 0, adjustment: 100 }); cpuHighAlarm.addAlarmAction(new AutoScalingAction(scaleUpStep)); const scaleDownStep = new StepScalingAction(this, "ScaleDown", { adjustmentType: AdjustmentType.CHANGE_IN_CAPACITY, autoScalingGroup: app.autoScalingGroup, cooldown: Duration.minutes(5), }); scaleDownStep.addAdjustment({ lowerBound: 0, adjustment: -1, }); cpuHighAlarm.addOkAction(new AutoScalingAction(scaleDownStep)); } 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") } }