本記事には広告(アフィリエイトリンク)が含まれます。

Kubernetes実践編 #06

【Kubernetes実践編 #06】ネットワーク構築と結合テスト — 外部公開・通信制御・E2E検証

6.1 はじめに

6.1.1 前回の振り返り — 内部では動いている状態

前回(第5回)で、設計書のマニフェストを依存順序に従ってクラスタに適用し、TaskBoardの全コンポーネントが内部的に稼働する状態を作りました。現在のクラスタの状態を確認しておきましょう。

[app Namespace]
  Nginx Deployment (replicas: 2) + Service (ClusterIP, port: 8080)
  TaskBoard API Deployment (replicas: 2) + Service (ClusterIP, port: 8080)
  HPA (Nginx: CPU 70%, min:2/max:6)
  HPA (TaskBoard API: CPU 70%, min:2/max:4)
  PDB (Nginx: minAvailable: 1)
  PDB (TaskBoard API: minAvailable: 1)
  ConfigMap (nginx-config)

[db Namespace]
  MySQL StatefulSet (replicas: 1) + Headless Service (port: 3306) + PVC
  DB初期化Job (Completed)
  DBバックアップCronJob
  Secret (mysql-secret)

[monitoring Namespace]
  ログ収集DaemonSet(全Worker Node)

[未適用]
  Gateway API(Gateway, HTTPRoute)← 本回で適用
  NetworkPolicy ← 本回で適用

第5回の受け入れテストで、全Pod間の通信が正常であることを確認しています。kubectl exec やテスト用Podからの接続テストで、API Pod → MySQL、Nginx Pod → API Pod、さらにはNginx Pod → MySQLの通信まですべて通る状態です。「すべて通る」のは検証には便利ですが、本番運用としては問題があります。

6.1.2 本回の問題提起 — 「外部公開と通信制御」で構築を完成させる

現状のTaskBoardには2つの課題が残っています。

1つ目は、外部からアクセスする手段がないこと。クラスタ内部ではすべて動いていますが、ブラウザやcurlからTaskBoardにアクセスできません。Gateway APIを適用して外部公開する必要があります。

2つ目は、通信制御がされていないこと。Nginx PodからMySQL Podに直接アクセスできてしまう状態です。基本設計書(第2回)で定めた「API Pod以外からのDB直接アクセスを遮断する」方針が未実装です。NetworkPolicyで通信を制御する必要があります。

本回はこの2つの課題を解決し、構築フェーズを完了させます。

6.1.3 本回のゴールと成果物

本回のゴールは、TaskBoardを「設計書通りに完全稼働する」状態にすることです。

成果物は以下の2つです。

  • 外部公開 + 通信制御済みのTaskBoard完全稼働環境: Gateway API経由で外部アクセスが可能かつ、NetworkPolicyで設計書通りの通信制御が適用された状態
  • E2Eテスト結果報告: 外部アクセス、内部疎通、通信遮断、ヘルスチェックのすべてを検証した結果を表形式で記録

以下の3フェーズで進行します。

Phase A: Gateway APIで外部公開する
Phase B: NetworkPolicyで通信を制御する(破壊 → 復活)
Phase C: E2E結合テストで全経路を検証する

6.2 VMのNW構築とK8sのNW構築

6.2.1 VMの世界でのネットワーク構築 — LB設定→FW設定→結合テスト

VMwareの世界で3層構成のWebシステムを構築した経験を思い出してください。サーバー(VM)を構築してアプリケーションをインストールした後、ネットワーク設定は必ず「アプリの後」に行っていたはずです。

  • 第1段階: ロードバランサー設定 — F5 BIG-IPやNSXロードバランサーにVirtual Serverを作成し、Pool MemberにWebサーバーのIPを追加する。外部からのアクセスが可能になる
  • 第2段階: ファイアウォール設定 — NSX分散ファイアウォールやセキュリティグループでルールを設定し、Web→AP→DBの通信のみ許可する。Web→DBの直接通信を遮断する
  • 第3段階: 結合テスト — 外部からの全経路テスト(ブラウザアクセス、APIコール、DB接続確認)を実施し、ファイアウォールによる遮断が設計通りかも確認する

この「LB設定 → FW設定 → 結合テスト」の順序には理由があります。まずアプリが動く状態でLBを設定し、通信が正常に流れることを確認する。その後にFWルールを適用し、意図した通信のみが通ることを検証する。もしLBとFWを同時に設定すると、問題が発生した際にLBの問題なのかFWの問題なのか切り分けが困難になります。

6.2.2 K8sのネットワーク構築 — Gateway API→NetworkPolicy→E2Eテスト

K8sでもこの順序はまったく同じです。対応関係を整理しておきます。

VMの世界Kubernetes本回のPhase
ロードバランサー設定(F5 / NSX LB)Gateway API(Gateway + HTTPRoute)Phase A
ファイアウォール設定(NSX DFW)NetworkPolicy(デフォルト拒否 + ホワイトリスト)Phase B
結合テスト(全経路の疎通確認)E2Eテスト(外部アクセス + 内部疎通 + 遮断確認)Phase C

「まず外部公開して通信を確認し、次に通信制御を入れて設計通りか検証する」——VMでもK8sでも、この段階的アプローチがトラブル切り分けの基本です。第5回でNetworkPolicyなしの状態で全通信を確認済みなので、Phase Bで問題が発生すればNetworkPolicyが原因だとすぐに判断できます。

6.2.3 通信制御マトリクスを設計書から確認する

Phase BでNetworkPolicyを適用する前に、基本設計書(第2回)と詳細設計書(第3回)で定めた通信制御方針を通信制御マトリクスとして確認します。このマトリクスが本回の羅針盤です。Phase Bの適用判断とPhase Cの検証に繰り返し参照します。

送信元 \ 送信先Nginx PodAPI PodMySQL Pod
Gateway(外部)✅ 許可✅ 許可❌ 遮断
Nginx Pod❌ 遮断❌ 遮断
API Pod❌ 遮断✅ 許可
MySQL Pod❌ 遮断❌ 遮断
CronJob(バックアップ)❌ 遮断❌ 遮断✅ 許可
その他❌ 遮断❌ 遮断❌ 遮断

このマトリクスは「デフォルト拒否 + ホワイトリスト」の設計思想に基づいています。すべての通信をまず拒否し、必要な通信のみを個別に許可する。VMの世界のNSX分散ファイアウォールで「デフォルトDeny + 必要なルールのみAllow」を設定するのと同じアプローチです。

注目すべきは、Nginx PodからAPI Podへの直接通信が「遮断」になっている点です。TaskBoardのアーキテクチャでは、クライアントからのリクエストはGateway API経由で直接API Podに到達します。NginxはAPI Podへのリバースプロキシではなく、フロントエンドの静的コンテンツを配信する役割です。したがって、Nginx → APIの直接通信は不要であり、遮断して問題ありません。

6.3 Phase A — Gateway APIで外部公開する

6.3.1 設計書のGateway API方針を確認する

基本設計書(第2回)で定めたGateway APIの設計方針を確認します。

  • コントローラー: Envoy Gateway(第4回で導入済み)
  • Gateway: app Namespaceに配置、HTTP 80番ポートでリッスン
  • ルーティング: パスベース — /api → TaskBoard API Service(port: 8080)、/ → Nginx Service(port: 8080)
  • GatewayClass: eg(Envoy Gatewayが提供するGatewayClass)

マニフェストは応用編第5回で作成したものをベースに、実践編での変更点(Nginxのポートが非root化により80→8080に変更)を反映します。

6.3.2 Gatewayリソースを適用する

まず、Gatewayリソースを作成します。app Namespaceに配置し、HTTPの80番ポートでリッスンするリスナーを定義します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: taskboard-gateway
  namespace: app
spec:
  # Envoy GatewayのGatewayClassを使用
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          # 同じNamespace(app)内のHTTPRouteのみ許可
          from: Same
EOF

マニフェストを適用します。

[Execution User: developer]

kubectl apply -f ~/k8s-production/gateway.yaml
gateway.gateway.networking.k8s.io/taskboard-gateway created

Gatewayの状態を確認します。

[Execution User: developer]

kubectl get gateway -n app
NAME                CLASS   ADDRESS          PROGRAMMED   AGE
taskboard-gateway   eg      172.18.0.x       True         30s

PROGRAMMED: True が表示されていれば、Envoy Gatewayがこの設定を受け取り、データプレーン(Envoy Proxy)に反映しています。続いて、データプレーンPodが作成されていることを確認します。

[Execution User: developer]

kubectl get pods -n envoy-gateway-system
NAME                                                 READY   STATUS    RESTARTS   AGE
envoy-app-taskboard-gateway-xxxxxxxxxx-xxxxx         2/2     Running   0          30s
envoy-gateway-6f8b9d7c4f-xxxxx                       1/1     Running   0          10m

envoy-app-taskboard-gateway- で始まるPodがデータプレーンです。Gatewayリソースの適用により自動的に作成されました。

6.3.3 HTTPRouteを適用する

次に、パスベースルーティングを定義するHTTPRouteを作成します。/api で始まるリクエストはTaskBoard API Serviceへ、それ以外は Nginx Serviceへ振り分けます。

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/httproute-taskboard.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: taskboard-route
  namespace: app
spec:
  # このHTTPRouteをアタッチするGateway
  parentRefs:
    - name: taskboard-gateway
      namespace: app
  rules:
    # ルール1: /api で始まるパス → TaskBoard API Service
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: taskboard-api
          port: 8080
    # ルール2: / で始まるパス → Nginx Service(フォールバック)
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: nginx
          port: 8080
EOF

応用編第5回のHTTPRouteからの変更点が1つあります。Nginx Serviceのport80から8080になっています。これは応用編第7回でNginxを非root化した際にポートを80→8080に変更し、Serviceのportもそれに合わせて8080にしたためです。詳細設計書(第3回)のService定義と一致していることを確認してください。

[Execution User: developer]

kubectl apply -f ~/k8s-production/httproute-taskboard.yaml
httproute.gateway.networking.k8s.io/taskboard-route created

HTTPRouteの状態を確認します。

[Execution User: developer]

kubectl get httproute -n app
NAME              HOSTNAMES   AGE
taskboard-route               30s

6.3.4 外部からのアクセスを確認する

GatewayとHTTPRouteが適用されました。外部からのアクセスを確認します。

kind環境ではクラウドのロードバランサーが利用できないため、kubectl port-forward でホストマシンからGatewayにアクセスします。まず、GatewayのService名を取得します。

[Execution User: developer]

# GatewayのService名を取得
export GATEWAY_SVC=$(kubectl get svc -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=taskboard-gateway \
  -o jsonpath='{.items[0].metadata.name}')
echo $GATEWAY_SVC

ポートフォワードを開始します。ホストの8080番ポートをGatewayの80番ポートに転送します。

[Execution User: developer]

kubectl port-forward -n envoy-gateway-system svc/$GATEWAY_SVC 8080:80 &
Forwarding from 127.0.0.1:8080 -> 10080
Forwarding from [::1]:8080 -> 10080

本番環境(マネージドK8s)では: EKS/GKE/AKSなどのマネージドKubernetesでは、GatewayのServiceに対してクラウドのロードバランサーが自動プロビジョニングされ、外部IPアドレスが割り当てられます。port-forwardは不要で、そのIPアドレス(またはDNS名)に直接アクセスできます。kindでの port-forward は学習環境ならではの手順です。

では、外部からのアクセスを確認します。

テスト1: / → Nginx応答

[Execution User: developer]

curl -s http://localhost:8080/ | head -5
<!DOCTYPE html>
<html>
<head>
<title>TaskBoard</title>
...

NginxがフロントエンドのHTMLを返しています。

テスト2: /api/tasks → TaskBoard API応答(JSON)

[Execution User: developer]

curl -s http://localhost:8080/api/tasks | head -3
[{"id":1,"title":"サンプルタスク1","completed":false},{"id":2,"title":"サンプルタスク2","completed":true}]

TaskBoard APIがMySQLからデータを取得し、JSON形式で返しています。パスベースルーティングが正しく機能しています。

テスト3: 存在しないパス → 404確認

[Execution User: developer]

curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/nonexistent
404

存在しないパスに対しては404が返ります。HTTPRouteの / ルールにマッチし、Nginxが404を返しています。

Phase A完了です。Gateway APIによる外部公開が成功しました。1つの入口(localhost:8080)から、パスによってNginxとTaskBoard APIに振り分けられています。

ただし、この状態では通信制御がまだ適用されていません。クラスタ内のPod間通信はすべてオープンです。次のPhase Bで、設計書の通信制御マトリクスに基づいてNetworkPolicyを適用します。

6.4 Phase B — NetworkPolicyで通信を制御する

ここからが本回のハイライトです。応用編第6回でもNetworkPolicyの「デフォルト拒否 → ホワイトリスト」パターンを体験しましたが、あのときは「試行錯誤」でした。今回は違います。6.2.3節の通信制御マトリクスという設計書があり、その通りに適用して、マトリクスと一致することを検証します。

6.4.1 Before — 現在の通信状態を記録する

NetworkPolicyを適用する前に、現在の通信状態をBefore状態として記録します。第5回の受け入れテストで確認済みの内容ですが、Phase Bの開始時点で改めて記録しておくことで、After状態との対比が明確になります。

テスト用Podを使って各経路の疎通を確認します。

テスト1: API Pod → MySQL(必要な通信)

[Execution User: developer]

kubectl run test-before-api -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=api" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306
mysql-headless.db.svc.cluster.local (192.168.x.x:3306) open
pod "test-before-api" deleted

通ります。これはNetworkPolicy適用後も維持されるべき通信です。

テスト2: Nginx Pod → MySQL(不要な通信 — 遮断すべき)

[Execution User: developer]

kubectl run test-before-nginx -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=frontend" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306
mysql-headless.db.svc.cluster.local (192.168.x.x:3306) open
pod "test-before-nginx" deleted

通ってしまいます。通信制御マトリクスではこの通信は「❌ 遮断」です。NetworkPolicy適用後に遮断されることを確認します。

Before状態を表にまとめます。

通信経路Before(現在)期待状態(マトリクス)
Gateway → Nginx✅ 通る✅ 許可
Gateway → API✅ 通る✅ 許可
API → MySQL✅ 通る✅ 許可
Nginx → MySQL✅ 通る❌ 遮断
CronJob → MySQL✅ 通る✅ 許可

「Nginx → MySQL」のセルだけ、現在の状態と期待状態が不一致です。NetworkPolicyでこのギャップを埋めます。

6.4.2 デフォルト拒否ポリシーを適用する — 全通信を止める

ここから「破壊 → 復活」のプロセスに入ります。応用編第6回でも同じパターンを体験しましたが、今回は通信制御マトリクスに基づく計画的な操作です。

まず、db NamespaceとApp Namespaceの両方にデフォルト拒否ポリシーを適用します。これにより、両Namespace内の全Podに対するすべての着信・発信が遮断されます。

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/netpol-db-default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: db
spec:
  # 全Podが対象
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
EOF

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/netpol-app-default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
EOF

2つのデフォルト拒否ポリシーを一括で適用します。

[Execution User: developer]

kubectl apply -f ~/k8s-production/netpol-db-default-deny.yaml
kubectl apply -f ~/k8s-production/netpol-app-default-deny.yaml
networkpolicy.networking.k8s.io/default-deny-all created
networkpolicy.networking.k8s.io/default-deny-all created

この瞬間、TaskBoardは機能停止します。

6.4.3 全通信遮断を確認する — TaskBoardが機能停止する

デフォルト拒否ポリシーの適用直後、TaskBoardがどうなったか確認します。

確認1: API → MySQL(遮断されるはず)

[Execution User: developer]

kubectl run test-deny-api -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=api" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306
nc: mysql-headless.db.svc.cluster.local (192.168.x.x:3306): Connection timed out
pod "test-deny-api" deleted

Connection timed out。API PodからMySQLへの通信が遮断されました。TaskBoard APIはデータベースに接続できず、APIリクエストはエラーになります。

確認2: 外部 → Gateway → Nginx/API(遮断されるはず)

[Execution User: developer]

curl -s -o /dev/null -w "%{http_code}" --max-time 10 http://localhost:8080/
503

503(Service Unavailable)またはタイムアウトが返ります。Gateway経由のアクセスも遮断されました。app Namespaceの全PodへのIngressが拒否されているため、Envoy Gatewayからのトラフィックも到達できません。

設計通りに「壊れた」ことを確認しました。デフォルト拒否ポリシーが正しく機能しています。ここから、通信制御マトリクスに基づいて必要な通信のみを許可していきます。

6.4.4 必要な通信のみを許可する — TaskBoardが復活する

通信制御マトリクスの「✅ 許可」セルに対応するNetworkPolicyを順に適用します。

ポリシー1: db Namespace — API Podからの3306ポートのみ許可 + DNS許可

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/netpol-db-allow-api.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-mysql
  namespace: db
spec:
  # 対象: MySQL Pod
  podSelector:
    matchLabels:
      app: taskboard
      component: db
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # app Namespace の API Pod からの 3306/tcp のみ許可
    - from:
        - namespaceSelector:
            matchLabels:
              layer: application       # app Namespace のラベル
          podSelector:
            matchLabels:
              app: taskboard
              component: api           # TaskBoard API Pod のラベル
      ports:
        - protocol: TCP
          port: 3306
  egress:
    # DNS(kube-dns)への発信を許可(53/udp, 53/tcp)
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
EOF

ingress.fromnamespaceSelectorpodSelector を同一ブロック内に記述しています。これはAND条件です。「layer: application ラベルを持つNamespace内の、component: api ラベルを持つPod」からのIngressのみ許可します。OR条件(別々の from ブロック)と間違えないよう注意してください。

ポリシー2: db Namespace — CronJobバックアップPodからのアクセスを許可

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/netpol-db-allow-backup.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backup-to-mysql
  namespace: db
spec:
  # 対象: MySQL Pod
  podSelector:
    matchLabels:
      app: taskboard
      component: db
  policyTypes:
    - Ingress
  ingress:
    # 同一Namespace(db)内のバックアップPodからの 3306/tcp を許可
    - from:
        - podSelector:
            matchLabels:
              app: taskboard
              component: db-backup
      ports:
        - protocol: TCP
          port: 3306
EOF

ポリシー3: db Namespace — CronJobバックアップPodのEgress許可

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/netpol-db-allow-backup-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backup-egress
  namespace: db
spec:
  # 対象: バックアップPod
  podSelector:
    matchLabels:
      app: taskboard
      component: db-backup
  policyTypes:
    - Egress
  egress:
    # 同一Namespace内のMySQLへの発信を許可
    - to:
        - podSelector:
            matchLabels:
              app: taskboard
              component: db
      ports:
        - protocol: TCP
          port: 3306
    # DNS への発信を許可
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
EOF

ポリシー4: app Namespace — Gateway経由のIngress + DNS許可

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/netpol-app-allow-gateway.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-gateway-ingress
  namespace: app
spec:
  # 対象: app Namespace の全Pod
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # envoy-gateway-system Namespace からの着信を許可
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: envoy-gateway-system
      ports:
        - protocol: TCP
          port: 8080   # Nginx(非root化後のポート)
  egress:
    # DNS(kube-dns)への発信を許可
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
EOF

Envoy Gatewayのデータプレーン(Envoy Proxy Pod)は envoy-gateway-system Namespaceに配置されています。kubernetes.io/metadata.name ラベルは Kubernetes 1.22以降すべてのNamespaceに自動付与されるため、カスタムラベルを事前定義していないNamespaceでも namespaceSelector で指定できます。

ポリシー5: app Namespace — API PodからMySQLへのEgress許可

[Execution User: developer]

cat <<'EOF' > ~/k8s-production/netpol-app-allow-api-to-db.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: app
spec:
  # 対象: TaskBoard API Pod のみ
  podSelector:
    matchLabels:
      app: taskboard
      component: api
  policyTypes:
    - Egress
  egress:
    # db Namespace の MySQL への発信を許可
    - to:
        - namespaceSelector:
            matchLabels:
              layer: database          # db Namespace のラベル
          podSelector:
            matchLabels:
              app: taskboard
              component: db            # MySQL Pod のラベル
      ports:
        - protocol: TCP
          port: 3306
    # DNS への発信も許可(明示的に)
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
EOF

このポリシーは podSelectorcomponent: api のPod(TaskBoard API)のみを対象にしています。Nginx Pod(component: frontend)にはこのポリシーは適用されないため、Nginx PodからMySQLへのEgressは引き続き遮断されたままです。これが通信制御マトリクスの「Nginx → MySQL: ❌ 遮断」を実現する仕組みです。

5つのポリシーを一括で適用します。

[Execution User: developer]

kubectl apply -f ~/k8s-production/netpol-db-allow-api.yaml
kubectl apply -f ~/k8s-production/netpol-db-allow-backup.yaml
kubectl apply -f ~/k8s-production/netpol-db-allow-backup-egress.yaml
kubectl apply -f ~/k8s-production/netpol-app-allow-gateway.yaml
kubectl apply -f ~/k8s-production/netpol-app-allow-api-to-db.yaml
networkpolicy.networking.k8s.io/allow-api-to-mysql created
networkpolicy.networking.k8s.io/allow-backup-to-mysql created
networkpolicy.networking.k8s.io/allow-backup-egress created
networkpolicy.networking.k8s.io/allow-gateway-ingress created
networkpolicy.networking.k8s.io/allow-api-to-db created

許可ポリシーが適用されました。TaskBoardが復活しているか確認します。

[Execution User: developer]

# 外部アクセス確認
curl -s http://localhost:8080/api/tasks | head -1
[{"id":1,"title":"サンプルタスク1","completed":false},{"id":2,"title":"サンプルタスク2","completed":true}]

TaskBoardが復活しました。デフォルト拒否で全通信を遮断した後、通信制御マトリクスに基づいて必要な通信のみを許可した結果、設計通りの通信制御下でTaskBoardが稼働しています。

6.4.5 After — 通信制御マトリクスと一致しているか検証する

NetworkPolicy適用後のAfter状態を確認し、通信制御マトリクスと照合します。

検証1: API Pod → MySQL — 許可済み

[Execution User: developer]

kubectl run test-after-api -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=api" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306
mysql-headless.db.svc.cluster.local (192.168.x.x:3306) open
pod "test-after-api" deleted

✅ マトリクス通り、許可されています。

検証2: Nginx Pod → MySQL — 遮断されるはず

[Execution User: developer]

kubectl run test-after-nginx -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=frontend" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306
nc: mysql-headless.db.svc.cluster.local (192.168.x.x:3306): Connection timed out
pod "test-after-nginx" deleted

✅ マトリクス通り、遮断されています。Beforeでは通っていた通信が、NetworkPolicyにより遮断されました。

検証3: Gateway → Nginx / API — 許可済み(外部アクセスで確認済み)

6.4.4節のcurlテストで確認済みです。

After状態を表にまとめ、Before状態と対比します。

通信経路BeforeAfterマトリクス判定
Gateway → Nginx✅ 許可OK
Gateway → API✅ 許可OK
API → MySQL✅ 許可OK
Nginx → MySQL❌ 遮断OK
CronJob → MySQL✅ 許可OK

全セルがマトリクスと一致しています。Phase B完了です。

適用したNetworkPolicyの一覧を確認しておきます。

[Execution User: developer]

echo "=== db Namespace ==="
kubectl get networkpolicy -n db
echo ""
echo "=== app Namespace ==="
kubectl get networkpolicy -n app
=== db Namespace ===
NAME                    POD-SELECTOR                                AGE
default-deny-all        <none>                                      5m
allow-api-to-mysql      app=taskboard,component=db                  3m
allow-backup-to-mysql   app=taskboard,component=db                  3m
allow-backup-egress     app=taskboard,component=db-backup           3m

=== app Namespace ===
NAME                    POD-SELECTOR                                AGE
default-deny-all        <none>                                      5m
allow-gateway-ingress   <none>                                      3m
allow-api-to-db         app=taskboard,component=api                 3m

6.5 Phase C — E2E結合テスト

Phase AとPhase Bで外部公開と通信制御が完了しました。最後に、E2E(End-to-End)結合テストで全経路を検証します。VMの世界でもサーバー構築後に結合テスト(受け入れテスト)を実施しますが、K8sでも同じです。「全経路が設計通りに動くこと」を体系的に確認し、記録に残します。

6.5.1 外部アクセステスト

Gateway API経由で、外部からの全パスをテストします。

[Execution User: developer]

# テスト1: / → Nginx(フロントエンド)
echo "=== Test 1: / → Nginx ==="
curl -s -o /dev/null -w "HTTP %{http_code}" http://localhost:8080/
echo ""

# テスト2: /api/tasks → TaskBoard API(JSON)
echo "=== Test 2: /api/tasks → API ==="
curl -s http://localhost:8080/api/tasks | python3 -m json.tool | head -5
echo ""

# テスト3: 存在しないパス → 404
echo "=== Test 3: /nonexistent → 404 ==="
curl -s -o /dev/null -w "HTTP %{http_code}" http://localhost:8080/nonexistent
echo ""
=== Test 1: / → Nginx ===
HTTP 200
=== Test 2: /api/tasks → API ===
[
    {
        "id": 1,
        "title": "サンプルタスク1",
        "completed": false
    },
...
=== Test 3: /nonexistent → 404 ===
HTTP 404

全テスト合格です。外部からのアクセスはGateway API経由で正しくルーティングされています。

6.5.2 内部疎通テスト(許可と遮断の両方を確認)

通信制御マトリクスの全セルを検証します。許可されている通信が通ること、遮断されている通信が通らないことの両方を確認します。

許可済み通信の確認:

[Execution User: developer]

# API Pod → MySQL(3306) — 許可されるはず
echo "=== API → MySQL (should PASS) ==="
kubectl run e2e-api -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=api" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306
=== API → MySQL (should PASS) ===
mysql-headless.db.svc.cluster.local (192.168.x.x:3306) open
pod "e2e-api" deleted

遮断済み通信の確認:

[Execution User: developer]

# Nginx Pod → MySQL(3306) — 遮断されるはず
echo "=== Nginx → MySQL (should BLOCK) ==="
kubectl run e2e-nginx -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=frontend" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306
=== Nginx → MySQL (should BLOCK) ===
nc: mysql-headless.db.svc.cluster.local (192.168.x.x:3306): Connection timed out
pod "e2e-nginx" deleted

[Execution User: developer]

# ラベルなしPod → MySQL(3306) — 遮断されるはず
echo "=== Unlabeled Pod → MySQL (should BLOCK) ==="
kubectl run e2e-other -n app --rm -it --restart=Never \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306
=== Unlabeled Pod → MySQL (should BLOCK) ===
nc: mysql-headless.db.svc.cluster.local (192.168.x.x:3306): Connection timed out
pod "e2e-other" deleted

通信制御マトリクスの全セルが設計通りです。

6.5.3 ヘルスチェック確認

TaskBoard API(Payara Micro)のMicroProfile Healthエンドポイントを確認します。API PodのヘルスチェックはProbeで継続的に監視されていますが、E2Eテストの一環として外部から手動で確認しておきます。

[Execution User: developer]

# API Podに直接接続してヘルスチェック
API_POD=$(kubectl get pod -n app -l component=api -o jsonpath='{.items[0].metadata.name}')

echo "=== /health/ready ==="
kubectl exec -n app $API_POD -- curl -s http://localhost:8080/health/ready

echo ""
echo "=== /health/live ==="
kubectl exec -n app $API_POD -- curl -s http://localhost:8080/health/live
=== /health/ready ===
{"status":"UP","checks":[{"name":"readiness","status":"UP","data":{"database":"connected"}}]}
=== /health/live ===
{"status":"UP","checks":[{"name":"liveness","status":"UP"}]}

readinessもlivenessもUPです。readinessチェックにはDB接続確認が含まれており、MySQLへの通信が正常であることも間接的に確認できています。

6.5.4 E2Eテスト結果報告を作成する

全テスト結果をE2Eテスト結果報告としてまとめます。

1. テスト環境

項目内容
クラスタkind(CP 1 + Worker 3)
CNICalico
Gateway APIコントローラーEnvoy Gateway v1.6.3
適用済みNetworkPolicydb: 4ポリシー、app: 3ポリシー

2. 外部アクセステスト

テスト項目期待結果実際の結果判定
/ → NginxHTTP 200 + HTML応答HTTP 200✅ PASS
/api/tasks → APIHTTP 200 + JSON応答HTTP 200 + JSON配列✅ PASS
/nonexistent → 404HTTP 404HTTP 404✅ PASS

3. 内部疎通テスト

通信経路期待結果実際の結果判定
API Pod → MySQL (3306)接続成功(許可済み)open✅ PASS
Nginx Pod → MySQL (3306)タイムアウト(遮断)Connection timed out✅ PASS
ラベルなしPod → MySQL (3306)タイムアウト(遮断)Connection timed out✅ PASS

4. ヘルスチェック確認

エンドポイント期待結果実際の結果判定
API /health/readystatus: UPUP(DB接続確認含む)✅ PASS
API /health/livestatus: UPUP✅ PASS

5. テスト結論

全テスト項目パス。 外部アクセス(Gateway API経由)、内部疎通(許可・遮断の両方)、ヘルスチェックのすべてが設計書通りに動作していることを確認しました。TaskBoardは設計書通りに完全稼働しています。

6.6 NetworkPolicyのデバッグ技法

本回のPhase Bでは全ポリシーが計画通りに機能しましたが、実務ではNetworkPolicyの適用後に意図しない通信遮断が発生することがあります。ここでは、そうした場面で使えるデバッグ技法を整理します。

6.6.1 意図しない遮断の切り分け方

NetworkPolicy適用後に通信障害が発生した場合、最も有効な切り分けロジックは以下です。

「第5回でNetworkPolicyなしの状態で疎通確認済み。問題があればNetworkPolicyが原因。」

このロジックが使えるのは、構築を段階的に進めているからです。第5回の受け入れテストでNetworkPolicyなしの全通信を確認済みであるため、Phase B以降で発生した問題はNetworkPolicyに起因すると断定できます。Service名の誤り、ポート番号の不一致、Pod自体の障害などの可能性を排除でき、デバッグの範囲を大幅に絞り込めます。

この段階的アプローチの利点は、VMの世界でも同じです。サーバー間の疎通を確認してからファイアウォールを設定すれば、疎通障害が発生した際に「ファイアウォールが原因」と即断できます。

6.6.2 テスト用Podによる接続テスト

NetworkPolicyはPodのラベルに基づいて通信を制御します。デバッグ時には、ラベルを明示的に付与したテスト用Podを使って、どのラベルの組み合わせで通信が許可/遮断されるかを切り分けます。

[Execution User: developer]

# API Podと同じラベルで接続テスト(通るはず)
kubectl run debug-api -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=api" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306

# ラベルなしで接続テスト(遮断されるはず)
kubectl run debug-nolabel -n app --rm -it --restart=Never \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306

# 異なるラベルで接続テスト(遮断されるはず)
kubectl run debug-wrong -n app --rm -it --restart=Never \
  --labels="app=taskboard,component=frontend" \
  --image=busybox:1.36 -- \
  nc -zv -w 5 mysql-headless.db.svc.cluster.local 3306

このようにラベルを変えながら接続テストを行うことで、「どのラベルの組み合わせがNetworkPolicyにマッチしているか」を正確に特定できます。タイムアウトは5秒(-w 5)に設定し、遮断時に長時間待たないようにしています。

6.6.3 kubectl describe networkpolicyでルールを確認する

kubectl describe networkpolicy で、適用されているルールの詳細を確認できます。

[Execution User: developer]

kubectl describe networkpolicy allow-api-to-mysql -n db
Name:         allow-api-to-mysql
Namespace:    db
Created on:   ...
Labels:       <none>
Annotations:  <none>
Spec:
  PodSelector:     app=taskboard,component=db
  Allowing ingress traffic:
    To Port: 3306/TCP
    From:
      NamespaceSelector: layer=application
      PodSelector: app=taskboard,component=api
  Allowing egress traffic:
    To Port: 53/UDP
    To Port: 53/TCP
    To:
      NamespaceSelector: kubernetes.io/metadata.name=kube-system
  Policy Types: Ingress, Egress

確認すべきポイントは以下の3つです。

  • PodSelector: このポリシーが適用される対象Pod。意図したPodに適用されているか
  • From / To: 許可される送信元/送信先。NamespaceSelectorとPodSelectorの組み合わせ(AND条件)が正しいか
  • To Port: 許可されるポート番号。3306/TCPが正しく設定されているか

意図しない遮断の多くは、ラベルの不一致(layer: application ラベルがNamespaceに付与されていない等)やポート番号の誤り(Nginx 8080を80と誤記等)が原因です。describe の出力とマニフェスト、そして実際のリソースのラベルを突き合わせることで原因を特定できます。

6.7 この回のまとめ

6.7.1 成果物の確認 — 完全稼働TaskBoard + E2Eテスト結果報告

本回で以下の成果物が完成しました。

[app Namespace]
  Nginx Deployment (replicas: 2) + Service (ClusterIP, port: 8080)
  TaskBoard API Deployment (replicas: 2) + Service (ClusterIP, port: 8080)
  HPA (Nginx, TaskBoard API)
  PDB (Nginx, TaskBoard API)
  ConfigMap (nginx-config)
  NetworkPolicy: default-deny-all, allow-gateway-ingress, allow-api-to-db

[db Namespace]
  MySQL StatefulSet (replicas: 1) + Headless Service (port: 3306) + PVC
  DB初期化Job (Completed)
  DBバックアップCronJob
  Secret (mysql-secret)
  NetworkPolicy: default-deny-all, allow-api-to-mysql,
                 allow-backup-to-mysql, allow-backup-egress

[monitoring Namespace]
  ログ収集DaemonSet(全Worker Node)

[Gateway API]
  Gateway (taskboard-gateway, port: 80)
  HTTPRoute (taskboard-route: /api → API, / → Nginx)

[E2Eテスト結果]
  全テスト項目パス

TaskBoardは設計書通りに完全稼働しています。外部からGateway API経由でアクセスでき、NetworkPolicyにより設計書の通信制御マトリクスが実装されています。

6.7.2 構築フェーズの振り返り(第4回〜第6回)

本回は構築フェーズの最終回です。第4回〜第6回の3回で行ったことを振り返ります。

構築対象構築の性質
第4回基盤(Namespace, Quota, LimitRange, RBAC, 基盤コンポーネント)箱を作る
第5回アプリケーション(全コンポーネント、HPA, PDB)中身を入れる
第6回ネットワーク(Gateway API, NetworkPolicy)出入口と壁を作る

VMの世界で3層構成のシステムを構築するときと同じ流れです。まず環境(ラック、ネットワーク、ストレージ)を整備し、次にサーバー(VM)を構築してアプリをデプロイし、最後にロードバランサーとファイアウォールを設定する。K8sでもこの段階的アプローチが有効であることを、3回の構築フェーズで確認しました。

設計フェーズ(第1回〜第3回)で作成した構成図、基本設計書、詳細設計書が、構築フェーズ(第4回〜第6回)の手順を導きました。「設計書 → 実動するTaskBoard」の変換が完了です。

6.7.3 次回予告 — 構築フェーズから運用フェーズへ

TaskBoardは動いています。しかし、「動いている」と「運用できる」は異なります。

現時点ではまだ以下が整備されていません。

  • 監視設計: Probeは設定済みだが、監視項目の一覧と閾値設計がない
  • スケーリング設計: HPAは設定済みだが、閾値の妥当性を負荷テストで検証していない
  • デプロイ戦略: ローリングアップデートのパラメータ設計とロールバック手順がない
  • バックアップ設計: CronJobは設定済みだが、リストア手順が未検証

次回(第7回)からは運用フェーズに入ります。「動くTaskBoard」を「運用できるTaskBoard」にするための運用設計書を作成します。

ポートフォワードを停止しておきましょう。

[Execution User: developer]

# バックグラウンドのport-forwardを停止
kill %1 2>/dev/null; echo "port-forward stopped"

AIコラム — Gateway API / NetworkPolicy設定の生成

NetworkPolicyのマニフェスト作成は、設計は人間が行い、YAML記述はAIに任せると効率的です。通信制御マトリクスをAIに渡して、NetworkPolicyのドラフトを生成させてみましょう。

💬 あなた → AI(Claude):
以下の通信制御マトリクスを満たすNetworkPolicyマニフェストを生成してください。

– db Namespace: デフォルト拒否(Ingress/Egress)。app Namespaceの component: api PodからのTCP 3306のみ許可。DNS(kube-system:53)も許可。
– app Namespace: デフォルト拒否(Ingress/Egress)。envoy-gateway-system NamespaceからのTCP 8080 Ingressを許可。component: api PodからのTCP 3306 Egressを許可(宛先: db Namespace, component: db)。DNS許可。

Namespaceラベル: app → layer: application、db → layer: database

AIは数秒でNetworkPolicyマニフェスト一式を生成してくれます。ただし、AIの出力をそのまま kubectl apply する前に、必ず通信制御マトリクスとの照合を行ってください。

AIの出力でよく確認すべきポイントは以下です。

  • AND条件とOR条件の記述ミス: namespaceSelectorpodSelector を同一ブロック内に書くとAND、別ブロックに書くとOR。AIが意図と異なる条件で生成することがある
  • DNS許可の漏れ: Egress拒否を設定した場合、DNS(53番ポート)の許可を忘れるとサービス名の名前解決ができなくなる。AIがこの点を見落とすことがある
  • ポート番号の正確性: 非root化後のNginxポート(8080)を旧ポート(80)で生成することがある
  • CronJobバックアップ用ポリシーの漏れ: アプリケーションの通信経路だけでなく、運用ツール(バックアップ、監視)の通信も考慮する必要がある。AIは運用ツールの通信要件を見落としやすい

AIの出力は「NetworkPolicyのドラフト」として扱い、通信制御マトリクスとの照合と、テスト用Podによる実際の接続テストで検証してください。マトリクスという設計書があるからこそ、AIの出力を機械的に検証できます。