Kubernetes の Nginx Ingress Controller が突然内部ドメインを解決しなくなった件

5 min read読了の目安(約5200字

この記事は LOCAL学生部アドベントカレンダー2020 12日目の記事です。OB ですが枠が開いているのでしれっと参加します。

ことの始まり

いま勤めている会社では内部で利用するために VPS を借りて、そこに Rancher を使って Kubernetes クラスタを建てています。はじめは何の問題もなく Rancher が用意してくれた Nginx Ingress Controller が動作していたのですが、最近 Rancher 2.5 系にアップデートしたあたりから大量にこのようなログが出力されるようになりました。

Nameserver limits were exceeded, some nameservers have been omitted, the applied nameserver line is: x.x.x.x y.y.y.y z.z.z.z

しかも、Ingress で設定しているドメインにアクセスしても504しか返ってこない状況に。ログを見ればクラスタ内部のドメインが解決できないと無限にログが吐き出されています。今度はあわてて Nginx Ingress Contoller が立ち上げている Nginx Pod の /etc/resolv.conf を見てみると、本来はクラスタ内部のドメインを解決するはずの設定が書かれているはずなのに、 nameserver が全て外向きのアドレスを解決するDNSに設定されているではありませんか。

resolv.confを表示するコマンド例
$ kubectl exec -n ingress-nginx nginx-ingress-controller-xxxxx -- cat /etc/resolv.conf
あるべき resolv.conf
nameserver 10.x.x.x
search ingress.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
実際の resolv.conf
nameserver x.x.x.x
nameserver y.y.y.y
nameserver z.z.z.z

さてこれはどうしたことかと調査に乗り出した、今回はそんなお話です。

dnsPolicy って何?

さて、何者かによって resolv.conf が強制的に上書きされているということは分かりましたが、何がそうさせているのかについてはこの時点で情報がありません。とりあえず数時間ググってみたところ Service / Pod に対して設定可能である dnsPolicy の値が影響していることがなんとなく分かってきました。さて、さっそくこの値がどういうふうに影響してくるか実験してみましょう。

公式ドキュメントによると設定できる値は以下の4種類あるようです。

  • Default
  • ClusterFirst
  • ClusterFirstWithHostNet
  • None

これらのうち、とりあえず関係なさそうな None を除いたもので、どんな結果が返ってくるか使い捨ての Pod で検証してみます。

Default の場合
$ kubectl run -i \
  --restart=Never \
  --rm test-${RANDOM} \
  --image=ubuntu \
  --overrides='{"kind":"Pod", "apiVersion":"v1", "spec": {"dnsPolicy":"Default"}}' -- cat /etc/resolv.conf

nameserver x.x.x.x
nameserver y.y.y.y
nameserver z.z.z.z
pod "test-7610" deleted
ClusterFirst の場合
$ kubectl run -i \
  --restart=Never \
  --rm test-${RANDOM} \
  --image=ubuntu \
  --overrides='{"kind":"Pod", "apiVersion":"v1", "spec": {"dnsPolicy":"ClusterFirst"}}' -- cat /etc/resolv.conf

nameserver 10.x.x.x
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
pod "test-20592" deleted
ClusterFirstWithHostNet の場合
$ kubectl run -i \
  --restart=Never \
  --rm test-${RANDOM} \
  --image=ubuntu \
  --overrides='{"kind":"Pod", "apiVersion":"v1", "spec": {"dnsPolicy":"ClusterFirstWithHostNet"}}' -- cat /etc/resolv.conf

nameserver 10.x.x.x
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
pod "test-402" deleted

なるほど、つまり少なくとも Nginx の Pod が "dnsPolicy":"ClusterFirst" の状態で起動していれば良さそうです。さて、さっそく Pod の定義を確認してみましょう。

Nginx の Pod 定義(抜粋)
dnsPolicy: ClusterFirst

おや、おかしいですね。すでに ClusterFirst に設定されています。この設定が有効になっていれば resolv.conf は内部の DNS に向いているはずです。でもそうなっていないということは別の箇所に要因があるはずです。ということで調査継続。

hostNetwork の罠

ここで Pod の定義をぼんやり眺めていると hostNetworktrue になっていることに気づきました。VPS 上に構築している Rancher は、インターネット側からアクセス可能にするために Nginx Ingress Controller で提供している Nginx Pod をクラスタ内部のネットワークではなくホスト側のネットワークに露出させる必要があります。どうもこの設定が何かしら絡んでいる可能性があるので、またもや一時的な Pod を作って実験をしてみることにしました。

ClusterFirst の場合
$ kubectl run -i \
  --restart=Never \
  --rm test-${RANDOM} \
  --image=ubuntu \
  --overrides='{"kind":"Pod", "apiVersion":"v1", "spec": {"dnsPolicy":"ClusterFirst", "hostNetwork": true}}' -- cat /etc/resolv.conf

nameserver x.x.x.x
nameserver y.y.y.y
nameserver z.z.z.z
pod "test-11579" deleted
ClusterFirstWithHostNet の場合
$ kubectl run -i \
  --restart=Never \
  --rm test-${RANDOM} \
  --image=ubuntu \
  --overrides='{"kind":"Pod", "apiVersion":"v1", "spec": {"dnsPolicy":"ClusterFirstWithHostNet", "hostNetwork": true}}' -- cat /etc/resolv.conf

nameserver 10.x.x.x
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
pod "test-7397" deleted

はい、ビンゴです。hostNetworktrue になっているとき、dnsPolicyClusterFirst に設定しても、ホスト側の DNS 設定が強制的に使われてしまいます。

ここで公式ドキュメントを目を皿にして読んでみると、このように書かれています。

For Pods running with hostNetwork, you should explicitly set its DNS policy "ClusterFirstWithHostNet".

つまり、ネットワーク的にはホストに露出させたくても、DNS は内部で解決させたいユースケースでは dnsPolicyClusterFirstWithHostNet に明示的に設定してあげる必要があるということです。

で、どこから設定を変えてあげれば?

さて、ここまで分かればあとは強制的に定義を書き換えてあげればOKです。ただし、Rancher の Nginx Ingress Contoller は Pod の定義を直接書き換えることはできません。

ingress-nginx 名前空間の DaemonSets に nginx-ingress-controller として定義されている方を書き換えてあげる必要があります(1敗)。ここの定義を書き換えると自動的に設定が反映された Pod が立ち上がってきます。

ここでようやく resolv.conf も期待通り、内部のドメインも無事に解決できて、外からも普通にアクセスできるようになりました。

「おわりに」という名の感想

記事上では割とあっさり解決まで導かれているように見えますが、ここまでたどり着くのに丸2日かけてしまいました。そもそも最初に表示されたログ(Nameserver limits were exceeded)から、実際の解決策(dnsPolicyの変更)が想像すらできなかったというのがかなり致命的だったと思います。やってみると分かるのですが「Nameserver limits were exceeded」でググっても大した情報が見つからなくて、そこで長いこと詰んでしまっていたという経緯があります。

hostNetwork の設定に気づいて、resolv.conf がどう変化するかという実験をしてみてからは割とスムーズにいけたと思いますが、あまり知見のない分野だったのでエスパー力が試されていた気がします。Kubernetes という比較的巨大なシステムで、どこか不具合が起きてもなかなかドンピシャな解決策を見つけるのは難しい、という経験を得られたというところが大きかったかなと思います。

というかそもそもデフォルトで ClusterFirstWithHostNet になっているべきだと思うのですがどうなんでしょうね。アップデート以前では不具合が起きてなかったのでそもそもどういう設定になっていたのか確認していなかったのが悔やまれます。

今回の件は環境依存の問題の可能性もあるのですが、もし同じ状況になっている方がいれば参考になると幸いです。それではまた。