We close the module with the piece that is still missing. Everything MercadoFresco has built so far starts from the same idea: there is a server switched on, waiting for work. The shop's EC2 instance is running even when it is Tuesday at 4 in the morning. So is the RDS instance. And all 730 hours of the month are paid for.
But there are tasks that do not fit that model. Generating the thumbnail of a photo when Luis uploads it happens twenty times a day and takes two seconds. Looking up the status of an order is a one-off call. Processing a delivery-route file happens once every morning. Keeping a server switched on for 730 hours to do 40 minutes of work a month is absurd.
AWS Lambda runs code only when something happens and only charges while it is running, with millisecond precision. In this lesson we finally connect the S3 event we left configured back in 02-03 and write the function that generates the thumbnails for MercadoFresco's product catalogue.
Contents
- What serverless computing is and how it differs from EC2
- The execution model: handler, event, context and response
- Cold starts and warm starts
- Concurrency: reserved, provisioned and the account limit
- Configuration: memory, CPU, timeout, variables and architecture
- The thumbnail function: the code
- Packaging with dependencies and layers
- Deploying from the console and from the CLI
- Permissions: the execution role and least privilege
- Logs in CloudWatch Logs and debugging
- Common event sources
- A second function: querying an order's status over HTTP
- The real limits you need to know
- Pricing, with MercadoFresco's concrete calculation
- Errors, retries and dead-letter queues
- Module recap: what is already in AWS and what is still missing
What serverless computing is and how it differs from EC2
"Serverless" does not mean there are no servers: it means they are not yours, you never see them and you do not pay for them when they are not working. AWS provisions the capacity, scales it and takes it away again, all of it invisible.
| EC2 | Containers (ECS/Fargate) | AWS Lambda | |
|---|---|---|---|
| Unit of deployment | A whole virtual machine | A container image | A function |
| Who manages the OS | You | AWS (with Fargate) | AWS |
| Start-up time | 30-60 s | 10-30 s | Milliseconds to 1-2 s |
| Billing | Per second switched on | Per second of task | Per ms of execution |
| Cost when idle | The full price | The full price | Zero |
| Maximum duration | Unlimited | Unlimited | 15 minutes |
| Scaling | Auto Scaling (minutes) | ECS service (seconds) | Automatic and instant |
| State on disk | Persistent (EBS) | Ephemeral | Ephemeral (512 MB in /tmp) |
| Control of the environment | Total | High | Limited |
| Typical use | Traditional applications, databases | Microservices, steady workloads | Events, short tasks, irregular traffic |
The criteria for choosing, in order of practical usefulness:
- Does the task take less than 15 minutes? If not, Lambda is ruled out.
- Is the traffic irregular or event-driven? Lambda wins by a mile. If it is steady and constant 24×7, a container or an EC2 works out cheaper.
- Do you need control of the operating system or state on disk? Then EC2 or containers.
- How much team time do you want to spend on infrastructure? Lambda is the least possible.
For MercadoFresco, generating thumbnails meets all four criteria perfectly: seconds of duration, triggered by sporadic events, stateless, and with nothing to administer.
The execution model: handler, event, context and response
A Lambda function is an ordinary function in your language with a specific signature:
def lambda_handler(event, context):
# event -> the data about what has happened (dict)
# context -> information about this particular execution
return {"result": "ok"}| Element | What it is |
|---|---|
| Handler | The entry point. It is declared as file.function, e.g. app.lambda_handler |
Event (event) |
A dictionary with the trigger's data. Its shape depends on the source: S3, API Gateway and SQS have very different structures |
Context (context) |
Metadata about the execution: aws_request_id, function_name, memory_limit_in_mb, get_remaining_time_in_millis() |
| Response | Whatever the function returns. With a synchronous invocation it reaches the caller; with an asynchronous one it is discarded |
The full life cycle, which is worth understanding properly because performance and cost both depend on it:
sequenceDiagram
participant E as Event (S3)
participant L as Lambda service
participant M as Execution environment
participant F as Function code
E->>L: Something happens (a photo is uploaded)
L->>L: Looks for an available environment
alt COLD start (no environment available)
L->>M: Creates a Firecracker microVM
M->>M: Downloads the code and the layers
M->>F: Runs the GLOBAL code (imports, boto3 clients)
Note over M,F: 100 ms - 2 s. IT IS BILLED (since 2024)
end
L->>F: Invokes lambda_handler(event, context)
F-->>L: Returns the response
Note over M: The environment is FROZEN, not destroyed
E->>L: Another event arrives within a few minutes
L->>F: Reuses the environment: WARM start
Note over M: After ~5-15 min unused, it is destroyed
This is where Lambda's most profitable optimisation comes from, and it is a single line of code:
import boto3
# CORRECT: outside the handler. It runs ONCE per environment and is reused
# by every subsequent invocation. Creating a boto3 client costs 100-300 ms.
s3 = boto3.client("s3")
def lambda_handler(event, context):
# It would be WRONG to create the client here: you would pay those 200 ms
# on EVERY invocation.
...And this is also where the matching trap comes from: the environment is reused, so global variables persist between invocations. That is handy for caches, but disastrous if you accumulate state by accident:
results = [] # DANGER: survives between invocations
def lambda_handler(event, context):
results.append(event) # grows unchecked until it exhausts the memoryCold starts and warm starts
A cold start is the time AWS takes to prepare a new environment: creating the microVM, downloading the code and the layers, initialising the interpreter and running the global code.
| Factor | Effect on the cold start |
|---|---|
| Language | Python and Node.js: 100-400 ms. Java and .NET: 1-4 s. Go and Rust: < 100 ms |
| Package size | The bigger it is, the longer the download takes |
| Initialisation code | Loading a large model or connecting to a database lengthens the start-up |
| VPC | It used to add 8-10 s; today the penalty is tens of ms |
| Memory allocated | More memory = more CPU = faster initialisation |
When it really matters: if the Lambda answers a synchronous request from a user (an API), an extra 1.5 seconds is unacceptable. If it processes an asynchronous event (a photo's thumbnail), nobody cares.
Mitigation strategies, from cheapest to most expensive:
- Move work into the global code and keep the package small. Free.
- Choose a lightweight language. Python or Node for latency-critical functions.
- Raise the memory. More CPU speeds up initialisation, and often the total cost does not go up because the execution is shorter.
- Provisioned concurrency. AWS keeps N environments always ready. It eliminates cold starts entirely, but you pay for it even when it is not used.
Concurrency: reserved, provisioned and the account limit
Lambda does not queue requests: it creates new environments. If 100 events arrive at the same time, 100 instances of the function run at once. That is its great strength and also its risk.
| Concept | What it is | What you pay |
|---|---|---|
| On-demand concurrency | Environments created as the events arrive | Only the execution |
| Account limit | Ceiling per region, 1,000 by default (can be raised) | — |
| Reserved concurrency | A slice of the limit set aside for one function | Nothing extra, but it comes off the total available |
| Provisioned concurrency | Pre-warmed environments always ready | Yes, even when idle |
Reserved concurrency has a double effect that is worth understanding:
- It guarantees that the function will always be able to use that amount, even when others are saturating the account.
- It caps that function at that maximum. It is a brake.
That brake is exactly what MercadoFresco needs in order to protect the database:
# The function that queries orders will NEVER open more than 20 connections to
# RDS, even if 5,000 simultaneous requests arrive. It protects the database
# from becoming the link that breaks.
aws lambda put-function-concurrency \
--function-name mercadofresco-estado-pedido \
--reserved-concurrent-executions 20 \
--profile mercadofresco-dev --region eu-west-1Without that limit, a traffic spike could open hundreds of connections to mercadofresco-pedidos
and bring the database down. It is a real and frequent case: Lambda scales, RDS does not.
Configuration: memory, CPU, timeout, variables and architecture
Memory and CPU go together
This is the parameter most people get wrong. In Lambda you only allocate memory, from 128 MB to 10,240 MB, and the CPU is allocated proportionally:
| Memory | Approximate vCPU | Comment |
|---|---|---|
| 128 MB | 0.08 | The minimum. Very slow for any computation |
| 512 MB | 0.29 | |
| 1,769 MB | 1.00 | A full core. The key reference point |
| 3,008 MB | 1.70 | |
| 10,240 MB | 6.00 | The maximum, with real multithreading |
The consequence is counter-intuitive and deserves a calculation. Take a function processing an image:
| Memory | Duration | Cost per 1,000 invocations |
|---|---|---|
| 512 MB | 8,000 ms | 512/1024 × 8 × 1,000 × 0.0000166667 = 0.0667 USD |
| 1,024 MB | 4,000 ms | 1 × 4 × 1,000 × 0.0000166667 = 0.0667 USD |
| 1,769 MB | 2,200 ms | 1.73 × 2.2 × 1,000 × 0.0000166667 = 0.0634 USD |
| 3,008 MB | 2,000 ms | 2.94 × 2 × 1,000 × 0.0000166667 = 0.0980 USD |
More memory can cost exactly the same or less, and be eight times faster. Setting 128 MB "to save money" is usually a mistake: the function takes so long that the cost does not drop and the latency goes through the roof. The AWS Lambda Power Tuning tool automates this analysis.
Timeout
From 1 second to 15 minutes (900 s), with 3 seconds as the default. The rule: set it a little above what the function actually takes, never at the maximum. A timeout of 900 s on a function that should take 2 s means a hang will be billed for 15 minutes.
Environment variables
aws lambda update-function-configuration \
--function-name mercadofresco-generar-miniaturas \
--environment "Variables={
BUCKET_DESTINO=mercadofresco-catalogo-fotos,
PREFIJO_MINIATURAS=miniaturas/,
ANCHO_MINIATURA=200,
NIVEL_LOG=INFO}" \
--profile mercadofresco-dev --region eu-west-1They are encrypted at rest automatically, but they are visible in the console to anyone with read permission. Never put passwords there: use Secrets Manager or Parameter Store (lesson 04-03), as we did with RDS in 02-04.
Architecture: x86_64 versus arm64 (Graviton)
| x86_64 | arm64 (Graviton2) | |
|---|---|---|
| Price | The reference | ~20 % cheaper |
| Performance | The reference | Equal or better on most workloads |
| Compatibility | Universal | Binary dependencies have to be compiled for ARM |
Choose arm64 unless a dependency stops you. It is a 20 % saving for changing one parameter. The only precaution is that libraries with native code —such as Pillow, which we are about to use— have to be packaged for the right architecture.
The thumbnail function: the code
Here is the goal. In lesson 02-03 we configured the notification on the mercadofresco-catalogo-fotos
bucket so that every .jpg uploaded under productos/ would trigger the function
mercadofresco-generar-miniaturas. Now we are going to write it.
"""
mercadofresco-generar-miniaturas
Generates a 200 px wide thumbnail every time a product photo is uploaded to
the mercadofresco-catalogo-fotos bucket under the productos/ prefix.
Trigger: s3:ObjectCreated:* event filtered by the prefix 'productos/'
and the suffix '.jpg' (configured in lesson 02-03).
Output: an object under the 'miniaturas/' prefix of the same bucket.
IMPORTANT: the output prefix is DIFFERENT from the input one. If we wrote the
thumbnail into 'productos/' it would generate a new event that would invoke
this function again: an infinite and very expensive loop.
"""
import io
import logging
import os
import urllib.parse
import boto3
from botocore.exceptions import ClientError
from PIL import Image
# --- GLOBAL code: it runs only once per execution environment ---
# Creating the client here saves 100-300 ms on every later invocation.
s3 = boto3.client("s3")
logger = logging.getLogger()
logger.setLevel(os.environ.get("NIVEL_LOG", "INFO"))
THUMBNAIL_PREFIX = os.environ.get("PREFIJO_MINIATURAS", "miniaturas/")
WIDTH = int(os.environ.get("ANCHO_MINIATURA", "200"))
JPEG_QUALITY = 82
def _thumbnail_key(source_key: str) -> str:
"""Turns 'productos/frutas/naranjas.jpg' into 'miniaturas/frutas/naranjas.jpg'.
The sub-prefix structure is preserved so that the thumbnail can be located
from the original without having to query any database.
"""
without_prefix = source_key.split("/", 1)[1] if "/" in source_key else source_key
return f"{THUMBNAIL_PREFIX}{without_prefix}"
def _generate(data: bytes) -> bytes:
"""Resizes keeping the aspect ratio and returns the bytes of the JPEG."""
with Image.open(io.BytesIO(data)) as img:
# Phone photos carry their orientation in the EXIF metadata; without
# this some thumbnails would come out rotated by 90 degrees.
img = img.convert("RGB")
height = int(img.height * (WIDTH / img.width))
# LANCZOS gives the best quality when downscaling. thumbnail() never
# enlarges, so an already small photo is left as it is.
img.thumbnail((WIDTH, height), Image.Resampling.LANCZOS)
output = io.BytesIO()
img.save(output, format="JPEG", quality=JPEG_QUALITY, optimize=True)
output.seek(0)
return output.read()
def lambda_handler(event, context):
"""Entry point. One S3 event can carry SEVERAL records."""
processed, failed = 0, 0
for record in event.get("Records", []):
bucket = record["s3"]["bucket"]["name"]
# Keys arrive URL-encoded: 'a+b.jpg' or '%C3%B1'.
# Without unquote_plus, a photo with spaces or accents gives NoSuchKey.
key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
# Defence in depth: even though the bucket filter already prevents it,
# we check that we are not processing a thumbnail.
if key.startswith(THUMBNAIL_PREFIX):
logger.warning("Ignoring %s: it is already a thumbnail", key)
continue
try:
logger.info("Processing s3://%s/%s", bucket, key)
original = s3.get_object(Bucket=bucket, Key=key)["Body"].read()
thumbnail = _generate(original)
destination = _thumbnail_key(key)
s3.put_object(
Bucket=bucket,
Key=destination,
Body=thumbnail,
ContentType="image/jpeg",
CacheControl="public, max-age=604800", # one week of caching
Metadata={"origen": key, "generado-por": context.function_name},
Tagging="Proyecto=mercadofresco&Componente=catalogo&Propietario=luis",
)
logger.info(
"Thumbnail created: s3://%s/%s (%d KB -> %d KB)",
bucket, destination, len(original) // 1024, len(thumbnail) // 1024,
)
processed += 1
except ClientError as e:
code = e.response["Error"]["Code"]
if code == "NoSuchKey":
# The object was deleted between the event and this execution.
# It is not a recoverable error: retrying would achieve nothing.
logger.warning("Object %s no longer exists, skipping it", key)
continue
logger.error("S3 error with %s: %s", key, e)
failed += 1
raise # re-raising triggers Lambda's automatic retry
except Exception as e:
logger.exception("Unexpected error with %s: %s", key, e)
failed += 1
raise
return {"processed": processed, "failed": failed}The details in that code are what separate a tutorial example from something that survives in production:
unquote_pluson the key. S3 encodes names in the event. Without this line, any photo with spaces or accented characters would giveNoSuchKey. It is the number one bug in S3 Lambdas.event["Records"]is a list. A single event can carry several records. Processing it withevent["Records"][0]works in testing and fails in production.- An output prefix different from the input one, plus the explicit check. Double protection against the recursive loop.
raiseon recoverable errors. Lambda automatically retries asynchronous invocations; swallowing the exception with areturnwould let the failure go unnoticed.NoSuchKeyis handled separately. It is a non-recoverable error: retrying it three times is time and money wasted.- Tagging the object with the project's scheme, consistent with what we have been doing.
Packaging with dependencies and layers
Pillow is not included in the Lambda environment: you have to supply it. There are three ways and
it is worth knowing when to use each one.
| Method | When | Limit |
|---|---|---|
| Inline code | A trivial function with no external dependencies | Edited in the console |
| ZIP archive | The usual choice: code + dependencies | 50 MB compressed, 250 MB uncompressed |
| Layer | Dependencies shared by several functions | 5 layers per function, 250 MB in total |
| Container image | Enormous dependencies (ML, ffmpeg) | 10 GB |
For MercadoFresco, a layer with Pillow is the right choice: it will be shared by the thumbnail function and by the ones that come later, and it keeps the code package small (faster cold starts and editing still possible in the console).
# --- Build the Pillow layer for arm64 ---
mkdir -p capa-pillow/python
# It is ESSENTIAL to build for the target platform: Pillow carries native code,
# and a macOS or x86 wheel would fail with "invalid ELF header".
pip install Pillow \
--target capa-pillow/python \
--platform manylinux2014_aarch64 \
--implementation cp \
--python-version 3.12 \
--only-binary=:all:
cd capa-pillow && zip -r ../capa-pillow.zip python && cd ..
# Publish the layer
LAYER_ARN=$(aws lambda publish-layer-version \
--layer-name mercadofresco-imagen \
--description "Pillow 10 for processing product photos (arm64)" \
--zip-file fileb://capa-pillow.zip \
--compatible-runtimes python3.12 \
--compatible-architectures arm64 \
--query 'LayerVersionArn' --output text \
--profile mercadofresco-dev --region eu-west-1)
echo "Layer published: $LAYER_ARN"The layer's directory structure is not negotiable: Python looks for libraries in /opt/python,
and AWS unpacks the layer into /opt. That is why everything has to hang from a directory called
exactly python/. Each language has its own path (nodejs/node_modules for Node.js).
# --- Package the function's code (no dependencies: they live in the layer) ---
zip funcion-miniaturas.zip lambda_function.pyDeploying from the console and from the CLI
From the console
- Console → Lambda → Ireland region → Create function.
- Author from scratch. Name:
mercadofresco-generar-miniaturas. Runtime: Python 3.12. Architecture: arm64. - Change default execution role → create a new one (we will tighten it in the next section).
- Paste the code into the editor or upload the ZIP.
- Configuration → General configuration: memory 1,024 MB, timeout 30 s.
- Configuration → Environment variables: the four we defined earlier.
- Layers → Add a layer →
mercadofresco-imagen. - Triggers → Add trigger → S3 → bucket
mercadofresco-catalogo-fotos, eventAll object create events, prefixproductos/, suffix.jpg. - Tags: the project's mandatory scheme.
From the CLI
aws lambda create-function \
--function-name mercadofresco-generar-miniaturas \
--runtime python3.12 \
--architectures arm64 \
--handler lambda_function.lambda_handler \
--role arn:aws:iam::111122223333:role/rol-lambda-miniaturas \
--zip-file fileb://funcion-miniaturas.zip \
--layers "$LAYER_ARN" \
--memory-size 1024 \
--timeout 30 \
--environment "Variables={
PREFIJO_MINIATURAS=miniaturas/,
ANCHO_MINIATURA=200,
NIVEL_LOG=INFO}" \
--tags Proyecto=mercadofresco,Entorno=produccion,Componente=catalogo,\
Propietario=luis,CentroCoste=marketing \
--profile mercadofresco-dev --region eu-west-1
# Allow S3 to invoke the function. Without this, the event fires
# and nothing happens, with no visible error message anywhere.
aws lambda add-permission \
--function-name mercadofresco-generar-miniaturas \
--statement-id permitir-s3-catalogo \
--action lambda:InvokeFunction \
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::mercadofresco-catalogo-fotos \
--source-account 111122223333 \
--profile mercadofresco-dev --region eu-west-1Updating the code after a change:
zip funcion-miniaturas.zip lambda_function.py
aws lambda update-function-code \
--function-name mercadofresco-generar-miniaturas \
--zip-file fileb://funcion-miniaturas.zip \
--publish \
--profile mercadofresco-dev --region eu-west-1--publish creates an immutable numbered version. Combined with aliases (produccion,
pruebas), it allows gradual deployments by sending, for example, 10 % of the traffic to the new
version. It is another piece against MercadoFresco's problem 4, the risky deployments, which is
tackled head-on in module 8.
Testing without uploading anything to S3:
cat > evento-prueba.json <<'JSON'
{
"Records": [{
"eventName": "ObjectCreated:Put",
"s3": {
"bucket": {"name": "mercadofresco-catalogo-fotos"},
"object": {"key": "productos/frutas/naranjas-valencia-1kg.jpg"}
}
}]
}
JSON
aws lambda invoke \
--function-name mercadofresco-generar-miniaturas \
--payload fileb://evento-prueba.json \
--log-type Tail \
--query 'LogResult' --output text \
respuesta.json \
--profile mercadofresco-dev --region eu-west-1 | base64 -d--log-type Tail returns the last 4 KB of the log encoded in base64: the quickest way to debug
without leaving the terminal.
Cost warning. Lambda's free tier is permanent, not 12 months: 1 million requests and 400,000 GB-seconds a month, forever. This exercise will not cost you anything. Even so, delete the function when you finish if you are not going on:
aws lambda delete-function --function-name mercadofresco-generar-miniaturas. And remember to remove the bucket notification and the layer too if you are not using them.
Permissions: the execution role and least privilege
A Lambda function has no credentials of its own: it assumes an IAM execution role, and that role's permissions are exactly what the function can do. No more, no less.
The trust policy, which defines who can assume the role:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}And the permissions policy, which defines what it can do. Here is the principle of least privilege applied rigorously:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EscribirRegistros",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/mercadofresco-generar-miniaturas:*"
},
{
"Sid": "LeerSoloLasFotosOriginales",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/productos/*"
},
{
"Sid": "EscribirSoloEnMiniaturas",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:PutObjectTagging"],
"Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/miniaturas/*"
}
]
}Four deliberate decisions, and each one avoids a real problem:
s3:GetObjectonly underproductos/. If the function is compromised, it cannot read the sales reports or the database backups.s3:PutObjectonly underminiaturas/. Even if there were a logic bug, the function cannot overwrite the original photos. It is the last line of defence against the recursive loop: even if the bucket filter failed, the writer has no permission on the input prefix.- No
s3:DeleteObject. The function does not need to delete anything, so it cannot. - Logging scoped to its own log group, not to
*.
Compared with the common practice of attaching AmazonS3FullAccess "so that it works", the
difference between a minor incident and a full data leak lies in these three statements. The
complete logic of IAM, roles, policies and how they are evaluated is lesson 04-01.
Logs in CloudWatch Logs and debugging
Every function automatically writes to a log group called
/aws/lambda/<function-name>. Everything you print with print() or logger.info() ends up
there.
# Follow the logs live while you test
aws logs tail /aws/lambda/mercadofresco-generar-miniaturas --follow \
--profile mercadofresco-dev --region eu-west-1
# Search only the errors from the last hour
aws logs tail /aws/lambda/mercadofresco-generar-miniaturas --since 1h \
--filter-pattern "ERROR" \
--profile mercadofresco-dev --region eu-west-1At the end of every invocation, Lambda writes a REPORT line that is pure gold for tuning the
configuration:
REPORT RequestId: 8f3c1a2b-... Duration: 1843.21 ms Billed Duration: 1844 ms
Memory Size: 1024 MB Max Memory Used: 187 MB Init Duration: 412.55 msHow to read it:
| Field | What it tells you | What to do with it |
|---|---|---|
Duration |
The real execution time | If it grows, something has degraded |
Billed Duration |
What is billed (rounded to the ms) | The basis of the cost calculation |
Memory Size |
What you allocated | 1,024 MB |
Max Memory Used |
What it actually used | 187 MB out of 1,024: memory to spare |
Init Duration |
The cold-start time | It only appears on cold starts |
In this case the temptation is to drop the memory to 256 MB. Careful: remember that the CPU is proportional. With 256 MB the function would take around 7 seconds instead of 1.8, and the cost would be practically the same with four times the latency. The right move is to try 512 MB and measure.
Two other basic debugging capabilities:
- Automatic CloudWatch metrics:
Invocations,Errors,Duration,Throttles,ConcurrentExecutions. An alarm onErrors > 0is the bare minimum in production. - Structured JSON logs: by setting the log format to JSON, the fields can be queried with CloudWatch Logs Insights without parsing text.
Monitoring in depth —dashboards, alarms, retention, Logs Insights— is lesson 05-01, and the distributed tracing of a request that crosses several functions and services is 05-02.
Common event sources
Lambda integrates with more than 200 services. These are the ones that matter when you start:
| Source | Invocation | Event structure | MercadoFresco use case | Covered in |
|---|---|---|---|---|
| Amazon S3 | Asynchronous | Records[].s3 |
Generating thumbnails | 02-03 and this lesson |
| API Gateway / Function URL | Synchronous | requestContext, body |
Order status endpoint | This lesson |
| Amazon SQS | Batch polling | Records[].body |
Processing orders from the Friday peak | Module 7 (07-01) |
| Amazon SNS | Asynchronous | Records[].Sns |
Reporting a delivery incident | Module 7 (07-02) |
| Amazon EventBridge | Asynchronous | detail, source |
A scheduled task every morning | Module 7 (07-03) |
| DynamoDB Streams | Batch polling | Records[].dynamodb |
Reacting to data changes | Module 6 (06-02) |
| CloudWatch Logs | Asynchronous | Compressed data | Alerting on error patterns | Module 5 (05-01) |
The three invocation modes have very different consequences for error behaviour:
| Mode | Who waits for a response | Automatic retries | Example |
|---|---|---|---|
| Synchronous | The caller waits | None (the client handles them) | API Gateway |
| Asynchronous | Nobody waits | 2 retries | S3, SNS |
| Polling | Lambda reads from the source | Until it expires or goes to the failure queue | SQS, DynamoDB Streams |
A second function: querying an order's status over HTTP
The first function reacted to an internal event. Now we are going to expose a function to the internet so that MercadoFresco's mobile app can look up the status of an order.
There are two ways of putting HTTP in front of a Lambda:
| Function URL | API Gateway | |
|---|---|---|
| Configuration | One checkbox | A separate service to configure |
| Cost | Free | ~1 USD per million requests |
| Custom domain | No | Yes |
| Authentication | None or IAM | IAM, Cognito, JWT, custom authorisers |
| Rate limiting | No | Yes |
| Multiple routes | No, a single URL | Yes, full routing |
| When | Prototypes, internal webhooks | Production |
We start with a Function URL for simplicity, knowing that production will ask for API Gateway.
"""
mercadofresco-estado-pedido
Returns the status of an order from its identifier.
Invocation: Function URL (HTTP GET), e.g. /?pedido=48213
Reserved concurrency = 20: the RDS database cannot cope with hundreds of
simultaneous connections, so the brake goes here.
"""
import json
import logging
import os
import boto3
import psycopg
logger = logging.getLogger()
logger.setLevel("INFO")
# --- Global code: it runs once per environment ---
# The secret is read ONCE and reused by the following invocations.
_secrets = boto3.client("secretsmanager")
_credentials = json.loads(
_secrets.get_secret_value(SecretId=os.environ["MF_SECRETO_BD"])["SecretString"]
)
DSN = (
f"host={os.environ['MF_BD_HOST']} port=5432 dbname=pedidos "
f"user={_credentials['username']} password={_credentials['password']} "
f"sslmode=require connect_timeout=3"
)
# One connection per execution environment, reused between invocations.
# It is the reason why reserved concurrency caps the connections to RDS.
_connection = None
def _get_connection():
global _connection
if _connection is None or _connection.closed:
_connection = psycopg.connect(DSN)
return _connection
def _response(status: int, body: dict) -> dict:
"""The response format a Function URL or API Gateway expects."""
return {
"statusCode": status,
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
"body": json.dumps(body, ensure_ascii=False),
}
def lambda_handler(event, context):
# Query parameters arrive in queryStringParameters (it can be None).
params = event.get("queryStringParameters") or {}
order_id = params.get("pedido")
if not order_id or not order_id.isdigit():
return _response(400, {"error": "Missing 'pedido' parameter, or it is not numeric"})
try:
conn = _get_connection()
with conn.cursor() as cur:
# Parameterised query: NEVER concatenate the identifier into the SQL.
cur.execute(
"""
SELECT id, estado, creado_en, entrega_estimada, ciudad
FROM pedidos
WHERE id = %s
""",
(int(order_id),),
)
row = cur.fetchone()
if row is None:
return _response(404, {"error": "Order not found"})
return _response(200, {
"pedido": row[0],
"estado": row[1],
"creado_en": row[2].isoformat(),
"entrega_estimada": row[3].isoformat() if row[3] else None,
"ciudad": row[4],
})
except psycopg.OperationalError as e:
# The connection may have died because of an RDS failover (lesson 02-04).
# It is discarded so that the next invocation creates a new one.
logger.error("Database connection error: %s", e)
global _connection
_connection = None
return _response(503, {"error": "Service temporarily unavailable"})
except Exception:
logger.exception("Unexpected error while querying order %s", order_id)
# Never return the exception detail to the client: it leaks information.
return _response(500, {"error": "Internal error"})# Create the Function URL. AuthType NONE leaves it public: for testing only.
# In production you put API Gateway with authentication in front.
aws lambda create-function-url-config \
--function-name mercadofresco-estado-pedido \
--auth-type NONE \
--cors '{"AllowOrigins":["https://mercadofresco.example"],"AllowMethods":["GET"]}' \
--profile mercadofresco-dev --region eu-west-1
# Cap the concurrency to protect RDS
aws lambda put-function-concurrency \
--function-name mercadofresco-estado-pedido \
--reserved-concurrent-executions 20 \
--profile mercadofresco-dev --region eu-west-1Four design decisions that deserve attention:
- The database connection lives in the global scope and is reused. Opening a connection to PostgreSQL costs tens of milliseconds; doing it on every invocation would multiply the latency and saturate RDS.
- Reserved concurrency of 20 as a ceiling on connections. Without it, a spike could open hundreds
of connections and bring
mercadofresco-pedidosdown. It is the lesson from 02-04 applied: Lambda scales, RDS does not. (For demanding cases there is RDS Proxy, which pools connections; it is outside the scope of this lesson.) - A parameterised query with
%s, never string concatenation. SQL injection avoided. - Internal errors are not detailed to the client: they are logged to CloudWatch and a generic message is returned.
The real limits you need to know
| Limit | Value | Practical consequence |
|---|---|---|
| Maximum duration | 15 minutes | A longer process needs Step Functions (07-04), ECS or EC2 |
| Memory | 128 MB to 10,240 MB | The CPU is tied to this value |
| Compressed ZIP package | 50 MB (direct upload) | Above that, upload from S3 |
| Uncompressed package + layers | 250 MB | The limit that gets in the way most. Alternative: a container image (10 GB) |
Space in /tmp |
512 MB by default, up to 10,240 MB | Enough for the photos; configurable with --ephemeral-storage |
| Synchronous payload | 6 MB in and out | Do not return large files: store them in S3 and return a pre-signed URL (02-03) |
| Asynchronous payload | 256 KB | The S3 event fits with room to spare |
| Concurrency per region | 1,000 by default | Can be raised with a quota request |
| Environment variables | 4 KB in total | Do not put configuration files in there |
| Layers per function | 5 | Group related dependencies together |
The 15-minute limit is the one that conditions the most architectural decisions. If the nightly process that recompresses the catalogue's 40,000 photos takes 3 hours, it is not a job for a single Lambda: either it is split into 40,000 two-second invocations (which do fit perfectly), or it runs as a container task. Splitting is almost always the right answer with Lambda.
Pricing, with MercadoFresco's concrete calculation
Lambda charges for two things:
- Requests: 0.20 USD per million.
- Duration: 0.0000166667 USD per GB-second (x86_64; arm64 is 20 % less).
And the free tier is permanent, not 12 months: 1 million requests and 400,000 GB-seconds a month, forever.
Case 1: the thumbnail function. Luis uploads about 20 photos a day (600 a month), the function uses 1,024 MB and takes 1.8 seconds:
Requests: 600 → within the free million = 0.00 USD
Duration: 600 × 1.8 s × 1 GB = 1,080 GB-second
→ within the free 400,000 = 0.00 USD
--------------------------------------------------------------
Total = 0.00 USD/monthZero cost. The equivalent on a t3.micro EC2 kept running to do the same would be around 7.50
USD a month. This is exactly the use case Lambda exists for.
Case 2: the order status endpoint, in the realistic scenario. MercadoFresco receives around 900,000 queries a month, the function uses 512 MB and takes 120 ms, on arm64:
Requests: 900,000 → within the free million = 0.00 USD
Duration: 900,000 × 0.12 s × 0.5 GB = 54,000 GB-s
→ within the free 400,000 = 0.00 USD
--------------------------------------------------------------
Total = 0.00 USD/monthCase 3: growth to three more cities (problem 3). Ten times the traffic: 9 million queries a month:
Requests: (9,000,000 − 1,000,000) / 1,000,000 × 0.20 = 1.60 USD
Duration: 9,000,000 × 0.12 × 0.5 = 540,000 GB-second
(540,000 − 400,000) × 0.0000166667 = 2.33 USD
arm64 discount (−20 % on the duration) = −0.47 USD
------------------------------------------------------------------------
Total ≈ 3.46 USD/monthLess than 4 dollars a month to serve 9 million requests, with no server to administer, patch or watch over. When the load is irregular and event-driven, the serverless model is very hard indeed to beat.
The break-even point, worth keeping in mind: above a sustained utilisation of 50-60 % of a server, a container or a reserved EC2 works out cheaper. Lambda is expensive if you use it as though it were an always-on server.
Errors, retries and dead-letter queues
What happens when the function fails depends on the invocation mode:
| Mode | Behaviour on error |
|---|---|
| Synchronous (Function URL, API Gateway) | The error is returned to the client. No retries |
| Asynchronous (S3, SNS, EventBridge) | Lambda retries 2 times with increasing backoff; if it keeps failing, the event is discarded |
| Polling (SQS, DynamoDB Streams) | It retries until the message expires or goes to the dead-letter queue |
"The event is discarded" is the dangerous phrase. If the thumbnail function fails three times on one particular photo, that photo is left with no thumbnail and nobody finds out. The protection is an on-failure destination or a dead-letter queue (DLQ): somewhere for the events that could not be processed to end up, so they can be reviewed and reprocessed.
# Configure destinations: failures go to an SQS queue, successes are ignored.
aws lambda put-function-event-invoke-config \
--function-name mercadofresco-generar-miniaturas \
--maximum-retry-attempts 2 \
--maximum-event-age-in-seconds 3600 \
--destination-config '{
"OnFailure": {
"Destination": "arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas"
}
}' \
--profile mercadofresco-dev --region eu-west-1Three principles to respect from day one:
- Idempotency. Delivery is "at least once": the same photo can be processed twice. Our function satisfies this because regenerating a thumbnail over itself gives the same result. If the function incremented a counter or charged a payment, it would need explicit protection.
- Tell recoverable errors from unrecoverable ones. A network timeout deserves a retry; a
NoSuchKeydoes not. Retrying the unrecoverable is time and money thrown away. - Watch the DLQ. A failure queue nobody looks at is the same as not having one. An alarm on
ApproximateNumberOfMessagesVisible > 0is mandatory.
Here we are only setting out the concept. SQS is covered in lesson 07-01, and the complete patterns of idempotency, retries and dead-letter queues are lesson 07-05.
Module recap: what is already in AWS and what is still missing
This is the point in the course where it is worth looking up. MercadoFresco started module 2 with a physical server in the office; it finishes with this:
flowchart TB
subgraph AWS["Account 111122223333 - eu-west-1 (Ireland)"]
subgraph COMPUTE["Compute"]
ASG["Auto Scaling Group<br/>asg-mercadofresco-tienda<br/>2-4 t3.micro instances<br/>across two AZs (02-01)"]
L1["Lambda<br/>generar-miniaturas (02-05)"]
L2["Lambda<br/>estado-pedido (02-05)"]
end
subgraph DATA["Data"]
S3["S3 mercadofresco-catalogo-fotos<br/>versioning + lifecycle (02-03)"]
RDS["RDS PostgreSQL<br/>mercadofresco-pedidos<br/>Multi-AZ + replica (02-04)"]
EBS["EBS gp3 + DLM snapshots (02-02)"]
end
ASG --> EBS
ASG --> RDS
S3 -->|"ObjectCreated event"| L1
L1 --> S3
L2 --> RDS
end
U["MercadoFresco customers"] --> ASG
U --> L2
What is solved and what is not:
| Problem | Status | Where it was solved |
|---|---|---|
| 1. The Friday outages | Partial: there is Auto Scaling, the load balancer that spreads the traffic is missing | 02-01; completed in 03-03 |
| 2. Unreliable backups | Solved: EBS snapshots with DLM, S3 versioning, automated RDS backups with PITR | 02-02, 02-03, 02-04 |
| 3. Not being able to grow to more cities | Partial: the infrastructure now scales, global distribution and a decoupled architecture are missing | 02-01, 02-05; modules 3, 6 and 7 |
| 4. Risky deployments | Started: versioned launch templates, Lambda versions and aliases | 02-01, 02-05; solved in module 8 |
And what still does not exist, which is a great deal:
- A network of our own. Everything lives in the default VPC, with the instances exposed. We still have to design public and private subnets, route tables, and move the database into a subnet with no way out to the internet. Module 3.
- A load balancer. The four instances in the ASG do not get traffic spread across them, and there is no HTTPS or custom domain. 03-03 and 03-05.
- Content delivery. 97 % of the S3 bill was outbound transfer: CloudFront fixes that. 03-04.
- Identity and secrets done properly. We have used roles and Secrets Manager in passing; the full IAM structure, encryption with our own keys and protection against attacks are still missing. Module 4.
- Observability. We have scattered metrics and the odd log. We are missing a system of dashboards, alarms, traces and an audit trail of who did what. Module 5.
- Infrastructure as code. We have created everything by hand or with one-off commands: it is neither reproducible nor reviewable. Module 9.
Common Mistakes and Tips
- Creating boto3 clients inside the handler. You pay 100-300 ms on every invocation. Always in the global scope.
- Not decoding the S3 key with
unquote_plus. Any photo with spaces or accented characters will fail withNoSuchKey. It is the number one bug in S3-triggered Lambdas. - Processing only
event["Records"][0]. An event can carry several records. Always iterate. - Writing to the same prefix that triggers the function. Infinite loop. Filter by prefix and restrict the role's write permissions, as a double defence.
- Allocating 128 MB "to save money". The CPU is proportional to the memory: the function is so
slow that the cost does not drop and the latency soars. Measure with
Max Memory Used. - Leaving the timeout at 900 s by default. A hang will be billed for 15 minutes.
- Keeping state in global variables without control. The environment is reused and a global list grows until it exhausts the memory.
- Giving
AdministratorAccessto the execution role. Grant only the exact actions and resources the function needs. - Forgetting
lambda:add-permissionfor the event source. The trigger does not work and no visible error appears anywhere. - Opening an RDS connection per invocation with no concurrency limit. A traffic spike brings the database down. Connection in the global scope and reserved concurrency.
- Not configuring an on-failure destination or DLQ for asynchronous invocations. Failed events are discarded silently.
- Returning large files in the response. The synchronous limit is 6 MB. Store in S3 and return a pre-signed URL.
- Tip: start on arm64 unless a dependency prevents it. It is a 20 % saving for changing one parameter.
Exercises
Exercise 1: choosing the compute technology
For each MercadoFresco workload, decide between Lambda, EC2/container or a combination, and justify it with the criteria from the lesson:
- A) The PHP web shop, which serves traffic continuously with a peak on Fridays.
- B) Generating the invoice PDF when an order is marked as delivered (around 900 a day, 1.5 s each).
- C) Recompressing the catalogue's 40,000 photos, a process that today takes 3 hours straight.
- D) A job that every night at 03:00 exports the day's orders to S3; it takes 90 seconds.
- E) A recommendation service that loads a 4 GB model into memory and answers in under 50 ms.
Exercise 2: calculating and optimising the cost
The thumbnail function is configured with 1,024 MB and takes 1,800 ms. Tests with AWS Lambda Power Tuning give these results:
| Memory | Duration |
|---|---|
| 512 MB | 3,400 ms |
| 1,024 MB | 1,800 ms |
| 1,769 MB | 1,150 ms |
| 3,008 MB | 1,050 ms |
MercadoFresco is growing and 200,000 photos a month are now processed (already outside the free tier for duration). Calculate the duration cost of each configuration on arm64 (0.0000133334 USD per GB-second) and decide which one you would choose, taking latency into account as well.
Exercise 3: writing the least-privilege role
MercadoFresco adds a third function, mercadofresco-exportar-pedidos, which every night:
- Reads the day's orders from the RDS read replica.
- Gets the database credentials from Secrets Manager.
- Writes a CSV file to
s3://mercadofresco-informes-analitica/informes/YYYY/MM/. - Sends a notification to an SNS topic when it finishes.
- Writes its logs to CloudWatch Logs.
Write the execution role's permissions policy applying least privilege, and explain two permissions you deliberately do not include even though they might look necessary.
Solutions
Solution 1.
| Case | Choice | Justification |
|---|---|---|
| A) PHP web shop | EC2 with Auto Scaling (what we already built in 02-01) | Continuous traffic, a monolithic application with state on disk, legacy code. The sustained utilisation makes Lambda more expensive and the redesign does not pay off |
| B) Invoice PDF | Lambda | 900 × 1.5 s = 22.5 minutes of compute a day. It is a short, event-driven, stateless task: the textbook case. An EC2 would be idle 98 % of the time |
| C) Recompressing 40,000 photos | Lambda, but split up | 3 hours straight is over the 15-minute limit. The answer is not to change technology: it is to split it into 40,000 invocations of ~2 s running in parallel, and the total work drops from 3 hours to a few minutes. A valid alternative: a container task if the process cannot be parallelised |
| D) 90-second nightly export | Lambda + scheduled EventBridge | Well under 15 minutes, run once a day. Keeping a server for 90 seconds a day makes no sense. EventBridge is covered in 07-03 |
| E) Recommender with a 4 GB model | Container on ECS/Fargate | The model exceeds the package's 250 MB limit (a 10 GB container image could be used), but the real problem is the cold start: loading 4 GB takes seconds and the requirement is 50 ms. It needs an always-on process with the model in memory. Module 10 |
Solution 2.
Duration cost on arm64, for 200,000 invocations:
GB-second = invocations × (memory_GB) × (duration_s) Cost = (GB-second − 400,000 free) × 0.0000133334
| Memory | GB | Duration | GB-second | Billable | Cost |
|---|---|---|---|---|---|
| 512 MB | 0.50 | 3.4 s | 340,000 | 0 (within the free tier) | 0.00 USD |
| 1,024 MB | 1.00 | 1.8 s | 360,000 | 0 (within the free tier) | 0.00 USD |
| 1,769 MB | 1.73 | 1.15 s | 397,900 | 0 (within the free tier) | 0.00 USD |
| 3,008 MB | 2.94 | 1.05 s | 617,400 | 217,400 | 2.90 USD |
A revealing result: the first three configurations cost exactly the same, nothing, because they all fit inside the free tier of 400,000 GB-seconds. The decision, therefore, is not an economic one but a matter of latency and headroom.
Choice: 1,769 MB. Reasons:
- It is the fastest of the three free ones: 1.15 s against 3.4 s, almost three times better.
- 1,769 MB corresponds to a full vCPU, the point where Pillow stops being CPU-bound.
- It sits at 397,900 GB-seconds, very tight against the limit. If the volume grows it will start to cost: at 300,000 photos it would be 596,850 GB-seconds, that is 2.62 USD a month. Still irrelevant.
- 3,008 MB is ruled out: it only gains 100 ms over 1,769 MB (the function is no longer CPU-bound) and it multiplies the GB-second consumption, pushing it out of the free tier.
The general lesson: above one vCPU, carrying on raising the memory stops speeding things up and starts costing money. The sweet spot is usually near 1,769 MB for single-threaded work.
Solution 3.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Registros",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/mercadofresco-exportar-pedidos:*"
},
{
"Sid": "LeerSoloElSecretoDeLaBaseDeDatos",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:rds!db-a1b2c3d4-*"
},
{
"Sid": "EscribirSoloLosInformes",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mercadofresco-informes-analitica/informes/*"
},
{
"Sid": "PublicarSoloEnElTemaDeAvisos",
"Effect": "Allow",
"Action": "sns:Publish",
"Resource": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-exportaciones"
},
{
"Sid": "InterfazDeRedParaAccederALaVPC",
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface"
],
"Resource": "*"
}
]
}Two permissions deliberately NOT included:
-
s3:GetObjectands3:DeleteObjecton the reports bucket. The function only writes: it does not need to read or delete anything. Leaving them out means that, if the function were compromised by a malicious dependency, it could not exfiltrate the historical sales reports or destroy them. It is the same reasoning we applied to the thumbnail function. -
Any
rds:*permission. It feels counter-intuitive, because the function queries the database, but connecting to PostgreSQL with a username and password does not go through IAM: it is a TCP connection authenticated by the engine itself. Therds:*permissions are for administering instances (creating, deleting, modifying them), and the function must not be able to do any of that. Granting them would be giving permission to delete the database to a process that only needs to read a table. (The exception would be RDS IAM authentication, which does requirerds-db:connecton a specific user resource.)
Two further design details: the secret's ARN carries a trailing wildcard because Secrets Manager
adds a random six-character suffix to the name; and the ec2:*NetworkInterface permissions are
mandatory and with Resource: "*" when the function connects to resources inside a VPC —it is one
of the very few legitimate exceptions to least privilege, imposed by the service itself.
Conclusion
With this lesson MercadoFresco closes its first module of real services. You know what serverless computing is and, more importantly, when not to use it: you have compared Lambda with EC2 and with containers in a table of criteria and applied the rule that above 50-60 % sustained utilisation the model stops paying off. You have mastered the execution model —handler, event, context, response— and the environment's life cycle, from which the most profitable optimisation in Lambda follows: creating the boto3 clients in the global scope, outside the handler, because the environment is reused. You know what a cold start is, what lengthens it and the four ways of mitigating it, and you know that reserved concurrency is at once a guarantee and a brake, the brake that protects RDS from Lambda's ability to scale without limit.
You have understood the memory-CPU-cost relationship, the parameter most people get wrong, and you have worked it out with real numbers: allocating 128 MB "to save money" usually costs just as much and is eight times slower. You know to choose arm64 for a 20 % saving, to set a tight timeout and not to keep passwords in environment variables.
And you have written the function that closes the circle opened in lesson 02-03:
mercadofresco-generar-miniaturas fires on the s3:ObjectCreated event of the
mercadofresco-catalogo-fotos bucket, decodes the key properly with unquote_plus, iterates over
every record in the event, writes to a prefix different from the input one so as not to create an
infinite loop, tells recoverable errors from unrecoverable ones and tags what it produces. You have
packaged it with Pillow in a layer built for the right platform, deployed it from the console
and from the CLI with create-function and update-function-code --publish, and given it a
least-privilege execution role that can only read under productos/, can only write under
miniaturas/ and cannot delete anything. You have added a second function,
mercadofresco-estado-pedido, exposed through a Function URL, with the RDS connection reused
between invocations, parameterised queries, concurrency capped at 20 and errors that leak no
information to the client. You know how to read the REPORT line in CloudWatch Logs to tune the
memory, you have walked through the common event sources noting where each one is covered, you
know the real limits —15 minutes, 250 MB, 512 MB in /tmp, a 6 MB synchronous payload— and you
have done the pricing calculation that shows that serving 9 million requests costs less than 4
dollars a month. Finally, you know what happens when a function fails in each invocation mode and
why a dead-letter queue nobody watches is the same as not having one.
Recapping the whole module: MercadoFresco now has, in eu-west-1, an Auto Scaling group for the
shop spread across two Availability Zones, EBS disks with automated snapshots managed by Data
Lifecycle Manager, the photo catalogue in S3 with versioning and lifecycle rules, the orders
database in RDS PostgreSQL with Multi-AZ, a read replica and point-in-time recovery, and two Lambda
functions that react to events and answer over HTTP. Problem 2, the unreliable backups, is
solved from start to finish. The other three are under way but they are still
incomplete.
And what is missing is, precisely, what holds everything else up. All of this lives in the default
VPC: the instances are exposed where they should not be, the database shares network space with
the shop, there is nothing spreading the traffic across Friday's four instances, there is no HTTPS
or custom domain, and 97 % of the S3 bill is still outbound transfer with no cache. In module 3,
"Networking and Content Delivery", starting with lesson 03-01 "Amazon VPC", we will build
MercadoFresco's own network: public and private subnets across two Availability Zones, route tables,
gateways and a database that stops being reachable from the internet. On top of that network we will
then build the security groups, the load balancer that completes the answer to the Friday peak,
CloudFront to make the photos cheaper and faster, and Route 53 so that mercadofresco.example
finally points to the cloud.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
