クラウド・インスタンスにおけるDNSサーバーの指定は、DHCPサーバーから情報を取得して利用するようになっています。具体的には dhclient が resolv.conf を上書きする感じですが、最近は NetworkManager さんがこの辺の面倒を見てくれるので、まともな構成 ってやつを考えてみました。
ドンピシャで正着に至ったというわけではないですが、ひとつの有効な手段として扱うことはできそうです。
目次
概要
NetworkManager は必ずしも必要なわけではないですが、CentOS7 など新し目のディストリビューションではデフォで動いていることが多いです。役割としては、その名の通りネットワーク周りを色々やってくれるのですが、今回はDHCPとDNSに絞ってまとめていきます。それを使って、主にパブリッククラウドにおける問題を軽減することを目的とします。問題とは、クラウドがDHCPで指定するDNSサーバーが利用不能になり、WANのサービスやプライベートのエンドポイントの名前解決ができず、実質的にサービスの障害となることです。
これはそうそう起きる問題ではないのですが、実際に経験したことがありますし、軽減策を入れる手間はかかっても悪いことはないので、検討の価値はあります。入れる条件として、DNSレコードの情報がWANとLANで同じ──つまり、プライベートDNSでしか引けないレコードがない、必要があります。また、仮にそのような条件だとしても、採用することで一部のダメージを軽減することが可能です。
あまり馴染みのないシステムなので気分はアレかもですが、ディストリビューションの変化を追うのも仕事ですので、グッとこらえて参りましょう。
ちなみに今回のOSは CentOS7 です。
NetworkManagerがない場合
OSのネットワーク起動時に、network-scripts で /sbin/dhclient デーモンが起動されます。そして、resolv.conf はこのような感じに編集されます。
1 2 3 |
; generated by /usr/sbin/dhclient-script search ap-northeast-1.compute.internal nameserver 10.0.0.2 |
これはAWSの例で、SubnetではなくCIDRのネットワークアドレス +2 が nameserver になります。クラウド環境によっては nameserver が2つ以上になることも普通にあります。
これによって、上の nameserver から順に利用され、使えない場合は毎回
options timeout:M 秒 attempts:N 回
を経て次の nameserver を試行する、という流れになります。この例のように nameserver が1つしかない場合はワンパンで即死する可能性がありますし、複数あっても名前解決のたびに処理が数秒停止するというのは、結構な致命傷になりえます。
DNSキャッシュを適切に設定していればダメージを軽減できるものの、不安が残る強度といえるでしょう。
NetworkManagerのデフォルトの挙動
インストールは簡単です。
1 |
yum install NetworkManager |
NetworkManagerの場合も dhclient が動くことに変わりはないのですが、プロセスがNetworkManager管理になり、
1 2 3 |
$ ps ww -C dhclient PID TTY STAT TIME COMMAND 43270 ? S 0:03 /sbin/dhclient -d -q -sf /usr/libexec/nm-dhcp-helper -pf /var/run/dhclient-eth0.pid -lf /var/lib/NetworkManager/dhclient-3074a704-f7a5-402f-9415-a442722a4ca2-eth0.lease -cf /var/lib/NetworkManager/dhclient-eth0.conf eth0 |
resolv.conf のメッセージ内の自己主張が変わります。
1 2 3 |
# Generated by NetworkManager search ap-northeast-1.compute.internal nameserver 10.0.0.2 |
このままならば、名前解決の挙動としては、さきほどと何も変わりません。
dnsmasqを利用する
dnsmasqはDNS・DHCPの多機能サーバーという感じですが、今回はDNSキャッシュサーバーとして利用します。NetworkManager と dnsmasq は仲良しのようで、簡単に切り替えられるのが良い点です。このように dns=dnsmasq を追加し、
1 2 3 4 5 6 |
[main] plugins=ifcfg-rh dns=dnsmasq [logging] level=WARN |
再起動すると、
1 |
systemctl restart NetworkManager |
nameserver がローカルホストになり、
1 2 3 |
# Generated by NetworkManager search ap-northeast-1.compute.internal nameserver 127.0.0.1 |
ローカルの53番ポートにdnsmasqが現れたことを確認できます。
1 2 3 4 5 6 7 8 |
$ ps ww -C dnsmasq PID TTY STAT TIME COMMAND 58471 ? S 0:00 /usr/sbin/dnsmasq --no-resolv --keep-in-foreground --no-hosts --bind-interfaces --pid-file=/var/run/NetworkManager/dnsmasq.pid --listen-address=127.0.0.1 --conf-file=/var/run/NetworkManager/dnsmasq.conf --cache-size=400 --proxy-dnssec --conf-dir=/etc/NetworkManager/dnsmasq.d $ lsof -i:53 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME dnsmasq 58471 nobody 4u IPv4 370111 0t0 UDP localhost.localdomain:domain dnsmasq 58471 nobody 5u IPv4 370112 0t0 TCP localhost.localdomain:domain (LISTEN) |
この dnsmasq デーモンは systemdではなく、NetworkManager が管理しています。
DNSサーバーの指定は –conf-file のファイルに書いてあり、
1 |
server=10.0.0.2 |
こちらにDHCPから取得したサーバーが羅列されます。この、1つ以上のserverに対して、指定した方法でserverを選択し、名前解決をForwardします。
任意のDNSサーバーを追加する
クラウド管理のDNSサーバー以外も追加することで、耐障害性の向上を試みます。今回はGoogle先生 8.8.8.8 に頼るものとします。
AWSの場合
VPCでDHCPオプションセットというのを作成し、VPCと関連付けることで、DHCPから渡される情報をカスタマイズできます。デフォルトのデータはこんな感じで登録されているので、
このように指定した、新しいオプションセットを作成し、
VPC毎に関連付けを変更することで、DNSサーバーのリストが変更されます。全てのクラウド環境に同様の機能があるとは限りませんが、まずは探してみて、なければ次の手段を検討するとよいです。
手動で追加する
ネットワークインターフェースの情報は nmcli コマンドで編集することができます。まずは接続情報を確認します。
1 2 3 |
$ nmcli connection show NAME UUID TYPE DEVICE System eth0 5fb06bd0-0bb0-7ffb-45f1-d6edd65f3e03 802-3-ethernet eth0 |
このAWSのCentOS7では “System eth0” という名前を確認できました。この名前は環境によってはただの “eth0” ということもあります。
次にこのインターフェースに対してDNS情報を追加します。
1 |
nmcli connection mod "System eth0" +ipv4.dns 8.8.8.8 |
これによって、/etc/sysconfig/network-scripts/ifcfg-eth0 が編集され、DNS1=8.8.8.8 が追加されます。
1 2 3 4 5 |
DEVICE="eth0" BOOTPROTO="dhcp" ... DNS1=8.8.8.8 ... |
最初は DNS[1-9] の行は何もありません。また、このファイルは手動で編集することも可能です。編集したら、NetworkManagerを再起動すると、
1 |
systemctl restart NetworkManager |
DHCPから取得した情報の後ろに加わる形で設定ができあがります。
1 2 |
server=10.0.0.2 server=8.8.8.8 |
余談ですが、nmcli はかなり強力で、さきほど追加したDNS情報を削除する場合は、
+ をこう – にするのが正しいのですが
1 |
nmcli connection mod "System eth0" -ipv4.dns 8.8.8.8 |
何を思ったか一度こう delete を実行して、インターフェースが丸っと削除されて通信不可になりました(コンソールから復旧)。気をつけましょう:-)
1 |
nmcli connection delete "System eth0" +ipv4.dns 8.8.8.8 |
起動時の設定として組み込む
DHCPに任意のDNSサーバーを追加する仕組みがない場合、nmcli などを実行する必要がでますが、AutoScaling やらプロビジョニング・ツールやらがあるので、設定は自動化しておかなくてはいけません。だいぶ色々探したのですが、あまり綺麗な設定としてはできないようで、
/etc/NetworkManager/dispatcher.d 内のスクリプトとして実装してみました。この中のスクリプトは、NetworkManager起動時や、数分~数十分に1回、実行されることになります。そのため、無駄な処理が行われないようなスクリプトにする必要があります。で、書いたのがこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#!/bin/bash DEVICE=$1 ACTION=$2 WORD="Itamae" DNS_SUB="8.8.8.8" CONF="/etc/resolv.conf" [ "$ACTION" == "up" ] || exit 0 CONTENT=$(cat << EOT # Generated by NetworkManager/dispatcher.d by ${WORD} options timeout:1 attempts:2 options single-request-reopen EOT ) grep "$WORD" $CONF || echo "$CONTENT" >> $CONF UUID=`nmcli connection show | grep " $DEVICE *$" | sed -e "s/^.* \([0-9a-f-]\+\) .*$/\1/"` SHOW=`nmcli connection show "$UUID"` echo "$SHOW" | grep "DNS" [ $? -ne 0 ] && exit 0 echo "$SHOW" | grep "${DNS_SUB}$" [ $? -eq 0 ] && exit nmcli connection mod "$UUID" +ipv4.dns "$DNS_SUB" nmcli connection up "$UUID" |
ポイントは
起動時の処理に restart を入れるのはループ臭がするのがアレですが、ちゃんと書けば大丈夫。そして、何回 restart されても結果が同じなのはもちろんです、と。
NetworkManagerをrestartすると、結果は、まず resolv.conf が
1 2 3 4 5 6 |
# Generated by NetworkManager search ap-northeast-1.compute.internal nameserver 127.0.0.1 # Generated by NetworkManager/dispatcher.d by Itamae options timeout:1 attempts:2 options single-request-reopen |
/etc/sysconfig/network-scripts/ifcfg-eth0 には DNS1=8.8.8.8 が追加され、
/var/run/NetworkManager/dnsmasq.conf にDHCPから取得するDNSサーバーと、その下に 8.8.8.8 が追加されていればバッチリです。ただ、これらのファイルを直接見るよりも、スクリプト内でも使っている nmcli connection show で見たほうがやり方としては正しそうです。
1 2 3 4 |
$ nmcli connection show "System eth0" | grep "ipv4.dns:\|IP4.DNS" ipv4.dns: 8.8.8.8 IP4.DNS[1]: 10.0.0.2 IP4.DNS[2]: 8.8.8.8 |
dnsmasqのForward方式
複数のDNSサーバーを指定し、いざ、どのようにDNSキャッシュとして動作するかの確認です。各種設定については man も参照してください。dnsmasqの設定場所
/etc/NetworkManager/dnsmasq.d/ の下に設定を置くことでdnsmasqに反映されます。
1 2 |
listen-address=::1 cache-size=1024 |
この例では、IPv6でもリクエストを受けられるようにしたり、キャッシュサイズを変更しています。ipv6.conf と cache.conf とかに分けてもいいですが、少なかったので1つにしています。
デーモンのプロセスを見ると、–cache-size=400 となっていますが、/var/log/message で起動時に 1024 に上書きされているのが確認できます。
デフォルトの挙動
Forwardの方法について特に指定しない場合、/var/run/NetworkManager/dnsmasq.conf で指定された server がランダムに利用されます。厳密に言うと、名前解決が必要なタイミングで全てのサーバーにシステムコール sendto が実行され、有効なサーバーから1つ選択されます。通信できないサーバーがあっても、その分が遅延することはなくスムーズにレスポンスが返るため、耐障害性的には上々といえます。
ただ、本来優先して使うべきDHCPからもらうDNSサーバーではない、8.8.8.8 も同じ割合で利用するので、十分ですがベストと言い切れないところではあります。
上から順に接続
strict-order という設定を入れると、resolv.conf のように上から順に試行し、ダメだったら次、という挙動になります。もし、DHCPのDNSサーバーがプライベートなDNSレコードを返す場合、普段は必要なレコードを全て取得でき、最悪の事態になってもWANの名前解決だけはできる、という予防線を張ることができます。また、単にDHCPのDNSサーバーを優先して利用したい、という場合にも使えます。
ただ、障害時には上から順に試行して1秒経過したら次、1秒経過したら次、という悪しき挙動は resolv.conf と同様になってしまうので、目的を達することは叶いません。そのため、これを採用するくらいなら、resolv.conf で簡単設定のまま運用した方がよさげです。
ひとつ検証の注意点として、server の障害演出として iptables で遮断しても、1秒の待機時間が発生しません。これは strace で見ているとわかるのですが、sendto は iptables が理由で通信できない場合 EPERM というエラーを受け取り、即座に諦めるからです。なので、無効なIPアドレスを指定するか、自前のテストDNSサーバーを指定して検証しなくては本当の挙動を知ることができません。
全てに接続
all-servers を設定すると、全てのサーバーに送って最初に返ってきた結果を使うので、耐障害性という意味ではこれが一番高いということになります。DNSキャッシュを適切に設定していれば、仮にコレにしたからといって激しく迷惑な行為になるわけではない(デフォの2~3倍のリクエストがいく)ので、選択は障害の程度に対するポリシーの問題となります。デフォルトならばIPアドレスが生きているけれどもレスポンスが遅い場合、名前解決が遅延する可能性があります。しかし、こちらならば1つ速ければOKとなります。難しいところですが、デフォルトで十分であり、それが十分でない現象に遭遇したら all-servers にする、というあたりが良い落とし所かもしれません。
DNSキャッシュ
設定の cache-size=N のレコード数分だけキャッシュしてくれます。なので、cache-size に収まり かつ TTL以内 ならば、Forward せず dnsmasq が結果を返して終わりになりますので、期待通りに動作してくれます。
耐障害性
resolv.conf と異なり、dnsmasq を使うと dnsmasqデーモンが必ず起動していなくてはいけません。よほどなことがない限り、この手のシステムはダウンしませんが、重要な部分なので挙動の理解は必要です。ここは非常によく出来ていて、dnsmasq が落ちたら NetworkManager が即座に起動してくれます。そして、NetworkManager が単体で落ちても dnsmasqデーモンは起動したままであり、NetworkManager自体は systemd が即座に起動し直してくれます。
systemd がー!までなっていたら、それはもう名前解決どころではないので、考えなくてもよいでしょう。dnsmasq を monit などで独自監視しなくてよいのは、とても嬉しいポイントです。これなら、resolv.conf の 127.0.0.1 の下に、さらに 8.8.8.8 を入れる予防は過剰と判断してよさげです。
理想の挙動
存在する挙動は、毎回上から順に試行して最低1秒待機が必要か、全てのForward先へ接続を行うか、の2種類です。unbound の forward-zone もお触り程度に試しましたが、やはり上から順の仕様でした。理想は、ダウンもしくはレスポンスが遅いサーバーに対して、最初の1回だけは数秒かかっても、2回目以降は選択対象から除外し、一定時間後に復帰判定するような仕組みです。イメージ的にはHAProxyなのですが、余計なプロキシを挟む複雑化や、あまりマイナーなDNSキャッシュを利用することは避けたかったので、今回 dnsmasq にしてみました。
クラウド環境や必要なDNSレコードなどによって、ベストな構成は異なるので参考程度ではありますが、新しいディストリビューションを追いつつ、という見方も含めれば十分な情報になったのではないかと思います。