Skip to content

Phase 3 · CLOUD

Project 2, Day 4: DNS, TLS & monitoring

Day 65 of 90 ~60 min 0/20 in phase Builds on Day 64

By the end of today

  • Point a Route 53 alias record at your ALB and validate an ACM cert
  • Add an HTTPS listener plus CloudWatch alarms for 5xx and CPU
  • Tear down every Project 2 resource in reverse order to stop the bill

Giving linkstash a name, a padlock, and a watcher — then deleting it all

Section 1 of 5 · ~2 min

Day 3 left linkstash reachable, but only at an ugly, unencrypted URL like http://linkstash-alb-123456.us-east-1.elb.amazonaws.com. Today you give it the three things a real service has — a name, a padlock, and someone watching it — then you tear the entire two-week build down. This is the Project 2 capstone: DNS, TLS, monitoring, and the discipline that stops a cloud bill outliving the project.

A name and a padlock: Route 53 + ACM

Route 53 is AWS’s DNS. You point a record at your ALB, but not with a plain CNAME — an ALB’s IP addresses change, so you use an alias record: a Route 53-specific A record that targets the load balancer by name and follows it automatically. Alias queries are free and, unlike a CNAME, can sit at a zone apex.

ACM (AWS Certificate Manager) issues the TLS certificate — free, and auto-renewing. You request one for app.yourdomain.com and prove ownership by DNS validation: ACM hands you a CNAME to add to your hosted zone, and once it sees the record it issues the cert. You then add an HTTPS listener on port 443 that presents the cert, and change the port-80 listener to a 301 redirect so nothing rides in the clear.

Someone watching: CloudWatch alarms

A service you can’t see is a service you can’t trust. CloudWatch already collects metrics from the ALB and the Fargate service; you promote two into alarmsHTTPCode_ELB_5XX_Count (the ALB returning server errors) and the service’s CPUUtilization (the container running hot). An alarm compares a metric to a threshold over a period and flips to ALARM, where it can page you through SNS. A small dashboard puts request count, 5xx, and CPU on one screen.

Real world: think of opening a pop-up shop. Route 53 is the street address on the door, ACM is the licence in the window proving you’re legitimate, and CloudWatch alarms are the smoke detectors wired to the fire brigade. The capstone’s real lesson is the step everyone skips: when the shop closes you hand back the keys, cancel the licence, and take the detectors down — or you keep paying rent on an empty room.

Netflix fronts its services with exactly this shape — Route 53 for DNS, ACM and ELB for TLS termination, CloudWatch for the first line of metrics — just multiplied across thousands of services and fully automated. You are building the pattern by hand, once, so the automated version makes sense later.

Then the part that matters most on a real bill: teardown in reverse order. You built network → data → compute → edge; you delete edge → compute → data → network. A forgotten ALB, NAT gateway, and RDS instance left running is roughly ₹5,000+ every month for nothing. The last third of today’s lab deletes every billable resource from Project 2 and confirms each is gone.

A user resolves app.yourdomain.com through a Route 53 alias record to the ALB, which terminates TLS on its HTTPS listener using an ACM certificate and forwards to the linkstash Fargate task; CloudWatch watches the ALB and the Fargate service and raises alarms on 5xx errors and high CPU. user app.yourdomain Route 53 alias → ALB ALB :443 ACM cert · TLS Fargate task linkstash CloudWatch — alarms: 5xx · CPU
A name resolves to the ALB, which terminates TLS with the ACM cert and forwards to the Fargate task; CloudWatch watches both and alarms on 5xx and CPU.

Hands-On Lab

Section 2 of 5 · ~6 min

Budget about 30 minutes. Drive this from the AWS CLI v2 as your IAM user (not root), in us-east-1, with the Day-3 stack still running: an ALB linkstash-alb, a target group linkstash-tg, the ECS service linkstash-svc on cluster linkstash-cluster, an RDS instance linkstash-db, and a public Route 53 hosted zone for a domain you own. Account IDs, ARNs, the ALB DNS name, hosted-zone and resource IDs are all unique to you — yours will differ. Substitute your real domain for yourdomain.com throughout.

What this costs: Today the teardown is the lesson. The new pieces are cheap — an ACM public cert is free, a Route 53 hosted zone is $0.50/month, and CloudWatch alarms are $0.10 each per month. The real money is everything Project 2 has left running: an ALB ($16/mo), a NAT gateway ($32/mo plus data), and an RDS db.t4g.micro on 24/7 are billing right now, whether or not anyone visits. None of it is free-tier once it’s up. Steps 10–12 delete every billable resource in reverse order — a forgotten ALB + NAT + RDS is roughly ₹5,000+/month for nothing. Finish the teardown; don’t just walk away.

# 1. Re-capture the Day-3 resources into variables (nothing new is created here).
export AWS_DEFAULT_REGION=us-east-1
read ALB_ARN ALB_DNS ALB_ZONE <<<"$(aws elbv2 describe-load-balancers --names linkstash-alb \
  --query 'LoadBalancers[0].[LoadBalancerArn,DNSName,CanonicalHostedZoneId]' --output text)"
TG_ARN=$(aws elbv2 describe-target-groups --names linkstash-tg --query 'TargetGroups[0].TargetGroupArn' --output text)
HTTP80=$(aws elbv2 describe-listeners --load-balancer-arn "$ALB_ARN" \
  --query "Listeners[?Port==\`80\`].ListenerArn" --output text)
ZONE_ID=$(aws route53 list-hosted-zones-by-name --dns-name yourdomain.com \
  --query 'HostedZones[0].Id' --output text | sed 's#/hostedzone/##')
echo "$ALB_DNS | $ALB_ZONE | $ZONE_ID"
# Output (all yours-will-differ):
# linkstash-alb-123456789.us-east-1.elb.amazonaws.com | Z35SXDOTRQ7X7K | Z0987654ABCDEFEXAMPLE
# 2. Request an ACM cert for app.yourdomain.com (DNS-validated) and read the CNAME it wants.
CERT_ARN=$(aws acm request-certificate --domain-name app.yourdomain.com \
  --validation-method DNS --query CertificateArn --output text)
sleep 5   # give ACM a moment to attach the validation record
aws acm describe-certificate --certificate-arn "$CERT_ARN" \
  --query 'Certificate.DomainValidationOptions[0].ResourceRecord'
# Output (Name/Value are yours — this CNAME proves you control the domain):
# {
#     "Name": "_a1b2c3.app.yourdomain.com.",
#     "Type": "CNAME",
#     "Value": "_x9y8z7.acm-validations.aws."
# }
# 3. Add that validation CNAME to your hosted zone, then wait for ACM to issue the cert.
VNAME=$(aws acm describe-certificate --certificate-arn "$CERT_ARN" --query 'Certificate.DomainValidationOptions[0].ResourceRecord.Name' --output text)
VVAL=$(aws acm describe-certificate --certificate-arn "$CERT_ARN" --query 'Certificate.DomainValidationOptions[0].ResourceRecord.Value' --output text)
aws route53 change-resource-record-sets --hosted-zone-id "$ZONE_ID" --change-batch "{
  \"Changes\": [{ \"Action\": \"UPSERT\", \"ResourceRecordSet\": {
    \"Name\": \"$VNAME\", \"Type\": \"CNAME\", \"TTL\": 300,
    \"ResourceRecords\": [{ \"Value\": \"$VVAL\" }] } }] }" --query 'ChangeInfo.Status' --output text
aws acm wait certificate-validated --certificate-arn "$CERT_ARN"
echo "cert issued"
# Output (the wait returns once ACM sees the record — up to a few minutes):
# PENDING
# cert issued
# 4. Add an HTTPS:443 listener with the cert, then flip HTTP:80 to a 301 redirect.
aws elbv2 create-listener --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_ARN" \
  --query 'Listeners[0].ListenerArn' --output text
aws elbv2 modify-listener --listener-arn "$HTTP80" \
  --default-actions '[{"Type":"redirect","RedirectConfig":{"Protocol":"HTTPS","Port":"443","StatusCode":"HTTP_301"}}]' \
  --query 'Listeners[0].DefaultActions[0].Type' --output text
# Output:
# arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/linkstash-alb/abc/def
# redirect
# 5. Point an alias A record for app.yourdomain.com at the ALB (alias needs the ALB's own zone ID).
aws route53 change-resource-record-sets --hosted-zone-id "$ZONE_ID" --change-batch "{
  \"Changes\": [{ \"Action\": \"UPSERT\", \"ResourceRecordSet\": {
    \"Name\": \"app.yourdomain.com\", \"Type\": \"A\",
    \"AliasTarget\": { \"HostedZoneId\": \"$ALB_ZONE\", \"DNSName\": \"$ALB_DNS\",
      \"EvaluateTargetHealth\": true } } }] }" --query 'ChangeInfo.Status' --output text
# Output:
# PENDING
# 6. Verify: HTTPS answers healthy, and plain HTTP 301-redirects up to HTTPS.
curl -s https://app.yourdomain.com/healthz
curl -sI http://app.yourdomain.com | head -n 2
# Output (allow a minute or two for DNS to propagate):
# {"status":"ok"}
# HTTP/1.1 301 Moved Permanently
# Location: https://app.yourdomain.com:443/
# 7. Alarm 1 — the ALB returning 5xx server errors (dimension is the ALB's app/... suffix).
ALB_DIM=$(echo "$ALB_ARN" | sed 's#.*:loadbalancer/##')
aws cloudwatch put-metric-alarm --alarm-name linkstash-alb-5xx \
  --namespace AWS/ApplicationELB --metric-name HTTPCode_ELB_5XX_Count \
  --dimensions Name=LoadBalancer,Value="$ALB_DIM" \
  --statistic Sum --period 300 --evaluation-periods 1 --threshold 5 \
  --comparison-operator GreaterThanThreshold --treat-missing-data notBreaching
echo "5xx alarm set"
# Output:
# 5xx alarm set
# 8. Alarm 2 — the Fargate service running hot (CPU averaged over 5 min, >80% for 2 periods).
aws cloudwatch put-metric-alarm --alarm-name linkstash-cpu-high \
  --namespace AWS/ECS --metric-name CPUUtilization \
  --dimensions Name=ClusterName,Value=linkstash-cluster Name=ServiceName,Value=linkstash-svc \
  --statistic Average --period 300 --evaluation-periods 2 --threshold 80 \
  --comparison-operator GreaterThanThreshold
echo "cpu alarm set"
# Output:
# cpu alarm set
# 9. Put a one-screen dashboard with request count, 5xx and CPU.
aws cloudwatch put-dashboard --dashboard-name linkstash --dashboard-body "{
  \"widgets\": [
    { \"type\": \"metric\", \"properties\": { \"title\": \"ALB requests + 5xx\", \"region\": \"us-east-1\",
      \"metrics\": [
        [\"AWS/ApplicationELB\",\"RequestCount\",\"LoadBalancer\",\"$ALB_DIM\"],
        [\"AWS/ApplicationELB\",\"HTTPCode_ELB_5XX_Count\",\"LoadBalancer\",\"$ALB_DIM\"] ] } },
    { \"type\": \"metric\", \"properties\": { \"title\": \"Fargate CPU\", \"region\": \"us-east-1\",
      \"metrics\": [[\"AWS/ECS\",\"CPUUtilization\",\"ClusterName\",\"linkstash-cluster\",\"ServiceName\",\"linkstash-svc\"]] } } ] }" \
  --query 'DashboardValidationMessages' --output text
# Output (empty = the dashboard JSON validated cleanly):
#
# 10. TEARDOWN, edge first: alarms, dashboard, Route 53 records, listeners, ACM cert.
aws cloudwatch delete-alarms --alarm-names linkstash-alb-5xx linkstash-cpu-high
aws cloudwatch delete-dashboards --dashboard-names linkstash
aws route53 change-resource-record-sets --hosted-zone-id "$ZONE_ID" --change-batch "{
  \"Changes\": [{ \"Action\": \"DELETE\", \"ResourceRecordSet\": { \"Name\": \"app.yourdomain.com\", \"Type\": \"A\",
    \"AliasTarget\": { \"HostedZoneId\": \"$ALB_ZONE\", \"DNSName\": \"$ALB_DNS\", \"EvaluateTargetHealth\": true } } },
  { \"Action\": \"DELETE\", \"ResourceRecordSet\": { \"Name\": \"$VNAME\", \"Type\": \"CNAME\", \"TTL\": 300,
    \"ResourceRecords\": [{ \"Value\": \"$VVAL\" }] } }] }" --query 'ChangeInfo.Status' --output text
for L in $(aws elbv2 describe-listeners --load-balancer-arn "$ALB_ARN" --query 'Listeners[].ListenerArn' --output text); do aws elbv2 delete-listener --listener-arn "$L"; done
aws acm delete-certificate --certificate-arn "$CERT_ARN"
echo "edge deleted"
# Output:
# PENDING
# edge deleted
# 11. TEARDOWN, compute: drain and delete the ECS service + cluster, then the ALB + target group.
aws ecs update-service --cluster linkstash-cluster --service linkstash-svc --desired-count 0 >/dev/null
aws ecs delete-service --cluster linkstash-cluster --service linkstash-svc --force >/dev/null
aws ecs delete-cluster --cluster linkstash-cluster --query 'cluster.status' --output text
aws elbv2 delete-load-balancer --load-balancer-arn "$ALB_ARN"
aws elbv2 wait load-balancers-deleted --load-balancer-arns "$ALB_ARN"
aws elbv2 delete-target-group --target-group-arn "$TG_ARN"
echo "compute deleted"
# Output:
# INACTIVE
# compute deleted
# 12. TEARDOWN, data + network: RDS (no final snapshot) + its subnet group, NAT gateway + EIP, then the VPC.
aws rds delete-db-instance --db-instance-identifier linkstash-db --skip-final-snapshot --delete-automated-backups >/dev/null
NAT=$(aws ec2 describe-nat-gateways --filter Name=tag:Name,Values=linkstash-nat --query 'NatGateways[0].NatGatewayId' --output text)
EIP=$(aws ec2 describe-nat-gateways --nat-gateway-ids "$NAT" --query 'NatGateways[0].NatGatewayAddresses[0].AllocationId' --output text)
aws ec2 delete-nat-gateway --nat-gateway-id "$NAT" >/dev/null
aws ec2 wait nat-gateway-deleted --nat-gateway-ids "$NAT"
aws ec2 release-address --allocation-id "$EIP"
aws rds wait db-instance-deleted --db-instance-identifier linkstash-db   # blocks until RDS is fully gone
aws rds delete-db-subnet-group --db-subnet-group-name linkstash-db-subnets   # created Day 2 — frees the subnets
VPC=$(aws ec2 describe-vpcs --filters Name=tag:Name,Values=linkstash-vpc --query 'Vpcs[0].VpcId' --output text)
echo "RDS + DB subnet group gone; NAT + EIP freed. Now remove subnets/route tables/IGW/SGs for $VPC, then: aws ec2 delete-vpc --vpc-id $VPC"
# Output (the db-instance-deleted wait blocks for several minutes until RDS is fully gone):
# RDS + DB subnet group gone; NAT + EIP freed. Now remove subnets/route tables/IGW/SGs for vpc-0abc123, then: aws ec2 delete-vpc --vpc-id vpc-0abc123

Read the teardown back: the ordering is the whole point. Edge (DNS, cert, listeners) comes off before the ALB, the ALB and service before the network they sit in, and the NAT gateway’s Elastic IP is released, not just detached — an unreleased EIP bills on its own. Confirm the finish with aws rds describe-db-instances (the instance should be deleting then gone) and, after the VPC’s dependencies are cleared, aws ec2 delete-vpc. When every command returns “not found”, Project 2 is costing you ₹0 again — which is exactly where a finished project should leave your account.

Common Errors & Fixes

Section 3 of 5 · ~3 min

These three catch almost everyone the first time they wire up TLS and then tear a VPC down. Read the error text slowly — parsing it is the actual skill.

Common error: Creating the HTTPS listener before the ACM cert has finished validating:

An error occurred (CertificateNotFound) when calling the CreateListener operation: Certificate 'arn:aws:acm:...' not found

Why: A cert stays in PENDING_VALIDATION until ACM sees the DNS record you added, and a listener can only bind an ISSUED cert. If you skip the aws acm wait certificate-validated step — or the validation CNAME never resolved because it went into the wrong hosted zone — the cert isn’t usable yet and the listener call can’t find a valid one.

Fix: Confirm status with aws acm describe-certificate --certificate-arn "$CERT_ARN" --query 'Certificate.Status'. If it’s stuck on PENDING_VALIDATION, check the CNAME is present and correct in the zone (aws route53 list-resource-record-sets). Wait for ISSUED, then create the listener.

How you’d spot it in prod: A deploy pipeline that fails at the listener/HTTPS step right after requesting a new cert almost always raced the validation. Gate the listener step on a certificate-validated wait, don’t add a blind sleep.

Common error: delete-vpc fails at the end of teardown because resources inside it still exist:

An error occurred (DependencyViolation) when calling the DeleteVpc operation: The vpc 'vpc-0abc123' has dependencies and cannot be deleted.

Why: A VPC can only be deleted once it is empty. A lingering ENI from the ALB or a still-deleting Fargate task, an un-deleted NAT gateway, the RDS DB subnet group (linkstash-db-subnets, which pins the subnets until the instance is fully deleted and the group is removed), subnets, route tables, or security groups all count as dependencies. Delete order matters: the ALB and ECS tasks must be fully gone (their ENIs released), and the RDS instance and its subnet group deleted, before the subnets and the VPC will go.

Fix: Work inwards from the error — aws ec2 describe-network-interfaces --filters Name=vpc-id,Values=$VPC shows what’s still attached. Delete NAT gateway, then subnets, route table associations, the internet gateway (detach then delete), and non-default security groups, then re-run delete-vpc.

How you’d spot it in prod: DependencyViolation on a teardown is never a bug — it’s ordering. It names the resource type blocking you; remove that layer and retry rather than force-deleting.

Common error: After teardown the bill still shows a small daily charge, traced to an Elastic IP:

aws ec2 describe-addresses --query 'Addresses[].PublicIp'
# [ "52.1.2.3" ]   # an EIP is still allocated to your account

Why: Deleting a NAT gateway frees the IP’s association but does not release the Elastic IP itself — it stays allocated to your account, and an EIP that isn’t attached to a running resource bills by the hour. It’s the classic “I deleted everything but I’m still paying” surprise.

Fix: Release it: aws ec2 release-address --allocation-id <id> (lab step 12 does this). Confirm nothing is left with aws ec2 describe-addresses returning an empty list.

How you’d spot it in prod: A near-zero but non-zero daily cost after a project ends is usually an unreleased EIP, an empty-but-alive load balancer, or an orphaned EBS volume. Check the Cost Explorer service breakdown, then release/delete the named resource.

DNS, TLS & Monitoring Interview Questions

Section 4 of 5 · ~1 min

Cover the answers below and say your own version out loud first — explain why an ALB needs an alias record not a CNAME, and name the two metrics you’d alarm on for this app, before you reveal each answer. Recalling before revealing is what makes these stick when an interviewer asks them cold. The four questions and answers render right after this note.

Go Deeper

Section 5 of 5 · ~1 min

Optional extras if you have ~30 more minutes today:

  • 10 min — Wire an SNS topic to both alarms so an ALARM state emails you: create the topic, subscribe your address, confirm the subscription, then add --alarm-actions <topic-arn> to each put-metric-alarm. Read AWS’s alarm actions guide.
  • 10 min — Skim the Route 53 alias vs CNAME doc and the ACM DNS validation page — the two edge concepts you used today, in AWS’s own words.
  • 5 min — Open Cost Explorer and group by service for the last few days: see the ALB, NAT gateway and RDS lines rise while Project 2 ran and fall after your teardown. Seeing the graph drop is the habit that prevents bill shock.
  • 5 min — Read the “cost” and “teardown” sections of the AWS for DevOps guide to lock in the reverse-order delete discipline you just practised as your default for every cloud project.
Why use a Route 53 alias record for an ALB instead of a CNAME? Both

An ALB has no fixed IP — AWS changes its addresses behind the DNS name — so you always point at the name, never an address. A plain CNAME would work, but Route 53 alias records are better in two concrete ways: they can sit at a zone apex (example.com, where CNAMEs are illegal), and alias queries are free rather than billed per lookup. An alias is a Route 53-only A record that targets an AWS resource by its hosted-zone ID and DNS name and follows it automatically. So for anything AWS-native — ALB, CloudFront, S3 website — I reach for an alias; a CNAME is what I'd use to point at a non-AWS host.

How does ACM DNS validation work, and why redirect HTTP to HTTPS? Both

When you request an ACM certificate with DNS validation, ACM gives you a CNAME record to add to the domain's hosted zone. Once ACM sees that record resolve, it issues the cert and then auto-renews it as long as the record stays — no manual renewal, no expiry pages. You attach the cert to an HTTPS listener on port 443 that terminates TLS at the ALB. The port-80 listener I don't just leave forwarding plaintext: I change it to a 301 redirect to HTTPS, so a user typing http:// is bounced to the encrypted URL and no request ever completes in the clear. Terminate TLS at the edge, redirect everything else up to it.

What are the parts of a CloudWatch alarm, and which metrics matter for this app? Product

An alarm watches one metric against a threshold over a period, for a number of evaluation periods, and flips between OK, ALARM, and INSUFFICIENT_DATA. For linkstash I set two. The first watches the ALB's HTTPCode_ELB_5XX_Count summed over five minutes — server errors the load balancer itself returns — because a spike there means the app is failing users. The second watches the ECS service's CPUUtilization averaged over five minutes, alarming above 80% for two periods, so I know when the container is running hot and needs more tasks. I also set treatMissingData to notBreaching on the 5xx alarm so quiet traffic doesn't false-alarm. Each alarm can notify an SNS topic that pages me.

In what order do you tear down this stack, and what quietly keeps billing if you forget? Both

Reverse of how you built it: edge first, then compute, then data, then network. So delete the Route 53 records, the HTTPS/HTTP listeners and the ACM cert; then the ECS service and cluster; then the RDS instance; then the ALB and target group; then the NAT gateway (and release its Elastic IP), the VPC endpoints, subnets, route tables, internet gateway and finally the VPC. The silent billers are the ones with no per-request cost so they're easy to forget: an idle ALB (~$16/mo), a NAT gateway (~$32/mo plus data), an RDS instance running 24/7, and any unreleased Elastic IP. Left together that is roughly ₹5,000+ a month for nothing.

Mark Day 65 complete

Tomorrow you leave AWS billing behind and open Phase 4 — orchestration — with why Kubernetes exists and the problems it solves.

Stuck on today’s lab? Ask in Mission 90 Q&A