In lesson 02-01 we sized the Auto Scaling group asg-mercadofresco-tienda for the Friday peak:
minimum 2, desired 2, maximum 4, with the cpu-objetivo-60 policy and the pico-viernes-tarde
scheduled action. The numbers worked out: the Friday peak from 17:00 to 21:00 is 900 orders per
hour, each instance handles 600 orders per hour, and with two instances there is capacity to
spare. In 03-01 and 03-02 we gave them a network of their own and a firewall that protects them.
And yet MercadoFresco still goes down on Fridays. Because the ASG can launch four instances, but nobody sends traffic to the last three. The domain points at one specific IP, that of the first machine, and the rest sit switched on and idle while that one drowns. The central piece is missing: something that receives all the traffic and shares it out.
That is a load balancer. In this lesson Marta builds alb-mercadofresco-tienda, connects it to
the Auto Scaling group and finishes solving the course's problem 1: the Friday outages. Along the
way we meet health checks, which are the part that has brought down more applications than anything
else by being badly configured.
Contents
- Why a load balancer is needed
- The types of load balancer: ALB, NLB, GWLB and the legacy CLB
- Anatomy of an ALB: listeners, rules and target groups
- Health checks: the dangerous part
- Integration with the Auto Scaling group
- Connection draining and deployments with no interruption
- Advanced routing by path and by host
- Redirects and fixed responses
- HTTPS on the load balancer: ACM, TLS policies and SNI
- Sticky sessions and why it is better not to need them
- Access logs and metrics
- Full creation from the CLI
- ALB cost and clean-up
- Friday's traffic, end to end
Why a load balancer is needed
A load balancer is a managed service that sits in front of a set of servers, receives every request and distributes them among the servers. What it brings goes well beyond the sharing out itself:
- A single front door. The domain
mercadofresco.examplepoints at the load balancer, not at any instance. Instances can be created, die or change IP without anyone noticing. - Health checks. The load balancer periodically asks each instance whether it is well, and stops sending it traffic if it does not answer. A failure stops being an outage.
- High availability across zones. Being in both public subnets, if the whole of
eu-west-1agoes down, traffic keeps coming in througheu-west-1b. - TLS termination. The certificate lives on the load balancer, not on every instance. A single place to renew.
- Real elastic scaling. The ASG registers new instances automatically, so the capacity added at 17:00 on Friday starts receiving traffic on its own.
- Isolation. The instances move to private subnets with no public IP: only the load balancer talks to them.
Without a load balancer each of those six points has to be solved by hand, and none is solved well.
The types of load balancer: ALB, NLB, GWLB and the legacy CLB
AWS offers three modern load balancers and one legacy one. Choosing badly breaks nothing, but it costs money or functionality.
| ALB Application LB |
NLB Network LB |
GWLB Gateway LB |
CLB (legacy) |
|
|---|---|---|---|---|
| OSI layer | 7 (application) | 4 (transport) | 3 (network) | 4 and 7, halfway |
| Protocols | HTTP, HTTPS, gRPC, WebSocket | TCP, UDP, TLS | IP (GENEVE) | HTTP, HTTPS, TCP, SSL |
| Routes by | Path, host, header, method, query string, source IP | Port and protocol | All traffic | Port |
| Added latency | ~milliseconds | ~microseconds | Low | Milliseconds |
| Static IP | No (DNS name) | Yes, one elastic IP per AZ | Not applicable | No |
| Targets | Instances, IPs, Lambda, nested ALB | Instances, IPs, ALB | Virtual appliances | Instances |
| Terminates TLS | Yes | Yes | No | Yes |
| Preserves the source IP | In the X-Forwarded-For header |
Yes, natively | Yes | In the header |
| Performance | Millions of requests/s | Millions of connections/s | High | Limited |
| Use case | Web applications and APIs | Games, IoT, MQTT, extreme latency, fixed IP | Third-party firewalls and IDS | Nothing new |
MercadoFresco's choice is the ALB, and the reasons are concrete:
- The traffic is HTTP/HTTPS. That is exactly what the ALB exists for.
- Path-based routing will be needed:
/api/*to the orders service and everything else to the shop. An NLB cannot do it, because it does not read the HTTP request. - Automatic HTTP to HTTPS redirection is needed, which the ALB does natively.
- The extra milliseconds of latency are irrelevant for a shop; for a real-time video game they would not be, and then the answer would be the NLB.
About the CLB: it is the first generation, it is still around for compatibility and it must not be used for anything new. If you come across one, migrating it to an ALB is almost always direct.
One pattern worth knowing even though we will not use it: an NLB in front of an ALB. It serves to obtain a fixed IP (because a payment provider demands an IP allowlist, for instance) while keeping the ALB's path-based routing.
Anatomy of an ALB: listeners, rules and target groups
An ALB has four pieces and you need them clear, because the CLI creates them one by one:
flowchart TB
NET["Internet"]
subgraph ALB["alb-mercadofresco-tienda (public subnets in 1a and 1b)"]
L80["HTTP listener :80<br/>default action: redirect to HTTPS"]
L443["HTTPS listener :443<br/>ACM certificate"]
R1["Rule 10: path = /api/*"]
R2["Rule 20: host = admin.mercadofresco.example"]
RD["Default action"]
end
subgraph TGS["Target groups"]
TG1["tg-mercadofresco-tienda<br/>protocol HTTPS :443"]
TG2["tg-mercadofresco-api<br/>protocol HTTPS :443"]
TG3["tg-mercadofresco-admin"]
end
E1["ASG instances<br/>snet-app-a"]
E2["ASG instances<br/>snet-app-b"]
NET --> L80
NET --> L443
L443 --> R1 --> TG2
L443 --> R2 --> TG3
L443 --> RD --> TG1
TG1 --> E1
TG1 --> E2
| Piece | What it is | Important detail |
|---|---|---|
| Load balancer | The resource itself, with its DNS name | Must be in at least two subnets in different AZs |
| Listener | A port and protocol being listened on | Each one has a mandatory default action |
| Rule | Condition + action, inside a listener | Evaluated in ascending priority order |
| Target group | The set of targets and their health check | The health check lives here, not on the load balancer |
Two concepts people confuse:
- The target group has its own protocol and port, independent of the listener's. The listener can receive HTTPS on 443 and forward to the instances over HTTP on port 8080. That is TLS termination.
- The health check is defined on the target group, not on the load balancer. One load balancer can have groups with very different checks.
The available target types:
| Type | What it registers | When it is used |
|---|---|---|
instance |
EC2 instance ID | The usual choice with an ASG |
ip |
Specific private IPs | Containers, on-premises targets over VPN |
lambda |
A Lambda function | Serverless APIs behind an ALB |
alb |
Another ALB | Only from an NLB, for the fixed-IP pattern |
And a target group's distribution algorithms:
| Algorithm | How it distributes | When |
|---|---|---|
round_robin |
One to each target in turn | Default; homogeneous requests |
least_outstanding_requests |
To the target with fewest requests in flight | Recommended when requests take very different times |
weighted_random |
Random with weights | With anomaly mitigation enabled |
For MercadoFresco, least_outstanding_requests: loading the home page does not take the same time as
confirming an order through a payment gateway.
Health checks: the dangerous part
The load balancer periodically sends a request to each registered target. If it answers correctly a given number of times in a row, it marks it healthy and sends it traffic. If it fails, it marks it unhealthy and stops sending.
Parameters and recommended values for MercadoFresco:
| Parameter | What it is | Default | MercadoFresco | Reason |
|---|---|---|---|---|
HealthCheckPath |
The path requested | / |
/salud |
A dedicated endpoint, not the home page |
HealthCheckProtocol |
HTTP or HTTPS | HTTP | HTTPS | Consistent with the target group |
HealthCheckIntervalSeconds |
How often it asks | 30 s | 15 s | Detect failures sooner |
HealthCheckTimeoutSeconds |
How long it waits for the reply | 5 s | 5 s | Lower than the interval, always |
HealthyThresholdCount |
Successes to declare it healthy | 5 | 2 | Bring it back quickly |
UnhealthyThresholdCount |
Failures to declare it unhealthy | 2 | 3 | Tolerate a one-off spike |
Matcher |
Accepted HTTP codes | 200 |
200 |
Explicit and strict |
With these values, a downed instance takes at most 3 × 15 = 45 seconds to stop receiving traffic.
What really happens when an instance is marked unhealthy
This is the exact sequence, and it is worth detailing because there is a surprising knock-on effect:
- The ALB stops sending it new requests. The ones already in flight are completed.
- The instance stays switched on and keeps costing money. The ALB does not shut it down.
- The target group's
HealthyHostCountmetric drops by one. - All the traffic is spread across the remaining instances. If there were two and one is left, that one receives double.
- If the ASG has the load balancer health check enabled (
--health-check-type ELB), the ASG terminates the instance and launches a new one.
Step 4 is the danger. Imagine a /salud that queries the database. If the database gets slow for a
moment, every instance fails the check at once, the ALB marks them all unhealthy and answers
503 Service Unavailable to every customer. The application was working; the health check took it
down.
Worse still with the ASG in ELB mode: the ASG terminates every instance and launches new ones,
which also fail because the database is still slow, and it enters a perpetual replacement loop.
How to write a health check that does not take the application down
The rule: the health check must verify that THIS instance can serve traffic, not that the whole system is working.
# In the shop application: two different endpoints with two different purposes.
@app.route("/salud")
def health():
"""Check for the load balancer. It must be fast, local and NOT touch dependencies.
It answers one thing only: is this process alive and ready to serve requests?"""
if not app.config.get("STARTUP_COMPLETE"):
return {"status": "starting"}, 503
return {"status": "ok", "version": app.config["VERSION"]}, 200
@app.route("/salud/profunda")
def deep_health():
"""Check for monitoring (CloudWatch, 05-01). This one does query dependencies,
but the load balancer NEVER uses it: it is for alerting, not for pulling instances."""
result = {"database": "ok", "s3": "ok", "efs": "ok"}
code = 200
try:
with get_connection() as conn:
conn.execute("SELECT 1")
except Exception as e:
result["database"] = f"error: {type(e).__name__}"
code = 503
return result, codeThe difference between the two endpoints is the whole lesson:
/saludanswers in microseconds, depends on nothing external and can only fail if this instance is broken. If it fails, pulling it out is always the right decision./salud/profundadoes check dependencies, but its failure should raise an alarm so that somebody looks, not the automatic removal of healthy servers.
The 503 during start-up matters just as much: it stops the ALB sending traffic to an instance that
is still downloading the application in user-data-tienda.sh.
Integration with the Auto Scaling group
Connecting asg-mercadofresco-tienda to the target group takes a single command, and from then on
everything is automatic:
aws autoscaling attach-load-balancer-target-groups \
--profile mercadofresco-dev --region eu-west-1 \
--auto-scaling-group-name asg-mercadofresco-tienda \
--target-group-arns "$TG_ARN"What happens from that moment on:
| Event | What the ASG does | What the ALB does |
|---|---|---|
The cpu-objetivo-60 policy launches an instance |
Creates it from lt-mercadofresco-tienda |
Registers it in the group, state initial |
The instance starts and /salud answers 200 twice |
— | It turns healthy and starts receiving traffic |
| The peak passes and the ASG scales in | Marks the instance for termination | Puts it into draining |
| The in-flight connections finish | Waits for the deregistration_delay |
Removes it from the group |
The ALB marks an instance unhealthy |
With --health-check-type ELB, it replaces it |
Stops sending it traffic |
The second important change is the ASG's health check type:
aws autoscaling update-auto-scaling-group \
--profile mercadofresco-dev --region eu-west-1 \
--auto-scaling-group-name asg-mercadofresco-tienda \
--health-check-type ELB \
--health-check-grace-period 300| Type | What the ASG checks | Problem |
|---|---|---|
EC2 (default) |
Only the hypervisor and system status | An instance with a dead application shows as healthy |
ELB |
The hypervisor status and the target group's | Detects downed applications, not just downed machines |
The --health-check-grace-period 300 matters as much as the type: it is the number of seconds the
ASG waits from the moment the instance starts before paying attention to the load balancer.
MercadoFresco's user-data-tienda.sh takes about two minutes to install and start the application;
with no grace period the ASG would kill the instance just before it finished starting, and would do
so in a loop. Five minutes give enough slack.
Connection draining and deployments with no interruption
When an instance is deregistered, the ALB does not cut open connections dead: it puts it into the
draining state. During the deregistration delay (deregistration_delay.timeout_seconds, 300
seconds by default) it receives no new requests, but it finishes the ones it had in flight.
aws elbv2 modify-target-group-attributes \
--profile mercadofresco-dev --region eu-west-1 \
--target-group-arn "$TG_ARN" \
--attributes \
Key=deregistration_delay.timeout_seconds,Value=60 \
Key=load_balancing.algorithm.type,Value=least_outstanding_requests \
Key=stickiness.enabled,Value=falseHow to choose the value: the longest request your application serves, plus a margin.
| Application type | Recommended value |
|---|---|
| API with millisecond responses | 15-30 s |
| Shop with order confirmation and payment gateway | 60 s |
| Large file uploads | 180-300 s |
| Long-lived WebSocket | Up to 3600 s |
MercadoFresco's 60 seconds cover the worst case: a customer who has clicked "confirm order" and is waiting for the gateway's answer. With the default of 300 s, scaling in after the Friday peak would take five minutes longer while paying for idle instances; with 5 s, a payment would be cut off halfway.
Advanced routing by path and by host
This is where the ALB earns its name: it reads the HTTP request and decides based on its content. MercadoFresco needs three different destinations under the same load balancer.
# Rule 10: anything starting with /api/ goes to the API group
aws elbv2 create-rule --profile mercadofresco-dev --region eu-west-1 \
--listener-arn "$LISTENER_443" \
--priority 10 \
--conditions '[{"Field":"path-pattern","Values":["/api/*"]}]' \
--actions "[{\"Type\":\"forward\",\"TargetGroupArn\":\"$TG_API\"}]"
# Rule 20: the administration panel, by host
aws elbv2 create-rule --profile mercadofresco-dev --region eu-west-1 \
--listener-arn "$LISTENER_443" \
--priority 20 \
--conditions '[{"Field":"host-header","Values":["admin.mercadofresco.example"]}]' \
--actions "[{\"Type\":\"forward\",\"TargetGroupArn\":\"$TG_ADMIN\"}]"The available conditions and an example of each:
| Condition | Example | Use in MercadoFresco |
|---|---|---|
path-pattern |
/api/* |
Separating the API from the website |
host-header |
admin.mercadofresco.example |
Internal panel on its own subdomain |
http-header |
X-Canario: si |
Targeted testing before a deployment |
http-request-method |
POST |
Routing writes to a different group |
query-string |
version=beta |
Switching on a new version with a parameter |
source-ip |
81.45.20.7/32 |
Restricting /admin to the office |
Evaluation rules you need to know:
- Rules are evaluated in ascending priority order and the first match applies. Leave gaps between priorities (10, 20, 30) so you can insert new ones.
- If none matches, the listener's default action applies.
- Path patterns are case sensitive and accept
*and?. - A rule can combine up to 5 conditions, and all of them must be met (
AND). - An ALB supports up to 100 rules per listener.
Redirects and fixed responses
Besides forwarding, a listener can answer by itself, without touching any instance.
HTTP to HTTPS redirection, the standard configuration of the port 80 listener:
aws elbv2 create-listener --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arn "$ALB_ARN" \
--protocol HTTP --port 80 \
--default-actions '[{
"Type": "redirect",
"RedirectConfig": {
"Protocol": "HTTPS",
"Port": "443",
"Host": "#{host}",
"Path": "/#{path}",
"Query": "#{query}",
"StatusCode": "HTTP_301"
}
}]'The placeholders #{host}, #{path} and #{query} preserve what the client asked for: anyone
landing on http://mercadofresco.example/producto/tomate?origen=email ends up on the same page over
HTTPS. A 301 is permanent and the browser caches it, so the second visit no longer goes through
port 80. If you were testing and did not want it cached, you would use HTTP_302.
Fixed response, useful for blocking paths or serving maintenance pages:
aws elbv2 create-rule --profile mercadofresco-dev --region eu-west-1 \
--listener-arn "$LISTENER_443" --priority 5 \
--conditions '[{"Field":"path-pattern","Values":["/.env","/.git/*","/wp-admin/*"]}]' \
--actions '[{
"Type": "fixed-response",
"FixedResponseConfig": {
"StatusCode": "403",
"ContentType": "text/plain",
"MessageBody": "Forbidden"
}
}]'That rule, with priority 5 (the highest), cuts off at the load balancer the automated attempts to read configuration files. They never even touch an instance. It is coarse filtering: serious filtering of malicious requests is AWS WAF, in 04-05.
HTTPS on the load balancer: ACM, TLS policies and SNI
MercadoFresco still serves over HTTP. Adding HTTPS with AWS is free and takes two steps.
AWS Certificate Manager (ACM) issues public TLS certificates at no cost and renews them automatically as long as they are attached to an AWS resource. That removes at a stroke the classic problem of the certificate that expires on a Sunday.
aws acm request-certificate --profile mercadofresco-dev --region eu-west-1 \
--domain-name mercadofresco.example \
--subject-alternative-names "*.mercadofresco.example" \
--validation-method DNS \
--tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
Key=Componente,Value=tienda Key=Propietario,Value=marta \
Key=CentroCoste,Value=operacionesTwo details:
- The wildcard
*.mercadofresco.examplecoverswww,admin,apiand any future subdomain with a single certificate. - The region matters: for an ALB, the certificate must be in the same region as the ALB
(
eu-west-1). For CloudFront it has to be inus-east-1, a trap we will see in 03-04.
The certificate stays in PENDING_VALIDATION until you prove the domain is yours by creating a CNAME
record that ACM specifies. That DNS validation is completed in lesson 03-05, once we have the
Route 53 hosted zone. Until then, to practise you can create the HTTPS listener with an imported
self-signed certificate, or work with port 80 only.
The HTTPS listener, once the certificate has been issued:
LISTENER_443=$(aws elbv2 create-listener --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arn "$ALB_ARN" \
--protocol HTTPS --port 443 \
--certificates CertificateArn="$CERT_ARN" \
--ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--default-actions "Type=forward,TargetGroupArn=$TG_TIENDA" \
--query 'Listeners[0].ListenerArn' --output text)The TLS security policies define which versions and ciphers are accepted:
| Policy | TLS supported | Comment |
|---|---|---|
ELBSecurityPolicy-TLS13-1-2-2021-06 |
1.2 and 1.3 | Recommended: secure and compatible with every current browser |
ELBSecurityPolicy-TLS13-1-3-2021-06 |
1.3 only | Maximum security; breaks old clients |
ELBSecurityPolicy-FS-1-2-Res-2020-10 |
1.2 with mandatory forward secrecy | Environments with compliance requirements |
ELBSecurityPolicy-2016-08 |
1.0, 1.1, 1.2 | Do not use: it allows TLS 1.0 |
TLS termination means the ALB decrypts the traffic and decides what to do with the request in
clear. It is the only way to route by path or by header: to read /api/* you have to be able to read
the request. From the ALB to the instances, traffic can travel in clear (faster, and it travels over
AWS's private network inside your VPC) or be encrypted again. MercadoFresco re-encrypts it: the
target group uses HTTPS on 443, because internal policy demands end-to-end encryption even when the
hop is private.
SNI (Server Name Indication) allows several certificates on the same listener: the client
states which domain it wants in the TLS handshake and the ALB presents the right certificate. That
way a single ALB can serve mercadofresco.example and, in the future, mercadofresco.pt with
different certificates, without extra load balancers.
# Add a second certificate to the same listener
aws elbv2 add-listener-certificates --profile mercadofresco-dev --region eu-west-1 \
--listener-arn "$LISTENER_443" \
--certificates CertificateArn="$CERT_ARN_PT"Sticky sessions and why it is better not to need them
Sticky sessions (stickiness) make a given client always go to the same instance. The ALB
inserts a cookie (AWSALB, or one belonging to the application) and uses it to direct the following
requests.
aws elbv2 modify-target-group-attributes --profile mercadofresco-dev --region eu-west-1 \
--target-group-arn "$TG_TIENDA" \
--attributes \
Key=stickiness.enabled,Value=true \
Key=stickiness.type,Value=lb_cookie \
Key=stickiness.lb_cookie.duration_seconds,Value=3600It is useful when the application keeps session state in the server's memory: the shopping basket, for example. And it brings four serious problems:
| Problem | Real consequence for MercadoFresco |
|---|---|
| Uneven distribution | On Friday, the new instances start empty and the old ones keep every client stuck to them. You pay for capacity that relieves nobody |
| Session loss | If the ASG terminates that instance as the peak drops, the customer loses the basket |
| Destructive deployments | Every update throws all the users off the instance being replaced |
| Useless scaling | Adding instances does not help the users already connected |
That is why MercadoFresco has stickiness.enabled=false and the application is stateless: the
session is kept outside the instance. The options:
| Where to keep the session | Advantage | Drawback |
|---|---|---|
| ElastiCache (Redis) | Fast, milliseconds, the industry standard | One more service to operate — covered in 06-05 |
| DynamoDB | Serverless, no operations, automatic TTL | Slightly higher latency — covered in 06-02 |
| Signed cookie on the client | Zero infrastructure | Limited size, the data travels on every request |
| Relational database | It already exists | Unnecessary load on RDS |
MercadoFresco will end up using ElastiCache. In the meantime, sticky sessions are an acceptable patch if documented as technical debt; what is not acceptable is enabling them and forgetting why.
Access logs and metrics
Access logs. The ALB can dump one line per request into S3, with the client's IP, the path, the response code, the processing times and the target that served it. They are switched off by default.
aws elbv2 modify-load-balancer-attributes --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arn "$ALB_ARN" \
--attributes \
Key=access_logs.s3.enabled,Value=true \
Key=access_logs.s3.bucket,Value=mercadofresco-registros-web \
Key=access_logs.s3.prefix,Value=alb \
Key=idle_timeout.timeout_seconds,Value=60 \
Key=routing.http.drop_invalid_header_fields.enabled,Value=trueThe bucket mercadofresco-registros-web needs a policy authorising the service to write to it;
without it the command appears to work but no file ever shows up. A discreet cost warning: in a shop
with traffic these logs grow fast. Apply the lifecycle rule we saw in 02-03 to move them to Glacier
after 30 days.
Key ALB metrics in CloudWatch:
| Metric | What it measures | Why it matters at MercadoFresco |
|---|---|---|
RequestCount |
Requests served | Confirms the Friday 17:00 to 21:00 peak with data |
TargetResponseTime |
How long the instances take | If it rises before CPU does, the bottleneck is RDS |
HTTPCode_Target_5XX_Count |
Errors generated by the application | The application is failing |
HTTPCode_ELB_5XX_Count |
Errors generated by the load balancer | There are no healthy targets: the classic 503 |
HealthyHostCount |
Healthy targets per AZ | If it drops below 2, redundancy has been lost |
UnHealthyHostCount |
Unhealthy targets | Spots broken instances before customers do |
ActiveConnectionCount |
Open connections | Sizing and detecting exhaustion |
RejectedConnectionCount |
Connections rejected on reaching a limit | The ALB has not scaled in time for a sharp spike |
The distinction between HTTPCode_Target_5XX_Count and HTTPCode_ELB_5XX_Count is the most useful
in the table: the first means "the application returned an error", the second "there was nobody to
ask". Creating alarms and dashboards on these metrics is covered in 05-01; here it is enough to
know what to look at.
Full creation from the CLI
We start from the 03-01 and 03-02 variables ($VPC_ID, $PUB_A, $PUB_B, $SG_ALB, $SG_TIENDA).
# 1. The load balancer, in BOTH public subnets
ALB_ARN=$(aws elbv2 create-load-balancer \
--profile mercadofresco-dev --region eu-west-1 \
--name alb-mercadofresco-tienda \
--type application \
--scheme internet-facing \
--ip-address-type ipv4 \
--subnets "$PUB_A" "$PUB_B" \
--security-groups "$SG_ALB" \
--tags Key=Name,Value=alb-mercadofresco-tienda \
Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
Key=Componente,Value=tienda Key=Propietario,Value=marta \
Key=CentroCoste,Value=operaciones \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)
# 2. The target group, with the health check properly tuned
TG_TIENDA=$(aws elbv2 create-target-group \
--profile mercadofresco-dev --region eu-west-1 \
--name tg-mercadofresco-tienda \
--protocol HTTPS --port 443 \
--vpc-id "$VPC_ID" \
--target-type instance \
--health-check-protocol HTTPS \
--health-check-path /salud \
--health-check-interval-seconds 15 \
--health-check-timeout-seconds 5 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3 \
--matcher HttpCode=200 \
--tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
Key=Componente,Value=tienda Key=Propietario,Value=marta \
Key=CentroCoste,Value=operaciones \
--query 'TargetGroups[0].TargetGroupArn' --output text)
# 3. Target group attributes
aws elbv2 modify-target-group-attributes --profile mercadofresco-dev --region eu-west-1 \
--target-group-arn "$TG_TIENDA" \
--attributes \
Key=deregistration_delay.timeout_seconds,Value=60 \
Key=load_balancing.algorithm.type,Value=least_outstanding_requests \
Key=stickiness.enabled,Value=false
# 4. HTTPS listener (requires the certificate; see 03-05 for the validation)
LISTENER_443=$(aws elbv2 create-listener --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arn "$ALB_ARN" --protocol HTTPS --port 443 \
--certificates CertificateArn="$CERT_ARN" \
--ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--default-actions "Type=forward,TargetGroupArn=$TG_TIENDA" \
--query 'Listeners[0].ListenerArn' --output text)
# 5. HTTP listener that only redirects
aws elbv2 create-listener --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arn "$ALB_ARN" --protocol HTTP --port 80 \
--default-actions '[{"Type":"redirect","RedirectConfig":{
"Protocol":"HTTPS","Port":"443","Host":"#{host}","Path":"/#{path}",
"Query":"#{query}","StatusCode":"HTTP_301"}}]'
# 6. Attach the ASG and make it trust the load balancer
aws autoscaling attach-load-balancer-target-groups \
--profile mercadofresco-dev --region eu-west-1 \
--auto-scaling-group-name asg-mercadofresco-tienda \
--target-group-arns "$TG_TIENDA"
aws autoscaling update-auto-scaling-group \
--profile mercadofresco-dev --region eu-west-1 \
--auto-scaling-group-name asg-mercadofresco-tienda \
--health-check-type ELB --health-check-grace-period 300
# 7. Wait for the ALB to become active and get its DNS name
aws elbv2 wait load-balancer-available --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arns "$ALB_ARN"
aws elbv2 describe-load-balancers --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arns "$ALB_ARN" \
--query 'LoadBalancers[0].DNSName' --output textThe last command returns something along the lines of
alb-mercadofresco-tienda-1234567890.eu-west-1.elb.amazonaws.com. That name is MercadoFresco's new
front door. Making mercadofresco.example point at it is exactly the job of lesson
03-05.
Checking the health of the targets, the command used most on a daily basis:
aws elbv2 describe-target-health --profile mercadofresco-dev --region eu-west-1 \
--target-group-arn "$TG_TIENDA" \
--query 'TargetHealthDescriptions[].{
Instance:Target.Id, Port:Target.Port,
State:TargetHealth.State, Reason:TargetHealth.Reason,
Detail:TargetHealth.Description}' \
--output tableThe reasons that show up most and what they really mean:
Reason |
Meaning | Where to look |
|---|---|---|
Elb.RegistrationInProgress |
Just registered, not yet checked | Wait |
Elb.InitialHealthChecking |
Being checked for the first time | Wait |
Target.Timeout |
Does not answer in time | Security group (03-02) or the application is down |
Target.FailedHealthChecks |
Answers, but not as expected | HTTP code different from the Matcher |
Target.ResponseCodeMismatch |
Returns 302, 404, 500… | The /salud path does not exist or redirects |
Target.NotInUse |
The group is not attached to any listener | Step 4 is missing |
Target.DeregistrationInProgress |
Draining connections | Normal when scaling in |
Target.Timeout is nearly always the same thing: the sg-mercadofresco-tienda group does not accept
443 from sg-mercadofresco-alb. If you followed lesson 03-02, it is already solved.
ALB cost and clean-up
⚠️ Cost warning: the ALB charges by the hour even if it receives no requests at all
In
eu-west-1the price has two components:
- Load balancer hour: ≈ 0.027 USD/h → ≈ 20 USD a month just for existing.
- LCU (Load Balancer Capacity Unit): ≈ 0.008 USD per LCU-hour.
An LCU is the maximum of four dimensions measured per hour: 25 new connections per second, 3,000 active connections per second, 1 GB/hour processed, or 1,000 rule evaluations per second. Only the highest dimension is billed, not the sum.
Estimate for MercadoFresco: around 2 LCUs on average → 2 × 0.008 × 730 ≈ 12 USD/month. In total, about 32 USD a month. Compared with the 70 USD of the two NAT gateways in 03-01, it is cheap for what it gives, but it is not in the free tier.
To practise: build the ALB, check that it distributes traffic and delete it the same day. A forgotten ALB costs 20 USD a month indefinitely and will blow the 10 USD budget we set up in 01-02 towards
[email protected].
Clean-up, in order:
# 1. Detach the ASG (otherwise it would register instances again)
aws autoscaling detach-load-balancer-target-groups \
--profile mercadofresco-dev --region eu-west-1 \
--auto-scaling-group-name asg-mercadofresco-tienda \
--target-group-arns "$TG_TIENDA"
# 2. Return the ASG to the basic health check
aws autoscaling update-auto-scaling-group --profile mercadofresco-dev --region eu-west-1 \
--auto-scaling-group-name asg-mercadofresco-tienda --health-check-type EC2
# 3. Delete the load balancer (this removes its listeners too)
aws elbv2 delete-load-balancer --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arn "$ALB_ARN"
aws elbv2 wait load-balancers-deleted --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arns "$ALB_ARN"
# 4. Delete the target group (not possible before: it depends on the listener)
aws elbv2 delete-target-group --profile mercadofresco-dev --region eu-west-1 \
--target-group-arn "$TG_TIENDA"
# 5. Check that none is left alive in the region
aws elbv2 describe-load-balancers --profile mercadofresco-dev --region eu-west-1 \
--query 'LoadBalancers[].[LoadBalancerName,State.Code]' --output tableThe ACM certificate costs nothing, so it can be left in place.
Friday's traffic, end to end
flowchart TB
U["Customers<br/>900 orders/hour (Friday 17-21 h)"]
DNS["mercadofresco.example<br/>(Route 53, lesson 03-05)"]
subgraph VPC["vpc-mercadofresco"]
subgraph PUBS["Public subnets"]
NA["ALB node<br/>eu-west-1a"]
NB["ALB node<br/>eu-west-1b"]
end
TG["tg-mercadofresco-tienda<br/>/salud check every 15 s<br/>least_outstanding_requests"]
subgraph APPS["Application subnets (no public IP)"]
I1["tienda-01 · 600 orders/h"]
I2["tienda-02 · 600 orders/h"]
I3["tienda-03 · peak"]
I4["tienda-04 · peak"]
end
RDS[("mercadofresco-pedidos<br/>data subnets")]
end
U --> DNS --> NA
DNS --> NB
NA --> TG
NB --> TG
TG --> I1
TG --> I2
TG -.->|"added by<br/>pico-viernes-tarde"| I3
TG -.-> I4
I1 --> RDS
I2 --> RDS
And Friday's arithmetic, now complete:
| Moment | Healthy instances | Capacity | Demand | Margin |
|---|---|---|---|---|
| Tuesday 11:00 | 2 | 1,200 orders/h | ~100 orders/h | 12× |
Friday 16:45 (pico-viernes-tarde action) |
4 | 2,400 orders/h | ~200 orders/h | Ready |
| Friday 19:00 (the real peak) | 4 | 2,400 orders/h | 900 orders/h | 2.6× |
| Friday 19:00 with one instance down | 3 | 1,800 orders/h | 900 orders/h | 2× |
Friday 19:00 with the whole 1a AZ down |
2 | 1,200 orders/h | 900 orders/h | 1.3× — it holds |
Friday 22:00 (cpu-objetivo-60 scales in) |
2 | 1,200 orders/h | ~150 orders/h | 8× |
The second-to-last row is what justifies all the work of these three lessons: losing a whole Availability Zone in the middle of the Friday peak no longer takes MercadoFresco down. Problem 1 is solved.
Common Mistakes and Tips
Putting the load balancer in a single subnet. AWS requires at least two AZs, but if you register instances in only one of them, you lose all the redundancy with no warning at all.
Using / as the health check path. The home page queries the database, loads the catalogue and
takes hundreds of milliseconds. If the database slows down, every instance fails at once and the ALB
returns 503 with a perfectly healthy application. Use a local, trivial /salud.
Setting too short a grace period on the ASG. With --health-check-grace-period 60 and a two
minute start-up, the ASG kills every instance just before it finishes starting, in a loop, spending
money without serving anything.
Leaving the deregistration delay at 300 seconds. Scaling in after the peak takes five minutes longer. Tune it to the duration of your longest request.
Enabling sticky sessions to "fix" a session problem. It works today and breaks scaling tomorrow. It is technical debt: document it and plan to move the session out of the instance.
Forgetting the log bucket's policy. The enabling command gives no error, but no file ever appears
in mercadofresco-registros-web.
Confusing HTTPCode_ELB_5XX_Count with HTTPCode_Target_5XX_Count. The first means there are no
healthy targets; the second, that the application is returning errors. They are different incidents
and they are fixed in different places.
Requesting the ACM certificate in the wrong region. For an ALB, in the ALB's region. For
CloudFront, always in us-east-1. We will see it in 03-04.
Leaving a test ALB running. 20 USD a month for a resource nobody uses. Check
describe-load-balancers in every region where you have been practising.
The golden tip: before connecting the ASG, register one instance by hand in the target group and
check that it turns healthy. If it does not, the problem is the security group or the health check,
and it is far easier to diagnose with a single instance than with four.
Exercises
Exercise 1: diagnosing an ALB that returns 503
Marta deploys the ALB and, on opening its DNS name, gets 503 Service Unavailable. In
describe-target-health both instances show as unhealthy with reason Target.Timeout. List at
least five possible causes, ordered from most to least likely, and the specific command that
confirms or rules out each one.
Exercise 2: designing the complete routing
MercadoFresco needs, under a single ALB: the shop at the root; the API on /api/* with a target
group of its own; the panel on admin.mercadofresco.example reachable only from the office IP
81.45.20.7; a 410 Gone for the old path /tienda-antigua/*; and redirection of all HTTP traffic
to HTTPS. Write the rules with their priorities and justify the order.
Exercise 3: calculating the cost and checking the capacity
Using MercadoFresco's data (2 instances during normal hours, 4 on Fridays from 17:00 to 21:00, 900 orders/hour at the peak, 600 orders/hour per instance), work out: the monthly cost of the ALB assuming 3 LCUs on average, how many instances would be needed if the business grew to 3,000 orders/hour, and whether the current ASG would cope.
Solutions
Solution 1
Causes, from most to least likely:
1. The instances' security group does not accept traffic from the load balancer. It is the number
one cause of Target.Timeout.
aws ec2 describe-security-groups --profile mercadofresco-dev --region eu-west-1 \
--group-ids "$SG_TIENDA" \
--query 'SecurityGroups[0].IpPermissions[].{P:FromPort,Groups:UserIdGroupPairs[].GroupId}'Port 443 with $SG_ALB in UserIdGroupPairs must appear.
2. The application is not listening on the target group's port. The group points at 443 but the application listens on 8080.
aws ssm start-session --profile mercadofresco-dev --region eu-west-1 --target "$INSTANCE_ID"
# once inside: ss -lntp | grep -E ':(443|8080)'3. The /salud path does not exist. It would give Target.ResponseCodeMismatch with a 404, but
if the server does not answer that path at all, it shows up as Target.Timeout.
4. The instances are in subnets with no route and the ALB cannot reach them. Check that the ASG's subnets are in the same VPC as the load balancer and that the ALB's AZs include the instances'.
aws elbv2 describe-load-balancers --profile mercadofresco-dev --region eu-west-1 \
--load-balancer-arns "$ALB_ARN" --query 'LoadBalancers[0].AvailabilityZones[].[ZoneName,SubnetId]'5. A NACL is blocking the return traffic. The ephemeral port range is missing on the application
subnet's outbound rules (lesson 03-02). It is confirmed in the Flow Logs by an inbound ACCEPT and
an outbound REJECT.
6. Less likely but real: the ASG's grace period is short and the instances are being replaced
before they start; you see it in describe-scaling-activities with continuous replacements.
Solution 2
| Priority | Condition | Action | Why in that order |
|---|---|---|---|
| 10 | host-header = admin.mercadofresco.example AND source-ip = 81.45.20.7/32 |
forward → tg-mercadofresco-admin |
Must be evaluated before rule 20, which denies the rest |
| 20 | host-header = admin.mercadofresco.example |
fixed-response 403 |
Catches panel access from any other IP |
| 30 | path-pattern = /tienda-antigua/* |
fixed-response 410 |
Before /api/* and before the default action |
| 40 | path-pattern = /api/* |
forward → tg-mercadofresco-api |
After the specific rules |
| (default) | — | forward → tg-mercadofresco-tienda |
Everything else |
Justification of the order: the key is the 10/20 pair. Rules are evaluated in ascending priority and
the first match wins; if 20 had the lower priority, it would also catch the office's legitimate
traffic and the panel would be unreachable for everybody. The condition on rule 10 combines two
fields with AND, which is how an ALB's multiple conditions work.
aws elbv2 create-rule --profile mercadofresco-dev --region eu-west-1 \
--listener-arn "$LISTENER_443" --priority 10 \
--conditions '[
{"Field":"host-header","HostHeaderConfig":{"Values":["admin.mercadofresco.example"]}},
{"Field":"source-ip","SourceIpConfig":{"Values":["81.45.20.7/32"]}}]' \
--actions "[{\"Type\":\"forward\",\"TargetGroupArn\":\"$TG_ADMIN\"}]"
aws elbv2 create-rule --profile mercadofresco-dev --region eu-west-1 \
--listener-arn "$LISTENER_443" --priority 20 \
--conditions '[{"Field":"host-header","HostHeaderConfig":{"Values":["admin.mercadofresco.example"]}}]' \
--actions '[{"Type":"fixed-response","FixedResponseConfig":{
"StatusCode":"403","ContentType":"text/plain","MessageBody":"Restricted access"}}]'The port 80 listener keeps the 301 redirect as its default action, with no rules: that way all the HTTP traffic, including the panel's, ends up on HTTPS.
An honest caveat: filtering by source-ip on the ALB works, but if CloudFront is put in front
tomorrow (lesson 03-04), the ALB will see CloudFront's IP and not the client's. You would then have
to filter in WAF (04-05) or on the X-Forwarded-For header.
Solution 3
Monthly cost of the ALB:
- Hours: 0.027 USD/h × 730 h = 19.71 USD
- LCUs: 3 LCUs × 0.008 USD × 730 h = 17.52 USD
- Total ≈ 37.23 USD/month
Compared with the complete network infrastructure: ALB ≈ 37 USD + 2 NAT gateways ≈ 70 USD = 107 USD a month on networking alone, without counting instances or the database. That is the argument for reviewing in 11-03 whether the two NATs are worth it.
Instances needed for 3,000 orders/hour:
- Strict capacity: 3,000 ÷ 600 = 5 instances.
- But you have to survive the loss of a whole Availability Zone. With instances spread across 2 AZs, losing one leaves half. For the surviving half to handle 3,000 orders/h you need 10 instances, 5 per AZ.
- A reasonable compromise, accepting degradation in the worst case: 8 instances (4 per AZ). With one AZ down you are left with 4 × 600 = 2,400 orders/h, 80 % of demand: the shop is slow but stays up.
Would the current ASG cope? No. asg-mercadofresco-tienda has a maximum of 4, that is, 2,400
orders per hour. With 3,000 of demand it would fall 20 % short even with no failures. The changes
needed:
aws autoscaling update-auto-scaling-group --profile mercadofresco-dev --region eu-west-1 \
--auto-scaling-group-name asg-mercadofresco-tienda \
--min-size 4 --desired-capacity 4 --max-size 10And before calling the change good, three more things have to be checked: that RDS can cope with 10
instances opening connections (or a pool is needed, or Aurora, covered in 06-03), that the subnets
have free IPs (with a /20 there are plenty) and that the account's instance quota allows it. Scaling
the web tier is the easy part; the bottleneck moves to the database.
Conclusion
MercadoFresco finally has a single front door. You know why a load balancer is needed beyond the sharing out itself: health checks, high availability across zones, TLS termination in one place and the possibility of instances living in private subnets with no public IP. You can tell the four types apart —ALB for HTTP and HTTPS with content-based routing, NLB for layer 4, microsecond latency and a fixed IP, GWLB for virtual security appliances, and the CLB you only ever inherit— and you can justify why MercadoFresco needs an ALB.
You know its anatomy —load balancer, listeners, rules and target groups— and the detail that the
health check lives on the target group, not on the load balancer. And above all you have understood
the dangerous part: a health check that queries the database can mark every instance unhealthy at
once and return 503 with a perfectly healthy application, or push the ASG into a replacement loop.
That is why /salud is local and trivial, /salud/profunda is for monitoring, and the ASG's grace
period is 300 seconds.
You have connected asg-mercadofresco-tienda to the tg-mercadofresco-tienda group so that
registration and deregistration are automatic, you have changed the ASG's check to ELB so that
it detects dead applications and not just dead machines, and you have tuned connection draining to
60 seconds, the length of the longest order. You know how to route by path and by host, combine
conditions, use fixed responses to cut off malicious requests at the load balancer and redirect HTTP
to HTTPS with a 301 that preserves the path. You have requested a free wildcard certificate from
ACM, chosen the TLS 1.2/1.3 policy and you understand what TLS termination is and what SNI is for
—knowing that the DNS validation is still pending until the next lesson. You know why sticky
sessions are a patch that breaks scaling and where the session should really live. You have enabled
access logs to mercadofresco-registros-web and you know what RequestCount,
TargetResponseTime and HealthyHostCount mean, and the difference between the target's 5XX and the
load balancer's 5XX.
And the arithmetic works out: with four healthy instances spread across two zones, MercadoFresco handles Friday's 900 orders per hour even after losing an entire Availability Zone. Problem 1, the Friday outages, is solved.
That leaves the money. In lesson 02-03 we did a calculation that hurt: 97 % of the S3 cost is
outbound transfer, because every catalogue photo is downloaded in full from Ireland each time
somebody looks at it, and now on top of that all the dynamic traffic goes through an ALB that charges
per GB processed. In lesson 03-04, "Amazon CloudFront", we will put a content delivery network in
front of everything: the photos will be served from the edge location nearest the customer, the
mercadofresco-catalogo-fotos bucket will stop being directly reachable thanks to Origin Access
Control, and we will see in numbers how far the bill drops. The ALB you have just built will still
be there behind it, but receiving only what genuinely cannot be cached.
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
