GKE 1.22 with Node Local DNS Cache で Alpine 3.13+ の名前解決に失敗する

先日、GKE の静的リリースに 1.22 がやってきました。
今回は、Node Local DNS Cache を有効化した GKE 1.22 において、Alpine を使用する際に注意しておいたほうが良い挙動とその検証結果をご紹介します。

以下の検証は、現時点の GKE 1.22 における最新の静的リリースである 1.22.6-gke.1000 を用いて行っています。

Node Local DNS Cache の設定

GKE の Node Local DNS Cache には、アップストリームに基づいた CoreDNS ベースのイメージが使われており、一応 ”-gke.” のようなサフィックスが付いていますがほぼ同じような挙動となっています。
設定は GKE 独自のものがコントロールプレーンのバージョンに応じて自動的に展開される仕組みとなっています。
ちなみに、GKE の Node Local DNS Cache は CoreDNS をベースとしている一方、GKE クラスタのデフォルト DNS は kube-dns となっています(最近は Cloud DNS を利用する選択肢も出てきていますね)。

cloud.google.com

github.com

GKE の Node Local DNS Cache の設定は、"kube-system" Namespace の "node-local-dns" という ConfigMap の "Corefile" の値として定義されています。
GKE 1.22 では、以下のように AAAA クエリ (IPv6) に関する設定が追加されました。

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.3", GitCommit:"816c97ab8cff8a1c72eccca1026f7820e93e0d25", GitTreeState:"clean", BuildDate:"2022-01-25T21:25:17Z", GoVersion:"go1.17.6", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.6-gke.1000", GitCommit:"5595443086b60d8c5c62342fadc2d4fda9c793e8", GitTreeState:"clean", BuildDate:"2022-02-02T09:35:41Z", GoVersion:"go1.16.12b7", Compiler:"gc", Platform:"linux/amd64"}

$ kubectl get pods -n kube-system
NAME                                                           READY   STATUS    RESTARTS   AGE
event-exporter-gke-5877b595cd-6cw96                            2/2     Running   0          7m
fluentbit-gke-lpvn6                                            2/2     Running   0          5m10s
fluentbit-gke-nsdwj                                            2/2     Running   0          5m9s
fluentbit-gke-z7l9p                                            2/2     Running   0          5m9s
gke-metrics-agent-bmb52                                        1/1     Running   0          5m10s
gke-metrics-agent-fq5gw                                        1/1     Running   0          5m9s
gke-metrics-agent-nrqvv                                        1/1     Running   0          5m9s
konnectivity-agent-7dbd649949-6vfcp                            1/1     Running   0          5m2s
konnectivity-agent-7dbd649949-h864p                            1/1     Running   0          5m2s
konnectivity-agent-7dbd649949-s7lx9                            1/1     Running   0          6m50s
konnectivity-agent-autoscaler-698b6d8768-gq2dx                 1/1     Running   0          6m46s
kube-dns-6bb46c7474-4vfqf                                      4/4     Running   0          7m14s
kube-dns-6bb46c7474-wlj7p                                      4/4     Running   0          5m1s
kube-dns-autoscaler-f4d55555-btzmw                             1/1     Running   0          7m12s
kube-proxy-gke-cluster-1-22-clone-default-pool-058fd934-0pvn   1/1     Running   0          4m18s
kube-proxy-gke-cluster-1-22-clone-default-pool-058fd934-g67w   1/1     Running   0          4m38s
kube-proxy-gke-cluster-1-22-clone-default-pool-058fd934-nbrj   1/1     Running   0          4m20s
l7-default-backend-69fb9fd9f9-vsqf2                            1/1     Running   0          6m42s
metrics-server-v0.4.5-bbb794dcc-87ttk                          2/2     Running   0          4m44s
node-local-dns-8f2mf                                           1/1     Running   0          5m7s
node-local-dns-sf8l6                                           1/1     Running   0          5m7s
node-local-dns-t7v9x                                           1/1     Running   0          5m7s
pdcsi-node-tpxh6                                               2/2     Running   0          5m8s
pdcsi-node-wzgr8                                               2/2     Running   0          5m9s
pdcsi-node-z4r68                                               2/2     Running   0          5m10s

$ kubectl describe configmaps node-local-dns -n kube-system
Name:         node-local-dns
Namespace:    kube-system
Labels:       addonmanager.kubernetes.io/mode=Reconcile
Annotations:  app.kubernetes.io/created-by: kube-addon-manager

Data
====
Corefile:
----
cluster.local:53 {
    errors

    template ANY AAAA {
      rcode NXDOMAIN
    }

    cache {
            success 9984 30
            denial 9984 5
    }
    reload
    loop

    bind 169.254.20.10 10.120.0.10
    health 169.254.20.10:8080

    forward . __PILLAR__CLUSTER__DNS__ {
            force_tcp
            expire 1s
    }
    prometheus :9253
    }
in-addr.arpa:53 {
    errors
    cache 30
    reload
    loop

    bind 169.254.20.10 10.120.0.10

    forward . __PILLAR__CLUSTER__DNS__ {
            force_tcp
            expire 1s
    }
    prometheus :9253
    }
ip6.arpa:53 {
    errors
    cache 30
    reload
    loop

    bind 169.254.20.10 10.120.0.10

    forward . __PILLAR__CLUSTER__DNS__ {
            force_tcp
            expire 1s
    }
    prometheus :9253
    }
.:53 {
    errors

    template ANY AAAA {
      rcode NXDOMAIN
    }

    cache 30
    reload
    loop

    bind 169.254.20.10 10.120.0.10

    forward . __PILLAR__UPSTREAM__SERVERS__ {
            force_tcp
    }
    prometheus :9253
    }


BinaryData
====

Events:  <none>

ひとつ前の GKE 1.21 (1.21.6-gke.1500) と比較してみるとわかりやすいかと思います。

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.3", GitCommit:"816c97ab8cff8a1c72eccca1026f7820e93e0d25", GitTreeState:"clean", BuildDate:"2022-01-25T21:25:17Z", GoVersion:"go1.17.6", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.6-gke.1500", GitCommit:"7ce0f9f1939dfc1aee910732e84cba03840df91e", GitTreeState:"clean", BuildDate:"2021-11-17T09:30:26Z", GoVersion:"go1.16.9b7", Compiler:"gc", Platform:"linux/amd64"}
WARNING: version difference between client (1.23) and server (1.21) exceeds the supported minor version skew of +/-1

$ kubectl get pods -n kube-system
NAME                                                     READY   STATUS    RESTARTS   AGE
event-exporter-gke-5479fd58c8-w889w                      2/2     Running   0          5d6h
fluentbit-gke-8zj4h                                      2/2     Running   0          5d6h
fluentbit-gke-dwwjk                                      2/2     Running   0          5d6h
fluentbit-gke-h2wp2                                      2/2     Running   0          5d6h
gke-metrics-agent-72h26                                  1/1     Running   0          5d6h
gke-metrics-agent-dxsht                                  1/1     Running   0          5d6h
gke-metrics-agent-v27lf                                  1/1     Running   0          5d6h
konnectivity-agent-5b9bf44468-kttgm                      1/1     Running   0          5d6h
konnectivity-agent-5b9bf44468-l74sx                      1/1     Running   0          5d6h
konnectivity-agent-5b9bf44468-tmmzs                      1/1     Running   0          5d6h
konnectivity-agent-autoscaler-5c49cb58bb-cdlk8           1/1     Running   0          5d6h
kube-dns-697dc8fc8b-8v5nl                                4/4     Running   0          5d6h
kube-dns-697dc8fc8b-rzgcl                                4/4     Running   0          5d6h
kube-dns-autoscaler-844c9d9448-v6gx2                     1/1     Running   0          5d6h
kube-proxy-gke-cluster-1-21-default-pool-2a39355d-ghz4   1/1     Running   0          5d6h
kube-proxy-gke-cluster-1-21-default-pool-2a39355d-hw8r   1/1     Running   0          5d6h
kube-proxy-gke-cluster-1-21-default-pool-2a39355d-xl2n   1/1     Running   0          5d6h
l7-default-backend-69fb9fd9f9-wq98k                      1/1     Running   0          5d6h
metrics-server-v0.4.4-857776bc9c-v5296                   2/2     Running   0          5d6h
node-local-dns-j2r7v                                     1/1     Running   0          5d6h
node-local-dns-pzn75                                     1/1     Running   0          5d6h
node-local-dns-vcwr2                                     1/1     Running   0          5d6h
pdcsi-node-5d99x                                         2/2     Running   0          5d6h
pdcsi-node-g47xg                                         2/2     Running   0          5d6h
pdcsi-node-n6btn                                         2/2     Running   0          5d6h

$ kubectl describe configmaps node-local-dns -n kube-system
Name:         node-local-dns
Namespace:    kube-system
Labels:       addonmanager.kubernetes.io/mode=Reconcile
Annotations:  <none>

Data
====
Corefile:
----
cluster.local:53 {
    errors
    cache {
            success 9984 30
            denial 9984 5
    }
    reload
    loop

    bind 169.254.20.10 10.36.16.10
    health 169.254.20.10:8080

    forward . __PILLAR__CLUSTER__DNS__ {
            force_tcp
            expire 1s
    }
    prometheus :9253
    }
in-addr.arpa:53 {
    errors
    cache 30
    reload
    loop

    bind 169.254.20.10 10.36.16.10

    forward . __PILLAR__CLUSTER__DNS__ {
            force_tcp
            expire 1s
    }
    prometheus :9253
    }
ip6.arpa:53 {
    errors
    cache 30
    reload
    loop

    bind 169.254.20.10 10.36.16.10

    forward . __PILLAR__CLUSTER__DNS__ {
            force_tcp
            expire 1s
    }
    prometheus :9253
    }
.:53 {
    errors
    cache 30
    reload
    loop

    bind 169.254.20.10 10.36.16.10

    forward . __PILLAR__UPSTREAM__SERVERS__ {
            force_tcp
    }
    prometheus :9253
    }


BinaryData
====

Events:  <none>

設定追加の契機

上述のように GKE 1.22 の Node Local DNS Cache に設定が追加された背景には、アップストリームにおけるプラグインの追加が関係していると思われます。
GKE 1.21 と GKE 1.22 のそれぞれにおける Node Local DNS Cache のバージョンは以下のとおりです。

  • GKE 1.21 : 1.21.1-gke.0
  • GKE 1.22 : 1.21.4-gke.0

以下の URL から、実際に GitHub でこのバージョン間の差分を確認することができます。

github.com

ふたつのバージョンの差はパッチバージョンレベルのみとなっていますが、この差分に設定追加のきっかけと思われるコミットが含まれています。
たどってみると、差分に含まれる以下のコミットで Node Local DNS Cache に dns64 プラグインが追加されていることがわかります。
このアップストリームの変更をきっかけに、GKE 1.22 の Node Local DNS Cache に AAAA クエリ (IPv6) の設定が追加された可能性が考えられそうです。

github.com

CoreDNS の dns64 プラグインの説明はこちらにあります。

coredns.io

変更の影響

今回の変更が何をもたらすのでしょうか。

タイトルのとおりですが、 Node Local DNS Cache を有効化した GKE 1.22 において、Alpine 3.13+ のコンテナが名前解決に失敗するようになる という影響が生じます。

DNS クライアントの実装によっては、A + AAAA ペアクエリの一方で NXDOMAIN が返却された場合にクエリ全体を失敗とみなすことがあり、Alpine 3.13+ が名前解決に使用している musl libc 1.2 系はまさにこのような挙動を取ります。
Node Local DNS Cache を有効化した GKE 1.22 (1.22.6-gke.1000) における実際の挙動は次のようになります。

$ kubectl run alpine313 --image=alpine:3.13 --tty -i sh
If you don't see a command prompt, try pressing enter.

/ # nslookup www.google.com
Server:         10.120.0.10
Address:        10.120.0.10:53

** server can't find www.google.com: NXDOMAIN

Non-authoritative answer:
Name:   www.google.com
Address: 142.250.148.99
Name:   www.google.com
Address: 142.250.148.103
Name:   www.google.com
Address: 142.250.148.106
Name:   www.google.com
Address: 142.250.148.147
Name:   www.google.com
Address: 142.250.148.104
Name:   www.google.com
Address: 142.250.148.105

/ # wget www.google.com
wget: bad address 'www.google.com'

/ #

nslookup の結果から、AAAA クエリに対して NXDOMAIN が返却されていることがわかります。
これは上述の Node Local DNS Cache に加えられた設定変更によるものです。
Node Local DNS Cache はこの設定により、AAAA クエリに対して無条件に NXDOMAIN を返却しています。

wget の結果から、名前解決が失敗し、"bad address" のエラーとなっていることがわかります。
詳細は後述しますが、このとき musl libc は A + AAAA ペアクエリの一方 (ここでは AAAA クエリ) で NXDOMAIN が返却されたことにより、もう一方 (A クエリ) が成功する状況であってもクエリ全体を失敗とみなし、名前解決に失敗しています。

ちなみに、GKE 1.21 (+ Node Local DNS Cache) 、または Node Local DNS Cache なしの GKE 1.22 であれば、Alpine 3.13+ のコンテナも名前解決に成功します。
以下に Node Local DNS Cache なしの GKE 1.22 の環境における実行結果を示します。

$ kubectl run alpine313 --image=alpine:3.13 --tty -i sh
If you don't see a command prompt, try pressing enter.

/ # nslookup www.google.com
Server:         10.93.0.10
Address:        10.93.0.10:53

Non-authoritative answer:
Name:   www.google.com
Address: 173.194.197.104
Name:   www.google.com
Address: 173.194.197.147
Name:   www.google.com
Address: 173.194.197.106
Name:   www.google.com
Address: 173.194.197.99
Name:   www.google.com
Address: 173.194.197.103
Name:   www.google.com
Address: 173.194.197.105

Non-authoritative answer:
Name:   www.google.com
Address: 2607:f8b0:4001:c5a::69
Name:   www.google.com
Address: 2607:f8b0:4001:c5a::67
Name:   www.google.com
Address: 2607:f8b0:4001:c5a::6a
Name:   www.google.com
Address: 2607:f8b0:4001:c5a::63

/ # wget www.google.com
Connecting to www.google.com (173.194.192.104:80)
saving to 'index.html'
index.html           100% |**************************************************************************************************************************| 14119  0:00:00 ETA
'index.html' saved

/ #

NXDOMAIN

上述の挙動の差を生んでいる NXDOMAIN について少し掘り下げてみましょう。
NXDOMAIN の定義を詳細化している RFC 8020 によれば、NXDOMAIN は「その名前にいかなる型のレコードも存在しない」ことを意味します。
アドレスファミリが AF_UNSPEC の場合、名前解決のために A クエリと AAAA クエリで計 2 回のクエリが発行されることになりますが、どちらか 1 回目のクエリで NXDOMAIN によってその名前に対するレコードの非存在を確認できれば、クエリの試行回数を最適化することができます。

www.rfc-editor.org

一方、「その名前に他の型のレコード、もしくはサブドメインが存在し得る」という場合には、NXDOMAIN ではなく NODATA を返却する必要があります(NODATA は疑似 RCODE であり、実際には NOERROR + 空の Answer セクションの返却となります)。
これは RFC 8020 の 3.1. Updates to RFC 1034 で定義されています。

This document clarifies possible ambiguities in [RFC1034] that did not clearly distinguish Empty Non-Terminal (ENT) names ([RFC7719]) from nonexistent names, and it refers to subsequent documents that do. ENTs are nodes in the DNS that do not have resource record sets associated with them but have descendant nodes that do. The correct response to ENTs is NODATA (i.e., a response code of NOERROR and an empty answer section). Additional clarifying language on these points is provided in Section 7.16 of [RFC2136] and in Sections 2.2.2 and 2.2.3 of [RFC4592].

musl libc の作者であり現在も主要メンテナである Rich Felker 氏の下記 Issue のコメントでも、この挙動とセマンティクスの対応関係が言及されています。

gitlab.alpinelinux.org

したがって、musl libc 1.2 系の「A + AAAA ペアクエリの一方が成功する (Answer セクションで有効な IP アドレスの回答が得られる) 状況であったとしても、もう一方で NXDOMAIN が返却された場合にクエリ全体を失敗とみなす挙動」は、特殊なものではなく、DNS のセマンティクスに沿った挙動であると言えそうです。
一方、他の DNS クライアントでは、ペアクエリの一方で NXDOMAIN が返却されたとしても、もう一方のクエリで有効な IP アドレスが得られればこれを名前解決の結果とする実装もあるようです。

ライブラリによって異なる名前解決の挙動

Alpine は、軽量な Linux ディストリビューションとして GKE などのコンテナ実行環境でよく使われています。
しかし、Alpine の元々の用途は組み込み系であり、BusyBox と musl libc をベースとしているため、他の Linux ディストリビューションの多くが採用している glibc などとは実装が異なります。
musl libc, glibc はそれぞれのライブラリにネットワークの共通的な機能を備えており、どちらのライブラリを採用しているかで名前解決の際の挙動が変わってくるということになります。

musl libc, glibc のいずれでも構成可能な、Gentoo Linux を使って動作を確認してみます。

まずは Gentoo (5.10.90+) + musl libc 1.2.2 (musl-1.2.2-r7) の場合です。

$ kubectl run gentoo-musl --image=gentoo/stage3:amd64-musl-20220224 --tty -i sh
If you don't see a command prompt, try pressing enter.

sh-5.1# emerge --info
!!! Section 'gentoo' in repos.conf has location attribute set to nonexistent directory: '/var/db/repos/gentoo'
!!! Invalid Repository Location (not a dir): '/var/db/repos/gentoo'
WARNING: One or more repositories have missing repo_name entries:

        /var/db/repos/gentoo/profiles/repo_name

NOTE: Each repo_name entry should be a plain text file containing a
unique name for the repository on the first line.


!!! It seems /run is not mounted. Process management may malfunction.
Portage 3.0.30 (python 3.9.9-final-0, unavailable, gcc-11.2.0, musl-1.2.2-r7, 5.10.90+ x86_64)
=================================================================
System uname: Linux-5.10.90+-x86_64-Intel-R-_Xeon-R-_CPU_@_2.20GHz-with-libc
KiB Mem:     4026068 total,    131520 free
KiB Swap:          0 total,         0 free
sh bash 5.1_p16
ld GNU ld (Gentoo 2.37_p1 p2) 2.37
dev-lang/python:          3.9.9-r1::gentoo, 3.10.0_p1-r1::gentoo
sys-devel/autoconf:       2.71-r1::gentoo
sys-devel/automake:       1.16.4::gentoo
sys-devel/binutils:       2.37_p1-r2::gentoo
sys-devel/libtool:        2.4.6-r6::gentoo
sys-kernel/linux-headers: 5.15-r3::gentoo (virtual/os-headers)
Repositories:

ACCEPT_LICENSE="* -@EULA"
CFLAGS="-O2 -pipe"
CHOST="x86_64-gentoo-linux-musl"
CONFIG_PROTECT="/etc /usr/share/gnupg/qualified.txt"
CONFIG_PROTECT_MASK="/etc/ca-certificates.conf /etc/env.d /etc/gentoo-release /etc/sandbox.d /etc/terminfo"
CXXFLAGS="-O2 -pipe"
DISTDIR="/var/cache/distfiles"
FEATURES="assume-digests binpkg-docompress binpkg-dostrip binpkg-logs buildpkg-live config-protect-if-modified distlocks ebuild-locks fixlafiles ipc-sandbox merge-sync multilib-strict network-sandbox news parallel-fetch pid-sandbox preserve-libs protect-owned qa-unresolved-soname-deps sandbox sfperms strict unknown-features-warn unmerge-logs unmerge-orphans userfetch userpriv usersandbox usersync xattr"
GENTOO_MIRRORS="http://distfiles.gentoo.org"
PKGDIR="/var/cache/binpkgs"
PORTAGE_TMPDIR="/var/tmp"
USE=""
Unset:  ACCEPT_KEYWORDS, EMERGE_DEFAULT_OPTS, ENV_UNSET, PORTAGE_BINHOST, PORTAGE_BUNZIP2_COMMAND

sh-5.1# wget www.google.com
--2022-02-28 00:10:30--  http://www.google.com/
Resolving www.google.com... failed: Name does not resolve.
wget: unable to resolve host address 'www.google.com'

sh-5.1# nslookup www.google.com
sh: nslookup: command not found

sh-5.1# emerge --sync
!!! It seems /run is not mounted. Process management may malfunction.
>>> Syncing repository 'gentoo' into '/var/db/repos/gentoo'...
 * Using keys from /usr/share/openpgp-keys/gentoo-release.asc
 * Refreshing keys via WKD ...                                                                                                                                     [ ok ]
!!! getaddrinfo failed for 'rsync.gentoo.org': [Errno -2] Name does not resolve
>>> Starting rsync with rsync://rsync.gentoo.org/gentoo-portage...
rsync: getaddrinfo: rsync.gentoo.org 873: Name does not resolve
rsync error: error in socket IO (code 10) at clientserver.c(137) [Receiver=3.2.3]
>>> Retrying...
!!! Exhausted addresses for rsync.gentoo.org

 * IMPORTANT: 12 news items need reading for repository 'gentoo'.
 * Use eselect news read to view new items.


Action: sync for repo: gentoo, returned code = 1


sh-5.1#

Alpine のときと同様、wget に失敗します。
なお、nslookup はデフォルトでは含まれていないため、Portage を使ってインストールする必要がありますが、名前解決に失敗するため emerge コマンドが機能しません。

次は Gentoo (5.10.90+) + glibc (glibc-2.33-r7) の場合です。

$ kubectl run gentoo-glibc --image=gentoo/stage3:amd64-systemd-20220224 --tty -i sh
If you don't see a command prompt, try pressing enter.

sh-5.1# emerge --info
!!! Section 'gentoo' in repos.conf has location attribute set to nonexistent directory: '/var/db/repos/gentoo'
!!! Invalid Repository Location (not a dir): '/var/db/repos/gentoo'
WARNING: One or more repositories have missing repo_name entries:

        /var/db/repos/gentoo/profiles/repo_name

NOTE: Each repo_name entry should be a plain text file containing a
unique name for the repository on the first line.


!!! It seems /run is not mounted. Process management may malfunction.
Portage 3.0.30 (python 3.9.9-final-0, unavailable, gcc-11.2.0, glibc-2.33-r7, 5.10.90+ x86_64)
=================================================================
System uname: Linux-5.10.90+-x86_64-Intel-R-_Xeon-R-_CPU_@_2.20GHz-with-glibc2.33
KiB Mem:     4026068 total,    128684 free
KiB Swap:          0 total,         0 free
sh bash 5.1_p16
ld GNU ld (Gentoo 2.37_p1 p2) 2.37
dev-lang/python:          3.9.9-r1::gentoo, 3.10.0_p1-r1::gentoo
sys-devel/autoconf:       2.71-r1::gentoo
sys-devel/automake:       1.16.4::gentoo
sys-devel/binutils:       2.37_p1-r2::gentoo
sys-devel/libtool:        2.4.6-r6::gentoo
sys-kernel/linux-headers: 5.15-r3::gentoo (virtual/os-headers)
Repositories:

ACCEPT_LICENSE="* -@EULA"
CFLAGS="-O2 -pipe"
CONFIG_PROTECT="/etc /usr/share/gnupg/qualified.txt"
CONFIG_PROTECT_MASK="/etc/ca-certificates.conf /etc/env.d /etc/gentoo-release /etc/sandbox.d /etc/terminfo"
CXXFLAGS="-O2 -pipe"
DISTDIR="/var/cache/distfiles"
FEATURES="assume-digests binpkg-docompress binpkg-dostrip binpkg-logs buildpkg-live config-protect-if-modified distlocks ebuild-locks fixlafiles ipc-sandbox merge-sync multilib-strict network-sandbox news parallel-fetch pid-sandbox preserve-libs protect-owned qa-unresolved-soname-deps sandbox sfperms strict unknown-features-warn unmerge-logs unmerge-orphans userfetch userpriv usersandbox usersync xattr"
GENTOO_MIRRORS="http://distfiles.gentoo.org"
PKGDIR="/var/cache/binpkgs"
PORTAGE_TMPDIR="/var/tmp"
USE=""
Unset:  ACCEPT_KEYWORDS, CHOST, EMERGE_DEFAULT_OPTS, ENV_UNSET, PORTAGE_BINHOST, PORTAGE_BUNZIP2_COMMAND

sh-5.1# wget www.google.com
--2022-02-28 00:17:16--  http://www.google.com/
Resolving www.google.com... 108.177.111.106, 108.177.111.103, 108.177.111.105, ...
Connecting to www.google.com|108.177.111.106|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: 'index.html'

index.html                                     [ <=>                                                                                   ]  13.91K  --.-KB/s    in 0s

2022-02-28 00:17:16 (36.6 MB/s) - 'index.html' saved [14243]

sh-5.1# emerge --sync
... (略) ...

sh-5.1# emerge net-dns/bind-tools -q
...

>>> Verifying ebuild manifests
>>> Emerging (1 of 1) net-dns/bind-tools-9.16.22::gentoo
>>> Installing (1 of 1) net-dns/bind-tools-9.16.22::gentoo
>>> Recording net-dns/bind-tools in "world" favorites file...

...

sh-5.1# nslookup www.google.com
Server:         10.120.0.10
Address:        10.120.0.10#53

Non-authoritative answer:
Name:   www.google.com
Address: 173.194.194.104
Name:   www.google.com
Address: 173.194.194.147
Name:   www.google.com
Address: 173.194.194.105
Name:   www.google.com
Address: 173.194.194.103
Name:   www.google.com
Address: 173.194.194.99
Name:   www.google.com
Address: 173.194.194.106
** server can't find www.google.com: NXDOMAIN

sh-5.1# 

こちらは wget に成功しています。
以上から、musl libc と glibc で名前解決に関する挙動が異なっていることがわかります。

バージョンが 3.12 までの Alpine であれば、GKE 1.22 + Node Local DNS Cache の環境であっても名前解決に成功します。
3.12 までの Alpine では musl libc 1.1 系が利用されており、1.2 系とは実装が異なるためです。
Alpine のリリースアナウンスでも 3.13 から musl libc 1.2 系 に変更されたことが記載されています。

alpinelinux.org

musl libc のコミットを追いかけてみると、1.2.1 で A + AAAA ペアクエリに対する挙動が変更されていることを確認できます。
この変更は、初版の 1.2.0 にはバックポートされなかったようです。

git.musl-libc.org

なお、musl libc では getaddrinfo のデフォルトのアドレスファミリが AF_UNSPEC となっているため、変更しない限りは A + AAAA のペアクエリになります。

git.musl-libc.org

回避手段

では GKE 1.22 において、Alpine 3.13+ のコンテナが名前解決を行えるようにするにはどうすればよいでしょうか。
以下のような手段が考えられます。

  1. クラスタの Node Local DNS Cache を無効化する
  2. 該当するコンテナの resolv.conf を書き換えて Node Local DNS Cache を迂回する
  3. Alpine の使用をやめる

いずれもクラスタやアプリケーションの構成変更が必要になるため、GKE ユーザとしては対処が悩ましいところですね。

1 はキャッシュが効かなくなるため性能面の影響が懸念されるでしょう。
2 は設定の変更対象が多いと大変ですし、その他の NW 要件との干渉で適切に迂回できない場合も考えられます。
3 はユーザアプリケーションなら検討の余地があるかもしれませんが、ミドルウェアのコンテナが Alpine 3.13+ を使用している場合には対処が困難です。

Node Local DNS Cache の Corefile の設定を書き換えることも可能ですが、kube-system Namespace 内のリソースはコントロールプレーンの一部として Google 側が管理・変更を行うため、手動で書き換えたとしても再び上書きされてしまう可能性があります。
GKE 1.22 の Node Local DNS Cache のデフォルト設定が、AAAA クエリに対して NODATA で応答する、もしくは kube-dns / Cloud DNS への再帰問い合わせを許可してくれるようになれば、根本的な解決となるかもしれません。

なお、GKE Autopilot では Node Local DNS Cache が強制的に有効化されるため、Autopilot 環境の Alpine ユーザは軒並みこの影響を受けることになると思われます。
こちらは resolv.conf で迂回するか、Alpine の使用をやめるくらいしか対応方法がなさそうですね。

まとめ

以下の相性問題により、GKE 1.22 で Node Local DNS Cache を有効化すると 3.13 以上の Alpine が名前解決に失敗することを確認しました。

  • GKE 1.22 の Node Local DNS Cache (1.21.4-gke.0) は AAAA に対して一律 NXDOMAIN を返す設定が追加されている
  • Alpine 3.13+ は musl libc 1.2.1+ を使用しており、A + AAAA ペアクエリのいずれかで NXDOMAIN が返るとクエリ全体が失敗したものとして扱われる

お読み頂いた方のお役に立てば幸いです。

参考