KubernetesのDiscovery&LBリソース(その1)
NodePort
前述のExternalIPは、指定したKubernetes NodeのIP:Portで受けたトラフィックをコンテナに転送する形で、外部疎通性を確立していました。それに対してNodePortは、全てのKubernetes NodeのIP:Portで受けたトラフィックをコンテナに転送する形で、外部疎通性を確立します。端的に言うのであれば、ExternalIP Serviceの全ノード版に近いと言えます。
Docker Swarmを使ったことがある方であれば、「ServiceをExposeした場合と同じような挙動となる」とイメージしてください。
NodePort Serviceの作成
NodePort Serviceは、下記のようなYAMLファイルから作成します。
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を明示的に指定しないことで空いている番号が自動でバインドされます。
設定項目 | 内容 |
---|---|
spec.ports[x].port | ClusterIPで受け付けるPort番号 |
spec.ports[x].targetPort | 転送先のコンテナのPort番号 |
spec.ports[x].nodePort | 全Kubernetes NodeのIP Adressで 受け付けるPort番号 |
また、このNodePortで利用できるポート範囲は、GKEでは30000~32767(Kubernetesのデフォルト値)となっており、範囲外の値を設定しようとするとエラーになります(Kubernetes Masterの設定を自分で変えられる場合には、この範囲をカスタマイズすることも可能です)。
$ 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で利用することもできません。
The Service "sample-nodeport-fail" is invalid: spec.ports[0].nodePort: Invalid value: 30080: provided port is already allocated
Serviceを確認してみると、新しくServiceが作成されています。NodePort Serviceを作成しましたが、コンテナ内からの通信は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 16m sample-nodeport NodePort 10.11.247.192 <none> 8080:30080/TCP 3m
コンテナ内から確認すると、内部DNSが返すIP AddressはExternalIPではなくClusterIPとなります。
$ 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している状態になっています。
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のファイアウォールのルールを確認して下さい。
$ 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へもロードバランシングされる形となっています。
$ 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の部分を確認してみて下さい)。
spec.externalTrafficPolicyがLocalの場合には、10.240.0.8:30080宛のリクエストはsample-deployment-5cddb77dd4-6gpdhのPodにのみ転送されます。もし、同じノード上に2つ以上のPodが存在する場合には、2つのPodへは均等に割り振られます。また、そのノード上にPodがない場合には、リクエストに応答することはできません。
例えば、externalTrafficPolicyをデフォルトのClusterからLocalにするには、下記のようなYAMLファイルを利用します。
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だけからレスポンスが返ってきます。
$ 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は、下記のようなYAMLファイルから作成します。
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の指定も可能です。
設定項目 | 内容 |
---|---|
spec.ports[x].port | LoadBalancerが払い出す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」の実装にもよりますが、多くの実装で数秒~数十秒程度待つケースが多いようです。
$ 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)も作成されています。
$ 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クラスタ外からも疎通が可能です。
# 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を指定できます。先ほどの例のように、指定しない場合には自動的に払い出されます。
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アドレス]から静的アドレスの予約を行ってから設定して下さい。
GKEやクラウドプロバイダーでの注意点
GKEでは 「type: LoadBalancer」 のServiceを作成するとGCLBが生成されます。GCP Loadbalancerを乱立させてコストが増えないようにするためにも、IP Addressの重複が許されたり、デプロイフロー上問題がない場合には、なるべくServiceをまとめるようにしましょう。
また、Serviceを作ったままGKEクラスタを削除した場合、GCLBは課金継続したまま残ってしまうため、注意して下さい。その他のServiceやIngressについては、次回紹介いたします。
連載バックナンバー
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で復数環境のマニフェストファイルを簡単整理