インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

CKA編⑪ Gateway APIでHTTPS公開

公開更新

新卒インフラエンジニア向け「Kubernetes 実践教科書 ② CKA クラスタ構築・運用編」(全16回)の第11回です。第4回で fanclub-api を kind から HA クラスタへ移したとき、外部公開だけは kubectl port-forward で我慢していました。第9回MetalLB が外部 IP を配れるようになり、第10回NetworkPolicy の通し方を押さえた今、いよいよブラウザの https://fanclub.local を復活させます。使う道具は 4 つ——Gateway API(ルーティングの標準 API)・Traefik(その実装)・cert-manager(TLS 証明書の発行)・CoreDNS(クラスタ内 DNS)です。第2巻ネットワークの総仕上げになります。

操作は作業端末 k8s-ops192.168.1.122)から kubectlhelm で行います。プロンプトの $ は一般ユーザー developer、第4回で配布した admin.conf(cluster-admin)を使います。本回で作る YAML は developer のホーム配下の作業ディレクトリにまとめます(所有者は developersudo は使いません)。以降の kubectl applyhelm は、このディレクトリにいる前提の相対パスです(fanclub の chart を扱う最後の節だけ ~ へ移動します)。

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

$ mkdir -p ~/gateway && cd ~/gateway
目次
  1. 今ここマップ(全 16 回中の現在地)
  2. この回のゴール
  3. 全体像:HTTPS が復活するまでの経路
  4. Gateway API を理解する
  5. Traefik を Gateway API の実装として設置する
  6. cert-manager で TLS 証明書を用意する
  7. CoreDNS をカスタマイズする(3 層の名前解決)
  8. fanclub の Gateway を有効化して HTTPS を復活させる
  9. やってみよう
  10. まとめ
  11. 理解度チェック(○×形式・全 9 問)
  12. 次回予告

今ここマップ(全 16 回中の現在地)

本シリーズは 6 部構成です。現在地は第3部「ネットワーク」の第11回、この部の最終回です。

  • 第1部 クラスタ構築(第1〜5回)
  • 第2部 ワークロード管理(第6〜8回)
  • 第3部 ネットワーク(第9〜11回)← 今ここ
  • 第4部 ストレージ(第12回
  • 第5部 監視・運用(第13〜14回)
  • 第6部 トラブルシュート(第15〜16回)
  • Kubernetes v1.36.2
  • kubectl v1.35.6(作業端末)
  • Gateway API v1.4.0
  • Traefik chart 39.0.9(Traefik v3.6.15)
  • cert-manager v1.20.0
  • MetalLB v0.16.1
  • Helm v4.1.4
  • busybox 1.37
  • AlmaLinux 10.2
  • Gateway API の gateway.networking.k8s.io/v1 は安定版で、Kubernetes のマイナー番号が変わっても同じ
  • 確認日 2026-07-16

この回のゴール

本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。

  • Gateway API の 3 リソース(GatewayClass/Gateway/HTTPRoute)の役割分担を説明できる
  • Traefik を Gateway API の実装として HA クラスタに設置できる
  • cert-manager の自己署名 CA で TLS 証明書を発行できる
  • CoreDNS の ConfigMap を編集してクラスタ内 DNS をカスタマイズできる
  • 3 層の名前解決(ホスト OS/VM/Pod)を切り分けて説明できる
  • https://fanclub.local をブラウザで動作確認できる

全体像:HTTPS が復活するまでの経路

先に完成形の経路を頭に入れておきます。ブラウザから fanclub の画面が出るまでに、パケットは次の順に流れます。

  1. ブラウザ(読者のホスト OS)fanclub.localホスト OS の /etc/hosts192.168.1.124(k8s-lb)に解決する。
  2. HAProxy(k8s-lb・.124:443)第3回gateway-https として定義済みの入口。転送先は 192.168.1.200:443
  3. Traefik(.200)MetalLB(第9回)がプール先頭の .200 を払い出す LoadBalancer Service。Gateway API の実装として動く。
  4. Gateway(TLS 終端):cert-manager が発行した証明書で HTTPS を終端し、HTTPRoute の規則に従って振り分ける。
  5. fanclub-frontend(80):静的画面を返す。/api は frontend の Nginx が backend(8080)へ中継し、backend が db(5432)を読む。

ここで重要なのは、この経路の両端は既に用意済みだという点です。.124 の HAProxy は第3回で traefik-https backend まで書いてあり(当時は転送先が居ないので DOWN でした)、.200 を配る MetalLB は第9回で入れました。本回で埋めるのはその間の Traefik・Gateway・証明書だけです。k8s-lb の設定を触る必要はありません。

ブラウザから fanclub-db までの HTTPS 経路図。ホスト OS の /etc/hosts が fanclub.local を 192.168.1.124 の HAProxy へ向け、HAProxy が MetalLB の払い出した 192.168.1.200 の Traefik へ転送し、Traefik を実装とする Gateway が cert-manager 発行の証明書で TLS を終端、HTTPRoute が fanclub-frontend へ振り分けて frontend から backend・db へ至る流れを示した図。HAProxy(第3回)と MetalLB(第9回)は既設で、本回が埋めるのは Traefik・Gateway・証明書であること、default-deny の fanclub 名前空間へは NetworkPolicy が Gateway からの通信だけを通すことも併記している
図1:ブラウザから fanclub-db までの HTTPS 経路

Gateway API を理解する

Gateway API は、HTTP の入口とルーティングを扱う標準 APIです。旧来の Ingress が「1 つのリソースにアノテーションで実装依存の設定を詰め込む」一枚岩だったのに対し、Gateway API は役割を 3 つに分離したのが最大の違いです。

  • GatewayClassどの実装を使うかを示す。本巻では Traefik。クラスタ管理者が用意する(クラスタ全体のリソース)。ストレージの StorageClass に相当する位置づけです。
  • Gateway入口の定義。どのポートで待ち受け、どのホスト名を扱い、TLS をどう終端するか。基盤担当が用意する。
  • HTTPRoute振り分けの規則。どのパス/ホストをどの Service に流すか。アプリ担当が用意する。

この分離により、1 つの Gateway(入口)を複数チームが HTTPRoute で分担して使えるようになります。基盤担当は入口と証明書だけを管理し、アプリ担当は自分のパスの振り分けだけを書く——運用者視点ではこの責務分割が Gateway API を選ぶ理由になります。

これら 3 つは Kubernetes の標準リソースではなく、CRD(CustomResourceDefinition)として後から追加する拡張です。CKA の D1 には「CRD を理解し、operator をインストール・設定する」が含まれます。まず CRD を適用します。マニフェストは GitHub のリリースから直接取得します(kubectldeveloper のシェルに設定済みのプロキシ経由で取りに行きます。第8回の metrics-server と同じ経路で、whitelist は通してあります)。

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

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

実行結果(抜粋):

customresourcedefinition.apiextensions.k8s.io/backendtlspolicies.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/grpcroutes.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/referencegrants.gateway.networking.k8s.io created

standard-install.yaml は安定版(standard channel)の CRD 一式です。追加された CRD を確認します。

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

$ kubectl get crds | grep gateway.networking

実行結果:

backendtlspolicies.gateway.networking.k8s.io            2026-07-16T12:37:06Z
gatewayclasses.gateway.networking.k8s.io                2026-07-16T12:37:06Z
gateways.gateway.networking.k8s.io                      2026-07-16T12:37:06Z
grpcroutes.gateway.networking.k8s.io                    2026-07-16T12:37:06Z
httproutes.gateway.networking.k8s.io                    2026-07-16T12:37:07Z
referencegrants.gateway.networking.k8s.io               2026-07-16T12:37:07Z

ただし、CRD を入れただけでは何も起きません。CRD は「そういう種類のリソースを API Server に登録した」だけで、Gateway を見て実際にパケットを通すコントローラが別に要ります。それが次に入れる Traefik です。「CRD で語彙を増やし、コントローラがそれを見て実体を作る」——これが operator パターンで、第14回の ArgoCD でも同じ形が出てきます。

なお CKA ではIngress リソースも出題範囲です。本巻は 2026 年 3 月に EOL を迎えた ingress-nginx を採らず Gateway API を主軸にしますが、試験では Ingress を読み書きする場面がありえるので、同じ振り分けを Ingress で書くとどうなるかを対比で押さえておきます。次は、この後 chart が作る HTTPRoute(fanclub.local/fanclub-frontend:80 へ)と等価な内容を、旧 Ingress で書いた場合の全量です(本巻では Ingress コントローラを入れないため、適用はせず読み方の確認に留めます)。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: fanclub-ingress
  namespace: fanclub
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - fanclub.local
    secretName: fanclub-tls
  rules:
  - host: fanclub.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: fanclub-frontend
            port:
              number: 80

1 つのリソースに「実装の指定(ingressClassName)・TLS・ホスト・パス・転送先」が全部入りになっているのが分かります。Gateway API はこれを 3 つに割ったので、次のように対応します。

  • ingressClassName: nginxGatewayClass(どの実装か)
  • tls:host:Gateway のリスナー(入口・TLS 終端)
  • rules[].http.paths[]HTTPRoute(パスと転送先)

試験では「どちらで問われても、この対応関係を辿って読み替えられる」ことが要点です。Ingress の細かなアノテーション(実装ごとに異なる方言)まで覚える必要はありません。

Traefik を Gateway API の実装として設置する

Traefik を入れる前に、第9回で残した土台が生きていることを確認します。MetalLB 本体(controller 1 + speaker 5 の 6 Pod)と、IP プール lan-pool・広告設定 lan-l2 です。

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

$ kubectl get pods -n metallb-system
$ kubectl get ipaddresspool,l2advertisement -n metallb-system

実行結果(6 Pod と、プール 192.168.1.200-210):

NAME                                  READY   STATUS    RESTARTS   AGE
metallb-controller-75f6b7f996-dzzcn   1/1     Running   0          2d
metallb-speaker-4vxqz                 1/1     Running   0          2d
metallb-speaker-6sqwl                 1/1     Running   0          2d
metallb-speaker-944r5                 1/1     Running   0          2d
metallb-speaker-b4vfx                 1/1     Running   0          2d
metallb-speaker-mpzfp                 1/1     Running   0          2d

NAME                                AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
ipaddresspool.metallb.io/lan-pool   true          false             ["192.168.1.200-192.168.1.210"]

NAME                                IPADDRESSPOOLS   IPADDRESSPOOL SELECTORS   INTERFACES
l2advertisement.metallb.io/lan-l2   ["lan-pool"]

第9回で lb-test を削除して以降、LoadBalancer タイプの Service は 1 つもありません(kubectl get svc -A | grep LoadBalancer が何も返さないことで確認できます)。つまりプール先頭の .200 が空いている状態です。ここに Traefik が最初の LoadBalancer Service として現れます。

Traefik を Helm で導入します。ここで名前空間の指定が重要です。第10回で読み解いたとおり、fanclub は default-deny のゼロトラスト構成で、Gateway を有効にすると chart が allow-frontend-from-gateway というポリシーを追加します。その中身は「名前空間 traefik から frontend の 80 番への Ingress を許可」(namespaceSelectorkubernetes.io/metadata.name: traefik を指定)です。つまり Traefik を traefik 以外の名前空間に置くと、default-deny に阻まれて HTTPS は通りません。名前空間名を合わせることが前提条件になります。

Traefik の設定値を vi などのエディタで ~/gateway/traefik-values.yaml として作成します。変更点にはコメントを付けています。

providers:
  kubernetesGateway:
    enabled: true          # Gateway API プロバイダを有効化(本回の主役)
  kubernetesIngress:
    enabled: false         # 旧 Ingress は使わないので無効
gateway:
  enabled: false           # chart 付属の Gateway は作らない(Gateway は fanclub chart 側が持つ)
gatewayClass:
  enabled: true            # GatewayClass "traefik" を登録する
service:
  type: LoadBalancer       # MetalLB に外部 IP(.200)を払い出させる
ports:
  web:
    port: 80               # 既定 8000 → 80 へ(Gateway のリスナーと番号を一致させる)
  websecure:
    port: 443              # 既定 8443 → 443 へ(fanclub-gateway の HTTPS リスナーが 443 のため)
securityContext:
  capabilities:
    drop: [ALL]
    add: [NET_BIND_SERVICE]  # 1024 未満を bind するために必要(既定は drop [ALL] のみ)

gateway.enabled: false としているのは、Gateway(入口)は fanclub の chart が fanclub-gateway として作るためです。Traefik には「実装(GatewayClass)」と「LoadBalancer の受け口」の役割だけを持たせます。先ほど説明した役割分離が、そのまま設定値に現れています。

portssecurityContext の 2 つは、書かないと HTTPS が通らない要注意点なので、理由を押さえてください。Traefik は Gateway のリスナーのポート番号と、自分の entryPoint のポート番号が一致するものを探して結び付けます。ところが chart の既定では、entryPoint websecureコンテナ内で 8443 番を待ち受け(Service 側が 443→8443 に変換)ています。一方 fanclub の Gateway のリスナーは 443 番です。この番号がずれたまま進むと、後の節で Gateway を作っても PROGRAMMED が空欄のままになり、kubectl describe gateway fanclub-gateway -n fanclub の Conditions に次のように出ます(本回の手順どおり ports を書いていれば、この状態にはなりません)。

参考(ポート番号がずれている場合に describe で見える Conditions の抜粋):

Message: Cannot find entryPoint for Gateway: no matching entryPoint for port 443 and protocol "HTTPS"
Reason:  PortUnavailable
Status:  False
Type:    Accepted

そこで entryPoint 側を 80/443 に合わせます。ただし 1024 未満のポートを bind するには特権が要り、Traefik の Pod は非 root(UID 65532)・ケーパビリティ全 drop で動く設定になっています。そこで NET_BIND_SERVICE(低番号ポートの bind だけを許すケーパビリティ)を足します。root で動かすのではなく必要な 1 つだけを与える——第10回のホワイトリスト設計と同じ最小権限の考え方です。

Helm リポジトリを追加します。chart の配信元は traefik.github.io です(alma-proxy の whitelist に通してあります。第9回の MetalLB で metallb.github.io を追加したのと同じ考え方です)。第1巻で追加済みの場合は already exists と表示されるので、そのまま次へ進んでください。

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

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

install します。名前空間は traefik--create-namespace で作ります。namespaceSelector が使う kubernetes.io/metadata.name: traefik ラベルは、Kubernetes が Namespace 作成時に自動で付けるので手当ては不要です)。--version で chart を固定します。先に適用した Gateway API の CRD が前提です(GatewayClass や Gateway という種類を API Server が知らない状態では、Traefik は Gateway API プロバイダとして起動できません)。

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

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

実行結果(抜粋):

NAME: traefik
LAST DEPLOYED: Thu Jul 16 21:39:40 2026
NAMESPACE: traefik
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
traefik with docker.io/traefik:v3.6.15 has been deployed successfully on traefik namespace!

Pod が Running になり、Service に外部 IP が付くまで数十秒かかります(ContainerCreating<pending> のままなら少し待って再実行してください)。

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

$ kubectl get pods,svc -n traefik

実行結果(EXTERNAL-IP に MetalLB が .200 を払い出している):

NAME                           READY   STATUS    RESTARTS   AGE
pod/traefik-69f4cd68d8-dv9sm   1/1     Running   0          63s

NAME              TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
service/traefik   LoadBalancer   10.97.208.55   192.168.1.200   80:30594/TCP,443:30504/TCP   63s

第9回で作ったプール lan-pool192.168.1.200-210)の先頭が、そのまま Traefik に割り当てられました。プールは自動割り当て(AUTO ASSIGN)なので先頭の空きから配られます。第9回で lb-test を削除して .200 を返却してあるため、いま LoadBalancer Service を作った Traefik がその .200 を受け取ります。ここが .200 であることは後続の前提です(HAProxy の転送先が 192.168.1.200:443 固定のため)。もし別の IP が付いた場合は、他に LoadBalancer Service が残っていないかを kubectl get svc -A で確認してください。今回はこの IP を本番の入口として使い続けます。GatewayClass も確認します。

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

$ kubectl get gatewayclass

実行結果(ACCEPTED が True なら実装として使える):

NAME      CONTROLLER                      ACCEPTED   AGE
traefik   traefik.io/gateway-controller   True       63s

NAMEtraefik であることが重要です。fanclub chart の Gateway は gatewayClassName: traefik を指定しているので、この名前が一致していないと Gateway は誰にも処理されません

cert-manager で TLS 証明書を用意する

HTTPS には証明書が要ります。cert-manager は、Certificate リソースを見て証明書を発行し Secret に保存してくれるコントローラです(これも operator パターンです)。本番では Let’s Encrypt 等の公的な CA を使いますが、fanclub.local は外部から検証できない名前なので、自己署名の CA を自分で立ててそこから発行します。第1巻と同じ考え方ですが、リソースは作り直しになります——第1巻は kind クラスタの中に作ったもので、本巻の kubeadm クラスタには存在しません。名前と鍵の種類も本巻の値で揃え直します(bootstrap 用 Issuer は selfsigned-issuer、CA の Secret は fanclub-ca-key-pair、鍵は RSA 2048)。

こちらも Helm リポジトリを追加します(配信元 charts.jetstack.io は whitelist 済み。第1巻で追加済みなら already exists と表示されます)。

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

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

名前空間 cert-manager に install します。この名前空間名は変えずにおいてください——後で作る ClusterIssuer は、参照する Secret をこの名前空間から読むためです(理由は次の CA 作成のところで詳しく触れます)。

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

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

実行結果(抜粋):

NAME: cert-manager
LAST DEPLOYED: Thu Jul 16 21:42:41 2026
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
cert-manager v1.20.0 has been deployed successfully!

--set crds.enabled=true で cert-manager 自身の CRD(Certificate/Issuer/ClusterIssuer 等)も一緒に入ります。3 つの Pod(controller・webhook・cainjector)が Running になるのを待ちます。ここは待たずに次へ進まないでください——cert-manager は Issuer や Certificate を作るときに webhook Pod へ検証を投げるため、webhook が立ち上がる前に次の kubectl apply を打つと failed calling webhook で弾かれます(その場合は数十秒待って同じコマンドを打ち直せば通ります)。

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

$ kubectl get pods -n cert-manager

実行結果:

NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-7d964c57f9-txxp4              1/1     Running   0          87s
cert-manager-cainjector-565fdcf77d-6gd9k   1/1     Running   0          87s
cert-manager-webhook-6fc5fccc59-rsmp7      1/1     Running   0          87s

次に自己署名 CA を作ります。3 段構えになっている点が、初見でつまずきやすいところです。①自分自身に署名する Issuerselfsigned-issuer)→ ②それで CA 証明書を発行fanclub-caisCA: true)→ ③その CA を使う ClusterIssuerfanclub-ca-issuer)、という流れです。①は「CA 証明書自体を作るための踏み台」、③が「アプリの証明書を発行する窓口」です。次の内容を vi などのエディタで ~/gateway/ca-issuer.yaml として作成します(traefik-values.yaml と同じ ~/gateway です。別のシェルを開き直した場合は cd ~/gateway で戻ってください)。

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: fanclub-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: fanclub-ca
  secretName: fanclub-ca-key-pair
  privateKey:
    algorithm: RSA
    size: 2048
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: fanclub-ca-issuer
spec:
  ca:
    secretName: fanclub-ca-key-pair

2 点補足します。CA の Certificate を cert-manager 名前空間に置いているのは、ClusterIssuer が参照する Secret は cert-manager 自身の名前空間から読まれるためです。そして ClusterIssuer の名前は fanclub-ca-issuer で固定です——fanclub の chart が持つ Certificate が issuerRef: fanclub-ca-issuer を指定しているので、名前が違うと証明書が発行されません。

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

$ kubectl apply -f ca-issuer.yaml
$ kubectl get clusterissuer

実行結果(2 つとも READY が True):

NAME                READY   AGE
fanclub-ca-issuer   True    12s
selfsigned-issuer   True    12s

fanclub-ca-issuerTrue になるには、②の CA 証明書(Secret fanclub-ca-key-pair)の発行が先に終わっている必要があります。apply の直後は False に見えることがあるので、数秒待って引き直してください。それでも False のままなら kubectl describe clusterissuer fanclub-ca-issuer の Events で原因を読みます(Secret not found なら②がまだ、が典型です)。

これで証明書を発行する窓口ができました。fanclub.local の証明書そのものは、次の Gateway 有効化のときに chart 側の Certificate リソースが自動で発行します(本回で手動発行する必要はありません)。ブラウザからは自己署名のため警告が出ますが、これはラボ環境の割り切りです。

CoreDNS をカスタマイズする(3 層の名前解決)

最初に位置づけを明確にしておきます。この節は https://fanclub.local を成立させるために必須ではありません。読者のブラウザの名前解決はホスト OS が行うからです。それでも扱うのは、CKA の D3 に「CoreDNS を設定して使う」が独立した項目として含まれるからと、ここで3 層の名前解決を切り分けて理解するためです。この 3 つは混同しやすく、トラブル時に見る場所を間違える原因になります。

  • 読者のホスト OS からの fanclub.localホスト OS の /etc/hosts192.168.1.124=HAProxy)。第1巻第18回と同じ手法。alma-proxy の dnsmasq でも CoreDNS でもありません
  • VM(cp/wl/k8s-ops 等)の外向きの名前解決alma-proxy の dnsmasq192.168.1.121)。k8s-lb などのホスト名はここで引きます(第2・3回で触りました)。
  • クラスタ内の Pod の名前解決CoreDNS(Service kube-dns10.96.0.10)。fanclub-backend のような Service 名はここで引きます。本節でカスタムするのはこれ

CoreDNS の設定は kube-system の ConfigMap coredns にある Corefile です。まず現状を見ます。

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

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

実行結果(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 {
       disable success cluster.local
       disable denial cluster.local
    }
    loop
    reload
    loadbalance
}

プラグインが上から順に並んでいます。kubernetescluster.local(Service 名)を解決し、それ以外は forward でノードの /etc/resolv.conf(=dnsmasq)へ投げる、という構造です。ここに hosts プラグインを足して、クラスタ内の Pod から見た fanclub.local を Traefik の .200 に解決させます。Pod から自分のサービスを外部と同じ名前で呼びたい場合に使う手法です。

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

$ kubectl edit configmap coredns -n kube-system

KUBE_EDITOREDITOR を設定していなければ vi が開きます。ready の次の行に、次の 4 行を追加して保存します(kubernetes プラグインより前に置くのが定石です。fallthrough は「ここで解決できなければ後続のプラグインへ渡す」という意味で、これが無いと cluster.local も外向きの名前解決も壊れます)。インデントは既存の readykubernetes の行に合わせてください——Corefile は ConfigMap の中に文字列として入っているため、kubectl edit の画面では Corefile 全体がさらに字下げされて表示されます。字下げがずれると保存時に検証エラーになり、再編集を促されます。

    hosts {
       192.168.1.200 fanclub.local
       fallthrough
    }

Corefile には reload プラグインが入っているので、CoreDNS は変更を自動で読み直します(最大 2 分程度)。すぐに反映したい場合は再起動します。

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

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

実行結果:

deployment "coredns" successfully rolled out

使い捨ての busybox から引いて確認します(-n を付けていないので default 名前空間に作られます。--rm なので nslookup が終わると同時に消えます)。fanclub 名前空間は第10回の default-deny 下にあるため、こうした確認用の Pod は default で動かすのが安全です。

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

$ kubectl run dnstest --image=busybox:1.37 --rm -it --restart=Never -- nslookup fanclub.local

実行結果(CoreDNS が .200 を返す):

Server:		10.96.0.10
Address:	10.96.0.10:53

Name:	fanclub.local
Address: 192.168.1.200

pod "dnstest" deleted from default namespace

Pod から見た fanclub.local.200(Traefik)、ホスト OS から見た fanclub.local.124(HAProxy)——同じ名前でも、どこから引くかで解決先も解決する仕組みも違うということです。この切り分けができると、名前解決のトラブルで見る場所を間違えなくなります。なお、この hosts の追記はそのまま残して構いませんfallthrough があるので他の名前解決には影響しません)。元に戻したい場合は、同じく kubectl edit configmap coredns -n kube-system で 4 行を削除して保存します。

3 層の名前解決の役割分担図。読者のホスト OS は /etc/hosts で fanclub.local を 192.168.1.124 の HAProxy に解決し、VM は alma-proxy の dnsmasq(192.168.1.121)で k8s-lb などのホスト名を解決し、クラスタ内の Pod は CoreDNS(10.96.0.10)で Service 名と hosts プラグインで追加した fanclub.local(192.168.1.200)を解決することを、3 つの層に分けて示した図
図2:3 層の名前解決の役割分担(/etc/hosts・dnsmasq・CoreDNS)

fanclub の Gateway を有効化して HTTPS を復活させる

土台が揃いました。GatewayClass traefik・ClusterIssuer fanclub-ca-issuer・Traefik の .200——fanclub の chart が要求する参照先が全て存在します。第4回で --set gateway.enabled=false としていたスイッチを、ここで反転させます。--set frontend.image.tag=1.0.0 は第4回と同じく毎回付けます(省くと chart 既定のタグに戻ってしまいます)。

chart は第4回で developer のホーム直下に置いた ~/fanclub-chart です(ここまでの作業ディレクトリ ~/gateway とは別なので、cd ~ で移動してから実行します)。--namespace fanclub を付けるのは、第4回の helm install--namespace fanclub を指定した=リリース fanclub がその名前空間で管理されているためです(付け忘れると Error: release: not found になります)。

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

$ cd ~
$ helm upgrade fanclub ./fanclub-chart \
    --namespace fanclub \
    --set gateway.enabled=true \
    --set frontend.image.tag=1.0.0

実行結果(抜粋):

Release "fanclub" has been upgraded. Happy Helming!
NAME: fanclub
LAST DEPLOYED: Thu Jul 16 21:47:53 2026
NAMESPACE: fanclub
STATUS: deployed
REVISION: 2
DESCRIPTION: Upgrade complete
TEST SUITE: None

このスイッチ 1 つで、chart は 4 つのリソースを追加しました。REVISION が 2 に上がっていますが、既存の Pod(frontend/backend/db)は再起動しません——増えたのは Gateway 周辺のリソースだけで、Deployment や StatefulSet の中身は第4回から変わっていないためです。稼働中のアプリを止めずに入口だけを足せる、という点も確認しておいてください。順に見ます。まず Gateway と HTTPRoute です。

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

$ kubectl get gateway,httproute -n fanclub

実行結果(Gateway の PROGRAMMED が True=Traefik が受理して設定済み):

NAME                                                CLASS     ADDRESS         PROGRAMMED   AGE
gateway.gateway.networking.k8s.io/fanclub-gateway   traefik   192.168.1.200   True         67s

NAME                                                HOSTNAMES           AGE
httproute.gateway.networking.k8s.io/fanclub-route   ["fanclub.local"]   67s

PROGRAMMED: True は「Gateway の内容を実装(Traefik)が受け取って設定を反映済み」の意味です。ここが False や空欄なら、kubectl describe gateway fanclub-gateway -n fanclub の Conditions を読みます。よくある原因は entryPoint のポート不一致(PortUnavailable・Traefik の ports を 443 にしていない)・GatewayClass 名の不一致・証明書の未発行の 3 つです。次に証明書を見ます。

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

$ kubectl get certificate,secret fanclub-tls -n fanclub

実行結果(cert-manager が発行し Secret になっている):

NAME                                      READY   SECRET        AGE
certificate.cert-manager.io/fanclub-tls   True    fanclub-tls   67s

NAME                 TYPE                DATA   AGE
secret/fanclub-tls   kubernetes.io/tls   3      66s

chart の Certificate が fanclub-ca-issuer に発行を依頼し、cert-manager が fanclub-tls Secret を作り、Gateway がそれを読んで TLS を終端する——宣言したものが自動で揃うという一連の流れです。4 つ目は NetworkPolicy です。

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

$ kubectl get networkpolicy -n fanclub

実行結果(第10回の 7 本に 1 本増えて 8 本):

NAME                               POD-SELECTOR           AGE
allow-backend-from-frontend        app=fanclub-backend    4d7h
allow-db-from-backend              app=fanclub-db         4d7h
allow-dns                          <none>                 4d7h
allow-egress-backend-to-db         app=fanclub-backend    4d7h
allow-egress-batch-to-db           app=fanclub-batch      4d7h
allow-egress-frontend-to-backend   app=fanclub-frontend   4d7h
allow-frontend-from-gateway        app=fanclub-frontend   67s
default-deny-all                   <none>                 4d7h

第10回で「frontend への外部からの入口はまだ無い」と確認した状態に、allow-frontend-from-gateway が加わりました。default-deny を維持したまま、必要になった 1 経路だけをホワイトリストで開ける——第10回で組んだ型が、そのまま本番の外部公開に適用された形です。Traefik を名前空間 traefik に置いたのは、このポリシーの namespaceSelector と一致させるためでした。

ここで k8s-lb 側も見ます。第3回で書いた traefik-https backend は、転送先の 192.168.1.200:443 に誰も居なかったので DOWN のままでした。k8s-lb に SSH して、第3・4回と同じく stats を CSV で確認します。

実行コマンド(k8s-lb・root):

# curl -s 'http://127.0.0.1:9000/stats;csv' | awk -F, '$1=="traefik-https"{print $2, $18}'

実行結果(第3回から DOWN だった backend が UP になっている):

traefik UP
BACKEND UP

ここは正確に押さえてください。この UP は、いま Gateway を有効化したから変わったのではありません。HAProxy の checkL4(TCP が繋がるか)だけを見るので、Traefik が .200 を取得して 443 番を待ち受けた時点——つまり 1 つ前の節の helm install traefik の直後——で既に UP に変わっています。逆に言えば、UP は「TCP が繋がる」ことしか保証しません。Gateway や HTTPRoute が正しいか、fanclub の画面が返るかは L4 チェックの範囲外で、それを確かめるのは次のブラウザでの確認です。「LB は UP なのにアプリが 404/503」というのは、この L4 と L7 の差から生まれる典型的な事象です。

いずれにせよ、k8s-lb の設定は一切変えていません。第3回に「第11回で生きる」と書いた配線に、Traefik が .200 で現れたことで血が通いました。HA の入口(.124)から Traefik(.200)まで一本に繋がっています。

最後にブラウザで確認します。読者のホスト OS(Kubernetes ノードではなく、普段使っている手元のマシン)の hosts ファイルに次の 1 行を追加します(Windows なら C:\Windows\System32\drivers\etc\hosts・Linux/macOS なら /etc/hosts。いずれも管理者権限が要ります)。第1巻第18回でも同じことをしました。

192.168.1.124 fanclub.local

ブラウザで https://fanclub.local を開きます。自己署名 CA の証明書なので警告が出ますが、詳細から続行してください。fanclub の会員一覧が表示され、会員の追加・削除(CRUD)も動きます。第4回以来 port-forward で我慢していた外部公開が、本番と同じ経路で復活しました

なお、動作確認を k8s-ops から curl行おうとすると、そのままでは失敗します。理由が 2 つ重なっているので順に手当てします。1 つ目は名前解決です。k8s-ops の /etc/hosts には第1巻で書いた 127.0.0.1 fanclub.local が残っています。第1巻の kind は extraPortMappings でホストの 443 番を Pod へ通していたため自分自身を指せばよかったのですが、いまの本番クラスタでは入口が HAProxy(192.168.1.124)に変わっています。この行を書き換えてください。2 つ目はプロキシです。k8s-ops のシェルにはプロキシが設定されており、no_proxy には IP レンジとノード名しか入っていないため、fanclub.local というホスト名宛ての通信は alma-proxy(whitelist)へ回されて弾かれます(第2回k8s-lbno_proxy に足したのと同じ話です)。

実行コマンド(k8s-ops・root → developer):

# sed -i 's/^127.0.0.1 fanclub.local$/192.168.1.124 fanclub.local/' /etc/hosts
$ export no_proxy="$no_proxy,fanclub.local"
$ curl -s -k -o /dev/null -w 'HTTP %{http_code}\n' https://fanclub.local/

実行結果:

HTTP 200

読者のホスト OS のブラウザはプロキシを通らず、hosts も先ほど 192.168.1.124 で書いたので、これらの手当ては k8s-ops から確認する場合にだけ必要です。

この画面が出た時点で、ブラウザから DB まで次の全てが繋がっています——ホスト OS の名前解決 → HAProxy(第3回)→ MetalLB が払い出した外部 IP(第9回)→ Traefik/Gateway API/cert-manager(本回)→ NetworkPolicy を通過(第10回)→ frontend → backend → db(第4回)。第3部「ネットワーク」で積み上げた 3 回分が、1 つの経路として完成した形です。

やってみよう

本回の演習は 3 つとも読み取りと確認だけで、クラスタの状態を変えません。本文をひととおり実行した直後の状態のまま取り組めます。本回で入れた Traefik・cert-manager・Gateway API の CRD、そして有効化した Gateway は第12回以降もそのまま使い続けるので、後始末として削除しないでください(~/gateway の YAML も残しておきます)。

Gateway API の 3 リソースの関係を読む

kubectl get gatewayclass,gateway,httproute -A を実行し、GatewayClass(実装=traefik)・Gateway(入口=fanclub-gateway・ADDRESS が .200)・HTTPRoute(振り分け=fanclub-route)の 3 つがどう繋がっているかを読み取ってください。kubectl describe gateway fanclub-gateway -n fanclub で、HTTPRoute が何本ぶら下がっているか(Attached Routes)も確認できます。

3 層の名前解決を実際に引き比べる

同じ fanclub.local が、引く場所によって別の答えになることを確認してください。①クラスタ内の Pod から kubectl run dnstest --image=busybox:1.37 --rm -it --restart=Never -- nslookup fanclub.local(CoreDNS が 192.168.1.200 を返す)、②ホスト OS の hosts ファイル(192.168.1.124)。どちらが正しいという話ではなく、層ごとに役割が違うことを読み取るのが狙いです。

HTTPS の経路を端から端まで確認する

ブラウザで https://fanclub.local にアクセスして会員の追加・削除まで行い、経路上の 4 点を確認してください。①kubectl get svc -n traefik で EXTERNAL-IP が 192.168.1.200(MetalLB の払い出し)、②k8s-lb で curl -s 'http://127.0.0.1:9000/stats;csv' | awk -F, '$1=="traefik-https"{print $2, $18}'UP(root で実行。ブラウザで http://192.168.1.124:9000/stats を開いても同じものが見えます)、③kubectl get certificate -n fanclubfanclub-tlsREADY Truekubectl describe certificate fanclub-tls -n fanclub の Events で、fanclub-ca-issuer が発行した経緯まで追えます)、④kubectl get networkpolicy -n fanclub8 本allow-frontend-from-gateway を含む)。

まとめ

本回では https://fanclub.local の外部公開を復活させました。要点は、①Gateway API は GatewayClass(実装)/Gateway(入口)/HTTPRoute(振り分け)に役割を分離した Ingress の後継で、その実体は CRD+コントローラ(operator パターン)、②Traefik は GatewayClass として登録され、その LoadBalancer Service に MetalLB が .200 を払い出す——このとき Gateway のリスナーと Traefik の entryPoint はポート番号で結び付くため、番号を 443 に合わせ、非 root のまま bind できるよう NET_BIND_SERVICE だけを与える、③cert-manager は自己署名 CA(fanclub-ca-issuer)を窓口として証明書を発行し、Gateway が TLS を終端する、④CoreDNS の Corefile に hosts プラグインを足せばクラスタ内 DNS をカスタマイズでき、名前解決はホスト OS/dnsmasq/CoreDNS の 3 層で役割が違う、⑤--set gateway.enabled=true の反転だけで Gateway・HTTPRoute・Certificate・NetworkPolicy が揃い、第3回に書いた HAProxy の配線は無変更のまま生きる(その UP は L4 チェックが通っただけで、アプリが返ることの保証ではない)、でした。第3部「ネットワーク」はこれで完結です。

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

次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。

  1. Gateway API は旧 Ingress の後継で、GatewayClass/Gateway/HTTPRoute に役割を分離している。
  2. GatewayClass は「どの実装(Traefik 等)を使うか」を指す。
  3. Gateway API の CRD を適用すれば、それだけで HTTP のルーティングが動きはじめる。
  4. Traefik の外部 IP(192.168.1.200)は MetalLB が払い出している。
  5. 読者のブラウザからの fanclub.local は CoreDNS が解決している。
  6. Traefik はどの名前空間に置いても、fanclub の HTTPS 公開は成立する。
  7. CoreDNS の hosts プラグインに fallthrough を書き忘れると、他の名前解決に影響が出る。
  8. 本回で HAProxy(k8s-lb)の設定を追加する必要があった。
  9. --set gateway.enabled=true にすると、chart が Gateway・HTTPRoute・Certificate と NetworkPolicy を 1 本追加する。
解答と解説

1=○(役割分離が後継の要点)/2=○(StorageClass に相当する位置づけ)/3=×(CRD は語彙の追加だけ。実体を作るコントローラ=Traefik が要る)/4=○(第9回の lan-pool 先頭 .200)/5=×(ホスト OS の /etc/hosts → 192.168.1.124。CoreDNS はクラスタ内 Pod 用)/6=×(chart の allow-frontend-from-gateway が namespaceSelector で traefik を指定=名前空間名を合わせないと default-deny で塞がれる)/7=○(fallthrough が無いと後続プラグインへ渡らない)/8=×(第3回で gateway-https を定義済み。無変更のまま、Traefik が .200 で 443 を待ち受けた時点で L4 チェックが通り UP になる)/9=○(Gateway/HTTPRoute/Certificate/allow-frontend-from-gateway の 4 つ)

次回予告

次回・第12回からは第4部「ストレージ」です。fanclub の DB は今、local-path-provisioner(第4回で暫定的に入れた既定 StorageClass)で 1 つの Workload Node のディスクに固定されています。ノードが落ちればデータへ届きません。PV/PVC/StorageClass の仕組みを掘り下げ、分散ストレージ Longhorn を導入して fanclub-db を無停止に近い形で移行します。

前の記事
次の記事