Discovery&LBリソース
連載の第3回目で、Kubernetesのリソースには大きく分けて5つの種類があると解説しました。今回と次回の2回に渡って、そのうちの1つであるDiscovery&LBリソースについてお話します。
5種類に大別できるKubernetesのリソース
リソースの分類 | 内容 |
Workloadsリソース | コンテナの実行に関するリソース |
Discovery&LBリソース | コンテナを外部公開するようなエンドポイントを提供するリソース |
Config&Storageリソース | 設定・機密情報・永続化ボリュームなどに関するリソース |
Clusterリソース | セキュリティやクォータなどに関するリソース |
Metadataリソース | リソースを操作する系統のリソース |
Discovery&LBリソースは、クラスタ上のコンテナに対するエンドポイントの提供や、ラベルに一致するコンテナのディスカバリに利用されるリソースです。内部的に利用されているものを除いて利用者が直接利用するものとしては、全部で2種類のDiscovery&LBリソースが存在します。そのうちのServiceリソースに関しては、Endpointの提供方法が異なるtypeがいくつかあります。
- Service
- ClusterIP
- NodePort
- LoadBalancer
- ExternalIP
- ExternalName
- Headless(None)
- Ingress
クラスタ内ネットワークとService
Kubernetesでクラスタを構築すると、Podのための内部用ネットワークが構成されます。この内部用のネットワークの構成は、CNI(Common Network Interface)と呼ばれるpluggableなモジュールごとに異なりますが、基本的にはノードごとに異なるネットワークセグメントを持ち、ノード間のトラフィックはVXLANやL2 Routingなどで転送されます。Kubernetesクラスタに割り当てられた内部用のネットワークセグメントを自動的に分割し、ノードごとのネットワークセグメントに割り当てるため、特別に意識することなく、共通の内部用ネットワークを利用可能です。
クラスタ内に構築されるPod Network
この状態でもコンテナ間通信を行うことは可能です。しかし、Serviceを利用することによって得られる大きなメリットが2つあります。
- Pod宛トラフィックのロードバランシング
- サービスディスカバリと内部DNS
これらの機能は、どのService typeからでも利用できます。
Pod宛トラフィックのロードバランシング
Serviceでは、受けたトラフィックを複数のPodにロードバランシングする機能を提供しています。例えば、Deploymentを作成するとPodが複数立ち上がりますが、Podごとに異なるIP Addressが振られているためこのままでは負荷分散させることができません。そこで、Serviceを作成することで、様々な形で複数のPod宛にロードバランシング可能なエンドポイントが提供されます。エンドポイントの種類には、クラスタ内で利用可能なVIP(Virtual IP Address)や、外部のロードバランサが払い出すVIPなど、様々な種類が提供されています。
Pod宛トラフィックのロードバランシングとエンドポイントの提供
今回は、連載の4回目で用いたDeploymentが作成するPodを例に、Serviceを作成してみます。まずDeploymentは、下記のように作成します。
リスト1:サンプルのDeploymentを作成するdeployment_sample.yml
04 | name: sample-deployment |
16 | - name: nginx-container |
22 | kubectl apply -f deployment_sample.yml |
おさらいになりますが、このDeploymentが作成するPodには、「app:sample-app」と「pod-template-hash:1788633880」のラベルが付与されています。
リスト2:アウトプット時に特定のKeyを指定して出力
1 | $ kubectl get pods sample-deployment-5cddb77dd4-6gpdh -o jsonpath='{.metadata.labels}' |
2 | map[app:sample-app pod-template-hash:1788633880]% |
Serviceでも、このラベルを用いて転送先のPodを選択しています。転送先のPodは「spec.selector」を使って設定します。
リスト3:サンプルのService(ClusterIP)を作成するclusterip_sample.yml
04 | name: sample-clusterip |
15 | # Service(ClusterIP)を作成 |
16 | kubectl apply -f clusterip_sample.yml |
ServiceのselectorとPodの関係
Serviceを作成したあとに詳細情報を確認してみると、Endpointsの部分に複数のIP Address(とPort)が書かれています。これは、Selectorの「app:sample-app」の条件にマッチしたPodのIP AddressとtargetPortになります。
リスト4:Serviceの詳細情報を確認
01 | $ kubectl describe svc sample-clusterip |
05 | Annotations: kubectl.kubernetes.io/last-applied-configuration={"api... |
06 | Selector: app=sample-app |
09 | Port: http-port 8080/TCP |
11 | Endpoints: 10.8.0.96:80,10.8.1.32:80,10.8.2.20:80 |
実際に「app:sample-app」のラベルを持つPodのIP Addressを確認してみると、正しく転送先が設定できていることが確認できます。
リスト5:特定のJSONPathをカラムにして出力
1 | $ kubectl get pods -l app=sample-app -o custom-columns="NAME:{metadata.name},IP:{status.podIP}" |
3 | sample-deployment-5cddb77dd4-6gpdh 10.8.1.32 |
4 | sample-deployment-5cddb77dd4-7znk5 10.8.2.20 |
5 | sample-deployment-5cddb77dd4-bnll5 10.8.0.96 |
今回はロードバランシングの確認をしやすくするために、各Pod上のindex.htmlを書き換えておきます。Shell芸で書くと、下記のような感じになるかと思います。まず「kubectl get」で対象となるラベルが割り当てられたPodの名前だけを取得します。それをforループで「kubectl exec」することでPod内で「hostname」の実行結果を/usr/share/nginx/html/index.htmlに書き出しています。
リスト6:Pod名を取得し、全Podで"hostname > /usr/share/nginx/html/index.html"を実行
1 | $ for PODNAME in `kubectl get pods -l app=sample-app -o jsonpath='{.items[*].metadata.name}'`; do |
2 | kubectl exec -it ${PODNAME} -- sh -c "hostname > /usr/share/nginx/html/index.html"; |
実際にロードバランシングするエンドポイントには、10.11.253.80が払い出されています。このIP Addressは「type: ClusterIP」を指定したため、クラスタ内でのみ利用可能なVIPとなっています(ClusterIPの詳細については、後ほど説明します)。今回は簡易的に確認しますが、Serviceが払い出したエンドポイントに向けてコンテナからリクエストを送ると、ロードバランシングしていずれかのPodに届いていることが確認できるかと思います。
リスト7:一時的にPodを立ち上げてServiceのエンドポイントに向けてリクエスト
2 | sample-deployment-5cddb77dd4-6gpdh |
3 | (複数回実行すると、ほぼ均等に3つのPod名が表示されます) |
4 | sample-deployment-5cddb77dd4-7znk5 |
5 | sample-deployment-5cddb77dd4-bnll5 |
EndpointとPodへの転送
サービスディスカバリと内部DNS
Kubernetesでは、サービスディスカバリの機能をServiceが提供しています。サービスディスカバリとは、端的に言うと特定の条件の対象となるメンバを列挙したり、名前からエンドポイントを判別する機能です。Kubernetesの文脈では、Serviceに属するPodを列挙したり、Serviceの名前からエンドポイントの情報を返すことを指します。サービスディスカバリの方法は、主に次の3種類が用意されています。
- Aレコードを利用したサービスディスカバリ
- SRVレコードを利用したサービスディスカバリ
- 環境変数を利用したサービスディスカバリ
Aレコードを利用したサービスディスカバリ
Serviceを作成すると、DNSレコードが自動的に登録されます。内部的にはkube-dnsと呼ばれるKubernetesのシステムコンポーネントが動いており、エンドポイントに対してDNSレコードを生成します。この名前解決ができない場合、Service作成時に自動で払い出されるエンドポイントのIP Addressを確認してから、アプリケーションなどの設定ファイルに埋め込む必要があり、非常に厄介です。_Kubernetesのポータビリティ性を保つために、IP Addressは極力Service名を使って名前解決するようにしましょう。_
もしServiceで払い出されるIP Addressを利用する場合には、IP Addressを特定しなければなりません。特定するには、明示的に指定するか、Serviceを作成した後に払い出されるIP Addressを設定ファイルなどに書き込む必要があります。Kubernetesではアドレスの管理から解放されるために、基本的にはServiceで払い出されるIP Addressは自動で払い出されるものを利用し、IP Addressを明示的に指定するのは望ましくありません。設定ファイルの変更もコンテナをImmutableに保つためにも、あまり望ましくありません。
IPアドレスで指定した場合
一方で、DNS名を利用することで、Serviceの再作成によるIP Addressの変更も気にする必要はありませんし、設定ファイルへの変更も行う必要はありません。
DNS名で指定した場合
先ほどのsample-clusteripという名前のService名を使って、上記と同様のHTTPリクエストを送ってみても同様の結果が得られるかと思います。
リスト8:コンテナ内からsample-clusterip:8080宛にHTTPリクエスト
2 | sample-deployment-5cddb77dd4-6gpdh |
これはsample-clusteripの名前解決が行われ、10.11.253.80宛にリクエストが送られるようになっているからです。また、実際にkube-dnsに登録されている正式なFQDNは、[Service名].[Namespace名].svc.[ClusterDomain名]となっています。
リスト9:コンテナ内からsample-clusterip.default.svc.cluster.localの名前解決を試みる
01 | $ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- dig sample-clusterip.default.svc.cluster.local |
05 | ;sample-clusterip.default.svc.cluster.local. IN A |
08 | sample-clusterip.default.svc.cluster.local. 30 IN A 10.11.253.80 |
このFQDNにはServiceが衝突しないようにNamespaceなどが含まれていますが、コンテナ内の/etc/resolv.confに下記のような記述(searchから始まる行)があるため、sample-clusterip.defaultやsample-clusteripだけでも名前解決できるようになっています。
リスト10:コンテナ内の/etc/resolv.conf
1 | $ kubectl run --image=centos:7 --restart=Never --rm -i testpod -- cat /etc/resolv.conf |
3 | search default.svc.cluster.local svc.cluster.local cluster.local c.cyberagent-001.internal google.internal |
また、逆引きもできるようになっています。
リスト11:コンテナ内から10.11.253.80の名前解決を試みる
01 | $ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- dig -x 10.11.253.80 |
06 | ;80.253.11.10.in-addr.arpa. IN PTR |
09 | 80.253.11.10.in-addr.arpa. 30 IN PTR sample-clusterip.default.svc.cluster.local. |
SRVレコードを利用したサービスディスカバリ
正引きと逆引きでDNS Recordが引けることは確認しましたが、SRVレコードを使ってServiceのエンドポイントも確認できます。レコードの形式は下記のとおりです。ServiceのPort名とProtocolにはPrefixに_が入ることに注意して下さい。
[_ServiceのPort名].[_Protocol].[Service名].[Namespace名].svc.[ClusterDomain名]
リスト12:SRVレコードを使った場合
01 | # kubectl run --image=centos:6 --restart=Never --rm -i testpod -- dig _http-port._tcp.sample-clusterip.default.svc.cluster.local SRV |
05 | ;_http-port._tcp.sample-clusterip.default.svc.cluster.local. IN SRV |
08 | _http-port._tcp.sample-clusterip.default.svc.cluster.local. 30 IN SRV 10 100 8080 sample-clusterip.default.svc.cluster.local. |
12 | sample-clusterip.default.svc.cluster.local. 30 IN A 10.11.253.80 |
環境変数を利用したサービスディスカバリ
Pod内からは、環境変数でも同じNamespaceのサービスが確認できるようになっています。「-」が含まれるサービス名は「_」に変更された上で大文字に変換されます。「docker --links ...」と同じ形式で環境変数が保存されるため、Docker単体で利用していた環境からの移行時にも利用しやすくなっています。ただし、コンテナ起動後のServiceの追加や削除に伴って環境変数が再度読み込まれるわけではないため、思わぬ事故に繋がる可能性もあります。ServiceよりもPodの方が先にできた場合は環境変数が登録されていないため、Podを再作成してみて下さい。
リスト13:サービス名などは環境変数でも参照できる
1 | $ kubectl exec -it sample-deployment-5cddb77dd4-bnll5 env | grep -i sample_clusterip |
4 | SAMPLE_CLUSTERIP_PORT_8080_TCP_ADDR=10.11.253.80 |
5 | SAMPLE_CLUSTERIP_PORT_8080_TCP_PORT=8080 |
6 | SAMPLE_CLUSTERIP_PORT_8080_TCP_PROTO=tcp |
7 | SAMPLE_CLUSTERIP_SERVICE_HOST=10.11.253.80 |
8 | SAMPLE_CLUSTERIP_SERVICE_PORT=8080 |
9 | SAMPLE_CLUSTERIP_SERVICE_PORT_HTTP_PORT=8080 |
複数Portを持つサービスとサービスディスカバリ
上記の例ではPortが一つでしたが、一つのServiceで複数のPortを割り当てることも可能です。例えば、httpとhttpsでClusterIP及び内部DNSレコードが異なると不便なことが多いため、こういったものは一つのServiceで複数のPortを持たせたほうが望ましいでしょう。今回の例ではPod側で443番のPortをListenしていないため、「https-port」の方は疎通性がありません。
リスト14:複数Portを持つService(ClusterIP)を作成するclusterip_multi_sample.yml
04 | name: sample-clusterip |
20 | $ kubectl apply -f clusterip_multi_sample.yml |
以下、それぞれのtypeについて解説していきます。
ClusterIP
Kubernetesの最も基本となるtype: ClusterIPのServiceリソースです。「type: ClusterIP」のServiceを作成すると、Kubernetesクラスタ内からでないと疎通性のないInternal Networkに作り出されるVIPが割り当てられます。そのため、ClusterIPと呼ばれています。ClusterIP宛の通信は各Node上で動いているシステムコンポーネントのkube-proxyがPod向けに転送を行います(実装はProxy-modeによって異なります)。
ClusterIP Service
そのためClusterIPを使う場面は、Kubernetes Cluster外からトラフィックを受ける必要がない箇所などで利用されます。デフォルトでは、Kubernetes APIに接続するためのServiceが作成されており、ClsuterIPも発行されています。
リスト15:Kubernetes APIに接続するためのService
2 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE |
3 | kubernetes ClusterIP 10.11.240.1 <none> 443/TCP 31d |
4 | sample-clusterip ClusterIP 10.11.253.80 <none> 8080/TCP 2h |
ClusterIP Serviceの作成
ClusterIP Serviceは、下記のようなYAMLファイルから作成します。
リスト16:ClusterIP Serviceを作成するclusterip_sample.yml
04 | name: sample-clusterip |
16 | $ kubectl apply -f clusterip_sample.yml |
「type: ClusterIP」を指定することで、ClusterIP Serviceを作成することが可能です。spec.ports[x].portにはClusterIPで受け付けるPort番号、spec.ports[x].targetPortは転送先のコンテナのPort番号を指定します。
ClusterIPの設定項目
設定項目 | 内容 |
spec.ports[x].port | ClusterIPで受け付けるPort番号 |
spec.ports[x].targetPort | 転送先のコンテナのPort番号 |
先ほどの例で作ったDeploymentを利用して、改めてHTTPリクエストをしてみます。すると、ClusterIP宛のトラフィックが各Podに転送されていることが確認できます。
リスト17:ClusterIP宛のトラフィックの転送先
2 | sample-deployment-5cddb77dd4-6gpdh |
3 | (複数回実行するとほぼ均等に3つのPod名が表示されます) |
4 | sample-deployment-5cddb77dd4-7znk5 |
5 | sample-deployment-5cddb77dd4-bnll5 |
静的なClusterIP VIPの指定
例えばアプリケーションからDatabaseサーバを指定する場合のように、特定のホストを指定する際には基本的にKubernetes Serviceで登録される内部DNSレコードを用いてホストの指定を行うことが望ましいです。また一方で、ケース指定することも可能です。
ClusterIPを自動付与ではなく手動で指定する場合には、spec.clusterIPを指定します。
リスト18:手動でClusterIPを指定するサンプルを作成するclusterip_vip_sample.yml
04 | name: sample-clusterip |
07 | clusterIP: 10.11.253.80 |
16 | ClusterIP Service(手動でIP Addressを指定)の作成 |
17 | $ kubectl apply -f clusterip_vip_sample.yml |
すでにClusterIPのServiceが作成されている場合は、後からClusterIPを変更することはできません。Kubernetesでは「kubectl apply」を使うことで設定値を更新できると説明しましたが、仕様上ClusterIPの値など一部のフィールドは変更できない(immutable)ようになっています。ClusterIPを指定したい場合は、まず削除してから再作成して下さい。
リスト19:ClusterIPのは変更できない(immutable)
1 | $ kubectl apply -f clusterip_vip_sample.yml |
2 | The Service "sample-clusterip" is invalid: spec.clusterIP: Invalid value: "10.11.240.11": field is immutable |
ExternalIP
ExternalIPでは、特定のKubernetes NodeのIP:Portで受けたトラフィックをコンテナに転送する形で外部疎通性を確立します。
ExternalIP Service
ExternalIP Serviceの作成
ExternalIP Serviceは、下記のようなYAMLファイルから作成します。
リスト20:ExternalIP Serviceを作成するexternalip_sample.yml
04 | name: sample-externalip |
18 | # ExternalIP Serviceの作成 |
19 | kubectl apply -f externalip_sample.yml |
ExternalIPは「type: ExternalIP」を指定するわけではありません(type自体はClusterIPです)。spec.ports[x].portにはKubernetes NodeのIP(10.240.0.7など)とClusterIPで受け付けるPort番号、spec.ports[x].targetPortは転送先のコンテナのPort番号を指定します。また、ExternalIPに利用するIPは、全てのKubernetes nodeの分でなくても問題ありません。
ExternalIPの設定項目
設定項目 | 内容 |
spec.ports[x].port | ClusterIPとKubernetes NodeのIP Address(ExternalIP)で 受け付けるPort番号 |
spec.ports[x].targetPort | 転送先のコンテナのPort番号 |
ExternalIPに利用可能なIP AddressはNodeの情報から確認することが可能です。GKEの場合にはKubernetes NodeとなるGCEインスタンスには、OS上から見たときに見えるIP AddressはInternal用(10.240.0.7など)のものだけになります。GCE環境上の35.xxx.xxx.xxxのようなグローバルアドレスは、OS上からは認識されていない状態で到達するように、GCPのNetworkが作られているため、ExternalIPには利用できない点に注意して下さい。その他の環境でNodeに直接グローバルアドレスが割り振られている場合には、ExternalIPとして利用することが可能です。
リスト21:IP Addressの確認
1 | $ kubectl get node -o custom-columns="NAME:{metadata.name},IP:{status.addresses[].address}" |
3 | gke-k8s-default-pool-9c2aa160-9f6b 10.240.0.8 |
4 | gke-k8s-default-pool-9c2aa160-d2pl 10.240.0.7 |
5 | gke-k8s-default-pool-9c2aa160-v5v4 10.240.0.9 |
Serviceを確認してみると、新しくサービスが作成されています。ExternalIPサービスを作成しましたが、コンテナ内からの通信はClusterIPを利用するために、ClusterIPも自動的に確保されます。
リスト22:ExternalIPサービスにもClusterIPが割り当てられている
2 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE |
3 | kubernetes ClusterIP 10.11.240.1 <none> 443/TCP 31d |
4 | sample-clusterip ClusterIP 10.11.253.80 <none> 8080/TCP 6h |
5 | sample-externalip ClusterIP 10.11.247.109 10.240.0.7,10.240.0.8 8080/TCP 3m |
ExternalIPを割り当てましたが、コンテナ内から確認すると内部DNSが返すIP Addressは、ExternalIPではなくClusterIPとなります。
リスト23:コンテナ内からDNSでExternalIPサービスを指定する
01 | $ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- dig sample-externalip.default.svc.cluster.local |
05 | ;sample-externalip.default.svc.cluster.local. IN A |
08 | sample-externalip.default.svc.cluster.local. 30 IN A 10.11.247.109 |
また、ExternalIPを利用しているKubernetes Node上でPortの状態を確認すると、8080番ポートでListenしている状態になっています。今回の場合には2ノードが該当します。
リスト24:ExternalIPを利用しているノードのポートの状態
1 | gke-k8s-default-pool-9c2aa160-9f6b ~ # ss -napt | grep 8080 |
2 | LISTEN 0 128 10.240.0.8:8080 *:* users:(("kube-proxy",pid=1781,fd=9)) |
4 | gke-k8s-default-pool-9c2aa160-d2pl ~ # ss -napt | grep 8080 |
5 | LISTEN 0 128 10.240.0.7:8080 *:* users:(("kube-proxy",pid=1771,fd=9)) |
7 | gke-k8s-default-pool-9c2aa160-v5v4 ~ # ss -napt | grep 8080 |
そのため、ExternalIPを利用している場合、Kubernetesクラスタ外からも疎通が可能です。また、Podへのリクエストも分散されます。下記のコマンドはGCEのネットワーク内で試して下さい。
リスト25:Podへのリクエストは分散される(GCEで有効)
1 | # 10.240.0.7,10.240.0.8 は疎通可能、10.240.0.9は疎通不可 |
3 | sample-deployment-5cddb77dd4-6gpdh |
4 | (複数回実行するとほぼ均等に3つのPod名が表示されます) |
5 | sample-deployment-5cddb77dd4-7znk5 |
6 | sample-deployment-5cddb77dd4-bnll5 |