Kubernetesのキャッシュネームサーバとリゾルバ

2023年03月06日 月曜日


【この記事を書いた人】
田口 景介

社会人生活の半分をフリーランス、半分をIIJで過ごすエンジニア。元々はアプリケーション屋だったはずが、クラウドと出会ったばかりに半身をインフラ屋に売り渡す羽目に。現在はコンテナ技術に傾倒中だが語りだすと長いので割愛。タグをつけるならコンテナ、クラウド、ロードバイク、うどん。

「Kubernetesのキャッシュネームサーバとリゾルバ」のイメージ

以前、こんなブログを公開しました。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への理解が深まるはずです。

 

田口 景介

2023年03月06日 月曜日

社会人生活の半分をフリーランス、半分をIIJで過ごすエンジニア。元々はアプリケーション屋だったはずが、クラウドと出会ったばかりに半身をインフラ屋に売り渡す羽目に。現在はコンテナ技術に傾倒中だが語りだすと長いので割愛。タグをつけるならコンテナ、クラウド、ロードバイク、うどん。

Related
関連記事