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