log
Swift Code Chronicles

Building ALB + Fargate with CDK

Published on April 27, 2025
Updated on April 27, 2025
27 min read
AWS

In modern service architectures, containerization and serverless patterns can greatly simplify operational work. This example shows how to deploy a serverless service on Fargate using AWS CDK (v2).

1. Project Structure Overview

Before you begin, make sure you have installed AWS CDK and initialized a TypeScript project. The directory layout might look like this:

my-cdk-project/
├── bin/
│   └── my-cdk-project.ts  // CDK app entry point
├── lib/
│   └── server-stack.ts    // The Stack we'll write next
├── package.json
└── tsconfig.json

2. Core CDK Code

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as log 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);

    // 1. Import existing VPC and Security Groups
    const albSg = ec2.SecurityGroup.fromSecurityGroupId(this, 'alb-sg', 'alb-sg-id');
    const ecsSg = ec2.SecurityGroup.fromSecurityGroupId(this, 'ecs-sg', 'ecs-sg-id');
    const vpc = ec2.Vpc.fromLookup(this, 'vpc', { vpcId: 'vpc-id' });
    const albSubnetA = ec2.Subnet.fromSubnetId(this, 'alb-subnet-a', 'alb-subnet-a-id');
    const albSubnetC = ec2.Subnet.fromSubnetId(this, 'alb-subnet-c', 'alb-subnet-c-id');
    const ecsSubnetA = ec2.Subnet.fromSubnetId(this, 'ecs-subnet-a', 'ecs-subnet-a-id');
    const ecsSubnetC = ec2.Subnet.fromSubnetId(this, 'ecs-subnet-c', 'ecs-subnet-c-id');

    // 2. Import existing IAM roles
    const taskRole = iam.Role.fromRoleArn(this, 'task-role', 'task-role-arn', {
      mutable: false,
    });
    const taskExecutionRole = iam.Role.fromRoleArn(this, 'task-execution-role', 'task-execution-role-arn', {
      mutable: false,
    });

    // 3. Create an ECS cluster
    const cluster = new ecs.Cluster(this, 'ecs-cluster', {
      vpc: vpc,
      clusterName: 'ecs-cluster',
    });

    // 4. Define the Fargate task
    const taskDefinition = new ecs.FargateTaskDefinition(this, 'task-definition', {
      taskRole: taskRole,
      executionRole: taskExecutionRole,
      cpu: 512,
      memoryLimitMiB: 1024,
    });

    // 5. Import the ECR repository
    const repository = ecr.Repository.fromRepositoryArn(this, 'ecr-repository', 'ecr-arn');

    // 6. Create a CloudWatch Logs log group
    const logGroup = new log.LogGroup(this, 'server-log', {
      logGroupName: '/aws/server-log',
      retention: log.RetentionDays.THREE_YEARS,
    });

    // 7. Configure the container image
    taskDefinition.addContainer('task', {
      image: ecs.ContainerImage.fromEcrRepository(repository, 'xxxxx'),
      logging: ecs.LogDriver.awsLogs({
        logGroup: logGroup,
        streamPrefix: 'api',
      }),
      portMappings: [{ containerPort: 80 }],
      environment: {
        NODE_ENV: 'it',
      },
      readonlyRootFilesystem: true,
    });

    // 8. Deploy the Fargate service
    const service = new ecs.FargateService(this, 'service', {
      serviceName: 'server_name',
      cluster: cluster,
      taskDefinition: taskDefinition,
      securityGroups: [ecsSg],
      vpcSubnets: { subnets: [ecsSubnetA, ecsSubnetC] },
    });

    // 9. Create an Application Load Balancer (ALB)
    const alb = new elbv2.ApplicationLoadBalancer(this, `alb`, {
      loadBalancerName: 'alb',
      securityGroup: albSg,
      vpc: vpc,
      vpcSubnets: { subnets: [albSubnetA, albSubnetC] },
    });

    // 10. Create a Target Group with health checks
    const targetGroup = new elbv2.ApplicationTargetGroup(this, 'alb-tg', {
      targetGroupName: '',
      vpc: vpc,
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targetType: elbv2.TargetType.IP,
      crossZoneEnabled: true,
      healthCheck: {
        path: '/health',
        healthyHttpCodes: '200',
      },
      targets: [service],
    });

    // 11. Add a listener and routing rule
    const listener = alb.addListener('alb-listener', {
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      open: false,
      defaultAction: elbv2.ListenerAction.fixedResponse(404, { contentType: 'application/json' }),
    });

    new elbv2.CfnListenerRule(this, 'server-rule', {
      listenerArn: listener.listenerArn,
      priority: 1,
      conditions: [
        {
          field: 'path-pattern',
          values: ['/api/*'], // Only forward requests matching this path to Fargate
        },
      ],
      actions: [{ type: 'forward', targetGroupArn: targetGroup.targetGroupArn }],
    });
  }
}

3. Conclusion

With this example, you can quickly reuse existing VPCs, subnets, security groups, IAM roles, ECR repositories, and log groups, and deploy an entire stack with a single CDK command:

  1. High Availability: Multi-AZ load balancing + serverless Fargate
  2. Security: Fine-grained security groups, read-only container filesystem
  3. Observability: Real-time logging with CloudWatch Logs

You can extend this template to support HTTPS, auto-scaling, blue-green deployments, and other production-grade features. Feel free to share your optimizations 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