Kubernetesのキャッシュネームサーバとリゾルバ
2023年03月06日 月曜日
CONTENTS
以前、こんなブログを公開しました。ServiceリソースとDNS、ロードバランサの関係を解説したものです。
このときはKubernetesが権威ネームサーバ(CoreDNS)をどのように使っているのか解説したので、今回はキャッシュネームサーバとリゾルバについて解説します。
本ブログと連動した解説動画も公開しています。こちらもぜひご覧ください。
Podに設定されるresolv.conf
Kubernetes上のワークロードがどのようにDNSで名前解決を行うのか知るために、/etc/resolv.confの設定から追いかけてみましょう。
まず、適当にnginxを起動してPodの中に設定される/etc/resolv.confを見てみます。このファイルはkubeletが指示を出してコンテナランタイムが生成するもので、コンテナイメージには含まれていませんし、ConfigMapのようにkubernetesがマウントしたものでもありません。
典型的には、以下のような内容になっているはずです。nameserverに設定されている10.96.0.10はお馴染みCoreDNSのものです。つまり、CoreDNSは以前解説した通り権威ネームサーバでありながら、キャッシュネームサーバでもあるわけです。これについては、後ほど解説します。
$ kubectl create deploy nginx --image=nginx:1.21 $ kubectl exec deploy/nginx -- cat /etc/resolv.conf search default.svc.cluster.local svc.cluster.local cluster.local example.com nameserver 10.96.0.10 options ndots:5
普段使っているLinux環境の設定に比べると、
- searchに指定されたサーチリストが長い
- オプョンndotsが指定されている
あたりが独特な設定になっているので、詳しく解説していきます。まずサーチリストですが、Linuxホストによくある設定と比較してみましょう。
$ cat /etc/resolv.conf search example.com namespace 172.16.0.53
サーチリストにexample.comがあるのは同じですが、その前に3つドメインが追加されていることがわかります。
- default.svc.cluster.local
- svc.cluster.local
- cluster.local
これらは主にServiceリソースと連動して利用されるドメインで、クラスタ内の通信に関してはFQDNを指定しなくてもすむようになっています。
例えば、ネームスペースdefaultにはServiceリソースkubernetsがデフォルトで用意されています。このサービスの先にはkube-apiserverがいて、Kubernetes APIへアクセスすることができます。
$ kubectl -n default get svc kubernetes NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3y19d
このサービスへアクセスする際、アクセス元のネームスペースによって指定すべき名前が異なりますが、どのネームスペースからでも確実にアクセスするためkuberenetes.defaultで参照するのが一般的です。
アクセス元 | アクセス先 | |
---|---|---|
ホストノード | kubernetes.default.svc.cluster.local | ホストノードからはFQDNでアクセス可能 |
namespace kube-systemのPod | kubernetes.default | ネームスペースが異なる場合は、Service名.ネームスペース名でアクセス可能 |
namespace defaultのPod | kubernetes | 同一ネームスペースのPod同士ならばService名だけでアクセス可能 |
ただ、サーチリストがこうなっているのは利便性のためだけではなく、もっと切実な理由があります。
サーチリストとndots
DNSの一般的な名前解決のルールとして、こんな風に覚えている方もいるかもしれません。
- 名前にピリオドが含まれていたら、FQDNとみなしてサーチリストを参照せずに名前解決を行う
- 名前にピリオドが含まれていなければ、サーチリストのドメインを末尾に連結して名前解決する
例えば、こんな具合です。
$ ping myhost # ピリオドが含まれていないため、myhost.example.comが名前解決される PING myhost.example.com (192.168.0.1) 56(84) バイトのデータ 64 バイト応答 送信元 myhost.example.com (192.168.0.1): icmp_seq=1 ttl=57 時間=12.4ミリ秒 $ ping www.iij.ad.jp # ピリオドが含まれているため、FQDNとみなしてサーチリストは使わずに名前解決される PING www.iij.ad.jp (202.232.2.180) 56(84) バイトのデータ 64 バイト応答 送信元 www.iij.ad.jp (202.232.2.180): icmp_seq=1 ttl=52 時間=12.3ミリ秒
ローカルネットワークのホスト名は名前だけで、インターネットのホスト名はFQDNで、自然に名前解決がうまくいく仕組みになっているわけです。
一方、Kubernetesの場合はちょっと工夫が必要です。まず、多くの環境ではServiceリソースを作成すると下記のようなFQDNでアクセスできるようにCoreDNSへAレコードが作成されます。
<Service名>.<Namespace名>.svc.cluster.local
ただし、常にこのとおりではありません。クラスタドメイン(末尾のcluster.localの部分)はクラスタ構築時に任意の名前にカスタマイズが可能なので、実際にはクラスタドメインに合わせて生成されます。つまり、クラスタドメインがcluster.localであると決めつけてアクセス先をFQDNで指定してしまうと、カスタマイズされたクラスタでは宛先が見つからなくなります。ですから、クラスタ内通信ではFQDNを使わず、<Service名>のみ、あるいは<Service名>.<Namespace名>でアクセスするのがセオリーです。そして、そのためにresolv.confには各クラスタのドメイン名に合わせたサーチリストが設定されているというわけです。
ところが、それだけではうまくいきません。前述の通りresolv.confがデフォルト設定のままではピリオドが一つ以上含まれていたらサーチリストが適用されないので、<Service名>.<Namespace名>がFQDNとみなされ、名前解決に失敗してしまいます。そこで、ndotsオプションを設定し、1個以上のピリオドが含まれていてもサーチリストが適用されるように設定する必要があるのです。ndotsオプションを指定すると、その値よりも小さな個数であればピリオドが含まれていてもサーチリストが適用されるようになります(デフォルト値は1です)。
ただ、その説明ではndotsの値は4で良さそうに思えますが、実際には5が設定されています。その理由はStatefulSetリソースとヘッドレスサービスで解説しているとおり、StatefulSetと対になって作成されるヘッドレスサービスではサブドメインが追加され、階層が一段深くなるからです。このケースも考慮すると、ndotsの値は5が正解ということになります。
<Pod名>.<Service名>.<Namespace名>.svc.cluster.local # StatefulSetが作成したPodごとにレコードが作られる
以上の解説でPodに設定されるresolv.confが理解できたと思います。
それにしても、こんな話をするたびに、DNSドメイン名もファイルシステムのパスと同じように、末尾にピリオドが指定されたらFQDN(≒絶対パス)、なければサーチリストを参照(≒相対パス)だったら混乱が少なかったのにと、愚痴らずにはいられませんね。歴史的経緯を考えると、そうはいかないのはわかりますが。
PodのDNSポリシー
クラスタ内の名前解決はこれで問題ありませんが、インターネットの名前解決を考えると場合によっては都合が悪いことがわかるでしょうか。ndotsオプションが5ということは、事実上あらゆる名前解決にサーチリストが適用されることを意味します。つまり、例えばwww.iij.ad.jpという弊社コーポレートサイトへアクセスする場合、
- www.iij.ad.jp.default.svc.cluster.local
- www.iij.ad.jp.svc.cluster.local
- www.iij.ad.jp.cluster.local
- www.iij.ad.jp.example.com
を試行し、すべてに失敗してようやくwww.iij.ad.jpの名前解決に成功するのです。なんという無駄。
実のところ、サーバサイドシステムでインターネットのFQDNを名前解決するシーンは思いのほか少ないですし、あったとしても同じホストへ繰り返しアクセスするだけならば、キャッシュにヒットするのでさほど問題にはなりません。しかし、例えばフォワードプロキシ(squid, envoy, etc)をKubernetes上で動かしたらどうでしょう。ブラウザに指定されるURLは多岐にわたりDNSキャッシュの効果は限定的ですし、高頻度にアクセスされるため、パフォーマンスへの影響は無視できないものとなるでしょう。
こうしたインターネットの名前解決を高頻度に行うPodには個別にDNSポリシーを設定し、resolv.confをカスタマイズすることをお勧めします。DNSポリシーに指定できるパラメータは数種類あるのですが、そのデフォルト設定は意外にもDefaultではなくClusterFirstです。ですから、インターネットの名前解決を優先したければDNSポリシーにDefaultを明示する必要があります(あるいはNoneを設定して、dnsConfigでndotsの値を小さくする)。
dnsPolicy | 用途 | resolv.conf |
---|---|---|
Default | インターネット等、クラスタ外の名前解決を優先する | ホストノードのresolv.confを引き継ぐ |
ClusterFirst | クラスタ内の名前解決を優先する | クラスタドメインをサーチリストに設定する |
None | プリセットによらずに設定 | dnsConfigで詳細設定 |
DNSポリシーをDefaultに変更すると、そのPodだけ以下のようなresolv.confが生成されるようになります。実際の設定はホストノードの設定に依存するので環境によりけりですが、サーチリストが短くなり、ネームサーバの参照先がCoreDNSではなくなっているのがわかるでしょう。
search example.com nameserver 172.16.0.53 nameserver 172.16.1.53
DNSポリシーを検討する機会はそれほどないかもしれませんが、例えばCoreDNSのPodにはDNSポリシーDefaultが設定されています。CoreDNS自身がCoreDNSへ問い合わせるわけにはいきませんから、ホストノードの設定を引き継ぐようになっているわけです。
$ kubectl -n kube-system get deploy coredns -ojsonpath='{.spec.template.spec.dnsPolicy}' Default
systemd-resolvedとDNSポリシー
DNSポリシーをDefaultに設定するとホストノードの設定を引き継ぐと解説しましたが、無条件でホストノードの/etc/resolv.confをPodにコピーすればよいわけではありません。最近のLinuxディストリビューションではsystemd-resolvedが導入されている場合があるからです。その場合、例えばresolv.confは以下のように設定されています。
nameserver 127.0.0.53 # systemd-resolvedのアドレスを指定 options edns0 trust-ad search example.com
つまり、各クライアントがリモートの参照用ネームサーバへ直接アクセスすることはなく、ローカルホスト上のsystemd-resolvedを利用します。そして、systemd-resolvedが代理で参照用ネームサーバへ中継し、レスポンスをキャッシュしつつクライアントへ返答します。最近のLinuxでは昔のようにviでresolv.confを編集することはなく、systemd-resolvedにリゾルバの設定をするように変わっているのです。
そのため、もし愚直にホストノードの/etc/resolv.confをPodに引き継いでしまうとnameserverの設定がローカルホスト(127.0.0.53)に向いてしまいます。PodのローカルホストはPod自身でしかないので、それではsystemd-resolvedにアクセスすることはできません。そこで、systemd-resolvedを使うホストでは、kubeletにsystemd-resolvedが生成するresolv.confのパス(例:/run/systemd/resolve/resolv.conf)を指定し、リモートのキャッシュネームサーバへ向くように設定するのがセオリーです。
$ # kubeletに設定された、コンテナに設定すべきresolv.confのパス $ grep resolvConf /var/lib/kubelet/config.yaml resolvConf: /run/systemd/resolve/resolv.conf
そうすれば、例えば、systemd-resolvedが以下のように設定されていた場合、
$ sudo resolvectl # systemd-resolvedの設定を確認する Current DNS Server: 172.16.0.53 DNS Servers: 172.16.0.53 172.16.1.53 DNS Domain: example.com
DNSポリシーにDefaultを設定したPodには下記の内容でresolv.confが設定されます。
search example.com nameserver 172.16.0.53 nameserver 172.16.1.53
もっとも、kubeadmのようなツールを使えばクラスタ構築時にsystemd-resolvedの有無が検知され、自動的に適切なresolv.confのパスがkubeletへ設定されるので、新規クラスタで困ることはあまりないと思います。それにしても、せっかく各ノードにキャッシュ付リゾルバがいるのに、活用されないのはちょっともったいない気もしますね。
キャッシュネームサーバとしてのCoreDNS
最後に、キャッシュネームサーバとしてのCoreDNSを見てみましょう。デフォルトのまま利用していれば、CoreDNSは以下のように設定されているはずです。
.:53 { errors health { lameduck 5s } ready kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa ttl 30 } prometheus :9153 forward . /etc/resolv.conf { max_concurrent 1000 } cache 30 loop reload loadbalance }
ここで注目すべきはforward行(forward . /etc/resolv.conf)です。DNSの世界でピリオドはルートを意味しますから、cluster.localにマッチしなかった名前解決は、すべて/etc/resolv.confの設定に従いフォワードされる設定になっています。そして、CoreDNSのresolv.confはホストノードの/etc/resolv.confを引き継いでいますから、corednsがデプロイされたワーカーノードの/etc/resolv.confに従うということです。そして、そのレスポンスはcache行(cache 30)の指示に従い、TTLを最大30秒に制限しつつキャッシュします。
このデフォルト設定が常に最適というわけではないので、大規模なクラスタを設計する際には検討すべきコンフィグレーションの一つです。
例えば、キャッシュネームサーバとしてのパフォーマンスを99パーセンタイルのレスポンスタイムで監視し、一定以上の値になっていたら見直しを検討してもよいでしょう。
histogram_quantile(0.99, sum(rate(coredns_forward_request_duration_seconds_bucket{job="coredns"}[5m])) by(to, le))
Kubernetesクラスタを運用するうえで最低限理解しておくべきキャッシュネームサーバとリゾルバの解説は以上です。Kubernetesを利用するには周辺システムへの理解が欠かせず、DNSはそのなかでも重要なコンポーネントの一つです。本ブログではKubernetesからみたDNSについて解説しましたが、もし興味がわいたらDNSそのものにも目を向けてみてください。結果的にKubernetesへの理解が深まるはずです。