Containerized workloads
Containers make the unit of deployment explicit. This standard covers the runtime decisions around that unit: where it runs, how it is networked, how it gets its configuration, and how it is replaced.
Choosing ECS or EKS
| Factor | ECS | EKS |
|---|---|---|
| Operational overhead | Lower — AWS manages the control plane and integrates with ALB, IAM, CloudWatch | Higher — Kubernetes concepts, add-ons, upgrades |
| Portability | AWS-specific task definitions | Standard Kubernetes manifests; portable |
| Ecosystem | AWS-native; fewer third-party integrations | Very large CNCF ecosystem |
| Team familiarity | Easier without Kubernetes experience | Requires Kubernetes competence |
| Multi-tenancy | Cluster or service isolation | Namespace isolation with quotas and policies |
| Cost | No control-plane charge for EC2 launch type | Per-cluster control-plane charge |
| Best fit | Most AWS-native containerized services | Teams already on Kubernetes, or needing its ecosystem |
Default: ECS on Fargate for new containerized services unless the team already operates Kubernetes or needs Kubernetes-specific tooling.
ECS reference architecture
Route 53
│
▼
ALB (public subnets, 2 AZs)
│
▼
ECS Service (private subnets, 2 AZs)
├── Task: app container
├── Task: sidecar (log router / proxy)
▼
RDS (private) + ElastiCache (private)
- Cluster per environment.
- Service per deployable component.
- ALB target group per service, with health checks on a real endpoint.
- Tasks in private subnets; no public IP.
- Task role for AWS API access; execution role for image pull and log write.
- Secrets injected from Secrets Manager at task start.
Task definition
{
"family": "example-service",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::<account>:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::<account>:role/example-service-task",
"containerDefinitions": [
{
"name": "app",
"image": "<account>.dkr.ecr.us-east-1.amazonaws.com/example-service@sha256:<digest>",
"essential": true,
"portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
"environment": [
{"name": "NODE_ENV", "value": "production"}
],
"secrets": [
{"name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:us-east-1:<account>:secret:db-url"}
],
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 20
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/example-service",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "app"
}
}
}
]
}
Rules.
- Image referenced by digest, not tag.
- Secrets via
secrets, notenvironment. - Non-root user in the image.
- Health check defined.
- Logs to CloudWatch with a known group and prefix.
- CPU and memory set; no unbounded task.
EKS reference architecture
Route 53 → ALB (AWS Load Balancer Controller)
│
▼
EKS cluster (private endpoint)
├── namespace: app
│ ├── Deployment (3 replicas)
│ ├── Service
│ └── HPA
├── namespace: platform
└── namespace: observability
- Private API endpoint; access through a bastion or VPN, or CI only.
- Managed node groups per workload class, or Fargate profiles for isolation.
- IRSA for pod-level AWS permissions — no node-wide instance role for application access.
- Network policies enabled to restrict pod-to-pod traffic.
- Pod security standards: restricted profile for application namespaces.
- Resource requests and limits on every container.
- HPA on the metric that predicts load; cluster autoscaler or Karpenter for nodes.
Scaling
| Layer | Mechanism |
|---|---|
| Service | Target tracking on CPU, memory, request count, or a custom metric |
| Node | Cluster autoscaler or Karpenter when pod capacity is exhausted |
| Database | Connection pooling first; read replicas before instance upsizing |
| Queue consumers | Scale on queue depth, not CPU |
Scale on the signal that leads demand. Scaling on CPU reacts after latency has already degraded; scaling on request concurrency or queue depth reacts before.
Configuration and secrets
| Item | Where it lives | How it reaches the container |
|---|---|---|
| Non-sensitive config | SSM Parameter Store | Environment variable at task start |
| Secrets | Secrets Manager | Injected via secrets or CSI driver |
| Feature flags | Config service or parameter store | Read at startup, refreshed on signal |
| Certificates | ACM | Terminated at the load balancer |
Configuration is never baked into the image. The same image runs in every environment with different injected configuration.
Observability
- Logs — structured JSON to CloudWatch Logs, one log group per service.
- Metrics — Container Insights or Prometheus; the RED metrics (rate, errors, duration) per service.
- Tracing — X-Ray or OpenTelemetry, with trace context propagated across services.
- Dashboards — per service, showing request rate, error rate, latency and saturation.
Rollback
| Platform | Mechanism | Notes |
|---|---|---|
| ECS | Redeploy previous task definition revision; circuit breaker rollback | Enable deployment_circuit_breaker.rollback = true |
| EKS | kubectl rollout undo deployment/<name> | Requires revisionHistoryLimit > 0 |
Rollback is tested during the deployment drill, not discovered during an incident.