スクリプタブル GeoDNS: AWS から Bunny への移行
Scriptable GeoDNS: Leaving AWS for Bunny (vincent.bernat.ch)
要約
著者は、ブログのパフォーマンス向上のために、AWSのCloudFrontとRoute 53を利用していましたが、Route 53のコストが高額であることに気づきました。そこで、ほぼ無料かつスクリプト可能な回答機能を備えたBunny DNSに移行しました。これにより、ユーザーに最も近いサーバーへのルーティングが最適化され、コストも大幅に削減されました。
全文翻訳
スクリプタブル GeoDNS: AWS から Bunny への移行
このブログのパフォーマンスメトリクスを最適化するために、アセットの配信にはAmazon CloudFrontを、コンテンツを提供する最も近いホストに読者を誘導する地理情報に基づいたDNSにはRoute 53を利用していました。しかし、Route 53は高価です。ゾーンあたり月額0.50ドル、地理情報クエリ100万件あたり0.70ドル、ヘルスチェック1件あたり月額0.75ドルに加え、DNSSECでゾーンに署名するために必要なKMSキーに月額1ドルがかかります。Bunny DNSが500ドメインまでほぼ無料であり、スクリプト可能な回答機能を備えていることを発見したとき、私は移行を決めました!さらに、スロベニアに拠点を置くヨーロッパの企業です。
Bunny CDN#
Bunny.netは2015年にBunnyCDNとして始まりました。2017年にはIPv6サポートを追加しました。移行は問題なく行われました。カスタムドメインは使用せず、b-cdn.netドメインからコンテンツを配信しています。コンテンツネゴシエーションに依存してAVIFとWebP画像を配信しているため、CDNを各画像フォーマットを個別にキャッシュするように設定しました。
個人情報の収集を避けるため、ログ記録を無効にしています。bunny.netのGDPRページではデータが匿名化されると約束されていますが、ログ記録が無効な場合にCloudFrontが自身のログに何を残すかは不明確です。
Bunny.netのCDNオファリングの唯一の欠点は、HTTP/3の欠如です。サポートに問い合わせたところ、2分後に作業中だがETAは共有できないとの回答がありました。
Bunny DNS#
Bunny.netは2022年にDNSオファリングを導入し、最近そのコストをほぼゼロに引き下げました。地理情報ルーティングをサポートしていますが、非常にクールな機能はスクリプト可能なDNSです。これは、各クエリに対してユーザー提供のJavaScriptを実行して回答を計算します。
私はこのブログを世界中でホストするために、パリ、ニュルンベルク、アッシュバーン、シンガポール、サンノゼ、サンパウロ、シドニーに7つの小さなNixOS仮想マシンを実行しています。Bunnyのスクリプト可能なDNSにより、最も近い2つのサーバーのIPアドレスを返すことができます。ブラウザは、最初のサーバーがダウンした場合に2番目のサーバーにフォールバックします。
const servers = [
{
ipv4: "45.90.160.60",
ipv6: "2a0c:8881:5:100::1",
latitude: 48.88,
longitude: 2.22,
},
/* […] */
{
ipv4: "139.180.162.114",
ipv6: "2001:19f0:5800:1855:5400:6ff:fef0:4c0a",
latitude: -33.87,
longitude: 151.21,
},
];
const families = { A: "ipv4", AAAA: "ipv6" };
const records = { A: ARecord, AAAA: AaaaRecord };
export default function handleQuery(query) {
const type = String(query.request.queryType).toUpperCase();
const family = families[type];
if (!family) return [];
// ユーザーの場所を取得
const location = query.request.geoLocation;
// 候補のリストを作成
let candidates = servers.map(
(s) => new Server(s[family], s.latitude, s.longitude, 100),
);
// 生きている最も近い2つのサーバーを選択
const selected = [];
while (selected.length < 2) {
const server = RoutingEngine.getClosestServer(candidates, location, true);
selected.push(server);
candidates = candidates.filter((s) => s.ip !== server.ip);
}
// それらのIPアドレスを返す
return selected.map((s) => new records[type](s.ip, 3600));
}
上記のスクリプトは、ユーザーに最も近い生きている[3]サーバーを返すヘルパーであるRoutingEngine.getClosestServer()に依存しています。[4] Doggoのおかげで、少なくともAレコードについては、期待通りに動作することを確認できます。[5]
$ doggo --gp-from="Amsterdam" A vincent.bernat.ch
LOCATION NAME TYPE TTL ADDRESS
Amsterdam, NL
vincent.bernat.ch. A 3600s 45.90.160.60
vincent.bernat.ch. A 3600s 116.203.18.48
$ doggo --gp-from="Texas" A vincent.bernat.ch
LOCATION NAME TYPE TTL ADDRESS
Houston (TX), US
vincent.bernat.ch. A 3600s 5.161.44.145
vincent.bernat.ch. A 3600s 149.28.219.155
この方法の最も迷惑な欠点は、ブエノスアイレスのユーザーがサンパウロ(1000マイル、RTT 30ms)とアッシュバーン(5200マイル、RTT 140ms)の両方を取得することです。ほとんどのリゾルバは順序をランダム化するため、ユーザーはアッシュバーンからウェブページを取得する可能性があります。最も近いサーバーのみを返すこともできます。そして、スクリプトされたレコードを組み込みの地理情報ルーティングと入れ替えます。しかし、サーバーが利用できなくなった場合、DNSキャッシュが期限切れになるまで、ブラウザには代替手段がありません。短いTTLであっても、利用できない短いウィンドウが残ります。
HTTPSレコードソリューション#
RFC 9460で定義されているHTTPS DNSレコードは、可用性とレイテンシの問題の両方を解決します。最新のブラウザは、代替ポートやアプリケーションレイヤープロトコル(HTTP/2またはHTTP/3)など、HTTPサーバーに到達するための追加情報を取得するためにこのレコードをクエリします。各レコードには優先度があります。ブラウザに最も近いサーバーに最初に連絡し、最初のサーバーが利用できない場合は次のサーバーにフォールバックするように要求できます。
vincent HTTPS 1 first.geo alpn="h3,h2"
vincent HTTPS 2 second.geo alpn="h3,h2"
vincent HTTPS 3 . alpn="h3,h2"
; Bunny DNS は "first.geo" と "second.geo" を地理情報またはレイテンシルーティングで提供する必要があります。
first.geo A 45.90.160.60
first.geo AAAA 2a0c:8881:5:100::1
second.geo A 116.203.18.48
second.geo AAAA 2a01:4f8:1c0c:5eb5::1
vincent A 45.90.160.60
vincent A 116.203.18.48
vincent AAAA 2a0c:8881:5:100::1
vincent AAAA 2a01:4f8:1c0c:5eb5::1
これは理想的なソリューションのように見えます!残念ながら、Chromeはターゲットが.でないレコードを無視するため、優先度3の最後のレコードが必要になります。さらに、CNAMEとは異なり、Bunny DNSはfirst.geoとsecond.geoのIPアドレスを追加セクションに返しません。リゾルバは、HTTPSレコードを取得するために最初の要求を送信し、first.geoを解決するために2番目の要求を送信する必要があります。
HTTPSレコードは、このユースケースにはまだ新しすぎます。元の方法に戻ります!📐
ハイブリッドソリューション#
妥協案として、スクリプトは、2つの最も近いサーバーがユーザーからの距離が近い場合、それらを返します。それ以外の場合は、最も近いサーバーのみを短いTTL(2分)で返します。
export default function handleQuery(query) {
const location = query.request.geoLocation;
let candidates = servers.map(
(s) => new Server(s.ipv4, s.latitude, s.longitude, 100),
);
// 最も近い2つのサーバーを選択
const selected = [];
while (selected.length < 2) {
const server = RoutingEngine.getClosestServer(candidates, location, true);
selected.push(server);
candidates = candidates.filter((s) => s.ip !== server.ip);
}
// それぞれのユーザーへの距離を計算
const [first, second] = selected.map((s) =>
GeoDistance.calculate(s, location),
);
// 2番目のサーバーが最初のサーバーから1000マイル以上離れており、その距離が2倍以上の場合
// 最初のサーバーのみをTTL 2分で返す。
if (second > 2 * first && second - first > 1000) {
return [new ARecord(selected[0].ip, 120)];
}
// それ以外の場合は、両方をTTL 1時間で返す。
return selected.map((s) => new ARecord(s.ip, 3600));
}
これをDoggoで確認できます。シカゴからは、TTL 2分の場合はアッシュバーンサーバーのみを取得しますが、メキシコからはアッシュバーンとサンノゼの両方を取得します。
$ doggo --gp-from="Chicago" A vincent.bernat.ch
LOCATION NAME TYPE TTL ADDRESS
Chicago (IL), US
vincent.bernat.ch. A 120s 5.161.44.145
$ doggo --gp-from="Mexico" A vincent.bernat.ch
LOCATION NAME TYPE TTL ADDRESS
Queretaro, MX
vincent.bernat.ch. A 3600s 5.161.44.145
vincent.bernat.ch. A 3600s 149.28.219.155
$ doggo --reverse 5.161.44.145 149.28.219.155
NAME TYPE TTL ADDRESS
145.44.161.5.in-addr.arpa. PTR 7187s web05.luffy.cx.
155.219.28.149.in-addr.arpa. PTR 3600s web07.luffy.cx.
注
下のマップをインタラクティブにするには、JavaScriptを有効にしてください。場所をポイントするとDNS応答が表示されるか、サーバーを無効にして応答がどのように変化するかを確認できます。
[画像: 各場所のBunny DNSによって応答されたサーバーのマップ]
完全なコードは少し複雑です。RoutingEngine.getClosestServer()がIPv4のみをサポートしている問題を回避し、回答を構築するのに十分な生きているサーバーがない場合のケースを処理します。
Bunny.netは包括的なTerraformプロバイダーを持っています。CDK Terrainを使用してTypeScriptでターゲット状態を構築し、OpenTofuを使用してデプロイします。詳細については、「Terraform、CDKTF、およびNixOSによるインフラストラクチャ管理」を参照してください。[6]
ACME DNS with Hickory DNS#
Route 53は、私のもう一つのユースケースである、DNS-01 ACMEチャレンジを解決するためのacme.luffy.cxの提供をカバーしていました。Route 53とは異なり、Bunny DNSはきめ細かなアクセスポリシーで単一ゾーンへのアクセスを制限できません。この用途は高可用性も低レイテンシも必要としないため、Rustで書かれたメモリセーフなDNSサーバーであるHickoryに置き換えました。これは自動DNSSEC署名と動的アップデートをサポートしています。その設定は簡単です。
[[zones]]
zone = "acme.luffy.cx"
zone_type = "Primary"
[zones.stores]
allow_update = true
type = "sqlite"
journal_path = "acme.luffy.cx.sqlite"
zone_path = "acme.luffy.cx.zone"
# DNSSECでゾーンに署名するための秘密鍵。生成方法:
# openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256
[[zones.keys]]
algorithm = "ECDSAP256SHA256"
key_path = "/etc/hickory-dns.acme.luffy.cx.pem"
# ゾーンアップデートに署名するためのTSIGキー。生成方法:
# openssl rand 32
[[zones.stores.tsig_keys]]
algorithm = "hmac-sha256"
key_file = "/etc/hickory-dns.acme.luffy.cx.update.acme.luffy.cx.tsig"
name = "update.acme.luffy.cx."
提供されるゾーンファイルは、初回起動時にデータベースをシードするだけです。私はDNS UPDATEプロトコルを使用してエントリをプッシュすることで、Lego(Let's Encryptから証明書を取得するために使用するACMEクライアント)にCNAMEレコードでこのゾーンへのDNS-01チャレンジを委任します。
_acme-challenge CNAME bernat.ch.acme.luffy.cx.
_acme-challenge.vincent CNAME vincent.bernat.ch.acme.luffy.cx.
残念ながら、Bunny DNSはゾーンにDSレコードを公開できません。[7] acme.luffy.cxゾーンと証明書配信を保護するためにこれが必要です。そのため、非営利団体によって運営されている無料のDNSホスティングサービスであるdeSECでluffy.cxゾーンをホストしています。[8] acme.luffy.cxのDNSKEYレコードから派生したDSレコードをluffy.cxに公開する必要があります。
$ ZONE=acme.luffy.cx
$ dig @nsupdate.luffy.cx $ZONE DNSKEY +noall +answer \
> | dnssec-dsfromkey -a SHA-256 -f - $ZONE
acme.luffy.cx. IN DS 55628 13 2 4946E7508F20E15F1B577A231104B029A1A82E04699A80CD46ADCE31227EE81F
全体として、bunny.netへの移行には非常に満足しています。DSレコードを公開できない、HTTP/3がない、IPv4のみのRoutingEngine.getClosestServer()ヘルパーなど、まだいくつかの問題点があります。請求額は月額5ドルから1ドルになりました。これまでのところ、パフォーマンスはAWSと同等ですが、シドニーのサーバーを含む現在のセットアップを完了したのは10月下旬です。もう少しデータを収集する時間が必要で、短いアップデートを投稿する予定です。📈