7. Application Load Balancer and HTTPS
ALB distributes traffic to your Fargate tasks; ACM issues free TLS certificates so you can serve HTTPS without buying certs.
The ECS tasks have private IPs that change every time a task restarts, so there's no stable URL to hand to a client. The Application Load Balancer fixes that: one public DNS name, two AZs, a target group the ECS service registers and deregisters tasks against automatically. Behind that hostname, tasks can start, stop, or be replaced, and the ALB only sends traffic to the ones currently passing health checks.
What an Application Load Balancer provides
ALBs handle multiple concerns that your application shouldn't manage itself:
- Traffic distribution. Incoming requests fan out across healthy targets using round-robin by default. Two tasks each see ~50% of traffic; four tasks each see ~25%. No configuration is needed beyond registering the targets.
- Health checks. The ALB pings each target on a configured path (this chapter uses
/docs) every 30 seconds. Targets that fail consecutive checks are pulled out of the pool and added back once they pass again. - TLS termination. An HTTPS listener attached to an ACM certificate decrypts traffic at the ALB and forwards plain HTTP to the tasks. The app keeps speaking HTTP; certificate renewals and cipher updates live on the ALB.
- Connection draining. When a task is removed (a deploy or a scale-in), the ALB stops sending it new requests but lets in-flight ones finish, which is what makes zero-downtime rolling deploys possible.
The ALB's DNS name will look like news-api-alb-123456789.us-east-1.elb.amazonaws.com. That hostname is stable; the tasks behind it are not.
Creating the load balancer
ALBs require several components: the load balancer itself, target groups (where traffic routes to), security groups (firewall rules), and listeners (routing rules for different protocols/ports). You'll create all of these to connect your public endpoint to your ECS containers.
Make: Create an Application Load Balancer:
# Get VPC and subnets (need at least 2 subnets in different AZs)
VPC_ID=$(aws ec2 describe-vpcs \
--filters "Name=isDefault,Values=true" \
--query 'Vpcs[0].VpcId' \
--output text)
SUBNET_IDS=$(aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=$VPC_ID" \
--query 'Subnets[0:2].SubnetId' \
--output text | tr '\t' ' ')
# Create security group for ALB (allows HTTP/HTTPS from internet)
ALB_SG=$(aws ec2 create-security-group \
--group-name news-api-alb-sg \
--description "Security group for News API ALB" \
--vpc-id $VPC_ID \
--query 'GroupId' \
--output text)
# Allow inbound HTTP (port 80) from anywhere
aws ec2 authorize-security-group-ingress \
--group-id $ALB_SG \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
# Allow inbound HTTPS (port 443) from anywhere
aws ec2 authorize-security-group-ingress \
--group-id $ALB_SG \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Create the Application Load Balancer
ALB_ARN=$(aws elbv2 create-load-balancer \
--name news-api-alb \
--subnets $SUBNET_IDS \
--security-groups $ALB_SG \
--scheme internet-facing \
--type application \
--ip-address-type ipv4 \
--query 'LoadBalancers[0].LoadBalancerArn' \
--output text)
echo "Load Balancer ARN: $ALB_ARN"
# Get the ALB's public DNS name
ALB_DNS=$(aws elbv2 describe-load-balancers \
--load-balancer-arns $ALB_ARN \
--query 'LoadBalancers[0].DNSName' \
--output text)
echo "Your API will be accessible at: http://$ALB_DNS"
What this creates: A public-facing load balancer in two availability zones (for high availability). The ALB accepts HTTP and HTTPS traffic from the internet and will forward it to your ECS containers. The DNS name is available immediately, but you need to configure target groups and listeners before traffic flows through.
Configuring target groups
Target groups define where the ALB sends traffic. For ECS, you create a target group with target type ip (since Fargate tasks use awsvpc networking and get IP addresses). ECS automatically registers and deregisters container IP addresses as tasks start and stop.
Make: Create a target group with health checks:
TARGET_GROUP_ARN=$(aws elbv2 create-target-group \
--name news-api-tg \
--protocol HTTP \
--port 8000 \
--vpc-id $VPC_ID \
--target-type ip \
--health-check-enabled \
--health-check-protocol HTTP \
--health-check-path /docs \
--health-check-interval-seconds 30 \
--health-check-timeout-seconds 5 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3 \
--query 'TargetGroups[0].TargetGroupArn' \
--output text)
echo "Target Group ARN: $TARGET_GROUP_ARN"
Health check configuration explained:
health-check-path: /docs: The ALB requests this path every 30 seconds. FastAPI's automatic documentation endpoint always returns 200 OK if the application is running. If the container can't respond (crashed, unresponsive, network issue), health checks fail.
healthy-threshold-count: 2: The container must pass 2 consecutive health checks (60 seconds total) before receiving traffic. This prevents routing to containers that are still starting up.
unhealthy-threshold-count: 3: The container must fail 3 consecutive health checks (90 seconds) before being marked unhealthy and removed from rotation. This prevents brief hiccups from triggering unnecessary container restarts.
The two thresholds trade off detection speed against false positives. Lower the unhealthy count and a momentary blip pulls a task; raise it and a real failure spends longer in the rotation than it should. The defaults above (2 healthy, 3 unhealthy) are a reasonable starting point for an API whose /docs endpoint is cheap to serve.
Creating load balancer listeners
Listeners define how the ALB handles incoming traffic. You'll create an HTTP listener that forwards all requests to your target group. For production, you'd also create an HTTPS listener with SSL certificates, but HTTP works for initial testing.
Make: Create HTTP listener:
aws elbv2 create-listener \
--load-balancer-arn $ALB_ARN \
--protocol HTTP \
--port 80 \
--default-actions Type=forward,TargetGroupArn=$TARGET_GROUP_ARN
# Output shows listener created successfully
What this does: Requests to the ALB DNS name on port 80 forward to the target group, which sends them to healthy ECS tasks on port 8000. Once the listener is in place the ALB starts routing immediately; there's nothing else to wire.
Connecting the ECS service to the load balancer
Your ECS service and ALB target group exist independently. The service needs a load-balancer configuration so ECS registers new task IP addresses with the target group when containers start and deregisters them when containers stop. One catch: that configuration can only be set when the service is created, so the Section 6 service has to be deleted and recreated with the target group attached.
Make: Recreate the ECS service with the load balancer attached:
# Update ECS security group to allow ALB traffic
aws ec2 authorize-security-group-ingress \
--group-id $ECS_SG \
--protocol tcp \
--port 8000 \
--source-group $ALB_SG
# Delete the existing service (load balancer configuration
# can't be added to a service after creation)
aws ecs delete-service \
--cluster news-api-cluster \
--service news-api-service \
--force
# Wait 2 minutes for service to fully terminate
# Recreate with load balancer
aws ecs create-service \
--cluster news-api-cluster \
--service-name news-api-service \
--task-definition news-api-task:1 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_IDS],securityGroups=[$ECS_SG],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=$TARGET_GROUP_ARN,containerName=news-api,containerPort=8000" \
--health-check-grace-period-seconds 60 \
--region us-east-1
health-check-grace-period-seconds: ECS waits 60 seconds after starting containers before health checks affect service stability. This grace period prevents ECS from marking containers unhealthy during startup (when they're still initializing databases, loading configuration, etc.).
Check: Wait 2-3 minutes for containers to register, then verify target health:
aws elbv2 describe-target-health \
--target-group-arn $TARGET_GROUP_ARN \
--query 'TargetHealthDescriptions[*].{Target:Target.Id,Health:TargetHealth.State}' \
--output table
# Output shows 2 targets with "healthy" status:
# --------------------------------
# | Target | Health |
# --------------------------------
# | 10.0.1.123 | healthy |
# | 10.0.2.456 | healthy |
# --------------------------------
When both targets show healthy, your API is ready to serve traffic through the load balancer.
Testing the public API
Your News API is now publicly accessible through the Application Load Balancer. The ALB DNS name provides a stable endpoint that distributes requests across your ECS containers.
Check: Test all endpoints through the load balancer:
# Get your ALB DNS name
ALB_DNS=$(aws elbv2 describe-load-balancers \
--load-balancer-arns $ALB_ARN \
--query 'LoadBalancers[0].DNSName' \
--output text)
echo "Testing API at: http://$ALB_DNS"
# Health check (public, no API key required)
curl http://$ALB_DNS/health
# Missing API key returns 401
curl http://$ALB_DNS/articles
# Response: {"detail":"Missing Authorization header"}
# Generate an API key (the fresh RDS database has none yet);
# the admin key is the ADMIN_API_KEY from the task definition
curl -X POST http://$ALB_DNS/admin/api-keys \
-H "X-Admin-Key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "AWS Smoke Test", "tier": "basic"}'
# Test the articles endpoint with the key you just generated
curl -H "Authorization: Bearer YOUR_API_KEY" http://$ALB_DNS/articles
# Test with filters
curl -H "Authorization: Bearer YOUR_API_KEY" \
"http://$ALB_DNS/articles?category=technology&limit=10"
Visit in browser: Open http://YOUR-ALB-DNS/docs in your browser and FastAPI's interactive documentation loads. The endpoint is public: anyone with the URL can hit it. That's the API key requirement from Ch26 doing its job at the application layer.
Success criteria: A 200 from /health, a 401 from the bare /articles call, and JSON coming back from the authenticated /articles call means the whole stack is wired correctly: ECR served the image to ECS, the task is reading from RDS and ElastiCache, and the ALB is routing traffic to healthy tasks.
Adding HTTPS with an ACM certificate
HTTP works for testing, but production APIs require HTTPS. AWS Certificate Manager (ACM) provides free SSL certificates for use with load balancers. You need a custom domain (like api.yourdomain.com) to use ACM certificates.
If you have a custom domain:
- Request an SSL certificate in ACM for your domain
- Validate domain ownership via DNS or email
- Create an HTTPS listener on port 443 using the ACM certificate
- Add a redirect rule on the HTTP listener to redirect HTTP -> HTTPS
- Create a Route 53 alias record pointing your domain to the ALB DNS name
Without a custom domain: ACM won't issue a certificate for an amazonaws.com hostname, so the bare ALB DNS name only serves HTTP. For chapter purposes that's enough to verify the deployment works; for any real client traffic you'd attach the ALB to a domain you own and run the five steps above.
A domain from any registrar (typically $10-15/year) works. The fastest path is to delegate it to Route 53 with a hosted zone, request the ACM certificate with DNS validation in the same region as the ALB, and add the alias record pointing the apex (or a subdomain like api.yourdomain.com) at the ALB. Once the certificate validates and the HTTPS listener is attached, the API serves over https://.
Next, in section 8, we recap the load-bearing decisions of this chapter -- IAM scope, security-group sources, Parameter Store for secrets, target-group health checks -- run the quiz, and point at where Chapter 29 automates the manual ECR push and ECS update done here.