NodePort と LoadBalancer の違い

v1.37 時点(2026年10月26日 確認)

Kubernetes 基本仕様第22回 / 全45回 ・ 第4部 ネットワーク
目次

この記事では Service をクラスタの外に公開する 2 つの種類である NodePort と LoadBalancer の仕組みを、図で説明します。NodePort がすべてのノードの同じポートで通信を受けること、LoadBalancer がその前に外部のロードバランサーを置くことがわかります。後半では kind のクラスタで、ノードが Docker のコンテナであることによる制約を確かめ、手元のマシンから NodePort と LoadBalancer の Service に届ける手順を示します。

Service の仕組み(ラベルセレクタ、cluster IP、EndpointSlice、kube-proxy)を扱った第21回の内容を前提にしています。まだの方は先にこちらをどうぞ。

基本仕様 ・ 第21回 Service を動かして理解するPod の集まりに変わらない入り口を与える Service の仕組みを図で説明します。ラベルセレクタで Pod を選ぶこと、cluster IP、転送先の一覧を持つ EndpointSlice、ノードで通信を Pod へ届ける kube-proxy を押さえ、kind のクラスタで Pod を増減させたときに転送先が自動で変わる様子を確かめる手順を紹介します。

以下の図は、クラスタの外のクライアントから見た、NodePort と LoadBalancer の Service の入り口の違いを表しています。

クラスタの外から見た、NodePort と LoadBalancer の Service の入り口

NodePort の Service では、クライアントはどれかのノードの IP アドレスと、Service に割り当てられたポート(nodePort)に接続します。LoadBalancer の Service では、クライアントはロードバランサーの IP アドレスに接続し、ロードバランサーがその通信をノードへ送ります。公式ドキュメントによると、LoadBalancer の Service でも Kubernetes はふつう(typically)、まず NodePort の Service と同じように nodePort を用意します。そのうえで cloud-controller-manager が、その nodePort へ通信を送るように外部のロードバランサーを設定します。ノードに届いた通信を Pod へ振り向けるのは、kube-proxy を使うクラスタでは第21回で見た kube-proxy の規則です。図では省いています。

参考:Service | Kubernetes、Service | Kubernetes、Virtual IPs and Service Proxies | Kubernetes

NodePort と LoadBalancer とは

Service の種類(spec.type)は第21回で見たとおり 4 つあります。公式ドキュメントは、アプリケーションの一部(たとえばフロントエンド)では、クラスタの外から届く IP アドレスで Service を公開したいことがある、と説明しています。フロントエンドは一般に、利用者のブラウザーなどと直接やり取りする側の部分です。Service の種類はこうした公開の仕方を選ぶためのものです。

参考:Service | Kubernetes

NodePort は 各ノードの IP アドレスの、決まったポートで Service を公開する種類 です。LoadBalancer は 外部のロードバランサーを使って、Service をクラスタの外に公開する種類 です。ロードバランサーは一般に、受け取った通信を複数の宛先に振り分ける装置やソフトウェアのことです。公式ドキュメントによると、Kubernetes 自身はロードバランサーの部品を直接は提供しません。利用者が用意するか、クラスタをクラウドプロバイダー(クラウドのサービスを提供する事業者)と連携させる必要があります。

参考:Service | Kubernetes

v1.37 時点の公式ドキュメントの説明を、4 つの種類について次の表にまとめます。

type公式ドキュメントの説明
ClusterIPクラスタの中の IP アドレスで Service を公開し、クラスタの中からだけ届く。type を書かないときの既定(第21回)
NodePort各ノードの IP アドレスの、決まったポート(nodePort)で Service を公開する。そのために、ClusterIP の Service と同じく cluster IP も用意される
LoadBalancer外部のロードバランサーを使って、Service をクラスタの外に公開する
ExternalNameService を externalName フィールドの内容(外部のホスト名)に対応付ける。クラスタの DNS サーバーがそのホスト名を返すように設定され、転送(プロキシ)は一切設定されない

参考:Service | Kubernetes

この回の主題は表の 2 行目と 3 行目です。ExternalName は通信を転送せず、クラスタの DNS の働きで外部の名前に対応付けるものです。クラスタの中の DNS は第23回で扱います。

この回には、似た名前の言葉がいくつか出てきます。公式ドキュメントでの意味に沿って、次の表にまとめます。

言葉指すもの
NodePortService の種類の 1 つ(type: NodePort)
nodePortノードで Service の通信を受けるポートの番号(spec.ports[].nodePort)。NodePort と LoadBalancer の Service で使う
portcluster IP で接続するときに使う、Service のポート(第21回)。公式のチュートリアルでは、LoadBalancer の Service の外部の IP アドレスにも、この番号で接続している
targetPort通信を送る先の、Pod のポート(第21回)
外部のロードバランサーService への通信を受けて、バックエンドへ送るもの。クラウドプロバイダーなどが用意する
status.loadBalancer.ingress用意されたロードバランサーの入り口(IP アドレスかホスト名)の一覧で、Service 宛ての通信はここへ送るべき(should)とされる。この ingress は「入り口」の意味で、第24回で扱う Ingress というリソースとは別のもの
cloud-controller-managerクラスタをクラウドプロバイダーの API とつなぐ、コントロールプレーンのコンポーネント(第4回)

参考:Service | Kubernetes API Reference、Service | Kubernetes API Reference、Exposing an External IP Address to Access an Application in a Cluster | Kubernetes、Cloud Controller Manager | Kubernetes

なぜ NodePort と LoadBalancer が必要なのか

第21回で見たとおり、ClusterIP の Service にはクラスタの中からだけ届きます。Pod の IP アドレスも API リファレンスの説明では「少なくともクラスタの中ではルーティングできる」アドレスです(第20回)。クラスタの外から届くかどうかは、この説明からはわかりません。

参考:Service | Kubernetes、Pod | Kubernetes API Reference

第20回で見たとおり、Cluster Networking のページはネットワークで解決すべき問題を 4 つに分けています。その 1 つが「クラスタの外から Service へ」の通信で、これも Service で扱うとしています。Web サイトのように、クラスタの外の利用者に使ってもらうアプリケーションでは、この通信の入り口が必要です。

参考:Cluster Networking | Kubernetes

NodePort はその入り口をノードの上に作ります。公式ドキュメントによると、NodePort を使うと、自分で負荷分散の仕組みを組み立てたり、Kubernetes が完全には対応していない環境を設定したり、1 つ以上のノードの IP アドレスを直接公開したりできます。たとえば、クラスタの外に自分で用意したロードバランサーから、各ノードの nodePort へ通信を送る使い方が考えられます。LoadBalancer は外部のロードバランサーを Service の前に置きます。外部のロードバランサーに対応したクラウドプロバイダーでは、type を LoadBalancer にすると、Service のためのロードバランサーが用意されます。

参考:Service | Kubernetes、Service | Kubernetes

Service の種類のほかに、ClusterIP の Service を Ingress や Gateway でクラスタの外に公開する方法もあります。Ingress と Gateway API は第24回で扱います(この Ingress は上の表の status.loadBalancer.ingress とは別のものです)。

参考:Service | Kubernetes

NodePort と LoadBalancer の仕組み

Service の種類は入れ子になっている

公式ドキュメントによると、Service の API の type は入れ子の作りで、それぞれの段が前の段に機能を足します。API リファレンスも、NodePort は ClusterIP の上に作られ、すべてのノードにポートを割り当てると説明しています。LoadBalancer は NodePort の上に作られ、今のクラウドが対応していれば外部のロードバランサーを作ります。どちらも、通信を届ける先は cluster IP と同じエンドポイントです。

参考:Service | Kubernetes、Service | Kubernetes API Reference

それぞれの種類で何が用意されるかを、公式ドキュメントと API リファレンスに沿って次の表にまとめます。

typecluster IPnodePort外部のロードバランサー
ClusterIP割り当てる割り当てないなし
NodePort割り当てる割り当てるなし
LoadBalancer割り当てる既定では割り当てるクラウドなどが対応していれば用意される

参考:Service | Kubernetes、Service | Kubernetes、Service | Kubernetes API Reference

入れ子の作りなので、NodePort や LoadBalancer の Service にも cluster IP があり、クラスタの中からは第21回と同じく cluster IP で届くと考えられます。入れ子の作りには、LoadBalancer の Service で nodePort の割り当てを止める設定という例外が 1 つあります。この設定は「LoadBalancer:外部のロードバランサーを前に置く」で説明します。

参考:Service | Kubernetes

NodePort:すべてのノードの同じポートで受ける

公式ドキュメントによると、type を NodePort にすると、コントロールプレーンが kube-apiserver の --service-node-port-range で決めた範囲からポートを 1 つ割り当てます。範囲の既定は 30000〜32767 です。各ノードはそのポート(すべてのノードで同じ番号)に届いた通信を Service へ中継します(原文は “Each node proxies that port”)。割り当てたポートは Service の spec.ports[*].nodePort に書かれます。公式ドキュメントは kube-proxy の iptables のモードについて、type: NodePort の Service やロードバランサーから入ってくる通信も、第21回の cluster IP への通信と同じ基本の流れで処理されると説明しています。

参考:Service | Kubernetes、kube-apiserver | Kubernetes、Virtual IPs and Service Proxies | Kubernetes

もう少しくわしく書くと、クラスタのすべてのノードが、割り当てられたポートで待ち受け、その通信を Service の準備のできた(ready)エンドポイントの 1 つへ転送するように、自分を設定します。そのため、既定の設定では、クラスタの外からは どのノードでもよいので、そのノードの IP アドレスと nodePort に接続すれば NodePort の Service に届きます。プロトコル(TCP、UDP、SCTP)は Service のプロトコルに合わせます。

参考:Service | Kubernetes

以下の図は、クラスタの外のクライアントが NodePort の Service に届く道筋を表しています。後半の「手元で試す」で作る foo-nodeport という Service にあたります。

どのノードの nodePort に届いても、Service の転送先の Pod へ転送される NodePort の Service

クライアントはノード 1〜3 のどれに接続してもかまいません。どのノードも同じ 30007 番で待ち受け、Service の転送先の Pod のどれかへ通信を転送します。既定の設定(後で説明する externalTrafficPolicy: Cluster)では、転送先の Pod が接続を受けたノードとは別のノードにあってもかまいません。Pod を選ぶのは第21回と同じく、Service のラベルセレクタです。図の Service は通信を中継するものではなくどの Pod へ送るかを決める設定を表しています。右の枠はラベルで選ばれる Pod の集まりで、Pod は実際にはどれかのノードの上にあります。

参考:Service | Kubernetes、Using Source IP | Kubernetes

nodePort を自分で決める

以下は公式ドキュメントにある、nodePort を指定した NodePort の Service のマニフェストです(そのまま引用しています)。

yaml
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  type: NodePort
  selector:
    app.kubernetes.io/name: MyApp
  ports:
    - port: 80
      # By default and for convenience, the `targetPort` is set to
      # the same value as the `port` field.
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane
      # will allocate a port from a range (default: 30000-32767)
      nodePort: 30007

参考:Service | Kubernetes

コメントにあるとおり、nodePort は書かなくてもかまいません。書かなければ、コントロールプレーンが範囲から割り当てます。書いた場合、コントロールプレーンはそのポートを割り当てるか、API の操作が失敗したことを返します。公式ドキュメントは、そのためポートがぶつからないように自分で気を付ける必要があり、NodePort 用に設定した範囲の中の番号を使う必要もある、と説明しています。API リファレンスによると、指定した番号が範囲の中にあり、まだ使われていなければ、その番号が使われます。そうでなければ操作は失敗します。

参考:Service | Kubernetes、Service | Kubernetes API Reference

ぶつかる心配を減らすために、NodePort の範囲は 2 つの帯に分かれています。自動の割り当ては既定では上の帯を使い、上の帯を使い切ると下の帯も使うことがあります。そのため、利用者は下の帯から選べば、ぶつかる心配が少なくなります。既定の範囲 30000〜32767 での分け方を次の表にまとめます。

帯範囲使われ方
静的な帯(static band)30000〜30085自分で nodePort を決めるときに選ぶと、ぶつかる心配が少ない
動的な帯(dynamic band)30086〜32767自動の割り当てが既定で使う

参考:Service | Kubernetes

引用したマニフェストの 30007 も、後半の手順で使う 30007 も、静的な帯に入っています。

ノードのどの IP アドレスで受けるか

ノードは IP アドレスを複数持つことがあります。公式ドキュメントによると、kube-proxy の iptables のモード(v1.37 時点の既定)では、NodePort の Service は既定ではノードのすべての IP アドレスで届きます。nftables のモードでは、既定ではノードの主な(primary)IP アドレスだけです。どの IP アドレスで受けるかは、kube-proxy の --nodeport-addresses で変えられます。第21回で見たとおり、kind のクラスタの kube-proxy は既定では iptables のモードで動くので、この回のクラスタでは、NodePort の Service はノードのすべての IP アドレスで届くはずです。くわしい違いは「注意点」の表にまとめます。

参考:Service | Kubernetes、Configuration | kind

送り元の IP アドレスは書き換えられる

公式のチュートリアルによると、NodePort の Service に送られたパケットは、既定では送り元の IP アドレスを書き換えられます(Source NAT。第20回)。チュートリアルの例では、転送先の Pod はノード 1 にあり、ノード 2 にはありません。チュートリアルは、クライアントがノード 2 の nodePort に送った通信を例に、次の 6 つの手順で説明しています。

  1. クライアントがノード 2 の nodePort にパケットを送る
  2. ノード 2 がパケットの送り元の IP アドレスを、自分の IP アドレスに書き換える(SNAT)
  3. ノード 2 がパケットの宛先の IP アドレスを、Pod の IP アドレスに書き換える
  4. パケットはノード 1 へ、そしてエンドポイントへ届けられる
  5. Pod の応答はノード 2 へ戻される
  6. Pod の応答がクライアントへ返される

参考:Using Source IP | Kubernetes

手順 5 と 6 のとおり、Pod の応答はいったんノード 2 に戻り、そこからクライアントへ返されます。送り元をノード 2 に書き換えておくので、Pod の応答はノード 2 宛てになり、ノード 2 を通ってクライアントへ返せる、という流れと読めます。そのため、Pod から見える送り元の IP アドレスは、クライアントのものではなく接続を受けたノードの IP アドレスになります。kube-proxy の iptables のモードの説明も、NodePort の Service やロードバランサーから届く通信では、cluster IP と同じ基本の流れで処理されるが、クライアントの IP アドレスは書き換えられる、としています。第21回で見た、iptables のモードでの cluster IP への通信では、クライアントの IP アドレスは書き換えられませんでした。

参考:Using Source IP | Kubernetes、Virtual IPs and Service Proxies | Kubernetes

外からの通信の届け先を決める externalTrafficPolicy

外から届いた通信をどの Pod へ届けるかは spec.externalTrafficPolicy で決めます。API リファレンスによると、ここでの「外から届いた通信」は、nodePort やロードバランサーの IP アドレスなど、Service をクラスタの外に向けて公開しているアドレスに届いた通信のことです。値は Cluster と Local の 2 つで、既定値は Cluster です。v1.37 時点の公式ドキュメントに沿って、違いを次の表にまとめます。

値届け先送り元の IP アドレス
Cluster(既定)準備のできた(ready)すべてのエンドポイント。別のノードの Pod にも転送する書き換えられる(NodePort と LoadBalancer の既定の動き)
Local接続を受けたノードにある、準備のできたエンドポイントだけ書き換えない。そのノードにエンドポイントが無ければ、kube-proxy はその Service の通信を転送しない

参考:Virtual IPs and Service Proxies | Kubernetes、Service | Kubernetes API Reference、Using Source IP | Kubernetes

Local にすると送り元の IP アドレスは残りますが、Pod の無いノードに届いた通信は捨てられます。API リファレンスは Local を、ノードのあいだの振り分けは外部のロードバランサーが受け持つ、という前提の設定だと説明しています。LoadBalancer の Service を Local にすると、spec.healthCheckNodePort にヘルスチェック用のポートが割り当てられます。ヘルスチェックは一般に、宛先が正常に応答できるかを定期的に確かめることです。ロードバランサーなどの外部のシステムは、このポートで、そのノードにエンドポイントがあるかどうかを確かめられます。

参考:Service | Kubernetes API Reference、Using Source IP | Kubernetes

API リファレンスによると、クラスタの中からロードバランサーの IP アドレスなどに送った通信は、いつも Cluster として扱われます。一方、クラスタの中から nodePort に送るクライアントについては、ノードを選ぶときにこの設定を考える必要があるかもしれない、としています。Local の Service では Pod の無いノードの nodePort を選ぶと届かない、という意味と読めます。

参考:Service | Kubernetes API Reference

LoadBalancer:外部のロードバランサーを前に置く

公式ドキュメントによると、外部のロードバランサーに対応したクラウドプロバイダーでは、type を LoadBalancer にすると、Service のためのロードバランサーが用意されます。ロードバランサーの実際の作成は非同期で行われます(原文は asynchronously)。Service を作る操作はロードバランサーができるのを待たずに終わり、ロードバランサーはあとから用意される、という意味と読めます。用意されるまでのあいだ、外部の IP アドレスは <pending> と表示されると考えられます(後で説明します)。用意されたロードバランサーの情報は Service の status.loadBalancer に書かれます。以下は公式ドキュメントにある例です(そのまま引用しています)。

yaml
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app.kubernetes.io/name: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
  clusterIP: 10.0.171.239
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
    - ip: 192.0.2.127

参考:Service | Kubernetes

例の status は、ロードバランサーが用意されたあとの状態を表したものです。利用者が書くのは spec までで、status はロードバランサーを用意した仕組みが書き込むと考えられます。API リファレンスによると、status.loadBalancer.ingress はロードバランサーの入り口の一覧で、Service 宛ての通信はここへ送るべき(should)とされます。入り口が IP アドレスのものは ip(ふつうは GCE や OpenStack のロードバランサー)、DNS の名前のものは hostname(ふつうは AWS のロードバランサー)に書かれます。GCE(Google Compute Engine)、OpenStack、AWS は、一般にクラウドの仮想マシンなどを提供するサービスやソフトウェアです。

参考:Service | Kubernetes API Reference、Service | Kubernetes

外部のロードバランサーからの通信は、バックエンドの Pod へ向けられます。どう振り分けるかはクラウドプロバイダーが決めます。公式ドキュメントによると、LoadBalancer の Service を実現するとき、Kubernetes はふつう、まず NodePort の Service を頼まれたときと同じ変更をします(原文は “making the changes that are equivalent to you requesting a Service of type: NodePort”)。つまり nodePort を割り当て、各ノードがそのポートで待ち受けるようにします。そのうえで cloud-controller-manager が、割り当てた nodePort へ通信を送るように外部のロードバランサーを設定します。

参考:Service | Kubernetes、Service | Kubernetes

公式のチュートリアルでは外部の IP アドレスと、Service の port の番号を組み合わせて接続しています(curl http://<external-ip>:<port>)。ロードバランサーは Service の port の番号で通信を受け、各ノードの nodePort へ送ると考えられます。

参考:Exposing an External IP Address to Access an Application in a Cluster | Kubernetes

cloud-controller-manager の中でこれを受け持つのは、サービスのコントローラー(service controller)です。公式ドキュメントによると、サービスのコントローラーは Service の作成、更新、削除を見張り、その Service に合わせてロードバランサーを設定します。そのために Service の status を書き換える権限も必要とします(権限の仕組みは第38回)。status.loadBalancer を書くのもこのコントローラーだと考えられます。

参考:Cloud Controller Manager | Kubernetes、Cloud Controller Manager | Kubernetes

以下の図は LoadBalancer の Service で、cloud-controller-manager、外部のロードバランサー、ノードがどう関係するかを表しています。

cloud-controller-manager が外部のロードバランサーを設定し、ロードバランサーがノードの nodePort へ通信を送る LoadBalancer の Service

cloud-controller-manager は Service を見張り、Service に合わせて外部のロードバランサーを設定します。クライアントの通信はロードバランサーからノードの nodePort に届き、そこから先は NodePort の Service と同じく Pod へ転送されます。図の実線は通信、点線は監視、破線は設定する(管理する)関係を表します。

参考:Service | Kubernetes、Cloud Controller Manager | Kubernetes

公式のチュートリアルの例では、外部の IP アドレスは kubectl get svc の EXTERNAL-IP 列と、kubectl describe svc の LoadBalancer Ingress の行に出ています。チュートリアルは、外部の IP アドレスが <pending> と表示されたら、1 分ほど待ってからもう一度同じコマンドを実行するよう案内しています。<pending> は、ロードバランサーがまだ用意されていないことを表すと読めます。

参考:Exposing an External IP Address to Access an Application in a Cluster | Kubernetes

第4回で見たとおり、公式ドキュメントは自社の設備(オンプレミス)や手元の PC の学習用の環境で動かすクラスタには cloud-controller-manager が無い、としています。そうしたクラスタで LoadBalancer の Service を作っても、ロードバランサーを用意するものがいなければ、外部の IP アドレスは <pending> のままのはずです。一方、nodePort と cluster IP は、外部のロードバランサーとは別に Kubernetes が割り当てるので、NodePort の Service としては使えると考えられます。

参考:Cluster Architecture | Kubernetes、Service | Kubernetes

ロードバランサーの実装によっては、nodePort を経ずに Pod へ直接通信を送るものもあります。公式ドキュメントによると、そうした実装のために、LoadBalancer の Service では spec.allocateLoadBalancerNodePorts を false にして、nodePort の割り当てを止められます(v1.24 から安定版)。既定値は true で、false にするのは Pod へ直接通信を送る実装のときだけにするべき(should)とされます。これが先に触れた入れ子の作りの例外です。すでに割り当てられた nodePort は、false に変えても自動では解放されません。

参考:Service | Kubernetes

公式のチュートリアルによると、LoadBalancer の Service に送られたパケットも、既定では送り元の IP アドレスを書き換えられます。準備のできた(Ready の)、Pod を置けるすべてのノードが、ロードバランサーの振り分け先の候補になるからです。エンドポイントの無いノードに届いたパケットは、エンドポイントのあるノードへ中継され、そのとき送り元の IP アドレスはノードの IP アドレスに書き換えられます。

参考:Using Source IP | Kubernetes

LoadBalancer の Service には、このほかにロードバランサーの実装を選ぶ spec.loadBalancerClass などのフィールドもあります。あとから変えられないものや非推奨のものは、「注意点」の表にまとめます。

kind のクラスタでの制約

第2回で見たとおり、kind は Docker のコンテナを「ノード」にして、手元のマシンにクラスタを作るツールです。ノードの IP アドレスはノードのコンテナの IP アドレスと考えられます。手元のマシンからノードの IP アドレスに届くかどうかは、Docker の動かし方で変わります。

参考:kind

kind の公式ドキュメント(kind v0.33.0 時点)の説明を、Docker の動かし方ごとに次の表にまとめます。

Docker の動かし方kind の公式ドキュメントの説明
Linux で、Docker Desktop を使わずに動かす追加のポートの対応付けなしで、手元のマシンからノードの IP アドレスに通信を送れる
macOS や Windows の Docker DesktopDocker Desktop はコンテナを Linux の仮想マシン(VM)の中で動かすので、コンテナのネットワークが手元のマシンに公開されず、ノードに IP アドレスでは届かない。ポートの対応付け(extraPortMappings)を使うことになる
Linux の Docker DesktopDocker Desktop を入れた場合は、OS が macOS、Windows、Linux のどれでも、ポートの対応付けを使うことになる

参考:Configuration | kind、Known Issues | kind

NodePort の Service はノードの IP アドレスで受けるので、macOS や Windows では、ノードの IP アドレスと nodePort を指定しても届かないはずです。kind の公式ドキュメントはこの制約を回避する方法として、ポートの対応付けや Cloud Provider KIND などを挙げています。

参考:Known Issues | kind

extraPortMappings:手元のマシンのポートをノードへつなぐ

kind のクラスタの設定ファイルの extraPortMappings は、手元のマシンのポートへの通信を、kind のノードへ転送(port forward)する設定です。v0.33.0 時点の kind の公式ドキュメントに沿って、項目を次の表にまとめます。

項目内容既定値
containerPort転送先の、ノードのコンテナのポート記載なし
hostPort手元のマシンで受けるポート記載なし
listenAddress手元のマシンで待ち受けるアドレス0.0.0.0(現時点の既定)
protocolTCP、UDP、SCTP のどれかTCP

参考:Configuration | kind

ここでの containerPort はノードのコンテナ(Docker のコンテナ)のポートです。Pod のマニフェストに書く spec.containers[].ports[].containerPort(第8回)とは別のものです。

kind の公式ドキュメントによると、NodePort の Service でポートの対応付けを使うには、ノードの containerPort と Service の nodePort を同じ番号にする必要があります。nodePort を自動で割り当てさせると番号が前もってわからないので、この方法では nodePort を自分で決めておくことになります。ポートの対応付けは第20回の補足で触れた Docker の -p でのポートの公開を、ノードのコンテナに設定するものと考えられます。

参考:Configuration | kind、Port publishing and mapping | Docker Docs

以下の図は後半の手順で作るクラスタで、手元のマシンの 127.0.0.1:8000 から、ワーカーのノードの Pod まで通信が届く道筋を表しています。

extraPortMappings でコントロールプレーンのノードの 30007 番へつなぎ、NodePort の Service がワーカーのノードの Pod へ転送する道筋

手元のマシンの 127.0.0.1:8000 への通信は、extraPortMappings でコントロールプレーンのノードのコンテナの 30007 番に送られます。30007 番は NodePort の Service の nodePort なので、ノードがそれを Service の転送先の Pod へ転送します。第15回で見たとおり、コントロールプレーンのノードの Taint を許容しない Pod は、コントロールプレーンのノードには置かれません。既定の externalTrafficPolicy: Cluster では、NodePort の Service はどのノードで受けても転送先の Pod へ届けるので、Pod の無いコントロールプレーンのノードで受けても届くはずです。

参考:Configuration | kind、Service | Kubernetes、Taints and Tolerations | Kubernetes

ポートの対応付けはクラスタを作るときに渡す設定ファイルに書きます。そのため、この回の手順ではポートの対応付けを書いた設定ファイルでクラスタを作り直します。

Cloud Provider KIND:kind のクラスタで LoadBalancer を動かす

第4回で見たとおり、kind のクラスタには cloud-controller-manager がありません。kind の公式ドキュメントは kind のクラスタで LoadBalancer の Service を動かす方法として、Cloud Provider KIND を案内しています。Cloud Provider KIND は手元のマシンで単独のプログラムとして動き、kind のクラスタに接続して、Service のために新しいロードバランサーのコンテナを用意します。システムのポートを開くにも、コンテナランタイム(この回では手元の Docker)に接続するにも、権限が必要です。

参考:LoadBalancer | kind

kind の公式ドキュメントが入れ方の説明として案内している、Cloud Provider KIND の README には、次の点が書かれています。表の WSL2 は Windows の中で Linux を動かす仕組み(WSL)のバージョン 2 です。

項目README の説明
動かし方端末で動かし続ける。すべての kind のクラスタと、LoadBalancer の Service を見張る
権限インターフェースに IP アドレスを足す権限と、特権ポート(一般に 1024 より小さい番号のポート)で待ち受ける権限が要る。macOS と WSL2 では sudo を付けて、Windows では「管理者として実行」で開いたシェルから実行する必要がある
macOS と Windowsコンテナが VM の中で動き、ノードに手元のマシンから届かないので、割り当てた IP アドレスにも直接は届かない。代わりに Docker のポートの対応付けでロードバランサーを手元のマシンに公開し、localhost の、割り当てたポートで届くようにする(必要な環境では自動で有効になる)
WSL2Windows の Docker Desktop の既定の NAT のネットワークモードを使うことが勧められている。WSL2 では外部の IP アドレスで、Windows では Docker のポートの対応付けで届く
開発の段階まだ開発の初期の段階(very alpha state)で、不具合があり得る

参考:kubernetes-sigs/cloud-provider-kind

名前と、cloud-controller-manager の役目を考えると、Cloud Provider KIND は kind のための cloud-controller-manager の代わりを、クラスタの外で務めるものと読めます。

手元で試す

ここまでの内容を手元の kind のクラスタで試す手順を示します。ポートの対応付けを書いた設定ファイルでマルチノードのクラスタを作り、NodePort の Service に手元のマシンから届くことを確かめます。続けて LoadBalancer の Service を作り、そのままでは外部の IP アドレスが割り当てられないこと、Cloud Provider KIND を動かすと割り当てられて届くことを確かめます。

この節で引用する出力の例は、公式ドキュメントにある、ほかのクラスタのものです。表示の形を見るためのもので、名前、IP アドレス、ポートの番号などの値は手元とは違います。各段の最後には成功かどうかを見分ける点を短く挙げます。どれも公式ドキュメントから予想される結果で、手元で実行して確かめたものではありません。

コマンドは macOS や Linux のシェル(bash や zsh)で実行する前提で書いています。Windows では WSL(Windows の中で Linux を動かす仕組み)などを使ってください。WSL2 で試す場合、LoadBalancer の段の動かし方と問い合わせ方は、README の WSL2 の説明に沿って分けて示します。試す環境は次のとおりです。

項目内容
kindv0.33.0
Kubernetesv1.37.0(kind v0.33.0 の既定のノードイメージ)
ノード構成マルチノード(クラスタ名 multi-node):コントロールプレーン 1、ワーカー 2。コントロールプレーンのノードにポートの対応付けを書く
必要なものDocker、kubectl(v1.36〜v1.38)、curl、テキストエディタ、端末(ターミナル)3 つ
LoadBalancer の段で必要なものCloud Provider KIND(入れるには、プログラミング言語 Go の開発環境か、配布ページのバイナリ)

参考:Release v0.33.0 · kubernetes-sigs/kind、Version Skew Policy | Kubernetes

kind でクラスタを作る手順は、以下の記事で解説しています。この回は第20回・第21回と同じくマルチノードのクラスタを使います。Pod がワーカーのノードに置かれるので、Pod の無いコントロールプレーンのノードで受けた通信も、NodePort の Service が Pod へ届けることを確かめられるからです。

基本仕様 ・ 第2回 kind で検証用のクラスタを作るDocker のコンテナをノードにして、手元のマシンに Kubernetes のクラスタを作るツール kind の仕組みを図で説明します。kind create cluster で何が起きるかを押さえ、シングルノードとマルチノードのクラスタを作って、ノードの正体が Docker のコンテナであることを確かめる手順を紹介します。

ポートを対応付けたクラスタを用意する

次の設定ファイルを使います。第2回のマルチノードの設定ファイルに、kind の公式ドキュメントの「NodePort with Port Mappings」の例にならって、コントロールプレーンのノードの extraPortMappings を足したものです。kind-multi-node-nodeport.yaml という名前で、コマンドを実行するディレクトリに保存します。

yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30007
    hostPort: 8000
    listenAddress: "127.0.0.1"
    protocol: TCP
- role: worker
- role: worker

参考:Configuration | kind、Configuration | kind、Quick Start | kind

手元のマシンの 8000 番への通信を、コントロールプレーンのノードのコンテナの 30007 番へ転送する設定です。この containerPort はノードのコンテナのポートで、Pod の containerPort とは別のものです。30007 はあとで作る NodePort の Service の nodePort と同じ番号にしてあります。listenAddress を 127.0.0.1 にしたのは、手元のマシンの中からだけ届くようにするためです。一般に 127.0.0.1 は自分自身を指すアドレスで、ほかのマシンからは使えません。kind の既定の 0.0.0.0 のままにすると、手元のマシンのすべてのアドレスで待ち受けると考えられます。kind の公式ドキュメントも API サーバーのアドレスの説明の中で、kind をループバック(自分自身)以外に公開しないよう強く勧めています(原文は “We strongly discourage exposing kind to anything other than loopback.")。この回の 127.0.0.1 もその考え方に合わせたものです。

参考:Configuration | kind、Configuration | kind

8000 番を手元のほかのプログラムが使っている場合は、hostPort を別の番号に変え、以降の 8000 もその番号に読み替えます。使われている番号のまま kind create cluster を実行すると、ポートを使えないことを知らせるエラーで止まると考えられます。そのときは、番号を変えてから作り直します。

まず、前の回のクラスタが残っていないかを確かめます。

console
$ kind get clusters

multi-node が表示されたら、ポートの対応付けの無い設定で作ったクラスタなので、次のコマンドで削除します。表示されなければ、このコマンドは飛ばします。

console
$ kind delete cluster --name multi-node

この回の設定ファイルでクラスタを作り、一覧します。

console
$ kind create cluster --name multi-node --config kind-multi-node-nodeport.yaml
$ kind get clusters

kind get clusters に multi-node が表示されれば、クラスタができています。第3回で決めたとおり、ここからのコマンドには --context kind-multi-node を付けて、操作するクラスタをはっきりさせます。

参考:Quick Start | kind

続けて、ノードを一覧します。

console
$ kubectl get nodes --context kind-multi-node

ノードが 3 つ表示され、STATUS 列が Ready になっていれば準備ができています。Ready になっていなければ、少し待ってからもう一度実行します。

参考:kubectl get | Kubernetes、Node Status | Kubernetes

マニフェストを用意する

使うマニフェストは 3 つです。1 つ目は kind の公式ドキュメントの LoadBalancer のガイドにある例から、2 つの Pod の部分を取り出したものです(Pod の部分はそのまま引用しています)。http-echo-pods.yaml という名前で、コマンドを実行するディレクトリに保存します。

yaml
kind: Pod
apiVersion: v1
metadata:
  name: foo-app
  labels:
    app: http-echo
spec:
  containers:
  - command:
    - /agnhost
    - serve-hostname
    - --http=true
    - --port=8080
    image: registry.k8s.io/e2e-test-images/agnhost:2.39
    name: foo-app
---
kind: Pod
apiVersion: v1
metadata:
  name: bar-app
  labels:
    app: http-echo
spec:
  containers:
  - command:
    - /agnhost
    - serve-hostname
    - --http=true
    - --port=8080
    image: registry.k8s.io/e2e-test-images/agnhost:2.39
    name: bar-app

参考:LoadBalancer | kind

foo-app と bar-app の 2 つの Pod を作ります。どちらも Pod そのもの(metadata.labels)に app: http-echo のラベルが付いていて、8080 番のポートで HTTP のリクエストを受けます。--- は 1 つのファイルに複数のマニフェストを並べるときの区切りです。この回では Deployment を使わず、kind のガイドの例に合わせて Pod を直接作ります。agnhost はイメージの置き場所の名前(e2e-test-images)から、Kubernetes のテスト用のイメージと読めます。serve-hostname は、名前からホスト名を返す機能と読めます。公式ドキュメントによると、Pod のホスト名(Pod の中から見たもの)は Pod の metadata.name です。そのため、応答の中身から、どちらの Pod が応答したのかを見分けられると考えられます。

参考:DNS for Services and Pods | Kubernetes、LoadBalancer | kind

2 つ目は NodePort の Service です。kind の公式ドキュメントの NodePort の例と、上の Pod に合わせて書いたものです。foo-nodeport.yaml という名前で保存します。

yaml
apiVersion: v1
kind: Service
metadata:
  name: foo-nodeport
spec:
  type: NodePort
  selector:
    app: http-echo
  ports:
  - port: 5678
    targetPort: 8080
    nodePort: 30007

参考:Configuration | kind、Service | Kubernetes API Reference

app: http-echo のラベルを持つ Pod の 8080 番へ通信を送る、NodePort の Service です。port の 5678 は cluster IP で接続するときのポート、nodePort の 30007 はノードで受けるポートです。30007 は設定ファイルの containerPort と同じで、NodePort の静的な帯(30000〜30085)に入っています。protocol を書いていないので、既定の TCP になります。

3 つ目は LoadBalancer の Service です。kind の公式ドキュメントの例の Service の部分です(そのまま引用しています)。foo-service.yaml という名前で保存します。

yaml
kind: Service
apiVersion: v1
metadata:
  name: foo-service
spec:
  type: LoadBalancer
  selector:
    app: http-echo
  ports:
  - port: 5678
    targetPort: 8080

参考:LoadBalancer | kind

同じ app: http-echo の Pod へ通信を送る、LoadBalancer の Service です。nodePort を書いていないので、nodePort はコントロールプレーンが自動で割り当てるはずです。

Service の監視を始める

ここからは端末を 3 つ使います。第21回と同じく、1 つ目の端末で変化を追い続け、2 つ目の端末でマニフェストを渡したり、curl で問い合わせたりします。3 つ目の端末は LoadBalancer の段で Cloud Provider KIND を動かし続けるのに使います。2 つ目の端末を開いたら、cd でマニフェストを保存したディレクトリに移動しておきます。

1 つ目の端末で、Service を監視します。第3回で見たとおり、--watch を付けると、一覧を表示したあとも変化を追い続けます。この監視は片付けの前まで止めずに続けます。

console
$ kubectl get services --watch --context kind-multi-node

参考:kubectl get | Kubernetes

まだ何も作っていないので、kubernetes という Service の行だけが表示されたあと、止まったように見えると考えられます。第21回の片付けで見たとおり、kubernetes は kube-apiserver をクラスタの中に公開している Service です。

参考:Accessing the Kubernetes API from a Pod | Kubernetes

Pod と NodePort の Service を作る

2 つ目の端末で、Pod と NodePort の Service のマニフェストを順に渡します。

console
$ kubectl apply -f http-echo-pods.yaml --context kind-multi-node
$ kubectl apply -f foo-nodeport.yaml --context kind-multi-node

pod/foo-app、pod/bar-app、service/foo-nodeport が作られたことを知らせる行が出るはずです。1 つ目の端末には foo-nodeport の行が現れ、TYPE 列が NodePort になっているはずです。PORT(S) 列には 5678:30007/TCP のように、port と nodePort とプロトコルが並ぶと考えられます。公式ドキュメントにも、LoadBalancer の Service で 8080:30163/TCP と表示した例があり、NodePort でも同じ形になると考えられます。EXTERNAL-IP 列は第21回の ClusterIP の Service の例と同じく <none> になると考えられます。

参考:Connecting Applications with Services | Kubernetes

次に、Pod を IP アドレスとノードの列つきで一覧します。Pod そのものに app: http-echo のラベルが付いているので、-l app=http-echo で絞り込めます。

console
$ kubectl get pods -l app=http-echo -o wide --context kind-multi-node

参考:kubectl get | Kubernetes

foo-app と bar-app の 2 行が表示され、STATUS 列がやがて Running になるはずです。クラスタを作ったばかりなら、イメージの取得に時間がかかることがあります。NODE 列にはワーカーのノードの名前が出ると考えられます。コントロールプレーンのノードには置かれないはずです(第15回)。2 つの Pod が同じワーカーのノードに置かれることもあると考えられます。前の段の kubectl get nodes の一覧と見比べてみてください。

続けて、Service のくわしい情報を表示します。

console
$ kubectl describe svc foo-nodeport --context kind-multi-node

公式のチュートリアルの例では、NodePort の Service の出力は次のとおりです。

Name:                   example-service
Namespace:              default
Labels:                 run=load-balancer-example
Annotations:            <none>
Selector:               run=load-balancer-example
Type:                   NodePort
IP:                     10.32.0.16
Port:                   <unset> 8080/TCP
TargetPort:             8080/TCP
NodePort:               <unset> 31496/TCP
Endpoints:              10.200.1.4:8080,10.200.2.5:8080
Session Affinity:       None
Events:                 <none>

参考:Use a Service to Access an Application in a Cluster | Kubernetes

例は example-service という別の Service のもので、名前、ラベル、ポートの番号が手順とは違います。手元では次のように見えるはずです。

  • Name: が foo-nodeport、Selector: が app=http-echo、Type: が NodePort
  • Port: の行が <unset> 5678/TCP、TargetPort: の行が 8080/TCP
  • NodePort: の行が <unset> 30007/TCP(自分で決めた番号)
  • Endpoints: の行に、2 つの Pod の IP アドレスと :8080 が並ぶ

IP: の行は cluster IP で、kind(v0.33.0 時点)の既定の Service の範囲 10.96.0.0/16 に入るはずです(第21回)。NodePort の Service にも cluster IP があることがわかります。手元では、Labels: の行が例と違い、例に無い行が出ることもあると考えられます。

参考:Configuration | kind、Service | Kubernetes

この段では、次の 3 点が合っていれば成功です。

  • 2 つの Pod の STATUS 列が Running で、NODE 列がワーカーのノード
  • 1 つ目の端末に foo-nodeport の行が現れ、TYPE 列が NodePort
  • kubectl describe の NodePort: の行が <unset> 30007/TCP で、Endpoints: の行に IP アドレスが 2 つ並ぶ

手元のマシンから NodePort に届ける

2 つ目の端末で、手元のマシンの 127.0.0.1:8000 に 10 回問い合わせます。kind の公式ドキュメントの LoadBalancer のガイドで、外部の IP アドレスに 10 回問い合わせている書き方にならっています。for _ in {1..10}; do ...; done は、do と done のあいだのコマンドを 10 回繰り返す、シェルの書き方です。

console
$ for _ in {1..10}; do curl 127.0.0.1:8000; done

参考:LoadBalancer | kind

kind のガイドは同じ 2 つの Pod に LoadBalancer の Service で 10 回問い合わせたときの結果を、「foo と bar が別々の行に出るはず」(原文のコメントは should output foo and bar on separate lines)としています。NodePort の Service でも転送先は同じ 2 つの Pod なので、foo-app と bar-app のどちらの Pod から応答が返ったのかを表す値が、混ざって出ると考えられます。応答の中身は Pod のホスト名(Pod の名前)のはずです。1 行につながって見えても、foo-app と bar-app が混ざっていれば成功です。

参考:LoadBalancer | kind、DNS for Services and Pods | Kubernetes

この通信は手元のマシンの 8000 番から、extraPortMappings でコントロールプレーンのノードのコンテナの 30007 番に送られたはずです。コントロールプレーンのノードには Pod が無いので、届いた応答は、ノードがワーカーのノードの Pod へ転送した結果と考えられます。第21回で見たとおり、kind のクラスタでは kube-proxy がノードごとに動き、iptables のモードでは、既定では転送先を無作為に選びます。2 種類の応答が混ざって出るのは、そのためと考えられます。

参考:Configuration | kind、Service | Kubernetes、Virtual IPs and Service Proxies | Kubernetes

何も返らずに止まったままなら、Ctrl+C で止めます。そのうえで、2 つの Pod が Running になっているか、設定ファイルの containerPort と Service の nodePort が同じ 30007 になっているかを見直します。

この段では、次の 2 点が合っていれば成功です。

  • 10 回とも応答が返る
  • 応答に、2 つの Pod のどちらから返ったかを表す 2 種類の値が混ざって出る(無作為に選ぶので、片方に偏ることもある)

ノードの IP アドレスで NodePort に届ける(Linux で Docker Desktop を使っていない場合)

この段は Linux で Docker Desktop を使わずに Docker を動かしている場合だけ試せます。macOS や Windows の Docker Desktop では、ノードに IP アドレスでは届かないので飛ばします(「kind のクラスタでの制約」)。

2 つ目の端末で、ノードを IP アドレスの列つきで一覧します。

console
$ kubectl get nodes -o wide --context kind-multi-node

参考:kubectl get | Kubernetes

INTERNAL-IP 列に、3 つのノードの IP アドレスが出るはずです。kind ではノードのコンテナの IP アドレスと考えられます。3 つのノードそれぞれについて、<ノードの INTERNAL-IP> に値を入れて問い合わせます。

console
$ curl <ノードの INTERNAL-IP>:30007

kind の公式ドキュメントによると、この環境ではポートの対応付けなしでノードの IP アドレスに通信を送れます。NodePort の Service は iptables のモードではノードのすべての IP アドレスで受け、externalTrafficPolicy が既定の Cluster のままなら、どのノードで受けても転送先の Pod へ届けるので、3 つのノードのどれに問い合わせても応答が返るはずです。Pod の無いコントロールプレーンのノードでも同じです。

参考:Configuration | kind、Service | Kubernetes、Service | Kubernetes

この段では 3 つのノードのどの IP アドレスに問い合わせても、30007 番で応答が返れば成功です。

LoadBalancer の Service を作る

2 つ目の端末で、LoadBalancer の Service のマニフェストを渡します。3 つ目の端末の Cloud Provider KIND は、まだ動かしません。

console
$ kubectl apply -f foo-service.yaml --context kind-multi-node

service/foo-service が作られたことを知らせる行が出るはずです。1 つ目の端末には foo-service の行が現れ、TYPE 列が LoadBalancer、EXTERNAL-IP 列が <pending> になると考えられます。kind のクラスタには cloud-controller-manager が無く(第4回)、Cloud Provider KIND もまだ動かしていないので、ロードバランサーを用意するものがいないはずだからです。しばらく待っても、<pending> のまま変わらないはずです。PORT(S) 列には 5678 と、自動で割り当てられた nodePort が並ぶと考えられます。

参考:Exposing an External IP Address to Access an Application in a Cluster | Kubernetes、Cluster Architecture | Kubernetes、Connecting Applications with Services | Kubernetes

外部の IP アドレスが無いあいだも、nodePort は割り当てられているはずです。-o jsonpath で、foo-service の nodePort を取り出します。公式のチュートリアルにも同じ JSONPath の式({.spec.ports[0].nodePort})で nodePort を取り出す例があります。

console
$ kubectl get svc foo-service -o jsonpath='{.spec.ports[0].nodePort}{"\n"}' --context kind-multi-node

参考:Using Source IP | Kubernetes、JSONPath Support | Kubernetes

数字が 1 つ表示されるはずです。nodePort を書いていないので、コントロールプレーンが自動で割り当てた番号です。自動の割り当ては既定では動的な帯を使うので、30086〜32767 のどれかになると考えられます。1 つ目の端末の PORT(S) 列の番号とも見比べてみてください。LoadBalancer の Service が NodePort の上に作られていることが、ここで確かめられます。この nodePort は extraPortMappings で手元のマシンにつないでいないので、macOS や Windows の手元のマシンからは使えません。

参考:Service | Kubernetes、Service | Kubernetes

この段では、次の 2 点が合っていれば成功です。

  • 1 つ目の端末の foo-service の行で、EXTERNAL-IP 列が <pending> のまま
  • nodePort を取り出すコマンドで、30000〜32767 の範囲の数字が表示される(上の帯を使い切ると下の帯も使うことがあるので、ここでは範囲全体にしています)

Cloud Provider KIND で外部の IP アドレスを割り当てる

まず、Cloud Provider KIND を入れます。kind の公式ドキュメントでは、Go で入れる方法と、配布ページのバイナリ(そのまま実行できる形にしたプログラムのファイル)を使う方法が案内されています。Go で入れる場合のコマンドは次のとおりです。README によると、入れたプログラムは $GOBIN(ふつうは ~/go/bin)に置かれます。

console
$ go install sigs.k8s.io/cloud-provider-kind@latest

参考:LoadBalancer | kind、kubernetes-sigs/cloud-provider-kind

3 つ目の端末で、Cloud Provider KIND を動かします。kind の公式ドキュメントによると、ポートを開く権限と、コンテナランタイムに接続する権限が要ります。README の Limitations にも、インターフェースに IP アドレスを足す権限と、特権ポートで待ち受ける権限が要るとあります。macOS と WSL2 では README のとおり sudo を付けて実行します。

console
$ sudo cloud-provider-kind

Linux で sudo が要るかどうかは、README に明記がありません。まず sudo を付けずに実行し、権限が足りないことを知らせるエラーで止まるか、そのエラーが出続ける場合は、sudo を付けて実行してください。

console
$ cloud-provider-kind

どちらの場合も、コマンドが見つからないことを知らせるエラー(command not found など)が出たら、~/go/bin/cloud-provider-kind のように置き場所を含めて指定します。

参考:LoadBalancer | kind、kubernetes-sigs/cloud-provider-kind

Cloud Provider KIND は動き続け、3 つ目の端末にはその記録が流れると考えられます。この端末は片付けの前まで、このままにしておきます。

しばらくすると、1 つ目の端末に foo-service の行が書き足され、EXTERNAL-IP 列が <pending> から IP アドレスに変わるはずです。Cloud Provider KIND が foo-service のためにロードバランサーのコンテナを用意し、Service の status.loadBalancer に書き込んだと考えられます。いつまでも <pending> のままなら、3 つ目の端末にエラーが出ていないかと、sudo などの権限が足りているかを見直します。

2 つ目の端末で、foo-service のくわしい情報を表示します。

console
$ kubectl describe svc foo-service --context kind-multi-node

公式のチュートリアルの例では、LoadBalancer の Service の出力は次のとおりです。

Name:           my-service
Namespace:      default
Labels:         app.kubernetes.io/name=load-balancer-example
Annotations:    <none>
Selector:       app.kubernetes.io/name=load-balancer-example
Type:           LoadBalancer
IP:             10.3.245.137
LoadBalancer Ingress:   104.198.205.71
Port:           <unset> 8080/TCP
NodePort:       <unset> 32377/TCP
Endpoints:      10.0.0.6:8080,10.0.1.6:8080,10.0.1.7:8080 + 2 more...
Session Affinity:   None
Events:         <none>

参考:Exposing an External IP Address to Access an Application in a Cluster | Kubernetes

例は my-service という別の Service のもので、クラウドのクラスタで Pod を 5 つ動かしたときのものです。手元では、Name: が foo-service、Port: の行が <unset> 5678/TCP になり、Endpoints: の行には 2 つの Pod の IP アドレスだけが並ぶはずです。LoadBalancer Ingress: の行には、1 つ目の端末の EXTERNAL-IP 列と同じ IP アドレスが出ると考えられます。NodePort: の行には、前の段で取り出した番号が <unset> に続いて出るはずです。手元では、例に無い行が出ることもあると考えられます。

ここからの問い合わせ方は README の説明に沿って次の 2 つに分かれます。

環境問い合わせ先
Linux(Docker Desktop を使わない)と WSL2外部の IP アドレスと、Service の port(5678)
macOS と Windowslocalhost と、ロードバランサーのコンテナに対応付けられた手元のマシンのポート

参考:kubernetes-sigs/cloud-provider-kind

Linux の Docker Desktop については README に記載がありません。Docker の公式ドキュメントによると Linux の Docker Desktop も VM を動かすので、macOS と同じく localhost のポートを使うことになると考えられます。ポートの対応付けが出ない場合は、README の Enabling Load Balancer Port Mapping の節にあるとおり、--enable-lb-port-mapping を付けて Cloud Provider KIND を動かします。

参考:Install Docker Desktop on Linux | Docker Docs、kubernetes-sigs/cloud-provider-kind

Linux(Docker Desktop を使わない)と WSL2 の場合は、kind のガイドのとおり、外部の IP アドレスを変数 LB_IP に入れてから、5678 番に 10 回問い合わせます。5678 は Service の port で、kubectl describe の Port: の行の番号です。外部のロードバランサーは Service の port で受けると考えられます(「LoadBalancer:外部のロードバランサーを前に置く」)。$( ... ) は、中のコマンドの出力をその場に埋め込むシェルの書き方です。

console
$ LB_IP=$(kubectl get svc/foo-service -o=jsonpath='{.status.loadBalancer.ingress[0].ip}' --context kind-multi-node)
$ for _ in {1..10}; do curl ${LB_IP}:5678; done

参考:LoadBalancer | kind

kind のガイドのコメントでは、foo と bar(2 つの Pod)からの応答が別々の行に出るはずとされています。前の段と同じく、1 行につながって見えても foo-app と bar-app が混ざっていれば成功です。

macOS と Windows の場合は、README によると、割り当てた IP アドレスには手元のマシンから直接は届きません。代わりに、ロードバランサーのコンテナ(名前は kindccm- で始まる)に手元のマシンのポートが対応付けられ、localhost のそのポートで届きます。README には Podman(Docker に似た、コンテナを動かすツール)でのコンテナの一覧(podman ps)の出力例があり、PORTS 列に 0.0.0.0:42381->5678/tcp のような対応が出ています。矢印の左の 42381 が手元のマシンのポート、右の 5678 がコンテナのポートと読めます。README はこの一時的な(ephemeral)ポートで Service に接続するよう説明しています。Docker では docker ps で同じように確かめられると考えられます。--filter name=kindccm は、名前に kindccm を含むコンテナだけを一覧する指定です。

console
$ docker ps --filter name=kindccm

参考:kubernetes-sigs/cloud-provider-kind、docker container ls | Docker Docs

PORTS 列に、->5678/tcp の形で、手元のマシンのポートとコンテナの 5678 番との対応が出ると考えられます。矢印の左の、: の後ろの番号を <手元のマシンのポート> に入れて、10 回問い合わせます。

console
$ for _ in {1..10}; do curl localhost:<手元のマシンのポート>; done

こちらも、2 つの Pod からの応答が混ざって出るはずです。

この段では、次の 3 点が合っていれば成功です。

  • 1 つ目の端末の foo-service の行で、EXTERNAL-IP 列が <pending> から IP アドレスに変わる
  • kubectl describe の LoadBalancer Ingress: の行に、その IP アドレスが出る
  • 問い合わせた 10 回の応答に、2 つの Pod からの応答が混ざって出る

片付け

1 つ目の端末で Ctrl+C を押して、監視を止めます。

2 つ目の端末で、作った Service と Pod を、マニフェストを指定して削除します。LoadBalancer の Service は、Cloud Provider KIND が動いているうちに消します。Cloud Provider KIND が Service の削除に気づいて、用意したロードバランサーのコンテナを片付けると考えられるからです。

console
$ kubectl delete -f foo-service.yaml --context kind-multi-node
$ kubectl delete -f foo-nodeport.yaml --context kind-multi-node
$ kubectl delete -f http-echo-pods.yaml --context kind-multi-node

参考:kubectl delete | Kubernetes

Pod を消すコマンドは Pod の終了までの猶予の時間(既定では 30 秒)のぶん、終わるまで少しかかることがあります(第20回)。少し待ってから、残っていないかを確かめます。

console
$ kubectl get pods -l app=http-echo --context kind-multi-node
$ kubectl get services --context kind-multi-node

参考:Pod Lifecycle | Kubernetes、kubectl get | Kubernetes

1 つ目のコマンドで、見つからないことを知らせる 1 行が出れば、Pod は削除できています。2 つ目のコマンドでは、kubernetes という Service だけが表示されると考えられます。この回で作ったものではないので、消さずに残します。

3 つ目の端末で Ctrl+C を押して、Cloud Provider KIND を止めます。そのあと、2 つ目の端末で、ロードバランサーのコンテナが残っていないかを確かめます。

console
$ docker ps -a --filter name=kindccm

-a は、止まっているコンテナも含めて一覧する指定です。見出しの行だけが表示されれば、残っていません。kindccm- で始まるコンテナが残っていたら、NAMES 列の名前を <コンテナの名前> に入れて削除します。-f は、動いているコンテナも止めて削除する指定です。

console
$ docker rm -f <コンテナの名前>

参考:docker container ls | Docker Docs、docker container rm | Docker Docs

最後に、クラスタを削除し、消えたことを確かめます。

console
$ kind delete cluster --name multi-node
$ kind get clusters

kind delete cluster は、存在しないクラスタを指定してもエラーを返しません。kind get clusters に multi-node が表示されなくなったことを確かめます。kubeconfig に kind-multi-node のコンテキストが残っていたら、第3回の片付けと同じく kubectl config delete-context kind-multi-node で消します。次の回からはポートの対応付けの無い第2回の設定ファイル(kind-multi-node.yaml)に戻ります。

参考:Quick Start | kind、kubectl config | Kubernetes

注意点

NodePort と LoadBalancer でつまずきやすい点と、あとから変えられない設定、バージョンで変わる点を、次の表にまとめます。

項目内容
nodePort の既定の範囲は 30000〜32767kube-apiserver の --service-node-port-range で決まり、この範囲はノードのエフェメラルポート(一般に、通信を始める側に OS が一時的に割り当てるポート)の範囲と重ならないようにする必要がある(must)。自分で nodePort を決めるなら、範囲の中で、まだ使われていない番号にし、静的な帯(既定の範囲では 30000〜30085)から選べばぶつかる心配が少ない
nodePort は要らない Service に書くと作成に失敗するAPI リファレンスによると、nodePort を必要としない Service(ClusterIP など)を作るときに書くと、作成は失敗する。type を ClusterIP に変えるなど、要らなくなる更新では消される
NodePort の Service が届くノードの IP アドレスは、kube-proxy のモードで変わる既定では iptables(v1.37 時点の kube-proxy の既定のモード)と ipvs(v1.35 から非推奨。第21回)のモードではすべての IP アドレス、nftables のモードでは主な IP アドレスだけ。kube-proxy の --nodeport-addresses(設定ファイルでは nodePortAddresses)に、IP アドレスの範囲か primary、localhost、all を書いて変えられる
ノードの上の localhost で nodePort に届くかも、モードで変わる(手順の手元のマシンの 127.0.0.1:8000 とは別の話)iptables のモードでは --nodeport-addresses に 127.0.0.1 を含めれば届く(カーネルの設定 route_localnet が要り、クラスタによっては安全上の副作用があり得る)が、ipvs のモードと、IPv6 だけのクラスタの iptables のモードでは届かない。nftables のモードでは、v1.37 からのアルファ版の機能ゲート KubeProxyNFTablesLocalhostNodePorts(既定で無効)を有効にし、localhost を明示的に書いた場合だけ届く(all では届かず、ふつうの転送より効率は落ちる)
送り元の IP アドレスは、既定では書き換えられるNodePort と LoadBalancer の Service は既定(externalTrafficPolicy: Cluster)では SNAT される。残したいときは Local にするが、Pod の無いノードに届いた通信は転送されない
LoadBalancer の Service には、ロードバランサーを用意するものが要るKubernetes 自身はロードバランサーの部品を直接は提供しない。手元の PC の学習用の環境のクラスタには cloud-controller-manager が無く、kind では Cloud Provider KIND などを使う
loadBalancerClass はあとから変えられないクラウドプロバイダーの既定とは別の、ロードバランサーの実装を選ぶフィールド(v1.24 から安定版)で、書くと既定の実装はその Service を無視する。一度書くと変えられず、LoadBalancer 以外の type に変えると消される
healthCheckNodePort はあとから変えられないexternalTrafficPolicy: Local の LoadBalancer の Service に割り当てられる、ヘルスチェック用のポート。書かなければ自動で割り当てられ、一度決まると変えられない
loadBalancerIP と externalIPs は非推奨spec.loadBalancerIP は v1.24 から非推奨で、公式ドキュメントは提供元が独自のアノテーション(第42回)で IP アドレスを指定できるならそちらに切り替えるべき(should)としている。spec.externalIPs は v1.36 から非推奨で、外部のロードバランサーのコントローラーか Gateway API の実装への移行を始めるべき(should)としている
kind のノードはコンテナmacOS や Windows の Docker Desktop では、ノードに IP アドレスで届かない。NodePort の Service には extraPortMappings で届け、containerPort と nodePort を同じ番号にする
この回の手順で externalTrafficPolicy: Local にした場合extraPortMappings の転送先のコントロールプレーンのノードには Pod が無いので、127.0.0.1:8000 への通信は Pod に届かなくなると考えられる

参考:Service | Kubernetes、Service | Kubernetes、kube-apiserver | Kubernetes、Service | Kubernetes、Service | Kubernetes、Service | Kubernetes、Service | Kubernetes、Service | Kubernetes、Service | Kubernetes API Reference、Service | Kubernetes API Reference、Virtual IPs and Service Proxies | Kubernetes、Virtual IPs and Service Proxies | Kubernetes、Using Source IP | Kubernetes、Cluster Architecture | Kubernetes、Known Issues | kind、Configuration | kind

関連記事

ClusterIP の Service をクラスタの外に公開する、もう 1 つの方法である Ingress と Gateway API については、以下の記事で解説しています。

基本仕様 ・ 第24回 Ingress と Gateway API の違いクラスタの外からの HTTP の通信を Service に振り分ける Ingress と Gateway API の違いを図で説明します。Ingress コントローラーが要ること、CRD で足す Gateway API の GatewayClass・Gateway・HTTPRoute の役割の分担を押さえ、kind のクラスタで同じ振り分けを両方で書いて比べる手順を紹介します。

この回では IP アドレスとポートで Service に接続しましたが、Service の名前で接続できる、クラスタの中の DNS については、以下の記事で解説しています。

基本仕様 ・ 第23回 なぜ Service 名で通信できるのかService の名前を IP アドレスに変えるクラスタ DNS の仕組みを図で説明します。CoreDNS と kube-dns の Service、kubelet が Pod に書き込む /etc/resolv.conf の search と ndots、Namespace をまたぐ名前の書き方を押さえ、kind のクラスタで名前を引いて確かめる手順を紹介します。

Kubernetes 基本仕様の目次へ