iTranslated by AI
[AWS CDK] Building Container Images and Launching Them on ECS Fargate
Overview
In this article, I will introduce code that performs a set of operations within a CDK deployment: building a container image, pushing it directly to ECR, and then launching an ECS service using that image as a single workflow.
For simplicity, this is a sample that just starts Nginx, but the same logic can be applied when containerizing an entire application, so it is reusable.
Environment
Node
node -v
v20.11.1
Excerpt from package.json
{
"dependencies": {
"aws-cdk": "2.127.0",
"aws-cdk-lib": "2.127.0",
"typescript": "~5.3.3"
}
}
Directory Structure
I am assuming a structure like the one shown below.
There are cdk, src (not used this time, but imagine application code being stored here), and infra directories, with the Dockerfile and other files located inside infra.
root
- cdk
- lib
- stack.ts
- src (Application body)
- infra
- docker
- nginx
- Dockerfile
- default.conf
Dockerfile
Regarding the Dockerfile, I am assuming that the context is set to the root directory.
Therefore, the COPY path specification starts from ./infra.
You can specify the context when using Stack.synthesizer.addDockerImageAsset (described later), but caution is required.
FROM nginx:1.25
# Copy default.conf
COPY ./infra/docker/nginx/default.conf /etc/nginx/conf.d/
CDK
Finally, here is the main CDK code.
In Stack.synthesizer.addDockerImageAsset, we specify the path and context of the previously mentioned Dockerfile. Note that the reference point is the path of the stack file (in this case, root/cdk/lib/stack.ts), so you must specify the relative path from there.
An ALB and other resources are created to verify access to Nginx, but these may be unnecessary depending on your use case.
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as path from 'path';
export class MainStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Build an image from the nginx Dockerfile and push it to ECR
const webLocation = this.synthesizer.addDockerImageAsset({
// Specify build context
directoryName: path.join(__dirname, '../..'),
// Specify Dockerfile
dockerFile: path.join(__dirname, '../../infra/docker/nginx/Dockerfile'),
sourceHash: "web",
});
// Create VPC
const vpc = new ec2.Vpc(this, 'MyVPC', {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});
// Create security group
const publicSecurityGroup = new ec2.SecurityGroup(this, 'MyPublicSecurityGroup', {
vpc,
description: 'Allow all inbound and outbound traffic',
allowAllOutbound: true, // Allow all outbound traffic
});
// Add an inbound rule to allow all traffic from any IPv4 address
publicSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.allTraffic(),
'Allow all inbound traffic'
);
// Create ECS cluster
const cluster = new ecs.Cluster(this, 'MyCluster', {
vpc,
enableFargateCapacityProviders: true
});
// Create ECS task definition
const taskDefinition = new ecs.FargateTaskDefinition(this, 'MyTaskDefinition', {
memoryLimitMiB: 3072,
cpu: 1024
});
// Create container
const container = taskDefinition.addContainer('web', {
containerName: "web",
image: ecs.ContainerImage.fromRegistry(webLocation.imageUri),
portMappings: [
{ name: "web-80-tcp", containerPort: 80, hostPort: 80, protocol: ecs.Protocol.TCP, },
],
logging: new ecs.AwsLogDriver({ streamPrefix: 'web' }),
});
// Create ALB
const alb = new elbv2.ApplicationLoadBalancer(this, 'MyALB', {
internetFacing: true,
vpc,
vpcSubnets: {
subnets: vpc.publicSubnets
},
securityGroup: publicSecurityGroup
});
// Create target group
const targetGroup = new elbv2.ApplicationTargetGroup(this, 'MyTargetGroup', {
// Health check settings
healthCheck: {
healthyHttpCodes: '200',
healthyThresholdCount: 5,
interval: Duration.seconds(30),
path: '/',
timeout: Duration.seconds(5),
unhealthyThresholdCount: 2
},
port: 3000,
protocol: elbv2.ApplicationProtocol.HTTP,
targetGroupName: 'my-tg',
vpc,
targetType: elbv2.TargetType.IP
});
// Listener settings for HTTP (80)
alb.addListener('MyHttpListener', {
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
defaultTargetGroups: [targetGroup]
});
// Define the Fargate service referenced by the ALB
const service = new ecs.FargateService(this, 'FargateService', {
cluster,
taskDefinition,
// Attach only PrivateSecurityGroup if you don't want direct access to Fargate
securityGroups: [publicSecurityGroup],
capacityProviderStrategies: [
{
capacityProvider: 'FARGATE',
weight: 1
}
],
assignPublicIp: false
});
service.attachToApplicationTargetGroup(targetGroup);
}
}
Summary
In this article, I introduced code to perform a series of steps using CDK, from building a container image to launching an ECS service.
I used Stack.synthesizer.addDockerImageAsset, but there are other options such as using @aws-cdk/aws-ecr-assets.
Based on a quick look, the usability doesn't seem significantly different, but since that module has many deprecated items and I was concerned about its future, I chose Stack.synthesizer.addDockerImageAsset.
I hope this content is helpful.
Discussion