あなたの知らないKubernetesのServiceの仕組み
2021年06月15日 火曜日
CONTENTS
Kubernetesの主要なリソースの一つにServiceリソースがあります。ServiceリソースとはKubernetes上のPodへクラスタの外からアクセスするために使うもの、という理解をしている人が多いかもしれません。確かにそのような役割を担っているのですが、実際にはクラスタ内部に閉じた通信にも利用されていますし、実はもっといろいろな機能を持っています。
端的に説明すれば、Serviceとは「ロードバランサとDNSサーバを設定するためのリソース」です。意外に聞こえますか? もし意外に思えたなら、ぜひこのまま読み進めてみてください。
本ブログと連動した解説動画も公開しています。こちらもぜひご覧ください。
インターナルなロードバランサを制御する
Kubernetesにはクラスタ内部に閉じた通信を制御するロードバランサが内蔵されています。Kubernetesを利用するということは、ほぼ例外なくこのロードバランサを利用しているのですが、あまり意識せずに利用されていることが多いかもしれません。
たとえば、以下の操作でnginxをデプロイし、Service経由でアクセスできるようにします。この場合、クラスタの外からはアクセスできませんが、実はすでにロードバランサが構成されています。
$ kubectl create deploy nginx --image=nginx:1.19 $ kubectl expose deploy nginx --port=80 --target-port=80
作成されたPodリソース、Serviceリソース、Endpointリソース(Serviceリソースと対になり自動的に作成されます)を見てみましょう。注目してほしいのは以下2つのIPアドレスです。
- ServiceのCLUSTER-IP: 10.102.97.154
- EndpointのENDPOINTS: 192.168.76.141(=Podのアドレス)
$ kubectl get svc nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx ClusterIP 10.102.97.154 <none> 80/TCP 98s $ kubectl get ep nginx NAME ENDPOINTS AGE nginx 192.168.76.141:80 106s $ kubectl get pods --selector=app=nginx -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-7b54d48599-2j79v 1/1 Running 0 3m14s 192.168.76.141 xxxxxxxxxxx <none> <none>
これは「10.102.97.154へアクセスすると、ロードバランサによって192.168.76.141へルーティングされる」ように設定されたことを意味します。つまり、service cluster networkとはロードバランサがサービスを公開するためのネットワークであり、Endpointリソースとはロードバランサがルーティングする宛先を管理するリソースということです。
図1 インターナルなロードバランサ
Podを2個にスケールアウトするとEndpointリソースには2つのIPアドレスが現れますが、これはロードバランサによって2つのPodへ負荷分散されるようになったことを意味します。
$ kubectl scale deploy nginx --replicas=2 $ kubectl get ep nginx NAME ENDPOINTS AGE nginx 192.168.76.141:80,192.168.76.142:80 10m
このロードバランサはkube-proxyによって制御されていて、モードがいくつかあるのですが、ipvsモード(LVSなどで使われているLinuxのカーネルモジュールとして実装されたロードバランサ)を利用している場合は、ipvsadmコマンドで以下のようにロードバランサが設定されていることを確認できます。
$ sudo ipvsadm -L -n IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.102.97.154:80 rr -> 192.168.76.141:80 Masq 1 0 0 -> 192.168.76.142:80 Masq 1 0 0
たったこれだけの操作でWebサーバがデプロイされ、ロードバランサによって負荷分散および冗長化されたシステムが作れるのはすごいと思いませんか?今回はヘルスチェックの設定を省略していますが、数行追加すれば障害が発生したノードを自動的にロードバランサから除外することも可能です。
Kubernetesの権威ネームサーバ
次にDNSの話をしましょう。Kubernetesはクラスタ内部に権威ネームサーバを持っています。このネームサーバはクラスタ外からアクセスされることはなく(できなくはありません)、主な用途はロードバランサが公開したservice cluster networkのIPアドレスに名前をつけることです。そして、その名前付けはServiceリソースを作成すると自動的に行われます。
前述の例であれば、ネームスペースdefaultにnginxという名前でServiceリソースを作成したので、「nginx.default.svc.cluster.local.」というFQDNでネームサーバへAレコードが登録されていることが確認できます。名前解決されたIPアドレスが前述のServiceのCLUSTER-IPと一致していることを確認してください。これによって、Serviceリソースを作成するとロードバランサが構成され、またそのロードバランサが公開したアドレスはネームサーバにAレコードが登録されるため、クラスタ内部では名前でアクセスできるということがわかりました。
$ dig @10.96.0.10 nginx.default.svc.cluster.local. ;; ANSWER SECTION: nginx.default.svc.cluster.local. 30 IN A 10.102.97.154
また、各Podの中では例えば以下のように複数のサーチドメインがresolv.confに設定されています。つまり、FQDNでは「nginx.default.svc.cluster.local.」ですが、単に「nginx」でも、「nginx.default」でも同じように名前解決できるということです。Pod間で通信する際にはServiceが登録したホスト名を使いますが、このときネームスペース名以降を省いて「nginx」だけで参照するようにしておけば、どのネームスペースにデプロイしても動くようになるので、必要以上にドメインを指定しない使い方がお勧めです。
# cat /etc/resolv.conf nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5
ヘッドレスサービス
Serviceリソースにはheadless serviceと呼ばれる使い方があります。これが何者なのか、DNS的な観点から説明します。
headless serviceとはcluster ipにNoneを指定したサービスであるとされています。つまり、ロードバランサがサービスを公開するアドレスがないということです。
$ kubectl expose deploy nginx --cluster-ip=None $ kubectl get svc nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx ClusterIP None <none> <none> 76s $ kubectl get ep nginx NAME ENDPOINTS AGE nginx 192.168.76.141,192.168.76.142 2m8s
察しのいい方はすでにおわかりかと思いますが、これはロードバランサを構成せず、DNSの設定だけを行うServiceリソースです。つまり、ロードバランサを使わずにDNSラウンドロビンしたいときに使うServiceということです。さきほどは「nginx.default.svc.cluster.local.」のAレコードにCluster IPが設定されていましたが、今度はEndpointのアドレス(=Podのアドレス)が直接Aレコードに設定され、Podが2個あるので2つ登録されていることがわかります。このように同一のドメイン名に複数設定されたAレコードは名前解決するたびに並び順が変わるため、返却された先頭のアドレスを使うことで、一種の負荷分散に利用することができます。
$ dig @10.96.0.10 nginx.default.svc.cluster.local. ;; ANSWER SECTION: nginx.default.svc.cluster.local. 30 IN A 192.168.76.141 nginx.default.svc.cluster.local. 30 IN A 192.168.76.142
どんなときにロードバランサを使わずに、DNSラウンドロビンを使うべきかという話についてはまたの機会に。
StatefulSetリソースとヘッドレスサービス
ヘッドレスサービスについて解説したので、ついでにStatefulSetリソースにも触れておきましょう。StatefulSetリソースを使うには、必ずペアになるヘッドレスサービスが必要です。理由は明快で、StatefulSetが生成するPodは通常それぞれにPersistentVolumeが割り当てられ、異なるデータを格納するからです。格納するデータが異なるということは、すべてのPodで同じプログラムが動いていても、どのPodへリクエストするかで結果が異なるということです。したがって、StatefulSetリソースでデプロイされたPodを利用する場合、ロードバランサあるいはDNSラウンドロビンによって任意のPodへルーティングされるという使い方だけではなく、どのPodへリクエストするのか宛先ホスト名を明示して使うことがあります。そのため、StatefulSetリソースを使うとPodごとに固有のFQDNが払い出され、ネームサーバに登録されるのです。
試しにStatefulSetを作ってみましょう。StatefulSetのくせにPVCを持っていませんが、サンプルなので気にしないように。
$ kubectl apply -f - <<EOF apiVersion: v1 kind: Service metadata: labels: app: nginx name: nginx spec: clusterIP: None selector: app: nginx --- apiVersion: apps/v1 kind: StatefulSet metadata: name: nginx spec: serviceName: "nginx" replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.19 EOF
今までと少し違うのは、普通のSerivceリソースの場合、「<Namespace名>.svc.cluster.local」というドメインにレコードが作成されますが、StatefulSetはService名でサブドメインを作り、そこにAレコードを登録していきます。この例では、FQDNが「<Pod名>.<Service名>.<Namespace名>.svc.cluster.local」になるということです。こうしないとネームスペース名とService名が重複した場合困りますからね。これでPodごとに固有のホスト名をつけることができるようになりました。
StatefulSetはPersistentVolumeを持つステートフルなワークロードを管理するためのものと説明されることが多いのですが、単にPodごとにDNSで名前解決できるホスト名をつけたい場合に使ってもよいでしょう(デフォルトでもありますが実用にはならないので)。
$ dig @10.96.0.10 nginx-0.nginx.default.svc.cluster.local. ;; ANSWER SECTION: nginx-0.nginx.default.svc.cluster.local. 30 IN A 192.168.136.206 $ dig @10.96.0.10 nginx-1.nginx.default.svc.cluster.local. ;; ANSWER SECTION: nginx-1.nginx.default.svc.cluster.local. 30 IN A 192.168.76.144
ちなみに、StatefulSetを使う場合でも普通のheadless serviceと同じようにDNSラウンドロビンされる設定もされています。
$ dig @10.96.0.10 nginx.default.svc.cluster.local. ;; ANSWER SECTION: nginx.default.svc.cluster.local. 30 IN A 192.168.76.144 nginx.default.svc.cluster.local. 30 IN A 192.168.136.206
ラベルセレクタのないサービス
ヘッドレスサービス以外にも、少し変わったServiceの使い方があります。まず、ラベルセレクタのないサービスです。通常Serviceリソースにはラベルセレクタを設定し、そのラベルセレクタにマッチしたPodがEndpointリソースに登録されるという働きになります。このラベルセレクタがないということは、自動的にEndpointリソースが管理されないということです。逆に言えば、Endpointに任意のIPアドレスを登録することができるということです。
これはPodではなくクラスタの外のアドレスに名前をつけたい場合に使うもので、以下のようにラベルセレクタのないサービスを作り、あわせてEndpointサービスを手作業で作ります。ここでは、202.232.2.180というグローバルアドレスを設定しています。
$ kubectl apply -f - <<EOF apiVersion: v1 kind: Service metadata: labels: app: nginx name: nginx spec: clusterIP: None --- apiVersion: v1 kind: Endpoints metadata: labels: app: nginx name: nginx subsets: - addresses: - ip: 202.232.2.180 EOF
これまでと同じように「nginx.default.svc.cluster.local」を名前解決すると、Endpointリソースに設定したグローバルアドレスを取得できることがわかります。この例ではクラスタの外であることを示すためにわかりやすくグローバルアドレスを使いましたが、実際にはクラスタの横で動いている仮想マシンなど、プライベートアドレスだがクラスタの外のアドレスを名前で解決するために利用することが多いと思います。
$ dig @10.96.0.10 nginx.default.svc.cluster.local. ;; ANSWER SECTION: nginx.default.svc.cluster.local. 30 IN A 202.232.2.180
ExternalName Service
まだあります。今度はExternalName Serviceです。これはIPアドレスを管理するEndpointリソースを使わず、代わりにホスト名を設定するというものです。
$ kubectl apply -f - <<EOF apiVersion: v1 kind: Service metadata: labels: app: nginx name: nginx spec: type: ExternalName externalName: www.iij.ad.jp EOF $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx ExternalName <none> www.iij.ad.jp <none> 68s
DNSに詳しい方でしたら、すでに説明は不要だと思いますが、externalNameを指定するとネームサーバにはCNAMEレコードが登録されます。ラベルセレクタがないサービスの例で登場したグローバルアドレスは実は弊社のコーポレートサイトのアドレスなのですが、本当にServiceリソースに登録したければexternalNameを使う方が100倍わかりやすくなりますね(そんなユースケースはないと思いますけれど)。
$ dig @10.96.0.10 nginx.default.svc.cluster.local. ;; ANSWER SECTION: nginx.default.svc.cluster.local. 30 IN CNAME www.iij.ad.jp. www.iij.ad.jp. 30 IN A 202.232.2.180
いろいろ見てきましたが、ServiceがロードバランサとDNSサーバを管理するリソースであるということが理解できたのではないでしょうか。そして、Serviceリソースにはいろいろなオプションがありますが、それはDNSの設定に応じたものであり、それ以上の機能ではありません。
今回はservice type ClusterIP、ExternalNameの話だけでしたが、service type LoadBalancerやExternalIPにはまた違ったややこしくもおもしろいネタがあるので、いずれ取り上げたいと思います。そして、その後にはnew Service APIと呼ばれるGatewayリソースがやってきますので、そのあたりも次の機会に。
関連リンク
IIJの商用サービスのインフラ改善を目指し、IIJ用KubernetesディストリビューションIKE(IIJ Kubernetes Engine)の開発を進めるIIJのSREチーム。
メンバーがウォッチしたKubernetes業界の話題と、大規模サービスインフラに求められるSREの気づきを投稿しています。