😇

Amazon EKS に Node Local DNS Cache を導入した際にハマった話

2022/10/18に公開

概要

DMM.com で SRE エンジニアをしている中井です。
DMM プラットフォームでは Amazon EKS をシングルクラスタ・マルチテナントで運用しているのですが、そのクラスタに Node Local DNS Cache を導入しようという話になりました。
本記事では 2022 年 10 月現在における、Node Local DNS Cache を EKS に導入するための手順を紹介したいと思います。
また、Node Local DNS Cache 導入後に、Security Group Policy を付与した Pod が通信ができないという現象が発生し、この現象の原因や解決策も併せて紹介できればと思います。
ちなみに、Google Cloud が提供する GKE での設定方法とは違い、GKE の場合は以下のドキュメントに記載されている通り、アドオンで追加できるようになっています。

https://cloud.google.com/kubernetes-engine/docs/how-to/nodelocal-dns-cache?hl=ja

EKS の場合は、GKE ほど手軽ではなく自ら k8s リソースを管理する必要があり、自分が導入するにあたって躓いた点等を参考にしてもらえればと思います!

Node Local DNS Cache とは?

詳細はこちらの k8s の公式ドキュメントに記載されている通りですが、この記事でも軽く触れておきます。

https://kubernetes.io/ja/docs/tasks/administer-cluster/nodelocaldns

Node Local DNS Cache は k8s の kube-dns(CoreDNS) のキャッシュを DaemonSet として、各 Node に配置し、DNS パフォーマンスを向上させるものです。

ダミーインターフェースを追加し、kube-proxy によって管理されている iptables のルールを書き換えることで、kube-dns(CoreDNS)へのリクエストを Node Local DNS Cache に転送し、キャッシュがあれば、名前解決結果を返します。なければ、kube-dns(CoreDNS)に問い合わせるといった挙動をします。

また、k8s のネットワークの問題にてしばしば話題に挙げる、 iptables の conntrack のステート管理メモリの容量が満杯になることで通信が不安定になる問題を解決してくれます。
従来では kube-dns(CoreDNS)への DNS クエリは UDP で行われていましたが、TCP にアップグレードすることにより、そもそもセッションを管理にする仕組みが必要なくなり、conntrack を使わずに済むというわけです。

導入手順と検証

動作検証環境

前提として、EKS の環境は以下のアドオンが導入されていることを想定しています。

  • EKS on EC2
  • AWS VPC CNI
  • Kube Proxy
    • iptables モードで動作
  • CoreDNS

導入手順

導入自体は非常に簡単で、公式ドキュメントにも記載されています。

https://kubernetes.io/ja/docs/tasks/administer-cluster/nodelocaldns/#設定

簡単に要約すると、kube-proxy がiptabelsモードで動作している場合、Github にあるnodelocaldns.yaml__PILLAR__DNS__SERVER____PILLAR__LOCAL__DNS__PILLAR__DNS__DOMAIN__を以下のように書き換えてApplyすれば、設定終了となります。

  • __PILLAR__DNS__SERVER__

    • 以下のコマンドにて kube-dns(CoreDNS)のサービスの IP アドレスを確認し、その値に書き換える
    • 自身の環境の場合は、172.20.0.10だったので後述の検証の部分でも、この値を使う
    ❯ kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}
    172.20.0.10%
    
  • __PILLAR__LOCAL__DNS__ : 169.254.20.10

    • デフォルト値
    • Pod 内の Node Local DNS Cache コンテナの IP アドレスに使用される
    • Cluster 内の既存 IP と衝突しなければ何でもいいと公式ドキュメントにも記載されている
  • __PILLAR__DNS__DOMAIN__ : cluster.local

    • デフォルト値
    • 基本的にクラスターのドメイン名を cluster.local で運用しているところが多いと思うので、ここは変更する必要はなさそう

動作検証

方法はいくつかあると思いますが、自分の場合は以下の 2 つで確認しました。

  1. Node Local DNS Cache を導入することで変更される設定を確認する
  2. 適当な Deployment を生成し、外部にリクエストが届くかを確認する

まず 1 つ目の方法について解説します。
前提として、Node Local DNS Cache を導入すると、前述の通り、iptables のルールが書きかわり、ダミーインターフェースが作成されます。
以下の実装を参考にすると、具体的にどのような挙動になっているかを把握することができます。

https://github.com/kubernetes/dns/blob/06504f2963798fbb54d65829dff9839c8c901fa3/cmd/node-cache/app/cache_app.go#L200-L234

まずはダミーインターフェースを確認してみましょう。
AWS であれば SSM で実行インスタンスに接続し、下記コマンドで確認できます。

sh-4.2$ ip addr
(省略)
7: nodelocaldns: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether 36:4c:13:db:7a:67 brd ff:ff:ff:ff:ff:ff
    inet 169.254.20.10/32 scope global nodelocaldns
       valid_lft forever preferred_lft forever
    inet 172.20.0.10/32 scope global nodelocaldns
       valid_lft forever preferred_lft forever

またダミーインターフェースの追加に伴い、インスタンス内の local route table に以下のエントリが作成されていることも確認できます。
(先に 2 で使う検証用の Deployment を Applyしています。)

sh-4.2$ ip rule
0:      from all lookup local
512:    from all to 10.0.76.255 lookup main # 検証用 Pod の IP アドレス
1024:   from all fwmark 0x80/0x80 lookup main
32766:  from all lookup main
32767:  from all lookup default

sh-4.2$ routel local
         target            gateway          source    proto    scope    dev tbl
(省略)
  169.254.20.10              local   169.254.20.10   kernel     hostnodelocaldns
    172.20.0.10              local     172.20.0.10   kernel     hostnodelocaldns

続いて、iptables の設定に関してですが、raw と filter のテーブルに以下の設定が追加されていれば問題ないです。こちらも SSM で同様に確認します。

sh-4.2$ sudo iptables -t raw -nL
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
CT         udp  --  0.0.0.0/0            172.20.0.10          udp dpt:53 NOTRACK
CT         tcp  --  0.0.0.0/0            172.20.0.10          tcp dpt:53 NOTRACK
CT         udp  --  0.0.0.0/0            169.254.20.10        udp dpt:53 NOTRACK
CT         tcp  --  0.0.0.0/0            169.254.20.10        tcp dpt:53 NOTRACK

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
CT         tcp  --  172.20.0.10          0.0.0.0/0            tcp spt:8080 NOTRACK
CT         tcp  --  0.0.0.0/0            172.20.0.10          tcp dpt:8080 NOTRACK
CT         udp  --  0.0.0.0/0            172.20.0.10          udp dpt:53 NOTRACK
CT         tcp  --  0.0.0.0/0            172.20.0.10          tcp dpt:53 NOTRACK
CT         udp  --  172.20.0.10          0.0.0.0/0            udp spt:53 NOTRACK
CT         tcp  --  172.20.0.10          0.0.0.0/0            tcp spt:53 NOTRACK
CT         tcp  --  169.254.20.10        0.0.0.0/0            tcp spt:8080 NOTRACK
CT         tcp  --  0.0.0.0/0            169.254.20.10        tcp dpt:8080 NOTRACK
CT         udp  --  0.0.0.0/0            169.254.20.10        udp dpt:53 NOTRACK
CT         tcp  --  0.0.0.0/0            169.254.20.10        tcp dpt:53 NOTRACK
CT         udp  --  169.254.20.10        0.0.0.0/0            udp spt:53 NOTRACK
CT         tcp  --  169.254.20.10        0.0.0.0/0            tcp spt:53 NOTRACK

sh-4.2$ sudo iptables -t filter -nL
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     udp  --  0.0.0.0/0            172.20.0.10          udp dpt:53
ACCEPT     tcp  --  0.0.0.0/0            172.20.0.10          tcp dpt:53
ACCEPT     udp  --  0.0.0.0/0            169.254.20.10        udp dpt:53
ACCEPT     tcp  --  0.0.0.0/0            169.254.20.10        tcp dpt:53

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     udp  --  172.20.0.10          0.0.0.0/0            udp spt:53
ACCEPT     tcp  --  172.20.0.10          0.0.0.0/0            tcp spt:53
ACCEPT     udp  --  169.254.20.10        0.0.0.0/0            udp spt:53
ACCEPT     tcp  --  169.254.20.10        0.0.0.0/0            tcp spt:53

続いて 2 つ目に関しては、 alpine にて、nslookupで外部にリクエストが届くかを確認してみました。
具体的には、google.com の IPアドレス を逆引きで取れるかを確認します。

/ $ nslookup google.com
Server:		172.20.0.10
Address:	172.20.0.10:53

Non-authoritative answer:
Name:	google.com
Address: 2404:6800:4004:826::200e

Non-authoritative answer:
Name:	google.com
Address: 142.251.42.206

無事に結果が表示されているので、これにて設定完了です。

Pod が Security Group Policy を利用している場合、Pod 内から通信ができなくなる

問題発覚

上記の検証結果から、無事に導入が完了したと思っていたのですが、導入した当日に EKS 上にアプリケーションをのせている開発チームから、「ある時刻に、アプリケーションのリリースしたところ、API の疎通ができなくなった」という旨の連絡を受け、明らかに Node Local DNS Cache を導入した直後の時刻だったため、すぐに Revert 対応を行い、なぜ API 疎通ができなくなったのかの原因究明を行いました。
その結果、API へのリクエスト自体は届いているのですが、そこから RDS 等のクラスタ外のリソースにアクセスができずに、API が正常に帰ってきていないということが判明いたしました。

原因

なぜ Pod 内から外部への通信ができなくなっているのかを調べていたところ、こちらの aws-vpc-cni に関する Issue に書かれていることが怪しそうということになりました。

https://github.com/aws/amazon-vpc-cni-k8s/issues/1384

報告してくださった開発チームのアプリケーションは AWS 上のネットワーク構成の都合で Security Group Policy という EC2 の Security Group をそのまま Pod にも適応できる設定を使用していました。これを設定した際の内部挙動として、新たに ENI が作成されるのと、その ENI に合わせて、route table の設定が変更されます。これが Node Local DNS を導入した際の設定とマッチせずに Pod から外部への通信ができなくなっていたことが原因でした。

この原因を詳細に説明します。
上記の Issue には、具体的に以下の点が原因であると記載されていました。

  1. adds NOTRACK iptables rules for clusterDNS IP as well as nodelocalDNS IP. This skips iptables DNAT and connection tracking (http://www.netfilter.org/documentation/HOWTO//netfilter-hacking-HOWTO-3.html#ss3.3 connection tracking is needed for NATs). This also avoids any potential race conditions while using Connection Tracking.
  2. nodelocaldns pods interfaces are setup using local routing table.

Since pods using security group will go out of branchENI(VLAN) device and doesn't know about local route table, packets will not reach nodeLocalDNS. Also, due to NOTRACK iptable rule, these pods won't be able to communicate with actual clusterDNS service IP as well.

先述の通り、Node Local DNS Cache は local route table を使い、Pod からの DNS クエリを Node Local DNS に向かせています。しかし、Security Group Policy を付与した Pod の場合、local route table を見ることができず、パケットが Node Local DNS Cache に到達ができないことと iptables に NOTRACK ルールを追加した結果、kube-dns(CoreDNS)とも通信ができなくなってしまいます。
実際に内部挙動を確認してみましょう。

まず、Security Group Policy を付与した Pod の場合、local route table を見ることができないことについてです。

前提として、Security Group Policy を付与した Pod には、新たに trunck eni と branch eni という特別な ENI がインスタンスに付与されます。
これに伴い、Security Group Policy を付与すると、ip rule の結果が以下のように変わります。
vlan.eth.2vlanc6461fc2633は trunk eni 及び brach eni になります。

sh-4.2$ ip rule
10:     from all iif vlan.eth.2 lookup 102
10:     from all iif vlanc6461fc2633 lookup 102
20:     from all lookup local
512:    from all to 10.0.42.112 lookup main
512:    from all to 10.0.42.114 lookup main
1024:   from all fwmark 0x80/0x80 lookup main
32766:  from all lookup main
32767:  from all lookup default

上記の結果から、Security Group Policy が付与された Pod は local route table の内容を把握せずに、 102 の route table を使って、通信を行なっていることがわかります。
さらに、102 の route table の設定を見てみましょう。
(target にある10.0.60.20は Security Group Policy を付与した適当な Pod の IP アドレスとなります。Security Group は VPC 作成時に作成されるデフォルトのものを使用しています。)

sh-4.2$ routel 102
target            gateway          source    proto    scope    dev tbl
default          10.0.32.1                                  vlan.eth.2
10.0.32.1                                                 linkvlan.eth.2
10.0.60.20                                                 linkvlanc6461fc2633

10.0.32.1 は Subnet Group のデフォルト GW です。(当たり前ですが Subnet によって、IPが変わります。)
この GW から kube-dns(CoreDNS)へのアクセス自体はできるのですが、iptables の NOTRACK ルールにより、名前解決結果のパケットが落ちてしまい、結果 Pod からリクエストが送れないという状況ができてしまいます。

解決策

この現象を解決するために、vpc-cni の環境変数であるPOD_SECURITY_GROUP_ENFORCING_MODEstandard に設定します。(デフォルトは strict
設定方法は以下のコマンドを打つだけです。

kubectl set env daemonset aws-node -n kube-system POD_SECURITY_GROUP_ENFORCING_MODE=standard

また、すでに Security Group Policy が付与されているアプリケーションがデプロイされている場合は、Pod の再起動(rolling update等)を行う必要があります。
ドキュメントにもこのことが記載されています。

https://github.com/aws/amazon-vpc-cni-k8s#pod_security_group_enforcing_mode-v1110

動作検証

上記のコマンドを実行し、先ほど使った検証用の Deployment に、Security Group Policy を付与して、再度 Apply をしました。
そして、ip rule コマンドで結果を確認したところ、外部へのリクエストは local route table を見て行うように変更されています。
この設定のおかげで、Node Local DNS Cache を使うことが可能になりました。

sh-4.2$ ip rule
20:     from all lookup local
512:    from all to 10.0.42.112 lookup main
512:    from all to 10.0.42.114 lookup main
512:    from all to 10.0.62.182 lookup main  # 検証用 Pod の IP アドレス
1024:   from all fwmark 0x80/0x80 lookup main
1536:   from 10.0.62.182 lookup 101 # 検証用 Pod の IP アドレス
32766:  from all lookup main
32767:  from all lookup default

また、検証用Podから外部にリクエストが届くかを確認しました。
これは先ほどと同様に nslookup にて、google の IPアドレスをとって来れるかを確認します。

/ $ nslookup google.com
Server:		172.20.0.10
Address:	172.20.0.10:53

Non-authoritative answer:
Name:	google.com
Address: 172.217.26.238

Non-authoritative answer:
Name:	google.com
Address: 2404:6800:4004:826::200e

問題なくとって来れていそうです。

まとめ

今回は Amazon EKS に Node Local DNS Cache の導入方法と検証方法及び Security Group Policy が付与された Pod が外部へ通信できなくなることの原因究明と解決策について述べました。

まとめると以下のような感じになります。

  1. Node Local DNS Cache の導入方法
    1. node-local-dns.yaml の設定値を書き換える
    2. 導入後、適当に Deployment を作成してそこからリクエストが届くかを確認する
  2. Security Group Policy が付与された Pod が外部へ通信できなくなることの解決策
    1. aws-vpc-cni の環境変数を書き換える
    2. 対象 Pod を再起動する

一応 AWS の公式ドキュメントに記載があるにもかかわらず、この解決策を見つけるまでにかなり時間がかかってしまった点が反省点ですが、タスクを完遂させる上で全くわかってなかったKubernetesのネットワーク関連の部分の解像度が上がったので、そこはよかったかなと思います。

Discussion