cdk/lib/printable-recipe-generator.ts (163 lines of code) (raw):

import type { GuStack } from '@guardian/cdk/lib/constructs/core'; import { GuParameter } from '@guardian/cdk/lib/constructs/core'; import { Vpc } from 'aws-cdk-lib/aws-ec2'; import { Repository } from 'aws-cdk-lib/aws-ecr'; import { Cluster, Compatibility, ContainerImage, CpuArchitecture, LaunchType, LogDriver, OperatingSystemFamily, TaskDefinition, } from 'aws-cdk-lib/aws-ecs'; import type { IEventBus } from 'aws-cdk-lib/aws-events'; import { EventField, Rule } from 'aws-cdk-lib/aws-events'; import { EcsTask } from 'aws-cdk-lib/aws-events-targets'; import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal, } from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; interface TestConstructProps { eventBus: IEventBus; } export class PrintableRecipeGenerator extends Construct { constructor(scope: GuStack, id: string, { eventBus }: TestConstructProps) { super(scope, id); const vpcid = new GuParameter(scope, 'VpcIdParam', { fromSSM: true, default: `/account/vpc/primary/id`, }); const publicSubnetIds = new GuParameter(scope, 'VpcPublicParam', { fromSSM: true, default: '/account/vpc/primary/subnets/public', type: 'List<String>', }); const privateSubnetIds = new GuParameter(scope, 'VpcPrivateParam', { fromSSM: true, default: '/account/vpc/primary/subnets/private', type: 'List<String>', }); const availabilityZones = new GuParameter(scope, 'VpcAZParam', { fromSSM: true, default: '/account/vpc/primary/availability-zones', type: 'List<String>', }); const vpc = Vpc.fromVpcAttributes(this, 'VPC', { vpcId: vpcid.valueAsString, publicSubnetIds: publicSubnetIds.valueAsList, privateSubnetIds: privateSubnetIds.valueAsList, availabilityZones: availabilityZones.valueAsList, }); const clusterArnParam = new GuParameter(scope, 'ClusterArn', { fromSSM: true, default: '/account/services/ecs-cluster-name', }); const repoNameParam = new GuParameter(scope, 'RepoName', { fromSSM: true, default: `/${scope.stage}/feast/recipes-backend/ecr-repo-printables`, }); const cluster = Cluster.fromClusterAttributes(this, 'ECSCluster', { clusterName: clusterArnParam.valueAsString, vpc, }); const role = new Role(this, 'IAMRole', { roleName: `printable-recipe-generator-${scope.stage}`, assumedBy: ServicePrincipal.fromStaticServicePrincipleName( 'ecs-tasks.amazonaws.com', ), inlinePolicies: { s3write: new PolicyDocument({ statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: ['s3:PutObject'], resources: [ `arn:aws:s3:::feast-recipes-static-${scope.stage.toLowerCase()}/content/*`, ], }), ], }), }, }); const taskDefinition = new TaskDefinition(this, 'PrintableRecipeGenTD', { compatibility: Compatibility.FARGATE, cpu: '1024', //1 vcpu ephemeralStorageGiB: 25, //minimum 21Gb ephemeral storage memoryMiB: '2048', //2 Gb RAM, minimum value for 1vcpu runtimePlatform: { cpuArchitecture: CpuArchitecture.X86_64, //Chrome headless does not appear to have an ARM64 package at present :-/ operatingSystemFamily: OperatingSystemFamily.LINUX, }, volumes: [{ name: 'tmp-volume' }, { name: 'home-volume' }], taskRole: role, }); const ecrRepo = Repository.fromRepositoryName( this, 'ECRRepo', repoNameParam.valueAsString, ); const imageTag = process.env['IMAGE_TAG'] ?? 'latest'; const container = taskDefinition.addContainer('main', { image: ContainerImage.fromEcrRepository(ecrRepo, imageTag), memoryLimitMiB: 2048, readonlyRootFilesystem: true, logging: LogDriver.awsLogs({ streamPrefix: 'printable-recipe-generator-', }), workingDirectory: '/home/pdfrender', }); container.addMountPoints({ sourceVolume: 'tmp-volume', containerPath: '/tmp', readOnly: false, }); container.addMountPoints({ sourceVolume: 'home-volume', containerPath: '/home/pdfrender', readOnly: false, }); const ruleTarget = new EcsTask({ taskDefinition, cluster, launchType: LaunchType.FARGATE, containerOverrides: [ { containerName: 'main', environment: [ { name: 'RECIPE_UID', value: EventField.fromPath('$.detail.uid'), }, { name: 'RECIPE_CSID', value: EventField.fromPath('$.detail.checksum'), }, { name: 'CONTENT', value: EventField.fromPath('$.detail.blob'), }, { name: 'BUCKET', value: `feast-recipes-static-${scope.stage.toLowerCase()}`, }, ], }, ], }); new Rule(this, 'PublicationConnect', { enabled: scope.stage !== 'PROD', //only enabled in the CODE environment until design work fully implemented, but available for testing in PROD eventBus, targets: [ruleTarget], eventPattern: { source: ['recipe-responder'], detailType: ['recipe-update'], }, }); } }