<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sam</title>
    <description>The latest articles on DEV Community by Sam (@clovis777).</description>
    <link>https://dev.to/clovis777</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071084%2F3a809c61-12a1-4402-9ecf-81fa952be8e9.png</url>
      <title>DEV Community: Sam</title>
      <link>https://dev.to/clovis777</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/clovis777"/>
    <language>en</language>
    <item>
      <title>How to Build an Anycast CDN with BIRD, NGINX, and GeoDNS</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Fri, 25 Sep 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/adiosdev/how-to-build-an-anycast-cdn-with-bird-nginx-and-geodns-28cp</link>
      <guid>https://dev.to/adiosdev/how-to-build-an-anycast-cdn-with-bird-nginx-and-geodns-28cp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu73waa9fppz5v03bo0x4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu73waa9fppz5v03bo0x4.png" alt=" " width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To build an Anycast CDN, announce the same IP prefix from multiple locations and run an HTTPS cache at each one.&lt;/p&gt;

&lt;p&gt;In this guide, we'll use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BIRD&lt;/strong&gt; for BGP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGINX&lt;/strong&gt; for HTTPS and caching&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv4 /24 routing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RPKI and ROAs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Health-based route withdrawal&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GeoDNS&lt;/strong&gt; for regional Anycast pools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll start with two servers sharing one global CDN address, then expand the design into regional pools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choose global Anycast or regional Anycast with GeoDNS
&lt;/h2&gt;

&lt;p&gt;Global Anycast uses one service IP across all your edges.&lt;/p&gt;

&lt;p&gt;DNS returns that IP, and BGP selects the receiving edge according to network policy and available paths.&lt;/p&gt;

&lt;p&gt;A cache hit is served there. A cache miss goes to your origin and can populate that edge's cache.&lt;/p&gt;

&lt;p&gt;Regional Anycast with GeoDNS adds another selection step.&lt;/p&gt;

&lt;p&gt;Each regional pool has a different service IP shared by the edges in that pool. GeoDNS returns a regional IP, and BGP then selects an edge advertising it.&lt;/p&gt;

&lt;p&gt;This lets you choose a regional pool before internet routing chooses the server.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Routing choice&lt;/th&gt;
&lt;th&gt;Global Anycast&lt;/th&gt;
&lt;th&gt;Regional Anycast + GeoDNS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DNS answer&lt;/td&gt;
&lt;td&gt;The same CDN IP for everyone.&lt;/td&gt;
&lt;td&gt;A different CDN IP for each selected regional pool.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BGP announcements&lt;/td&gt;
&lt;td&gt;All edges announce the same prefix.&lt;/td&gt;
&lt;td&gt;Edges in each pool announce that pool's prefix.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One edge fails&lt;/td&gt;
&lt;td&gt;Withdraw its route; other announcing edges remain available.&lt;/td&gt;
&lt;td&gt;Withdraw its route; another edge in the same pool can receive new connections.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An entire region fails&lt;/td&gt;
&lt;td&gt;Another advertising region may receive the traffic after convergence.&lt;/td&gt;
&lt;td&gt;DNS must select a healthy fallback pool. Cached DNS answers can still point to the failed pool.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Choose it when&lt;/td&gt;
&lt;td&gt;You want one stable IP and one global pool.&lt;/td&gt;
&lt;td&gt;You want explicit regional pool selection, separate capacity, or different regional origins.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The request flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GLOBAL ANYCAST

cdn.example.com -&amp;gt; one IP -&amp;gt; BGP -&amp;gt; Paris or New York cache
                                              |
                                          cache miss
                                              v
                                            origin


REGIONAL ANYCAST + GEODNS

cdn.example.com -&amp;gt; GeoDNS -&amp;gt; Europe IP -&amp;gt; BGP -&amp;gt; Paris / Frankfurt
                         -&amp;gt; US IP     -&amp;gt; BGP -&amp;gt; New York / Chicago
                         -&amp;gt; default   -&amp;gt; chosen fallback pool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A regional pool is a deployment choice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Its routes can still be reachable globally. Restricting where BGP announcements propagate is a separate upstream policy.&lt;/p&gt;

&lt;p&gt;Neither GeoDNS nor BGP guarantees the nearest server or enforces data residency by itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What you need for the first build
&lt;/h3&gt;

&lt;p&gt;Start with the global setup.&lt;/p&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two fresh Debian 12 servers with systemd&lt;/li&gt;
&lt;li&gt;BIRD 2&lt;/li&gt;
&lt;li&gt;NGINX&lt;/li&gt;
&lt;li&gt;Customer BGP support from both providers&lt;/li&gt;
&lt;li&gt;One authorized IPv4 &lt;code&gt;/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An agreed origin ASN&lt;/li&gt;
&lt;li&gt;An HTTPS origin on a separate IP&lt;/li&gt;
&lt;li&gt;A domain whose DNS you control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The examples below assume direct BGP peers.&lt;/p&gt;

&lt;p&gt;Your upstream must provide the real peering values.&lt;/p&gt;

&lt;p&gt;Budget for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Address space&lt;/li&gt;
&lt;li&gt;ASN arrangements&lt;/li&gt;
&lt;li&gt;Edge servers&lt;/li&gt;
&lt;li&gt;Outbound traffic&lt;/li&gt;
&lt;li&gt;Origin traffic&lt;/li&gt;
&lt;li&gt;DNS health checks&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get current quotes and confirm BGP eligibility before ordering infrastructure.&lt;/p&gt;

&lt;p&gt;All IP addresses and ASNs shown in this article are documentation examples and must be replaced with your own.&lt;/p&gt;

&lt;p&gt;Deployment and failure timings must be measured on your own network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/info/rfc4786/" rel="noopener noreferrer"&gt;RFC 4786: Operation of Anycast Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-geo.html" rel="noopener noreferrer"&gt;Amazon Route 53: Geolocation Routing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Get a /24 and permission to announce it
&lt;/h2&gt;

&lt;p&gt;For your own IPv4 announcements on the public internet, &lt;code&gt;/24&lt;/code&gt; is the practical minimum block size.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;/24&lt;/code&gt; contains &lt;strong&gt;256 addresses&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Higher prefix lengths represent smaller networks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/25&lt;/code&gt; = 128 addresses&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/26&lt;/code&gt; = 64 addresses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those smaller blocks are commonly filtered on the public internet.&lt;/p&gt;

&lt;p&gt;Before buying or leasing address space, confirm that both hosting locations support customer BGP and will accept your prefix.&lt;/p&gt;

&lt;p&gt;There are several ways to obtain a &lt;code&gt;/24&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;th&gt;Check before committing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lease a block with your own ASN&lt;/td&gt;
&lt;td&gt;Lease a &lt;code&gt;/24&lt;/code&gt; and have the holder authorize your ASN to originate it. Arrange the ASN separately if needed.&lt;/td&gt;
&lt;td&gt;Confirm ROA and IRR updates, permission to announce from both PoPs, and renewal and exit terms.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request a provider-assigned block&lt;/td&gt;
&lt;td&gt;Ask a provider such as Vultr for address space it can route for your BGP setup.&lt;/td&gt;
&lt;td&gt;Confirm whether a full &lt;code&gt;/24&lt;/code&gt; is available, which ASN originates it, and whether you can announce it elsewhere.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apply directly to a registry&lt;/td&gt;
&lt;td&gt;Request an allocation from your regional internet registry. RIPE NCC maintains a &lt;code&gt;/24&lt;/code&gt; waiting list for eligible LIRs that have never received an IPv4 allocation.&lt;/td&gt;
&lt;td&gt;Membership or application fees do not guarantee allocation or delivery time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buy an existing block&lt;/td&gt;
&lt;td&gt;Purchase a block from an existing holder directly or through a broker and complete the registry transfer process.&lt;/td&gt;
&lt;td&gt;Verify seller authority, transfer eligibility, fees, routing history, abuse history, and upstream acceptance.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Provider-managed Anycast is another entry point.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A provider can expose individual Anycast service IPs from inside its own aggregate.&lt;/p&gt;

&lt;p&gt;AWS Global Accelerator, for example, provides managed Anycast IPs and supports eligible BYOIP ranges while AWS handles the announcements.&lt;/p&gt;

&lt;p&gt;That is different from operating customer BGP yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Make the block routable
&lt;/h3&gt;

&lt;p&gt;Agree the origin ASN with your upstreams.&lt;/p&gt;

&lt;p&gt;If you need your own ASN, apply through your regional registry or through an eligible sponsoring provider.&lt;/p&gt;

&lt;p&gt;The ASN identifies the network originating the prefix. It is separate from the IP allocation, lease, or transfer.&lt;/p&gt;

&lt;p&gt;The resource holder should authorize the ASN using &lt;strong&gt;RPKI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Create a ROA for the &lt;code&gt;/24&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maximum length: /24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the corresponding IRR route object if required by your upstream, and provide a Letter of Authorization if requested.&lt;/p&gt;

&lt;p&gt;Remember:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A ROA authorizes a route. It does not announce the route.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Send something like this to both hosting providers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Our prefix:       [your /24]
Our origin ASN:   [your ASN]
Locations:        Paris and New York, announced simultaneously

Please confirm:
- Prefix accepted; required ROA, IRR record, and authorization
- Peer IP, peer ASN, local source IP, and any BGP password
- Direct or multihop peering
- Replies sourced from our /24 are allowed
- Global export policy and available regional communities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not configure production BGP until both providers confirm that they will accept the prefix and provide their peer configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.vultr.com/support/products/network/does-vultr-provide-bgp-sessions" rel="noopener noreferrer"&gt;Vultr: BGP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/global-accelerator/latest/dg/using-byoip.html" rel="noopener noreferrer"&gt;AWS Global Accelerator BYOIP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ripe.net/manage-ips-and-asns/ipv4/how-waiting-list-works/" rel="noopener noreferrer"&gt;RIPE NCC IPv4 Waiting List&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ripe.net/manage-ips-and-asns/resource-transfers-and-mergers/transfer-of-ip-addresses-and-as-numbers/transfers-in-the-ripe-ncc-service-region/" rel="noopener noreferrer"&gt;RIPE NCC Resource Transfers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.arin.net/resources/guide/ipv4/" rel="noopener noreferrer"&gt;ARIN IPv4 Addressing Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ripe.net/manage-ips-and-asns/resource-management/rpki/resource-certification-roa-management/" rel="noopener noreferrer"&gt;RIPE NCC: Creating ROAs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prepare the first edge server
&lt;/h2&gt;

&lt;p&gt;We'll put the first edge in &lt;strong&gt;Paris&lt;/strong&gt; and the second in &lt;strong&gt;New York&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each location is a Point of Presence, or &lt;strong&gt;PoP&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both locations receive the same CDN IP.&lt;/p&gt;

&lt;p&gt;Each server also keeps its own normal unicast address for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH&lt;/li&gt;
&lt;li&gt;Management&lt;/li&gt;
&lt;li&gt;Origin requests&lt;/li&gt;
&lt;li&gt;Health monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Replace all addresses, ASNs, and &lt;code&gt;example.com&lt;/code&gt; names below.&lt;/p&gt;

&lt;p&gt;These are documentation values, not addresses you can announce.&lt;/p&gt;

&lt;p&gt;Our example address plan is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdn.example.com → 203.0.113.80
                       |
                  BGP chooses a PoP
                   /             \
              Paris cache     New York cache
                   \             /
                    cache misses
                        |
               origin.example.com
                  192.0.2.10:443

Prefix:               203.0.113.0/24
CDN IP on BOTH PoPs:  203.0.113.80/32
Origin ASN:           64496
Upstream ASN:         64497
Paris node / peer:    198.51.100.10 / 198.51.100.1
New York / peer:      198.51.100.20 / 198.51.100.17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install packages and bind the CDN address
&lt;/h3&gt;

&lt;p&gt;Run this on the Paris server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; bird2 nginx curl ca-certificates iproute2 python3
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; www-data &lt;span class="nt"&gt;-g&lt;/span&gt; www-data /var/cache/nginx/cdn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CDN service IP will be bound as a &lt;code&gt;/32&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A covering blackhole route will discard packets sent to unused addresses in the &lt;code&gt;/24&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Allow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTPS traffic to the CDN IP&lt;/li&gt;
&lt;li&gt;BGP TCP port &lt;code&gt;179&lt;/code&gt; from your provider's peer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep management traffic on the server's unicast IP.&lt;/p&gt;

&lt;p&gt;This reverse-proxy configuration does not require Linux packet forwarding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the address across reboots
&lt;/h3&gt;

&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/systemd/system/cdn-address.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CDN service address&lt;/span&gt;
&lt;span class="py"&gt;Before&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;bird.service nginx.service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/usr/sbin/ip link add anycast0 type dummy&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/sbin/ip address replace 203.0.113.80/32 dev anycast0&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/sbin/ip link set anycast0 up&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/sbin/ip route replace blackhole 203.0.113.0/24&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Order services after the address
&lt;/h3&gt;

&lt;p&gt;Make BIRD and NGINX depend on this unit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;service &lt;span class="k"&gt;in &lt;/span&gt;bird nginx&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/systemd/system/&lt;span class="nv"&gt;$service&lt;/span&gt;.service.d

  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/&lt;span class="nv"&gt;$service&lt;/span&gt;.service.d/cdn-address.conf &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Requires=cdn-address.service
After=cdn-address.service
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; cdn-address

ip address show dev anycast0
ip route get 192.0.2.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route to the origin should use the normal unicast network, not &lt;code&gt;anycast0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc5737.html" rel="noopener noreferrer"&gt;RFC 5737: IPv4 Address Blocks Reserved for Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc5398.html" rel="noopener noreferrer"&gt;RFC 5398: AS Numbers Reserved for Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Configure BIRD to announce the /24
&lt;/h2&gt;

&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/bird/bird.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;on the Paris server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;router id 198.51.100.10;

protocol device {
  scan time 10;
}

protocol static cdn_prefix {
  disabled yes;

  ipv4;

  route 203.0.113.0/24 blackhole;
}

filter export_cdn {
  if net = 203.0.113.0/24 then accept;
  reject;
}

protocol bgp transit {
  local as 64496;
  source address 198.51.100.10;
  neighbor 198.51.100.1 as 64497;

  graceful restart off;

  ipv4 {
    import none;
    export filter export_cdn;
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the export filter.&lt;/p&gt;

&lt;p&gt;Only the CDN prefix can be exported.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;filter export_cdn {
  if net = 203.0.113.0/24 then accept;
  reject;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cdn_prefix&lt;/code&gt; static protocol starts &lt;strong&gt;disabled&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means the server initially advertises nothing.&lt;/p&gt;

&lt;p&gt;We will only advertise the route after HTTPS has been tested locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the BGP session
&lt;/h3&gt;

&lt;p&gt;Add any provider-required authentication or multihop options.&lt;/p&gt;

&lt;p&gt;Then validate the configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;bird &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; /etc/bird/bird.conf
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; bird
&lt;span class="nb"&gt;sudo &lt;/span&gt;birdc configure

&lt;span class="nb"&gt;sudo &lt;/span&gt;birdc show protocols all transit
&lt;span class="nb"&gt;sudo &lt;/span&gt;birdc show route &lt;span class="nb"&gt;export &lt;/span&gt;transit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The BGP session should reach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Established
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but no CDN route should be exported yet.&lt;/p&gt;

&lt;p&gt;If the BGP session does not establish, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Peer address&lt;/li&gt;
&lt;li&gt;Peer ASN&lt;/li&gt;
&lt;li&gt;Local ASN&lt;/li&gt;
&lt;li&gt;Firewall rules&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Multihop requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reference&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bird.nic.cz/doc/bird-2.18.html" rel="noopener noreferrer"&gt;BIRD 2 Configuration Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Issue HTTPS certificates
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;DNS-01&lt;/strong&gt; certificate validation so you can issue the CDN certificate before advertising the CDN IP.&lt;/p&gt;

&lt;p&gt;Here is a Certbot example for a zone hosted on Cloudflare DNS.&lt;/p&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; certbot python3-certbot-dns-cloudflare
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 700 /root/.secrets
&lt;span class="nb"&gt;sudo touch&lt;/span&gt; /root/.secrets/cloudflare.ini
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /root/.secrets/cloudflare.ini
sudoedit /root/.secrets/cloudflare.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;dns_cloudflare_api_token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;YOUR_TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restrict the Cloudflare token to the required DNS zone with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zone:DNS:Edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then request the certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot certonly &lt;span class="nt"&gt;--dns-cloudflare&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-cloudflare-credentials&lt;/span&gt; /root/.secrets/cloudflare.ini &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-cloudflare-propagation-seconds&lt;/span&gt; 60 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cert-name&lt;/span&gt; cdn.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; cdn.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--email&lt;/span&gt; ops@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--agree-tos&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--non-interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Issue the certificate independently on each edge.&lt;/p&gt;

&lt;p&gt;The servers do not have to share the same private key.&lt;/p&gt;

&lt;p&gt;As the edge fleet grows, however, you may want to centralize issuance and securely distribute certificates to reduce DNS credential exposure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reload NGINX after renewal
&lt;/h3&gt;

&lt;p&gt;After the NGINX configuration later in this guide passes &lt;code&gt;nginx -t&lt;/code&gt;, install a Certbot deploy hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/letsencrypt/renewal-hooks/deploy

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/letsencrypt/renewal-hooks/deploy/reload-nginx &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/sh
set -eu

/usr/sbin/nginx -t
/usr/bin/systemctl reload nginx
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx

&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; certbot.timer
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--run-deploy-hooks&lt;/span&gt;

systemctl list-timers certbot.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://certbot-dns-cloudflare.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;Certbot Cloudflare DNS Plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eff-certbot.readthedocs.io/en/stable/using.html#renewing-certificates" rel="noopener noreferrer"&gt;Certbot Renewal and Deploy Hooks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Configure HTTPS and the edge cache
&lt;/h2&gt;

&lt;p&gt;We'll use an HTTPS origin at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;192.0.2.10:443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and an origin hostname of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;origin.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The origin should serve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/assets/logo.v1.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with headers similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=60, s-maxage=300
ETag: "cdn-demo-v1"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should also serve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/cdn-probe.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: no-store
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/nginx/conf.d/cdn.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_cache_path&lt;/span&gt; &lt;span class="n"&gt;/var/cache/nginx/cdn&lt;/span&gt;
  &lt;span class="s"&gt;levels=1:2&lt;/span&gt;
  &lt;span class="s"&gt;keys_zone=cdn:50m&lt;/span&gt;
  &lt;span class="s"&gt;max_size=10g&lt;/span&gt;
  &lt;span class="s"&gt;inactive=60m&lt;/span&gt;
  &lt;span class="s"&gt;use_temp_path=off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$http_authorization$http_cookie&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$skip_private&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;default&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;""&lt;/span&gt;      &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;cdn_origin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;192.0.2.10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="nf"&gt;203.0.113.80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;cdn.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt;     &lt;span class="n"&gt;/etc/letsencrypt/live/cdn.example.com/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/cdn.example.com/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt; &lt;span class="s"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;cdn.example.com)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;421&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="s"&gt;origin.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;proxy_ssl_server_name&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_ssl_name&lt;/span&gt; &lt;span class="s"&gt;origin.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_ssl_verify&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_ssl_trusted_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/ssl/certs/ca-certificates.crt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_ssl_verify_depth&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;proxy_hide_header&lt;/span&gt; &lt;span class="s"&gt;X-Edge-Id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_hide_header&lt;/span&gt; &lt;span class="s"&gt;X-Cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Edge-Id&lt;/span&gt; &lt;span class="s"&gt;"paris-1"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Cache&lt;/span&gt; &lt;span class="nv"&gt;$upstream_cache_status&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/__edge/health&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;default_type&lt;/span&gt; &lt;span class="nc"&gt;text/plain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;"edge-ok&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;n"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="s"&gt;^~&lt;/span&gt; &lt;span class="n"&gt;/assets/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;https://cdn_origin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;proxy_cache&lt;/span&gt; &lt;span class="s"&gt;cdn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_cache_key&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;$request_uri&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_cache_methods&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt;
      &lt;span class="nv"&gt;$skip_private&lt;/span&gt;
      &lt;span class="nv"&gt;$http_cache_control&lt;/span&gt;
      &lt;span class="nv"&gt;$http_pragma&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;proxy_no_cache&lt;/span&gt;
      &lt;span class="nv"&gt;$skip_private&lt;/span&gt;
      &lt;span class="nv"&gt;$http_cache_control&lt;/span&gt;
      &lt;span class="nv"&gt;$http_pragma&lt;/span&gt;
      &lt;span class="nv"&gt;$upstream_http_set_cookie&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;proxy_cache_lock&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_cache_revalidate&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;https://cdn_origin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration only caches &lt;code&gt;/assets/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everything else goes directly to the origin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Give the origin predictable test responses
&lt;/h3&gt;

&lt;p&gt;If the origin also uses NGINX, add these locations inside its HTTPS server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/assets/logo.v1.svg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;default_type&lt;/span&gt; &lt;span class="nc"&gt;image/svg&lt;/span&gt;&lt;span class="s"&gt;+xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;max-age=60,&lt;/span&gt; &lt;span class="s"&gt;s-maxage=300"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;ETag&lt;/span&gt; &lt;span class="s"&gt;'"cdn-demo-v1"'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;'&amp;lt;svg&lt;/span&gt; &lt;span class="s"&gt;xmlns="http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="s"&gt;width="80"&lt;/span&gt; &lt;span class="s"&gt;height="80"&amp;gt;&amp;lt;rect&lt;/span&gt; &lt;span class="s"&gt;width="80"&lt;/span&gt; &lt;span class="s"&gt;height="80"&lt;/span&gt; &lt;span class="s"&gt;fill="blue"/&amp;gt;&amp;lt;/svg&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/cdn-probe.txt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;default_type&lt;/span&gt; &lt;span class="nc"&gt;text/plain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"no-store"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;"origin-ok&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;n"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What this cache will store
&lt;/h3&gt;

&lt;p&gt;The test asset remains fresh in the shared cache for &lt;strong&gt;300 seconds&lt;/strong&gt; because of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s-maxage=300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The NGINX setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inactive=60m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;controls removal of unused objects, not HTTP freshness.&lt;/p&gt;

&lt;p&gt;Our cache key includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheme&lt;/li&gt;
&lt;li&gt;Hostname&lt;/li&gt;
&lt;li&gt;Full request URI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Requests with either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cookies&lt;/li&gt;
&lt;li&gt;Authorization headers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;bypass the cache.&lt;/p&gt;

&lt;p&gt;NGINX also respects relevant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-store&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set-Cookie&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vary&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;response behavior.&lt;/p&gt;

&lt;p&gt;Each PoP has its &lt;strong&gt;own disk cache&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Use versioned asset URLs when releasing new versions.&lt;/p&gt;

&lt;p&gt;This basic configuration does not provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A global purge API&lt;/li&gt;
&lt;li&gt;Forced stale serving during origin failure&lt;/li&gt;
&lt;li&gt;A shared global cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/" rel="noopener noreferrer"&gt;NGINX Content Caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html" rel="noopener noreferrer"&gt;NGINX Proxy Module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://letsencrypt.org/docs/challenge-types/" rel="noopener noreferrer"&gt;Let's Encrypt DNS-01 Challenge&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Enable the first PoP
&lt;/h2&gt;

&lt;p&gt;Before announcing anything to the internet, test HTTPS locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the edge health endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resolve&lt;/span&gt; cdn.example.com:443:203.0.113.80 &lt;span class="se"&gt;\&lt;/span&gt;
  https://cdn.example.com/__edge/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;edge-ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then test origin connectivity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resolve&lt;/span&gt; cdn.example.com:443:203.0.113.80 &lt;span class="se"&gt;\&lt;/span&gt;
  https://cdn.example.com/cdn-probe.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;origin-ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both checks must succeed with a valid certificate before announcing the route.&lt;/p&gt;

&lt;h3&gt;
  
  
  Announce the route and set DNS
&lt;/h3&gt;

&lt;p&gt;Enable the BIRD static protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;birdc &lt;span class="nb"&gt;enable &lt;/span&gt;cdn_prefix
&lt;span class="nb"&gt;sudo &lt;/span&gt;birdc show route &lt;span class="nb"&gt;export &lt;/span&gt;transit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask your upstream to confirm the &lt;code&gt;/24&lt;/code&gt; is accepted and propagated.&lt;/p&gt;

&lt;p&gt;Then create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdn.example.com. 300 IN A 203.0.113.80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your authoritative DNS provider also offers a CDN proxy, keep this record in &lt;strong&gt;DNS-only&lt;/strong&gt; mode.&lt;/p&gt;

&lt;p&gt;Test the hostname from an external network before proceeding.&lt;/p&gt;




&lt;h2&gt;
  
  
  Add the second PoP
&lt;/h2&gt;

&lt;p&gt;Repeat the setup in New York.&lt;/p&gt;

&lt;p&gt;Keep these values the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CDN service IP&lt;/li&gt;
&lt;li&gt;Origin ASN&lt;/li&gt;
&lt;li&gt;CDN hostname&lt;/li&gt;
&lt;li&gt;Origin&lt;/li&gt;
&lt;li&gt;Cache configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Change the server-specific BGP and edge values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Paris&lt;/th&gt;
&lt;th&gt;New York&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BIRD router ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;198.51.100.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;198.51.100.20&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BGP source address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;198.51.100.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;198.51.100.20&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BGP neighbor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;198.51.100.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;198.51.100.17&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NGINX &lt;code&gt;X-Edge-Id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paris-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new-york-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Install a valid certificate and verify local HTTPS before enabling &lt;code&gt;cdn_prefix&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confirm both locations serve requests
&lt;/h3&gt;

&lt;p&gt;Leave DNS unchanged.&lt;/p&gt;

&lt;p&gt;Probe the public hostname from multiple networks and inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Edge-Id
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on internet routing policy, clients should reach different PoPs.&lt;/p&gt;

&lt;p&gt;To test a specific edge, run &lt;code&gt;curl --resolve&lt;/code&gt; directly from that server.&lt;/p&gt;

&lt;p&gt;Using the Anycast IP from your laptop does &lt;strong&gt;not&lt;/strong&gt; let you force the request to Paris or New York.&lt;/p&gt;




&lt;h2&gt;
  
  
  Withdraw unhealthy edges automatically
&lt;/h2&gt;

&lt;p&gt;A live BGP session does not mean HTTPS is healthy.&lt;/p&gt;

&lt;p&gt;An edge might still be advertising while:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NGINX is dead&lt;/li&gt;
&lt;li&gt;TLS is broken&lt;/li&gt;
&lt;li&gt;The service address disappeared&lt;/li&gt;
&lt;li&gt;The application is stuck&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The edge therefore needs health-aware route withdrawal.&lt;/p&gt;

&lt;p&gt;The example controller used by this guide:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Probes the local CDN IP with the correct TLS hostname&lt;/li&gt;
&lt;li&gt;Withdraws &lt;code&gt;cdn_prefix&lt;/code&gt; after three consecutive failures&lt;/li&gt;
&lt;li&gt;Requires 30 seconds of continuous health before recovery&lt;/li&gt;
&lt;li&gt;Uses a 60-second withdrawal hold-down&lt;/li&gt;
&lt;li&gt;Reads BIRD state back after every action&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Download the example files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--remote-name&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://adios.dev/examples/anycast-cdn/edge-health.py

curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--remote-name&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://adios.dev/examples/anycast-cdn/edge-health.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review both files before installing them.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 755 edge-health.py /usr/local/sbin/edge-health.py
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 644 edge-health.service /etc/systemd/system/edge-health.service

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/default/edge-health &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
CDN_HOST=cdn.example.com
CDN_IP=203.0.113.80
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;sudoedit /etc/default/edge-health

&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; edge-health

&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; edge-health &lt;span class="nt"&gt;-n&lt;/span&gt; 30 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The systemd unit attempts route withdrawal when the controller exits and restarts it if it crashes.&lt;/p&gt;

&lt;p&gt;It also uses a watchdog to detect a stalled controller loop.&lt;/p&gt;

&lt;p&gt;This example checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local TLS&lt;/li&gt;
&lt;li&gt;NGINX responsiveness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should separately monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache disk health&lt;/li&gt;
&lt;li&gt;Origin reachability&lt;/li&gt;
&lt;li&gt;Public routing&lt;/li&gt;
&lt;li&gt;Upstream connectivity&lt;/li&gt;
&lt;li&gt;Route propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A shared origin outage should not necessarily withdraw every edge that can still serve cached content.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Test the controller on your own network.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The policy and command handling may have automated tests, but that does not mean your BGP environment is automatically production-qualified.&lt;/p&gt;

&lt;p&gt;Test process failure, hung controllers, BIRD socket errors, upstream graceful restart behavior, and reboots.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://adios.dev/examples/anycast-cdn/edge-health.py" rel="noopener noreferrer"&gt;Health controller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adios.dev/examples/anycast-cdn/edge-health.service" rel="noopener noreferrer"&gt;systemd service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adios.dev/examples/anycast-cdn/README.txt" rel="noopener noreferrer"&gt;Example requirements and maintenance instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/systemd/systemd/blob/main/man/systemd.service.xml" rel="noopener noreferrer"&gt;systemd.service Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Test cache hits and failover
&lt;/h2&gt;

&lt;p&gt;On each PoP, request the same test asset twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;attempt &lt;span class="k"&gt;in &lt;/span&gt;1 2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--resolve&lt;/span&gt; cdn.example.com:443:203.0.113.80 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    https://cdn.example.com/assets/logo.v1.svg
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fresh cache key should show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MISS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;followed by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the origin access log.&lt;/p&gt;

&lt;p&gt;The second request should not reach the origin.&lt;/p&gt;

&lt;p&gt;An initial &lt;code&gt;HIT&lt;/code&gt; simply means the object was already cached.&lt;/p&gt;

&lt;p&gt;Now test a private request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resolve&lt;/span&gt; cdn.example.com:443:203.0.113.80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Cookie: session=cdn-test'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  https://cdn.example.com/assets/logo.v1.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Cache: BYPASS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Withdraw one PoP
&lt;/h3&gt;

&lt;p&gt;From an external network currently reaching Paris, repeatedly create new HTTPS connections.&lt;/p&gt;

&lt;p&gt;On Paris, stop the automatic controller first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop edge-health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then withdraw the route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;birdc disable cdn_prefix
&lt;span class="nb"&gt;sudo &lt;/span&gt;birdc show route &lt;span class="nb"&gt;export &lt;/span&gt;transit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;External requests should eventually begin reaching New York.&lt;/p&gt;

&lt;p&gt;Record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failed requests&lt;/li&gt;
&lt;li&gt;Total failover time&lt;/li&gt;
&lt;li&gt;Edge ID&lt;/li&gt;
&lt;li&gt;Connection behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Existing TCP connections may need to reconnect.&lt;/p&gt;

&lt;p&gt;Once local HTTPS is healthy again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start edge-health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller should require sustained health before advertising the route again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test a service failure and a reboot
&lt;/h3&gt;

&lt;p&gt;With the route advertised and controller running, stop NGINX on Paris:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the failure threshold.&lt;/p&gt;

&lt;p&gt;Inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; edge-health &lt;span class="nt"&gt;-n&lt;/span&gt; 30 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;birdc show route &lt;span class="nb"&gt;export &lt;/span&gt;transit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The health controller should withdraw the prefix.&lt;/p&gt;

&lt;p&gt;External requests should begin reaching New York.&lt;/p&gt;

&lt;p&gt;Restart NGINX:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the recovery delay prevents immediate re-advertisement.&lt;/p&gt;

&lt;p&gt;Repeat the same experiment on the other edge.&lt;/p&gt;

&lt;p&gt;Then reboot one edge while the other continues serving traffic.&lt;/p&gt;

&lt;p&gt;Verify the following recover in the expected order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dummy interface&lt;/li&gt;
&lt;li&gt;Anycast service address&lt;/li&gt;
&lt;li&gt;BIRD&lt;/li&gt;
&lt;li&gt;BGP session&lt;/li&gt;
&lt;li&gt;NGINX&lt;/li&gt;
&lt;li&gt;Certificate availability&lt;/li&gt;
&lt;li&gt;Health controller&lt;/li&gt;
&lt;li&gt;Route advertisement&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Measure both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warm-cache behavior&lt;/li&gt;
&lt;li&gt;Cold-cache behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never assume a fixed BGP convergence time.&lt;/p&gt;

&lt;p&gt;Measure it on your own providers and client networks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/info/rfc4786/" rel="noopener noreferrer"&gt;RFC 4786: Anycast Service Health and Route Stability&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Add regional Anycast pools with GeoDNS
&lt;/h2&gt;

&lt;p&gt;Once the global Anycast setup works, you can create regional pools.&lt;/p&gt;

&lt;p&gt;Build at least two edges in each pool.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;h3&gt;
  
  
  Europe
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EU_CDN_IP
   |
   +-- Paris
   |
   +-- Frankfurt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  North America
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;US_CDN_IP
   |
   +-- New York
   |
   +-- Chicago
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every edge in a regional pool advertises the same pool prefix.&lt;/p&gt;

&lt;p&gt;Each independently routed IPv4 pool needs an independently routable prefix.&lt;/p&gt;

&lt;p&gt;In practice, that normally means another &lt;code&gt;/24&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two different IP addresses from one shared &lt;code&gt;/24&lt;/code&gt; do &lt;strong&gt;not&lt;/strong&gt; give you two independently controllable BGP routes.&lt;/p&gt;

&lt;p&gt;For each pool, change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dummy interface address&lt;/li&gt;
&lt;li&gt;Covering route&lt;/li&gt;
&lt;li&gt;BIRD static prefix&lt;/li&gt;
&lt;li&gt;BIRD export filter&lt;/li&gt;
&lt;li&gt;NGINX listen address&lt;/li&gt;
&lt;li&gt;Health controller &lt;code&gt;CDN_IP&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep management and origin IP addresses outside those service prefixes.&lt;/p&gt;

&lt;p&gt;All pools should serve the same hostname with valid certificates and matching cache behavior.&lt;/p&gt;

&lt;p&gt;You can use separate origins per region if required.&lt;/p&gt;

&lt;p&gt;An example GeoDNS policy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query location&lt;/th&gt;
&lt;th&gt;DNS answer for &lt;code&gt;cdn.example.com&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Edges announcing that IP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Europe&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EU_CDN_IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paris and Frankfurt using &lt;code&gt;EU_PREFIX/24&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;North America&lt;/td&gt;
&lt;td&gt;&lt;code&gt;US_CDN_IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New York and Chicago using &lt;code&gt;US_PREFIX/24&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default / fallback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GLOBAL_CDN_IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Global fallback pool using &lt;code&gt;GLOBAL_PREFIX/24&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Create the GeoDNS records
&lt;/h3&gt;

&lt;p&gt;With a GeoDNS platform such as Route 53, create multiple geolocation A records with the same hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdn.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but different record identifiers.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Europe        -&amp;gt; EU_CDN_IP
North America -&amp;gt; US_CDN_IP
Default       -&amp;gt; GLOBAL_CDN_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with a TTL around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;60 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and measure actual resolver behavior.&lt;/p&gt;

&lt;p&gt;If you move authoritative DNS from Cloudflare to Route 53, remember that the Cloudflare Certbot DNS plugin no longer controls the authoritative records.&lt;/p&gt;

&lt;p&gt;Either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the Route 53 DNS plugin, or&lt;/li&gt;
&lt;li&gt;Deliberately delegate the ACME challenge zone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Associate each GeoDNS record with the health of its regional pool.&lt;/p&gt;

&lt;p&gt;With Route 53, an unhealthy geographic record can fall back to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A broader geographic record&lt;/li&gt;
&lt;li&gt;The default record&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep the fallback pool healthy and large enough to absorb additional traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check pool health, then test both failure paths
&lt;/h3&gt;

&lt;p&gt;You need two kinds of checks.&lt;/p&gt;

&lt;p&gt;First, monitor individual edges through their &lt;strong&gt;unicast addresses&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Second, probe the shared regional Anycast service IP.&lt;/p&gt;

&lt;p&gt;A probe against the Anycast IP may continue succeeding after one server fails because BGP sends it to another healthy edge.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remove a regional pool from GeoDNS when the pool itself cannot serve traffic, not whenever a single edge fails.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Test two failure scenarios.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scenario 1: one edge fails
&lt;/h4&gt;

&lt;p&gt;Withdraw Paris.&lt;/p&gt;

&lt;p&gt;Frankfurt should continue serving the European service IP.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scenario 2: an entire regional pool fails
&lt;/h4&gt;

&lt;p&gt;Make the European pool unavailable in a controlled test.&lt;/p&gt;

&lt;p&gt;Fresh DNS responses should select the fallback pool.&lt;/p&gt;

&lt;p&gt;Also test clients that still have the old DNS answer cached.&lt;/p&gt;

&lt;p&gt;DNS changes cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Move an existing TCP connection&lt;/li&gt;
&lt;li&gt;Instantly replace every cached DNS answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GeoDNS also does not know the user's exact location.&lt;/p&gt;

&lt;p&gt;It estimates based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recursive resolver location&lt;/li&gt;
&lt;li&gt;EDNS Client Subnet hints where available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test from multiple real networks and public resolvers.&lt;/p&gt;

&lt;p&gt;Also remember:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A default DNS record is not a guaranteed fail-safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Route 53 can return unhealthy records when all eligible records fail health checks.&lt;/p&gt;

&lt;p&gt;Testing from target regions might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short cdn.example.com A

curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  https://cdn.example.com/assets/logo.v1.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DNS answer
X-Edge-Id
X-Cache
HTTP errors
Latency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat during:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-edge withdrawal&lt;/li&gt;
&lt;li&gt;Entire pool failure&lt;/li&gt;
&lt;li&gt;Recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Other routing setups
&lt;/h3&gt;

&lt;p&gt;GeoDNS isn't the only option.&lt;/p&gt;

&lt;p&gt;You can also have globally reachable nodes and ask an upstream to limit route propagation using documented BGP communities.&lt;/p&gt;

&lt;p&gt;This is a separate routing policy from GeoDNS.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NO_EXPORT&lt;/code&gt;, for example, refers to AS boundaries. It does not mean "keep this route inside Europe."&lt;/p&gt;

&lt;p&gt;If a hosting provider cannot provide customer BGP, another option is to use a transit provider that announces your prefix and delivers traffic over tunnels.&lt;/p&gt;

&lt;p&gt;If you do that, test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Return routing&lt;/li&gt;
&lt;li&gt;Source filtering&lt;/li&gt;
&lt;li&gt;MTU&lt;/li&gt;
&lt;li&gt;Path MTU discovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And place an actual cache at the receiving location.&lt;/p&gt;

&lt;p&gt;Sending every regional tunnel back to one distant cache leaves the central cache and its network path in every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-geo.html" rel="noopener noreferrer"&gt;Route 53 Geolocation Routing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values-geo.html" rel="noopener noreferrer"&gt;Route 53 Geolocation Record Fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-how-route-53-chooses-records.html" rel="noopener noreferrer"&gt;Route 53 Health-Based Record Selection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-edns0.html" rel="noopener noreferrer"&gt;Route 53 and EDNS Client Subnet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/info/rfc4786/" rel="noopener noreferrer"&gt;RFC 4786&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Keep the CDN working
&lt;/h2&gt;

&lt;p&gt;Building the first two PoPs is only the beginning.&lt;/p&gt;

&lt;p&gt;Monitor each location through its &lt;strong&gt;unicast management path&lt;/strong&gt; as well as through the public Anycast IP.&lt;/p&gt;

&lt;p&gt;Otherwise, a failed server can disappear from your monitoring because the Anycast probe simply moves to another healthy location.&lt;/p&gt;

&lt;p&gt;Here are some of the most important failure modes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What you will notice&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GeoDNS points clients at an unavailable pool&lt;/td&gt;
&lt;td&gt;Fresh lookups or cached answers continue reaching a failed region.&lt;/td&gt;
&lt;td&gt;Check pool health, fallback policy, DNS caching, and fallback capacity. Test whole-region failure separately from one-edge withdrawal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lease, ROA, or routing-record changes&lt;/td&gt;
&lt;td&gt;Some networks stop reaching the prefix even though BGP sessions remain established.&lt;/td&gt;
&lt;td&gt;Track renewal dates and RPKI validity. Recheck authorization before changing ASN or upstream.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic shifts after a provider change&lt;/td&gt;
&lt;td&gt;Clients reach a distant PoP or overload a smaller location.&lt;/td&gt;
&lt;td&gt;Measure &lt;code&gt;X-Edge-Id&lt;/code&gt; and latency from several access networks. Review provider routing policy before adjusting communities or prepending.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One PoP fails or its cache restarts&lt;/td&gt;
&lt;td&gt;The surviving edge or origin receives a large traffic spike.&lt;/td&gt;
&lt;td&gt;Test failover capacity with a cold cache and reserve disk/bandwidth headroom. Add an origin shield if duplicate fills become expensive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old or private content is cached&lt;/td&gt;
&lt;td&gt;Users receive stale releases or another user's response.&lt;/td&gt;
&lt;td&gt;Use versioned assets and test Cookie, Authorization, &lt;code&gt;private&lt;/code&gt;, &lt;code&gt;no-store&lt;/code&gt;, and &lt;code&gt;Set-Cookie&lt;/code&gt; behavior.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A deployment or certificate differs between PoPs&lt;/td&gt;
&lt;td&gt;Only some networks see TLS failures, errors, or old behavior.&lt;/td&gt;
&lt;td&gt;Roll out one PoP first. Track certificate expiry and configuration versions per node.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The health controller fails or flaps&lt;/td&gt;
&lt;td&gt;A bad edge remains advertised or traffic repeatedly switches locations.&lt;/td&gt;
&lt;td&gt;Supervise the controller, use recovery hold-downs, test watchdog behavior, and coordinate graceful restart with upstreams.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A tunnel or return path breaks&lt;/td&gt;
&lt;td&gt;Small requests work but larger transfers stall, or responses never arrive.&lt;/td&gt;
&lt;td&gt;Check MTU, PMTU discovery, return routing, and source-address filtering.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An attack saturates the link&lt;/td&gt;
&lt;td&gt;Servers are technically healthy but unreachable before HTTP limits can help.&lt;/td&gt;
&lt;td&gt;Arrange upstream DDoS mitigation and understand how to activate it. Restrict origin access and monitor egress.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several services share the same &lt;code&gt;/24&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Withdrawing the prefix for one service also moves healthy services.&lt;/td&gt;
&lt;td&gt;Define health policy for every service on the prefix or use separate prefixes where independent withdrawal is required.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Your first working CDN
&lt;/h3&gt;

&lt;p&gt;You're ready to add real traffic when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both PoPs serve valid HTTPS&lt;/li&gt;
&lt;li&gt;Each location proves &lt;code&gt;MISS&lt;/code&gt; followed by &lt;code&gt;HIT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Private responses stay uncached&lt;/li&gt;
&lt;li&gt;Withdrawing either route moves new connections to the surviving location&lt;/li&gt;
&lt;li&gt;Origin connectivity is monitored&lt;/li&gt;
&lt;li&gt;The health controller has been failure-tested&lt;/li&gt;
&lt;li&gt;Route recovery has been tested&lt;/li&gt;
&lt;li&gt;Reboots have been tested&lt;/li&gt;
&lt;li&gt;External probes confirm real internet routing behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with public static assets.&lt;/p&gt;

&lt;p&gt;Measure the effect on origin traffic before expanding what you cache.&lt;/p&gt;

&lt;p&gt;From there you can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More PoPs&lt;/li&gt;
&lt;li&gt;More regional pools&lt;/li&gt;
&lt;li&gt;Origin shielding&lt;/li&gt;
&lt;li&gt;Purge APIs&lt;/li&gt;
&lt;li&gt;Centralized certificates&lt;/li&gt;
&lt;li&gt;Deployment automation&lt;/li&gt;
&lt;li&gt;Configuration versioning&lt;/li&gt;
&lt;li&gt;DDoS protection&lt;/li&gt;
&lt;li&gt;Edge rate limiting&lt;/li&gt;
&lt;li&gt;Capacity-aware routing&lt;/li&gt;
&lt;li&gt;Regional origins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difficult part of Anycast isn't getting two servers to announce the same address.&lt;/p&gt;

&lt;p&gt;The difficult part is keeping routing, application health, TLS, cache behavior, failure recovery, and operational state synchronized as the network grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html" rel="noopener noreferrer"&gt;RFC 9111: HTTP Caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ripe.net/manage-ips-and-asns/resource-management/rpki/bgp-origin-validation/" rel="noopener noreferrer"&gt;RIPE NCC: BGP Origin Validation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About Adios.dev
&lt;/h2&gt;

&lt;p&gt;This article comes from the engineering work behind &lt;a href="https://www.adios.dev/" rel="noopener noreferrer"&gt;Adios.dev&lt;/a&gt;, where we're building a developer cloud for running applications, APIs, databases, and AI workloads.&lt;/p&gt;

&lt;p&gt;You can explore the network here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.adios.dev/network" rel="noopener noreferrer"&gt;Adios network map&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.adios.dev/blog/inside-the-adios-edge-network" rel="noopener noreferrer"&gt;Inside the Adios edge network&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building your own Anycast or regional CDN and want help with BGP, routing, caching, monitoring, upgrades, or ongoing operation, you can also reach the Adios engineering team.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.adios.dev/blog/building-anycast-ingress-for-a-developer-cloud" rel="noopener noreferrer"&gt;Adios.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>networking</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
