新卒インフラエンジニア向け「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-ops(192.168.1.122)から kubectl と helm で行います。プロンプトの $ は一般ユーザー developer、第4回で配布した admin.conf(cluster-admin)を使います。本回で作る YAML は developer のホーム配下の作業ディレクトリにまとめます(所有者は developer・sudo は使いません)。以降の kubectl apply・helm は、このディレクトリにいる前提の相対パスです(fanclub の chart を扱う最後の節だけ ~ へ移動します)。
実行コマンド(k8s-ops・developer):
$ mkdir -p ~/gateway && cd ~/gateway
目次
今ここマップ(全 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 の画面が出るまでに、パケットは次の順に流れます。
- ブラウザ(読者のホスト OS):
fanclub.localをホスト OS の/etc/hostsで192.168.1.124(k8s-lb)に解決する。 - HAProxy(k8s-lb・.124:443):第3回で
gateway-httpsとして定義済みの入口。転送先は192.168.1.200:443。 - Traefik(.200):MetalLB(第9回)がプール先頭の
.200を払い出す LoadBalancer Service。Gateway API の実装として動く。 - Gateway(TLS 終端):cert-manager が発行した証明書で HTTPS を終端し、HTTPRoute の規則に従って振り分ける。
- 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 の設定を触る必要はありません。

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 のリリースから直接取得します(kubectl は developer のシェルに設定済みのプロキシ経由で取りに行きます。第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: nginx→ GatewayClass(どの実装か)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 を許可」(namespaceSelector で kubernetes.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 の受け口」の役割だけを持たせます。先ほど説明した役割分離が、そのまま設定値に現れています。
ports と securityContext の 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-pool(192.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
NAME が traefik であることが重要です。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 段構えになっている点が、初見でつまずきやすいところです。①自分自身に署名する Issuer(selfsigned-issuer)→ ②それで CA 証明書を発行(fanclub-ca・isCA: true)→ ③その CA を使う ClusterIssuer(fanclub-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-issuer が True になるには、②の 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/hosts(192.168.1.124=HAProxy)。第1巻第18回と同じ手法。alma-proxy の dnsmasq でも CoreDNS でもありません。 - VM(cp/wl/k8s-ops 等)の外向きの名前解決 → alma-proxy の dnsmasq(
192.168.1.121)。k8s-lbなどのホスト名はここで引きます(第2・3回で触りました)。 - クラスタ内の Pod の名前解決 → CoreDNS(Service
kube-dns・10.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
}
プラグインが上から順に並んでいます。kubernetes が cluster.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_EDITOR/EDITOR を設定していなければ vi が開きます。ready の次の行に、次の 4 行を追加して保存します(kubernetes プラグインより前に置くのが定石です。fallthrough は「ここで解決できなければ後続のプラグインへ渡す」という意味で、これが無いと cluster.local も外向きの名前解決も壊れます)。インデントは既存の ready や kubernetes の行に合わせてください——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 行を削除して保存します。

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 の check は L4(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-lb を no_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 fanclub の fanclub-tls が READY True(kubectl describe certificate fanclub-tls -n fanclub の Events で、fanclub-ca-issuer が発行した経緯まで追えます)、④kubectl get networkpolicy -n fanclub が 8 本(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 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
- Gateway API は旧 Ingress の後継で、GatewayClass/Gateway/HTTPRoute に役割を分離している。
- GatewayClass は「どの実装(Traefik 等)を使うか」を指す。
- Gateway API の CRD を適用すれば、それだけで HTTP のルーティングが動きはじめる。
- Traefik の外部 IP(192.168.1.200)は MetalLB が払い出している。
- 読者のブラウザからの
fanclub.localは CoreDNS が解決している。 - Traefik はどの名前空間に置いても、fanclub の HTTPS 公開は成立する。
- CoreDNS の
hostsプラグインにfallthroughを書き忘れると、他の名前解決に影響が出る。 - 本回で HAProxy(k8s-lb)の設定を追加する必要があった。
--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 を無停止に近い形で移行します。
