From 38a1d33d8ad9139d49bc15662e6d674f49658757 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Thu, 29 Aug 2024 23:03:34 +0800 Subject: [PATCH] refactor(form-ecs): extract into own construct --- lib/constructs/form-ecs.ts | 152 ++++++++++++++++++++++++++++ lib/constructs/virus-scanner-ecs.ts | 2 +- lib/formsg-on-cdk-stack.ts | 116 +++++---------------- 3 files changed, 180 insertions(+), 90 deletions(-) create mode 100644 lib/constructs/form-ecs.ts diff --git a/lib/constructs/form-ecs.ts b/lib/constructs/form-ecs.ts new file mode 100644 index 0000000..6481007 --- /dev/null +++ b/lib/constructs/form-ecs.ts @@ -0,0 +1,152 @@ +import { Construct } from 'constructs' +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import { FormsgS3Buckets } from './s3'; +import { ApplicationLoadBalancer, ApplicationProtocol } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { LogGroup } from 'aws-cdk-lib/aws-logs'; +import { Duration } from 'aws-cdk-lib'; + + +export class FormEcs extends Construct { + readonly service: ecs.FargateService + readonly hostname: string + + constructor( + scope: Construct, + { + cluster, + logGroupSuffix, + s3Buckets, + environment, + secrets, + loadBalancer, + } : { + cluster: ecs.Cluster; + logGroupSuffix: string; + s3Buckets: FormsgS3Buckets; + environment: Record; + secrets: Record; + loadBalancer: ApplicationLoadBalancer + } + ) { + super(scope, 'form') + const port = 5000 + const taskDefinition = new ecs.FargateTaskDefinition(this, 'task') + taskDefinition + .addContainer('web', { + image: ecs.ContainerImage.fromRegistry('opengovsg/formsg-intl'), + containerName: 'web', + environment, + secrets, + logging: ecs.LogDriver.awsLogs({ + logGroup: new LogGroup(this, 'cloudwatch', { + logGroupName: `/aws/ecs/logs/form/${logGroupSuffix}`, + }), + streamPrefix: 'form', + }), + portMappings: [ + { containerPort: port, hostPort: port }, + ], + }) + taskDefinition.addToTaskRolePolicy( + new PolicyStatement({ + actions: [ + 's3:PutObject', + 's3:GetObject', + 's3:DeleteObject', + 's3:PutObjectAcl', + ], + resources: [ + s3Buckets.s3Attachment, + s3Buckets.s3Image, + s3Buckets.s3Logo, + ].map(({ bucketArn }) => `${bucketArn}/*`), + }) + ) + + taskDefinition.addToTaskRolePolicy( + new PolicyStatement({ + actions: [ + 's3:PutObject', + 's3:GetObject', + ], + resources: [`${s3Buckets.s3PaymentProof.bucketArn}/*`], + }) + ) + + taskDefinition.addToTaskRolePolicy( + new PolicyStatement({ + actions: [ + 's3:PutObject', + ], + resources: [`${s3Buckets.s3VirusScannerQuarantine.bucketArn}/*`], + }) + ) + + taskDefinition.addToTaskRolePolicy( + new PolicyStatement({ + actions: [ + 's3:GetObjectVersion', + ], + resources: [`${s3Buckets.s3VirusScannerClean.bucketArn}/*`], + }) + ) + + taskDefinition.addToTaskRolePolicy( + new PolicyStatement({ + actions: [ + 's3:GetObject', + 's3:GetObjectTagging', + 's3:GetObjectVersion', + 's3:DeleteObject', + 's3:DeleteObjectVersion', + ], + resources: [`${s3Buckets.s3VirusScannerQuarantine.bucketArn}/*`], + }) + ) + taskDefinition.addToTaskRolePolicy( + new PolicyStatement({ + actions: [ + 's3:PutObject', + 's3:PutObjectTagging', + ], + resources: [`${s3Buckets.s3VirusScannerClean.bucketArn}/*`], + }) + ) + + const service = new ecs.FargateService(this, 'service', { + cluster, + taskDefinition, + }) + + const listener = loadBalancer.addListener('alb-listener', { + port: 80, + protocol: ApplicationProtocol.HTTP, + }) + + const scaling = service.autoScaleTaskCount({ maxCapacity: 2 }) + scaling.scaleOnCpuUtilization('scaling', { + targetUtilizationPercent: 50, + scaleInCooldown: Duration.seconds(60), + scaleOutCooldown: Duration.seconds(60), + }) + + service.registerLoadBalancerTargets({ + containerName: 'web', + containerPort: port, + listener: ecs.ListenerConfig.applicationListener( + listener, + { + protocol: ApplicationProtocol.HTTP, + port, + healthCheck: { + healthyHttpCodes: ['200', '403'].join(',') + } + }, + ), + newTargetGroupId: 'ecs', + }) + + this.service = service + } +} diff --git a/lib/constructs/virus-scanner-ecs.ts b/lib/constructs/virus-scanner-ecs.ts index ef1004a..fd71e22 100644 --- a/lib/constructs/virus-scanner-ecs.ts +++ b/lib/constructs/virus-scanner-ecs.ts @@ -34,7 +34,7 @@ export class VirusScannerEcs extends Construct { cpu: 1024, }) taskDefinition - .addContainer('task-container', { + .addContainer('web', { image: ecs.ContainerImage.fromRegistry('opengovsg/lambda-virus-scanner:latest-ecs'), containerName: 'web', environment: { diff --git a/lib/formsg-on-cdk-stack.ts b/lib/formsg-on-cdk-stack.ts index 0e5895e..1851b67 100644 --- a/lib/formsg-on-cdk-stack.ts +++ b/lib/formsg-on-cdk-stack.ts @@ -2,20 +2,18 @@ import * as cdk from 'aws-cdk-lib' import { Construct } from 'constructs' import * as ecs from 'aws-cdk-lib/aws-ecs' -import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns' import * as ec2 from 'aws-cdk-lib/aws-ec2' import { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2' import { Secret } from 'aws-cdk-lib/aws-secretsmanager' -import { PolicyStatement } from 'aws-cdk-lib/aws-iam' import { AllowedMethods, CachePolicy, Distribution, OriginProtocolPolicy, OriginRequestPolicy, ViewerProtocolPolicy } from 'aws-cdk-lib/aws-cloudfront' import { LoadBalancerV2Origin } from 'aws-cdk-lib/aws-cloudfront-origins' import { FormsgS3Buckets } from './constructs/s3' import { FormsgEcr } from './constructs/ecr' -import defaultEnvironment from './formsg-env-vars' -import { LogGroup } from 'aws-cdk-lib/aws-logs' +import defaultEnvironment from './formsg-env-vars'; import { OriginVerify } from '@alma-cdk/origin-verify' import { VirusScannerEcs } from './constructs/virus-scanner-ecs' +import { FormEcs } from './constructs/form-ecs' export class FormsgOnCdkStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { @@ -210,6 +208,18 @@ export class FormsgOnCdkStack extends cdk.Stack { }, }) + // Create Session Secret + const sessionSecret = ecs.Secret.fromSecretsManager( + new Secret(this, 'session-secret', { + secretName: 'session-secret', + removalPolicy: cdk.RemovalPolicy.DESTROY, + generateSecretString: { + excludePunctuation: true, + excludeCharacters: "/¥'%:{}", + }, + }) + ) + const s3Suffix = suffixSecret.secretValue.unsafeUnwrap() const s3Buckets = new FormsgS3Buckets(this, { s3Suffix, origin: distributionUrl }) @@ -245,100 +255,28 @@ export class FormsgOnCdkStack extends cdk.Stack { VIRUS_SCANNER_LAMBDA_ENDPOINT: `http://${virusScanner.hostname}`, } - // Create Session Secret - const sessionSecret = ecs.Secret.fromSecretsManager( - new Secret(this, 'session-secret', { - secretName: 'session-secret', - removalPolicy: cdk.RemovalPolicy.DESTROY, - generateSecretString: { - excludePunctuation: true, - excludeCharacters: "/¥'%:{}", - }, - }) - ) + const secrets = { + DB_HOST: dbHostString, + SESSION_SECRET: sessionSecret, + SES_USER: sesUserSecret, + SES_PASS: sesPassSecret, + GOOGLE_CAPTCHA: googleCaptchaSecret, + } - // Create Fargate Service - const fargate = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'app', { + const form = new FormEcs(this, { cluster, - taskImageOptions: { - image: ecs.ContainerImage.fromRegistry('opengovsg/formsg-intl'), - environment, - secrets: { - DB_HOST: dbHostString, - SESSION_SECRET: sessionSecret, - SES_USER: sesUserSecret, - SES_PASS: sesPassSecret, - GOOGLE_CAPTCHA: googleCaptchaSecret, - }, - logDriver: ecs.LogDriver.awsLogs({ - logGroup: new LogGroup(this, 'cloudwatch', { - logGroupName: `/aws/ecs/logs/form/${logGroupSuffix}`, - }), - streamPrefix: 'form', - }), - containerPort: 5000, - }, + logGroupSuffix, + s3Buckets, + environment, + secrets, loadBalancer, - healthCheckGracePeriod: cdk.Duration.seconds(0), }) - const scaling = fargate.service.autoScaleTaskCount({ maxCapacity: 2 }) - scaling.scaleOnCpuUtilization('scaling', { - targetUtilizationPercent: 50, - scaleInCooldown: cdk.Duration.seconds(60), - scaleOutCooldown: cdk.Duration.seconds(60), - }) - fargate.service.connections.securityGroups.forEach((securityGroup) => { + form.service.connections.securityGroups.forEach((securityGroup) => { dbSecurityGroup.addIngressRule(securityGroup, ec2.Port.tcp(27017)) }) - ;[ - s3Buckets.s3Attachment, - s3Buckets.s3Image, - s3Buckets.s3Logo, - ].forEach(({ bucketArn }) => - fargate.taskDefinition.addToTaskRolePolicy( - new PolicyStatement({ - actions: [ - 's3:PutObject', - 's3:GetObject', - 's3:DeleteObject', - 's3:PutObjectAcl', - ], - resources: [`${bucketArn}/*`], - }) - ) - ) - - fargate.taskDefinition.addToTaskRolePolicy( - new PolicyStatement({ - actions: [ - 's3:PutObject', - 's3:GetObject', - ], - resources: [`${s3Buckets.s3PaymentProof.bucketArn}/*`], - }) - ) - - fargate.taskDefinition.addToTaskRolePolicy( - new PolicyStatement({ - actions: [ - 's3:PutObject', - ], - resources: [`${s3Buckets.s3VirusScannerQuarantine.bucketArn}/*`], - }) - ) - - fargate.taskDefinition.addToTaskRolePolicy( - new PolicyStatement({ - actions: [ - 's3:GetObjectVersion', - ], - resources: [`${s3Buckets.s3VirusScannerClean.bucketArn}/*`], - }) - ) - new cdk.CfnOutput(this, 'url', { value: cloudFront.distributionDomainName }) } }