Skip to main content

AWS ECS Provider

A Story of Labels & Elastic Containers Attach labels to your AWS ECS tasks and let Traefik automatically discover and route traffic. The ECS provider integrates with Amazon Elastic Container Service for seamless service discovery in AWS.

Quick Start

1

Set Up IAM Permissions

Create an IAM policy for Traefik:
ecs-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "TraefikECSReadAccess",
      "Effect": "Allow",
      "Action": [
        "ecs:ListClusters",
        "ecs:DescribeClusters",
        "ecs:ListTasks",
        "ecs:DescribeTasks",
        "ecs:DescribeContainerInstances",
        "ecs:DescribeTaskDefinition",
        "ec2:DescribeInstances",
        "ssm:DescribeInstanceInformation"
      ],
      "Resource": "*"
    }
  ]
}
Attach to Traefik’s task role or instance role.
2

Enable ECS Provider

providers:
  ecs:
    autoDiscoverClusters: true
    region: "us-east-1"
    exposedByDefault: false
3

Add Labels to Task Definition

task-definition.json
{
  "containerDefinitions": [
    {
      "name": "web",
      "image": "myapp:latest",
      "dockerLabels": {
        "traefik.enable": "true",
        "traefik.http.routers.web.rule": "Host(`example.com`)",
        "traefik.http.routers.web.entrypoints": "websecure",
        "traefik.http.services.web.loadbalancer.server.port": "8080"
      }
    }
  ]
}
4

Deploy Task

aws ecs register-task-definition --cli-input-json file://task-definition.json
aws ecs run-task --cluster my-cluster --task-definition my-task
Traefik automatically discovers the task!

How It Works

The ECS provider:
  1. Polls ECS clusters at configured intervals
  2. Lists running tasks in specified clusters
  3. Reads task labels from container definitions
  4. Discovers task IPs from ENIs or container instances
  5. Creates routes dynamically based on labels
  6. Updates on changes when tasks start/stop

Provider Configuration

autoDiscoverClusters

Default: false Automatically discover all ECS clusters:
providers:
  ecs:
    autoDiscoverClusters: true
When false, only configured clusters are watched.

clusters

Default: ["default"] Specific clusters to watch:
providers:
  ecs:
    clusters:
      - "production"
      - "staging"
Ignored when autoDiscoverClusters: true.

region

Optional, Default: Auto-detected AWS region:
providers:
  ecs:
    region: "us-west-2"
Auto-detected from:
  • EC2 metadata (for EC2 tasks)
  • AWS_REGION environment variable (for Fargate)

accessKeyID / secretAccessKey

Optional, Default: Auto-detected Explicit AWS credentials:
providers:
  ecs:
    region: "us-east-1"
    accessKeyID: "${AWS_ACCESS_KEY_ID}"
    secretAccessKey: "${AWS_SECRET_ACCESS_KEY}"
Prefer IAM roles over hardcoded credentials. Credentials are auto-detected from:
  • Environment variables
  • Shared credentials file (~/.aws/credentials)
  • EC2 instance role
  • ECS task role

exposedByDefault

Default: true Expose all tasks by default:
providers:
  ecs:
    exposedByDefault: false  # Require traefik.enable=true label

constraints

Optional, Default: "" Filter tasks by labels:
providers:
  ecs:
    constraints: "Label(`environment`, `production`)"

healthyTasksOnly

Default: false Only discover healthy tasks:
providers:
  ecs:
    healthyTasksOnly: true
Tasks must have HEALTHY health status.

refreshSeconds

Default: 15 Polling interval in seconds:
providers:
  ecs:
    refreshSeconds: 30

defaultRule

Default: Host(`{{ normalize .Name }}`) Default routing rule:
providers:
  ecs:
    defaultRule: "Host(`{{ .Name }}.example.com`)"

ecsAnywhere

Default: false Enable ECS Anywhere support:
providers:
  ecs:
    ecsAnywhere: true
Requires ssm:DescribeInstanceInformation IAM permission.

Task Definition Examples

Basic Web Service

{
  "family": "web-app",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "myapp:v1.0",
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp"
        }
      ],
      "dockerLabels": {
        "traefik.enable": "true",
        "traefik.http.routers.web.rule": "Host(`app.example.com`)",
        "traefik.http.routers.web.entrypoints": "websecure",
        "traefik.http.routers.web.tls.certresolver": "letsencrypt",
        "traefik.http.services.web.loadbalancer.server.port": "8080"
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3
      }
    }
  ]
}

Service with Middleware

{
  "containerDefinitions": [
    {
      "name": "api",
      "image": "myapi:latest",
      "dockerLabels": {
        "traefik.enable": "true",
        
        "traefik.http.routers.api.rule": "Host(`api.example.com`) && PathPrefix(`/v1`)",
        "traefik.http.routers.api.entrypoints": "websecure",
        "traefik.http.routers.api.middlewares": "api-auth,rate-limit",
        
        "traefik.http.middlewares.api-auth.basicauth.users": "admin:$apr1$...",
        "traefik.http.middlewares.rate-limit.ratelimit.average": "100",
        "traefik.http.middlewares.rate-limit.ratelimit.burst": "50",
        
        "traefik.http.services.api.loadbalancer.server.port": "8080",
        "traefik.http.services.api.loadbalancer.healthcheck.path": "/health",
        "traefik.http.services.api.loadbalancer.healthcheck.interval": "10s"
      }
    }
  ]
}

Multiple Containers per Task

{
  "family": "app-with-sidecar",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "myapp:latest",
      "portMappings": [{"containerPort": 8080}],
      "dockerLabels": {
        "traefik.enable": "true",
        "traefik.http.routers.app.rule": "Host(`app.example.com`)",
        "traefik.http.services.app.loadbalancer.server.port": "8080"
      }
    },
    {
      "name": "metrics",
      "image": "metrics-exporter:latest",
      "portMappings": [{"containerPort": 9090}],
      "dockerLabels": {
        "traefik.enable": "true",
        "traefik.http.routers.metrics.rule": "Host(`metrics.example.com`)",
        "traefik.http.services.metrics.loadbalancer.server.port": "9090"
      }
    }
  ]
}

Complete Setup Example

{
  "family": "traefik",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "taskRoleArn": "arn:aws:iam::123456789:role/TraefikECSRole",
  "executionRoleArn": "arn:aws:iam::123456789:role/ECSTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "traefik",
      "image": "traefik:v3.6",
      "portMappings": [
        {"containerPort": 80, "protocol": "tcp"},
        {"containerPort": 443, "protocol": "tcp"},
        {"containerPort": 8080, "protocol": "tcp"}
      ],
      "command": [
        "--entrypoints.web.address=:80",
        "--entrypoints.websecure.address=:443",
        "--entrypoints.traefik.address=:8080",
        "--api.dashboard=true",
        "--providers.ecs=true",
        "--providers.ecs.region=us-east-1",
        "--providers.ecs.autodiscoverclusters=true",
        "--providers.ecs.exposedbydefault=false",
        "--log.level=INFO"
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/traefik",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "traefik"
        }
      }
    }
  ]
}

Network Modes

AWSVPC (Fargate / EC2)

Tasks get their own ENI with private IP:
{
  "networkMode": "awsvpc",
  "containerDefinitions": [{
    "portMappings": [
      {"containerPort": 8080}
    ]
  }]
}
Traefik connects to task’s private IP.

Bridge (EC2 only)

Port mapping with dynamic host ports:
{
  "networkMode": "bridge",
  "containerDefinitions": [{
    "portMappings": [
      {
        "containerPort": 8080,
        "hostPort": 0  // Dynamic port
      }
    ]
  }]
}
Traefik discovers the host:port mapping.

Host (EC2 only)

Direct host networking:
{
  "networkMode": "host",
  "containerDefinitions": [{
    "portMappings": [
      {"containerPort": 8080}
    ]
  }]
}

Advanced Patterns

Blue-Green Deployments

# Blue environment (current)
aws ecs update-service --cluster prod --service web --task-definition web:blue --desired-count 3

# Deploy green (new version)
aws ecs create-service --cluster prod --service web-green --task-definition web:green --desired-count 3

# Test green deployment
curl -H "Host: green.example.com" https://app.example.com

# Switch traffic (update labels)
# Scale down blue
aws ecs update-service --cluster prod --service web --desired-count 0

Canary Deployments

// Stable version (90% of tasks)
{
  "dockerLabels": {
    "traefik.http.services.api.loadbalancer.weight": "90"
  }
}

// Canary version (10% of tasks)
{
  "dockerLabels": {
    "traefik.http.services.api-canary.loadbalancer.weight": "10"
  }
}

Multi-Region Setup

# us-east-1 Traefik
providers:
  ecs:
    region: "us-east-1"
    constraints: "Label(`region`, `us-east`)"

# eu-west-1 Traefik
providers:
  ecs:
    region: "eu-west-1"
    constraints: "Label(`region`, `eu-west`)"

Troubleshooting

Tasks Not Discovered

1

Check IAM Permissions

aws ecs list-tasks --cluster my-cluster
aws ecs describe-tasks --cluster my-cluster --tasks <task-arn>
2

Verify exposedByDefault

If false, ensure traefik.enable=true label exists.
3

Check Constraints

Ensure task labels match configured constraints.
4

Review Traefik Logs

aws logs tail /ecs/traefik --follow

Network Connectivity Issues

  • AWSVPC: Verify security groups allow traffic between Traefik and tasks
  • Bridge: Check container instance security groups
  • Fargate: Ensure tasks are in same VPC/subnets

Port Detection

Explicitly set port if auto-detection fails:
{
  "dockerLabels": {
    "traefik.http.services.myapp.loadbalancer.server.port": "8080"
  }
}

Best Practices

1

Use Task Roles

Prefer IAM task roles over access keys for security.
2

Enable Health Checks

Configure container health checks for reliable routing.
3

Set exposedByDefault=false

Explicitly enable Traefik per service for better control.
4

Use Constraints

Filter by environment, region, or team to organize services.
5

Monitor Refresh Interval

Adjust refreshSeconds based on deployment frequency.

Next Steps

ECS Routing

Detailed ECS label configuration

AWS ECS Docs

Official AWS ECS documentation

Build docs developers (and LLMs) love