Certified Kubernetes Administrator (CKA) #20 Networking 3: CoreDNS、NetworkPolicy

#19 Networking 2 では Ingress で外部トラフィックを host と path で振り分けました。ところがクラスター内で Pod 同士は互いをどう見つけるのでしょうか。Pod IP は再起動のたびに変わるので IP を直接使えず、Service に付く IP すらマニフェストに埋め込むには不安定です。そこで Kubernetes は 名前で互いを見つけるクラスター内部 DNS を標準で提供します。

今回の記事の 2 つのテーマは、その名前解決を担う CoreDNS と、Pod 間のトラフィックを誰が誰に送れるかを統制する NetworkPolicy です。一方は「どう見つけるか」、もう一方は「誰と通信できるか」を扱います。どちらも試験の Services and Networking ドメインで頻出で、運用でも障害追跡の出発点になることが多い箇所なので、動作原理とデバッグの流れに重点を置きます。

CoreDNS: クラスターの名前解決器 #

CoreDNS は Kubernetes クラスターの デフォルト DNS サーバー です。kubeadm でインストールしたクラスターには kube-system ネームスペースに CoreDNS が Deployment として立っており、その前に kube-dns という名前の Service が付いて固定の ClusterIP を持ちます。Pod 内の /etc/resolv.conf はこの ClusterIP を nameserver として指すので、Pod で発生する名前解決リクエストはすべて CoreDNS へ流れます。

# CoreDNS Deployment と Pod の確認
k get deploy coredns -n kube-system
k get pods -n kube-system -l k8s-app=kube-dns

# CoreDNS の前段 Service (名前は kube-dns で維持される)
k get svc kube-dns -n kube-system

CoreDNS Pod が落ちていたり ClusterIP が変わったりすると、クラスター全体の名前解決が止まります。そのため「あるアプリが別のアプリにつながらない」という症状の第一の容疑者は常に DNS です。

Service と Pod の DNS 名 #

CoreDNS はクラスターオブジェクトに規則的な名前を付与します。最もよく使うのは Service の DNS 名です。

<service>.<namespace>.svc.cluster.local

たとえば default ネームスペースの web Service は web.default.svc.cluster.local として解決されます。同じネームスペース内では短く web だけでも届き、別のネームスペースの Service を呼ぶときは web.default のようにネームスペースを付けます。この短縮が可能なのは、Pod の resolv.conf に search ドメインが入っているからです。

名前の形意味
web同じネームスペースの web Service
web.defaultdefault ネームスペースの web Service
web.default.svc.cluster.localFQDN (完全な名前)。どこからでも同じく解決される

Pod にも DNS 名が付きますが形が異なります。Pod IP のドットをハイフンに変えた 10-244-1-5.default.pod.cluster.local という形で、実務で直接使うことはまれです。ただし Headless Service にひもづく StatefulSet Pod は <pod>.<service>.<namespace>.svc.cluster.local という安定した名前を得て、この名前で個別の Pod を指名できます。

Corefile: CoreDNS の設定 #

CoreDNS の動作は Corefile という設定で定義され、この Corefile は kube-system ネームスペースの coredns ConfigMap に入っています。設定を見たり直したりするときは、この ConfigMap を扱います。

# Corefile の内容を確認
k get configmap coredns -n kube-system -o yaml

ConfigMap 内の Corefile はおおよそ次の形です。

.:53 {
    errors
    health
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
    }
    prometheus :9153
    forward . /etc/resolv.conf       # クラスター外のドメインは上位 DNS へ
    cache 30
    loop
    reload
    loadbalance
}

核心となるブロックは 2 つです。kubernetes cluster.local ...cluster.local ドメインを Kubernetes API と連携して Service・Pod 名を解決するプラグインで、forward . /etc/resolv.conf はクラスター内部ドメインではない外部の名前 (例: google.com) をノードの上位 DNS へ渡す設定です。Corefile を直した後は CoreDNS が reload プラグインで自動的に反映しますが、即座に適用したい場合は CoreDNS Pod を再起動します。

# Corefile 変更後すぐ反映するには CoreDNS をロールアウト再起動
k rollout restart deploy coredns -n kube-system

DNS デバッグ #

試験でも運用でも「名前が解決されない」を切り分ける最も速い方法は、一時的な Pod を立てて直接照会してみることです。busybox イメージの nslookup が標準ツールです。

# 使い捨て Pod で Service 名を照会 (終わると自動削除)
k run -it --rm test --image=busybox:1.28 --restart=Never -- nslookup web

# FQDN で別のネームスペースの Service を照会
k run -it --rm test --image=busybox:1.28 --restart=Never -- \
  nslookup web.default.svc.cluster.local

busybox イメージはバージョンによって nslookup の動作が異なります。1.28 タグが DNS 照会で最もトラブルが少なく、デバッグの事実上の標準として使われます。

照会が失敗したら次の順で絞り込みます。

  • CoreDNS Pod の状態: k get pods -n kube-system -l k8s-app=kube-dns で Running か確認します。CrashLoop なら k logs で原因を見ます。
  • kube-dns Service: k get svc kube-dns -n kube-system で ClusterIP があるか、endpoints が空でないかを見ます。
  • Pod の resolv.conf: k exec で入って cat /etc/resolv.conf を確認します。nameserver が kube-dns ClusterIP を指している必要があります。
  • 名前そのもの: ネームスペースを抜かしたか FQDN のつづりが間違っている可能性があります。同じネームスペースでなければ <service>.<namespace> の形で呼びます。

DNS トラブルシューティングは #25 で証明書・RBAC と一緒に改めて総合します。

NetworkPolicy: Pod 間トラフィックの統制 #

デフォルト状態の Kubernetes クラスターでは すべての Pod が互いに自由に通信できます。ネームスペースが違っても、ラベルが違っても、IP さえ分かれば届きます。この全許可 (all-allow) 状態を狭める道具が NetworkPolicy です。

NetworkPolicy の動作方式には 1 つの核心ルールがあります。どの Pod にもポリシーが掛かっていなければ、その Pod はずっと全許可です。 ところが、ある Pod にポリシーが 1 つでも掛かった瞬間、その Pod は 明示的に許可したトラフィックだけを受け取るホワイトリスト方式 に変わります。つまり NetworkPolicy はブロックリストではなく許可リストです。

状態動作
Pod にポリシーなしすべてのインバウンド/アウトバウンド許可
Pod にポリシー 1 つ以上その方向 (ingress/egress) はポリシーに書いたものだけ許可、残りは遮断

NetworkPolicy の構造 #

核心となるフィールドは次のとおりです。

  • podSelector: このポリシーが適用される対象 Pod をラベルで選びます。空にすると ({}) ネームスペースのすべての Pod が対象です。
  • policyTypes: このポリシーが Ingress (入ってくるトラフィック)、Egress (出ていくトラフィック)、またはその両方を扱うかを宣言します。
  • ingress.from: 誰からのインバウンドを許可するか。podSelectornamespaceSelectoripBlock で出所を指定します。
  • egress.to: どこへのアウトバウンドを許可するか。同じく 3 つの selector で宛先を指定します。
  • ports: 許可するポートとプロトコルを狭めます。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: default
spec:
  podSelector:              # このポリシーの適用対象: app=api Pod
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:          # 同じネームスペースの app=frontend Pod だけ許可
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

上のポリシーは app=api Pod に適用され、同じネームスペースの app=frontend Pod が TCP 8080 で送るインバウンドだけ を許可します。それ以外のすべてのインバウンドは遮断されます。egress は policyTypes にないので手を付けず、ずっと全許可のままです。

from/to の selector 3 種 #

fromto 内の selector は出所・宛先を選ぶ方式が 3 つあります。この 3 つを正確に区別することが試験でよく差が付く箇所です。

selector意味
podSelector同じネームスペース内でラベルで Pod を選ぶ
namespaceSelectorラベルで選んだネームスペース (群) の Pod を選ぶ
ipBlockCIDR 範囲で IP を選ぶ (クラスター外の出所など)
  ingress:
  - from:
    - namespaceSelector:     # team=prod ラベルの付いたネームスペースから来るトラフィックを許可
        matchLabels:
          team: prod
    - ipBlock:               # 特定の CIDR から来るトラフィックを許可 (ただし 10.0.5.0/24 を除く)
        cidr: 10.0.0.0/16
        except:
        - 10.0.5.0/24

ここで微妙ですが試験に定番で出る落とし穴があります。podSelectornamespaceSelector1 つの from 項目の中に一緒に 置くと「そのネームスペース内のそのラベル Pod」という AND 条件になります。一方 別々の項目 として - 2 つに分けると「そのネームスペースのすべての Pod」または「そのラベル Pod」という OR 条件になります。

  ingress:
  - from:
    - namespaceSelector:     # AND: team=prod ネームスペースの app=frontend Pod だけ
        matchLabels:
          team: prod
      podSelector:           # (前の項目と同じ - の下、インデントでまとまる)
        matchLabels:
          app: frontend

- namespaceSelectorpodSelector の間に - がない点に注目します。ハイフン 1 つの有無が AND と OR を分けるので、マニフェストを書いた後は k describe networkpolicy で意図どおりにまとまったか必ず確認します。

default deny パターン #

運用で最もよく使うパターンは、まずネームスペース全体を遮断 したうえで、必要な通信だけ別のポリシーで開けてやる方式です。すべてをふさぐ default deny ポリシーは、podSelector を空にして ingress ルールを置かないことで作ります。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: default
spec:
  podSelector: {}            # ネームスペースのすべての Pod に適用
  policyTypes:
  - Ingress
  # ingress ルールがないので = インバウンド全遮断

podSelector: {} でネームスペースのすべての Pod を対象に取り、ingress 項目自体を置かないと、許可リストが空になりすべてのインバウンドがふさがれます。egress までふさぐには policyTypesEgress を追加して egress ルールを空にします。インバウンド・アウトバウンドを両方ふさぐ完全遮断は次のとおりです。

spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

このように default deny を敷いておくと、先の allow-frontend-to-api のような許可ポリシーを 1 つずつ足していき、必要な経路だけが開いたクラスター を作れます。NetworkPolicy は同じ Pod に複数掛かると 和集合 (OR) で動作するので、遮断ポリシーと許可ポリシーを一緒に置くと許可ポリシーが開けた経路は通ります。

default deny ネームスペースでは CoreDNS へ向かう DNS クエリ (UDP/TCP 53) もふさがれます。egress を遮断したなら、kube-system の kube-dns へ向かう 53 ポートの egress を別途許可しないと名前解決が動作しません。名前が解決されない症状に出会ったとき NetworkPolicy を疑う理由です。

CNI が対応してこそ動作する #

NetworkPolicy で最もよく見落とされる事実があります。NetworkPolicy オブジェクトを作っても、CNI プラグインがこれを施行 (enforce) しなければ何も起きません。 NetworkPolicy はルールの宣言にすぎず、実際にパケットをふさぐのは CNI です。

  • 施行する: Calico、Cilium、Weave Net、Antrea など
  • 施行しない: Flannel (デフォルト構成) のように NetworkPolicy をサポートしない CNI

つまり Flannel だけが入ったクラスターでは、NetworkPolicy をどれだけ正確に書いてもトラフィックはそのまま流れます。「ポリシーを作ったのに遮断されない」という症状の最初の確認点が CNI です。Pod ネットワーキングモデルと CNI の役割は #3 クラスターアーキテクチャ 2 で扱いました。

検証とトラブルシューティング #

NetworkPolicy を作った後は、実際にふさがれるか・通るかを一時的な Pod で確認します。

# ポリシーと適用対象・ルールの確認
k get networkpolicy
k describe networkpolicy allow-frontend-to-api

# 許可された出所ラベルを付けた Pod から接続試行 (通れば正常)
k run probe --image=busybox:1.28 --restart=Never --labels=app=frontend \
  -- wget -qO- --timeout=2 api:8080

# 許可されていない Pod から接続試行 (ふさがれば正常)
k run probe2 --image=busybox:1.28 --restart=Never \
  -- wget -qO- --timeout=2 api:8080

ふさがれる場合は wget がタイムアウトで落ち、通る場合は応答が返ってきます。意図と結果が食い違ったら次を見ます。

  • 対象 Pod のラベル: podSelector が選んだラベルが実際の対象 Pod に付いているか k get pods --show-labels で照合します。
  • 出所のラベル/ネームスペース: from の selector が実際の出所 Pod・ネームスペースのラベルと合っているか見ます。
  • AND vs OR: from 項目のハイフン構造が意図した条件か改めて確認します。
  • CNI: 遮断がまったく効かなければ CNI が NetworkPolicy を施行するか見ます。
  • DNS egress: egress をふさいだポリシーなら 53 ポートが開いているか確認します。

試験ポイント #

CKA の Services and Networking ドメイン (20%) では CoreDNS と NetworkPolicy が一緒に頻出します。次を手に覚えておきます。

  • Service の DNS 名は <service>.<namespace>.svc.cluster.local です。同じネームスペースは短い名前、別のネームスペースは <service>.<namespace> で呼びます。
  • DNS デバッグは k run -it --rm test --image=busybox:1.28 -- nslookup <名前> が標準です。CoreDNS は kube-system の Deployment で、設定は coredns ConfigMap の Corefile です。
  • NetworkPolicy は ホワイトリスト です。ポリシーが掛かった Pod は明示的に許可したものだけ受け取ります。ポリシーのない Pod は全許可です。
  • podSelector: {} + ルールなし = default deny。policyTypes で ingress/egress の方向を選びます。
  • from/topodSelectornamespaceSelectoripBlock を区別し、1 つの項目内の AND と別々の項目の OR を切り分けます。
  • NetworkPolicy は CNI が施行するときだけ 動作します。Flannel のデフォルト構成では無視されます。

NetworkPolicy は命令型で作るのが難しく、マニフェスト作成が事実上必須です。公式ドキュメントの例を素早くコピーしてラベルとポートだけ変えるのが試験場で最も速い方法です。クラスター内部通信と DNS のより広い文脈は K8s 中級 #7 で一緒に復習するとよいでしょう。

まとめ #

この記事で押さえたこと:

  • CoreDNS はクラスターのデフォルト DNS です。kube-system の Deployment として立っており、前段 Service の名前は kube-dns、Service は <service>.<namespace>.svc.cluster.local として解決されます。
  • CoreDNS の設定は coredns ConfigMap の Corefile です。kubernetes プラグインがクラスター名を、forward が外部の名前を処理します。
  • DNS デバッグは busybox 1.28 の nslookup を使い捨て Pod として立てて始めます。
  • NetworkPolicy はホワイトリスト です。ポリシーがなければ全許可、ポリシーが掛かればその方向は許可リストだけ通します。
  • podSelector/policyTypes/ingress.from/egress.to/namespaceSelector/ipBlock/ports でルールを組み、default deny でふさいだ後に必要な経路だけ開けます。
  • NetworkPolicy は CNI が施行してこそ 動作します。Flannel のデフォルト構成では効果がありません。

次へ: Helm と Kustomize #

ネットワーキング 3 編で Service・Ingress・DNS・NetworkPolicy までクラスターの通信レイヤーを整理しました。次はこのすべてのマニフェストを効率的に管理する道具です。

#21 Helm と Kustomize: マニフェスト管理 では、マニフェストをテンプレートと値で扱う Helm (チャート・リリース・helm install/upgrade) と、ベースにパッチを重ねて環境別の変形を作る Kustomize (kustomization.yaml・overlay) を運用の視点から比較しながら整理します。

X