MercadoFresco now has a network of its own, a layered firewall, a load balancer that spreads traffic across two Availability Zones and a CDN that serves the catalogue from Madrid for 0.97 USD a month. And yet a customer cannot buy anything, because the shop answers on alb-mercadofresco-tienda-1234567890.eu-west-1.elb.amazonaws.com and the catalogue on d111111abcdef8.cloudfront.net. On top of that, the two ACM certificates we requested in lessons 03-03 and 03-04 are still in PENDING_VALIDATION, waiting for DNS records that do not yet exist.

Amazon Route 53 is AWS's DNS service: it translates names into addresses, yes, but it also checks the health of the destinations and decides which one to send each visitor to according to weight, latency, country or the state of the system. Its name comes from port 53, the DNS port.

In this lesson Marta points mercadofresco.example at the cloud, validates the certificates, closes off the outstanding HTTPS and gets ready the routing policies MercadoFresco is going to need: the 10 % canary for safe deployments, geolocation to open in Portugal and failover to a maintenance page. And with that we close module 3.

Contents

  1. A practical refresher on DNS: zones, delegation, resolution and TTL
  2. Registering and transferring domains in Route 53
  3. Public and private hosted zones
  4. Record types
  5. Alias records: the piece only AWS has
  6. Validating the ACM certificate by DNS
  7. Route 53 health checks
  8. Routing policies, one by one
  9. Configuring MercadoFresco's DNS from the console and from the CLI
  10. Route 53 Resolver and the forwarding rules to the office
  11. Route 53 costs and clean-up
  12. Closing the module: the complete network architecture

A practical refresher on DNS: zones, delegation, resolution and TTL

DNS (Domain Name System) is a distributed, hierarchical database that translates names into addresses. Its structure is read from right to left:

                                    . (root)
                                    |
                      +-------------+-------------+
                      |             |             |
                    .com          .example      .org        ← top-level domains (TLD)
                                    |
                            mercadofresco.example            ← second-level domain (ours)
                                    |
                +-------------------+-------------------+
                |                   |                   |
        www.mercadofresco   api.mercadofresco   admin.mercadofresco   ← subdomains

A zone is the portion of the tree somebody is responsible for. When MercadoFresco registers mercadofresco.example, it becomes responsible for that zone and for everything hanging off it.

Delegation is the mechanism that connects the levels: the .example servers do not know the shop's IP; all they can say is "for anything under mercadofresco.example, ask these four name servers". That is expressed with NS records.

Recursive resolution, step by step:

sequenceDiagram
    participant N as Browser<br/>(Seville)
    participant R as Recursive resolver<br/>(the ISP's)
    participant Root as Root server
    participant TLD as .example servers
    participant R53 as Route 53<br/>(authoritative)

    N->>R: IP of www.mercadofresco.example?
    Note over R: Do I have it cached? No
    R->>Root: Who knows about .example?
    Root-->>R: Ask the .example servers (NS)
    R->>TLD: Who knows about mercadofresco.example?
    TLD-->>R: ns-123.awsdns-45.com and three more (NS)
    R->>R53: IP of www.mercadofresco.example?
    R53-->>R: 13.32.x.x (with TTL 300)
    R-->>N: 13.32.x.x
    Note over R: Cached for 300 s:<br/>the next queries never leave here

Four concepts come out of this diagram:

  • Route 53 is the authoritative server: the source of truth for the zone.
  • The recursive resolver (the ISP's, or Google's 8.8.8.8) does the work of going and asking, and caches the result.
  • The TTL is how many seconds that answer may be cached. Route 53 only charges for the queries that reach it, so a high TTL saves money, but it slows changes down.
  • Route 53 uses anycast: the same name servers are announced from many points around the world, and every query reaches the nearest one.

How to choose the TTL:

Situation TTL Reason
A stable record (MX, verification TXT) 86,400 (1 day) It never changes; it saves queries
Normal production 300 (5 min) MercadoFresco's sensible default
Days before a migration 60 So the change propagates fast when the time comes
During a migration 60 Being able to roll back in a minute
Alias records (not applicable) Route 53 manages it itself

The technique of dropping the TTL to 60 several days before a migration is what separates a calm migration from a long Sunday: the old TTL has to expire in every cache in the world before the new one takes effect.

Registering and transferring domains in Route 53

Route 53 does three different things that are worth not confusing:

Function What it is You pay
Domain registration Buying mercadofresco.example The TLD's annual fee
Authoritative DNS Answering queries about the zone Per hosted zone and per query
Health checks Watching destinations and routing by their health Per check

They can be used separately: it is perfectly valid to have the domain registered with another provider and the DNS in Route 53. All you have to do is change the name servers in the registrar's panel so that they point at the four Route 53 assigns to the zone.

# Check availability (domain commands are ALWAYS in us-east-1)
aws route53domains check-domain-availability --profile mercadofresco-dev \
  --region us-east-1 --domain-name mercadofresco.example

# List the domains already registered in the account
aws route53domains list-domains --profile mercadofresco-dev --region us-east-1 \
  --query 'Domains[].{Domain:DomainName,Expires:Expiry,AutoRenew:AutoRenew}' --output table

Two settings you should always check after registering a domain:

  • Automatic renewal switched on. An expired domain is a shop switched off, and getting it back can be impossible.
  • Transfer lock switched on. It stops anyone moving the domain to another registrar without authorisation.

Route 53 also includes registration privacy at no extra cost: MercadoFresco's contact details do not appear in public WHOIS lookups.

Public and private hosted zones

A hosted zone is the container for all of a domain's records. There are two types and the difference between them is fundamental:

Public zone Private zone
Who queries it The whole internet Only the associated VPCs
Name servers Four public AWS ones The internal resolver (10.0.0.2)
Needs a registered domain Yes No: it can be made up
Cost 0.50 USD/month 0.50 USD/month
Use in MercadoFresco mercadofresco.example interno.mercadofresco.example

The private zone solves a very specific problem. Today the application connects to the database with a horrible name such as mercadofresco-pedidos.abc123xyz.eu-west-1.rds.amazonaws.com. With a private zone associated with vpc-mercadofresco, that name can be replaced by bd.interno.mercadofresco.example, which can also be repointed at another instance without touching the application's configuration.

# PUBLIC zone for the main domain
PUB_ZONE=$(aws route53 create-hosted-zone --profile mercadofresco-dev \
  --name mercadofresco.example \
  --caller-reference "mercadofresco-publica-$(date +%s)" \
  --hosted-zone-config Comment="MercadoFresco public zone",PrivateZone=false \
  --query 'HostedZone.Id' --output text | sed 's|/hostedzone/||')

# The four name servers that have to be configured at the registrar
aws route53 get-hosted-zone --profile mercadofresco-dev --id "$PUB_ZONE" \
  --query 'DelegationSet.NameServers' --output table

# PRIVATE zone, associated with the VPC from lesson 03-01
PRIV_ZONE=$(aws route53 create-hosted-zone --profile mercadofresco-dev \
  --name interno.mercadofresco.example \
  --caller-reference "mercadofresco-privada-$(date +%s)" \
  --vpc VPCRegion=eu-west-1,VPCId="$VPC_ID" \
  --hosted-zone-config Comment="Internal VPC names",PrivateZone=true \
  --query 'HostedZone.Id' --output text | sed 's|/hostedzone/||')

An important requirement: for a private zone to work, the VPC must have enableDnsSupport and enableDnsHostnames set to true. It is exactly the setting we switched on in 03-01, and forgetting it is the number one cause of a private zone "not resolving".

Record types

Records are the zone's entries. These are the ones MercadoFresco needs:

Type What it associates Example in mercadofresco.example Notes
A Name → IPv4 oficina.mercadofresco.example → 81.45.20.7 The most common
AAAA Name → IPv6 www → 2600:9000:... Needed if the ALB or CloudFront have IPv6
CNAME Name → another name blog → mercadofresco.wordpress.com Cannot be used at the domain apex
MX Mail 10 mail1.proveedor.example The number is the priority: lower wins
TXT Free text "v=spf1 include:_spf.proveedor.example ~all" SPF, DKIM, ownership verification
NS The zone's name servers ns-123.awsdns-45.com Created for you; do not delete it
SOA Zone metadata ns-123... awsdns-hostmaster... Created for you; one per zone
CAA Which authorities may issue certificates 0 issue "amazon.com" Recommended: stops another CA issuing for your domain
SRV Service, protocol, port _sip._tcp 10 60 5060 sip.example Telephony and special services
PTR IP → name (reverse DNS) Managed by the owner of the IP range

Two of them deserve a comment:

CAA. With this record, MercadoFresco declares that only Amazon may issue certificates for its domain. If an attacker managed to fool another certificate authority, that authority would look up the CAA and refuse to issue. It costs nothing and it prevents a whole class of attack:

mercadofresco.example.  CAA  0 issue "amazon.com"
mercadofresco.example.  CAA  0 issuewild "amazon.com"
mercadofresco.example.  CAA  0 iodef "mailto:aws-alertas@mercadofresco.example"

CNAME and its limitation. A CNAME says "this name is really that other one". RFC 1034 forbids a name that has a CNAME from having any other record. And the domain apex (mercadofresco.example, with no www) necessarily has SOA and NS records. Therefore:

A domain apex CANNOT have a CNAME record. Ever.

This is a serious problem, because an ALB and a CloudFront distribution have no fixed IP: all they have is names. You cannot use an A record, because there is no IP to put in it, and you cannot use a CNAME, because it is the apex. The solution is what comes next.

Alias records: the piece only AWS has

An alias record is an extension of Route 53's own. From the outside it behaves like an A record (or AAAA): it returns an IP. Inside, Route 53 works out on the spot what the current IP of the AWS resource it points at is.

CNAME Alias record
Is it standard DNS Yes No: it is Route 53's own
Can it be used at the apex No Yes
What it returns to the client Another name (forcing a second query) The IP directly
Cost of the queries Charged Free
It can point at Any DNS name AWS resources and other records in the same zone
TTL You define it AWS manages it
Adapts if the resource changes IP Yes (the name does not change) Yes, automatically
Checks the destination's health No Yes, with EvaluateTargetHealth

Three concrete, measurable advantages:

  1. It works at the apex. mercadofresco.example can point at CloudFront. With a CNAME it would be impossible.
  2. It is free. Route 53 does not charge for queries resolved by alias to AWS resources. With 1.2 million queries a month, that is about 0.48 USD you do not pay; at scale, far more.
  3. It is faster. A CNAME forces the resolver to make two queries: first the original name, then the destination. An alias returns the IP in one.

What an alias can point at:

Destination Example in MercadoFresco
CloudFront distribution mercadofresco.exampled111111abcdef8.cloudfront.net
Load balancer (ALB or NLB) directo.mercadofresco.example → the ALB
S3 bucket as a website mantenimiento.mercadofresco.example
API Gateway, VPC endpoint, Global Accelerator
Another record in the same zone www → the apex

The practical rule: if the destination is an AWS resource, always use an alias. CNAME only for third-party services.

Validating the ACM certificate by DNS

Now we close what was left pending in 03-03 and 03-04. ACM needs to check that the domain is yours, and the way to prove it is to create a specific CNAME record that only somebody who controls the zone can create.

# See which CNAME record ACM is asking for (one per name on the certificate)
aws acm describe-certificate --profile mercadofresco-dev --region eu-west-1 \
  --certificate-arn "$CERT_ARN_ALB" \
  --query 'Certificate.DomainValidationOptions[].{
      Domain:DomainName, Status:ValidationStatus,
      Name:ResourceRecord.Name, Type:ResourceRecord.Type, Value:ResourceRecord.Value}' \
  --output table

It returns something like:

Name:  _a79865eb4cd1a6ab990a45779b53961d.mercadofresco.example
Type:  CNAME
Value: _424c7224e9b0146f9a8808af955727d0.acm-validations.aws.

A shortcut creates the record automatically if the domain is in Route 53 in the same account:

# The long way (instructive): build the change file
cat > validacion-acm.json <<'JSON'
{
  "Comment": "DNS validation of the shop certificate",
  "Changes": [{
    "Action": "UPSERT",
    "ResourceRecordSet": {
      "Name": "_a79865eb4cd1a6ab990a45779b53961d.mercadofresco.example",
      "Type": "CNAME",
      "TTL": 300,
      "ResourceRecords": [
        { "Value": "_424c7224e9b0146f9a8808af955727d0.acm-validations.aws." }
      ]
    }
  }]
}
JSON

aws route53 change-resource-record-sets --profile mercadofresco-dev \
  --hosted-zone-id "$PUB_ZONE" --change-batch file://validacion-acm.json

# Wait for ACM to spot it and issue the certificate (usually 5-30 minutes)
aws acm wait certificate-validated --profile mercadofresco-dev --region eu-west-1 \
  --certificate-arn "$CERT_ARN_ALB"

You have to repeat it with the us-east-1 certificate we requested for CloudFront. A useful detail: since both certificates cover the same domains, the validation CNAME record is identical, so in practice creating it once validates both.

Two properties of DNS validation that make it far superior to email validation:

  • Renewal is automatic. As long as the CNAME record still exists, ACM renews the certificate on its own, every year, for ever. Never delete that record.
  • It does not depend on somebody reading an email. Email validation is sent to admin@mercadofresco.example and expires in 72 hours; if nobody opens it, you start again.

With the certificates issued, you can now really create the ALB's HTTPS listener from 03-03 and associate the custom domain with the CloudFront distribution from 03-04. MercadoFresco's HTTPS is now closed off.

Route 53 health checks

Route 53 can watch destinations from a global network of checkers and use the result to decide what it answers. They are the basis of the failover policy.

Type What it checks When
Endpoint A specific destination over HTTP, HTTPS or TCP Watching the ALB or an external server
Calculated Combines other checks with AND, OR, NOT "Healthy if at least 2 of 3 are"
CloudWatch alarm The state of an alarm Routing by a business metric (05-01)
HC_ID=$(aws route53 create-health-check --profile mercadofresco-dev \
  --caller-reference "hc-alb-mercadofresco-$(date +%s)" \
  --health-check-config '{
    "Type": "HTTPS",
    "FullyQualifiedDomainName": "alb-mercadofresco-tienda-1234567890.eu-west-1.elb.amazonaws.com",
    "Port": 443,
    "ResourcePath": "/salud",
    "RequestInterval": 30,
    "FailureThreshold": 3,
    "MeasureLatency": true,
    "EnableSNI": true
  }' \
  --query 'HealthCheck.Id' --output text)

aws route53 change-tags-for-resource --profile mercadofresco-dev \
  --resource-type healthcheck --resource-id "$HC_ID" \
  --add-tags Key=Name,Value=hc-mercadofresco-tienda Key=Proyecto,Value=mercadofresco \
             Key=Entorno,Value=produccion Key=Componente,Value=tienda \
             Key=Propietario,Value=marta Key=CentroCoste,Value=operaciones

Important details:

  • It uses the same /salud path as the ALB in 03-03, and for the same reasons: fast and local.
  • Route 53's checkers come from the internet, from about 15 locations. The destination has to be publicly reachable; a check against an instance in a private subnet will never pass.
  • With RequestInterval: 30 and FailureThreshold: 3, a failure is spotted in about 90 seconds. There is a 10-second option (fast interval) that costs more.
  • MeasureLatency: true adds latency graphs by region, very useful and at no extra cost.
  • Cost: 0.50 USD a month per check of an AWS destination, 0.75 USD if it is external.

Routing policies, one by one

Here Route 53 stops being "a table of names" and becomes an architecture tool. Each policy answers a different question.

flowchart TD
    Q{"How many destinations<br/>for the same name?"}
    Q -->|One| S["<b>Simple</b><br/>Always returns the same thing"]
    Q -->|Several| Q2{"What decides<br/>where each visitor goes?"}
    Q2 -->|"A % I set"| P["<b>Weighted</b><br/>10 % canary"]
    Q2 -->|"The fastest"| L["<b>Latency</b><br/>Multi-region"]
    Q2 -->|"Their country"| G["<b>Geolocation</b><br/>Portugal in Portuguese"]
    Q2 -->|"The main one, if alive"| F["<b>Failover</b><br/>Maintenance page"]
    Q2 -->|"All the healthy ones"| M["<b>Multivalue</b><br/>Simple sharing with health"]

Simple

One name, one destination. It is the default policy and the one MercadoFresco uses for almost all.

Name Type Destination
mercadofresco.example A (alias) CloudFront distribution
www.mercadofresco.example A (alias) CloudFront distribution

It does not support health checks. If the destination goes down, Route 53 keeps returning it.

Weighted: the 10 % canary

A weight is assigned to each destination and Route 53 shares traffic out proportionally: destination weight ÷ sum of all weights.

MercadoFresco's case: Luis is going to deploy the new version of the shop and wants only 10 % of customers to receive it, so he can measure errors before exposing it to everyone.

Identifier Weight Destination % of traffic
tienda-estable 90 tg-mercadofresco-tienda (current version) 90 %
tienda-canario 10 tg-mercadofresco-tienda-nueva 10 %
{
  "Comment": "Canary deployment to 10 % of the new shop version",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "tienda.mercadofresco.example",
        "Type": "A",
        "SetIdentifier": "tienda-estable",
        "Weight": 90,
        "AliasTarget": {
          "HostedZoneId": "Z32O12XQLNTSW2",
          "DNSName": "alb-mercadofresco-tienda-1234567890.eu-west-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "tienda.mercadofresco.example",
        "Type": "A",
        "SetIdentifier": "tienda-canario",
        "Weight": 10,
        "AliasTarget": {
          "HostedZoneId": "Z32O12XQLNTSW2",
          "DNSName": "alb-mercadofresco-nueva-9876543210.eu-west-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }
  ]
}

Three details in the JSON:

  • SetIdentifier is compulsory in every policy that is not simple: it is what tells apart two records with the same name and type.
  • HostedZoneId: Z32O12XQLNTSW2 is not MercadoFresco's zone: it is the zone identifier of the ELB service in eu-west-1, a fixed value published by AWS. Every service and region has its own, and confusing it with your own zone is a common mistake. For CloudFront it is always Z2FDTNDATAQYW2.
  • EvaluateTargetHealth: true means that if an ALB has no healthy targets, Route 53 stops returning it and sends all the traffic to the other one.

Setting a weight to 0 withdraws a destination without deleting the record: the quickest way to abort a canary. This mechanism comes back in lesson 08-03, when CodeDeploy automates progressive deployment.

Latency

Route 53 answers with the destination in the region with the lowest measured latency for the resolver asking. Not "the geographically nearest", but the fastest by real AWS measurements.

MercadoFresco's future scenario: if the expansion works and a region is opened in eu-central-1 for Germany, a customer in Munich would go to Frankfurt and one in Seville to Ireland, with the same name. Today, with a single region, this policy adds nothing.

Geolocation

It routes according to where the visitor is from: continent, country or, in the United States, state. It is a content decision, not a performance one.

MercadoFresco's case: opening in Portugal with the shop in Portuguese and prices at their VAT rate.

SetIdentifier Location Destination
es Country ES tg-mercadofresco-tienda (Spanish)
pt Country PT tg-mercadofresco-tienda-pt (Portuguese)
defecto * (default) tg-mercadofresco-tienda (Spanish)
# The default record is COMPULSORY: without it, a visitor from a country
# that is not covered receives NO answer at all (NXDOMAIN)
aws route53 change-resource-record-sets --profile mercadofresco-dev \
  --hosted-zone-id "$PUB_ZONE" --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "mercadofresco.example",
        "Type": "A",
        "SetIdentifier": "defecto",
        "GeoLocation": { "CountryCode": "*" },
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    }]
  }'

The default record is the part people forget, and it causes the hardest incident to reproduce: everything works in the office and a customer in Andorra gets "server not found".

There is also geoproximity routing, which lets you shift the sharing out with a numeric bias —"widen the radius of the Ireland region by 30 %"— and which can only be configured with Route 53 Traffic Flow.

Failover: the maintenance page

One primary destination and one secondary. Route 53 returns the primary while its health check is healthy; if it fails, it returns the secondary.

MercadoFresco's case: if the whole ALB goes down, instead of a connection error customers see a static page hosted in S3 explaining the situation and giving the support phone number.

flowchart LR
    C["Client"] --> R53["Route 53<br/>mercadofresco.example"]
    HC{{"Health check<br/>hc-mercadofresco-tienda<br/>/salud every 30 s"}}
    R53 -.queries.-> HC
    HC -->|"Healthy"| ALB["PRIMARY<br/>alb-mercadofresco-tienda"]
    HC -->|"Unhealthy"| S3["SECONDARY<br/>mercadofresco-mantenimiento<br/>(static site in S3)"]
{
  "Comment": "Failover to the maintenance page",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "directo.mercadofresco.example",
        "Type": "A",
        "SetIdentifier": "primario-alb",
        "Failover": "PRIMARY",
        "HealthCheckId": "abcdef01-2345-6789-abcd-ef0123456789",
        "AliasTarget": {
          "HostedZoneId": "Z32O12XQLNTSW2",
          "DNSName": "alb-mercadofresco-tienda-1234567890.eu-west-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "directo.mercadofresco.example",
        "Type": "A",
        "SetIdentifier": "secundario-mantenimiento",
        "Failover": "SECONDARY",
        "AliasTarget": {
          "HostedZoneId": "Z1BKCTXD74EZPE",
          "DNSName": "s3-website-eu-west-1.amazonaws.com",
          "EvaluateTargetHealth": false
        }
      }
    }
  ]
}

The maintenance page lives in a static-website bucket (the technique from 02-03) that must be named exactly the same as the DNS name: directo.mercadofresco.example. It is an S3 requirement for hosting websites with a custom domain.

And the honest limitation: DNS is cached. If the TTL is 300 seconds, some customers will carry on going to the ALB that is down for five minutes. That is why DNS failover is a last-resort safety net, and real high availability is the one we have already built: two Availability Zones behind a single ALB, which switches over in seconds and without depending on DNS.

Multivalue

It returns up to eight healthy records at once, chosen at random, and the client tries one. It is like the classic round-robin sharing of DNS, but with health checks: destinations that are down are not returned.

It is not a load balancer: it does not measure load, it does not drain connections and it depends on the client retrying. It is good for destinations that are not behind an ALB, for example a set of mail servers or internal APIs. MercadoFresco does not need it, having an ALB.

Summary table

Policy Decides by Needs SetIdentifier Uses health checks Case in MercadoFresco
Simple Nothing No No mercadofresco.example → CloudFront
Weighted A fixed percentage Yes Yes 10 % canary (comes back in 08-03)
Latency The fastest region Yes Yes Future multi-region
Geolocation The visitor's country Yes Yes Opening in Portugal
Geoproximity Distance with a bias Yes Yes Requires Traffic Flow
Failover The primary's health Yes Compulsory Maintenance page in S3
Multivalue Random among the healthy Yes Yes Not applicable: there is an ALB

Policies can be nested with Traffic Flow: geolocation by country first and, within each country, weighted for the canary. It is the way to combine "Portugal sees the Portuguese shop" with "10 % of the Portuguese see the new version".

Configuring MercadoFresco's DNS from the console and from the CLI

From the console, in summary

  1. Route 53 → Hosted zones → mercadofresco.example.
  2. Create record. Blank name for the apex.
  3. Switch on Alias, choose "Alias to CloudFront distribution" and select the distribution.
  4. Routing policy: Simple. Save.
  5. Repeat with www, this time "Alias to another record in this zone" → the apex.
  6. For the MX, TXT and CAA records: switch Alias off and enter the values, one per line.

The console has a real advantage here: when you choose an alias destination it offers it in a list, so there is no way to get the service's HostedZoneId wrong.

From the CLI, with the complete change file

change-resource-record-sets applies a batch of changes atomically: either they all apply or none of them does.

{
  "Comment": "Complete DNS configuration for MercadoFresco",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "mercadofresco.example",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "mercadofresco.example",
        "Type": "AAAA",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "www.mercadofresco.example",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "admin.mercadofresco.example",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z32O12XQLNTSW2",
          "DNSName": "alb-mercadofresco-tienda-1234567890.eu-west-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "mercadofresco.example",
        "Type": "MX",
        "TTL": 3600,
        "ResourceRecords": [
          { "Value": "10 mx1.proveedorcorreo.example" },
          { "Value": "20 mx2.proveedorcorreo.example" }
        ]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "mercadofresco.example",
        "Type": "TXT",
        "TTL": 3600,
        "ResourceRecords": [
          { "Value": "\"v=spf1 include:_spf.proveedorcorreo.example -all\"" }
        ]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "mercadofresco.example",
        "Type": "CAA",
        "TTL": 3600,
        "ResourceRecords": [
          { "Value": "0 issue \"amazon.com\"" },
          { "Value": "0 issuewild \"amazon.com\"" },
          { "Value": "0 iodef \"mailto:aws-alertas@mercadofresco.example\"" }
        ]
      }
    }
  ]
}
CHANGE_ID=$(aws route53 change-resource-record-sets --profile mercadofresco-dev \
  --hosted-zone-id "$PUB_ZONE" \
  --change-batch file://dns-mercadofresco.json \
  --query 'ChangeInfo.Id' --output text)

# Wait for the change to propagate to every Route 53 server
aws route53 wait resource-record-sets-changed --profile mercadofresco-dev --id "$CHANGE_ID"

# Check the result
aws route53 list-resource-record-sets --profile mercadofresco-dev \
  --hosted-zone-id "$PUB_ZONE" \
  --query 'ResourceRecordSets[].{Name:Name,Type:Type,TTL:TTL,
           Alias:AliasTarget.DNSName,Values:ResourceRecords[].Value}' \
  --output table

Notes about this batch:

  • UPSERT creates the record if it does not exist and updates it if it does. It is idempotent, which makes it safe to re-run. CREATE would fail on the second run and DELETE requires the values to match exactly.
  • Alias records carry no TTL: AWS manages it.
  • The TXT record carries quotation marks inside the value, escaped in the JSON. Leaving them out is a common mistake.
  • Z2FDTNDATAQYW2 is the zone identifier of CloudFront, identical the world over; Z32O12XQLNTSW2 is the one for ELB in eu-west-1.

A check from outside, which is the only thing that confirms it really works:

# Query the authoritative servers directly, bypassing the caches
dig +short mercadofresco.example @ns-123.awsdns-45.com

# See the full resolution chain
dig +trace www.mercadofresco.example

# Confirm that HTTPS now works with the right certificate
curl -sI https://mercadofresco.example | head -5
openssl s_client -connect mercadofresco.example:443 -servername mercadofresco.example </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -dates

Route 53 Resolver and the forwarding rules to the office

The Route 53 Resolver is the resolver that lives at the VPC's .2 address (10.0.0.2) and that we already met in 03-01. As well as resolving AWS names and those of the private zones, it supports two kinds of rule for connecting to the office network:

Type Direction What it does
Outbound endpoint + forwarding rule VPC → office Queries about mercadofresco.local are forwarded to the office DNS
Inbound endpoint Office → VPC The office can resolve bd.interno.mercadofresco.example

Using it for real requires the network connectivity that only a VPN or Direct Connect provides, which we mentioned in 03-01 and which is outside the scope of this course. Indicative cost: each endpoint is two ENIs and costs about 0.125 USD per hour per ENI, so it is not a resource to leave switched on out of curiosity.

Route 53 costs and clean-up

⚠️ Cost warning

Item Price MercadoFresco
Hosted zone 0.50 USD/month each (the first 25) 2 zones → 1.00 USD/month
Standard queries 0.40 USD per million (first 1,000 M) ~1.2 M → 0.48 USD
Queries resolved by alias to AWS resources Free Most of them → 0.00 USD
Special routing queries (latency, geo) 0.60 USD per million Marginal
Health check (AWS destination) 0.50 USD/month 1 → 0.50 USD/month
Health check (external destination) 0.75 USD/month
Domain registration Depends on the TLD, annual
Estimated total ≈ 1.60 USD/month

Route 53 is by far the cheapest thing in the module. But there is an unpleasant detail: the hosted zone is charged from the moment it is created, even with no records, and the first 12 hours are charged even if you delete it. Creating and deleting zones to practise is not free.

Clean-up (you have to empty the zone before deleting it; the NS and SOA records go on their own):

# Delete every record that is not NS or SOA
aws route53 list-resource-record-sets --profile mercadofresco-dev \\
  --hosted-zone-id "$PUB_ZONE" \\
  --query 'ResourceRecordSets[?Type!=`NS` && Type!=`SOA`]' > registros.json
# (build the change-batch with "Action": "DELETE" for each one and apply it)

aws route53 delete-hosted-zone --profile mercadofresco-dev --id "$PUB_ZONE"
aws route53 delete-health-check --profile mercadofresco-dev --health-check-id "$HC_ID"

What cannot be undone: registering a domain is not refundable and cannot be cancelled. Only register a domain when you are really going to use it.

Closing the module: the complete network architecture

flowchart TB
    U["Customers<br/>Spain and Portugal"]
    R53(["<b>Route 53</b><br/>mercadofresco.example<br/>alias A/AAAA · managed TTL"])
    CF["<b>CloudFront</b> · PriceClass_100<br/>~700 edge locations · HTTP/3<br/>ACM certificate (us-east-1)"]

    subgraph AWS["Account 111122223333 — eu-west-1 (Ireland)"]
        subgraph VPC["vpc-mercadofresco — 10.0.0.0/16"]
            subgraph PUB["Public subnets · 10.0.0.0/20 and 10.0.16.0/20"]
                ALB["<b>alb-mercadofresco-tienda</b><br/>sg-mercadofresco-alb<br/>HTTPS 443 · TLS 1.2/1.3<br/>redirect 80 → 443"]
                NAT["NAT Gateway ×2"]
            end
            subgraph APP["Application subnets · 10.0.32.0/20 and 10.0.48.0/20"]
                A1["asg-mercadofresco-tienda<br/>eu-west-1a<br/>sg-mercadofresco-tienda"]
                A2["asg-mercadofresco-tienda<br/>eu-west-1b<br/>sg-mercadofresco-tienda"]
            end
            subgraph DAT["Data subnets · 10.0.64.0/20 and 10.0.80.0/20 — no way out to the internet"]
                DB[("<b>mercadofresco-pedidos</b><br/>PostgreSQL 16 Multi-AZ<br/>sg-mercadofresco-basedatos")]
                EFS["efs-mercadofresco-fotos"]
            end
            VPCE(["vpce-mercadofresco-s3<br/>(free)"])
        end
        S3[("<b>mercadofresco-catalogo-fotos</b><br/>closed with OAC<br/>only distribution E2QWERTY123ABC")]
        LOG[("mercadofresco-registros-web<br/>ALB + CloudFront + Flow Logs")]
    end

    U --> R53 --> CF
    CF -->|"/productos/* · /miniaturas/*<br/>90 % from cache"| S3
    CF -->|"HTML and /api/*<br/>https-only"| ALB
    ALB --> A1
    ALB --> A2
    A1 --> DB
    A2 --> DB
    A1 --> EFS
    A1 --> VPCE --> S3
    A1 -.-> NAT
    ALB -.-> LOG
    CF -.-> LOG

What is solved by the end of module 3

Course problem Status How
1. Friday outages Solved ASG in 2 AZs + ALB with health checks: 2,400 orders/h of capacity against 900 of demand, and it holds up losing a whole AZ
2. Unreliable backups Solved (module 2) DLM snapshots, S3 versioning, RDS PITR
3. Not being able to grow to more cities On track Reproducible multi-AZ network, global CloudFront, geolocation ready for Portugal
4. Risky deployments On track Weighted routing for the 10 % canary; the automation is missing (module 8)

And in money, MercadoFresco's complete network:

Component Approximate monthly cost
2 NAT Gateways ≈ 70 USD
ALB (hours + LCUs) ≈ 37 USD
CloudFront (within the free tier) ≈ 0 USD
Route 53 (2 zones + 1 check) ≈ 1.60 USD
S3 gateway endpoint 0 USD
Network total ≈ 109 USD/month
Saving achieved on S3 transfer −27 USD/month

The two NAT Gateways are 64 % of the network spend and will be the first candidate for review in lesson 11-03 with Cost Explorer.

What is still outstanding

The network is built and protected at the packet level, but four gaps remain, and they are exactly the content of module 4:

  • Fine-grained permissions: who can do what in the account. Today there is one admin user and roles created as needed, with no systematic least-privilege policy → 04-01, IAM.
  • Managed encryption: the alias/mercadofresco-datos key exists, but we have not studied how it is governed, rotated or shared → 04-02, KMS.
  • Secrets: mfadmin's password is still where it should not be → 04-03, Secrets Manager and Parameter Store.
  • Protection against attacks: the NACL we used to block an IP in 03-02 does not scale, and a distributed denial of service would knock the shop over despite everything built → 04-04, Shield and 04-05, WAF, both applied at CloudFront's edge.

Common Mistakes and Tips

Trying to put a CNAME at the domain apex. It is forbidden by the standard. The answer is always an alias record.

Forgetting the default record in geolocation. A visitor from a country you have not covered gets no answer. Always create the record with CountryCode: "*".

Confusing the alias HostedZoneId with your zone's. The one in AliasTarget is the destination service's (Z2FDTNDATAQYW2 for CloudFront, a different one per region for ELB), not mercadofresco.example's.

Requesting the CloudFront certificate in the wrong region. It has to be in us-east-1. It came up in 03-04 already and it is still the most repeated trap.

Deleting the ACM validation CNAME record. It looks like rubbish and it is not: without it, ACM cannot renew the certificate and it will expire in a year, silently.

Not lowering the TTL before a migration. If you migrate with a TTL of 86,400, some customers will go to the old destination for a whole day. Drop it to 60 days in advance.

Trusting high availability to DNS. Route 53's failover is limited by the caches of the resolvers. Real availability is provided by the ALB across two AZs, in seconds and without depending on DNS.

Using a CNAME where an alias fits. It costs money in queries, adds an extra query of latency and does not evaluate the destination's health.

Forgetting the domain's automatic renewal. An expired domain is a company that has vanished from the internet, and getting it back can be impossible if somebody else registers it.

The golden tip: after every DNS change, check with dig +short <name> @<authoritative server> instead of opening the browser. The browser and the operating system have caches of their own and will make you believe the change has not worked for minutes on end.

Exercises

Exercise 1: design the DNS for the opening in Portugal

MercadoFresco is opening in Portugal. Requirements: visitors from Portugal must see the Portuguese shop (a different target group on the same ALB), those from Spain the Spanish one, and any other country the Spanish one; on top of that, 10 % of Portuguese visitors must receive the new version of the shop to test it; and if the ALB goes down, everybody must see the maintenance page. Design the record structure, stating policies, identifiers and nesting.

Exercise 2: diagnose a domain that does not resolve

Marta has created the hosted zone, has added the alias at the apex, and mercadofresco.example is still not working 24 hours later, while dig @ns-123.awsdns-45.com mercadofresco.example does answer correctly. Explain what is going on and write the checks, in order, that confirm it.

Exercise 3: calculate the annual DNS cost and decide the TTL

MercadoFresco receives 5 million DNS queries a month, of which 80 % are resolved by alias towards CloudFront. It has 2 hosted zones and 3 health checks, one of them against an external provider. Calculate the annual cost, and estimate how much would be saved by raising the TTL of the non-alias records from 300 to 3,600 seconds.

Solutions

Solution 1

Two nested levels are needed, and since Route 53 does not allow policies to be nested directly on the same name, it is solved with intermediate names (which is exactly what Traffic Flow does under the bonnet).

Level 1 — geolocation on mercadofresco.example:

SetIdentifier GeoLocation Destination
geo-es CountryCode: ES alias → es.mercadofresco.example
geo-pt CountryCode: PT alias → pt.mercadofresco.example
geo-defecto CountryCode: * alias → es.mercadofresco.example

Level 2 — weighting on pt.mercadofresco.example (only Portugal gets a canary):

SetIdentifier Weight Destination
pt-estable 90 alias → ALB (current Portuguese target group)
pt-canario 10 alias → ALB (new Portuguese target group)

Level 2 — es.mercadofresco.example: simple policy, alias to the ALB with EvaluateTargetHealth: true.

Failover: it is applied at level 2, on es and on pt, each one with its own primary/secondary pair pointing at the maintenance bucket. Putting it at level 1 would not work, because geolocation and failover cannot be combined in the same set of records.

Details that make this really work:

  • The geo-defecto record is essential: without it, a customer in Andorra or in France gets no answer.
  • The aliases to es. and pt. are aliases to another record in the same zone, which are also free.
  • A 10 % canary limited to Portugal exposes a few hundred customers to the new version: enough to spot errors, too few to do damage. Setting the weight of pt-canario to 0 aborts the deployment in a minute.
  • In practice, and for more than two levels, you would use Route 53 Traffic Flow, which lets you draw the decision tree and version the policy.

Solution 2

The symptom is unmistakable: Route 53 answers correctly when it is asked directly, but the world never gets as far as asking it. That means the delegation has not been done: the registrar's name servers are still pointing somewhere else.

# 1. Which name servers has Route 53 assigned to the zone?
aws route53 get-hosted-zone --profile mercadofresco-dev --id "$PUB_ZONE" \
  --query 'DelegationSet.NameServers' --output text
# 2. Which name servers does the TLD actually publish? (the decisive check)
dig NS mercadofresco.example @a.gtld-servers.net

If the four names from steps 1 and 2 do not match, there is the problem.

# 3. If the domain is registered in Route 53, check and fix it
aws route53domains get-domain-detail --profile mercadofresco-dev --region us-east-1 \
  --domain-name mercadofresco.example --query 'Nameservers[].Name'

aws route53domains update-domain-nameservers --profile mercadofresco-dev --region us-east-1 \
  --domain-name mercadofresco.example \
  --nameservers Name=ns-123.awsdns-45.com Name=ns-456.awsdns-78.net \
                Name=ns-789.awsdns-01.org Name=ns-012.awsdns-34.co.uk

A second possible cause, if the NS records do match: there are two hosted zones for the same domain (it is easy to create a duplicate), and the records were added to the one that is not delegated. Each zone has different name servers.

aws route53 list-hosted-zones --profile mercadofresco-dev \
  --query 'HostedZones[?Name==`mercadofresco.example.`].{Id:Id,Records:ResourceRecordSetCount}'

If it returns more than one, keep the delegated one and delete the other.

A third cause: a private zone with the same name as the public one, associated with the VPC. From inside the VPC the private one would resolve and from outside the public one, giving contradictory results depending on where you test from. This is what is called split-horizon DNS, useful when it is deliberate and very confusing when it is not.

A fourth cause, the most trivial: the local cache. You rule it out by comparing dig +short mercadofresco.example (with cache) with dig +short mercadofresco.example @8.8.8.8 (external resolver).

Solution 3

Monthly cost:

Item Calculation Cost
Hosted zones 2 × 0.50 USD 1.00 USD
Alias queries (80 % of 5 M = 4 M) Free 0.00 USD
Standard queries (20 % = 1 M) 1 × 0.40 USD/million 0.40 USD
Checks of AWS destinations 2 × 0.50 USD 1.00 USD
Check of an external destination 1 × 0.75 USD 0.75 USD
Monthly total 3.15 USD

Annual cost: 37.80 USD.

Effect of raising the TTL from 300 to 3,600 seconds. It only affects the million non-alias queries, which are the ones that are charged. Multiplying the TTL by 12 reduces the queries that reach Route 53 by roughly the same proportion, though not exactly: there are many different resolvers, each one with its own cache, and some clients ignore long TTLs. With a conservative reduction of 70 %:

  • Billable queries: 1,000,000 → ~300,000
  • Cost of queries: 0.40 → 0.12 USD/month
  • Saving: 0.28 USD/month, 3.36 USD a year.

The honest conclusion of the exercise: it is not worth it. Saving three dollars a year in exchange for every DNS change taking an hour to propagate is a bad deal. The TTL should be chosen on operational grounds —how long it takes you to be able to revert a change— and not on cost, except at the scale of hundreds of millions of queries.

Where there is a real economic decision is in the health checks: 2.10 USD a month on three checks is more than the queries and the zones put together. And where money is genuinely saved is in the 80 % of queries resolved by alias: if those 4 million were CNAMEs, they would cost an extra 1.60 USD a month, five times more than all the currently billable queries.

Conclusion

MercadoFresco is on the internet. mercadofresco.example answers, with valid HTTPS, served from the edge location closest to each customer, over a network that has been designed, filtered and spread across two Availability Zones. You know how DNS really works —zones, delegation through NS records, recursive resolution and the role of caching—, you know that Route 53 only charges for the queries that reach it and that the TTL is an operational decision: 300 seconds in production and 60 days in advance of a migration. You can tell apart the service's three separate functions: domain registration, authoritative DNS and health checks.

You have created the public zone for mercadofresco.example and the private zone interno.mercadofresco.example associated with vpc-mercadofresco, which only works because in 03-01 we switched on enableDnsHostnames. You know the record types and you have added a CAA that stops any authority other than Amazon issuing certificates for the domain. And you understand the limitation that explains everything: a domain apex cannot have a CNAME, because it already has SOA and NS. Hence the alias records, which are Route 53's own, work at the apex, return the IP in a single query, are free and evaluate the destination's health with EvaluateTargetHealth. You know that an alias's HostedZoneId is the destination service'sZ2FDTNDATAQYW2 for CloudFront— and not your zone's.

You have created the CNAME record that validates the ACM certificates and you have closed off the HTTPS that was outstanding from 03-03 and 03-04, knowing that this record must never be deleted because it is what makes automatic renewal possible. You have set up a health check against /salud and you have gone through the routing policies with a real MercadoFresco case for each one: simple for the apex, weighted for the 10 % canary that will come back in 08-03, latency for the future multi-region setup, geolocation for the opening in Portugal —with the default record almost everybody forgets—, failover to the maintenance page in S3, and multivalue for destinations with no load balancer. You have applied the complete DNS with change-resource-record-sets and UPSERT, you have checked it with dig against the authoritative servers, and you know that Route 53 costs MercadoFresco 1.60 USD a month: the cheapest thing in the whole module.

With this, module 3 is closed. MercadoFresco's architecture today is: client → Route 53 → CloudFront → ALB → Auto Scaling group across two Availability Zones → RDS Multi-AZ in private subnets with no way out to the internet, with the catalogue in S3 closed off by Origin Access Control and every log in mercadofresco-registros-web. Problem 1, the Friday outages, is solved, with 2,400 orders/hour of capacity against 900 of demand, even when a whole zone is lost. Problem 3, growing to more cities, already has the infrastructure ready, and problem 4, risky deployments, has the canary mechanism in place waiting for the automation of module 8.

But this whole architecture rests on assumptions we have not yet examined. There is an administrator user with broad permissions and roles created on the fly with no systematic least-privilege policy. mfadmin's password is still where it should not be. The KMS key alias/mercadofresco-datos exists but nobody has decided who may use it or how often it rotates. And the DENY rule numbered 50 with which we blocked an abusive IP is completely useless against a distributed attack from ten thousand addresses. In module 4, "Security and identity", starting with lesson 04-01 "AWS Identity and Access Management (IAM)", we will build the missing layer: identities, policies and minimum permissions; then managed encryption with KMS, the safekeeping of secrets with Secrets Manager and Parameter Store, and edge protection with Shield and WAF, applied right in front of the CloudFront distribution you have just built.

© Copyright 2026. All rights reserved