Kubernetes LoadBalancer ServiceがPending状態のまま — External IPが割り当てられない問題を修正

intermediate☸️ Kubernetes2026-05-13| Kubernetes 1.20以降、任意のクラウドプロバイダー(GKE、EKS、AKS)またはベアメタルクラスター(kubeadm、k3s、kind、minikube)

Error Message

Service is in Pending state and External IP is <pending>
#kubernetes#service#loadbalancer#cloud-provider

何が起きたか

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: LoadBalancertype: NodePort に切り替えて、<node-IP>:<node-port> に直接アクセスする。スマートな解決策ではないが、1分以内に動作する。
  • L2モードのMetalLBはBGPのセットアップが不要 — シンプルなIP範囲の設定でほとんどのホームラボやオンプレミス環境をカバーできる。BGPモードはより本番向けだが、ルーターのサポートが必要になる。
  • kubectl describe svc のイベントは過小評価されている。コントローラーが何を試みてなぜ失敗したかを正確に教えてくれることが多く、20分間の手探りデバッグを省ける。

Related Error Notes