Building an ECS Fargate Service with AWS CDK
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:
- A VPC (with public and private subnets and a private security group) has already been created.
- ECSTaskExecutionRole and ECSTaskRole have already been created.
- An ECR repository has already been created.
- 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!