Skip to main content
AWS ECS (Elastic Container Service) with Fargate allows you to run Docker containers without managing servers. This guide covers creating the cluster, task definitions, and services for the ADMA application.

ECS Architecture Overview

The ADMA deployment uses two ECS services:
  • Backend Service: Spring Boot API (port 8080)
  • Frontend Service: Nginx static site (port 80)
Both services run in private subnets and are accessed through the Application Load Balancer.

Create ECS Cluster

Create a Fargate-only cluster:
aws ecs create-cluster \
  --cluster-name adma-cluster \
  --capacity-providers FARGATE FARGATE_SPOT \
  --region $AWS_REGION
FARGATE_SPOT can reduce costs by up to 70% but may interrupt tasks. Use it for non-critical workloads or in combination with regular Fargate.
Verify the cluster:
aws ecs describe-clusters \
  --clusters adma-cluster \
  --region $AWS_REGION

Create IAM Roles for ECS

ECS tasks require two IAM roles:

Task Execution Role

This role allows ECS to pull images from ECR, write logs to CloudWatch, and read secrets from SSM.
1
Create the Trust Policy
2
cat > /tmp/ecs-trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
3
Create the Role
4
aws iam create-role \
  --role-name ecsTaskExecutionRole \
  --assume-role-policy-document file:///tmp/ecs-trust-policy.json
5
Attach Managed Policies
6
# Allow ECS to pull from ECR and write to CloudWatch
aws iam attach-role-policy \
  --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
7
Add SSM Parameter Access
8
Create an inline policy to read secrets:
9
cat > /tmp/ssm-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameters",
        "ssm:GetParameter"
      ],
      "Resource": [
        "arn:aws:ssm:$AWS_REGION:$ACCOUNT_ID:parameter/adma/prod/*"
      ]
    }
  ]
}
EOF

aws iam put-role-policy \
  --role-name ecsTaskExecutionRole \
  --policy-name SSMParameterAccess \
  --policy-document file:///tmp/ssm-policy.json

Task Role (Optional)

The task role grants permissions to the application code itself (e.g., to access S3, DynamoDB). For ADMA, this is optional.
aws iam create-role \
  --role-name ecsTaskRole \
  --assume-role-policy-document file:///tmp/ecs-trust-policy.json

Create Security Groups

Create security groups for the backend and frontend services:
# Create backend security group
BACKEND_SG=$(aws ec2 create-security-group \
  --group-name adma-backend-sg \
  --description "Security group for ADMA backend ECS tasks" \
  --vpc-id $VPC_ID \
  --region $AWS_REGION \
  --query 'GroupId' \
  --output text)

echo "Backend SG: $BACKEND_SG"

# Allow inbound from ALB (will be configured after ALB creation)
# Placeholder for: aws ec2 authorize-security-group-ingress...

# Allow outbound to RDS
aws ec2 authorize-security-group-egress \
  --group-id $BACKEND_SG \
  --protocol tcp \
  --port 5432 \
  --source-group $RDS_SG \
  --region $AWS_REGION

# Allow outbound HTTPS (for SSM, CloudWatch)
aws ec2 authorize-security-group-egress \
  --group-id $BACKEND_SG \
  --protocol tcp \
  --port 443 \
  --cidr 0.0.0.0/0 \
  --region $AWS_REGION

Create CloudWatch Log Groups

Create log groups for container logs:
# Backend logs
aws logs create-log-group \
  --log-group-name /ecs/adma-backend \
  --region $AWS_REGION

# Frontend logs
aws logs create-log-group \
  --log-group-name /ecs/adma-frontend \
  --region $AWS_REGION
Set retention policy (optional):
# Retain logs for 7 days
aws logs put-retention-policy \
  --log-group-name /ecs/adma-backend \
  --retention-in-days 7 \
  --region $AWS_REGION

aws logs put-retention-policy \
  --log-group-name /ecs/adma-frontend \
  --retention-in-days 7 \
  --region $AWS_REGION

Register Task Definitions

Backend Task Definition

The backend task definition includes environment variables and secrets configuration.
1
Edit the Task Definition
2
Open infrastructure/task-def-backend.json from your repository and replace placeholders:
3
PlaceholderReplace WithACCOUNT_IDYour AWS account ID (appears 3 times)adma-postgres.xxxxx.region.rds.amazonaws.comYour RDS endpointhttps://go.yourdomain.comYour production domain for short URLshttps://yourdomain.comYour frontend domain (for CORS)
4
Task Definition Structure
5
{
  "family": "adma-backend",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskRole",
  "containerDefinitions": [
    {
      "name": "backend",
      "image": "ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/adma/backend:latest",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 8080,
          "hostPort": 8080,
          "protocol": "tcp"
        }
      ],
      "environment": [
        { "name": "DB_HOST", "value": "adma-postgres.xxxxx.eu-west-1.rds.amazonaws.com" },
        { "name": "DB_PORT", "value": "5432" },
        { "name": "DB_NAME", "value": "urlshortener" },
        { "name": "DB_USERNAME", "value": "appuser" },
        { "name": "APP_BASE_URL", "value": "https://go.yourdomain.com" },
        { "name": "CORS_ALLOWED_ORIGINS", "value": "https://yourdomain.com" },
        { "name": "SERVER_PORT", "value": "8080" },
        { "name": "JWT_EXPIRATION_MS", "value": "86400000" }
      ],
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:ssm:eu-west-1:ACCOUNT_ID:parameter/adma/prod/DB_PASSWORD"
        },
        {
          "name": "JWT_SECRET",
          "valueFrom": "arn:aws:ssm:eu-west-1:ACCOUNT_ID:parameter/adma/prod/JWT_SECRET"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/adma-backend",
          "awslogs-region": "eu-west-1",
          "awslogs-stream-prefix": "backend"
        }
      },
      "healthCheck": {
        "command": [
          "CMD-SHELL",
          "curl -f http://localhost:8080/actuator/health || exit 1"
        ],
        "interval": 30,
        "timeout": 10,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}
6
Register the Task Definition
7
aws ecs register-task-definition \
  --cli-input-json file://infrastructure/task-def-backend.json \
  --region $AWS_REGION

Frontend Task Definition

The frontend task definition is simpler since it only serves static files:
1
Edit the Task Definition
2
Open infrastructure/task-def-frontend.json and replace ACCOUNT_ID with your AWS account ID.
3
Task Definition Structure
4
{
  "family": "adma-frontend",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "frontend",
      "image": "ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/adma/frontend:latest",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/adma-frontend",
          "awslogs-region": "eu-west-1",
          "awslogs-stream-prefix": "frontend"
        }
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:80/ || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 10
      }
    }
  ]
}
5
Register the Task Definition
6
aws ecs register-task-definition \
  --cli-input-json file://infrastructure/task-def-frontend.json \
  --region $AWS_REGION

Resource Allocation Guide

CPU and Memory Units

Fargate tasks use specific CPU/memory combinations:
vCPUMemory Options (MB)
0.25512, 1024, 2048
0.51024 - 4096 (1 GB increments)
12048 - 8192 (1 GB increments)
24096 - 16384 (1 GB increments)
48192 - 30720 (1 GB increments)
backend:
  cpu: "512"    # 0.5 vCPU
  memory: "1024" # 1 GB

frontend:
  cpu: "256"    # 0.25 vCPU
  memory: "512"  # 512 MB

Environment Variables Reference

Backend Environment Variables

VariableTypeExampleDescription
DB_HOSTPlainadma-postgres.xxx.rds.amazonaws.comRDS endpoint
DB_PORTPlain5432PostgreSQL port
DB_NAMEPlainurlshortenerDatabase name
DB_USERNAMEPlainappuserDatabase user
DB_PASSWORDSecret(SSM)Database password from SSM
JWT_SECRETSecret(SSM)JWT signing key from SSM
JWT_EXPIRATION_MSPlain86400000Token expiration (24 hours)
APP_BASE_URLPlainhttps://go.example.comBase URL for short links
CORS_ALLOWED_ORIGINSPlainhttps://example.comAllowed CORS origins
SERVER_PORTPlain8080Backend port
Never hardcode secrets in the environment array. Always use the secrets array with SSM Parameter Store ARNs.

Container Health Checks

Health checks determine when a container is ready to receive traffic:

Backend Health Check

{
  "command": [
    "CMD-SHELL",
    "curl -f http://localhost:8080/actuator/health || exit 1"
  ],
  "interval": 30,      // Check every 30 seconds
  "timeout": 10,       // Fail if no response in 10 seconds
  "retries": 3,        // Mark unhealthy after 3 failures
  "startPeriod": 60    // Wait 60 seconds before first check
}
Spring Boot Actuator provides the /actuator/health endpoint automatically. It checks database connectivity and application status.

Frontend Health Check

{
  "command": ["CMD-SHELL", "curl -f http://localhost:80/ || exit 1"],
  "interval": 30,
  "timeout": 5,
  "retries": 3,
  "startPeriod": 10
}

Logging Configuration

Logs are sent to CloudWatch Logs using the awslogs driver:
{
  "logDriver": "awslogs",
  "options": {
    "awslogs-group": "/ecs/adma-backend",
    "awslogs-region": "eu-west-1",
    "awslogs-stream-prefix": "backend"
  }
}
View logs in real-time:
# Backend logs
aws logs tail /ecs/adma-backend --follow --region $AWS_REGION

# Frontend logs
aws logs tail /ecs/adma-frontend --follow --region $AWS_REGION

Next Steps

With task definitions registered:
  1. Configure Load Balancer - Set up ALB and routing
  2. Create ECS services (covered after ALB setup)
  3. Enable HTTPS/SSL - Secure with certificates
ECS services require the ALB and target groups to exist first. We’ll create the services after setting up the load balancer.

Build docs developers (and LLMs) love