KubernetesのDiscovery&LBリソース(その1)
Discovery&LBリソース
連載の第3回目で、Kubernetesのリソースには大きく分けて5つの種類があると解説しました。今回と次回の2回に渡って、そのうちの1つであるDiscovery&LBリソースについてお話します。
リソースの分類 | 内容 |
---|---|
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クラスタに割り当てられた内部用のネットワークセグメントを自動的に分割し、ノードごとのネットワークセグメントに割り当てるため、特別に意識することなく、共通の内部用ネットワークを利用可能です。
この状態でもコンテナ間通信を行うことは可能です。しかし、Serviceを利用することによって得られる大きなメリットが2つあります。
- Pod宛トラフィックのロードバランシング
- サービスディスカバリと内部DNS
これらの機能は、どのService typeからでも利用できます。
Pod宛トラフィックのロードバランシング
Serviceでは、受けたトラフィックを複数のPodにロードバランシングする機能を提供しています。例えば、Deploymentを作成するとPodが複数立ち上がりますが、Podごとに異なるIP Addressが振られているためこのままでは負荷分散させることができません。そこで、Serviceを作成することで、様々な形で複数のPod宛にロードバランシング可能なエンドポイントが提供されます。エンドポイントの種類には、クラスタ内で利用可能なVIP(Virtual IP Address)や、外部のロードバランサが払い出すVIPなど、様々な種類が提供されています。
今回は、連載の4回目で用いたDeploymentが作成するPodを例に、Serviceを作成してみます。まずDeploymentは、下記のように作成します。
apiVersion: apps/v1 kind: Deployment metadata: name: sample-deployment spec: replicas: 3 selector: matchLabels: app: sample-app template: metadata: labels: app: sample-app spec: containers: - name: nginx-container image: nginx:1.12 ports: - containerPort: 80 # Deploymentを作成 kubectl apply -f deployment_sample.yml
おさらいになりますが、このDeploymentが作成するPodには、「app:sample-app」と「pod-template-hash:1788633880」のラベルが付与されています。
$ kubectl get pods sample-deployment-5cddb77dd4-6gpdh -o jsonpath='{.metadata.labels}' map[app:sample-app pod-template-hash:1788633880]%
Serviceでも、このラベルを用いて転送先のPodを選択しています。転送先のPodは「spec.selector」を使って設定します。
apiVersion: v1 kind: Service metadata: name: sample-clusterip spec: type: ClusterIP ports: - name: "http-port" protocol: "TCP" port: 8080 targetPort: 80 selector: app: sample-app # Service(ClusterIP)を作成 kubectl apply -f clusterip_sample.yml
Serviceを作成したあとに詳細情報を確認してみると、Endpointsの部分に複数のIP Address(とPort)が書かれています。これは、Selectorの「app:sample-app」の条件にマッチしたPodのIP AddressとtargetPortになります。
$ kubectl describe svc sample-clusterip Name: sample-clusterip Namespace: default Labels: <none> Annotations: kubectl.kubernetes.io/last-applied-configuration={"api... Selector: app=sample-app Type: ClusterIP IP: 10.11.253.80 Port: http-port 8080/TCP TargetPort: 80/TCP Endpoints: 10.8.0.96:80,10.8.1.32:80,10.8.2.20:80 Session Affinity: None Events: <none>
実際に「app:sample-app」のラベルを持つPodのIP Addressを確認してみると、正しく転送先が設定できていることが確認できます。
$ kubectl get pods -l app=sample-app -o custom-columns="NAME:{metadata.name},IP:{status.podIP}" NAME IP sample-deployment-5cddb77dd4-6gpdh 10.8.1.32 sample-deployment-5cddb77dd4-7znk5 10.8.2.20 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に書き出しています。
$ for PODNAME in `kubectl get pods -l app=sample-app -o jsonpath='{.items[*].metadata.name}'`; do kubectl exec -it ${PODNAME} -- sh -c "hostname > /usr/share/nginx/html/index.html"; done
実際にロードバランシングするエンドポイントには、10.11.253.80が払い出されています。このIP Addressは「type: ClusterIP」を指定したため、クラスタ内でのみ利用可能なVIPとなっています(ClusterIPの詳細については、後ほど説明します)。今回は簡易的に確認しますが、Serviceが払い出したエンドポイントに向けてコンテナからリクエストを送ると、ロードバランシングしていずれかのPodに届いていることが確認できるかと思います。
$ kubectl run --image=centos:7 --restart=Never --rm -i testpod -- curl -s http://10.11.253.80:8080 sample-deployment-5cddb77dd4-6gpdh (複数回実行すると、ほぼ均等に3つのPod名が表示されます) sample-deployment-5cddb77dd4-7znk5 sample-deployment-5cddb77dd4-bnll5
サービスディスカバリと内部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に保つためにも、あまり望ましくありません。
一方で、DNS名を利用することで、Serviceの再作成によるIP Addressの変更も気にする必要はありませんし、設定ファイルへの変更も行う必要はありません。
先ほどのsample-clusteripという名前のService名を使って、上記と同様のHTTPリクエストを送ってみても同様の結果が得られるかと思います。
$ kubectl run --image=centos:7 --restart=Never --rm -i testpod -- curl -s http://sample-clusterip:8080 sample-deployment-5cddb77dd4-6gpdh
これはsample-clusteripの名前解決が行われ、10.11.253.80宛にリクエストが送られるようになっているからです。また、実際にkube-dnsに登録されている正式なFQDNは、[Service名].[Namespace名].svc.[ClusterDomain名]となっています。
$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- dig sample-clusterip.default.svc.cluster.local …(省略)… ;; QUESTION SECTION: ;sample-clusterip.default.svc.cluster.local. IN A ;; ANSWER SECTION: 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だけでも名前解決できるようになっています。
$ kubectl run --image=centos:7 --restart=Never --rm -i testpod -- cat /etc/resolv.conf nameserver 10.11.240.10 search default.svc.cluster.local svc.cluster.local cluster.local c.cyberagent-001.internal google.internal options ndots:5 ...
また、逆引きもできるようになっています。
$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- dig -x 10.11.253.80 …(省略)… ;; QUESTION SECTION: ;80.253.11.10.in-addr.arpa. IN PTR ;; ANSWER SECTION: 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名]
# kubectl run --image=centos:6 --restart=Never --rm -i testpod -- dig _http-port._tcp.sample-clusterip.default.svc.cluster.local SRV …(省略)… ;; QUESTION SECTION: ;_http-port._tcp.sample-clusterip.default.svc.cluster.local. IN SRV ;; ANSWER SECTION: _http-port._tcp.sample-clusterip.default.svc.cluster.local. 30 IN SRV 10 100 8080 sample-clusterip.default.svc.cluster.local. ;; ADDITIONAL SECTION: sample-clusterip.default.svc.cluster.local. 30 IN A 10.11.253.80 …(省略)…
環境変数を利用したサービスディスカバリ
Pod内からは、環境変数でも同じNamespaceのサービスが確認できるようになっています。「-」が含まれるサービス名は「_」に変更された上で大文字に変換されます。「docker --links ...」と同じ形式で環境変数が保存されるため、Docker単体で利用していた環境からの移行時にも利用しやすくなっています。ただし、コンテナ起動後のServiceの追加や削除に伴って環境変数が再度読み込まれるわけではないため、思わぬ事故に繋がる可能性もあります。ServiceよりもPodの方が先にできた場合は環境変数が登録されていないため、Podを再作成してみて下さい。
$ kubectl exec -it sample-deployment-5cddb77dd4-bnll5 env | grep -i sample_clusterip SAMPLE_CLUSTERIP_PORT=tcp://10.11.253.80:8080 SAMPLE_CLUSTERIP_PORT_8080_TCP=tcp://10.11.253.80:8080 SAMPLE_CLUSTERIP_PORT_8080_TCP_ADDR=10.11.253.80 SAMPLE_CLUSTERIP_PORT_8080_TCP_PORT=8080 SAMPLE_CLUSTERIP_PORT_8080_TCP_PROTO=tcp SAMPLE_CLUSTERIP_SERVICE_HOST=10.11.253.80 SAMPLE_CLUSTERIP_SERVICE_PORT=8080 SAMPLE_CLUSTERIP_SERVICE_PORT_HTTP_PORT=8080
複数Portを持つサービスとサービスディスカバリ
上記の例ではPortが一つでしたが、一つのServiceで複数のPortを割り当てることも可能です。例えば、httpとhttpsでClusterIP及び内部DNSレコードが異なると不便なことが多いため、こういったものは一つのServiceで複数のPortを持たせたほうが望ましいでしょう。今回の例ではPod側で443番のPortをListenしていないため、「https-port」の方は疎通性がありません。
apiVersion: v1 kind: Service metadata: name: sample-clusterip spec: type: ClusterIP ports: - name: "http-port" protocol: "TCP" port: 8080 targetPort: 80 - name: "https-port" protocol: "TCP" port: 8443 targetPort: 443 selector: app: sample-app # 複数Portを持つServiceを作成 $ 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を使う場面は、Kubernetes Cluster外からトラフィックを受ける必要がない箇所などで利用されます。デフォルトでは、Kubernetes APIに接続するためのServiceが作成されており、ClsuterIPも発行されています。
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.11.240.1 <none> 443/TCP 31d sample-clusterip ClusterIP 10.11.253.80 <none> 8080/TCP 2h
ClusterIP Serviceの作成
ClusterIP Serviceは、下記のようなYAMLファイルから作成します。
apiVersion: v1 kind: Service metadata: name: sample-clusterip spec: type: ClusterIP ports: - name: "http-port" protocol: "TCP" port: 8080 targetPort: 80 selector: app: sample-app # ClusterIP Serviceを作成 $ kubectl apply -f clusterip_sample.yml
「type: ClusterIP」を指定することで、ClusterIP Serviceを作成することが可能です。spec.ports[x].portにはClusterIPで受け付けるPort番号、spec.ports[x].targetPortは転送先のコンテナのPort番号を指定します。
設定項目 | 内容 |
---|---|
spec.ports[x].port | ClusterIPで受け付けるPort番号 |
spec.ports[x].targetPort | 転送先のコンテナのPort番号 |
先ほどの例で作ったDeploymentを利用して、改めてHTTPリクエストをしてみます。すると、ClusterIP宛のトラフィックが各Podに転送されていることが確認できます。
$ kubectl run --image=centos:7 --restart=Never --rm -i testpod -- curl -s http://sample-clusterip:8080 sample-deployment-5cddb77dd4-6gpdh (複数回実行するとほぼ均等に3つのPod名が表示されます) sample-deployment-5cddb77dd4-7znk5 sample-deployment-5cddb77dd4-bnll5
静的なClusterIP VIPの指定
例えばアプリケーションからDatabaseサーバを指定する場合のように、特定のホストを指定する際には基本的にKubernetes Serviceで登録される内部DNSレコードを用いてホストの指定を行うことが望ましいです。また一方で、ケース指定することも可能です。
ClusterIPを自動付与ではなく手動で指定する場合には、spec.clusterIPを指定します。
apiVersion: v1 kind: Service metadata: name: sample-clusterip spec: type: ClusterIP clusterIP: 10.11.253.80 ports: - name: "http-port" protocol: "TCP" port: 8080 targetPort: 80 selector: app: sample-app ClusterIP Service(手動でIP Addressを指定)の作成 $ kubectl apply -f clusterip_vip_sample.yml
すでにClusterIPのServiceが作成されている場合は、後からClusterIPを変更することはできません。Kubernetesでは「kubectl apply」を使うことで設定値を更新できると説明しましたが、仕様上ClusterIPの値など一部のフィールドは変更できない(immutable)ようになっています。ClusterIPを指定したい場合は、まず削除してから再作成して下さい。
$ kubectl apply -f clusterip_vip_sample.yml 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は、下記のようなYAMLファイルから作成します。
apiVersion: v1 kind: Service metadata: name: sample-externalip spec: type: ClusterIP externalIPs: - 10.240.0.7 - 10.240.0.8 ports: - name: "http-port" protocol: "TCP" port: 8080 targetPort: 80 selector: app: sample-app # ExternalIP Serviceの作成 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の分でなくても問題ありません。
設定項目 | 内容 |
---|---|
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として利用することが可能です。
$ kubectl get node -o custom-columns="NAME:{metadata.name},IP:{status.addresses[].address}" NAME IP gke-k8s-default-pool-9c2aa160-9f6b 10.240.0.8 gke-k8s-default-pool-9c2aa160-d2pl 10.240.0.7 gke-k8s-default-pool-9c2aa160-v5v4 10.240.0.9
Serviceを確認してみると、新しくサービスが作成されています。ExternalIPサービスを作成しましたが、コンテナ内からの通信はClusterIPを利用するために、ClusterIPも自動的に確保されます。
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.11.240.1 <none> 443/TCP 31d sample-clusterip ClusterIP 10.11.253.80 <none> 8080/TCP 6h sample-externalip ClusterIP 10.11.247.109 10.240.0.7,10.240.0.8 8080/TCP 3m
ExternalIPを割り当てましたが、コンテナ内から確認すると内部DNSが返すIP Addressは、ExternalIPではなくClusterIPとなります。
$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- dig sample-externalip.default.svc.cluster.local …(省略)… ;; QUESTION SECTION: ;sample-externalip.default.svc.cluster.local. IN A ;; ANSWER SECTION: sample-externalip.default.svc.cluster.local. 30 IN A 10.11.247.109 …(省略)…
また、ExternalIPを利用しているKubernetes Node上でPortの状態を確認すると、8080番ポートでListenしている状態になっています。今回の場合には2ノードが該当します。
gke-k8s-default-pool-9c2aa160-9f6b ~ # ss -napt | grep 8080 LISTEN 0 128 10.240.0.8:8080 *:* users:(("kube-proxy",pid=1781,fd=9)) gke-k8s-default-pool-9c2aa160-d2pl ~ # ss -napt | grep 8080 LISTEN 0 128 10.240.0.7:8080 *:* users:(("kube-proxy",pid=1771,fd=9)) gke-k8s-default-pool-9c2aa160-v5v4 ~ # ss -napt | grep 8080
そのため、ExternalIPを利用している場合、Kubernetesクラスタ外からも疎通が可能です。また、Podへのリクエストも分散されます。下記のコマンドはGCEのネットワーク内で試して下さい。
# 10.240.0.7,10.240.0.8 は疎通可能、10.240.0.9は疎通不可 $ curl -s http://10.240.0.8:8080 sample-deployment-5cddb77dd4-6gpdh (複数回実行するとほぼ均等に3つのPod名が表示されます) sample-deployment-5cddb77dd4-7znk5 sample-deployment-5cddb77dd4-bnll5
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- KubernetesのDiscovery&LBリソース(その2)
- Kubernetes上のコンテナをIngressでインターネットに公開するまで
- Oracle Cloud Hangout Cafe Season7 #1「Kubnernetes 超入門」(2023年6月7日開催)
- kustomizeやSecretを利用してJavaアプリケーションをデプロイする
- KubernetesのWorkloadsリソース(その1)
- Kubernetes上のアプリケーション開発を加速させるツール(2) Telepresence
- KubernetesのマニフェストをMagnumで実行する
- NGINX Ingress Controllerの柔軟なアプリケーション制御、具体的なユースケースと設定方法を理解する
- Kubernetesの基礎
- kustomizeで復数環境のマニフェストファイルを簡単整理