Resolver
CoreDNSを運用していて、Aレコードを外部から書き換えたかった。
IDCにValkey Sentinelの構成を組んでいた。マスターが落ちたらレプリカに切り替える。Sentinel自体はちゃんと動く。問題は、アプリケーションがどうやって新しいマスターを知るかだ。クライアント側のSentinel対応ライブラリを使う手はあるが、すべてのアプリにそれを要求したくなかった。シンプルに、DNSの応答先を切り替えたい。アプリはvalkey.service.localを引くだけ。中身が変わっても気づかない。
これが内部DNSという文化の本質だと思う。IPアドレスはマシンの住所だが、DNS名はサービスの肩書きだ。住所は変わる。引っ越しもする。建て替えもある。でも肩書きは変わらない。アプリケーションが知っていればいいのは肩書きだけで、現在の住所は問い合わせのたびに教えてもらえばいい。
サーバー間通信をIP直書きしていた時代は、それで成立していた。マシンが固定で、台数も少なくて、めったに動かない。設定ファイルにIPをハードコードしても困らなかった。インスタンスが刹那的に生まれては消え、Podが勝手に増減する世界では、その前提が崩れる。住所を直接呼ぶのは無理だ。肩書きで呼ぶしかない。
ただし肩書きを管理する仕組みは脆い。
2021年のFacebookでは、BGPの設定ミスで自社の権威DNSが世界中から見えなくなった。6時間続いた。社員のバッジまで使えなくなったというのが象徴的だった。AWSも何度かRoute 53起因の障害を起こしているし、Cloudflareも例外ではない。DNSが落ちるとき、それは静かに、しかし全方位的に効く。HTTPは生きていてもDNSが死んでいれば、誰も目的地にたどり着けない。
それなのに、現代のインフラはDNSへの依存をむしろ深めている。Kubernetesのクラスタ内通信はほぼすべて内部DNSで動く。Service名がCoreDNSで解決され、その先のPodのIPは裏で勝手に動いても外からは見えない。逆に言えば、CoreDNSが止まるとクラスタ内のあらゆる通信が止まる。脆い橋だと知りつつ、全員その上を走っている。
話を戻す。Valkey Sentinelの件で、わたしが欲しかったのはCoreDNSの応答先をHTTPで書き換える仕組みだった。
選択肢は調べた。CoreDNSのetcdプラグインを使えば近いことは実現できる。ただetcdクラスタをひとつ立てるところから始めるのは本末転倒だ。Consulもある。これも重い。Kubernetesに乗ればService抽象がそのまま使えるが、IDCの構成にKubernetesを入れるかという話になる。やりたいのはAレコードを動的に差し替えることだけなのに、選択肢がどれもオーバーキルだった。
ないなら作る。CoreDNSのプラグインを書いた。coredns-dynresolveという名前にした。やることは単純で、HTTPのPUTで状態を投げ込むと、それが次のクエリから応答に反映される。それだけ。外部のコントローラ——監視エージェントでも、systemd timerでも、CIでも——がヘルスチェックして状態を投げる。プラグインはその状態をDNSとして返す。
設計で一番気を使ったのは「DNSは止めない」ということだった。APIが落ちてもDNSは応答し続ける。プラグインがバグってもCoreDNS本体は落ちない。書き込みはAPIから、読み出しはin-memoryから、両者は完全に分離する。フォールバックも段階的にした。キャッシュが新鮮ならそれを返す、なければ状態ストアから読む、それも壊れていたら古いキャッシュ、最後はフォールバックIP。すべて尽きたら次のプラグインに渡す。どこかが壊れても、DNSとしての応答はやめない。
Aレコードしか対応していない。CNAMEもMXもSRVもない。割り切った。やりたいことが「サービスの応答先IPを動的に切り替える」だけなら、Aレコードがあれば足りる。
書き終えてから思ったのは、結局これも脆い橋を補強しただけだということだ。DNSが死ねばすべて止まる前提は変わらない。フェイルオーバーが速くなった分、依存はむしろ濃くなった。
橋が脆いのは知っている。それでも渡らないと、向こう岸には行けない。