iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🤖

[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.

root/infra/docker/nginx/Dockerfile
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.

https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_core.IStackSynthesizer.html

An ALB and other resources are created to verify access to Nginx, but these may be unnecessary depending on your use case.

root/cdk/lib/stack.ts
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.

References

Discussion