DEV Community

Clariza Look
Clariza Look

Posted on

Building Serverless Application with AWS CDK: ECS Fargate & DynamoDB

AWS offers a powerful combination of services that enable developers to build serverless architectures without managing underlying infrastructure. In this guide, we'll explore how to build a robust serverless application using AWS CDK with ECS Fargate and DynamoDB.

Why This Architecture Matters

The serverless paradigm has transformed how we think about application deployment. By combining ECS Fargate for containerized compute with DynamoDB for managed NoSQL storage, we create an architecture that scales automatically, reduces operational overhead, and optimizes costs.

The AWS CDK (Cloud Development Kit) makes this entire setup reproducible and maintainable through code.

Github Repository Example to Build Serverless Application with AWS CDK: ECS Fargate & DynamoDB

Architecture Deep Dive

Our serverless architecture follows a clean, layered approach that separates concerns while maintaining high availability:

             ┌───────────────────────────────────┐
             │      Internet Users               │
             └───────────────────────────────────┘
                             │
                             ▼
             ┌───────────────────────────────────┐
             │  Application Load Balancer (ALB)  │
             │       - Listens on Port 80        │
             │       - Routes to ECS Service     │
             └───────────────────────────────────┘
                             │
                             ▼
     ┌─────────────────────────────────────────────┐
     │          ECS Fargate Service (2 Tasks)      │
     │ ┌─────────────────────────────────────────┐ │
     │ │  Task Definition                        │ │
     │ │  - Nginx Container (Port 3000)          │ │
     │ │  - Logs to AWS CloudWatch               │ │
     │ │  - Reads/Writes to DynamoDB             │ │
     │ └─────────────────────────────────────────┘ │
     └─────────────────────────────────────────────┘
                             │
                             ▼
           ┌───────────────────────────────────┐
           │        DynamoDB Table             │
           │  - Stores messages data           │
           │  - PAY_PER_REQUEST mode           │
           └───────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Traffic Flow Breakdown

Request Journey: Internet users send requests to the Application Load Balancer, which intelligently distributes traffic across multiple ECS Fargate tasks running in different availability zones. Each task runs an Nginx container that processes requests and interacts with DynamoDB for data persistence.

Scaling Mechanism: The ECS service automatically adjusts the number of running tasks based on CPU and memory utilization, while DynamoDB scales read/write capacity on-demand, ensuring consistent performance regardless of load.

Technology Stack Explained

AWS CDK (TypeScript)

The AWS CDK serves as our infrastructure-as-code foundation. Unlike traditional CloudFormation templates, CDK allows us to define cloud resources using familiar programming languages, making infrastructure more maintainable and testable.

Key Benefits:

  • Type safety and IDE support
  • Reusable constructs and patterns
  • Automatic CloudFormation generation
  • Built-in best practices and security

Amazon ECS Fargate

Fargate eliminates the need to provision and manage EC2 instances for containerized applications. It provides a serverless compute engine that automatically handles infrastructure scaling, patching, and security.

Why Fargate Over Lambda?

  • Longer execution times (no 15-minute limit)
  • Full control over runtime environment
  • Better suited for HTTP services and APIs
  • Container-based deployment flexibility

Amazon DynamoDB

As a fully managed NoSQL database, DynamoDB provides single-digit millisecond latency at any scale. The pay-per-request billing model aligns perfectly with serverless principles.

DynamoDB Advantages:

  • Automatic scaling without downtime
  • Built-in security with encryption at rest
  • Global secondary indexes for flexible querying
  • Point-in-time recovery and backups

Project Structure and Organization

We are using AWS Cloud Development Kit (CDK) to deploy our infrastructure to AWS. The structure looks like below:

├── bin/                    # CDK entry point
├── lib/                    # CDK Stack Definition
│   ├── ecs-construct-example-stack.ts  # Main CDK Stack
├── cdk.json               # CDK Configuration
├── package.json           # Dependencies
├── tsconfig.json          # TypeScript Configuration
└── README.md             # Project Documentation
Enter fullscreen mode Exit fullscreen mode

The structure follows CDK best practices, separating the application entry point (bin/) from the actual infrastructure definitions (lib/). This organization makes the codebase maintainable and allows for easy extension with additional stacks or constructs.

Deployment Process

Prerequisites Setup

Before deploying, ensure the development environment is properly configured (in the terminal):

# Install Node.js 18.x or higher
node --version

# Configure AWS CLI with appropriate permissions
aws configure

# Install AWS CDK globally
npm install -g aws-cdk

# Bootstrap CDK (first-time setup)
cdk bootstrap

# Verify CDK installation
cdk --version
Enter fullscreen mode Exit fullscreen mode

To setup the first CDK template, follow the steps in CDK Setup template to get the CDK file structure format similar to the above.

Deployment Steps

1. Updating the bin folder

Update the file from the bin folder with ecs-construct-example.ts. It contains the entry point files for your CDK application. These are the files that get run when you execute CDK commands like cdk deploy or cdk synth.

As you can see from the script below, it is importing the files from the lib folder called EcsFargateStack.

// bin/ecs-construct-example.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { EcsFargateStack } from '../lib/ecs-construct-example-stack';

const app = new cdk.App();

new EcsFargateStack(app, 'EcsFargateStack', {
  projectName: 'SimpleEcs',
  environment: 'Dev',
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
    }
});

Enter fullscreen mode Exit fullscreen mode

2. Creating the stack definition in the lib folder

Let's go to the lib folder and update one of the files with lib/ecs-construct-example-stack.ts.

Github Reference for the lib folder to setup the ecs fargate stack

// in lib/ecs-construct-example-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { RemovalPolicy, CfnOutput } from 'aws-cdk-lib';
import * as elasticloadbalancing from 'aws-cdk-lib/aws-elasticloadbalancingv2';

interface ECSStackProps extends cdk.StackProps {
  projectName: string;
  environment: string;
}

export class EcsFargateStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: ECSStackProps) {
    super(scope, id, props);

    const projectName = props.projectName;
    const projectPrefix = `${projectName}-${props.environment}-server`;

    // Create a Dynamo db table
    const dynamoTable = new dynamodb.Table(this, 'DynamoDbMessages', {
      partitionKey: {
        name: 'id',
        type: dynamodb.AttributeType.STRING
      }, 
      sortKey: {
        name: 'created_at',
        type: dynamodb.AttributeType.NUMBER
      },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: RemovalPolicy.DESTROY, 
    });

    new CfnOutput(this, 'TableName', { value: dynamoTable.tableName });

    /******** 
     * 1. Create a vpc for the ecs or Can also reference a vpc if 
     * it has been created prior this deployment
    *********/
    const vpc = new ec2.Vpc(this, "Vpc", {
      vpcName:'primary-vpc',
      natGateways: 1,
      subnetConfiguration: [{  
          cidrMask: 24, subnetType: ec2.SubnetType.PUBLIC, name: "Public" },
        {  
          cidrMask: 24, subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, name: "Private"}],
      maxAzs: 2 
    });

    // Create a security group that allows HTTP traffic on port 80 for the ECS container
    const ecsSecGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', {
      securityGroupName: `${projectName}-${props.environment}-SecGroup`,
      vpc,
      allowAllOutbound: false,
    });

    ecsSecGroup.addIngressRule(ec2.Peer.ipv4('0.0.0.0/0'), ec2.Port.tcp(3000));

    // Create cluster
    const cluster = new ecs.Cluster(this, `ECS-Cluster`, {
      clusterName: `${projectPrefix}-cluster`,
      vpc,
    });

    const executionRolePolicy =  new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      resources: ['*'],
      actions: [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ]
    });

    // Create a fargate task definition 
    const fargateTaskDefinition = new ecs.FargateTaskDefinition(
      this, 'FargateTaskDefinition', {
      memoryLimitMiB: 512,
      cpu: 256,
    });

fargateTaskDefinition.addToExecutionRolePolicy(executionRolePolicy);

   fargateTaskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      resources: [dynamoTable.tableArn],
      actions: ['dynamodb:*']
    }));

    const ecsContainer = fargateTaskDefinition.addContainer('NginxServerContainer', {
      image: ecs.ContainerImage.fromRegistry('nginx:latest'),
      cpu: 100,
      memoryLimitMiB: 256,
      essential: true,
      logging: ecs.LogDrivers.awsLogs({streamPrefix: 'nginx-ecs-server'}),
      environment: { 
        'DYNAMODB_MESSAGES_TABLE': dynamoTable.tableName,
        'APP_ID' : 'nginx-ecs-server'
      }
    });

    ecsContainer.addPortMappings({
      containerPort: 3000,
      protocol: ecs.Protocol.TCP,
    });

    // Create the service
    const ecsFargateService = new ecs.FargateService(this, 'Ecs-Service', {
      cluster,
      taskDefinition: fargateTaskDefinition,
      desiredCount: 2,
      assignPublicIp: false,
      securityGroups: [ecsSecGroup],
    });

    const lb = new elasticloadbalancing.ApplicationLoadBalancer(this, 'ALB', {
      vpc,
      internetFacing: true
    });

    const albListener = lb.addListener('AlbListener', {
      port: 80,
    });

    albListener.addTargets('Target', {
      port: 80,
      targets: [ecsFargateService],
      healthCheck: { path: '/api/' }
    });

    albListener.connections.allowDefaultPortFromAnyIpv4('Open to all');
  }
}
Enter fullscreen mode Exit fullscreen mode

This ECS Fargate stack sets up a load-balanced, containerized web application running on ECS Fargate with DynamoDB backend storage.

  • DynamoDB Table - Creates a table for messages with partition key 'id' and sort key 'created_at'
  • VPC Setup - Creates a new VPC with public and private subnets across 2 availability zones
  • Security Group - Allows inbound traffic on port 3000 for the ECS containers
  • ECS Cluster - Sets up a Fargate cluster to run containerized applications
  • Task Definition - Configures a Fargate task with 512 MiB memory and 256 CPU units
  • Container - Runs an nginx:latest container with DynamoDB table name as environment variable
  • ECS Service - Deploys 2 instances of the task in private subnets without public IPs
  • Load Balancer - Creates an internet-facing ALB that routes traffic to the ECS service on port 80
  • IAM Permissions - Grants the task execution role ECR/CloudWatch permissions and task role DynamoDB access

3. Deploy

We first install the node dependencies and then test the cdk

# Install project dependencies
npm install

# Preview changes before deployment
cdk diff

# Deploy the complete infrastructure
cdk deploy
Enter fullscreen mode Exit fullscreen mode

The deployment process creates a comprehensive serverless infrastructure including VPC networking, security groups, IAM roles, and all necessary AWS resources. The entire process typically completes within 10-15 minutes.

Real-World Use Cases

1. Chat API Backend

This architecture excels as a microservices backend for real-time messaging applications. The ALB efficiently routes WebSocket connections and HTTP requests to Fargate tasks, while DynamoDB stores messages, user sessions, and conversation metadata.

Implementation Flow:

  • Users send messages through the frontend application
  • ALB routes requests to available ECS tasks
  • Nginx processes the requests and validates user authentication
  • Message data gets stored in DynamoDB with timestamps and metadata
  • Real-time updates are pushed back to connected clients

2. Serverless Web Application Backend

Perfect for supporting modern frontend frameworks like React, Angular, or Vue.js, this architecture provides a scalable API layer that handles user authentication, data processing, and business logic.

Typical API Endpoints:

  • /api/users - User management and profiles
  • /api/messages - Message CRUD operations
  • /api/analytics - Dashboard metrics and reporting
  • /api/health - System health monitoring

3. Analytics Dashboard System

The architecture supports complex analytical workloads by leveraging DynamoDB's querying capabilities and ECS Fargate's processing power for data aggregation and report generation.

Security and Best Practices

This structure follows best practices for:

Network Security

  • VPC isolation with public and private subnets
  • Security groups restricting access to necessary ports only
  • ALB SSL termination for encrypted communication

IAM and Access Control

  • Least privilege access principles
  • Task-specific IAM roles for ECS services
  • DynamoDB access limited to required operations only

Monitoring and Observability

  • CloudWatch integration for comprehensive logging
  • Application-level metrics and health checks
  • Automated alerting for performance anomalies

Cost Optimization

This serverless architecture optimizes costs through several mechanisms:

Pay-per-use pricing: DynamoDB's on-demand billing and Fargate's per-second billing ensure you only pay for actual resource consumption.

Automatic scaling: Resources scale down during low-traffic periods, significantly reducing costs compared to always-on infrastructure.

No idle capacity: Unlike traditional server-based approaches, there's no wasted capacity during off-peak hours.

Conclusion

The beauty of this architecture lies in its simplicity to deploy yet powerful enough to handle production workloads. Whether you're building a startup MVP or scaling an enterprise application, this serverless foundation provides the flexibility and reliability needed for modern cloud applications.

Start by cloning the repository, following the deployment steps, and customizing the Nginx container to serve your specific application logic. The modular CDK structure makes it easy to extend with additional AWS services like ElastiCache for caching, RDS for relational data, or SNS for notifications.

The future of cloud applications is serverless, and this architecture provides a proven foundation for building scalable, cost-effective solutions that grow with your business needs.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more