Writing ECS Tasks

Simfra runs ECS tasks as real Docker containers. Your container code works without modification - AWS_ENDPOINT_URL is injected automatically, so all AWS SDK calls route to Simfra.

Prerequisites

  • Simfra running with SIMFRA_DOCKER=true
  • Docker daemon accessible
  • AWS CLI configured to point at Simfra (see Endpoint Discovery)

How It Works

When Simfra launches an ECS task, it:

  1. Pulls the container image (from Docker Hub, Simfra's ECR, or any accessible registry)
  2. Resolves secrets from Secrets Manager and SSM Parameter Store using the task's execution role
  3. Injects AWS_ENDPOINT_URL, region, and credentials into the container environment
  4. Creates the container on the appropriate VPC Docker network (for awsvpc network mode)
  5. Starts the container with the configured resource limits, health checks, and entry point

Automatic Environment Injection

Every ECS task container receives:

Variable Value Purpose
AWS_ENDPOINT_URL Simfra gateway endpoint Routes all SDK calls to Simfra
AWS_DEFAULT_REGION Cluster's region Region for API calls
AWS_ACCESS_KEY_ID Root credentials Authentication
AWS_SECRET_ACCESS_KEY Root credentials Authentication

These are injected after your task definition's environment variables, but user-defined variables with the same names take precedence.

Secrets Injection

ECS task definitions can reference secrets from Secrets Manager and SSM Parameter Store. Simfra resolves these using the task's execution role and injects them as environment variables before the container starts.

{
  "containerDefinitions": [
    {
      "name": "app",
      "image": "my-app:latest",
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:000000000000:secret:prod/db-password"
        },
        {
          "name": "API_KEY",
          "valueFrom": "arn:aws:ssm:us-east-1:000000000000:parameter/prod/api-key"
        }
      ]
    }
  ]
}

The execution role must have secretsmanager:GetSecretValue and ssm:GetParameter permissions for the referenced resources.

Networking

awsvpc mode

Tasks using awsvpc network mode get their own IP on the VPC's Docker network. This matches real AWS behavior where each task receives an elastic network interface with a private IP in the VPC subnet.

aws ecs run-task \
  --cluster default \
  --task-definition my-task:1 \
  --network-configuration '{
    "awsvpcConfiguration": {
      "subnets": ["subnet-abc123"],
      "securityGroups": ["sg-abc123"]
    }
  }'

Containers in the same VPC network can reach each other by IP, and DNS resolution routes through Simfra's DNS container.

Example: SQS Consumer Service

This example shows a Node.js service that reads messages from SQS and writes to DynamoDB.

Application code

// app.js
const { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } = require('@aws-sdk/client-sqs');
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');

// No endpoint override needed - AWS_ENDPOINT_URL is injected by Simfra
const sqs = new SQSClient({});
const dynamodb = new DynamoDBClient({});

const QUEUE_URL = process.env.QUEUE_URL;
const TABLE_NAME = process.env.TABLE_NAME;

async function poll() {
  while (true) {
    const { Messages } = await sqs.send(new ReceiveMessageCommand({
      QueueUrl: QUEUE_URL,
      MaxNumberOfMessages: 10,
      WaitTimeSeconds: 20
    }));

    for (const msg of Messages || []) {
      const data = JSON.parse(msg.Body);
      await dynamodb.send(new PutItemCommand({
        TableName: TABLE_NAME,
        Item: {
          id: { S: data.id },
          payload: { S: JSON.stringify(data) },
          processed_at: { S: new Date().toISOString() }
        }
      }));
      await sqs.send(new DeleteMessageCommand({
        QueueUrl: QUEUE_URL,
        ReceiptHandle: msg.ReceiptHandle
      }));
    }
  }
}

poll().catch(console.error);

Dockerfile

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY app.js .
CMD ["node", "app.js"]

Infrastructure setup

# Create SQS queue and DynamoDB table
aws sqs create-queue --queue-name orders
aws dynamodb create-table \
  --table-name processed-orders \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

# Build and push image to Simfra's ECR
aws ecr create-repository --repository-name order-processor
docker build -t order-processor .
docker tag order-processor localhost:4599/000000000000/order-processor:latest
docker push localhost:4599/000000000000/order-processor:latest

# Register task definition
aws ecs register-task-definition --cli-input-json '{
  "family": "order-processor",
  "networkMode": "awsvpc",
  "containerDefinitions": [{
    "name": "processor",
    "image": "000000000000.dkr.ecr.us-east-1.amazonaws.com/order-processor:latest",
    "environment": [
      {"name": "QUEUE_URL", "value": "http://localhost:4599/000000000000/orders"},
      {"name": "TABLE_NAME", "value": "processed-orders"}
    ],
    "memory": 512,
    "cpu": 256
  }]
}'

# Run the task
aws ecs run-task \
  --cluster default \
  --task-definition order-processor:1 \
  --network-configuration '{
    "awsvpcConfiguration": {
      "subnets": ["subnet-abc123"]
    }
  }'

Service Discovery

ECS services can register with Cloud Map for DNS-based service discovery. Other services discover your containers by querying the registered DNS name:

# Create a Cloud Map namespace
aws servicediscovery create-private-dns-namespace \
  --name local \
  --vpc vpc-abc123

# Create a Cloud Map service
aws servicediscovery create-service \
  --name order-processor \
  --namespace-id ns-abc123 \
  --dns-config '{"DnsRecords": [{"Type": "A", "TTL": 60}]}'

# Create ECS service with service discovery
aws ecs create-service \
  --cluster default \
  --service-name order-processor \
  --task-definition order-processor:1 \
  --desired-count 2 \
  --network-configuration '{
    "awsvpcConfiguration": {
      "subnets": ["subnet-abc123"]
    }
  }' \
  --service-registries '[{
    "registryArn": "arn:aws:servicediscovery:us-east-1:000000000000:service/srv-abc123"
  }]'

Other containers can then reach the service at order-processor.local.

Load-Balanced Services

ECS services can register with an Application Load Balancer for HTTP traffic distribution:

aws ecs create-service \
  --cluster default \
  --service-name web-api \
  --task-definition web-api:1 \
  --desired-count 2 \
  --network-configuration '{
    "awsvpcConfiguration": {
      "subnets": ["subnet-abc123"]
    }
  }' \
  --load-balancers '[{
    "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/web-tg/abc123",
    "containerName": "web",
    "containerPort": 8080
  }]'

Simfra automatically registers and deregisters task IPs in the target group as tasks start and stop.

Health Checks

Container health checks defined in the task definition are passed to Docker:

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

ECS monitors the Docker health check status and replaces unhealthy tasks in services.

Next Steps