Kubernetes特集
夜の港で、光の線がコンテナの山を結び、右奥の倉庫へ続いている

Kubernetes特集 | 第4章(Day 5 / 6)

Kubernetesのネットワークとストレージ
届く経路と消えないデータ

Service・DNS・Ingress/Gateway API・NetworkPolicy・PVC。
「繋がらない」「データが消えた」を、層に分けて確かめられるようにします。

📅 Day 5 | 10/23(金)公開

5日目です。昨日の第3章では、Deployment api の前にServiceを置き、ReadyなPodにだけ通信が届くところまで確かめました。ただ、そのときは同じNamespaceの中から http://api と呼んだだけです。今日は、別のNamespaceから名前で呼び、外からの入口の考え方を押さえ、NetworkPolicyで通信を絞ります。後半はストレージに移って、Podを消しても残るデータを作ります。

昨日の最後に lab は片付けたので、今日も作るところから始めます。Serviceの port と targetPort があやふやな方は、第3章の「Serviceとラベル選択」を先に読んでおくと、演習1が追いやすくなります。

この章の結論

💡 Kubernetesのネットワークは、Pod IP・Service・DNS名・外からの入口の4つの層に分けて考えます。Pod IPは作り直すと変わり、Serviceがそれを隠して変わらない名前を渡し、IngressやGateway APIが外からのHTTPの入口を表します。NetworkPolicyは許可する通信を狭めますが、効くかどうかはネットワークプラグイン(CNI)しだいです。ストレージでは、PVCは「欲しい」という要求、PVは実体、StorageClassは作り方の方針です。PVCを作っただけではデータは守れません。バックアップと、別の場所への復元テストまでが設計の範囲です。

この章で手に入れてほしいのは、「繋がらない」を1つの症状として扱わない目です。名前が引けないのか、転送先がないのか、ポートが違うのか、ポリシーに止められているのか。それぞれ見る場所が違います。ストレージも同じで、Bound と表示されても、アプリが書けるとは限りません。

✅ この章を読み終えるとできること

  • 別のNamespaceからServiceを名前で呼び、DNS名の組み立て方を説明できる
  • ClusterIP・NodePort・LoadBalancer・Ingress・Gateway APIの違いを図4-3・図4-4で説明できる
  • NetworkPolicyで「この Namespace からだけ許可」を書き、効いているかを確かめられる
  • emptyDirとPVCの違いを、Podを消して確かめられる。PVC・PV・StorageClassの関係を言える
  • 繋がらないときに、DNS → Service → EndpointSlice → Policy → アプリ の順に切り分けられる

今日つくる構成

今日はNamespaceを2つ使います。lab にサーバー側の api とストレージの演習用Pod、lab-front に呼び出す側の一時Podを置きます。最終的に図4-1の形になります。

Namespace: lab-front curl Pod 呼び出す側(使い捨て) Namespace: lab api.lab Service api ClusterIP port 80 → targetPort http Deployment api(replicas: 2) Pod :80 Pod :80 NetworkPolicy: lab-front からの TCP 80 だけ許可 Pod notes /cache = emptyDir /data = PVC PVC notes 1Gi PV StorageClassが作る PVはNamespaceに属さない(クラスター全体のリソース)

図4-1: 今日つくる構成。上半分がネットワーク、下半分がストレージ。

ファイル中身作る節
api.yamlDeployment と Service(第3章の簡略版)今日つくる構成
np-allow-front.yamlNetworkPolicyNetworkPolicyで通信を絞る
notes.yamlPVC と Pod(emptyDir と PVC の両方を持つ)Podの一時ファイルとPVC
np-front-egress.yaml・pvc-bad.yaml演習用わざと壊す演習

まずContextを確かめて、Namespaceを2つ作ります。

kubectl config current-context      # kind-xxx / minikube / docker-desktop などが出ればOK
kubectl create namespace lab
kubectl create namespace lab-front

サーバー側は、昨日の完成形からProbeと設定を省いた簡略版です。今日はネットワークの話に集中したいので、2台にしています。

# api.yaml ― 第3章の api を簡略化。Deployment と Service を1ファイルに
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: api
    spec:
      containers:
        - name: api
          image: nginx:stable
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: http
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app.kubernetes.io/name: api
  ports:
    - name: http
      port: 80
      targetPort: http
kubectl -n lab apply -f api.yaml
kubectl -n lab rollout status deployment/api
kubectl -n lab get svc,endpointslice,pod -o wide

⚠️ YAMLの中身は学習用です。 nginx:stable のような動き続けるタグ、requests/limitsのない定義は、本番ではそのまま使いません(第3章の注記を参照)。今日のコマンドは、kindでの動きを基準に書いています。minikubeやDocker Desktopで出力が違う箇所は、その場で注記します。

ネットワークの4つの層

Kubernetesの通信で迷う原因の多くは、いくつもの層を1つに見てしまうことです。図4-2のように、下から順に分けて考えます。

① Pod IP Podごとに1つ。作り直すと変わるので、設定に書かない ② Service(ClusterIP と EndpointSlice) 変わらない仮想IPと、いまの転送先の一覧 ③ DNS名 api.lab.svc.cluster.local。クラスター内のDNSが答える ④ 外からの入口 Ingress・Gateway API・LoadBalancer。クラスターの外と繋ぐ get ingress / gateway / httproute nslookup(Podの中から) get svc / endpointslice get pod -o wide この層で見るコマンド NetworkPolicyは①〜④を横切って効く

図4-2: ネットワークは4つの層に分けて見る。障害のときは、どの層まで届いているかを確かめる。

かなり雑に言うと、Pod IPは「今日の席番号」、Serviceは「部署の代表番号」、DNS名は「電話帳の名前」、外からの入口は「会社の受付」です。席替えがあっても代表番号は変わらず、電話帳から代表番号を引けば、いま座っている人に繋がります。

ServiceとDNS ― 別のNamespaceから名前で呼ぶ

Serviceを作ると、クラスター内のDNS(多くの環境ではCoreDNS)が名前を答えるようになります。名前は <Service名>.<Namespace>.svc.<クラスターのドメイン> の形で、クラスターのドメインは既定で cluster.local です。

呼び出す側書ける名前理由
同じNamespace(lab)apiPodの検索ドメインに lab.svc.cluster.local が入っている
別のNamespace(lab-front)api.lab検索ドメインの svc.cluster.local が補われる
どこからでもapi.lab.svc.cluster.local省略のない完全な名前(FQDN)

lab-front に使い捨てのPodを作って、確かめます。

# 名前を引く(DNSの層)
kubectl -n lab-front run dns --image=busybox:1.36 --rm -i --restart=Never -- \
  nslookup api.lab.svc.cluster.local

# 名前で呼ぶ(HTTPまで)。同じNamespaceではないので、api だけでは届かない
kubectl -n lab-front run curl --image=curlimages/curl:8.10.1 --rm -i --restart=Never -- \
  curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://api.lab
# 出力の例(IPは環境によって違う)
Name:	api.lab.svc.cluster.local
Address: 10.96.142.17
pod "dns" deleted

200
pod "curl" deleted

DNSが返したのは、Podではなく Service の ClusterIP(10.96.x.x のような番号)です。実際にどのPodへ届くかは、EndpointSliceが決めます。だから、名前が引けたのに繋がらないときは、次にEndpointSliceを見ます。

kubectl -n lab get endpointslice -l kubernetes.io/service-name=api
kubectl -n lab get pod -o wide        # EndpointSlice の IP と、Pod の IP が一致するか

古い入門記事では kubectl get endpoints を使っていますが、Endpoints APIは v1.33 で非推奨になりました。今後はEndpointSliceを見る習慣を付けておくと、表示の警告に戸惑いません。

個々のPodのIPをDNSで知りたい場合は、clusterIP: None のHeadless Serviceを使います。DNSはServiceの仮想IPではなく、ReadyなPodのIPを全部返します。第3章で触れたStatefulSetと組み合わせて、db-0.db のようにPodを1つずつ名前で呼ぶ用途です。通常のWeb APIでは、振り分けをServiceに任せるほうが扱いやすいです。

⚠️ クラスターのドメインは、変えられていることがあります。 cluster.local は既定値です。社内のクラスターで名前が引けないときは、Podの中の /etc/resolv.conf の search 行を見てください(kubectl -n lab-front run r --image=busybox:1.36 --rm -i --restart=Never -- cat /etc/resolv.conf)。

ClusterIP・NodePort・LoadBalancer

Serviceの type は、どこまで外に開くかを決めます。3つは別物というより、図4-3のように外側へ重ねていく関係です。

LoadBalancer クラウド側にロードバランサーを作る(作り方・料金はプロバイダーごと) NodePort 全Nodeの同じ番号(既定で30000〜32767)を開ける ClusterIP(既定) クラスターの中だけから届く仮想IP。今日の api はこれ 外からの HTTP は、この前に Ingress / Gateway を置くのが一般的 外側の種類は、内側の機能をすべて含む

図4-3: Serviceの種類は「どこまで外に開くか」。外側ほど、クラウドや費用の都合が増える。

typeどこから届くか主な用途気をつけること
ClusterIPクラスターの中だけアプリ間の通信、Ingress/Gatewayの転送先外から直接は届かない
NodePortNodeのIPと番号外部ロードバランサーからの転送、検証Nodeのファイアウォール。Nodeが入れ替わるとIPも変わる
LoadBalancer外部ロードバランサーのアドレスTCP/UDPをそのまま外に出すServiceごとに1台作られることが多く、台数と費用が増える。アノテーション・サブネット・IPの予約方法がプロバイダーごとに違う

学習環境で外から試したいだけなら、Serviceの種類を変えずに port-forward を使うのが手軽です。

kubectl -n lab port-forward svc/api 8080:80    # このターミナルは開いたまま
# 別のターミナルで
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080      # 200

port-forward は、kubectlがAPI Server経由でPodを1つ選んで直接つなぐ仕組みです。Serviceの振り分けやreadinessの確認にはならないので、動作確認用と割り切ってください。Ctrl+Cで止めます。

⚠️ kindでは、NodePortやLoadBalancerは手元から届かないことがあります。 kindのNodeはDockerコンテナなので、クラスター作成時にポートの対応(extraPortMappings)を書いていなければ、NodePortにブラウザから届きません。LoadBalancerは、クラウドでなければ EXTERNAL-IP が <pending> のままになります(minikubeは minikube tunnel、kindは別のツールで補えます)。本番で使うときは、その環境のドキュメントで作り方と費用を確かめてください。

IngressとGateway API

外からのHTTP/HTTPSを、ホスト名とパスでServiceへ振り分けるのが、IngressとGateway APIです。どちらもYAMLだけでは動きません。YAMLを読んで実際に通信を中継するプログラム(Ingress ControllerやGatewayの実装)が、クラスターに入っている必要があります。

Ingress Gateway API Ingress(1つのリソース) ホスト名・パス・TLS をまとめて書く 細かい設定は Controller ごとの アノテーションに頼る ingressClassName で Controller を指定 GatewayClass どの実装を使うか インフラ提供者 Gateway 入口のポート・TLS クラスター運用者 HTTPRoute ホスト名・パス → Service アプリ開発者 Service(ClusterIP)→ Pod どちらも、実際に通信を中継するプログラム(Controller・実装)がないと何も起きない

図4-4: Ingressは1枚にまとめて書く。Gateway APIは担当者ごとにリソースを分ける。

Ingressは長く使われてきたAPIで、今も使えます。ただし、ホスト名とパス以外の設定(タイムアウト、ボディサイズ、リダイレクトなど)は、Controllerごとのアノテーションで書くことが多く、Controllerを替えるとYAMLも書き直しになります。Gateway APIは、それを標準の項目として持ち、インフラ担当とアプリ担当の責任を分けられるように作られています。

📌 広く使われてきた Ingress NGINX(kubernetes/ingress-nginx)は、2026年3月で保守を終えました。 既存の環境はそのまま動きますが、新しいリリースやセキュリティ修正は出ません。Kubernetesプロジェクトは、Gateway APIか、別のIngress Controllerへの移行を勧めています。終了したのはこのControllerで、IngressというAPIが廃止されたわけではありません。いま新しく入口を作るなら、まずGateway APIに対応した実装を検討するのが順当です。

Gateway APIで書くと、次のような形になります。example-gateway と api.example.com は例です。

# httproute.yaml(例)― api.example.com/ への通信を Service api へ
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api
spec:
  parentRefs:
    - name: example-gateway            # 運用者が用意した Gateway の名前
      namespace: infra
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: api                    # Service の名前と port
          port: 80

今日はControllerを入れないので、適用はしません。代わりに、自分のクラスターに何が入っているかを確かめます。

kubectl get ingressclass                         # Ingress Controller が入っていれば一覧が出る
kubectl api-resources | grep gateway.networking  # Gateway API の定義(CRD)が入っているか

kindの初期状態では、どちらも何も出ません。この状態で上のHTTPRouteを適用すると、no matches for kind "HTTPRoute" というエラーになります。Gateway APIはKubernetes本体とは別に配布され、定義(CRD)と実装を後から入れる仕組みだからです。Ingressのほうは、定義は最初からあるので適用は成功します。ただしControllerがなければ ADDRESS が空のままで、どこにも繋がりません。「適用できた」と「通信が届く」は別、というのは第3章のProbeと同じ話です。

⚠️ 外部公開のYAMLは、環境しだいです。 DNSのゾーン、証明書の発行、公開サブネット、WAF、ファイアウォール、クラウドのIAMが準備されていなければ、適用に成功しても使えません。Gateway APIは実装ごとに対応している機能と版が違うので、使う実装の対応表で確かめてください。Gateway API自体は2026年6月にv1.6が出ています。

NetworkPolicyで通信を絞る

ここまでの状態では、クラスター内のどのPodからでも api に届きます。多くのクラスターでは、Pod同士の通信は初めから全部許可されているからです。NetworkPolicyは、Podに対して「この通信だけ許可する」を書くリソースです。

押さえておく決まりは3つです。

  • どのPolicyにも選ばれていないPodは、全部許可のまま
  • あるPodが Ingress(受信)のPolicyに1つでも選ばれると、そのPodへの受信は「どれかのPolicyで許可されたものだけ」になる
  • Policyは足し算。複数あれば、許可の和になる。「拒否」を書く欄はない
# np-allow-front.yaml ― api への受信を、lab-front の TCP 80 だけに絞る
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-allow-front
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: api      # 守る対象のPod
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: lab-front   # Namespaceに自動で付くラベル
      ports:
        - protocol: TCP
          port: 80                     # Serviceの番号ではなく、Pod側の番号

このYAMLを適用すると、裏で起きること

  1. API Serverが内容を検証し、etcdに保存する(ここまではCNIに関係なく成功する)
  2. 各Nodeのネットワークプラグイン(CNI)がPolicyを読み、通信を通す・落とす規則を作る
  3. CNIがNetworkPolicyに対応していなければ、2が起きない。エラーも出ない
kubectl -n lab apply -f np-allow-front.yaml

# lab-front から:許可されているので 200
kubectl -n lab-front run curl --image=curlimages/curl:8.10.1 --rm -i --restart=Never -- \
  curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://api.lab

# lab の中から:同じNamespaceでも、許可していないので届かない(5秒で打ち切り)
kubectl -n lab run curl --image=curlimages/curl:8.10.1 --rm -i --restart=Never -- \
  curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://api
# 出力の例(NetworkPolicyが効いている場合)
200
pod "curl" deleted

000
pod "curl" deleted
pod lab/curl terminated (Error)

000 は、HTTPの応答が返ってこなかったという意味です。拒否の応答が返るのではなく、パケットが捨てられてタイムアウトします。これがNetworkPolicyで止められたときの典型的な見え方で、「アプリが落ちている」と取り違えやすい点です。

⚠️ NetworkPolicyが効くかどうかは、CNIしだいです。 上の2つ目が 200 になったら、そのクラスターのCNIはNetworkPolicyを実装していません(YAMLは受理されるので、エラーでは気づけません)。最近のkindの既定CNI(kindnet)は対応していますが、古いkindでは効きません。minikubeは minikube start --cni=calico のように対応CNIで作り直します。Docker Desktop内蔵のKubernetesは、効かないことがあります。本番では、使っているCNIのドキュメントで対応を確かめてから設計してください。

本番に入れるときは、いきなり「全部拒否」にしません。DNS、監視、ログ収集、Namespaceをまたぐ必要な通信を先に洗い出し、許可のPolicyを用意してから絞ります。また、NetworkPolicyはアプリの認証の代わりにはなりません。侵入されたときに横へ広がるのを減らす、防御の1層です。

Podの一時ファイルとPVC

ここからストレージです。コンテナの中に書いたファイルは、コンテナを作り直すと消えます。emptyDir は、同じPodの中のコンテナで共有できる一時領域ですが、Podが消えれば一緒に消えます。Podより長くデータを残すには、PVC(PersistentVolumeClaim)を使います。

Pod emptyDir Podと一緒に消える volume: data claimName: notes PVC(要求) 1Gi・ReadWriteOnce storageClassName Bound PV(実体) ディスク・NFSなど Namespaceに属さない StorageClass(方針) 誰がどう作るか・消すときどうするか PVを自動で作る PodはPVCを名前で指すだけ。どのディスクかは知らない

図4-5: PVCは要求、PVは実体、StorageClassは作り方の方針。emptyDirはPodの一部。

まず、クラスターにどのStorageClassがあるかを見ます。

kubectl get storageclass
# 出力の例(kind)
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  2d
列意味
(default)PVCで storageClassName を省略したときに使われる
RECLAIMPOLICYPVCを消したとき、PVと中のデータを消す(Delete)か残す(Retain)か
VOLUMEBINDINGMODEWaitForFirstConsumer は、PVCを使うPodの置き場所が決まってからPVを作る。Immediate はすぐ作る
ALLOWVOLUMEEXPANSIONあとから容量を増やせるか

次に、PVCと、それを使うPodを作ります。Podには emptyDir とPVCの両方を付けて、違いを並べて確かめます。

# notes.yaml ― PVC と、それを使う Pod
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: notes
spec:
  accessModes: [ReadWriteOnce]         # 1つのNodeからだけマウントできる、という意味が多い
  resources:
    requests:
      storage: 1Gi
  # storageClassName を省略 → (default) の StorageClass を使う
---
apiVersion: v1
kind: Pod
metadata:
  name: notes
spec:
  terminationGracePeriodSeconds: 5     # sleep は停止の合図を無視するので、待ち時間を短く
  containers:
    - name: notes
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: cache
          mountPath: /cache
        - name: data
          mountPath: /data
  volumes:
    - name: cache
      emptyDir: {}                     # Podと一緒に消える
    - name: data
      persistentVolumeClaim:
        claimName: notes               # Podより長く残る

適用して、PVCとPVの状態を見ます。

kubectl -n lab apply -f notes.yaml kubectl -n lab get pvc notes kubectl get pv # PVは Namespace を付けずに見る

kindでは、Podが置かれるまでの数秒間、PVCが Pending と表示されることがあります。WaitForFirstConsumer なので、Podの置き場所が決まるまでPVを作らないからです。describe pvc のEventsに waiting for first consumer to be created before binding と出ていれば、正常な待ちです。Podが動き出すと Bound に変わり、get pv に pvc- で始まる名前のPVが1つ現れます。

では、両方に書いて、Podを消して、作り直します。

kubectl -n lab exec notes -- sh -c 'date > /data/hello.txt; date > /cache/hello.txt'
kubectl -n lab delete pod notes
kubectl -n lab apply -f notes.yaml     # PVCはそのまま。Podだけ作り直される
kubectl -n lab wait --for=condition=Ready pod/notes --timeout=60s
kubectl -n lab exec notes -- cat /data/hello.txt     # さっきの日時が残っている
kubectl -n lab exec notes -- cat /cache/hello.txt    # No such file or directory

Podを消しても、PVCを消していなければデータは残ります。逆に言うと、kubectl delete pvc notes を実行すると、RECLAIMPOLICY が Delete のStorageClassでは、PVと中身も一緒に消えます。本番では、大事なデータのStorageClassを Retain にするか、消す手順に承認を入れるかを決めておきます。

⚠️ StorageClassは環境ごとに違います。 minikubeは standard(k8s.io/minikube-hostpath)、Docker Desktopは hostpath が既定で、どちらも Immediate なのでPVCはすぐ Bound になります。クラウドでは、ディスクの種類、Zoneの制約(あるZoneのディスクは別のZoneのNodeに付かない)、暗号化、拡張、スナップショットの対応がStorageClassごとに違います。原稿にある fast-encrypted のような名前は例で、自分のクラスターにあるとは限りません(演習3)。

アクセスモードも、名前どおりに受け取らないようにします。ReadWriteOnce は「1つのNodeからだけ読み書きできる」という意味のことが多く、同じNodeの複数Podからは使えても、別のNodeのPodとは共有できません。複数のNodeで共有したいからと ReadWriteMany を選んでも、アプリが同時書き込みを想定していなければファイルが壊れます。

第3章で触れたStatefulSetは、volumeClaimTemplates でPodごとに専用のPVCを作ります。db-0 が作り直されても、同じ data-db-0 のPVCに繋がります。ただし、壊れたトランザクションを直したり、レプリカ間の整合をとったりするのはDB自身の仕事です。StatefulSetが保証するのは、名前とディスクの対応までです。

バックアップと復元テスト

「YAMLはGitにあるから大丈夫」は、半分しか正しくありません。YAMLから作り直せるのは定義だけで、PVCの中のデータ、外部のDNSや証明書、Secretの実際の値は戻りません。何を、どこへ、どの頻度で取るかを一覧にします。

対象取り方の例見落としやすい点
リソースの定義Git、GitOpsのリポジトリ手で kubectl edit した変更はGitにない
Secretの値外部のシークレット管理(第3章)平文のバックアップを作らない
PVCのデータボリュームスナップショット、アプリのバックアップ機能スナップショットは「ある瞬間のディスク」で、DBの整合までは保証しないことがある
外の依存DNSゾーン、証明書、ロードバランサーの設定クラスターを作り直しても戻らない

DBなら、スナップショットの前にDBのバックアップモードを使うか、DB自身のダンプ・ログを取ります。そのうえで、2つの数字を決めます。RPO(どの時点まで戻れればよいか)とRTO(何分で戻せればよいか)です。決めた数字は、実際に復元してみるまで当てになりません。

復元テストは、元のPVCに上書きせず、別のNamespaceに戻して、件数やスキーマを確かめます。保存先が元と同じ障害の範囲(同じZone、同じアカウント)にあれば、ディスクと一緒に失う可能性もあります。

kubectl api-resources | grep -i volumesnapshot   # スナップショットの定義が入っているか

kindの初期状態では何も出ません。VolumeSnapshot は、スナップショット用の定義(CRD)・コントローラー・対応したCSIドライバーがそろって初めて使えます。クラウドのマネージドKubernetesでは用意されていることが多いので、自分の環境で確かめてください。

📌 障害のときは、まず新しい書き込みを止めるか、読み取り専用に切り替えるかを判断します。急いで古いPVCやPVを消すと、復旧の材料まで失います。削除の手順と、誰が承認するかは、障害が起きる前に決めておきます。

わざと壊す演習

今日作った api・NetworkPolicy・notes を使って、3つ壊します。壊したら、図4-2のどの層で止まっているかを考えてから直してください。

演習1

ServiceのtargetPortを取り違える

Serviceの転送先を、nginxが待ち受けていない 8080 に変えます。アプリの設定を変えたのに、Serviceを直し忘れた、という状況です。先にNetworkPolicyを外しておきます。Policyが許可しているのは80番だけなので、残したままだと8080番への通信はPolicyに捨てられ、症状がタイムアウトに変わってしまうからです。

kubectl -n lab delete networkpolicy api-allow-front    # 演習1の間だけ外す
kubectl -n lab patch svc api --type=json \
  -p='[{"op":"replace","path":"/spec/ports/0/targetPort","value":8080}]'

kubectl -n lab-front run curl --image=curlimages/curl:8.10.1 --rm -i --restart=Never -- \
  curl -sS -m 5 http://api.lab

curl: (7) Failed to connect to api.lab port 80 ... Connection refused のような表示になります。名前は引けていて(DNSの層はOK)、接続を試みた結果、相手から断られています。NetworkPolicyでタイムアウトしたときの 000 とは、見え方が違います。

kubectl -n lab get endpointslice -l kubernetes.io/service-name=api
# PORTS が 8080、ENDPOINTS には Pod の IP が2つ。転送先はあるのに、番号が違う

Serviceの層で、番号を間違えています。EndpointSliceは空ではないので、第3章の「selectorの不一致(転送先が0)」とは別の症状です。

実際の障害では、ポートの取り違えとPolicyが重なって、タイムアウトに見えることもあります。その場合も、EndpointSliceの PORTS とPolicyの port を突き合わせれば、原因にたどり着けます。

kubectl -n lab apply -f api.yaml                # ファイルの定義(targetPort: http)に戻す
kubectl -n lab apply -f np-allow-front.yaml     # 外したPolicyを戻す
演習2

送信を絞ったら、名前が引けなくなった

lab-front から外へ出る通信(Egress)を、全部止めます。「呼び出し側からも、必要な相手にしか送らせない」つもりで、許可を書き忘れた状態です。

# np-front-egress.yaml ― lab-front の全Podの送信を止める(許可の欄なし)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: front-deny-egress
spec:
  podSelector: {}                      # Namespace内の全Pod
  policyTypes: [Egress]
kubectl -n lab-front apply -f np-front-egress.yaml
kubectl -n lab-front run curl --image=curlimages/curl:8.10.1 --rm -i --restart=Never -- \
  curl -sS -m 5 http://api.lab

curl: (6) Could not resolve host: api.lab になります(CNIがNetworkPolicyに対応している場合)。HTTPの前に、DNSへの問い合わせ(UDP/TCPの53番)が止められています。送信を絞るときに、いちばん忘れやすいのがDNSです。

DNSと api への送信だけを許可するPolicyを足します。Policyは足し算なので、さっきのPolicyは残したままで構いません。

# np-front-egress.yaml に追記する2つ目のPolicy
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: front-allow-dns-and-api
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:                              # クラスターのDNS(CoreDNS)へ
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    - to:                              # lab の api へ
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: lab
          podSelector:
            matchLabels:
              app.kubernetes.io/name: api
      ports:
        - protocol: TCP
          port: 80
kubectl -n lab-front apply -f np-front-egress.yaml
kubectl -n lab-front run curl --image=curlimages/curl:8.10.1 --rm -i --restart=Never -- \
  curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://api.lab      # 200

DNSのPodに付いているラベル(k8s-app: kube-dns)は、kindやminikubeでの例です。自分の環境では kubectl -n kube-system get pod --show-labels で確かめてください。

演習3

存在しないStorageClassを指定する

本番の手順書からPVCの定義をコピーしてきた、という状況です。

# pvc-bad.yaml ― このクラスターにない StorageClass を指定
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: uploads
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: fast-encrypted
  resources:
    requests:
      storage: 1Gi
kubectl -n lab apply -f pvc-bad.yaml      # 適用は成功する
kubectl -n lab get pvc uploads             # STATUS Pending のまま
kubectl -n lab describe pvc uploads        # Events を見る

Eventsに storageclass.storage.k8s.io "fast-encrypted" not found と出ます。API Serverは、参照先のStorageClassが存在するかを確かめずに受け付けます。第3章の演習2(存在しないConfigMap)と同じ形の失敗です。このPVCを使うPodを作ると、Podも Pending のまま置き場所が決まりません。

PVCの storageClassName は、作成後に書き換えられません。消してから作り直します。

kubectl -n lab delete pvc uploads
# pvc-bad.yaml の storageClassName の行を消す(既定を使う)か、get storageclass にある名前に直して
kubectl -n lab apply -f pvc-bad.yaml
kubectl -n lab get pvc uploads             # kind では、使うPodがないので Pending(WaitForFirstConsumer の正常な待ち)

直したあとも Pending なのは、kindの WaitForFirstConsumer のためです。Eventsの文言が「not found」から「waiting for first consumer」に変わったことで、原因が消えたと判断できます。表示だけでなく、理由の文言まで読むのがポイントです。

壊したら、メモを残す

第1章から使っている6項目です。今日は「最初に見たもの」に、図4-2のどの層かを添えてください。

# 演習メモの例
日時/Context : 2026-10-23 21:30 / kind-kind(Namespace: lab, lab-front)
症状         : lab-front から curl http://api.lab が Could not resolve host
最初に見たもの: エラーの種類(6 = 名前解決の失敗)→ ③DNSの層。get networkpolicy -n lab-front
原因         : front-deny-egress で送信を全部止め、DNS(53番)を許可していなかった
修正         : kube-dns への UDP/TCP 53 と、lab の api への TCP 80 を許可するPolicyを追加
再発防止     : Egressを絞るPolicyには、DNSの許可を必ずセットで書く。適用後に名前解決から確かめる

繋がらないときに見る順番

演習で見たとおり、エラーの見え方で、止まっている層の見当がつきます。図4-6の順に、送信元に近いほうから確かめます。

1. 名前が引けるか(③DNS) Could not resolve host → 名前の綴り・Namespace・EgressのPolicy 2. 転送先があるか(②Service) EndpointSlice が空 / ready=false → selector・readinessProbe(第3章) 3. ポートが合っているか(②Service) Connection refused → targetPort・アプリの待受ポート 4. Policyに止められていないか タイムアウト(000) → 送信元と送信先、両方のNetworkPolicy 5. 入口のルート(④外からの入口) 404 / ADDRESS が空 / 証明書エラー → Ingress・Gateway・Controllerのイベント 6. アプリのログ(①Pod) 5xx・アプリのエラー → kubectl logs・アプリの設定

図4-6: 送信元に近い層から順に見る。エラーの種類が、止まっている層の手がかりになる。

大事なのは、実際の送信元と同じ条件で試すことです。デバッグ用のPodを別のNamespaceに作ると、NetworkPolicyの条件が変わり、「自分のPodからは繋がるのに」という食い違いが起きます。演習で lab-front と lab の両方から試したのは、このためです。

# ネットワークの確認セット
kubectl -n lab get svc,endpointslice
kubectl -n lab describe svc api
kubectl get networkpolicy -A
# ストレージの確認セット
kubectl -n lab get pvc
kubectl get pv
kubectl -n lab describe pvc notes
kubectl -n lab describe pod notes          # Events にマウントの成否が出る

ストレージも、Bound を「アプリから使える」と読み替えません。Podのイベントでマウントの成功を確かめ、コンテナの中で書き込み、再起動後に読めるかまで試します。書けないときは、ファイルの権限(コンテナを動かすユーザー)、アクセスモード、NodeとディスクのZone、容量、CSIドライバーのイベントを順に見ます。

⚠️ デバッグ用のPodを作れない環境もあります。 本番では、イメージの取得元が制限されていたり、Podの作成権限がなかったりします。その場合は、既存の診断用ワークロード、Nodeの監視、クラウドのロードバランサーのログを使います。デバッグ用Podを許可する場合も、特権モードやホストネットワークを安易に付けず、終わったら消します。

設計レビューで確かめること

ネットワークは、利用者 → 外部ロードバランサー → Ingress/Gateway → Service → Pod → DB という経路を図にし、矢印ごとに次の項目を書きます。ストレージは、アプリが書くデータ、キャッシュ、バックアップ、監査ログを分けて書きます。

対象確かめること
通信経路誰が接続を始めるか、名前はどこで引くか、ポート、TLSをどこで終えるか、送信元IPを残すか
タイムアウトと再試行ロードバランサー・Gateway・アプリ・DBのタイムアウトが、利用者の待ち時間と矛盾しないか。再試行が相手の負荷を増やさないか
NetworkPolicyCNIが対応しているか。DNS・監視・ログの許可があるか。段階的に入れる計画か
ボリューム容量だけでなく、IOPS・スループット・Zoneの制約・拡張の可否・RECLAIMPOLICY
バックアップRPO・RTO、保存先が別の障害範囲にあるか、別のNamespaceへの復元を試したか、復元後の接続先の切り替え手順

どの項目も、200 が返った、Bound になった、だけで完了にしないためのものです。

片付け

kubectl delete namespace lab lab-front
kubectl get namespaces          # lab と lab-front が消えたことを確認
kubectl get pv                  # notes の PV も消えている(RECLAIMPOLICY が Delete のため)

Namespaceを消すと、中のPVCも消え、kindの standard では対応するPVと中身も消えます。PVは数秒遅れて消えることがあります。Retain のStorageClassを使った場合は、PVが Released で残るので、手で消してください。クラウドで同じことをすると、残ったディスクに料金がかかり続けます。今日は、ロードバランサーは作っていません。

まとめ

  • ネットワークは Pod IP・Service・DNS名・外からの入口の4層で見る。Pod IPは設定に書かない
  • 別のNamespaceからは api.lab。名前が引けても繋がらなければ、EndpointSliceとポートを見る
  • Serviceの種類は「どこまで外に開くか」。外からのHTTPは Ingress か Gateway API。どちらも実装が要る。Ingress NGINXは2026年3月に保守終了
  • NetworkPolicyは許可の足し算。効くかはCNIしだい。Egressを絞るならDNSを忘れない
  • PVCは要求、PVは実体、StorageClassは方針。Podを消してもPVCが残ればデータは残る
  • バックアップは復元して初めて意味がある。RPO・RTOを決め、別の場所へ戻して確かめる

🔖 明日10月24日(土)、最終章の第5章「本番運用」を公開します。 可用性、RBAC、イメージの管理、監視、オートスケール、コスト、アップグレード、障害対応、そしてマネージドKubernetesの選び方まで。本番に出す前のチェックリストも付けます。

🔖 特集トップをブックマークしておいてください

この特集は10月24日まで、毎日1ページずつ増えていきます。明日が最終日です。新しい章へのリンクは、すべて特集トップに追加されます。章ごとにブックマークしなくても、特集トップを1つ保存しておけば迷いません。

Windows Ctrl+DMac ⌘+Dスマホ 共有 → ブックマーク
𝕏 更新のお知らせを受け取る

毎日の公開は、X(@BusinessHub413)でもお知らせします。

公式資料

※本章の記述は2026年10月時点の公式ドキュメントと公式ブログに基づきます。イベントの文言、kubectl の出力形式、StorageClassの名前、NetworkPolicyへの対応は、Kubernetesのバージョンや学習環境(kind・minikube・Docker Desktop)によって異なることがあります。本章のコマンドは手順として整理したもので、すべての環境での動作を確認したものではありません。

Kubernetesの導入判断や設計についてご相談ください

「うちはKubernetesが要るのか」「ECSやCloud Runで足りるのか」から、
クラスター設計・運用体制づくりまで、30分の無料相談で承ります。

無料相談を予約する →