Skip to content

Repository files navigation

AWS ECS Multi-Tier Application Reference Architecture

This is a complete, production-grade reference implementation demonstrating a real-world AWS deployment architecture with infrastructure-as-code, CI/CD automation, and containerized multi-tier services.

Reference Implementation: This repository contains example application code and placeholder AWS values. Customize all configuration values for your production environment.

Architecture Overview

AWS Infrastructure (Terraform)

┌─────────────────────────────────────────────────────────────────┐
│                         AWS Account                              │
│                                                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                   Route53 + ACM                          │   │
│  │  • Hosted Zone: staging.example.com                    │   │
│  │  • Wildcard ACM Certificate: *.staging.example.com     │   │
│  │  • DNS A Records: api.staging, staging.example.com     │   │
│  └──────────────────┬───────────────────────────────────────┘   │
│                     │ HTTPS (443)                                │
│  ┌──────────────────▼───────────────────────────────────────┐   │
│  │     Application Load Balancer (ALB)                      │   │
│  │  • Public Internet Facing                               │   │
│  │  • HTTP→HTTPS Redirect                                  │   │
│  │  • Host-based routing (API vs Frontend)                 │   │
│  └──────────┬─────────────────────────────┬────────────────┘   │
│             │                             │                     │
│    ┌────────▼────────┐        ┌──────────▼──────────┐          │
│    │  API Target     │        │ Frontend Target     │          │
│    │  Group (TCP     │        │ Group (TCP 3000)   │          │
│    │  8000)          │        │                     │          │
│    └────────┬────────┘        └──────────┬──────────┘          │
│             │                             │                     │
│  ┌──────────▼──────────────────────────────▼──────────┐         │
│  │        ECS Cluster (Fargate)                        │        │
│  │                                                     │        │
│  │  ┌────────────────────────────────────────────┐   │        │
│  │  │ API Service (FastAPI)                      │   │        │
│  │  │ • Fargate Launch Type                      │   │        │
│  │  │ • Desired Count: 2 (HA)                    │   │        │
│  │  │ • CPU: 512, Memory: 1024 MiB              │   │        │
│  │  │ • Auto Scaling Policy                      │   │        │
│  │  └────────────────────────────────────────────┘   │        │
│  │                                                     │        │
│  │  ┌────────────────────────────────────────────┐   │        │
│  │  │ Worker Service (Celery)                    │   │        │
│  │  │ • Fargate Launch Type                      │   │        │
│  │  │ • Desired Count: 1                         │   │        │
│  │  │ • CPU: 512, Memory: 1024 MiB              │   │        │
│  │  │ • Background Job Processing                │   │        │
│  │  └────────────────────────────────────────────┘   │        │
│  │                                                     │        │
│  │  ┌────────────────────────────────────────────┐   │        │
│  │  │ Frontend Service (Express)                 │   │        │
│  │  │ • Fargate Launch Type                      │   │        │
│  │  │ • Desired Count: 1                         │   │        │
│  │  │ • CPU: 512, Memory: 1024 MiB              │   │        │
│  │  │ • Static Web Serving                       │   │        │
│  │  └────────────────────────────────────────────┘   │        │
│  │                                                     │        │
│  └─────────────┬──────────────────┬──────────────────┘         │
│                │                  │                             │
│    ┌───────────▼──┐      ┌────────▼────────┐                   │
│    │ ECR Repos    │      │ CloudWatch Logs │                   │
│    │ • API        │      │ • /ecs/api      │                   │
│    │ • Worker     │      │ • /ecs/worker   │                   │
│    │ • Frontend   │      │ • /ecs/frontend │                   │
│    └──────────────┘      └─────────────────┘                   │
│                                                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Private Subnets (Data Tier)                 │   │
│  │                                                           │   │
│  │  ┌─────────────────────┐      ┌──────────────────────┐  │   │
│  │  │  RDS PostgreSQL     │      │ ElastiCache Redis    │  │   │
│  │  │  • db.t4g.micro     │      │ • cache.t4g.micro    │  │   │
│  │  │  • Multi-AZ capable │      │ • Single node (dev)   │  │   │
│  │  │  • 20 GiB storage   │      │ • Port 6379           │  │   │
│  │  │  • Backups: 7 days  │      │ • Broker + Cache     │  │   │
│  │  └─────────────────────┘      └──────────────────────┘  │   │
│  │                                                           │   │
│  │  ┌──────────────────────────────────────────────────┐   │   │
│  │  │       VPC Security Groups                        │   │   │
│  │  │ • ALB SG: 80/443 from Internet                  │   │   │
│  │  │ • ECS SG: 8000/3000 from ALB                    │   │   │
│  │  │ • RDS SG: 5432 from ECS                         │   │   │
│  │  │ • Redis SG: 6379 from ECS                       │   │   │
│  │  └──────────────────────────────────────────────────┘   │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Repository Structure

aws-ecs-reference/
├── backend/                          # Python FastAPI + Celery services
│   ├── app/
│   │   ├── main.py                  # FastAPI application
│   │   ├── celery_app.py            # Celery task queue
│   │   └── tasks.py                 # Background tasks
│   ├── tests/
│   │   └── test_health.py           # API tests
│   ├── requirements.txt             # Python dependencies
│   ├── Dockerfile                   # Dev container
│   ├── Dockerfile.prod              # Production API container
│   └── Dockerfile.celery.prod       # Production worker container
│
├── frontend/                         # Express.js web service
│   ├── src/
│   │   └── server.js                # Express server
│   ├── package.json                 # Node.js dependencies
│   ├── Dockerfile                   # Dev container
│   └── Dockerfile.prod              # Production container
│
├── infra/terraform/                 # AWS Infrastructure as Code
│   ├── main.tf                      # Core ECS, ALB, RDS, ElastiCache
│   ├── variables.tf                 # Terraform variables
│   ├── outputs.tf                   # Terraform outputs
│   ├── versions.tf                  # Provider versions
│   ├── terraform.staging.tfvars.example       # Staging variables
│   ├── terraform.prod.tfvars.example         # Production variables
│   ├── backend-staging.hcl.example           # Staging S3 backend
│   └── backend-prod.hcl.example              # Production S3 backend
│
├── deploy/task-definitions/         # ECS Task Definition Templates
│   ├── api.json.tmpl                # API task definition
│   ├── worker.json.tmpl             # Worker task definition
│   └── frontend.json.tmpl           # Frontend task definition
│
├── scripts/                          # Deployment automation
│   ├── deploy-ecs-local.sh          # ECS deployment script
│   ├── .env.deploy.staging.example  # Staging deployment env
│   └── .env.deploy.prod.example     # Production deployment env
│
├── .github/workflows/               # GitHub Actions CI/CD (manual-only)
│   ├── lint.yml                     # Code linting
│   ├── test.yml                     # Unit tests
│   └── deploy.yml                   # ECS deployment
│
├── docker-compose.yaml              # Local development stack
├── .env.backend.example             # Backend env template
├── .env.frontend.example            # Frontend env template
├── README.md                         # This file
├── AWS_DEPLOYMENT.md                # AWS deployment guide
└── .gitignore                       # Git ignore rules

Quick Start: Local Development

Prerequisites

  • Docker and Docker Compose
  • Python 3.13+ (for local testing)
  • Node.js 22+ (for frontend development)

Run Locally with Docker Compose

  1. Clone and navigate:

    git clone <repo-url> aws-ecs-ref
    cd aws-ecs-ref
  2. Copy environment templates:

    cp .env.backend.example .env.backend
    cp .env.frontend.example .env.frontend
  3. Start all services (Docker Compose starts: PostgreSQL, Redis, API, Worker, Frontend):

    docker compose up --build
  4. Access services:

  5. Stop services:

    docker compose down

Local Service Details

Service Port Tech Stack Purpose
PostgreSQL 5432 PostgreSQL 17 Primary relational database
Redis 6379 Redis 7 Celery broker and cache
API 8000 FastAPI + Uvicorn REST API backend
Worker Celery (background) Async job processing
Frontend 3000 Express.js Web application

AWS Deployment

Prerequisites for AWS Deployment

  • AWS Account with appropriate permissions (VPC, RDS, ElastiCache, ECS, ECR, IAM)
  • Existing VPC and Subnets (public and private subnets in at least 2 AZs)
  • AWS CLI configured with credentials
  • Terraform >= 1.6.0
  • Docker with Buildx support for multi-platform builds
  • GitHub Repository with OIDC role configured (for GitHub Actions CI/CD)

Step 1: Prepare Terraform Variables

  1. Copy Terraform staging variables:

    cd infra/terraform
    cp terraform.staging.tfvars.example terraform.staging.tfvars
  2. Fill in your AWS details in terraform.staging.tfvars:

    aws_region          = "us-east-1"
    vpc_id              = "vpc-your-vpc-id"
    public_subnet_ids   = ["subnet-public-1", "subnet-public-2"]
    private_subnet_ids  = ["subnet-private-1", "subnet-private-2"]
    domain_name         = "staging.your-domain.com"      # Your actual domain
    db_password         = "your-secure-password"
    create_route53_zone = false                           # Set to true if Route53 zone doesn't exist
  3. Repeat for production:

    cp terraform.prod.tfvars.example terraform.prod.tfvars
    # Edit with production domain, AWS details

Step 2: Initialize Terraform State

  1. Create S3 bucket and DynamoDB table for Terraform state (run once per AWS account):

    aws s3 mb s3://myapp-terraform-state-staging --region us-east-1
    aws dynamodb create-table \
      --table-name myapp-terraform-locks \
      --attribute-definitions AttributeName=LockID,AttributeType=S \
      --key-schema AttributeName=LockID,KeyType=HASH \
      --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
      --region us-east-1
  2. Copy and configure backend state:

    cp backend-staging.hcl.example backend-staging.hcl
    # Edit backend-staging.hcl with your S3 bucket and DynamoDB table names
  3. Initialize Terraform:

    terraform init -backend-config=backend-staging.hcl -reconfigure

Step 3: Deploy AWS Infrastructure with Terraform

  1. Plan the deployment:

    terraform plan -var-file=terraform.staging.tfvars
  2. Apply infrastructure:

    terraform apply -var-file=terraform.staging.tfvars
  3. Capture outputs for deployment script:

    terraform output -json > /tmp/staging-outputs.json

    Key outputs you'll need:

    • ecr_api_repository_url
    • ecr_worker_repository_url
    • ecr_frontend_repository_url
    • ecs_cluster_name
    • ecs_api_service_name
    • ecs_worker_service_name
    • ecs_frontend_service_name
    • rds_endpoint
    • redis_endpoint
    • Relevant ARNs and security group IDs

Step 4: Deploy Services to ECS

  1. Copy and configure deployment environment:

    cd ../..
    cp scripts/.env.deploy.staging.example scripts/.env.deploy.staging
  2. Fill in values from Terraform outputs:

    # Edit scripts/.env.deploy.staging with:
    export AWS_REGION="us-east-1"
    export ECR_API_REPOSITORY_URI="<from terraform output>"
    export ECS_CLUSTER="<from terraform output>"
    # ... etc (see .env.deploy.staging.example for all variables)
  3. Source the environment and deploy:

    source scripts/.env.deploy.staging
    ./scripts/deploy-ecs-local.sh

    The script will:

    • Build and push Docker images to ECR
    • Register ECS task definitions
    • Run database migrations
    • Update ECS services
  4. Verify deployment:

    # Check service status
    aws ecs describe-services \
      --cluster "$ECS_CLUSTER" \
      --services "$ECS_API_SERVICE" "$ECS_WORKER_SERVICE" "$ECS_FRONTEND_SERVICE" \
      --region us-east-1
    
    # Check logs
    aws logs tail "/ecs/myapp-staging-api" --follow --region us-east-1

AWS Architecture Components

Load Balancer (ALB):

  • Internet-facing Application Load Balancer
  • HTTP 80 → HTTPS 301 redirect
  • Host-based routing rules:
    • api.staging.example.com → API target group
    • staging.example.com → Frontend target group

Containers (ECS Fargate):

  • API Service: 2 tasks (HA), 512 CPU, 1 GiB memory
  • Worker Service: 1 task, 512 CPU, 1 GiB memory
  • Frontend Service: 1 task, 512 CPU, 1 GiB memory

Database (RDS PostgreSQL):

  • db.t4g.micro instance (free tier eligible)
  • 20 GiB storage, 7-day backups
  • Private subnet (no public access)
  • Security group allows only ECS access

Cache (ElastiCache Redis):

  • cache.t4g.micro node (free tier eligible)
  • Single node for development
  • Celery broker and session store
  • Private subnet, security group allows only ECS access

DNS & TLS:

  • Route53 hosted zone (created or existing)
  • ACM certificate for staging domain and wildcard
  • DNS validation via Route53 records
  • HTTPS listener on ALB

Logging:

  • CloudWatch Log Groups for each service (30-day retention)
  • Structured logs with Docker JSON driver

ECR Image Registries:

  • Private registries for API, Worker, Frontend images
  • Automatic image cleanup (old images deleted)

CI/CD with GitHub Actions

Key Features

All workflows are manual-only (no automatic triggers on push/PR):

  1. Lint Workflow (lint.yml)

    • Static code analysis (Ruff for Python)
    • Frontend linting
    • Trigger: Manual via GitHub Actions UI
  2. Test Workflow (test.yml)

    • Runs API tests against temporary PostgreSQL and Redis
    • Trigger: Manual via GitHub Actions UI
  3. Deploy Workflow (deploy.yml)

    • Builds and pushes Docker images to ECR
    • Registers ECS task definitions
    • Updates ECS services
    • Requires GitHub OIDC role for AWS credentials
    • Trigger: Manual via GitHub Actions UI (select staging or prod)

Setting Up GitHub Actions for AWS Deployment

  1. Create GitHub OIDC Identity Provider in AWS:

    aws iam create-open-id-connect-provider \
      --url https://token.actions.githubusercontent.com \
      --client-id-list sts.amazonaws.com \
      --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
  2. Create GitHub Actions IAM Role:

    # Create trust policy (trust-policy.json)
    cat > trust-policy.json <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
          },
          "Action": "sts:AssumeRoleWithWebIdentity",
          "Condition": {
            "StringEquals": {
              "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
              "token.actions.githubusercontent.com:sub": "repo:your-github-org/aws-ecs-reference:ref:refs/heads/main"
            }
          }
        }
      ]
    }
    EOF
    
    # Create role
    aws iam create-role \
      --role-name github-actions-myapp-deploy \
      --assume-role-policy-document file://trust-policy.json
  3. Attach deployment policy to role:

    # Create policy with permissions for ECR, ECS, CloudWatch
    cat > deploy-policy.json <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "ecr:GetAuthorizationToken",
            "ecr:BatchCheckLayerAvailability",
            "ecr:PutImage",
            "ecr:InitiateLayerUpload",
            "ecr:UploadLayerPart",
            "ecr:CompleteLayerUpload"
          ],
          "Resource": "*"
        },
        {
          "Effect": "Allow",
          "Action": [
            "ecs:RegisterTaskDefinition",
            "ecs:UpdateService",
            "ecs:DescribeServices"
          ],
          "Resource": "*"
        },
        {
          "Effect": "Allow",
          "Action": "iam:PassRole",
          "Resource": [
            "arn:aws:iam::ACCOUNT_ID:role/myapp-*-ecs-task-execution",
            "arn:aws:iam::ACCOUNT_ID:role/myapp-*-ecs-task"
          ]
        }
      ]
    }
    EOF
    
    aws iam put-role-policy \
      --role-name github-actions-myapp-deploy \
      --policy-name myapp-deploy-policy \
      --policy-document file://deploy-policy.json
  4. Add GitHub repository secrets (in GitHub UI):

    AWS_DEPLOY_ROLE_ARN = arn:aws:iam::ACCOUNT_ID:role/github-actions-myapp-deploy
    
  5. Add GitHub repository variables (in GitHub UI):

    AWS_REGION = us-east-1
    

Environment Configuration

Local Development (Docker Compose)

.env.backend:

  • APP_NAME: Application name
  • DEBUG: Enable debug mode (true for local)
  • DATABASE_HOST: db (Docker Compose service name)
  • REDIS_URL: redis://redis:6379/0

.env.frontend:

  • NODE_ENV: development
  • PORT: 3000
  • API_URL: http://api:8000 (Docker Compose service name)

AWS Staging Deployment

scripts/.env.deploy.staging:

  • AWS ECR repository URIs (from Terraform outputs)
  • ECS cluster and service names (from Terraform outputs)
  • RDS endpoint and credentials
  • ElastiCache Redis endpoint
  • Domain names for APIs (staging.example.com, api.staging.example.com)

AWS Production Deployment

scripts/.env.deploy.prod:

  • Same structure as staging
  • Production domain names
  • Production ECR registry IDs
  • Production RDS and Redis endpoints

Staging vs Production Configuration

Aspect Staging Production
Domain staging.example.com example.com
API Endpoint https://api.staging.example.com https://api.example.com
ACM Certificate Wildcard: *.staging.example.com Wildcard: *.example.com
ECS Desired Count (API) 2 (HA) 2-4 (can scale)
RDS Instance db.t4g.micro db.t3.small+
Redis Node cache.t4g.micro cache.t3.small+
Log Retention 30 days 90 days
Deployment Via scripts/.env.deploy.staging Via scripts/.env.deploy.prod

Configuration Values

Before deploying to AWS, customize all of the following placeholder values:

Terraform Variables:

  • domain_name: Change from staging.example.com to your actual domain
  • vpc_id: Replace vpc-0123456789abcdef with your VPC ID
  • public_subnet_ids / private_subnet_ids: Replace with your actual subnet IDs
  • db_password: Replace with a secure password
  • AWS account ID in examples: 111122223333 → your actual AWS account ID

Deployment Environment:

  • ECR Repository URIs
  • RDS endpoint and credentials
  • Redis endpoint
  • ECS cluster/service names
  • Domain names and URLs
  • AWS IAM role ARNs

GitHub Secrets:

  • AWS_DEPLOY_ROLE_ARN: Update with your actual role ARN

Application Secrets (not in repo):

  • NEXTAUTH_SECRET (or your auth token)
  • API keys for external services
  • Stripe, SendGrid, Cloudinary credentials (if used)

Security Notes

  1. Never commit filled environment files – Only commit .example templates
  2. Use AWS Secrets Manager for production secrets instead of env files
  3. Enable MFA on AWS account and GitHub
  4. Restrict RDS to ECS security group only
  5. Rotate database passwords regularly
  6. Use IAM roles instead of hardcoded credentials
  7. Enable CloudTrail for AWS API audit logging
  8. Enable VPC Flow Logs for network troubleshooting

Troubleshooting

Local Development

Port already in use:

# Kill process on port 8000
lsof -ti:8000 | xargs kill -9

# Or change ports in docker-compose.yaml

PostgreSQL connection refused:

# Wait for PostgreSQL to become healthy
docker compose logs db

Redis connection timeout:

# Check Redis is running
docker compose logs redis

AWS Deployment

ECS task fails to start:

# Check CloudWatch logs
aws logs tail "/ecs/myapp-staging-api" --follow

# Check task details
aws ecs describe-tasks --cluster myapp-staging-cluster --tasks <task-arn> --region us-east-1

ALB health check fails:

  • Verify API is responding on 8000: curl http://localhost:8000/health/
  • Check security group allows ALB → ECS traffic
  • Verify ALLOWED_HOSTS includes the domain

ACM certificate pending validation:

  • Ensure Route53 records are created
  • Run terraform apply again to create validation records
  • Wait for DNS propagation (up to 15 minutes)

License

This reference implementation is provided as-is for use as a template for your own deployments.

About

Enterprise-grade AWS ECS Fargate reference architecture featuring Terraform IaC, FastAPI microservices, Celery workers, PostgreSQL, Redis, GitHub Actions CI/CD, ALB routing, ACM TLS, Route53, and production deployment workflows.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages