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
- A practical refresher on DNS: zones, delegation, resolution and TTL
- Registering and transferring domains in Route 53
- Public and private hosted zones
- Record types
- Alias records: the piece only AWS has
- Validating the ACM certificate by DNS
- Route 53 health checks
- Routing policies, one by one
- Configuring MercadoFresco's DNS from the console and from the CLI
- Route 53 Resolver and the forwarding rules to the office
- Route 53 costs and clean-up
- 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 ← subdomainsA 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 tableTwo 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 | 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:[email protected]"
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:
- It works at the apex.
mercadofresco.examplecan point at CloudFront. With a CNAME it would be impossible. - 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.
- 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.example → d111111abcdef8.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 tableIt 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
[email protected]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=operacionesImportant details:
- It uses the same
/saludpath 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: 30andFailureThreshold: 3, a failure is spotted in about 90 seconds. There is a 10-second option (fast interval) that costs more. MeasureLatency: trueadds 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:
SetIdentifieris compulsory in every policy that is not simple: it is what tells apart two records with the same name and type.HostedZoneId: Z32O12XQLNTSW2is not MercadoFresco's zone: it is the zone identifier of the ELB service ineu-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 alwaysZ2FDTNDATAQYW2.EvaluateTargetHealth: truemeans 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
- Route 53 → Hosted zones →
mercadofresco.example. - Create record. Blank name for the apex.
- Switch on Alias, choose "Alias to CloudFront distribution" and select the distribution.
- Routing policy: Simple. Save.
- Repeat with
www, this time "Alias to another record in this zone" → the apex. - 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:[email protected]\"" }
]
}
}
]
}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 tableNotes about this batch:
UPSERTcreates the record if it does not exist and updates it if it does. It is idempotent, which makes it safe to re-run.CREATEwould fail on the second run andDELETErequires 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.
Z2FDTNDATAQYW2is the zone identifier of CloudFront, identical the world over;Z32O12XQLNTSW2is the one for ELB ineu-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 -datesRoute 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-datoskey 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-defectorecord is essential: without it, a customer in Andorra or in France gets no answer. - The aliases to
es.andpt.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-canarioto 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.netIf 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.ukA 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's —Z2FDTNDATAQYW2 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.
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
