何が起きたか
type: LoadBalancer でサービスをデプロイして kubectl get svc を実行すると、EXTERNAL-IP の列が <pending> のままになっている。5分待っても変わらない。10分待っても変わらない。
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-app-svc LoadBalancer 10.96.123.45 <pending> 80:31234/TCP 12m
ほとんどの場合、原因は次の3つのどれかに絞られる:クラウドプロバイダーのコントローラーが動いていない、クラウド設定が間違っている、またはベアメタル・ローカルクラスターを使っていてLoadBalancerのプロビジョニング機能がそもそも存在しない。
デバッグ手順
ステップ1 — まずサービスのイベントを確認する
kubectl describe svc my-app-svc
一番下の Events セクションまでスクロールする。何も表示されないか <none> だけの場合、クラウドプロバイダーのコントローラーがIPのプロビジョニングをまったく試みていないことを意味する。ほぼ確実にコントローラー自体が起動していない。
ステップ2 — クラウドプロバイダーのコントローラーが存在するか確認する
# クラウドクラスターの場合、cloud-controller-managerを確認
kubectl get pods -n kube-system | grep cloud-controller
# ベアメタルでMetalLBを使用している場合
kubectl get pods -n metallb-system
# k3s(デフォルトでServiceLB/Klipperを使用)の場合
kubectl get pods -n kube-system | grep svclb
これらのいずれも存在しない場合、原因が判明した。外部IPを割り当てる担当が誰もいないということだ。
ステップ3 — クラウドクラスターのノードアノテーションを確認する
クラウドコントローラーマネージャーはノードのアノテーションを読み取り、どのリージョン、ゾーン、VPCにプロビジョニングするかを判断する。
kubectl describe nodes | grep -A5 'Labels\|Annotations' | grep -i 'region\|zone\|provider'
topology.kubernetes.io/region(または1.17以前のクラスターでは古い failure-domain.beta.kubernetes.io/region)を確認する。プロバイダーラベルが存在しない場合、通常はノードの起動時に正しいクラウドメタデータフラグが指定されていなかったことを意味する。
ステップ4 — オンプレミス/ベアメタルの場合:IPプールが設定されていないことを確認する
# MetalLBがインストールされている場合、IPAddressPoolが存在するか確認
kubectl get ipaddresspool -n metallb-system
# 古いMetalLBはConfigMapを使用
kubectl get configmap config -n metallb-system -o yaml
解決策
ケースA — ベアメタルまたはオンプレミスクラスター:MetalLBをインストールする
ベアメタルのKubernetesにはLoadBalancerの実装が含まれていない。MetalLBがその空白を埋める。
# MetalLBをインストール
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml
# 準備完了まで待機
kubectl wait --namespace metallb-system \
--for=condition=ready pod \
--selector=app=metallb \
--timeout=90s
次に、LANの範囲からIPプールを定義する。DHCPでルーターがすでに払い出しているアドレスと重複しないものを選ぶこと:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: local-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: local-advert
namespace: metallb-system
kubectl apply -f metallb-pool.yaml
ケースB — ローカル開発クラスター(minikube、kind、Docker Desktop)
ローカルクラスターは実際のIPをプロビジョニングしない。minikubeの場合、tunnelコマンドで対処できる:
# 別のターミナルで実行し、起動したままにしておく
minikube tunnel
これを実行してから kubectl get svc を再度確認する。数秒以内に外部IPが 127.0.0.1 に変わるはずだ。
kindを使っている場合は、上記のベアメタルと同じ手順でMetalLBをインストールするか、ローカルテスト用にサービスを NodePort に切り替えるだけでよい。使い捨て環境ならセットアップの手間が少ない方が楽だ。
ケースC — クラウドクラスター(EKS、GKE、AKS):cloud-controller-managerが動いていない
# コントローラーマネージャーのログを確認
kubectl logs -n kube-system -l component=cloud-controller-manager --tail=50
# マネージドクラスターの場合、プロバイダー固有のコントローラーを確認
# EKS: aws-load-balancer-controller
kubectl get pods -n kube-system | grep aws-load-balancer
kubectl logs -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller --tail=50
EKSはずいぶん前にツリー内のクラウドプロバイダーを廃止した。AWS Load Balancer Controllerを別途インストールする必要がある:
# Helmでインストール
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=my-cluster \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller
GKEはコントローラーを自動管理してくれる。問題が発生している場合は、ノードプールの健全性を確認し、GKEのサービスアカウントがVPCに対して compute.networks.use 権限を持っていることを確認する。
ケースD — クラウドプロバイダーでのサブネットまたはアノテーションの設定ミス
LoadBalancerはアタッチするサブネットを知る必要がある。アノテーションを省略すると、何も通知されないまま永遠に待ち続ける。
# EKS: サービスにサブネットIDのアノテーションを付ける
kubectl annotate svc my-app-svc \
service.beta.kubernetes.io/aws-load-balancer-subnets=subnet-abc123,subnet-def456
# GKE: 内部限定ネットワークを使用している場合の内部LBアノテーション
kubectl annotate svc my-app-svc \
cloud.google.com/load-balancer-type=Internal
ケースE — ヘルスチェックをブロックしているファイアウォールまたはセキュリティグループ
クラウドロードバランサーはノードにヘルスチェックのトラフィックを送信する。セキュリティグループがそのトラフィックをブロックすると、プロビジョニング中にLBが何も通知せずに止まってしまう。
# EKS: ノードのセキュリティグループがロードバランサーSGからのインバウンドを許可しているか確認
# ヘルスチェックが使用するポートを確認
kubectl describe svc my-app-svc | grep -i 'node.*port\|health'
動作確認
# EXTERNAL-IPが割り当てられるまで監視
kubectl get svc my-app-svc -w
# IPが表示されたら疎通確認
curl http://<EXTERNAL-IP>:80
# エンドポイントが正常か確認
kubectl get endpoints my-app-svc
-w フラグはリアルタイムで変更をストリーミングする。修正を適用した後、30〜60秒以内に <pending> が実際のIPアドレスに変わるはずだ。
学んだこと
- 毎回まず確認すべきこと:このクラスターに
LoadBalancerのプロビジョニングを担当するものが存在するか?ベアメタルやローカルクラスターでは答えはノーだ — MetalLBや同様のコントローラーを明示的にインストールしない限り。 - アップグレード後に壊れたクラウドクラスターは、バージョンの不一致が原因であることが多い。cloud-controller-managerがコントロールプレーンと同期してバンプされなかった場合、サイレントな障害が続く。
- とにかく早く問題を解消したい場合は?
type: LoadBalancerをtype: NodePortに切り替えて、<node-IP>:<node-port>に直接アクセスする。スマートな解決策ではないが、1分以内に動作する。 - L2モードのMetalLBはBGPのセットアップが不要 — シンプルなIP範囲の設定でほとんどのホームラボやオンプレミス環境をカバーできる。BGPモードはより本番向けだが、ルーターのサポートが必要になる。
kubectl describe svcのイベントは過小評価されている。コントローラーが何を試みてなぜ失敗したかを正確に教えてくれることが多く、20分間の手探りデバッグを省ける。

