この連載が書籍になりました!『Kubernetes完全ガイド

KubernetesのDiscovery&LBリソース(その1)

2018年4月11日(水)
青山 真也
今回と次回の2回に渡って、5種類に大別されるKubernetesのリソースのうち、Discover&LBリソースを解説する。

NodePort

前述のExternalIPは、指定したKubernetes NodeのIP:Portで受けたトラフィックをコンテナに転送する形で、外部疎通性を確立していました。それに対してNodePortは、全てのKubernetes NodeのIP:Portで受けたトラフィックをコンテナに転送する形で、外部疎通性を確立します。端的に言うのであれば、ExternalIP Serviceの全ノード版に近いと言えます。

Docker Swarmを使ったことがある方であれば、「ServiceをExposeした場合と同じような挙動となる」とイメージしてください。

NodePort Service

NodePort Service

NodePort Serviceの作成

NodePort Serviceは、下記のようなYAMLファイルから作成します。

リスト26:NodePort Serviceを作成するnodeport_sample.yml

apiVersion: v1
kind: Service
metadata:
  name: sample-nodeport
spec:
  type: NodePort
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30080
  selector:
    app: sample-app

# NodePort Serviceの作成
kubectl apply -f nodeport_sample.yml

spec.ports[x].portにはClusterIPで受け付けるPort番号、spec.ports[x].targetPortは転送先のコンテナのPort番号、spec.ports[x].nodePortには 全Kubernetes Nodeで受け付けるPort番号を指定します。全Kubernetes NodeのIPでspec.ports[x].nodePortで指定したポートをListenするため、Portのバッティングを起こさないように注意する必要があります。もし、割り当てられるPort番号を指定する必要がないのであれば、spec.ports[x].nodePortを明示的に指定しないことで空いている番号が自動でバインドされます。

NodePortの設定項目

設定項目内容
spec.ports[x].portClusterIPで受け付けるPort番号
spec.ports[x].targetPort転送先のコンテナのPort番号
spec.ports[x].nodePort全Kubernetes NodeのIP Adressで
受け付けるPort番号

また、このNodePortで利用できるポート範囲は、GKEでは30000~32767(Kubernetesのデフォルト値)となっており、範囲外の値を設定しようとするとエラーになります(Kubernetes Masterの設定を自分で変えられる場合には、この範囲をカスタマイズすることも可能です)。

リスト27:範囲外の値をPort番号に設定しようとするとエラーになる

$ sed -e 's|nodePort: 30080|nodePort: 8888|' nodeport_sample.yml | kubectl apply -f -
The Service "sample-nodeport" is invalid: spec.ports[0].nodePort: Invalid value: 8888: provided port is not in the valid range. The range of valid ports is 30000-32767

また、同じPort番号を複数のNodePortで利用することもできません。

リスト28:NodePort間でPort番号の重複はできない

The Service "sample-nodeport-fail" is invalid: spec.ports[0].nodePort: Invalid value: 30080: provided port is already allocated
NodePortの衝突

NodePortの衝突

Serviceを確認してみると、新しくServiceが作成されています。NodePort Serviceを作成しましたが、コンテナ内からの通信はClusterIPを利用するために、ClusterIPも自動的に確保されています。

リスト29:NodePortサービスにもCluterIPが割り当てられている

$ 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         16m
sample-nodeport     NodePort    10.11.247.192   <none>                  8080:30080/TCP   3m

コンテナ内から確認すると、内部DNSが返すIP AddressはExternalIPではなくClusterIPとなります。

リスト30:コンテナ内からDNSでNodePortサービスを指定する

$ kubectl run --image=centos:6 --restart=Never --rm  -i testpod -- dig sample-nodeport.default.svc.cluster.local
…(省略)…

;; QUESTION SECTION:
;sample-nodeport.default.svc.cluster.local. IN        A

;; ANSWER SECTION:
sample-nodeport.default.svc.cluster.local. 30 IN A 10.11.247.192

…(省略)…

また、NodePortを利用しているKubernetes Node上でPortの状態を確認すると、全ノードの30080番ポートでListenしている状態になっています。

リスト31:NodePortを利用しているノードのPortを確認

gke-k8s-default-pool-9c2aa160-9f6b ~ # ss -napt | grep 30080
LISTEN     0      128         :::30080                   :::*                   users:(("kube-proxy",pid=1781,fd=10))

gke-k8s-default-pool-9c2aa160-d2pl ~ # ss -napt | grep 30080
LISTEN     0      128         :::30080                   :::*                   users:(("kube-proxy",pid=1771,fd=10))

gke-k8s-default-pool-9c2aa160-v5v4 ~ # ss -napt | grep 30080
LISTEN     0      128         :::30080                   :::*                   users:(("kube-proxy",pid=1790,fd=9))

そのため、ExternalIPとは異なり、全NodeのIP AddressでKubernetesクラスタ外からも疎通が可能です。また、Podへのリクエストも分散されます。GKEの場合には、GCEに割り当てられる35.xxx.xxx.xxxのようなグローバルIPアドレスでも疎通することが可能です。疎通性がない場合には、GCEのファイアウォールのルールを確認して下さい。

リスト32:Podへのリクエストは分散される

$ curl -s http://35.xxx.xxx.xxx:30080
sample-deployment-5cddb77dd4-6gpdh
(複数回実行するとほぼ均等に3つのPod名が表示されます)
sample-deployment-5cddb77dd4-7znk5
sample-deployment-5cddb77dd4-bnll5

ノード間通信の排除(ノードをまたいだロードバランシングの排除)

NodePortではノード上のNodePortに到達したパケットは、さらにノードをまたいだPodへもロードバランシングされる形となっています。

2段階ロードバランシング

2段階ロードバランシング

リスト33:Podの状態を確認

$ kubectl get pods -o custom-columns="NAME:{metadata.name},Node:{spec.nodeName},NodeIP:{status.hostIP}"
NAME                                 Node                                 NodeIP
sample-deployment-5cddb77dd4-6gpdh   gke-k8s-default-pool-9c2aa160-9f6b   10.240.0.8
sample-deployment-5cddb77dd4-7znk5   gke-k8s-default-pool-9c2aa160-v5v4   10.240.0.9
sample-deployment-5cddb77dd4-bnll5   gke-k8s-default-pool-9c2aa160-d2pl   10.240.0.7

例えば上記の例ではgke-k8s-default-pool-9c2aa160-9f6b(10.240.0.8)のホスト上にはsample-deployment-5cddb77dd4-6gpdhのPodがいますが、10.240.0.8 へのリクエストはノードをまたいでロードバランシングされ、3 つのPod全てに到達します。一方でDaemonSetなどでは、1ノードにつき1Podが確約されているため、NodePortサービスで同じNode上のPodにだけ通信してほしいなどの要望があります。そういった場合にはspec.externalTrafficPolicyを使うことで実現可能です。

  • Cluster(Default)
    • ノード到達後にノードをまたいでロードバランシングを行い、Podへの負荷を均等にします
  • Local
    • ノード到達後にノードをまたいでロードバランシングを行いません
    • そのノードに該当するラベルのPodが存在しない場合、
      レスポンスを返すことができないため注意して下さい

spec.externalTrafficPolicyがClusterの場合には、10.240.0.8:30080宛のリクエストは、3つのPodにほぼ均等に転送されます(ただし、実際にはkube-proxyの設定でiptablesのproxy modeを利用している場合には、実際には少し自分のノードに多くなるように転送されているようです。確認してみたい方は、iptables-saveなどでstatisticsの部分を確認してみて下さい)。

externalTrafficPolicy Clusterの挙動

externalTrafficPolicy Clusterの挙動

spec.externalTrafficPolicyがLocalの場合には、10.240.0.8:30080宛のリクエストはsample-deployment-5cddb77dd4-6gpdhのPodにのみ転送されます。もし、同じノード上に2つ以上のPodが存在する場合には、2つのPodへは均等に割り振られます。また、そのノード上にPodがない場合には、リクエストに応答することはできません。

externalTrafficPolicy Localの挙動

externalTrafficPolicy Localの挙動

例えば、externalTrafficPolicyをデフォルトのClusterからLocalにするには、下記のようなYAMLファイルを利用します。

リスト34:externalTrafficPolcyをLocalにするnodeport_local_sample.yml

apiVersion: v1
kind: Service
metadata:
  name: sample-nodeport-local
spec:
  type: NodePort
  externalTrafficPolicy: Local
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30081
  selector:
    app: sample-app

# NodePort Serviceの作成
kubectl apply -f nodeport_local_sample.yml

先ほどと同様に新しく作成したNodePortにリクエストを送ると、今度は1つのPodだけからレスポンスが返ってきます。

リスト35:この設定では1つのPodだけからレスポンスが返る

$ curl -s http://10.240.0.8:30081
sample-deployment-5cddb77dd4-6gpdh
(複数回実行しても、同一ノード上のPodだけからレスポンスが返ってきます)

また、externalTrafficPolicyはtype: NodePortだけではなく、後述するtype: LoadBalancerでも利用可能です。ただし、ClusterIPなどでは利用することはできません。

LoadBalancer

最も使い勝手がよく、一番実用的なServiceリソースです。

LoadBalancerでは、Kubernetesクラスタ外のLoadBalancerに外部疎通性のあるVIPを払い出してもらうことが可能です。NodePortなどでは、結局のところNodeに割り当てられたIP Addressがエンドポイントを担う形になってしまうため、SPoF(Single Point of Failure)となってしまいました。しかし、type: LoadBalancerでは外部のロードバランサを利用することによって、Kubernetes Nodeの障害に強いという特徴があります。

一方で、外部のLoadBalancerと連携できるのは、GCP、AWS、Azure、OpenStackを始めとしたCloudProviderのみとなってしまうため、利用環境が限られるといった点もあります(最近ではMetalLBなども出てきたため、利用できる方法が増えてきてはいます)。

イメージ的にはNodePort Serviceを作り、クラスタ外のLoadBalancerからKubernetes Node宛にバランシングするような形です。

LoadBalancer Service

LoadBalancer Service

LoadBalancer Serviceの作成

LoadBalancer Serviceは、下記のようなYAMLファイルから作成します。

リスト36:サンプルのLoadBalancer Serviceを作成するlb_sample.yml

apiVersion: v1
kind: Service
metadata:
  name: sample-lb
spec:
  type: LoadBalancer
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30082
  selector:
    app: sample-app

# LoadBalancer Serviceを作成
kubectl apply -f lb_sample.yml

spec.ports[x].portにはLoadBalancerが払い出すVIPとClusterIPで受け付けるPort番号、spec.ports[x].targetPortは転送先のコンテナのPort番号を指定します。また「type: LoadBalancer」利用時には、NodePortも自動的に割り振られるため、spec.ports[x].nodePortの指定も可能です。

LoadBalancerの設定項目

設定項目内容
spec.ports[x].portLoadBalancerが払い出すVIPとClusterIPで受け付けるPort番号
spec.ports[x].targetPort転送先のコンテナのPort番号
spec.ports[x].nodePort全Kubernetes NodeのIP Adressで受け付けるPort番号

Serviceを確認してみると、新しくサービスが作成されているのがわかります。しかし、EXTERNAL-IPの部分が<pending>になったままとなっています。これは、GCPのLoadBalancerを作成する処理が裏で行われている段階で、未割り当ての状態だからです。「type: LoadBalancer」の実装にもよりますが、多くの実装で数秒~数十秒程度待つケースが多いようです。

リスト37:EXTERNAL-IPは未割り当て

$ kubectl get svc sample-lb
NAME                    TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)          AGE
sample-lb               LoadBalancer   10.11.251.185   <pending>       8080:30082/TCP   3s

LoadBalancerサービスを作成しましたが、コンテナ内からの通信はClusterIPを利用するために、ClusterIPも自動的に確保されます。また、NodePort(:30082)も作成されています。

リスト38:LoadBalancerサービスにもCluterIPが割り当てられている

$ 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         56m
sample-lb               LoadBalancer   10.11.251.185   35.yyy.yyy.yyy          8080:30082/TCP   3s
sample-nodeport         NodePort       10.11.247.192   <none>                  8080:30080/TCP   43m
sample-nodeport-local   NodePort       10.11.255.82    <none>                  8080:30081/TCP   14m

また、払い出されるVIPはKubernetes nodeへ分散されるため、Kubernetes Nodeのスケーリングの際に変更すべき点は一切ありません。

「type: LoadBalancer」も、当然Kubernetesクラスタ外からも疎通が可能です。

リスト39:LoadBalancerもクラスタ外から疎通が可能

# 35.yyy.yyy.yyyはGCEインスタンスのIP Addressではなく、GCLBが払い出したVIP
$ curl -s http://35.yyy.yyy.yyy:8080
sample-deployment-5cddb77dd4-6gpdh
(複数回実行するとほぼ均等に3つのPod名が表示されます)
sample-deployment-5cddb77dd4-7znk5
sample-deployment-5cddb77dd4-bnll5

ノード間通信の排除(ノードをまたいだロードバランシングの排除

「type: NodePort」と同様に、externalTrafficPolicyを利用してノードをまたいだロードバランシングの排除が可能です。

LoadBalancerが払い出すVIPの指定

本番運用するサービスなどでは、利用するIP Addressを固定したいケースも多いことでしょう。そういったときには、spec.LoadBalancerIPで外部のLoadBalancerで利用するIP Addressを指定できます。先ほどの例のように、指定しない場合には自動的に払い出されます。

リスト40:IP Addressを指定したLoadBalance Serviceを作成するlb_fixip_sample.yml

apiVersion: v1
kind: Service
metadata:
  name: sample-lb-fixip
spec:
  type: LoadBalancer
  loadBalancerIP: xxx.xxx.xxx.xxx
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30083
  selector:
    app: sample-app

# IP Addressを指定したLoadBalance Serviceを作成
kubectl apply -f lb_sample.yml

また、GKEの場合には利用者が勝手に好きなVIPを利用できないようにする実装がされていることが多いため、自分が持っていないアドレスを指定した場合には、<pending>状態のままになってしまいます。GCPの場合には[VPCネットワーク]-[外部IPアドレス]から静的アドレスの予約を行ってから設定して下さい。

GCPにおける静的アドレスの確保

GCPにおける静的アドレスの確保

GKEやクラウドプロバイダーでの注意点

GKEでは 「type: LoadBalancer」 のServiceを作成するとGCLBが生成されます。GCP Loadbalancerを乱立させてコストが増えないようにするためにも、IP Addressの重複が許されたり、デプロイフロー上問題がない場合には、なるべくServiceをまとめるようにしましょう。

また、Serviceを作ったままGKEクラスタを削除した場合、GCLBは課金継続したまま残ってしまうため、注意して下さい。その他のServiceやIngressについては、次回紹介いたします。

株式会社サイバーエージェント アドテク本部 Strategic Infrastructure Agency

2016年入社。OpenStackを使ったプライベートクラウドやGKE互換なコンテナプラットフォームをゼロから構築し、国内カンファレンスでのKeynoteに登壇。
その後、世界で2番目にCertified Kubernetes Application Developer、138番目にCertified Kubernetes Administratorの認定資格を取得。
現在はKubernetesやOpenStackなどOSSへのコントリビュート活動をはじめ、CNCF公式のCloud Native Meetup TokyoのOrganizerやJapan Container Daysの運営などコミュニティ活動にも従事している。

連載バックナンバー

仮想化/コンテナ技術解説
第9回

KubernetesのConfig&Storageリソース(その2)

2018/6/6
連載9回目となる今回は、Config&Storageリソースのうち、PersistentVolumeClaimについて解説する。
仮想化/コンテナ技術解説
第8回

KubernetesのConfig&Storageリソース(その1)

2018/5/31
今回と次回の2回に渡って、5種類に大別されるKubernetesのリソースのうち、Config&Storageリソースを解説する。
仮想化/コンテナ技術解説
第7回

KubernetesのDiscovery&LBリソース(その2)

2018/5/16
前回に引き続き、Discovery&LBリソースのServiceとIngressを紹介する。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています