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

Gateway API+Traefik入門【CKA第11回】

広告

新卒インフラエンジニア向け Kubernetes 実践教科書(第2巻 CKA 編)の第11回です。動作確認バージョン: AlmaLinux 10.1 / K8s v1.35.5 / Gateway API v1.4.0 / Traefik chart v39.0.0(Traefik v3.6.x)/ cert-manager v1.20.0 / MetalLB v0.16.0(2026-05-24 時点)

広告

今ここマップ(第11回 / 全16回 / 第3部完走)

今ここ: 第11回 / 全16回(第3部:ネットワーク)
▓▓▓▓▓▓▓▓▓▓▓░░░░░  69%

第1部(クラスタ構築):       ■■■■■ 5/5 回(完了)
第2部(ワークロード管理):   ■■■   3/3 回(完了)
第3部(ネットワーク):       ■■■   3/3 回(完了)← 今ここ
第4部(ストレージ):         □     0/1 回
第5部(監視・運用):         □□    0/2 回
第6部(トラブルシュート):   □□    0/2 回

第10回では NetworkPolicy の default-deny-all から段階許可、CoreDNS Egress 遮断のトラブルシュートを実施しました。第11回では、より高度なトラフィック制御として Gateway API を導入します。ingress-nginx が EOL を迎えた今、Gateway API が Kubernetes の新標準です。

第11回のキャッチコピー: 「Gateway API + Traefik + cert-manager + CoreDNS の 4 機構連携で https://fanclub.local を実現する」

第11回は第3部「ネットワーク」の完走回です。第9回 Service + MetalLB、第10回 NetworkPolicy + Calico の知識を統合し、外部ブラウザから HTTPS で fanclub-api に到達する完成形を構築します。

第11回終了時の達成状態:

  • Gateway API v1.4.0 の CRD(GatewayClass / Gateway / HTTPRoute)がインストール済み
  • Traefik chart v39.0.0 が traefik Namespace で稼働し、MetalLB EXT IP 192.168.1.200 を保持
  • cert-manager v1.20.0 が稼働し、self-signed CA 経由で fanclub-tls Certificate が READY: True
  • HTTPRoute fanclub-httprouteATTACHED: True で Traefik Gateway にバインド済み
  • CoreDNS の hosts プラグインで fanclub.local192.168.1.200 に解決される
  • curl -k https://fanclub.local で HTTP 200 OK が返り fanclub-api のレスポンスが取得できる

第11回のスコープと設計 — 4 機構の依存関係を把握する

本セクションでは、第11回で扱うこと・扱わないこと、4 機構の依存関係、演習 Namespace 設計、そして本番運用での 3 つの警告を整理します。

第11回で「やること」と「やらないこと」

やることやらないこと
Gateway API v1.4.0 standard channel CRD のインストールexperimental channel(TCPRoute / TLSRoute / UDPRoute)
Traefik chart v39.0.0 を Gateway API 実装として導入ingress-nginx(2026-03 EOL のため本シリーズ不採用)
cert-manager v1.20.0 + self-signed CA 2 段階方式Let’s Encrypt / ACME(公開ドメインが必要・第3巻外)
CoreDNS hosts プラグインで fanclub.local 名前解決ExternalDNS による自動 DNS 管理(第3巻 SRE 編で扱う)
curl -k https://fanclub.local 200 OK 確認HTTP → HTTPS 自動リダイレクト(第11回スコープ外)
HTTPRoute による hostname ベースルーティングHTTPRoute の重み付けトラフィック分割(第3巻で扱う)

4 機構の依存関係図

Gateway API + Traefik + cert-manager + CoreDNS の 4 機構連携
Gateway API + Traefik + cert-manager + CoreDNS の 4 機構連携

4 機構は順序依存です。CRD なしに Traefik は GatewayClass を作れず、cert-manager の Secret なしに Gateway は TLS を終端できず、HTTPRoute なしに hostname ルーティングは成立せず、CoreDNS hosts なしに fanclub.local が IP に解決されません。

演習 Namespace の設計

Namespace役割削除タイミング
traefik(新規)Traefik Pod / GatewayClass / Gateway / LoadBalancer Service削除しない(以降の回で使用)
cert-manager(新規)cert-manager コンポーネント + fanclub-ca Secret削除しない(以降の回で使用)
fanclub(既存)fanclub-api 稼働中 + Certificate + HTTPRoute変更しない
kube-system(既存)CoreDNS ConfigMap 編集(hosts プラグイン追加)変更しない

設計判断 5 件

判断① Gateway API CRD は v1.4.0 standard channel を --server-side で適用する

CRD サイズが client-side apply のアノテーション上限(256 KiB)を超える場合があるため、Gateway API 公式でも --server-side を推奨しています。standard channel は GatewayClass / Gateway / HTTPRoute / GRPCRoute / ReferenceGrant / BackendTLSPolicy の 6 リソースを含み、v1.4.0 で BackendTLSPolicy が standard 昇格しました。

判断② Traefik chart v39.0.0 は Gateway API 専用設定・kubernetesIngress は無効化する

providers.kubernetesIngress.enabled: false で旧 Ingress リソースを無効化し、providers.kubernetesGateway.enabled: true + gateway.enabled: true で Helm が GatewayClass と Gateway を自動生成します。gateway.namespacePolicy: All により fanclub Namespace の HTTPRoute も受け入れます。

判断③ cert-manager は self-signed CA 2 段階方式を採用する

selfsigned ClusterIssuer でルート CA Certificate を発行し、その Secret を参照する CA ClusterIssuer(fanclub-ca-issuer)を作成、最後に fanclub.local 用 TLS Certificate を発行します。「selfsigned のみ」では CA 証明書の配布・信頼追加が難しいため、2 段階方式が cert-manager 公式の推奨パターンです。

判断④ CoreDNS hosts プラグインで fanclub.local192.168.1.200 の A レコードを追加する

MetalLB が Traefik Service に払い出す EXT IP は 192.168.1.200 です。CoreDNS の hosts プラグインに静的 A レコードを追加することで、全 Pod から fanclub.local を名前解決できるようになります。fallthrough を必ず記述します。省略するとマッチしなかった全クエリが SERVFAIL になり、クラスタ全体の名前解決が壊れます。

判断⑤ やってみよう演習は 3 本に分割する

4 機構を 1 本の演習に詰め込むと読者が迷子になるため、やってみよう①(Gateway API CRD + Traefik)→ ②(cert-manager + Certificate)→ ③(HTTPRoute + CoreDNS + curl 確認)の独立完結型 3 本構成にします。各演習が「完了状態を確認してから次へ進む」流れを徹底します。

本番運用警告 3 本

本番警告①: Self-signed 証明書は本番環境で使用しない

本記事では self-signed CA → ClusterIssuer → Certificatefanclub.local の TLS を実現します。この方式はブラウザに「接続は安全ではありません」警告が表示され、信頼チェーンが構築されません。本番環境では Let’s Encrypt / ACME(cert-managerClusterIssuer: spec.acme)または企業内 PKI(中間 CA)を使用します。Let’s Encrypt は公開ドメインと外部向けエンドポイント(HTTP-01 / DNS-01 チャレンジ)が必要となるため、社内システムでは企業 PKI 連携が現実的な選択肢です。

本番警告②: MetalLB L2 モードはシングルノードボトルネック

L2 モードでは Gratuitous ARP により 1 ノードだけが VIP の ARP 応答を担当します。そのノードを経由してすべての LoadBalancer トラフィックが流れるため、ノード障害時は ARP キャッシュ失効と GARP 再送が完了するまで(数秒〜数十秒)断が発生します。本番環境では BGP モードを採用し、ECMP(Equal-Cost Multipath)で複数ノードに分散させてノード障害時の切り替えを数秒レベルに短縮します。本シリーズの Hyper-V 環境には家庭ルータしかなく BGP ピアが構成できないため、L2 モードは開発・学習環境向けの選択であることを理解しておきます。

本番警告③: CoreDNS hosts プラグインは ExternalDNS の代替ではない

本記事では CoreDNS ConfigMap に手動で A レコードを追記します。この方法は Gateway の External IP が変わるたびに手動更新が必要で、複数ドメインが増えると ConfigMap が肥大化し、誤った設定で全クラスタの名前解決が壊れるリスクがあります。本番では kubernetes-sigs/external-dns を導入し、Service / Gateway の External IP 変化を DNS プロバイダー(Route53 / Cloud DNS / PowerDNS 等)に自動反映します。本シリーズのオンプレ環境では第2巻スコープ外とし、第3巻 SRE 編で扱う予定です。

Gateway API とは — GatewayClass / Gateway / HTTPRoute の設計思想

なぜ Ingress から Gateway API への移行が必要か

Kubernetes の外部公開の標準だった Ingress リソース(networking.k8s.io/v1)には設計上の限界があります。TLS 終端の挙動、リライト、認証連携、トラフィック分割など、ほとんどの設定が「実装依存のアノテーション」に押し込まれており、Ingress Controller を別実装に切り替えた瞬間に YAML が動かなくなります。さらに、長年 Ingress の事実上の標準実装だった ingress-nginx は 2026 年 3 月で EOL を迎えました。

Gateway API(gateway.networking.k8s.io/v1)は SIG-Network が「Ingress の正式な後継」として開発した API です。CKA 2026 から D3 ドメインに明示追加され、ingress-nginx EOL と相まって 2026 年が Gateway API への本格移行年になります。

GatewayClass / Gateway / HTTPRoute の 3 層構造

Gateway API の 3 リソースと役割分担
Gateway API の 3 リソースと役割分担

Ingress では「ホスト名・パス・TLS・annotations」がすべて 1 リソースに集約されており、誰が何を変更したのかを追跡しづらい問題がありました。Gateway API は責務を 3 層に分け、それぞれ異なるロール(クラスタ管理者・インフラチーム・アプリチーム)が独立して管理できる Role-oriented design を採用しています。

Ingress と Gateway API の比較表

項目IngressGateway API
API バージョンnetworking.k8s.io/v1gateway.networking.k8s.io/v1
TLS 終端設定annotations で実装依存Gateway spec で標準化
トラフィック分割(重み付け)実装依存(annotations)HTTPRoute backendRefs の weight で標準化
責務分離1 リソースにすべて集約GatewayClass / Gateway / HTTPRoute に分離
CKA 2026 出題参考程度必須(D3 明示)
ingress-nginx 状況2026-03 EOL本シリーズで採用

standard channel と experimental channel

channel含まれる CRD用途
standardGatewayClass / Gateway / HTTPRoute / GRPCRoute / ReferenceGrant / BackendTLSPolicy本番運用・CKA 試験範囲
experimentalstandard + TCPRoute / TLSRoute / UDPRouteL4 や gRPC ルーティングを必要とする検証用途

第11回では standard channel のみを使用します。HTTP / HTTPS のルーティングが要件のため、experimental channel は不要です。CKA D3 試験も standard channel の範囲で出題されます。

Traefik chart v39.x を Gateway API 実装として導入する

Traefik chart v39.x の概要

項目
Helm chart バージョンv39.0.0
Traefik 本体バージョンTraefik v3.6.7(chart v39.0.0 同梱)
Gateway API サポート標準提供(providers.kubernetesGateway
chart v38 からの破壊的変更ポート設定の http 明示ネスト要求 / Traefik Hub は v3.19.0 以降のみ
本シリーズでの Namespacetraefik(新規作成)

values.yaml の設計説明

第11回で使用する traefik-values.yaml の各セクションの意味を解説します。

  • providers.kubernetesIngress.enabled: false — 旧 Ingress リソースの監視を停止する。ingress-nginx EOL に伴い本シリーズでは Ingress を使わない
  • providers.kubernetesGateway.enabled: true — Gateway API リソース(Gateway / HTTPRoute)の監視を有効化する
  • gateway.enabled: true — Helm が GatewayClass(traefik)と Gateway(traefik-gateway)を自動生成する
  • gateway.namespacePolicy: All — 全 Namespace の HTTPRoute を受け入れる。fanclub Namespace の HTTPRoute をルーティング対象にするために必須
  • service.type: LoadBalancer — MetalLB が EXT IP 192.168.1.200 を払い出す
  • ports.web.exposedPort: 80 / ports.websecure.exposedPort: 443 — HTTP :80 と HTTPS :443 の両エントリーポイントを公開する(HTTPS の TLS 終端は後述の Gateway listener の certificateRefs が担うため、Traefik entrypoint には tls を書かない。chart v39 のスキーマでも ports.websecure.tls は許可されない)

traefik-values.yaml 全量

providers:
  kubernetesIngress:
    enabled: false
  kubernetesGateway:
    enabled: true
    namespaces: []
    experimentalChannel: false

gateway:
  enabled: true
  namespacePolicy: All

service:
  type: LoadBalancer

ports:
  web:
    port: 8000
    expose:
      default: true
    exposedPort: 80
  websecure:
    port: 8443
    expose:
      default: true
    exposedPort: 443

Pod 内では Traefik プロセスが :8000:8443 で待ち受け、Kubernetes Service が :80:443exposedPort として外部公開する構成です。なぜ Pod 内で 80/443 を直接 LISTEN しないかというと、Traefik コンテナは非 root(UID 1000)で起動するため特権ポート(<1024)にはバインドできず、内部では非特権ポート(8000/8443)を LISTEN する必要があるからです。MetalLB が Service に EXT IP を割り当てるため、外部からは 192.168.1.200:443 でアクセスできます。

cert-manager v1.20.0 + self-signed CA + ClusterIssuer

cert-manager の役割

cert-manager は Kubernetes の Certificate リソースから TLS 証明書を発行し、結果を Secret として保存する Operator です。証明書の有効期限管理、自動更新、ACME チャレンジ(Let’s Encrypt)対応など、TLS 運用に必要な機能を提供します。第11回では ACME ではなく self-signed CA を発行元として使い、社内向けエンドポイント fanclub.local の TLS を実現します。

self-signed CA 2 段階方式の階層構造

cert-manager 証明書チェーン — self-signed CA から fanclub-tls まで
cert-manager 証明書チェーン — self-signed CA から fanclub-tls まで

2 段階方式の利点: ルート CA Certificate(fanclub-ca-secret)が独立した Secret として存在するため、その ca.crt をブラウザに信頼ルートとして追加すれば「接続は安全ではありません」警告を解消できます。selfsigned のみで TLS 証明書を直接発行する方式では、サーバー証明書ごとに信頼チェーンが切れるため、ルート CA を取り出して配布する運用ができません。

cert-manager v1.20.0 の K8s v1.35 互換性

cert-manager v1.20.0 は Kubernetes v1.31 以降を要件としており、本シリーズの v1.35.5 で問題なく動作します。Helm chart のインストール時に --set crds.enabled=true を指定することで CRD も同時インストールされます。v1.15.0 以降では旧来の installCRDs=true は deprecated となり、crds.enabled=true が公式の正式オプションです。

ClusterIssuer と Issuer の違い

リソーススコープ用途
IssuerNamespace スコープ特定 Namespace 内の Certificate のみ発行可能
ClusterIssuerクラスタスコープ全 Namespace の Certificate を発行可能

本記事では fanclub Namespace 以外でも証明書を発行できるよう ClusterIssuer を採用します。ClusterIssuer が参照する CA Secret は cert-manager Namespace に置きます(cert-manager のデフォルト動作)。

CoreDNS — hosts プラグインで fanclub.local を解決する

CoreDNS hosts プラグインの仕組み

CoreDNS の hosts プラグインは、/etc/hosts ファイルライクな静的レコードを CoreDNS の応答に組み込む機能です。クラスタ内 DNS(kube-dns Service / ClusterIP 10.96.0.10)を経由する全 Pod から、追加した A レコードが名前解決可能になります。

kubeadm デフォルトの Corefile 構造

.:53 {
    errors
    health {
       lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
       ttl 30
    }
    prometheus :9153
    forward . /etc/resolv.conf {
       max_concurrent 1000
    }
    cache 30
    loop
    reload
    loadbalance
}

hosts ブロック追加後の Corefile

.:53 {
    errors
    health {
       lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
       ttl 30
    }
    hosts {
       192.168.1.200 fanclub.local
       fallthrough
    }
    prometheus :9153
    forward . /etc/resolv.conf {
       max_concurrent 1000
    }
    cache 30
    loop
    reload
    loadbalance
}

絶対に守る 2 点

  • fallthrough を必ず記述する — 省略すると hosts ブロックでマッチしなかった全クエリが SERVFAIL になり、Pod 間の Service 名解決を含むクラスタ全体の名前解決が壊滅する
  • reload プラグインを既存 Corefile から削除しない — ConfigMap 変更後に Corefile を最大 2 分で自動再読み込みする機能。即時反映が必要な場合は kubectl rollout restart -n kube-system deployment/coredns を併用する

やってみよう①: Gateway API CRD + Traefik インストール

Gateway API v1.4.0 standard channel CRD を kubectl apply --server-side で適用し、Traefik chart v39.0.0 を Helm でインストールします。完了後、GatewayClass が ACCEPTED: True、Traefik Service が MetalLB EXT IP を保持していることを確認します。作業場所は k8s-ops(developer ユーザー)です。

Step 1: Gateway API v1.4.0 standard channel CRD を適用する

実行コマンド:

$ kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml

実行結果:

customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/grpcroutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/referencegrants.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/backendtlspolicies.gateway.networking.k8s.io serverside-applied

実行コマンド:

$ kubectl get crds | grep gateway.networking.k8s.io

実行結果:

backendtlspolicies.gateway.networking.k8s.io   2026-05-24T08:01:00Z
gatewayclasses.gateway.networking.k8s.io       2026-05-24T08:01:00Z
gateways.gateway.networking.k8s.io             2026-05-24T08:01:00Z
grpcroutes.gateway.networking.k8s.io           2026-05-24T08:01:00Z
httproutes.gateway.networking.k8s.io           2026-05-24T08:01:00Z
referencegrants.gateway.networking.k8s.io      2026-05-24T08:01:00Z

Step 2: traefik-values.yaml を作成する

実行コマンド:

$ cat > traefik-values.yaml <<'EOF'
providers:
  kubernetesIngress:
    enabled: false
  kubernetesGateway:
    enabled: true
    namespaces: []
    experimentalChannel: false

gateway:
  enabled: true
  namespacePolicy: All

service:
  type: LoadBalancer

ports:
  web:
    port: 8000
    expose:
      default: true
    exposedPort: 80
  websecure:
    port: 8443
    expose:
      default: true
    exposedPort: 443
EOF

Step 3: Traefik Helm リポジトリを追加して chart v39.0.0 をインストールする

実行コマンド:

$ helm repo add traefik https://traefik.github.io/charts
$ helm repo update

実行結果:

"traefik" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "traefik" chart repository
Update Complete. Happy Helming!

実行コマンド:

$ helm upgrade --install traefik traefik/traefik \
    --namespace traefik \
    --create-namespace \
    --version 39.0.0 \
    -f traefik-values.yaml

実行結果:

Release "traefik" does not exist. Installing it now.
NAME: traefik
LAST DEPLOYED: Sun May 24 17:05:12 2026
NAMESPACE: traefik
STATUS: deployed
REVISION: 1
TEST SUITE: None

Step 4: GatewayClass の ACCEPTED 状態を確認する

実行コマンド:

$ kubectl get gatewayclasses

実行結果:

NAME      CONTROLLER                      ACCEPTED   AGE
traefik   traefik.io/gateway-controller   True       2m

ACCEPTED: True は、Traefik コントローラーが GatewayClass を認識し管理対象として受け入れたことを意味します。False または空欄の場合は Traefik Pod の起動状態を kubectl get pods -n traefik で確認します。

Step 5: Traefik Pod と MetalLB EXT IP を確認する

実行コマンド:

$ kubectl get pods -n traefik

実行結果:

NAME                       READY   STATUS    RESTARTS   AGE
traefik-7f5b8c8d6f-z2x4n   1/1     Running   0          2m

実行コマンド:

$ kubectl get service -n traefik

実行結果:

NAME      TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)                      AGE
traefik   LoadBalancer   10.107.215.117   192.168.1.200   80:31234/TCP,443:32567/TCP   2m

EXTERNAL-IP: 192.168.1.200 は MetalLB の IPAddressPool(第9回で設定済み)から払い出された VIP です。この IP に対して :80 と :443 の両ポートが Traefik Pod の :8000 / :8443 へ転送されます。

Step 6: 自動生成された Gateway を確認する

実行コマンド:

$ kubectl get gateway -n traefik

実行結果:

NAME              CLASS     ADDRESS         PROGRAMMED   AGE
traefik-gateway   traefik   192.168.1.200   True         2m

Helm が gateway.enabled: true 設定に従い Gateway リソースを自動生成しています。PROGRAMMED: True は Gateway が Traefik コントローラーによって実体化(リスナーポート確保)されたことを意味します。自動生成される Gateway 名は chart の Release 名(traefik)に -gateway を付した traefik-gateway です。なお本回ではこの後、TLS リスナー(websecure)と証明書参照を明示的に定義した Gateway(名前 traefik)を別途 apply し、HTTPRoute はその明示 Gateway(traefik)を parentRefs で参照します。

やってみよう②: cert-manager + Certificate + TLS Secret 確認

cert-manager v1.20.0 を Helm でインストールし、self-signed CA 2 段階方式で fanclub-tls Certificate を発行します。完了後、fanclub Namespace に fanclub-tls Secret が生成され、READY: True 状態になることを確認します。

Step 1: Jetstack Helm リポジトリを追加して cert-manager をインストールする

実行コマンド:

$ helm repo add jetstack https://charts.jetstack.io
$ helm repo update

実行結果:

"jetstack" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jetstack" chart repository
Update Complete. Happy Helming!

実行コマンド:

$ helm upgrade --install cert-manager jetstack/cert-manager \
    --namespace cert-manager \
    --create-namespace \
    --version v1.20.0 \
    --set crds.enabled=true

実行結果:

Release "cert-manager" does not exist. Installing it now.
NAME: cert-manager
LAST DEPLOYED: Sun May 24 17:15:42 2026
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
NOTES:
cert-manager v1.20.0 has been deployed successfully!

Step 2: cert-manager Pod の起動を確認する

実行コマンド:

$ kubectl get pods -n cert-manager

実行結果:

NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-7c6dd44d8c-h5n8r              1/1     Running   0          90s
cert-manager-cainjector-5c5f6d4f9b-8z2qb   1/1     Running   0          90s
cert-manager-webhook-6f9c8d4b7c-jx5fp      1/1     Running   0          90s

3 Pod(controller / cainjector / webhook)が全て Running になるまで待ちます。webhook が Running になる前に ClusterIssuer を作成しようとすると admission webhook エラーで失敗します。

Step 3: selfsigned-bootstrap ClusterIssuer を作成する

実行コマンド:

$ kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-bootstrap
spec:
  selfSigned: {}
EOF

実行結果:

clusterissuer.cert-manager.io/selfsigned-bootstrap created

Step 4: ルート CA Certificate(fanclub-ca)を作成する

実行コマンド:

$ kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: fanclub-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: fanclub-local-ca
  secretName: fanclub-ca-secret
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned-bootstrap
    kind: ClusterIssuer
    group: cert-manager.io
EOF

実行結果:

certificate.cert-manager.io/fanclub-ca created

実行コマンド:

$ kubectl get certificate -n cert-manager

実行結果:

NAME         READY   SECRET              AGE
fanclub-ca   True    fanclub-ca-secret   15s

Step 5: fanclub-ca-issuer ClusterIssuer を作成する

実行コマンド:

$ kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: fanclub-ca-issuer
spec:
  ca:
    secretName: fanclub-ca-secret
EOF

実行結果:

clusterissuer.cert-manager.io/fanclub-ca-issuer created

実行コマンド:

$ kubectl get clusterissuers

実行結果:

NAME                   READY   AGE
fanclub-ca-issuer      True    10s
selfsigned-bootstrap   True    3m

Step 6: fanclub-tls Certificate を作成する

実行コマンド:

$ kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: fanclub-tls
  namespace: fanclub
spec:
  secretName: fanclub-tls
  duration: 2160h
  renewBefore: 720h
  privateKey:
    algorithm: ECDSA
    size: 256
  dnsNames:
    - fanclub.local
  issuerRef:
    name: fanclub-ca-issuer
    kind: ClusterIssuer
    group: cert-manager.io
EOF

実行結果:

certificate.cert-manager.io/fanclub-tls created

実行コマンド:

$ kubectl get certificate -n fanclub

実行結果:

NAME          READY   SECRET        AGE
fanclub-tls   True    fanclub-tls   30s

Step 7: TLS Secret の中身を確認する

実行コマンド:

$ kubectl get secret fanclub-tls -n fanclub -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -issuer -dates

実行結果:

subject=CN = fanclub.local
issuer=CN = fanclub-local-ca
notBefore=May 24 08:15:00 2026 GMT
notAfter=Aug 22 08:15:00 2026 GMT

subject に fanclub.local、issuer に fanclub-local-ca(Step 4 で作成したルート CA)が表示され、有効期限が 90 日後(duration: 2160h = 90 日)に設定されていることを確認します。

やってみよう③: HTTPRoute + CoreDNS hosts + https://fanclub.local 確認

HTTPRoute を作成して fanclub-api への hostname ルーティングを設定し、CoreDNS hosts プラグインで fanclub.local の名前解決を追加します。最後に curl -k https://fanclub.local で HTTP 200 OK が返ることを確認します。これが第11回および第3部「ネットワーク」完走の達成状態です。

Step 0: fanclub-frontend をデプロイする

HTTPRoute のルーティング先となる frontend(Nginx + 静的 HTML)をデプロイします。第4回の fanclub-api chart は backend のみを含むため、frontend はここで Deployment + Service(fanclub-frontend-svc)として追加します。静的 HTML は ConfigMap で配信します。実行コマンド(k8s-ops):

$ kubectl create configmap fanclub-frontend-html -n fanclub \
    --from-literal=index.html='<!DOCTYPE html><html><head><title>fanclub</title></head><body><h1>Welcome to fanclub</h1></body></html>'
$ kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-frontend
  namespace: fanclub
spec:
  replicas: 1
  selector:
    matchLabels:
      app: fanclub-frontend
  template:
    metadata:
      labels:
        app: fanclub-frontend
    spec:
      containers:
      - name: nginx
        image: 192.168.1.123:5000/nginx:1.27
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
          readOnly: true
      volumes:
      - name: html
        configMap:
          name: fanclub-frontend-html
---
apiVersion: v1
kind: Service
metadata:
  name: fanclub-frontend-svc
  namespace: fanclub
spec:
  selector:
    app: fanclub-frontend
  ports:
  - port: 80
    targetPort: 80
EOF

kubectl rollout status deploy/fanclub-frontend -n fanclubRunning を確認します。この fanclub-frontend-svc が後続の HTTPRoute の backendRefs ルーティング先になります。frontend は Nginx 1.27 + 静的 HTML 構成で、本番では CRUD UI(Bootstrap + Vanilla JS・第1巻で実装)を同梱した専用イメージを使います。なお backend の API は HTTPRoute で /api プレフィックスを別 backendRefs に振り分ける構成へ拡張できます。

Step 1: Gateway を TLS リスナー付きに上書きする

Helm が自動生成した Gateway(traefik-gateway)は HTTP リスナーのみを持つため、HTTPS リスナー(certificateRefs: fanclub-tls)を備えた Gateway を別途新規作成します。この明示 Gateway の名前は traefik(Namespace も traefik)で、自動生成された traefik-gateway とは別リソースとして並存します。後続の HTTPRoute はこの明示 traefik Gateway を parentRefs で参照します。

実行コマンド:

$ kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: traefik
  namespace: traefik
spec:
  gatewayClassName: traefik
  listeners:
    - name: web
      port: 8000
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: All
    - name: websecure
      port: 8443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - name: fanclub-tls
            namespace: fanclub
      allowedRoutes:
        namespaces:
          from: All
EOF

実行結果:

gateway.gateway.networking.k8s.io/traefik created

Gateway が異 Namespace(fanclub)の Secret を参照しているため、ReferenceGrant が必要です。次の Step で適用します。

Step 2: ReferenceGrant で Namespace 間 Secret 参照を許可する

実行コマンド:

$ kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: traefik-to-fanclub-tls
  namespace: fanclub
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: Gateway
      namespace: traefik
  to:
    - group: ""
      kind: Secret
      name: fanclub-tls
EOF

実行結果:

referencegrant.gateway.networking.k8s.io/traefik-to-fanclub-tls created

実行コマンド:

$ kubectl get gateway traefik -n traefik

実行結果:

NAME      CLASS     ADDRESS         PROGRAMMED   AGE
traefik   traefik   192.168.1.200   True         8m

Step 3: fanclub-httproute HTTPRoute を作成する

実行コマンド:

$ kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: fanclub-httproute
  namespace: fanclub
spec:
  parentRefs:
    - name: traefik
      namespace: traefik
      sectionName: websecure
  hostnames:
    - fanclub.local
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: fanclub-frontend-svc
          port: 80
EOF

実行結果:

httproute.gateway.networking.k8s.io/fanclub-httproute created

Step 4: HTTPRoute の Accepted / ResolvedRefs を確認する

実行コマンド:

$ kubectl describe httproute fanclub-httproute -n fanclub

実行結果(抜粋):

Name:         fanclub-httproute
Namespace:    fanclub
API Version:  gateway.networking.k8s.io/v1
Kind:         HTTPRoute
Spec:
  Hostnames:
    fanclub.local
  Parent Refs:
    Name:        traefik
    Namespace:   traefik
    Section Name: websecure
Status:
  Parents:
    Conditions:
      Type:    Accepted
      Status:  True
      Reason:  Accepted
      Type:    ResolvedRefs
      Status:  True
      Reason:  ResolvedRefs
    Controller Name:  traefik.io/gateway-controller

Accepted: TrueResolvedRefs: True の両方が表示されれば HTTPRoute は Gateway にバインドされ、backendRef(fanclub-frontend-svc)も正しく解決されています。Accepted: False の場合は Gateway の allowedRoutes.namespaces.from 設定(All でなければならない)を疑います。

Step 5: CoreDNS ConfigMap に hosts ブロックを追加する

kubectl edit configmap coredns -n kube-system で開き、kubernetes プラグインブロックの直後に hosts ブロックを追加します。

実行コマンド:

$ kubectl edit configmap coredns -n kube-system

追記する hosts ブロック:

    hosts {
       192.168.1.200 fanclub.local
       fallthrough
    }

編集後の Corefile 全体を確認します。

実行コマンド:

$ kubectl get configmap coredns -n kube-system -o jsonpath='{.data.Corefile}'

実行結果:

.:53 {
    errors
    health {
       lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
       ttl 30
    }
    hosts {
       192.168.1.200 fanclub.local
       fallthrough
    }
    prometheus :9153
    forward . /etc/resolv.conf {
       max_concurrent 1000
    }
    cache 30
    loop
    reload
    loadbalance
}

Step 6: CoreDNS を rollout restart して即時反映する

実行コマンド:

$ kubectl rollout restart -n kube-system deployment/coredns
$ kubectl rollout status -n kube-system deployment/coredns

実行結果:

deployment.apps/coredns restarted
Waiting for deployment "coredns" rollout to finish: 1 of 2 updated replicas are available...
deployment "coredns" successfully rolled out

reload プラグインが入っているため最大 2 分待てば自動反映されますが、確実な反映のため rollout restart を実行します。

Step 7: クラスタ内 Pod から nslookup で名前解決を確認する

実行コマンド:

$ kubectl run dns-test --image=192.168.1.123:5000/busybox:1.36 --rm -it --restart=Never -- nslookup fanclub.local

実行結果:

Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      fanclub.local
Address 1: 192.168.1.200
pod "dns-test" deleted

fanclub.local192.168.1.200(Traefik LoadBalancer EXT IP)に解決されることを確認しました。これでクラスタ内 Pod から fanclub.local 経由で fanclub-api に到達できる状態です。

Step 8: k8s-ops から curl で完成形を確認する

k8s-ops では /etc/hosts192.168.1.200 fanclub.local を追記してから curl を実行します(k8s-ops は作業端末 VM であり、Pod ネットワーク経由でクラスタ内 CoreDNS を参照する手段がないため、ホスト側の名前解決を直接設定します)。

実行コマンド(k8s-ops 上・root で実行):

# echo "192.168.1.200 fanclub.local" >> /etc/hosts

実行コマンド(k8s-ops 上・developer で実行):

$ curl -k -I https://fanclub.local

実行結果:

HTTP/2 200
content-type: text/html
content-length: 103
date: Sun, 24 May 2026 08:25:13 GMT

実行コマンド:

$ curl -k https://fanclub.local

実行結果:

<!DOCTYPE html><html><head><title>fanclub</title></head><body><h1>Welcome to fanclub</h1></body></html>

HTTP/2 200 が返り、Step 0 で ConfigMap から配信した fanclub-frontend の HTML(Welcome to fanclub)が取得できました。第11回および第3部「ネットワーク」の達成状態に到達しました。-k オプションは self-signed CA の警告を無視するためで、本番では ca.crt をブラウザに信頼ルートとして追加すれば -k なしでも警告なくアクセスできます。本番の frontend では Step 0 で触れた CRUD UI(Bootstrap + Vanilla JS・第1巻実装)を同梱した専用イメージに差し替えます。

まとめ・現場ヒヤリハット・理解度チェック

第11回のまとめ

  • Gateway API は GatewayClass(コントローラー)→ Gateway(リスナー)→ HTTPRoute(ルール)の 3 層で責務を分離する。ingress-nginx の「1 リソースにアノテーション集約」より標準化された Role-oriented 設計
  • kubectl apply --server-side は CRD サイズが大きい場合の事実上の必須オプション。省略するとアノテーションサイズ超過(256 KiB 上限)で apply 失敗する
  • cert-manager の ClusterIssuer はクラスタスコープ、Issuer は Namespace スコープ。CA Certificate の Secret は cert-manager Namespace に置く
  • self-signed CA 2 段階方式(selfsigned-bootstrap → fanclub-ca → fanclub-ca-issuer → fanclub-tls)でルート CA Secret を取り出してブラウザに信頼追加できる
  • CoreDNS hosts プラグインには fallthrough を必ず記述する。省略するとマッチしなかった全クエリが SERVFAIL になり、クラスタ全体の名前解決が壊滅する
  • Gateway が異 Namespace の Secret を参照する場合は ReferenceGrant が必要。Gateway 側ではなく Secret 側 Namespace に作成する
  • allowedRoutes.namespaces.from: All の設定漏れは HTTPRoute が Accepted: false になる最多の原因
  • MetalLB L2 モードはシングルノードボトルネック。本番では BGP モード + ECMP に移行する

次回予告

第12回では PV / PVC / StorageClass + Longhorn v1.11.1 構築 + fanclub-db ボリューム移行を扱います。第3部「ネットワーク」を完走した第11回までは、データ層は emptyDir または hostPath で動いていました。第12回では Longhorn 分散ストレージを導入し、Workload Node 障害時でもデータが失われない耐障害性を確保します。第4部「ストレージ」の単独回です。

現場ヒヤリハット①: kubectl apply で Gateway API CRD apply が「Too long」で失敗した

状況: Gateway API v1.4.0 の standard-install.yaml を新人エンジニアが kubectl apply -f で適用しようとしたところ、以下のエラーで失敗しました。

The CustomResourceDefinition "gateways.gateway.networking.k8s.io" is invalid:
metadata.annotations: Too long: must have at most 262144 bytes

原因:

  • client-side apply は Last-Applied アノテーションに CRD YAML 全文を格納する
  • Gateway API の CRD は 1 リソースあたり数百 KiB あり、Kubernetes のアノテーションサイズ上限(262144 バイト = 256 KiB)を超えた
  • Gateway API 公式ドキュメントの最初のページに「--server-side 推奨」と書いてあるが、新人は「とりあえず apply」で進めてしまった

修正: --server-side フラグを追加して再実行します。

$ kubectl apply --server-side -f standard-install.yaml

教訓: CRD 適用は --server-side を標準とします。チームの kubeadm 環境ブートストラップ手順書には「Gateway API CRD / cert-manager CRD は --server-side 必須」と明記し、新人がハマる前にレビュー段階で気づける状態にしておきます。

現場ヒヤリハット②: HTTPRoute が Accepted: false でルーティングが機能しない

状況: HTTPRoute を作成して curl -k https://fanclub.local を実行したところ 404 Not Found が返ってきました。Traefik Pod は Running、Gateway は PROGRAMMED: True、HTTPRoute も作成されていました。

原因: kubectl describe httproute fanclub-httproute -n fanclub で確認すると Accepted: False / Reason NotAllowedByListeners が表示されました。Gateway の allowedRoutes.namespaces.from がデフォルトの Same のままで、Gateway(traefik Namespace)と異なる Namespace(fanclub)の HTTPRoute を拒否していました。

修正: Gateway の各 listener に allowedRoutes.namespaces.from: All を追加します。

listeners:
  - name: websecure
    port: 8443
    protocol: HTTPS
    allowedRoutes:
      namespaces:
        from: All
    tls:
      mode: Terminate
      certificateRefs:
        - name: fanclub-tls
          namespace: fanclub

教訓: Gateway API では Namespace 跨ぎが頻出します。HTTPRoute が Accepted: false の症状を見たら、まず allowedRoutes.namespaces.from を確認します。デフォルトの Same は「同一 Namespace のみ」なので、マルチテナント運用や fanclub のような分離型構成では All または Selector(labels で許可 Namespace を絞る)が必要です。

現場ヒヤリハット③: CoreDNS hosts プラグインに fallthrough を書き忘れて全クラスタ DNS が崩壊した

状況: CoreDNS ConfigMap に hosts ブロックを追加した直後、全 Pod から「DNS が引けない」と障害連絡が殺到しました。Service 名による Pod 間通信、kube-apiserver への接続、外部ドメインの解決、すべてが失敗していました。

原因: hosts ブロックに fallthrough を書き忘れていました。

hosts {
   192.168.1.200 fanclub.local
}
  • hosts プラグインは「マッチしたら応答、マッチしなかったらクエリを終了する」が既定動作
  • fallthrough を書かないと、fanclub.local 以外のクエリがすべて SERVFAIL になる
  • kubernetes プラグイン(cluster.local)や forward プラグイン(外部 DNS)に処理が引き渡されない

修正: hosts ブロックの末尾に fallthrough を追加します。

hosts {
   192.168.1.200 fanclub.local
   fallthrough
}

教訓: CoreDNS の hosts プラグインを使うときは fallthrough をテンプレート化します。「CoreDNS hosts プラグインの追加手順書」には fallthrough を必ず記述し、変更レビューでも fallthrough 行の有無を必須チェック項目にします。本番 ConfigMap の変更前には必ず kubectl get configmap coredns -n kube-system -o yaml > backup.yaml でバックアップを取り、障害時に即ロールバックできる体制を整えます。

理解度チェック(○×形式・7 問)

各問に対して○か×で答え、解説を確認してください。

#問題文正解解説
Q1GatewayClass は Namespace スコープのリソースである×GatewayClass はクラスタスコープ。Gateway がリスナーポートを持つ Namespace スコープのリソース
Q2Gateway API standard channel の CRD は kubectl apply --server-side で適用することが推奨されるCRD サイズが client-side apply のアノテーション上限を超えるため、server-side apply が公式推奨
Q3allowedRoutes.namespaces.from: Same を設定した Gateway は、別の Namespace にある HTTPRoute もルーティング対象にする×Same は同一 Namespace のみ。別 Namespace からは All または Selector が必要
Q4cert-manager の ClusterIssuer は特定の Namespace に属し、その Namespace 内の Certificate だけを発行できる×ClusterIssuer はクラスタスコープ。Namespace を問わず Certificate を発行可能
Q5CoreDNS の hosts プラグインで fallthrough を省略すると、hosts ブロックにマッチしないすべてのクエリへの応答が失敗するfallthrough を省略すると hosts でマッチしなかった場合に次のプラグインへ渡されず SERVFAIL になる
Q6MetalLB L2 モードでは VIP のトラフィックが複数のノードに ECMP で分散される×L2 モードは 1 ノードがすべてのトラフィックを受け取るシングルノードボトルネック。ECMP は BGP モードの特徴
Q7ingress-nginx は Kubernetes 公式の標準 Ingress Controller として 2026 年現在も活発にメンテナンスされている×ingress-nginx は 2026 年 3 月に EOL。本シリーズ(2026 年)では Gateway API + Traefik を採用

シリーズ一覧

第1部:クラスタ構築

第2部:ワークロード管理

第3部:ネットワーク

第4部:ストレージ

第5部:監視・運用

第6部:トラブルシュート

広告
kubernetes
スポンサーリンク