import camelCase from 'camelcase'; import { ITerraformDependable } from 'cdktf'; import { Construct } from 'constructs'; import kebabCase from 'lodash.kebabcase'; import { CloudwatchLogGroup } from '../generated/providers/aws/cloudwatch'; import { EcsCluster, EcsService, EcsTaskDefinition, } from '../generated/providers/aws/ecs'; import { DataAwsIamPolicyDocument, IamPolicy, IamRole, IamRolePolicyAttachment, } from '../generated/providers/aws/iam'; import { SecurityGroup } from '../generated/providers/aws/vpc'; import { getId } from '../utils'; import LoadBalancer from './LoadBalancer'; import Network from './Network'; export type ContainerClusterProps = { network: Network; region: string; port: number; }; export default class ContainerCluster extends Construct { readonly network: Network; readonly clusterId: string; readonly port: number; readonly securityGroup: SecurityGroup; readonly executionRolePolicy: DataAwsIamPolicyDocument; readonly region: string; readonly loadBalancer: LoadBalancer; constructor(scope: Construct, id: string, props: ContainerClusterProps) { super(scope, id); this.network = props.network; this.port = props.port; this.region = props.region; // set the cluster const cluster = new EcsCluster(this, 'cluster', { name: kebabCase(getId('api')), }); this.clusterId = cluster.id; // create a new role for the task execution this.executionRolePolicy = new DataAwsIamPolicyDocument( this, 'exec_policy', { statement: [ { actions: ['sts:AssumeRole'], principals: [ { type: 'Service', identifiers: ['ecs-tasks.amazonaws.com'], }, ], }, ], } ); this.loadBalancer = new LoadBalancer(this, 'lb', { network: this.network }); this.securityGroup = new SecurityGroup(this, 'security_group', { namePrefix: kebabCase(getId('api', 'ecs')), ingress: [ { fromPort: 0, toPort: 0, protocol: '-1', securityGroups: [this.loadBalancer.securityGroup.id], // Only allowing traffic in from the load balancer security group }, ], egress: [ { fromPort: 0, toPort: 0, protocol: '-1', cidrBlocks: ['0.0.0.0/0'], // Allowing traffic out to all IP addresses }, ], lifecycle: { createBeforeDestroy: true, }, }); } setupAPIService({ envVarsArns, envVarsBucketArn, imageUrl, dependsOn, }: { dependsOn?: ITerraformDependable[]; envVarsArns: string[]; envVarsBucketArn: string; imageUrl: string; }): void { const taskExecutionId = getId('api', 'exec'); const taskExecutionRole = new IamRole(this, 'exec_role', { namePrefix: camelCase(taskExecutionId, { pascalCase: true }), assumeRolePolicy: this.executionRolePolicy.json, }); // attach the iam policies to the role new IamRolePolicyAttachment(this, 'exec_role_attach', { role: taskExecutionRole.name, policyArn: 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy', }); // Allow this role to read all s3 files const ecsTaskGetS3EnvPolicyId = getId(taskExecutionId, 's3', 'policy'); const ecsTaskGetS3EnvPolicy = new IamPolicy(this, ecsTaskGetS3EnvPolicyId, { namePrefix: camelCase(ecsTaskGetS3EnvPolicyId, { pascalCase: true }), policy: JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Allow', Action: ['s3:GetBucketLocation'], Resource: [envVarsBucketArn], }, ...envVarsArns.map((envVarsArn) => ({ Effect: 'Allow', Action: ['s3:GetObject'], Resource: [envVarsArn], })), ], }), }); new IamRolePolicyAttachment( this, getId(ecsTaskGetS3EnvPolicyId, 'attach'), { role: taskExecutionRole.name, policyArn: ecsTaskGetS3EnvPolicy.arn, } ); // setup logging for the task const logsGroupId = getId('api', 'logs'); const logsGroup = new CloudwatchLogGroup(this, 'logs', { namePrefix: kebabCase(logsGroupId), retentionInDays: 3, }); const containerTaskDefinitionId = getId('api', 'task'); const containerTaskDefinition = new EcsTaskDefinition( this, containerTaskDefinitionId, { family: kebabCase(containerTaskDefinitionId), containerDefinitions: JSON.stringify([ { logConfiguration: { logDriver: 'awslogs', options: { 'awslogs-group': logsGroup.id, 'awslogs-region': this.region, 'awslogs-stream-prefix': kebabCase(getId('api')), }, }, name: kebabCase(containerTaskDefinitionId), image: imageUrl, essential: true, portMappings: [ { containerPort: this.port, hostPort: this.port, }, ], environmentFiles: envVarsArns.map((envVarsArn) => ({ value: `${envVarsArn}`, type: 's3', })), memory: 1024, cpu: 512, }, ]), requiresCompatibilities: ['FARGATE'], networkMode: 'awsvpc', executionRoleArn: taskExecutionRole.arn, memory: '1024', cpu: '512', dependsOn: dependsOn ?? [], lifecycle: { createBeforeDestroy: true, }, } ); // start ecs service new EcsService(this, 'ecs_service', { name: kebabCase(getId('api')), cluster: this.clusterId, taskDefinition: containerTaskDefinition.arn, launchType: 'FARGATE', desiredCount: process.env.NODE_ENV === 'production' ? 4 : 1, waitForSteadyState: true, healthCheckGracePeriodSeconds: 120, networkConfiguration: { subnets: this.network.subnetIds, assignPublicIp: true, securityGroups: [this.securityGroup.id], }, loadBalancer: [ { targetGroupArn: this.loadBalancer.targetGroup.arn, containerName: containerTaskDefinition.family, containerPort: this.port, }, ], dependsOn: dependsOn ?? [], lifecycle: { createBeforeDestroy: true, }, }); } setupJobQueueWorkers({ envVarsArns, envVarsBucketArn, imageUrl, dependsOn, }: { envVarsArns: string[]; envVarsBucketArn: string; imageUrl: string; dependsOn?: ITerraformDependable[]; }): void { const taskExecutionId = getId('worker', 'exec'); const taskExecutionRole = new IamRole(this, 'worker_role', { namePrefix: camelCase(taskExecutionId, { pascalCase: true }), assumeRolePolicy: this.executionRolePolicy.json, }); // attach the iam policies to the role new IamRolePolicyAttachment(this, 'worker_role_attach', { role: taskExecutionRole.name, policyArn: 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy', }); // Allow this role to read all s3 files const ecsTaskGetS3EnvPolicyId = getId(taskExecutionId, 's3', 'policy'); const ecsTaskGetS3EnvPolicy = new IamPolicy(this, ecsTaskGetS3EnvPolicyId, { namePrefix: camelCase(ecsTaskGetS3EnvPolicyId, { pascalCase: true }), policy: JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Allow', Action: ['s3:GetBucketLocation'], Resource: [envVarsBucketArn], }, ...envVarsArns.map((envVarsArn) => ({ Effect: 'Allow', Action: ['s3:GetObject'], Resource: [envVarsArn], })), ], }), }); new IamRolePolicyAttachment( this, getId(ecsTaskGetS3EnvPolicyId, 'attach'), { role: taskExecutionRole.name, policyArn: ecsTaskGetS3EnvPolicy.arn, } ); // setup logging for the task const logsGroupId = getId('worker', 'logs'); const logsGroup = new CloudwatchLogGroup(this, 'worker_logs', { namePrefix: kebabCase(logsGroupId), retentionInDays: 3, }); const containerTaskDefinitionId = getId('worker', 'task'); const containerTaskDefinition = new EcsTaskDefinition( this, containerTaskDefinitionId, { family: kebabCase(containerTaskDefinitionId), containerDefinitions: JSON.stringify([ { logConfiguration: { logDriver: 'awslogs', options: { 'awslogs-group': logsGroup.id, 'awslogs-region': this.region, 'awslogs-stream-prefix': kebabCase(getId('worker')), }, }, name: kebabCase(containerTaskDefinitionId), image: imageUrl, essential: true, portMappings: [ { containerPort: this.port, hostPort: this.port, }, ], environmentFiles: envVarsArns.map((envVarsArn) => ({ value: `${envVarsArn}`, type: 's3', })), memory: 2048, cpu: 1024, }, ]), requiresCompatibilities: ['FARGATE'], networkMode: 'awsvpc', executionRoleArn: taskExecutionRole.arn, memory: '2048', cpu: '1024', dependsOn: dependsOn ?? [], } ); const securityGroup = new SecurityGroup(this, 'worker_security_group', { namePrefix: kebabCase(getId('worker')), egress: [ { fromPort: 0, toPort: 0, protocol: '-1', cidrBlocks: ['0.0.0.0/0'], // Allowing traffic out to all IP addresses }, ], lifecycle: { createBeforeDestroy: true, }, }); // start ecs service new EcsService(this, 'worker_ecs_service', { name: kebabCase(getId('worker')), cluster: this.clusterId, taskDefinition: containerTaskDefinition.arn, launchType: 'FARGATE', desiredCount: process.env.NODE_ENV === 'production' ? 5 : 1, networkConfiguration: { subnets: this.network.subnetIds, assignPublicIp: true, securityGroups: [securityGroup.id], }, dependsOn: dependsOn ?? [], lifecycle: { createBeforeDestroy: true, }, }); } }