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:
- Pulls the container image (from Docker Hub, Simfra's ECR, or any accessible registry)
- Resolves secrets from Secrets Manager and SSM Parameter Store using the task's execution role
- Injects
AWS_ENDPOINT_URL, region, and credentials into the container environment - Creates the container on the appropriate VPC Docker network (for
awsvpcnetwork mode) - 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
- Endpoint Discovery - how ECS tasks discover the Simfra endpoint automatically
- Writing Lambda Functions - building Lambda functions that run in Simfra
- VPC Isolation - how VPC networks affect ECS task connectivity