UNPKG

@studion/infra-code-blocks

Version:
398 lines (397 loc) 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EcsService = void 0; const pulumi = require("@pulumi/pulumi"); const aws = require("@pulumi/aws"); const common_tags_1 = require("../../shared/common-tags"); const merge_with_defaults_1 = require("../../shared/merge-with-defaults"); const resolve_aws_region_1 = require("../../shared/resolve-aws-region"); const policies_1 = require("./policies"); const task_size_1 = require("./task-size"); /** * 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 = { deploymentController: 'ECS', desiredCount: 1, size: 'small', environment: [], secrets: [], volumes: [], enableServiceAutoDiscovery: false, assignPublicIp: false, taskExecutionRoleInlinePolicies: [], taskRoleInlinePolicies: [], autoscaling: { enabled: false, minCount: 1, maxCount: 1, }, }; class EcsService extends pulumi.ComponentResource { name; vpc; logGroup; taskDefinition; taskExecutionRole; taskRole; service; securityGroups; serviceDiscoveryService; persistentStorage; constructor(name, args, opts = {}) { super('studion:ecs-service:EcsService', name, {}, { ...opts, aliases: [...(opts.aliases || []), { type: 'studion:ecs:Service' }], }); const argsWithDefaults = (0, merge_with_defaults_1.mergeWithDefaults)(defaults, args); const region = (0, resolve_aws_region_1.resolveAwsRegion)(argsWithDefaults, this); const taskExecutionRoleInlinePolicies = pulumi.output(args.taskExecutionRoleInlinePolicies || defaults.taskExecutionRoleInlinePolicies); const taskRoleInlinePolicies = pulumi.output(args.taskRoleInlinePolicies || defaults.taskRoleInlinePolicies); this.name = name; this.securityGroups = []; this.vpc = pulumi.output(argsWithDefaults.vpc); this.logGroup = this.createLogGroup(argsWithDefaults.logGroupNamePrefix); this.taskExecutionRole = this.createTaskExecutionRole(taskExecutionRoleInlinePolicies); this.taskRole = this.createTaskRole(taskRoleInlinePolicies); this.persistentStorage = pulumi .output(argsWithDefaults.volumes) .apply(volumes => volumes.length ? this.createPersistentStorage(this.vpc) : undefined); this.taskDefinition = this.createTaskDefinition(argsWithDefaults.containers, pulumi.output(argsWithDefaults.volumes), this.taskExecutionRole, this.taskRole, argsWithDefaults.family, argsWithDefaults.size, { ...common_tags_1.commonTags, ...argsWithDefaults.tags }, region); if (argsWithDefaults.enableServiceAutoDiscovery) { this.serviceDiscoveryService = this.createServiceDiscovery(); } this.service = this.createEcsService(argsWithDefaults); if (argsWithDefaults.autoscaling.enabled) { this.enableAutoscaling(pulumi.output(argsWithDefaults.cluster).name, this.service.name, argsWithDefaults.autoscaling.minCount, argsWithDefaults.autoscaling.maxCount); } this.registerOutputs(); } static createTcpPortMapping(port) { return { containerPort: port, hostPort: port, protocol: 'tcp', }; } createLogGroup(namePrefix) { const logGroup = new aws.cloudwatch.LogGroup(`${this.name}-log-group`, { retentionInDays: 14, namePrefix: namePrefix ?? `/ecs/${this.name}-`, tags: common_tags_1.commonTags, }, { parent: this }); return logGroup; } createTaskDefinition(containers, volumes, taskExecutionRole, taskRole, family, size, tags, region) { const stack = pulumi.getStack(); const { cpu, memory } = pulumi.output(size).apply(task_size_1.parseTaskSize); const containerDefinitions = containers.map(container => { return this.createContainerDefinition(container, region); }); const taskDefinitionVolumes = this.createTaskDefinitionVolumes(volumes); return new aws.ecs.TaskDefinition(`${this.name}-task-definition`, { family: family ?? `${this.name}-task-definition-${stack}`, networkMode: 'awsvpc', executionRoleArn: taskExecutionRole.arn, taskRoleArn: taskRole.arn, cpu, memory, requiresCompatibilities: ['FARGATE'], containerDefinitions: pulumi.jsonStringify(containerDefinitions), volumes: taskDefinitionVolumes, tags: { ...common_tags_1.commonTags, ...tags }, }, { parent: this }); } createTaskDefinitionVolumes(volumes) { return pulumi .all([volumes, this.persistentStorage]) .apply(([volumes, storage]) => { if (!volumes.length || !storage) return; return volumes.map(volume => ({ name: pulumi.output(volume).name, efsVolumeConfiguration: { fileSystemId: storage.fileSystem.id, transitEncryption: 'ENABLED', authorizationConfig: { accessPointId: storage.accessPoint.id, iam: 'ENABLED', }, }, })); }); } createContainerDefinition(container, region) { return pulumi .all([this.logGroup.name, region]) .apply(([logGroupName, region]) => ({ ...container, readonlyRootFilesystem: false, ...(container.mountPoints && { mountPoints: container.mountPoints.map(mountPoint => pulumi .all([ mountPoint.sourceVolume, mountPoint.containerPath, mountPoint.readOnly, ]) .apply(([sourceVolume, containerPath, readOnly]) => ({ containerPath, sourceVolume, readOnly: readOnly ?? false, }))), }), logConfiguration: { logDriver: 'awslogs', options: { 'awslogs-group': logGroupName, 'awslogs-region': region, 'awslogs-stream-prefix': 'ecs', }, }, })); } createTaskExecutionRole(inlinePolicies) { 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}-task-exec-role`, { namePrefix: `${this.name}-task-exec-role-`, assumeRolePolicy: policies_1.assumeRolePolicy, managedPolicyArns: [ 'arn:aws:iam::aws:policy/CloudWatchFullAccess', 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess', ], inlinePolicies: inlinePolicies.apply(policies => [ secretManagerSecretsInlinePolicy, ...policies, ]), tags: common_tags_1.commonTags, }, { parent: this }); return taskExecutionRole; } createTaskRole(inlinePolicies) { const execCmdInlinePolicy = { name: `${this.name}-exec`, policy: JSON.stringify({ Version: '2012-10-17', Statement: [ { Sid: 'AllowContainerToCreateECSExecSSMChannel', Effect: 'Allow', Action: [ 'ssmmessages:CreateControlChannel', 'ssmmessages:CreateDataChannel', 'ssmmessages:OpenControlChannel', 'ssmmessages:OpenDataChannel', ], Resource: '*', }, ], }), }; return new aws.iam.Role(`${this.name}-task-role`, { namePrefix: `${this.name}-task-role-`, assumeRolePolicy: policies_1.assumeRolePolicy, inlinePolicies: inlinePolicies.apply(policies => [ execCmdInlinePolicy, ...policies, ]), tags: common_tags_1.commonTags, }, { parent: this }); } addSecurityGroup(securityGroup) { this.securityGroups.push(pulumi.output(securityGroup)); } createDefaultSecurityGroup() { const securityGroup = new aws.ec2.SecurityGroup(`${this.name}-service-security-group`, { vpcId: this.vpc.vpcId, ingress: [ { fromPort: 0, toPort: 0, protocol: '-1', cidrBlocks: [this.vpc.vpc.cidrBlock], }, ], egress: [ { fromPort: 0, toPort: 0, protocol: '-1', cidrBlocks: ['0.0.0.0/0'], }, ], tags: common_tags_1.commonTags, }, { parent: this }); this.addSecurityGroup(securityGroup); } createEcsService(ecsServiceArgs) { if (ecsServiceArgs.securityGroup) { this.addSecurityGroup(ecsServiceArgs.securityGroup); } if (!this.securityGroups.length) { this.createDefaultSecurityGroup(); } const networkConfiguration = { assignPublicIp: ecsServiceArgs.assignPublicIp, subnets: ecsServiceArgs.assignPublicIp ? this.vpc.publicSubnetIds : this.vpc.privateSubnetIds, securityGroups: pulumi .all(this.securityGroups) .apply(groups => groups.map(it => it.id)), }; const dependsOn = this.persistentStorage.apply(s => s?.mountTargets ?? pulumi.output([])); return new aws.ecs.Service(`${this.name}-service`, { name: ecsServiceArgs.name ?? this.name, cluster: pulumi.output(ecsServiceArgs.cluster).id, launchType: 'FARGATE', deploymentController: { type: ecsServiceArgs.deploymentController }, desiredCount: ecsServiceArgs.desiredCount, taskDefinition: this.taskDefinition.arn, enableExecuteCommand: true, networkConfiguration, ...(ecsServiceArgs.loadBalancers && { loadBalancers: ecsServiceArgs.loadBalancers, }), ...(this.serviceDiscoveryService && { serviceRegistries: { registryArn: this.serviceDiscoveryService.arn, }, }), tags: { ...common_tags_1.commonTags, ...ecsServiceArgs.tags }, }, { parent: this, dependsOn }); } createServiceDiscovery() { const privateDnsNamespace = this.createPrivateDnsNameSpace(); return new aws.servicediscovery.Service(`${this.name}-service-discovery`, { name: this.name, dnsConfig: { namespaceId: privateDnsNamespace.id, dnsRecords: [ { ttl: 10, type: 'A', }, ], routingPolicy: 'MULTIVALUE', }, tags: common_tags_1.commonTags, }, { parent: this }); } createPrivateDnsNameSpace() { return new aws.servicediscovery.PrivateDnsNamespace(`${this.name}-private-dns-namespace`, { vpc: this.vpc.vpcId, name: this.name, tags: common_tags_1.commonTags, }, { parent: this }); } enableAutoscaling(clusterName, serviceName, minCount, maxCount) { const autoscalingTarget = new aws.appautoscaling.Target(`${this.name}-autoscale-target`, { minCapacity: minCount, maxCapacity: maxCount, resourceId: pulumi.interpolate `service/${clusterName}/${serviceName}`, serviceNamespace: 'ecs', scalableDimension: 'ecs:service:DesiredCount', tags: common_tags_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 }); } createPersistentStorage(vpc) { 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: { ...common_tags_1.commonTags, Name: `${this.name}-data`, }, }, { parent: this }); const securityGroup = new aws.ec2.SecurityGroup(`${this.name}-persistent-storage-security-group`, { vpcId: vpc.vpcId, ingress: [ { fromPort: 2049, toPort: 2049, protocol: 'tcp', cidrBlocks: [vpc.vpc.cidrBlock], }, ], tags: common_tags_1.commonTags, }, { parent: this }); const mountTargets = this.vpc.privateSubnetIds.apply(subnetIds => subnetIds.map(subnetId => new aws.efs.MountTarget(`${this.name}-mount-target-${subnetId}`, { fileSystemId: efs.id, subnetId, securityGroups: [securityGroup.id], }, { parent: this }))); const accessPoint = new aws.efs.AccessPoint(`${this.name}-efs-ap`, { fileSystemId: efs.id, 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, }, }, }, { parent: this }); return { fileSystem: efs, accessPoint, mountTargets }; } } exports.EcsService = EcsService;