@studion/infra-code-blocks
Version:
Studion common infra components
398 lines (397 loc) • 16.3 kB
JavaScript
"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;