こんにちは、モノタロウのコンテナ基盤グループの千葉(@anthisfan)です。
弊社では、長きにわたりVM上で運用されてきたアプリケーションをコンテナ化し、Kubernetes(以下k8s)への移行を進めています。
過去経緯からGKEとEKSを運用していますが、近年はEKSを利用する案件が増加傾向にあります。
本記事では、EKS で発生したちょっとしたトラブルの原因と対応について共有してみます。
まずは、前提知識として必要になる NodeLocalDNS について説明します。
(十分に詳しい方は、「3. 何が起きたか」へ進んでください)
1. NodeLocalDNSについて
この記事のタイトルにある「NodeLocalDNS」は、
CoreDNSの負荷を軽減するために使用されるDNSキャッシュサーバです。
CoreDNSの過負荷により名前解決に問題が生じるケースはよく経験されることかと思いますが、弊社でも同様の問題を経験しており、対策としてNodeLocalDNSを導入しています。
標準構成
まずは NodeLocalDNS 導入前の構成から説明します。
以下の図は EKS の標準構成における名前解決の導線を示しています。

各 Client Pod のリゾルバ設定には「172.20.0.10」がリクエスト先に指定されています。
このIPアドレスは、CoreDNS をバックエンドに持つ kube-dns サービスの IP です。
各 Pod は名前解決を行う際 kube-dns サービスエンドポイントを通して CoreDNS に名前解決リクエストを送信します。
NodeLocalDNS 導入後の構成
NodeLocalDNSを導入すると、構成が以下の図のように変わります。

NodeLocalDNS DaemonSet が各ノードに配置され、同ノード内に配置されている Client Pod のDNSリクエストを処理します。
Client Pod がクラスタ内ドメインについてDNSリクエストを行うと、同ノードの NodeLocalDNS がリクエストを受けた後に CoreDNS へ代理でリクエストを行い、その応答をClient に返すと同時にキャッシュとして保持します。
2. NodeLocalDNS導入前後のトラフィックの変化
NodeLocalDNS 導入前のトラフィックフロー
まず、NodeLocalDNS 導入前のトラフィックフローを以下の図に示します

非 Host Network (以降 Pod Network と表現します)内のClient Pod から出力されたDNSリクエストは、Pod Network でのルーティング処理後、Host Network へ流入します。その後、Host Network 環境下で改めてルーティング処理や NAT処理がなされ、CoreDNS に到達します。
Host Network 内に配置されたClient Pod を起点とした DNS リクエストについては、Host Network 上でルーティング処理・NAT処理がなされた後、CoreDNS に到達します。
NodeLocalDNS 導入後のトラフィックフロー
次に、NodeLocalDNS 導入後のトラフィックフローを以下の図に示します

NodeLocalDNS を導入すると、kube-dns サービスエンドポイント宛の通信について、Host Network での NAT 処理が省かれ、最終的に同ノード内の NodeLocalDNS にルーティングされるようになります。
NodeLocalDNS は、起動後に以下の処理を行うことでこれを実現しています。
- NodeLocalDNS の起動
- ダミーインターフェース(NIC)が設置され、kube-dns サービスIP (172.20.0.10) が設定される
- kube-dns サービスIP宛の通信をダミーNICにルーティングするルールが追加される
- kube-dns サービスIP:53 をあて先とするパケットの NAT 処理をバイパスするルールの登録
これにより、kube-dns サービス宛のパケットが、同一ホスト上で動作する NodeLocalDNS に到達するようになります。
3. 何が起きたか
前置きが長くなりましたが、ここまでの話を踏まえて私たちが直面したトラブルについて説明します。
ここからは、同じチームメンバーとして一緒にトラブルシュートを行うような気持ちで読んでいただくと面白いかもしれません。
弊社では、EKS EC2ワーカーノード上に配置された Client Pod のアプリケーションログを、同ノードに配置された FluentBit DaemonSet を利用して社内ログ基盤に転送しています。
また、kubelet API から情報を収集する関係上、FluentBit DaemonSet は Host Network 上に配置されています。
ある時、チームメンバーが FluentBit のエラーログにログ送信先エンドポイントに関する名前解決エラーが出ていることに気づきチームに報告しました。

このエラーは以下の特徴を持っていました。
- FluentBit のログ転送先ドメインに関する名前解決エラーが出ている
- 名前解決エラーが出ていない FluentBit Pod も少数存在している
- エラーの発生しているタイミングは、FluentBit DaemonSet の起動直後のように見える
(即ち、ノードのスケールアウトとほぼ同タイミングで発生しているように見える) - エラーはノード起動から数分内に自然解消されているように見える
- NodeLocalDNS では致命的なエラーを示すようなログの出力がない
いったい何が起きているのでしょうか。
4. 影響範囲の調査

本格的な原因調査を開始する前に、この事象が Pod Network に配置されている商用サービスPod にも影響を来していないかを確認したところ、本事象は Pod Network では再現せず、Host Network 環境に配置された Pod でのみ発生することが分かりました。
5. 原因の調査

原因を調査するべく NodeLocalDNS, FluentBit Pod のログを再度確認しましたが、FluentBit で DNSリクエストのタイムアウトを示す以上の情報を見つけることができませんでした。
そこで、名前解決エラー前後でどのようなDNSパケットが飛び交っているのかをパケットキャプチャにより確認することにしました。
パケットの調査
Host Network, Pod Network それぞれを起点とした DNS リクエストについて DNS のエラーが発生している期間と自然解消後の期間でキャプチャを行い、パケットの送信元・送信先IPの差異に着目すると以下の結果を得ることができました。
| キャプチャタイミング | Host Network 起点のパケット | Pod Network 起点のパケット |
| FluentBit DNS エラー期間 |
送信元IP: kube-dns サービスIP (172.20.0.10)
送信先IP: |
送信元IP: Client Pod の IP
送信先IP: |
| 自然解消後 | 送信元IP: kube-dns サービスIP (172.20.0.10)
送信先IP: |
送信元IP: Client Pod の IP
送信先IP: |
この結果をかみ砕いていきましょう。
Host Network 起点のパケットについて
送信元IP が 172.20.0.10 (kube-dns サービスIP)である (赤字部分)
| キャプチャタイミング | Host Network 起点のパケット | Pod Network 起点のパケット |
| FluentBit DNS エラー期間 |
送信元IP: kube-dns サービスIP (172.20.0.10)
送信先IP: |
送信元IP: Client Pod の IP
送信先IP: |
| 自然解消後 | 送信元IP: kube-dns サービスIP (172.20.0.10)
送信先IP: |
送信元IP: Client Pod の IP
送信先IP: |
Host Network 起点のDNSリクエストの送信元IPは、キャプチャタイミングによらず、kube-dnsサービスIP(172.20.0.10)であることが確認できました。
これは、同ネットワークスペースに配置されている NodeLocalDNS がダミーインターフェースに割り当てた kube-dns サービス IP に起因します。
Linux では、発せられたパケットの送信元IPが明示されていない場合、送信元アドレスはルーティングテーブルの送信元アドレスヒントに基づいて決定されます。
具体的には、以下の図のようにして FluentBit が 172.20.0.10 を宛先とし、送信元を明示的に設定せずに通信を行うと、NodeLocalDNS のIP アドレス設定に伴い自動設定されたルーティングルールの送信元ヒントが作用し、送信元IPが 172.20.0.10 となります。

これはループバックインターフェイス lo に設定されている、127.0.0.1 にも同様のことが言えます。
ご興味があれば以下のコマンドをお手元で実行してみてください。
$ ip addr show dev lo 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever $ ip route show table local (snip) local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1 local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1 (snip) $ sudo tcpdump -i lo icmp -w icmp.pcap & $ ping -c 1 127.0.0.1 $ sudo pkill tcpdump $ sudo tcpdump -n -r icmp.pcap
送信元IPはどうなっていましたか?
Pod Network 起点のパケットについて
送信元IP がClient Pod の IP である (青字部分)
| キャプチャタイミング | Host Network 起点のパケット | Pod Network 起点のパケット |
| FluentBit DNS エラー期間 |
送信元IP: kube-dns サービスIP (172.20.0.10)
送信先IP: |
送信元IP: Client Pod の IP
送信先IP: |
| 自然解消後 | 送信元IP: kube-dns サービスIP (172.20.0.10)
送信先IP: |
送信元IP: Client Pod の IP
送信先IP: |
Pod Network 起点のDNSリクエストの送信元 IP は Client Pod に割り当てられている IP となっていました(青文字部分)
これは、各 Pod がそれぞれのネットワークネームスペースを起点にリクエストしているためです。
以下の図をご参照ください。

- Pod Network 上のClient Pod から名前解決リクエストが発行される (送信元IP指定なし)
- Pod Network 環境の送信元IPアドレスが Client Pod のIP (出力NICの eth0)にセットされる
- Pod Network から、Host Network にパケットが流れる
- Host Network 上でのルーティングが行われ、NodeLocalDNS にパケットが届く
ポイントとなるのは、2 です。
Pod Network は独立したネットワークネームスペースで、各 Pod 専用の NIC や、ルーティングテーブルが存在します。逆に言えば、Host Network 環境にあるNodeLocalDNS の設定したダミーインターフェイスや、それに関連するルーティングテーブル・ルールは、Pod Network からはみえません。
具体例としては以下のような設定が入っており、外部宛の通信を行うと 10.121.34.14 を送信元IPアドレスとしたパケットが 169.254.1.1 (これは、Host Network 宛です)に向けて eth0 から発出されます。
# ip route default via 169.254.1.1 dev eth0 169.254.1.1 dev eth0 scope link # ip link show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 3: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP mode DEFAULT group default link/ether 12:9f:d4:35:1f:13 brd ff:ff:ff:ff:ff:ff link-netnsid 0 # ip addr show dev eth0 3: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP group default link/ether 12:9f:d4:35:1f:13 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.121.46.60/32 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::109f:d4ff:fe35:1f13/64 scope link valid_lft forever preferred_lft forever
Pod Network 環境下の Pod のルーティングルールとNICの設定状況
宛先 IP アドレスの変化について (緑字部分)
| キャプチャタイミング | Host Network 起点のパケット | Pod Network 起点のパケット |
| FluentBit DNS エラー期間 |
送信元IP: kube-dns サービスIP (172.20.0.10)
送信先IP: |
送信元IP: Client Pod の IP
送信先IP: |
| 自然解消後 | 送信元IP: kube-dns サービスIP (172.20.0.10)
送信先IP: |
送信元IP: Client Pod の IP
送信先IP: |
Host Network, Pod Network のいずれの環境下でも、FluentBit のDNSエラー期間前後で宛先 IP アドレスが CoreDNS の IP から、kube-dns サービス IP へ変化している様子を確認できました。

つまり、エラー期間中は、本来バイパスされるべき NAT 設定が有効である可能性が高いことがわかります。この結果を受けて実際に iptables の設定を見ると、エラー期間中は確かに期待された設定が登録されていませんでした。
これにより、Host Network を起因としたDNSリクエストでは 172.20.0.10 を送信元、CoreDNS Pod のIPをあて先としたパケットが生成されていました。172.20.0.10 は ClusterIP で、AWS VPC 環境でノードを跨いで通信する際に送信元IPとしては利用できないIPです。
結果として CoreDNS が他ノードに存在する場合はタイムアウト、偶然同ノードに CoreDNS が配置されている場合は疎通が取れるという状況が起きたのです。

なお、Pod Network においては、AWS VPC 上でも疎通の取れる Client Pod の IPを送信元とした通信が行われるため、NAT の有無に関わらず問題なく疎通が取れていました。
この事象がノード起動から「数分内に自然解消される」ということを鑑みると、NodeLocalDNS が何らかの理由でルール登録を遅延させている可能性が考えられます。
そこで、次に NodeLocalDNS が NAT バイパスのルールをどのように登録しているか、その実装を確認することにしました。
6. 実装からわかった遅延の原因と対応
ここまでの調査を踏まえて NodeLocalDNS の実装を確認したところ、定期的に NAT のバイパスルールの登録状況を確認し、設定がなければ登録を行うといった処理があり、デフォルトでは1分間隔となっていることがわかりました。 github.com
これは FluentBit が起動してから 1分後に NAT のバイパス処理が行われることを意味します。
また、この実装を見ていただくとわかるように、pidfile オプションを登録することでDNSキャッシュサーバの起動直前に NAT のバイパスルールを登録する動きをすることがわかりました。
弊社においてはこのオプションを導入することで Host Network 環境下の Pod において DNS タイムアウトエラーが発生しないよう暫定対処しています。
ただし、このオプションを付与すると NodeLocalDNS の DNS サーバがリッスンを開始する前に iptables による操作がなされる可能性もあるため、問題が生じた場合は pidfile オプションを利用するのではなく、Interval オプションで適当な遅延を挟んだり、DNS サーバが立ち上がったことを確認次第 setupNetworking() を呼び出すといった実装の変更に踏み込む必要があると考えています。
7. その他、試したことや気づいたことなど
様々な検証を行った中の一部を本記事に載せていますが、再現テスト中に Kind や、Kubeadm といったツールで構築されたクラスタで本事象が再現されないことにも気づきました。
AWS EKS(VPC-CNI)では、Pod が VPC 内の IP を利用する関係で他ノードへの通信を行う際に SNAT を行いません。Pod が ClusterIP を持つような Kind, Kubeadm(Flannel)では、Host Network 環境下の Pod から他ノードの CoreDNS への通信時に 適当な SNAT が行われ、疎通が取れるというオチでした。
8. 再現方法
お持ちの環境がこの問題に該当するかを確認する方法は、以下の手順で確認できます。
[1] hostNetwork: true, dnsPolicy: ClusterFirstWithHostNet の Pod を用意する
apiVersion: v1 kind: Pod metadata: name: dig-monotaro spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet containers: - name: dig image: busybox command: - /bin/sh - -c - | while true; do dig +timeout=1 +tries=1 www.monotaro.com sleep 1 done
[2] Pod のログをリアルタイムにチェック
$ kubectl logs -f dig-monotaro
[3] 別ターミナルで、NodeLocalDNS を公式手順まま導入する kubernetes.io
[4] 名前解決に関するエラーが出た場合は本記事同様に[3]のセットアップで利用した nodelocaldns.yaml を修正し、pidfile オプションを付与してみてください
@@ -143,7 +143,7 @@ requests: cpu: 25m memory: 5Mi - args: [ "-localip", "169.254.20.10,172.20.0.10", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ] + args: [ "-localip", "169.254.20.10,172.20.0.10", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream", "-pidfile", "/tmp/pidfile" ] securityContext: capabilities: add:
まとめ
本記事では、EKS 環境において NodeLocalDNS を導入した際に、Host Network 環境下の一部 Pod で一時的な DNS タイムアウトが発生するという事象について、その原因と対応を紹介しました。
ポイントを整理すると以下の通りです。
- NodeLocalDNS は kube-dns Service IP 宛の通信を同一ノード内で処理するために、ダミー NIC や iptables ルールを用いて NAT をバイパスする
- NodeLocalDNS 起動直後は、この NAT バイパス用のルールが未登録の時間帯が存在する
- EKS(VPC-CNI)環境では、Host Network 起点の通信で kube-dns Service IP を送信元としたパケットが生成されると、ノードを跨いだ通信が成立しない。その結果、Host Network 環境下の Pod でのみ、NodeLocalDNS 起動直後に DNS タイムアウトが発生する
- Pod Network 環境では送信元 IP が VPC で疎通可能な Pod IP となるため、本事象は発生しない
本件は、EKS + VPC-CNI + NodeLocalDNS + Host Network Pod という条件が揃った場合にのみ顕在化する、比較的気づきにくい問題でした。
暫定的な対処としては、NodeLocalDNS に -pidfile オプションを付与し、DNS サーバ起動前に NAT バイパスルールを登録させることで、Host Network 環境下の Pod における DNS タイムアウトを回避できました。
一方で、本質的には NodeLocalDNS 側の初期化順序やルール登録タイミングに依存した挙動であり、環境や要件によっては別のアプローチ(Interval の調整や実装改善)が必要になる可能性もあります。
NodeLocalDNS は「入れれば安心」というコンポーネントではなく、Host Network 環境に対してどのようなルーティングや NAT の副作用を持ち込むのかを理解した上で使う必要があることを、今回のトラブルを通して改めて認識しました。
同様に Host Network 環境下の Pod を運用している方や、EKS で NodeLocalDNS の導入を検討している方の参考になれば幸いです。
最後までお読みいただき、ありがとうございました。
MonotaRO では資材調達ネットワークを変革するために様々な改善に取り組んでいます。 また、「他者への敬意」「傾聴」「主体性」といった行動規範が日々のコミュニケーションや意思決定の基盤として大切にされており、それが風通しの良さや協力し合える環境につながっています。実際に多様な考えを尊重し合いながら、自分の意見やアイデアを発信できる文化が根付いています。
もしこのような価値観や働き方に興味があれば、ぜひ カジュアル面談 でざっくばらんにお話ししましょう。 (特に本記事にご興味のある方は「コンテナ基盤グループ」をご指名ください😉)