keepalivedでキャッシュDNSを冗長化した話

こんにちは。日々是発見が楽しみな西山です。
今回は、keepalivedでキャッシュDNSの冗長化をやってみました。

何で冗長化しようと思った?

これを書き出すとちょっと長くなるのですが……

前にもこのエンジニアブログで書いたとおり、自宅ネットワークではunboundを使ったキャッシュDNSを動かしていますが、unboundサーバーはProxmoxホストに載っています。
Proxmoxホストはシングル構成の1台だけなので、Proxmoxをアップデートしたりハードウェアのメンテナンス等で落とすと、自宅ネットワークからキャッシュDNSがいなくなってしまいます。
DNSがいないのはネットを使えないに等しいので、余っていたラズパイにunboundを入れてセカンダリDNSにしています。

192.168.0.251  <-- プライマリDNS(Proxmoxコンテナ)
192.168.0.250  <-- セカンダリDNS(ラズパイ4)

ラズパイはProxmoxコンテナより性能が劣るのと、RaspberryPi OS(Debianベース)の関係でだいぶ古いunboundしか入れられないため、PCやスマホがセカンダリDNSに聞きに行くとレスポンスがちょっと悪くなります。
悪いことに、WindowsのChrome系ブラウザは、起動して数時間経つと突然、DNS問い合わせを全部セカンダリに向けるような挙動をします。

セカンダリDNSは遅いからできるだけ使ってほしくないのに、よりにもよってPCで一番利用頻度の高いWebブラウザがセカンダリDNSを優先して使い出す……という状況に悩んだ結果、keepalivedというプロダクトにたどり着きました。
keepalivedでDNS用の仮想IPを立てて端末にはそれを設定し、keepalivedはプライマリが生きてる限りそちらにDNSトラフィックを流す」よう構築してみます。

192.168.0.254  <-- 仮想IP(端末にはこれ1つだけを設定する)
      192.168.0.251が落ちてる時だけトラフィックを192.168.0.250に向けかえる

192.168.0.251  <-- プライマリDNS(Proxmoxコンテナ)
192.168.0.250  <-- セカンダリDNS(ラズパイ4)

keepalivedのインストールと基本設定

自宅のDNSサーバーはプライマリがubuntu、セカンダリがRaspberryPi OSなので、どちらもaptコマンドでパッケージをインストールします。
AlmaLinuxのようなRHEL系なら、dnfでパッケージを入れればその後の作業は同じです。

apt install keepalived

keepalivedは/etc/keepalived/以下に設定ファイルを置きます。
keepalived.conf.sampleというファイルが置かれていますが、これを流用するとハマりがちなので、一から作ります。
※keepalived.conf.sampleは複数のサンプル設定が列記されているので、よく理解せずIPだけ書き換えて起動させたりすると、予想外の設定が刺さって動かなくなったりします。

プライマリ(192.168.0.251)側
vi /etc/keepalived/keepalived.conf

global_defs {
    router_id dns01
    vrrp_garp_master_refresh 60
    enable_script_security
    script_user root
}

vrrp_script track_unbound {
    script "/usr/bin/systemctl is-active unbound.service"
    interval 5
    fall 3
    rise 2
}

vrrp_instance VRRP1 {
    state MASTER
    # nopreempt
    interface eth0
    virtual_router_id 101
    priority 200
    advert_int 1
    authentication {
        auth_type AH
        auth_pass <パスワード>
    }
    virtual_ipaddress {
        192.168.0.254/24
    }
    virtual_ipaddress_excluded {
        2001:db8::254/32
    }
    track_script {
        track_unbound
    }
}

セカンダリ(192.168.0.250)側
プライマリと違う部分を赤文字にしていますが、この違いが動作のキモとなるので注意して下さい。
vi /etc/keepalived/keepalived.conf

global_defs {
    router_id dns01
    vrrp_garp_master_refresh 60
    enable_script_security
    script_user root
}

vrrp_script track_unbound {
    script "/usr/bin/systemctl is-active unbound.service"
    interval 5
    fall 3
    rise 2
}

vrrp_instance VRRP1 {
    state BACKUP
    # nopreempt
    interface eth0
    virtual_router_id 101
    priority 100
    advert_int 1
    authentication {
        auth_type AH
        auth_pass <パスワード>
    }
    virtual_ipaddress {
        192.168.0.254/24
    }
    virtual_ipaddress_excluded {
        2001:db8::254/32
    }
    track_script {
        track_unbound
    }
}

vrrp_instanceセクションの中で、プライマリは

state MASTER
priority 200

セカンダリは

state BACKUP
priority 100

と定義されています。この設定により、
「通常はpriorityが大きいプライマリ側に仮想IPが付き、プライマリが落ちた時だけセカンダリが昇格して仮想IPを持つが、プライマリが戻ってきたらセカンダリは降格し元に戻る」
という動作が実現します。

ちなみに、プライマリに戻る動作(フェールバック)をさせたくない場合は、

nopreempt

のコメントを外して有効にします。

その他keepalive設定のキモ

vrrp_garp_master_refresh 60

keepalivedはARPで伝えるMACアドレスを変化させることで、仮想IPの向け先を切り替えます。
仮想IP切り替えと同時にLANスイッチが落ちたり、その他何らかの理由でARPキャッシュの更新に失敗した場合、MACアドレスの変化がうまく伝わらないことがあります。
「vrrp_garp_master_refresh 60」を設定すると、60秒ごとにGratuitous ARPを送信し、各機器のARPキャッシュを更新させます。

vrrp_script track_unbound {
    script "/usr/bin/systemctl is-active unbound.service"
    interval 5
    fall 3
    rise 2
}

keepalivedはノードのIPアドレスの死活を検知して切り替わりますが、それ以外のリソース状況も切替条件に入れたい時に「vrrp_script」を利用します。
上記サンプルコードの場合は「/usr/bin/systemctl is-active unbound.service」でunboundサービスの死活を5秒ごとに確認し、3回連続で落ちていたら仮想IPを切り替えます。
「vrrp_script」でコマンドを実行させる場合、

enable_script_security
script_user <実行ユーザー>

のセキュリティに関する設定を「global_defs」に入れる必要があります。

authentication {
    auth_type AH
    auth_pass <パスワード>
}

keepalivedクラスタへの参加に認証を必要とします。
「悪意あるノードの参加」や「誤った設定のノードの参加」といった事故を防ぐことができます。
特に、誤設定ノードの参加を弾けるのはうっかり事故防止に役立てられると思います。

virtual_ipaddress {
    192.168.0.254/24
}
virtual_ipaddress_excluded {
    2001:db8::254/32
}

IPv4/v6デュアルスタックをまとめて切り替える時はこう記述します。
仮想IPを定義する「virtual_ipaddress」にはIPv4、IPv6どちらも書けますが、1つのセクションにv4とv6を混ぜて書くことはできません
そのため、IPv6の設定は「virtual_ipaddress_excluded」の方に分けて書きます。

unbound側の設定

keepalivedから接続を受けるために、仮想IPのinterface設定を追記します。

interface:  192.168.0.254
interface:  2001:db8::254/32

「interface-automatic: yes」とする方法もネット上で紹介されていますが、手元の環境ではちゃんと応答してくれなかったため、仮想IPをベタ書きしました。

keepalivedよかったこと

keepalivedのおかげで、これまで端末任せだったプライマリとセカンダリのDNS選択を確実にコントロールできるようになりました。
同一筐体のunboundにクエリを集約すれば、DNSキャッシュを最大限有効活用できて効率が良くなります。体感速度も向上しました。

実は最初、keepalivedではなくPacemakerを使ったクラスタを作ろうとしていたのですが、Pacemakerは参加ノード全てのPacemakerバージョンが揃っていないとクラスタを起動できません(当たり前といえば当たり前ですが)。
今回はプライマリDNSがubuntu25.04、セカンダリがRaspberryPi OSなので、パッケージで提供されているPacemakerのバージョンが違ってしまっていました。バージョンを揃えるにはソースコードからセルフビルドするしか方法はなく、あまりに手間が膨大すぎるのでPacemakerの利用は断念しました。
よりシンプルなkeepalivedでやりたかったことは実現できたので、結果オーライでしたね。

keepalivedはOSIモデルのL4で動作するため、それより上位層のアプリケーション側整合性を保証することはできません。ですので、バックエンドDBの一貫性だったり、機微情報入力で厳密なセッション管理を求められるようなシステムには不向きです。
その特性が理解できていれば、

  • 問い合わせが1往復で完結するDNS
  • 静的コンテンツのみのWebサーバー

といったインフラの冗長化にkeepalivedを利用できるのが分かるかと思います。
keepalivedはLinuxカーネルモジュールに組み込まれ、軽量かつ堅牢に動作するので、使い所を選べば信頼性の高い冗長化システムを組むことができます。ぜひ活用してみて下さい。