log
Swift Code Chronicles

Building an ECS Fargate Service with AWS CDK

Published on January 19, 2025
Updated on January 19, 2025
40 min read
AWS

This article provides a detailed guide on how to build an ECS Fargate-based service using the AWS Cloud Development Kit (CDK). It covers VPC configuration, creating an Application Load Balancer (ALB), task definitions, and setting up auto-scaling.


Project Background

The goal is to deploy a containerized service on AWS ECS Fargate and expose it to the public via an Application Load Balancer (ALB). The service has the following key features:

  • Network security is ensured using a private VPC and security groups.
  • ECR endpoints are configured to securely pull container images from private subnets.
  • Auto-scaling adjusts service instances dynamically based on CPU and memory load.

Prerequisites

Before using this sample code to set up ECS Fargate, the following conditions must be met:

  1. A VPC (with public and private subnets and a private security group) has already been created.
  2. ECSTaskExecutionRole and ECSTaskRole have already been created.
  3. An ECR repository has already been created.
  4. The server code has been packaged into a Docker image and pushed to ECR.

CDK Code

Below is an example of a CDK stack for this setup:

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Cluster, ContainerImage, LogDriver } from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
import { ApplicationLoadBalancer, ApplicationProtocol } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { Role } from 'aws-cdk-lib/aws-iam';
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

export class ServerStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC ID
    const vpcId = 'xxx';

    // Private Security Group ID
    const privateSecurityGroupId = 'xxx';

    // ECS Task Execution Role ARN
    const ecsTaskExecutionRoleArn = 'xxx';

    // ECS Task Role ARN
    const ecsTaskRoleArn = 'xxx';

    // ECR Repository URI
    const ecrRepositoryURI = 'xxxxxx';

    // ECR Image Tag
    const ecrImageTag = 'xxxxxx';

    // -------------------------------------------

    // Retrieve the VPC
    const vpc = ec2.Vpc.fromLookup(this, 'VPC', { vpcId: vpcId });

    // Retrieve the private security group
    const privateSecurityGroup = ec2.SecurityGroup.fromLookupById(this, 'PrivateSecurityGroup', privateSecurityGroupId);

    // Create ECR Docker VPC Endpoint (to pull images from private subnets securely)
    new ec2.InterfaceVpcEndpoint(this, `EcrDkrEndpoint`, {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
      subnets: { subnets: vpc.privateSubnets },
      securityGroups: [privateSecurityGroup],
    });

    // Create ECR API Interface Endpoint (to access ECR API from private subnets securely)
    new ec2.InterfaceVpcEndpoint(this, `EcrApiEndpoint`, {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.ECR,
      subnets: { subnets: vpc.privateSubnets },
      securityGroups: [privateSecurityGroup],
    });

    // Create CloudWatch Logs Endpoint
    new ec2.InterfaceVpcEndpoint(this, `EcrLog`, {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
      subnets: { subnets: vpc.privateSubnets },
      securityGroups: [privateSecurityGroup],
    });

    // Retrieve ECS Task Execution Role
    const ecsTaskExecutionRole = Role.fromRoleArn(this, `EcsTaskExecutionRole`, ecsTaskExecutionRoleArn, {
      mutable: false,
    });

    // Retrieve ECS Task Role
    const ecsTaskRole = Role.fromRoleArn(this, `EcsTaskRole`, ecsTaskRoleArn, {
      mutable: false,
    });

    // Create the ALB
    const alb = new ApplicationLoadBalancer(this, `ALB`, {
      vpc: vpc,
      vpcSubnets: { subnets: vpc.publicSubnets },
      internetFacing: true,
      loadBalancerName: `<ALB_NAME>`,
      securityGroup: privateSecurityGroup,
      idleTimeout: cdk.Duration.seconds(60),
    });

    // Create a Log Group
    const ecsLogGroup = new LogGroup(this, `LogGroup`, {
      logGroupName: '<LOG_GROUP_NAME>',
      retention: RetentionDays.ONE_MONTH,
    });

    // Create an ECS Cluster
    const cluster = new Cluster(this, `ECSCluster`, {
      vpc: vpc,
      clusterName: `<CLUSTER_NAME>`,
    });

    // Create ECS Service
    const service = new ApplicationLoadBalancedFargateService(this, `ECSService`, {
      cluster: cluster,
      protocol: ApplicationProtocol.HTTP,
      targetProtocol: ApplicationProtocol.HTTP,
      securityGroups: [privateSecurityGroup],
      listenerPort: 80,
      loadBalancer: alb,
      openListener: false,
      publicLoadBalancer: false,
      taskSubnets: { subnets: vpc.privateSubnets },
      memoryLimitMiB: 1024,
      cpu: 512,
      // Number of tasks to launch by default
      desiredCount: 2,
      // Maintains the minimum percentage of healthy tasks during service updates or scaling operations.
      // For example, if `desiredCount` is 2 and `minHealthyPercent` is 50%, at least 1 healthy task must continue running during the update process.
      minHealthyPercent: 50,
      // Configures the maximum percentage of healthy tasks that can run concurrently during service updates or scaling operations.
      // For example, if `desiredCount` is 2 and `maxHealthyPercent` is 200%, up to 4 tasks (2 current + 2 new) can run concurrently during the update process.
      maxHealthyPercent: 200,
      healthCheckGracePeriod: cdk.Duration.seconds(500),
      taskImageOptions: {
        image: ContainerImage.fromRegistry(`${ecrRepositoryURI}:${ecrImageTag}`),
        logDriver: LogDriver.awsLogs({
          logGroup: ecsLogGroup,
          streamPrefix: 'api',
        }),
        executionRole: ecsTaskExecutionRole,
        taskRole: ecsTaskRole,
        // server port number
        containerPort: 3000,
        // environment vars
        environment: {
          XX: 'XX',
        },
      },
      serviceName: `<SERVICE_NAME>`,
    });

    // Configure Health Check
    service.targetGroup.configureHealthCheck({
      path: '<health check path in your server>',
    });

    // Auto-scaling settings
    const autoScaling = service.service.autoScaleTaskCount({
      minCapacity: 1, // Minimum number of task instances within the ECS service
      maxCapacity: 10, // Maximum number of task instances within the ECS service
    });

    // scaleOnCpuUtilization and scaleOnMemoryUtilization:
    // - Defines auto-scaling rules based on CPU and memory utilization.
    // - When the utilization exceeds the target value (`targetUtilizationPercent: 60`), tasks are automatically scaled out.
    // - When the utilization falls below the target value, tasks are automatically scaled in.
    //
    // scaleInCooldown and scaleOutCooldown:
    // - Configures cooldown periods for scaling in and scaling out to avoid frequent scaling fluctuations.
    // - Setting this to 60 seconds ensures that no new scaling actions occur for at least 60 seconds after the last scaling operation.
    autoScaling.scaleOnCpuUtilization(`ScalingOnCPU`, {
      targetUtilizationPercent: 60,
      scaleInCooldown: cdk.Duration.seconds(60),
      scaleOutCooldown: cdk.Duration.seconds(60),
    });
    autoScaling.scaleOnMemoryUtilization(`ScalingOnMemory`, {
      targetUtilizationPercent: 60,
      scaleInCooldown: cdk.Duration.seconds(60),
      scaleOutCooldown: cdk.Duration.seconds(60),
    });
  }
}

Conclusion

This code demonstrates how to comprehensively configure networking, security, load balancing, logging, and auto-scaling for an ECS Fargate service using AWS CDK. Adopting this Infrastructure as Code (IaC) approach significantly enhances deployment efficiency and consistency.

If you have any questions about implementing or deploying this code, feel free to ask in the comments!

About

A personal blog sharing technical insights, experiences and thoughts

Quick Links

Contact

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 Swift Code Chronicles. All rights reserved