UNPKG

@studion/infra-code-blocks

Version:
358 lines (357 loc) 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EcsService = exports.assumeRolePolicy = exports.awsRegion = void 0; const pulumi = require("@pulumi/pulumi"); const aws = require("@pulumi/aws"); const constants_1 = require("../constants"); const config = new pulumi.Config('aws'); exports.awsRegion = config.require('region'); exports.assumeRolePolicy = { Version: '2012-10-17', Statement: [ { Action: 'sts:AssumeRole', Principal: { Service: 'ecs-tasks.amazonaws.com', }, Effect: 'Allow', Sid: '', }, ], }; /** * Standard directory permissions: * - Owner: read, write, execute (7) * - Group: read, execute (5) * - Others: read, execute (5) */ const STANDARD_DIRECTORY_PERMISSIONS = '0755'; const FIRST_POSIX_NON_ROOT_USER = { userId: 1000, groupId: 1000, permissions: STANDARD_DIRECTORY_PERMISSIONS }; const defaults = { desiredCount: 1, size: 'small', environment: [], secrets: [], enableServiceAutoDiscovery: false, assignPublicIp: false, taskExecutionRoleInlinePolicies: [], taskRoleInlinePolicies: [], autoscaling: { enabled: false, minCount: 1, maxCount: 1, }, }; class EcsService extends pulumi.ComponentResource { constructor(name, args, opts = {}) { super('studion:ecs:Service', name, {}, opts); const argsWithDefaults = Object.assign({}, defaults, args); this.name = name; this.logGroup = this.createLogGroup(); this.taskDefinition = this.createTaskDefinition(args); if (argsWithDefaults.enableServiceAutoDiscovery) { this.serviceDiscoveryService = this.createServiceDiscovery(argsWithDefaults.vpcId); } this.service = this.createEcsService(args, opts); if (argsWithDefaults.autoscaling.enabled) { this.enableAutoscaling(args); } this.registerOutputs(); } createLogGroup() { const logGroup = new aws.cloudwatch.LogGroup(`${this.name}-log-group`, { retentionInDays: 14, namePrefix: `/ecs/${this.name}-`, tags: constants_1.commonTags, }, { parent: this }); return logGroup; } createPersistentStorage({ vpcId, vpcCidrBlock, subnetIds, }) { const efs = new aws.efs.FileSystem(`${this.name}-efs`, { encrypted: true, lifecyclePolicies: [ { transitionToPrimaryStorageClass: 'AFTER_1_ACCESS', }, { transitionToIa: 'AFTER_7_DAYS', }, ], performanceMode: 'generalPurpose', throughputMode: 'bursting', tags: Object.assign(Object.assign({}, constants_1.commonTags), { Name: `${this.name}-data` }), }, { parent: this }); const securityGroup = new aws.ec2.SecurityGroup(`${this.name}-persistent-storage-security-group`, { vpcId: vpcId, ingress: [ { fromPort: 2049, toPort: 2049, protocol: 'tcp', cidrBlocks: [vpcCidrBlock], }, ], tags: constants_1.commonTags, }, { parent: this }); pulumi.all([subnetIds]).apply(([ids]) => { ids.forEach(it => { const mountTarget = new aws.efs.MountTarget(`${this.name}-mount-target-${it}`, { fileSystemId: efs.id, subnetId: it, securityGroups: [securityGroup.id], }, { parent: this }); }); }); return efs; } createTaskDefinition(args) { const argsWithDefaults = Object.assign({}, defaults, args); const stack = pulumi.getStack(); const secretManagerSecretsInlinePolicy = { name: `${this.name}-secret-manager-access`, policy: JSON.stringify({ Version: '2012-10-17', Statement: [ { Sid: 'AllowContainerToGetSecretManagerSecrets', Effect: 'Allow', Action: ['ssm:GetParameters', 'secretsmanager:GetSecretValue'], Resource: '*', }, ], }), }; const taskExecutionRole = new aws.iam.Role(`${this.name}-ecs-task-exec-role`, { namePrefix: `${this.name}-ecs-task-exec-role-`, assumeRolePolicy: exports.assumeRolePolicy, managedPolicyArns: [ 'arn:aws:iam::aws:policy/CloudWatchFullAccess', 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess', ], inlinePolicies: [ secretManagerSecretsInlinePolicy, ...argsWithDefaults.taskExecutionRoleInlinePolicies, ], tags: constants_1.commonTags, }, { parent: this }); const execCmdInlinePolicy = { name: `${this.name}-ecs-exec`, policy: JSON.stringify({ Version: '2012-10-17', Statement: [ { Sid: 'AllowContainerToCreateECSExecSSMChannel', Effect: 'Allow', Action: [ 'ssmmessages:CreateControlChannel', 'ssmmessages:CreateDataChannel', 'ssmmessages:OpenControlChannel', 'ssmmessages:OpenDataChannel', ], Resource: '*', }, ], }), }; const taskRole = new aws.iam.Role(`${this.name}-ecs-task-role`, { namePrefix: `${this.name}-ecs-task-role-`, assumeRolePolicy: exports.assumeRolePolicy, inlinePolicies: [ execCmdInlinePolicy, ...argsWithDefaults.taskRoleInlinePolicies, ], tags: constants_1.commonTags, }, { parent: this }); const parsedSize = pulumi.all([argsWithDefaults.size]).apply(([size]) => { const mapCapabilities = ({ cpu, memory }) => ({ cpu: String(cpu), memory: String(memory), }); if (typeof size === 'string') { return mapCapabilities(constants_1.PredefinedSize[size]); } if (typeof size === 'object') { return mapCapabilities(size); } throw Error('Incorrect EcsService size argument'); }); const fileSystemId = this.createPersistentStorage(argsWithDefaults).id; const accessPoint = new aws.efs.AccessPoint(`${this.name}-efs-ap`, { fileSystemId, posixUser: { uid: FIRST_POSIX_NON_ROOT_USER.userId, gid: FIRST_POSIX_NON_ROOT_USER.groupId, }, rootDirectory: { path: '/data', creationInfo: { ownerUid: FIRST_POSIX_NON_ROOT_USER.userId, ownerGid: FIRST_POSIX_NON_ROOT_USER.groupId, permissions: FIRST_POSIX_NON_ROOT_USER.permissions, }, }, }); const taskDefinition = new aws.ecs.TaskDefinition(`${this.name}-task-definition`, Object.assign(Object.assign({ family: `${this.name}-task-definition-${stack}`, networkMode: 'awsvpc', executionRoleArn: taskExecutionRole.arn, taskRoleArn: taskRole.arn, cpu: parsedSize.cpu, memory: parsedSize.memory, requiresCompatibilities: ['FARGATE'], containerDefinitions: pulumi .all([ this.name, argsWithDefaults.image, argsWithDefaults.port, argsWithDefaults.environment, argsWithDefaults.secrets, argsWithDefaults.persistentStorageConfig, argsWithDefaults.dockerCommand, this.logGroup.name, exports.awsRegion, ]) .apply(([containerName, image, port, environment, secrets, persistentStorageConfig, command, logGroup, region,]) => { return JSON.stringify([ Object.assign(Object.assign({ readonlyRootFilesystem: false, name: containerName, image, essential: true, portMappings: [ { containerPort: port, protocol: 'tcp', }, ] }, (persistentStorageConfig && { mountPoints: persistentStorageConfig.mountPoints.map(mountPoint => { var _a; return ({ containerPath: mountPoint.containerPath, sourceVolume: mountPoint.sourceVolume, readOnly: (_a = mountPoint.readOnly) !== null && _a !== void 0 ? _a : false, }); }), })), { logConfiguration: { logDriver: 'awslogs', options: { 'awslogs-group': logGroup, 'awslogs-region': region, 'awslogs-stream-prefix': 'ecs', }, }, command, environment, secrets }), ]); }) }, (argsWithDefaults.persistentStorageConfig && { volumes: argsWithDefaults.persistentStorageConfig .volumes .map(volume => ({ name: volume.name, efsVolumeConfiguration: { fileSystemId, transitEncryption: 'ENABLED', authorizationConfig: { accessPointId: accessPoint.id, iam: 'ENABLED', }, } })), })), { tags: Object.assign(Object.assign({}, constants_1.commonTags), argsWithDefaults.tags) }), { parent: this }); return taskDefinition; } createServiceDiscovery(vpcId) { const privateDnsNamespace = new aws.servicediscovery.PrivateDnsNamespace(`${this.name}-private-dns-namespace`, { vpc: vpcId, name: this.name, tags: constants_1.commonTags, }, { parent: this }); return new aws.servicediscovery.Service(`${this.name}-service-discovery`, { name: this.name, dnsConfig: { namespaceId: privateDnsNamespace.id, dnsRecords: [ { ttl: 10, type: 'A', }, ], routingPolicy: 'MULTIVALUE', }, tags: constants_1.commonTags, }, { parent: this }); } createEcsService(args, opts) { const argsWithDefaults = Object.assign({}, defaults, args); const securityGroup = argsWithDefaults.securityGroup || new aws.ec2.SecurityGroup(`${this.name}-service-security-group`, { vpcId: argsWithDefaults.vpcId, ingress: [ { fromPort: 0, toPort: 0, protocol: '-1', cidrBlocks: [argsWithDefaults.vpcCidrBlock], }, ], egress: [ { fromPort: 0, toPort: 0, protocol: '-1', cidrBlocks: ['0.0.0.0/0'], }, ], tags: constants_1.commonTags, }, { parent: this }); const service = new aws.ecs.Service(`${this.name}-service`, Object.assign(Object.assign(Object.assign(Object.assign({ name: this.name, cluster: argsWithDefaults.clusterId, launchType: 'FARGATE', desiredCount: argsWithDefaults.desiredCount, taskDefinition: this.taskDefinition.arn, enableExecuteCommand: true }, (argsWithDefaults.lbTargetGroupArn && { loadBalancers: [ { containerName: this.name, containerPort: argsWithDefaults.port, targetGroupArn: argsWithDefaults.lbTargetGroupArn, }, ], })), { networkConfiguration: { assignPublicIp: argsWithDefaults.assignPublicIp, subnets: argsWithDefaults.subnetIds, securityGroups: [securityGroup.id], } }), (argsWithDefaults.enableServiceAutoDiscovery && this.serviceDiscoveryService && { serviceRegistries: { registryArn: this.serviceDiscoveryService.arn, }, })), { tags: Object.assign(Object.assign({}, constants_1.commonTags), argsWithDefaults.tags) }), { parent: this, dependsOn: opts.dependsOn, }); return service; } enableAutoscaling(args) { const argsWithDefaults = Object.assign({}, defaults, args); const autoscalingTarget = new aws.appautoscaling.Target(`${this.name}-autoscale-target`, { minCapacity: argsWithDefaults.autoscaling.minCount, maxCapacity: argsWithDefaults.autoscaling.maxCount, resourceId: pulumi.interpolate `service/${argsWithDefaults.clusterName}/${this.service.name}`, serviceNamespace: 'ecs', scalableDimension: 'ecs:service:DesiredCount', tags: constants_1.commonTags, }, { parent: this }); const memoryAutoscalingPolicy = new aws.appautoscaling.Policy(`${this.name}-memory-autoscale-policy`, { policyType: 'TargetTrackingScaling', resourceId: autoscalingTarget.resourceId, scalableDimension: autoscalingTarget.scalableDimension, serviceNamespace: autoscalingTarget.serviceNamespace, targetTrackingScalingPolicyConfiguration: { predefinedMetricSpecification: { predefinedMetricType: 'ECSServiceAverageMemoryUtilization', }, targetValue: 70, }, }, { parent: this }); const cpuAutoscalingPolicy = new aws.appautoscaling.Policy(`${this.name}-cpu-autoscale-policy`, { policyType: 'TargetTrackingScaling', resourceId: autoscalingTarget.resourceId, scalableDimension: autoscalingTarget.scalableDimension, serviceNamespace: autoscalingTarget.serviceNamespace, targetTrackingScalingPolicyConfiguration: { predefinedMetricSpecification: { predefinedMetricType: 'ECSServiceAverageCPUUtilization', }, targetValue: 70, }, }, { parent: this }); } } exports.EcsService = EcsService;