Skip to main content

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​

FactorECSEKS
Operational overheadLower — AWS manages the control plane and integrates with ALB, IAM, CloudWatchHigher — Kubernetes concepts, add-ons, upgrades
PortabilityAWS-specific task definitionsStandard Kubernetes manifests; portable
EcosystemAWS-native; fewer third-party integrationsVery large CNCF ecosystem
Team familiarityEasier without Kubernetes experienceRequires Kubernetes competence
Multi-tenancyCluster or service isolationNamespace isolation with quotas and policies
CostNo control-plane charge for EC2 launch typePer-cluster control-plane charge
Best fitMost AWS-native containerized servicesTeams 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, not environment.
  • 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​

LayerMechanism
ServiceTarget tracking on CPU, memory, request count, or a custom metric
NodeCluster autoscaler or Karpenter when pod capacity is exhausted
DatabaseConnection pooling first; read replicas before instance upsizing
Queue consumersScale 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​

ItemWhere it livesHow it reaches the container
Non-sensitive configSSM Parameter StoreEnvironment variable at task start
SecretsSecrets ManagerInjected via secrets or CSI driver
Feature flagsConfig service or parameter storeRead at startup, refreshed on signal
CertificatesACMTerminated 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​

PlatformMechanismNotes
ECSRedeploy previous task definition revision; circuit breaker rollbackEnable deployment_circuit_breaker.rollback = true
EKSkubectl rollout undo deployment/<name>Requires revisionHistoryLimit > 0

Rollback is tested during the deployment drill, not discovered during an incident.