in packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts [232:453]
private ecs() {
const {
categoryName,
apiName,
policies,
containers,
secretsArns,
taskEnvironmentVariables,
exposedContainer,
taskPorts,
isInitialDeploy,
desiredCount,
currentStackName,
createCloudMapService,
} = this.props;
let cloudMapService: cloudmap.CfnService = undefined;
if (createCloudMapService) {
cloudMapService = new cloudmap.CfnService(this, 'CloudmapService', {
name: apiName,
dnsConfig: {
dnsRecords: [
{
ttl: 60,
type: cloudmap.DnsRecordType.SRV,
},
],
namespaceId: this.cloudMapNamespaceId,
routingPolicy: cloudmap.RoutingPolicy.MULTIVALUE,
},
});
}
const task = new ecs.TaskDefinition(this, 'TaskDefinition', {
compatibility: ecs.Compatibility.FARGATE,
memoryMiB: '1024',
cpu: '512',
family: `${this.envName}-${apiName}`,
});
(task.node.defaultChild as ecs.CfnTaskDefinition).overrideLogicalId('TaskDefinition');
policies.forEach(policy => {
const statement = isPolicyStatement(policy) ? policy : wrapJsonPoliciesInCdkPolicies(policy);
task.addToTaskRolePolicy(statement);
});
const containersInfo: {
container: ecs.ContainerDefinition;
repository: ecr.IRepository;
}[] = [];
containers.forEach(
({
name,
image,
build,
portMappings,
logConfiguration,
environment,
entrypoint: entryPoint,
command,
working_dir: workingDirectory,
healthcheck: healthCheck,
secrets: containerSecrets,
}) => {
const logGroup = new logs.LogGroup(this, `${name}ContainerLogGroup`, {
logGroupName: `/ecs/${this.envName}-${apiName}-${name}`,
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const { logDriver, options: { 'awslogs-stream-prefix': streamPrefix } = {} } = logConfiguration;
const logging: ecs.LogDriver =
logDriver === 'awslogs'
? ecs.LogDriver.awsLogs({
streamPrefix,
logGroup: logs.LogGroup.fromLogGroupName(this, `${name}logGroup`, logGroup.logGroupName),
})
: undefined;
let repository: ecr.IRepository;
if (build) {
const logicalId = `${name}Repository`;
const repositoryName = `${currentStackName}-${categoryName}-${apiName}-${name}`;
if (this.props.existingEcrRepositories.has(repositoryName)) {
repository = ecr.Repository.fromRepositoryName(this, logicalId, repositoryName);
} else {
repository = new ecr.Repository(this, logicalId, {
repositoryName: `${this.envName}-${categoryName}-${apiName}-${name}`,
removalPolicy: cdk.RemovalPolicy.RETAIN,
lifecycleRules: [
{
rulePriority: 10,
maxImageCount: 1,
tagPrefixList: ['latest'],
tagStatus: ecr.TagStatus.TAGGED,
},
{
rulePriority: 100,
maxImageAge: cdk.Duration.days(7),
tagStatus: ecr.TagStatus.ANY,
},
],
});
(repository.node.defaultChild as ecr.CfnRepository).overrideLogicalId(logicalId);
}
// Needed because the image will be pulled from ecr repository later
repository.grantPull(task.obtainExecutionRole());
}
const secrets: ecs.ContainerDefinitionOptions['secrets'] = {};
const environmentWithoutSecrets = environment || {};
containerSecrets.forEach((s, i) => {
if (secretsArns.has(s)) {
secrets[s] = ecs.Secret.fromSecretsManager(ssm.Secret.fromSecretPartialArn(this, `${name}secret${i + 1}`, secretsArns.get(s)));
}
delete environmentWithoutSecrets[s];
});
const container = task.addContainer(name, {
image: repository ? ecs.ContainerImage.fromEcrRepository(repository) : ecs.ContainerImage.fromRegistry(image),
logging,
environment: {
...taskEnvironmentVariables,
...environmentWithoutSecrets,
},
entryPoint,
command,
workingDirectory,
healthCheck: healthCheck && {
command: healthCheck.command,
interval: cdk.Duration.seconds(healthCheck.interval ?? 30),
retries: healthCheck.retries,
timeout: cdk.Duration.seconds(healthCheck.timeout ?? 5),
startPeriod: cdk.Duration.seconds(healthCheck.start_period ?? 0),
},
secrets,
});
containersInfo.push({
container,
repository,
});
// TODO: should we use hostPort too? check network mode
portMappings?.forEach(({ containerPort, protocol, hostPort }) => {
container.addPortMappings({
containerPort,
protocol: ecs.Protocol.TCP,
});
});
},
);
const serviceSecurityGroup = new ec2.CfnSecurityGroup(this, 'ServiceSG', {
vpcId: this.vpcId,
groupDescription: 'Service SecurityGroup',
securityGroupEgress: [
{
description: 'Allow all outbound traffic by default',
cidrIp: '0.0.0.0/0',
ipProtocol: '-1',
},
],
securityGroupIngress: taskPorts.map(servicePort => ({
ipProtocol: 'tcp',
fromPort: servicePort,
toPort: servicePort,
cidrIp: this.vpcCidrBlock,
})),
});
let serviceRegistries: ecs.CfnService.ServiceRegistryProperty[] = undefined;
if (cloudMapService) {
serviceRegistries = [
{
containerName: exposedContainer.name,
containerPort: exposedContainer.port,
registryArn: cloudMapService.attrArn,
},
];
}
const service = new ecs.CfnService(this, 'Service', {
serviceName: `${apiName}-service-${exposedContainer.name}-${exposedContainer.port}`,
cluster: this.clusterName,
launchType: 'FARGATE',
desiredCount: isInitialDeploy ? 0 : desiredCount, // This is later adjusted by the Predeploy action in the codepipeline
networkConfiguration: {
awsvpcConfiguration: {
assignPublicIp: 'ENABLED',
securityGroups: [serviceSecurityGroup.attrGroupId],
subnets: <string[]>this.subnets,
},
},
taskDefinition: task.taskDefinitionArn,
serviceRegistries,
});
new cdk.CfnOutput(this, 'ServiceName', {
value: service.serviceName,
});
new cdk.CfnOutput(this, 'ClusterName', {
value: this.clusterName,
});
return {
service,
serviceSecurityGroup,
containersInfo,
cloudMapService,
};
}