Lambda, containers & PaaS

Serverless, Fargate, ECS/EKS, Beanstalk.

Intermediate30 min · lesson 5 of 15

There are four ways to get a parcel across town. Drive it yourself in a truck you own and maintain. Hire a dispatcher to run a fleet of your trucks. Hand it to a courier who owns the vehicles and charges you by the mile. Or drop it in a mailbox and let the postal service do everything for a flat per-item fee. AWS compute splits the same four ways. The truck is EC2 (Elastic Compute Cloud), the rented server you met last lesson. The dispatcher is ECS or EKS, the two AWS container schedulers, running on EC2 instances you own. The courier is Fargate. The mailbox is Lambda. Your cargo, the code, is identical in all four. What changes is how much delivery machinery you own, patch, and pay for while it sits idle.

Lambda: code that wakes up when something happens

A vending machine sits dark and idle until somebody drops a coin in. Lambda is that vending machine for your code. You upload a function (your code plus its configuration) and there is no server anywhere for you to see, patch, or size. Nothing runs until an event fires. An event is a JSON document (JavaScript Object Notation, a plain-text way of writing down structured data) saying that something happened: a file landed in an S3 bucket (Simple Storage Service, the AWS object store), an HTTP request arrived through API Gateway (the AWS front door for application programming interfaces), a message showed up on an SQS queue (Simple Queue Service). Lambda hands that JSON to your handler, the one named entry point in your code, then starts the meter. You pay per millisecond of run time multiplied by the memory you allocated, a unit AWS calls GB-seconds, plus a small flat fee per million requests. No event, no charge. A Lambda-backed endpoint that receives no traffic overnight costs you exactly zero overnight.

Under the hood, every invocation runs in its own Firecracker microVM, a stripped-down virtual machine that boots in milliseconds. The first request into a fresh environment pays a cold start while Lambda downloads your package and starts the runtime, typically 100 to 500 ms for Python and longer for anything running on the Java virtual machine. After that the environment is kept warm and reused. Scaling is the part that surprises people, because there is nothing to configure. Every event that arrives at the same moment gets its own environment, so 400 parallel S3 uploads become 400 environments and you touch no dial. One quirk is worth memorizing, because the exam loves it: there is no CPU setting. CPU scales with the memory slider, and you reach one full vCPU (virtual CPU, roughly one processor core's worth of work) at 1,769 MB. A function stuck waiting on the processor often gets *faster and cheaper* when you give it more memory, because it finishes in a fraction of the time.

Ship one from the command line

A Lambda function is a zip file plus an execution role, the IAM role (Identity and Access Management, the AWS permissions system) that Lambda assumes so it can act on your behalf. Think of the role as a temporary badge the function wears while it runs, and only while it runs. Give every function its own badge with the narrowest permissions it needs, exactly as the IAM lesson showed. Package, create, invoke:

deploy and invoke a Lambda function
# package a one-file Python handler and create the function
zip function.zip lambda_function.py
aws lambda create-function \
--function-name thumb-generator \
--runtime python3.12 \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip \
--role arn:aws:iam::123456789012:role/lambda-exec-role \
--timeout 30 --memory-size 512
# {
# "FunctionName": "thumb-generator",
# "Runtime": "python3.12",
# "MemorySize": 512,
# "Timeout": 30,
# "PackageType": "Zip",
# "State": "Pending"
# }
# invoke it synchronously and read the result
aws lambda invoke \
--function-name thumb-generator \
--cli-binary-format raw-in-base64-out \
--payload '{"bucket": "img-uploads", "key": "cat.jpg"}' \
response.json
# {
# "StatusCode": 200,
# "ExecutedVersion": "$LATEST"
# }
cat response.json
# {"thumbnail": "thumbs/cat.jpg", "duration_ms": 214}

Two details in that output catch people out. The create-function response says "State": "Pending" because creation happens in the background; the function flips to Active a few seconds later. And look at --cli-binary-format raw-in-base64-out on the invoke. AWS CLI v2 (the command line interface, version 2) treats --payload as base64-encoded by default, so leaving that flag off is the single most common reason a first invoke dies with an *Invalid base64* error.

Every limit here is an exam question in disguise

Lambda's hard limits are really a map of where Lambda stops being the right answer, and the SAA exam (Solutions Architect Associate) pokes at them constantly. A function may run for 900 seconds (15 minutes) and no longer. Memory runs from 128 MB to 10,240 MB. Ephemeral /tmp scratch space runs from 512 MB to 10 GB and vanishes when the environment does. Deployment packages stop at 50 MB zipped, 250 MB unzipped, though a function packaged as a container image can reach 10 GB. And you get 1,000 concurrent executions per Region by default, meaning 1,000 copies of your functions running at the same instant. That last one is a soft limit, so you can ask AWS to raise it; brand-new accounts start lower and AWS lifts the quota automatically as your usage grows. Do not take any of these numbers from a blog post. Ask the API:

read the limits, then fence the function
# what is this function allowed to use?
aws lambda get-function-configuration \
--function-name thumb-generator \
--query '{Mem:MemorySize, Timeout:Timeout, TmpMB:EphemeralStorage.Size}'
# {
# "Mem": 512,
# "Timeout": 30,
# "TmpMB": 512
# }
# what are the Region-wide account limits?
aws lambda get-account-settings --query 'AccountLimit'
# {
# "TotalCodeSize": 322122547200,
# "CodeSizeUnzipped": 262144000, <- 250 MB unzipped
# "CodeSizeZipped": 52428800, <- 50 MB zipped upload
# "ConcurrentExecutions": 1000, <- shared by EVERY function
# "UnreservedConcurrentExecutions": 1000
# }
# cap this function so a stampede can't drain the shared pool
aws lambda put-function-concurrency \
--function-name thumb-generator \
--reserved-concurrent-executions 100
# {
# "ReservedConcurrentExecutions": 100
# }
One runaway function can starve everything else in the Region
That pool of 1,000 concurrent executions belongs to the account, not to any single function. *Every* function in the Region draws from it. An S3 trigger misconfigured to write back into the bucket that triggers it, or a retry storm against a downstream service that is already failing, can swallow the whole pool. Your customer-facing API functions then start throwing TooManyRequestsException while their own code is perfectly fine. In production, put reserved concurrency on any function attached to a high-volume trigger, and set an alarm on the Throttles metric in CloudWatch (the AWS monitoring service) so you hear about it before your users tell you.

Containers: two orchestrators, two places to run them

A shipping container fits any truck, ship, or crane because the box is standard even when the cargo is not. A software container pulls the same trick. It packs an application together with everything it depends on so the thing behaves identically on your laptop and in production, and it shares the host machine's kernel instead of booting an operating system of its own. Run more than a handful and you need an orchestrator: software that decides which host each container lands on, restarts the ones that crash, and adds or removes copies as load moves. AWS sells two. ECS (Elastic Container Service) is the AWS-native one, and it charges nothing for the control plane, the management brain doing the scheduling. EKS (Elastic Kubernetes Service) is managed Kubernetes, the open-source orchestrator, at $0.10 per cluster-hour or roughly $73 a month, aimed at teams that want that ecosystem or the option to run the same setup on another cloud. Three words carry most of the ECS vocabulary. A task definition is the blueprint (image, CPU, memory, ports). A task is one running copy of that blueprint. A service keeps N tasks alive behind a load balancer, the traffic cop that spreads requests across the copies.

Which orchestrator you pick is one axis. The launch type is a separate axis answering a different question: where do the containers actually run? The EC2 launch type puts tasks on instances you register, patch, and pay for, which is what you want when you need GPUs (graphics processing units, the chips that make machine-learning work fast), a specific instance family, or an agent that has to live on the host. The Fargate launch type is serverless containers. AWS builds an isolated microVM sized to each task, anywhere from 0.25 vCPU up to 32 vCPU and as much as 244 GB of memory, then bills per vCPU-second and GB-second. Nothing to patch. No bin-packing puzzle, which is the game of fitting differently sized containers onto fixed-size hosts. No capacity planning. The catch shows up on the exam again and again: Fargate has no GPU support, which pushes every GPU workload back onto the EC2 launch type.

run the same image on Fargate — no hosts anywhere
aws ecs create-cluster --cluster-name web-api
# "cluster": { "clusterName": "web-api", "status": "ACTIVE" }
# the task definition is the blueprint: image, CPU, memory, ports
aws ecs register-task-definition \
--family api --requires-compatibilities FARGATE \
--network-mode awsvpc --cpu 256 --memory 512 \
--execution-role-arn arn:aws:iam::123456789012:role/ecsTaskExecutionRole \
--container-definitions '[{"name":"api","image":"123456789012.dkr.ecr.eu-west-1.amazonaws.com/api:1.4.2","portMappings":[{"containerPort":8080}]}]'
# "taskDefinition": { "family": "api", "revision": 1, "status": "ACTIVE" }
# launch two copies into private subnets
aws ecs run-task --cluster web-api \
--launch-type FARGATE --count 2 \
--task-definition api:1 \
--network-configuration 'awsvpcConfiguration={subnets=[subnet-0ab1c2d3e4f5a6b7c],securityGroups=[sg-0f9e8d7c6b5a4d3e2],assignPublicIp=DISABLED}'
# "tasks": [ { "lastStatus": "PROVISIONING", "cpu": "256", "memory": "512" }, ... ]
aws ecs describe-tasks --cluster web-api --tasks 7f4b9d2ea8c14f0e9b6d3a1c5e7f2a4b \
--query 'tasks[].{Status:lastStatus, Health:healthStatus}'
# [ { "Status": "RUNNING", "Health": "UNKNOWN" } ]
# healthStatus stays UNKNOWN until the task definition declares a container health check

Elastic Beanstalk: built for you, still yours to open up

Platform as a Service (PaaS) is the meal-kit version of infrastructure. You bring the recipe, the box brings everything else. Hand Elastic Beanstalk a zip of your web app and it stands up the EC2 instances, the load balancer, the Auto Scaling group that adds and removes instances as load changes, and the CloudWatch alarms. What separates it from closed PaaS products is that nothing is hidden. Every resource it creates sits in your own account, visible and editable, so you can reach in and change a security group without walking away from the platform. Beanstalk adds no charge of its own; you pay for the resources it builds. Exam trigger phrase: *"deploy a web application quickly without managing infrastructure"* means Beanstalk.

Beanstalk: an environment from two commands
# platform names version frequently — always look up the current one
aws elasticbeanstalk list-available-solution-stacks \
--query 'SolutionStacks[?contains(@, `Python 3.12`)] | [0]'
# "64bit Amazon Linux 2023 v4.7.1 running Python 3.12"
aws elasticbeanstalk create-application --application-name shop
aws elasticbeanstalk create-environment \
--application-name shop --environment-name shop-prod \
--solution-stack-name "64bit Amazon Linux 2023 v4.7.1 running Python 3.12"
# {
# "EnvironmentName": "shop-prod",
# "Status": "Launching",
# "Health": "Grey"
# }
# ~5 minutes later the EC2 instances, security group, and load
# balancer exist in your account as ordinary, inspectable resources.

How to choose: take the least-ops option that works

Choosing AWS compute: default to least-ops, branch right only when forced
Pick the least-operational option that meets the real requirement
Start at Lambda; every layer you hand to AWS is one you no longer patch, scale, or wake up for
Event-driven, spiky or idle-heavy, runs in ≤15 min
Lambda
Per-ms billing (GB-s); one environment per event; zero cost when idle
Long-running containers, minimal ops, no GPU needed
Fargate
Serverless containers billed per vCPU-s + GB-s; no hosts to patch
Need GPUs, specific instance families, host daemons, or Kubernetes
ECS/EKS on EC2
You register and patch the nodes; EKS adds $0.10/cluster-hour
Full OS/kernel control, or steady fully-utilized 24/7 (Reserved is cheaper)
EC2 / Beanstalk
Most ops owned; Beanstalk is a PaaS wrapper over inspectable EC2 resources
You branch to exactly one service based on a hard requirement. You do not walk through all four. Move right only when runtime >15 min, GPUs, kernel access, or cost math forces it.

Your default as an architect is the least-operational option that meets the *real* requirement, because every layer you hand to AWS is a layer you never patch, never scale, and never get paged about at 3 a.m. Move rightward only when something forces you: work that runs beyond 15 minutes, GPU workloads, kernel modules or host agents. Cost forces you too. A steady workload that keeps a machine busy around the clock is usually cheaper on Reserved-priced EC2 (a one or three year commitment in exchange for a discount) than on per-millisecond Lambda, while Lambda wins on spiky traffic and long idle stretches. The exam keyword mapping is short enough to memorize: *event-driven, no management* → Lambda; *containers, minimal overhead* → Fargate; *Kubernetes* → EKS; *deploy my web app* → Beanstalk.

Notice what all four models share: the compute is disposable. Lambda environments evaporate and take their /tmp with them. Fargate tasks get replaced without ceremony. Beanstalk rebuilds instances during a deployment. None of them is a safe home for anything you care about keeping. Durable state has to live in storage built for the job, and choosing between object, block, and file storage is a design decision in its own right. That is the next lesson: S3, EBS & EFS.

A few sharp edges before you go. Lambda is at its best when work arrives in bursts and each unit finishes in seconds or a couple of minutes. It is a poor home for a sticky WebSocket farm (long-lived two-way connections that pin a user to one process), or a 45-minute video encode with no checkpointing. Cold starts, package size, and the 15-minute ceiling are constraints you design around, not footnotes you discover in production.

ECS on EC2 still leaves you owning the nodes. Fargate hands the nodes back and leaves you owning the task definition, plus a bill for vCPU and memory while the task runs. EKS gives you the whole Kubernetes API surface, a gift when your team already lives in Kubernetes and an expensive tax when all you wanted was something to run one container. Beanstalk is the lever you pull when the team is small, the app is conventional, and nobody wants to assemble a stack from parts.

The least-ops rule is exam gold. If the problem fits a managed runtime like Lambda, Fargate, or RDS (Relational Database Service), do not go inventing a fleet of hand-fed EC2 pets. If you genuinely need the pets, say out loud who patches them and when.

Try this

Build the smallest Lambda that will run, invoke it once, and read what comes back. Delete the function when you are finished, and do this in a lab account, never a production one.

terminal
printf 'def handler(event, context):\n return {"ok": True, "echo": event}\n' > /tmp/app.py
cd /tmp && zip -q fn.zip app.py
aws lambda create-function --function-name saa-echo \
--runtime python3.12 --role arn:aws:iam::111122223333:role/lambda-basic \
--handler app.handler --zip-file fileb://fn.zip \
--query '{Name:FunctionName,Runtime:Runtime,Timeout:Timeout}' --output table
aws lambda invoke --function-name saa-echo --payload '{"ping":1}' /tmp/out.json
cat /tmp/out.json
output
------------------------------------------
| CreateFunction |
+-----------+-------------+--------------+
| Name | Runtime | Timeout |
+-----------+-------------+--------------+
| saa-echo | python3.12 | 3 |
+-----------+-------------+--------------+
{"ok": true, "echo": {"ping": 1}}

Takeaway

Match the compute model to two things: how long one unit of work runs, and how much of the operating system you want to own. Lambda and Fargate buy back your ops time. EC2 and EKS buy you control.

Next: take one service you run today and redraw it three ways, as Lambda, as Fargate, and as EC2. Then write down the first limit that would force you off each one.

Quick check
01Every Lambda function in your account draws from one concurrency pool per Region. A batch function wired to a high-volume S3 trigger hits a retry storm and drains that pool, so your customer-facing API functions start returning TooManyRequestsException even though nothing is wrong with their code. What is the right production fix?
Correct — Reserving a ceiling for the noisy function fences off its share and leaves headroom for the API functions. The lesson pairs this with an alarm on the Throttles metric.
Incorrect — It is a soft limit you can raise, but a retry storm will drain a bigger pool too. Without a fence you have only delayed the same outage.
Incorrect — The throttling comes from exhausted concurrency, not slow code. Extra memory cannot help when there is no free environment left to run in.
Incorrect — Pools are per-Region, so it would technically work, but relocating latency-sensitive user traffic to dodge a config mistake is drastic. Reserved concurrency fixes it where it is.
02A Lambda function is CPU-bound and running slowly, and you have noticed there is nowhere to assign it vCPUs directly. What does this lesson tell you to do?
Incorrect — You do control CPU, indirectly, so a rewrite is not the fix the lesson points to.
Correct — The lesson ties CPU to the memory setting, and a CPU-bound function that finishes in a fraction of the time frequently costs less even at higher memory.
Incorrect — Fifteen minutes is a hard ceiling, and timeout governs how long a function may run, not how much CPU it receives.
Incorrect — Ephemeral /tmp is scratch disk space and has nothing to do with CPU allocation.
03A workload runs long-lived containers that need GPU acceleration for machine-learning inference, and the team would rather not manage an orchestration control plane if it can be avoided. Which compute option fits BEST?
Incorrect — The lesson flags Fargate's lack of GPU support as a recurring exam discriminator, so it cannot run GPU workloads at all.
Incorrect — Lambda stops at 15 minutes and offers no GPU, so long-lived GPU inference is out.
Incorrect — Beanstalk is a PaaS wrapper for getting a conventional web app deployed, not an orchestrator for GPU container workloads.
Correct — With no GPU on Fargate, GPU containers have to run on the EC2 launch type under ECS or EKS, where you register GPU-capable instances yourself.

Related