Part 7 of 1229 min read · 11 diagramsAI-assisted

Containers & Serverless

A note on scope: Kubernetes itself — architecture, workloads, networking, and specifically EKS — already received an exhaustive, dedicated treatment in the Kubernetes Deep Dive series (including Part 5's full EKS deep dive). This part covers AWS's OTHER container and serverless compute options — ECS, Fargate, and Lambda — which are genuinely distinct services with their own operational models, not just "AWS's version of Kubernetes."

Table of Contents#

  1. Why AWS Has Multiple Container Services
  2. ECR — Elastic Container Registry
  3. ECS — Elastic Container Service, Core Concepts
  4. Task Definitions — The ECS Equivalent of a Pod Spec
  5. ECS Services — Keeping Tasks Running
  6. The EC2 Launch Type vs the Fargate Launch Type
  7. Fargate — Serverless Containers, In Depth
  8. ECS Networking — awsvpc Mode
  9. ECS Service Auto Scaling
  10. ECS Deployment Strategies
  11. ECS vs EKS — Choosing Between AWS's Two Orchestrators
  12. AWS App Runner — An Even Simpler Option
  13. Lambda — Core Concepts
  14. The Lambda Execution Model and Cold Starts
  15. Lambda Triggers — What Actually Invokes a Function
  16. Lambda Concurrency — Reserved and Provisioned
  17. Lambda Layers and Container Images
  18. Lambda Networking — Running Inside a VPC
  19. Step Functions — Orchestrating Multiple Lambdas
  20. Choosing Between EC2, ECS, Fargate, EKS, and Lambda
  21. A Full Worked Example: A Three-Service Application, Three Ways
  22. Debugging Running Containers: ECS Exec
  23. Container Insights — Observability for ECS and EKS
  24. Lambda Error Handling: Dead-Letter Queues and Destinations
  25. Container and Lambda Security Fundamentals
  26. Part 7 CLI Cheat Sheet
  27. Common Mistakes
  28. Worked Practice Problems
  29. Summary and What's Next

Why AWS Has Multiple Container Services#

A genuinely common point of confusion worth resolving immediately: AWS offers THREE distinct ways to run containers — ECS (AWS's own, simpler orchestrator), EKS (managed Kubernetes, already covered in the Kubernetes Deep Dive series), and Fargate (which isn't a separate orchestrator at all, but a serverless COMPUTE ENGINE that both ECS and EKS can run on top of).

Diagram

Why understanding this two-axis relationship precisely matters, worth stating explicitly as a genuinely strong interview answer: "orchestrator" (ECS vs EKS) and "compute engine" (EC2 vs Fargate) are two SEPARATE, INDEPENDENT decisions — you can run ECS on EC2, ECS on Fargate, EKS on EC2, or EKS on Fargate. A common mistake is treating "Fargate" as if it were a competitor to EKS, when it's actually an option AVAILABLE to EKS itself.


ECR — Elastic Container Registry#

Before any container can run on ECS, EKS, or Fargate, its image needs to live somewhere — ECR is AWS's fully managed, private Docker/OCI container registry.

# Create a repository
aws ecr create-repository --repository-name my-app --image-scanning-configuration scanOnPush=true

# Authenticate Docker to ECR (temporary credentials, Part 2's
# temporary-credential theme applied to registry auth)
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

# Build, tag, and push
docker build -t my-app .
docker tag my-app:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.2.3
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.2.3

# Set a lifecycle policy — automatically expire old, untagged images
aws ecr put-lifecycle-policy \
  --repository-name my-app \
  --lifecycle-policy-text '{"rules":[{"rulePriority":1,"description":"Expire untagged images after 14 days","selection":{"tagStatus":"untagged","countType":"sinceImagePushed","countUnit":"days","countNumber":14},"action":{"type":"expire"}}]}'

Why scanOnPush=true matters, directly connecting to the container image scanning discussion already covered in depth in the DevSecOps series (Part 3): ECR's built-in scanning (powered by Amazon Inspector, covered further in Part 9) automatically checks every pushed image against known CVE databases, surfacing exactly the same class of finding the DevSecOps series' Trivy discussion covers, but as a native, zero-extra-setup feature of the registry itself.


ECS — Elastic Container Service, Core Concepts#

ECS is AWS's own, purpose-built container orchestrator — genuinely simpler than Kubernetes, trading some of Kubernetes's flexibility and ecosystem for a much smaller operational learning curve.

Diagram

A genuinely useful direct vocabulary mapping for anyone coming from the Kubernetes Deep Dive series, worth stating explicitly: ECS's "Task Definition" is the rough equivalent of a Kubernetes Pod spec; an ECS "Task" is the rough equivalent of a running Pod; an ECS "Service" is the rough equivalent of a Kubernetes Deployment (Kubernetes series, Part 2) — same underlying job (keep N replicas running, handle rolling updates), different vendor-specific name and simpler feature set.

aws ecs create-cluster --cluster-name production-cluster

Task Definitions — The ECS Equivalent of a Pod Spec#

A Task Definition is a JSON document describing one or more containers that run together as a unit — the container image, CPU/memory, networking, IAM role, and logging configuration.

cat <<'TASKDEF'
{
  "family": "my-app",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/myAppTaskRole",
  "containerDefinitions": [{
    "name": "my-app",
    "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.2.3",
    "portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {"awslogs-group": "/ecs/my-app", "awslogs-region": "us-east-1", "awslogs-stream-prefix": "ecs"}
    }
  }]
}
TASKDEF

aws ecs register-task-definition --cli-input-json file://taskdef.json

Why there are TWO separate IAM roles here, worth stating precisely, directly extending Part 2's IAM discussion: the executionRoleArn is used by ECS itself (to pull the image from ECR, write logs) — the taskRoleArn is used by the APPLICATION CODE running inside the container (e.g. to read from S3, Part 5) — this is the ECS-specific realization of the same least-privilege separation-of-concerns already covered for IRSA in the Kubernetes Deep Dive series (Kubernetes manages the platform; the application gets its own narrowly-scoped identity).


ECS Services — Keeping Tasks Running#

An ECS Service wraps a Task Definition with a desired count, health checking, and rolling-update behavior — the direct analog of an Auto Scaling Group (Part 3), just for tasks instead of EC2 instances.

aws ecs create-service \
  --cluster production-cluster \
  --service-name my-app-service \
  --task-definition my-app \
  --desired-count 4 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-private-1a,subnet-private-1b],securityGroups=[sg-app123],assignPublicIp=DISABLED}" \
  --load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/my-app-tg,containerName=my-app,containerPort=8080"

# Check service status
aws ecs describe-services --cluster production-cluster --services my-app-service \
  --query 'services[0].{Running:runningCount,Desired:desiredCount,Pending:pendingCount}'

The EC2 Launch Type vs the Fargate Launch Type#

Directly resolving the two-axis confusion from this part's opening section, now with full CLI context.

EC2 launch typeFargate launch type
Who manages the underlying serversYou (an ECS-optimized AMI running on EC2 instances you provision, patch, and scale via an Auto Scaling Group, Part 3)AWS — fully serverless, no EC2 instances to manage at all
BillingPer EC2 instance-hour, regardless of task packing efficiencyPer TASK, based on the exact vCPU/memory the task requests
Bin-packing controlYou control instance sizes and can pack multiple tasks per instance tightly for cost efficiencyAWS handles placement; less direct control over packing density
Startup timeFaster once a warm instance has capacitySlightly slower per-task cold start, since compute is provisioned per task
Best fitVery high task density, tight cost control, specialized instance needs (GPU, etc.)Simpler operations, spiky/unpredictable workloads, teams wanting zero server management

Fargate — Serverless Containers, In Depth#

Worth its own dedicated, precise explanation, since "serverless containers" is a genuinely distinct model worth understanding on its own terms, not just as "EC2 launch type, but AWS-managed."

Diagram

Why Fargate is worth stating as a genuinely different economic and operational model, not just "managed EC2," worth explaining precisely: with the EC2 launch type, you pay for whole EC2 instances and are responsible for packing tasks efficiently onto them (an idle, under-packed instance is still billed in full) — with Fargate, you pay per TASK, for exactly the resources that task requested, with zero bin-packing waste and zero instance patching/lifecycle management at all. This directly mirrors the EC2-vs-Fargate tradeoff already covered for EKS specifically in the Kubernetes Deep Dive series (Part 5) — the same underlying AWS compute-abstraction choice, just reused across both orchestrators.


ECS Networking — awsvpc Mode#

The networkMode: awsvpc setting seen in the Task Definition example above is worth its own explicit callout — it's the modern, recommended default, giving each TASK its own real Elastic Network Interface (ENI) with its own private IP, directly inside your VPC (Part 4).

Diagram

Why this matters directly, connecting back to Part 4's security-group-chaining pattern: since each task gets its own ENI and security group, the exact same "reference a security group, not a CIDR" pattern from Part 4 applies natively at the TASK level — a database's security group can allow traffic specifically from the application task's security group, with the same automatic, scale-safe correctness Part 4 already covered for EC2 fleets.


ECS Service Auto Scaling#

Directly extending the target-tracking scaling concepts already covered for EC2 Auto Scaling Groups in Part 3 — ECS Service Auto Scaling applies the identical philosophy to task count instead of instance count.

aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/production-cluster/my-app-service \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 --max-capacity 20

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/production-cluster/my-app-service \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name cpu-target-tracking \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{"TargetValue":60.0,"PredefinedMetricSpecification":{"PredefinedMetricType":"ECSServiceAverageCPUUtilization"}}'

On the EC2 launch type specifically, worth stating an important, real nuance: scaling the ECS SERVICE (task count) and scaling the underlying EC2 CLUSTER (instance count, via Cluster Auto Scaling / Capacity Providers) are TWO SEPARATE scaling decisions that must both work correctly together — a service trying to scale out to more tasks than the cluster's current EC2 capacity can actually host will simply have tasks stuck in a PENDING state, exactly the same class of "desired doesn't match actual" silent capacity gap already covered for EC2 in Part 1's Service Quotas discussion. Fargate eliminates this entire class of problem, since there's no underlying cluster capacity to separately manage.


ECS Deployment Strategies#

Directly, precisely the same deployment strategies already covered in depth in the Automation series (Part 1) — ECS implements them natively as configuration, not custom tooling.

# Rolling update (the ECS default) — configure the safety margins
aws ecs update-service \
  --cluster production-cluster --service my-app-service \
  --deployment-configuration "minimumHealthyPercent=100,maximumPercent=200"

# Blue-green via CodeDeploy integration (Part 11 covers CodeDeploy
# in depth) — ECS supports this natively through a CodeDeploy
# deployment controller
aws ecs create-service \
  --cluster production-cluster --service-name my-app-bluegreen \
  --task-definition my-app --deployment-controller type=CODE_DEPLOY

Why minimumHealthyPercent/maximumPercent directly map to the maxSurge/maxUnavailable concepts already covered for Kubernetes rolling updates in the Kubernetes Deep Dive series, worth stating explicitly: maximumPercent=200 with minimumHealthyPercent=100 means ECS can temporarily run up to DOUBLE the desired task count during a deployment (launching all-new tasks before terminating any old ones) — the exact same "surge above desired capacity for a safer rollout" tradeoff already covered generically, just with different parameter names.


ECS vs EKS — Choosing Between AWS's Two Orchestrators#

A genuinely common, real architectural decision worth having a clear, ready framework for.

Diagram
ECSEKS
Learning curveGenuinely smaller — AWS-native concepts onlySteeper — full Kubernetes API surface (Kubernetes series)
EcosystemAWS-native tooling onlyThe entire Kubernetes ecosystem (Helm, Operators, service mesh)
PortabilityAWS-only — no multi-cloud storyKubernetes API is portable across clouds/on-prem
Control plane costFreeA per-hour control plane charge (Kubernetes series, Part 5)
Best fitAWS-only shops prioritizing simplicityTeams needing portability, existing Kubernetes expertise, or ecosystem tools

A genuinely honest, senior-level answer worth having ready: "there's no universally 'better' choice — ECS is a legitimately excellent, simpler option for an AWS-only shop that doesn't need Kubernetes's ecosystem or portability; EKS is the right choice when a team already has Kubernetes expertise, needs multi-cloud portability, or specifically needs an ecosystem tool that assumes the Kubernetes API. Choosing EKS 'because it's more popular' without an actual need for its ecosystem is a common form of unnecessary complexity."


AWS App Runner — An Even Simpler Option#

Worth knowing by name for completeness: App Runner is AWS's simplest possible "give me a container or source repo, I'll build, deploy, scale, and load-balance it" service — genuinely less configuration than even Fargate-backed ECS.

aws apprunner create-service \
  --service-name my-simple-app \
  --source-configuration '{"ImageRepository":{"ImageIdentifier":"123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.2.3","ImageRepositoryType":"ECR"}}'

When to reach for it, worth stating precisely: App Runner trades away nearly all infrastructure-level control (no VPC networking customization by default, limited scaling configuration) for maximum simplicity — a genuinely good fit for a small service, an internal tool, or a team explicitly prioritizing speed-to-deploy over fine-grained control, but not a fit for anything needing the networking/scaling precision ECS or EKS provide.


Lambda — Core Concepts#

AWS Lambda runs code in response to events, without provisioning or managing any server at all — the purest expression of "serverless" in AWS's portfolio.

Diagram
# Create a function
aws lambda create-function \
  --function-name process-upload \
  --runtime python3.13 \
  --role arn:aws:iam::123456789012:role/LambdaExecutionRole \
  --handler app.handler \
  --zip-file fileb://function.zip \
  --memory-size 256 --timeout 30

# Invoke it directly (for testing)
aws lambda invoke --function-name process-upload --payload '{"key":"value"}' response.json

Why "zero cost while idle" is worth stating as Lambda's single most defining economic property, directly connecting to the FinOps/cost-optimization theme carried across this series: unlike EC2 (Part 3) or even Fargate (billed for the task's full running duration), a Lambda function costs literally nothing between invocations — making it a genuinely strong fit for workloads with unpredictable, spiky, or very low average traffic, where paying for standing capacity would be pure waste.


The Lambda Execution Model and Cold Starts#

Directly extending the cold-start concept already introduced for EC2 Auto Scaling in the Capacity Planning series and Part 3 — Lambda has its own, distinct version of this same underlying idea.

Diagram

Why understanding WHEN a cold start happens matters, worth stating precisely: AWS keeps a recently-used execution environment "warm" for a period after an invocation completes, specifically to serve the NEXT invocation without re-provisioning — a cold start only happens on the FIRST invocation, after a genuine idle period, or when concurrency scales UP beyond the number of currently-warm environments. A function receiving low, occasional traffic will experience cold starts more often than one receiving sustained, high-frequency traffic, simply because warm environments expire during longer idle gaps.


Lambda Triggers — What Actually Invokes a Function#

Lambda's genuinely defining architectural property: it never runs on its own — something always triggers it, and AWS offers a wide range of native trigger sources.

Trigger sourceCommon use case
API GatewayA serverless HTTP API backend
S3 (Part 5)Process an uploaded file (thumbnail generation, virus scanning)
DynamoDB Streams (Databases series, Part 7)React to data changes in near-real-time
SQS/SNS/EventBridge (Part 11)Process a queued message, or react to an event bus event
CloudWatch Events/EventBridge (scheduled)Run on a cron-style schedule, replacing a traditional cron job
Application Load Balancer (Part 8)Serve HTTP traffic directly from an ALB, without API Gateway
# Wire an S3 upload event directly to a Lambda function
aws lambda add-permission \
  --function-name process-upload --statement-id s3-trigger \
  --action lambda:InvokeFunction --principal s3.amazonaws.com \
  --source-arn arn:aws:s3:::my-uploads-bucket

aws s3api put-bucket-notification-configuration \
  --bucket my-uploads-bucket \
  --notification-configuration '{"LambdaFunctionConfigurations":[{"LambdaFunctionArn":"arn:aws:lambda:us-east-1:123456789012:function:process-upload","Events":["s3:ObjectCreated:*"]}]}'

Lambda Concurrency — Reserved and Provisioned#

Two distinct concurrency controls worth understanding precisely, since they solve genuinely different problems.

Diagram
# Reserved concurrency — a hard ceiling
aws lambda put-function-concurrency \
  --function-name process-upload --reserved-concurrent-executions 50

# Provisioned concurrency — pre-warmed, at an ongoing cost
# (you pay for the reserved capacity whether invoked or not,
# similar in spirit to Part 3's Capacity Reservations)
aws lambda put-provisioned-concurrency-config \
  --function-name process-upload --qualifier v3 \
  --provisioned-concurrent-executions 10

Why Provisioned Concurrency reintroduces a standing cost, worth stating explicitly as a real, deliberate tradeoff: it's the direct opposite of Lambda's "zero cost while idle" default property — you're explicitly paying to eliminate cold starts, which is the right call for a latency-critical user-facing API, but defeats Lambda's main cost advantage for a background batch job where an occasional cold start genuinely doesn't matter.


Lambda Layers and Container Images#

Two ways to package Lambda code and dependencies beyond a simple zip file, worth knowing precisely.

# A Layer — shared code/dependencies reused across MULTIPLE functions
aws lambda publish-layer-version \
  --layer-name shared-utils \
  --zip-file fileb://layer.zip \
  --compatible-runtimes python3.13

aws lambda update-function-configuration \
  --function-name process-upload \
  --layers arn:aws:lambda:us-east-1:123456789012:layer:shared-utils:3

# A Lambda function packaged as a CONTAINER IMAGE instead —
# genuinely useful for larger dependencies (up to 10GB, vs
# the zip-based 250MB unzipped limit) or teams already
# standardized on container-based CI/CD (Part 11)
docker build -t my-lambda-app .
docker tag my-lambda-app:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-lambda-app:latest
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-lambda-app:latest

aws lambda create-function \
  --function-name my-lambda-app \
  --package-type Image \
  --code ImageUri=123456789012.dkr.ecr.us-east-1.amazonaws.com/my-lambda-app:latest \
  --role arn:aws:iam::123456789012:role/LambdaExecutionRole

Why Layers are worth adopting once multiple functions share common code, worth stating explicitly, directly connecting to the DRY (Don't Repeat Yourself) principle underlying good software design generally: bundling the same shared utility library into every function's own zip file means updating that library requires re-deploying every single function — a Layer lets many functions reference ONE shared, versioned dependency, updated independently of each function's own business logic.


Lambda Networking — Running Inside a VPC#

By default, a Lambda function does NOT run inside your VPC (Part 4) at all — it runs in an AWS-managed network, with direct internet access but no path to reach private VPC resources (like an RDS database, Part 6, sitting in an isolated subnet).

aws lambda update-function-configuration \
  --function-name process-upload \
  --vpc-config SubnetIds=subnet-private-1a,subnet-private-1b,SecurityGroupIds=sg-lambda123

Why attaching Lambda to a VPC is a real, deliberate tradeoff, worth stating precisely: it's REQUIRED if the function needs to reach a private resource (a database in an isolated subnet, an internal API), but it also means the function now needs a route to the internet (via a NAT Gateway, Part 4) for any external API calls, and historically added meaningful cold-start latency (though AWS has substantially improved this in recent years via Hyperplane ENIs) — only attach Lambda to a VPC when it genuinely needs to reach something private inside it.


Step Functions — Orchestrating Multiple Lambdas#

For workflows spanning multiple Lambda invocations with real sequencing, branching, error handling, and retry logic, AWS Step Functions provides a managed state-machine orchestrator, rather than hand-rolling that coordination logic inside a single, increasingly complex Lambda function.

Diagram
aws stepfunctions create-state-machine \
  --name order-processing \
  --definition file://state-machine.json \
  --role-arn arn:aws:iam::123456789012:role/StepFunctionsRole

Why this is worth reaching for once a workflow's logic genuinely needs multi-step orchestration, worth stating explicitly: it makes retry policies, error handling, and parallel/sequential branching an explicit, visualizable, auditable part of the WORKFLOW DEFINITION itself, rather than scattered try/except logic buried inside application code across several Lambda functions — a genuinely important maturity step once a serverless application's business logic outgrows a single function's linear execution.


Choosing Between EC2, ECS, Fargate, EKS, and Lambda#

Consolidating this entire part (and cross-referencing Parts 3 and the Kubernetes Deep Dive series) into one practical decision framework.

Diagram

A Full Worked Example: A Three-Service Application, Three Ways#

Bringing this entire part together into one concrete comparison — the SAME hypothetical application (an image-processing API), built three different, legitimate ways, each with an explicit reasoning.

Option A — Fully serverless (API Gateway + Lambda + S3 + DynamoDB): the API Gateway receives HTTP requests, invoking a Lambda function per request; the function reads/writes S3 (Part 5) and DynamoDB (Databases series, Part 7). Best fit when: traffic is genuinely spiky/unpredictable, and the team wants zero server management and zero idle cost.

Option B — ECS on Fargate: a long-running container behind an ALB (Part 8), scaled by ECS Service Auto Scaling. Best fit when: the workload benefits from a persistent process (e.g. an in-memory model loaded once and reused across many requests, avoiding Lambda's per-invocation cold-start-prone model), but the team still wants zero EC2 management.

Option C — EKS: the same containerized application, now running on Kubernetes, likely because the broader organization already standardized on Kubernetes for other services and wants this one to share the same tooling, observability stack (Kubernetes series, Part 4), and deployment pipeline (Automation series).

Why all three are legitimate, worth stating explicitly as the honest answer to "which is correct": there is no universally right answer here — the correct choice depends on traffic shape (spiky vs sustained), whether the workload benefits from a long-lived, warm process (favoring containers over Lambda), and organizational context (existing Kubernetes investment favoring EKS). A senior-level answer names the SPECIFIC tradeoff driving the choice for THIS workload, rather than defaulting to whichever service is most familiar.


Debugging Running Containers: ECS Exec#

Directly the ECS equivalent of kubectl exec (Kubernetes Deep Dive series) — a genuinely important operational capability worth knowing, since Fargate tasks have no underlying EC2 instance you could otherwise SSH into.

# Enable ECS Exec when creating/updating a service
aws ecs update-service \
  --cluster production-cluster --service my-app-service \
  --enable-execute-command

# Open an interactive shell INSIDE a running task's container,
# with zero open inbound ports — authenticated and authorized
# through IAM (Part 2), exactly the same underlying mechanism
# as SSM Session Manager already covered for EC2 in Part 3
aws ecs execute-command \
  --cluster production-cluster \
  --task arn:aws:ecs:us-east-1:123456789012:task/production-cluster/abc123 \
  --container my-app \
  --command "/bin/sh" --interactive

Why this matters especially for Fargate, worth stating explicitly: since a Fargate task has no underlying EC2 instance at all, there's fundamentally no host to SSH into even if you wanted to — ECS Exec is the ONLY way to get an interactive shell inside a running Fargate container for live debugging, using the same IAM-authenticated, zero-open-port model already established as the best practice for EC2 access in Part 3.


Container Insights — Observability for ECS and EKS#

A managed CloudWatch feature providing automatic CPU/memory/network dashboards and metrics at the cluster, service, and task level — a preview of the fuller monitoring discussion in Part 10, worth introducing here specifically because it's container-platform-specific.

aws ecs put-account-setting --name containerInsights --value enabled

aws ecs update-cluster-settings \
  --cluster production-cluster \
  --settings name=containerInsights,value=enabled

Why this is worth enabling by default on any production cluster, worth stating explicitly: it automatically instruments container-level resource metrics (CPU/memory per task, not just per EC2 instance) without any application-level instrumentation work — directly extending the Golden Signals/USE Method discussion from the Monitoring Methodologies series down to the container-orchestration layer specifically.


Lambda Error Handling: Dead-Letter Queues and Destinations#

A genuinely important, frequently-overlooked production concern: what happens to an EVENT if a Lambda invocation fails, and all its automatic retries are also exhausted?

Diagram
# Configure a Dead-Letter Queue — without this, a permanently
# failing async invocation's event is simply DISCARDED after
# retries are exhausted, with no record it ever happened
aws lambda update-function-configuration \
  --function-name process-upload \
  --dead-letter-config TargetArn=arn:aws:sqs:us-east-1:123456789012:process-upload-dlq

# Destinations — a more modern, flexible alternative, routing
# based on SUCCESS or FAILURE outcome, to more targets than
# just a DLQ (SQS, SNS, EventBridge, or even another Lambda)
aws lambda put-function-event-invoke-config \
  --function-name process-upload \
  --destination-config '{"OnFailure":{"Destination":"arn:aws:sqs:us-east-1:123456789012:process-upload-dlq"},"OnSuccess":{"Destination":"arn:aws:sns:us-east-1:123456789012:process-upload-success"}}'

Why skipping a DLQ is a genuinely dangerous, silent failure mode, worth stating explicitly, directly connecting to the alerting-design discipline from the Observability series: without one, a permanently failing asynchronous invocation's event is simply DROPPED once retries are exhausted — no error surfaces anywhere by default, meaning real, lost work (a failed order, an unsent notification) can go completely unnoticed unless a DLQ (with its own CloudWatch alarm watching its queue depth) is explicitly configured to catch it.


Container and Lambda Security Fundamentals#

A focused recap of the security controls specific to this part's services — Part 9 covers AWS security services in full depth, but these container/serverless-specific points are worth flagging here directly.

  • Every ECS task and Lambda function should have its OWN, narrowly-scoped IAM role (the taskRoleArn pattern already shown earlier in this part) — never a broad, shared role reused across unrelated services, directly reinforcing the least-privilege theme from Part 2.
  • Never bake secrets into a container image or Lambda deployment package. Use Secrets Manager or Parameter Store (both covered in depth in Part 9), referenced at runtime — a container image with an embedded secret remains a leak risk for as long as that image tag exists anywhere, including in ECR's image history.
  • Scan every image on push (scanOnPush=true, already shown in this part's ECR section) — directly reusing the container image scanning discipline from the DevSecOps series (Part 3).
  • Lambda functions should follow least privilege as strictly as any other identity — a function that only reads from one S3 prefix should never have a role granting s3:* on every bucket.

Part 7 CLI Cheat Sheet#

AreaCommandPurpose
ECRaws ecr create-repository / get-login-passwordCreate a registry, authenticate Docker
ECSaws ecs create-clusterCreate an ECS cluster
ECSaws ecs register-task-definitionRegister a Task Definition
ECSaws ecs create-serviceCreate a Service (keeps tasks running)
ECSaws ecs update-serviceUpdate desired count / deployment config
ECS scalingaws application-autoscaling register-scalable-targetEnable Service Auto Scaling
App Runneraws apprunner create-serviceSimplest managed container service
Lambdaaws lambda create-functionCreate a function
Lambdaaws lambda invokeInvoke a function directly
Lambdaaws lambda put-function-concurrencySet reserved concurrency
Lambdaaws lambda put-provisioned-concurrency-configSet provisioned (pre-warmed) concurrency
Lambdaaws lambda publish-layer-versionPublish a shared Layer
Step Functionsaws stepfunctions create-state-machineCreate a multi-step workflow

Common Mistakes#

MistakeWhy It's WrongFix
Treating Fargate as a competitor to EKSFargate is a compute engine EKS itself can run on — not a separate orchestratorUnderstand orchestrator (ECS/EKS) and compute engine (EC2/Fargate) as two independent decisions
Scaling an ECS Service on the EC2 launch type without also scaling the underlying cluster capacityTasks get stuck in PENDING if the cluster has no room, even though the service "wants" to scaleConfigure Cluster Auto Scaling/Capacity Providers alongside Service Auto Scaling, or use Fargate to avoid this entirely
Attaching every Lambda function to a VPC "just in case"Adds unnecessary NAT Gateway dependency and historically added cold-start latency, for functions that never actually needed private resource accessOnly attach Lambda to a VPC when it genuinely needs to reach a private resource
Using Provisioned Concurrency on every Lambda function by defaultReintroduces a standing cost, defeating Lambda's core "zero cost while idle" advantageReserve Provisioned Concurrency for genuinely latency-critical, user-facing functions only
Choosing EKS purely because it's more popular, without a genuine need for its ecosystem or portabilityTakes on real, unnecessary operational complexity compared to a simpler ECS setupChoose based on actual requirements — Kubernetes ecosystem needs, multi-cloud portability, or existing team expertise
Bundling large, shared dependencies into every Lambda function's own deployment packageDuplicates the same code across every function, complicating updatesExtract shared dependencies into a Lambda Layer
Hand-rolling multi-step orchestration logic inside a single, growing Lambda functionBecomes an unmaintainable tangle of retry/error-handling logic as the workflow growsUse Step Functions once a workflow needs genuine multi-step sequencing, branching, or retry policies

Worked Practice Problems#

Problem 1: A team runs an ECS service on the EC2 launch type and configures Service Auto Scaling to add more tasks under load, but during a traffic spike, CloudWatch shows the desired task count rising while the actual running task count stays flat, with several tasks stuck in PENDING. What's the likely cause, and what's the fix?

Answer: The underlying EC2 cluster capacity hasn't scaled to match the service's new desired task count — on the EC2 launch type, ECS Service Auto Scaling (task count) and Cluster Auto Scaling (EC2 instance count) are two separate, independent scaling mechanisms that both need to be correctly configured together. If the cluster's EC2 instances are already at full capacity, new tasks have nowhere to be placed and remain PENDING indefinitely, regardless of how high the service's desired count climbs. The fix is configuring ECS Capacity Providers (or a directly-managed Cluster Auto Scaling policy) so the underlying EC2 fleet scales in tandem with task demand — or, more simply, switching the service to the Fargate launch type, which eliminates this entire class of problem since there's no underlying cluster capacity to separately manage.

Problem 2: An application team notices their user-facing Lambda-based API experiences noticeably slow response times specifically for the first request after a period of low traffic, but subsequent requests are fast. Product management considers this unacceptable for their latency SLA (SRE Fundamentals series, Part 1). What's causing this, and what Lambda feature directly addresses it?

Answer: This is a classic cold start — after a period without invocations, Lambda's warm execution environments expire, and the next invocation must wait for a new environment to be provisioned and initialized before it can run, adding real, measurable latency specifically to that first request. Subsequent requests, arriving while the environment is still warm, skip this penalty entirely, matching the described symptom precisely. Provisioned Concurrency directly addresses this by keeping a specified number of execution environments pre-warmed at all times, eliminating the cold-start penalty for invocations within that provisioned capacity — the tradeoff, worth stating honestly, is that Provisioned Concurrency reintroduces a standing cost (you pay for the reserved warm capacity continuously, whether invoked or not), which is the right call here specifically because the team has an explicit latency SLA that a cold start would violate.

Problem 3: A platform team is deciding whether to migrate an existing containerized application from ECS on Fargate to EKS, motivated by a desire to "use the same technology as everyone else in the industry." No specific technical requirement (Kubernetes ecosystem tooling, multi-cloud portability, existing team expertise) is driving the request. How would you evaluate this proposal?

Answer: This proposal should be scrutinized carefully rather than approved by default — "use the same technology as everyone else" is not, by itself, a technical requirement, and migrating to EKS introduces genuine additional operational complexity (a steeper learning curve, the full Kubernetes API surface, an additional per-hour control plane cost) without a concrete benefit identified to justify it. The right response is asking what SPECIFIC capability EKS would provide that ECS on Fargate currently lacks — if the honest answer is "none, specifically," the migration is a case of unnecessary complexity for its own sake, exactly the kind of decision this part's "ECS vs EKS" framework warns against making without a genuine, named requirement driving it. If a real requirement does emerge (e.g. the team wants to adopt a specific Kubernetes-ecosystem tool, or genuinely needs multi-cloud portability), that becomes the actual, defensible justification — worth surfacing explicitly rather than deciding based on industry popularity alone.

Problem 4: A team discovers, weeks after an incident, that a Lambda function processing order confirmations had been silently failing for several days on a subset of malformed events, and no error, alert, or record of the failure exists anywhere — the failed events are simply gone. What's the root cause of this silent data loss, and what should have prevented it?

Answer: The root cause is a missing Dead-Letter Queue (or equivalent failure Destination) on the function — Lambda's default behavior for an asynchronous invocation that fails even after exhausting its automatic retries is to simply discard the event, with no error surfaced by default and no record retained anywhere that the failure ever happened. This is exactly why a DLQ should be treated as a mandatory, not optional, configuration for any Lambda function handling asynchronous, business-critical events: routing failed invocations to an SQS DLQ (with its own CloudWatch alarm on queue depth, connecting to the alerting-design discipline from the Observability series) would have surfaced this failure immediately as it started happening, rather than leaving it completely invisible until a customer complaint or downstream discrepancy eventually surfaced it weeks later.

Problem 5: A team running Fargate-based ECS tasks in production needs to debug a live issue affecting one specific running task, but reports they "can't SSH into it to check what's happening." What's the correct way to get interactive access to a running Fargate container, and why is SSH not an option here?

Answer: SSH isn't an option because Fargate tasks have no underlying EC2 instance at all to SSH into — Fargate is a fully serverless compute engine (this part's earlier distinction), so there's fundamentally no host-level access path the way there would be on the EC2 launch type. The correct approach is ECS Exec, enabled on the service (--enable-execute-command) and invoked via aws ecs execute-command, which opens an interactive shell directly inside the running container's own environment, authenticated and authorized entirely through IAM — the same zero-open-port, IAM-authenticated access model already established as the standard for EC2 access via SSM Session Manager in Part 3, just applied to a running container instead of a virtual machine.


Summary and What's Next#

  • AWS offers three distinct ways to run containers — ECS, EKS (fully covered in the Kubernetes Deep Dive series), and Fargate — where Fargate is a serverless COMPUTE ENGINE usable by either orchestrator, not a competing orchestrator itself.
  • ECS trades some of Kubernetes's flexibility and ecosystem for a meaningfully smaller operational learning curve — Task Definitions, Tasks, and Services map directly onto Kubernetes's Pod specs, Pods, and Deployments.
  • The EC2 launch type requires managing underlying instance capacity yourself (and scaling it in tandem with task demand); Fargate eliminates this entirely, billing per-task instead of per-instance.
  • Lambda is AWS's purest serverless compute model — zero cost while idle, triggered by a wide range of native event sources, with cold starts as its core, well-understood operational characteristic.
  • Reserved Concurrency sets a ceiling protecting other functions; Provisioned Concurrency sets a floor eliminating cold starts, at the cost of reintroducing standing spend.
  • Step Functions provides managed orchestration for multi-step Lambda workflows, avoiding hand-rolled retry/branching logic inside application code.
  • Choosing between EC2, ECS, Fargate, EKS, and Lambda comes down to traffic shape, whether the workload benefits from a persistent warm process, and genuine organizational/ecosystem requirements — never popularity alone.

Continue to Part 8 (08-load-balancing-cdn-and-dns.md) to see how traffic actually reaches these compute services — Application/Network Load Balancers, CloudFront, and Route 53.