Edited at

Kubernetes "サービス"の概要についての自習ノート

More than 1 year has passed since last update.

この自習ノートは、IBM Cloud に依存しない Kubernetesの機能についての内容にしたいと思います。 k8sのサービスの機能について、個人が自習したノートであって、一部IBMクラウドの実装について言及していますが、特定のクラウドに依存する内容ではありません。


サービスとは

Kubernetesのサービスは、論理的なポッドのセットと、それにアクセスするポリシーを定義する抽象的なものです。 これらは、しばしばマイクロサービスとされます。サービスとして接続先になるポッドのセットは、普通、ラベル・セレクターによって判別されます。


サービスの例

コンテナのレジストリに登録されたアプリケーションのコンテナ・イメージmaho/express1:1.0を kubectl で次のファイルを利用して、3つのノードから成るk8sクラスタにデプロイしたとします。


express-app.yml

apiVersion: extensions/v1beta1

kind: Deployment
metadata:
name: express-app
spec:
replicas: 3
template:
metadata:
labels:
app: express
spec:
containers:
- name: express1
image: maho/express1:1.0
ports:
- containerPort: 3000

そうすると、次の図の様に、k8sのクラスタを構成するノードに、ポッドが配置されます。ポッドはクラスタ・ネットワークのIPアドレスを持ち、そして、ノード間を横断する様にクラスタ・ネットワークは形成されています。そして、ノードにもIPアドレスを持っています。

スクリーンショット 2017-12-19 22.52.24.png

このポッドのIPアドレスは、永続的なものではなく、例えば、上記のアプリケーションのコンテナのバージョンを express1:1.0 から express1:1.1 へ更新したコンテナへロールアウト(リリース)すると、ポッドのIPアドレスは変わってしまいます。 そこで、レプリカ・セットのメンバーのポッドのIPアドレスを意識せずに、代表のIPアドレスを通じてアクセスする方法が必要です。 

つまり、複数存在するポッドの数やノードの存在を意識せずに抽象的なアクセスを提供する機能が、k8sのサービスです。

スクリーンショット 2017-12-21 10.57.21.png

例として、3つのレプリカで実行されている画像処理バックエンドを考えてみましょう。これらのレプリカは代替可能なものです。フロントエンドは、バックエンドのどれを利用するか、気にしません。 一方で、バックエンドを構成する実際のポッドは、変更されるかもしれませんが、フロントエンドのクライアントは、それらを認識したり、バックエンド自体のリストを追跡する必要はありません。 サービスの抽象化により、このデカップリングが可能になります。


サービスの定義

例えば、前述の図のサービスを作るには、次のYAMLをkubectlを使って、マスターノードのapiserverにポストします。

サービスの転送先のポッドのセットは、普通はラベル・セレクターによって判別されます。この定義ではapp: expressと同じラベルを持つレプリカ・セットのポッドたちへ、リクエストを転送します。

kind: Service

apiVersion: v1
metadata:
name: express-app-svc
spec:
selector:
app: express
ports:
- protocol: TCP
port: 3000
targetPort: 3000

このYAML設定は、「app=express」ラベルを持つ任意のポッドに向けて、TCPポート3000をターゲットにする"express-app-svc"という名前の新たにサービス・オブジェクトを生成します。 このサービスには、クラスタIPとされるIPアドレスが割り当てられます、これは、サービス・プロキシで利用されます。 サービスのセレクタは、継続的に評価され、結果は "express"というエンドポイント・オブジェクトにPOSTされます。

サービスは、着信ポートを任意のtargetPortにマップできることに注意してください。デフォルトでは、targetPortはportフィールドと同じ値に設定されます。 これにより、サービスのデプロイと更新に多大な柔軟性がもたらされます。たとえば、クライアントを壊すことなく、ポッドが公開するポート番号をバックエンドソフトウェアの次のバージョンで変更することができます。

Kubernetes Servicesは、プロトコル用にTCPとUDPをサポートしています。デフォルトはTCPです


セレクタを利用しないケース

サービスは一般的に Kubernetes ポッドへのアクセスを抽象化しますが、他の種類のバックエンドを抽象化することもできます。

例えば:


  • 本番環境では外部データベースクラスタを使用したいが、テストではテスター独自のデータベースを使用する。

  • 別のネームスペース内のサービス、または別のクラスター上のサービスに向ける必要がある。

  • Kubernetes へワークロードを移行しているが、一部のバックエンドはKubernetesの外部で実行している。


セレクター無しでサービスを定義して別途エンドポイントを作る方法

セレクター無しでServcieを定義しておき、リクエスト転送先のエンドポイントを別途定義する方法は次の様に作っていきます。


t017-service-wo-selector.yml

apiVersion: v1

kind: Service
metadata:
name: baremetal-svc
spec:
ports:
- protocol: TCP
port: 9376
targetPort: 80

このYAMLでは、TCPポート番号 9376 でリクエストを待ち受ける barematal-svcという名前でサービスを作成します。そして、同じ名前を持つエンドポイントへリクエストを転送します。


t018-endpoint.yml

kind: Endpoints

apiVersion: v1
metadata:
name: baremetal-svc
subsets:
- addresses:
- ip: 161.202.xxx.yyy
ports:
- port: 80

このYAMLは、エンドポイントのアドレスとポート番号を定義します。 k8sの外部のシステムがサービスを待ち受けるアドレスとポート番号を定義します。

kubectl getで、エンドポイント、サービスをチェックした結果です。 この二つの繋がりはメタデータのnameフィールドです。

vagrant@vagrant-ubuntu-trusty-64:~/$ kubectl get ep baremetal-svc

NAME ENDPOINTS AGE
baremetal-svc 161.202.xxx.yyy:80 11m

vagrant@vagrant-ubuntu-trusty-64:~/$ kubectl get svc baremetal-svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
baremetal-svc ClusterIP 172.21.15.20 <none> 9376/TCP 11m

同じクラスタ・ネットワークに存在するポッドからDNS名でアクセスしてみます。 確かに外部システムのアドレスからHTTPヘッダーを取得してきた事が判ります。

root@bash-57559bc848-xm6dk:/# curl -I http://baremetal-svc:9376/

HTTP/1.1 200 OK
Server: nginx/1.4.6 (Ubuntu)
Date: Thu, 21 Dec 2017 02:36:13 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 04 Mar 2014 11:46:45 GMT
Connection: keep-alive
ETag: "5315bd25-264"
Accept-Ranges: bytes


ExternalName service を利用する方法

前述のサービスとエンドポイントを個別に作成する方法では、複数のエンドポイントを代表するクラスタIPアドレスが取得します。 一方で、ExternalNameを利用する方法では、kube-dnsに登録されるだけで、プロキシーの設定はありません。 このYAMLファイルは ExternalNameで外部のアクセス先を登録するもので、プロキシーを設定しないので、ポート番号をマップする事は出来ません。


t014-extenal-svc.yml

kind: Service

apiVersion: v1
metadata:
name: bm-service
namespace: default
spec:
type: ExternalName
externalName: 10.132.253.39

実行状態を取得すると、以下の様になります。

vagrant@vagrant-ubuntu-trusty-64:~/$ kubectl get svc bm-service

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
bm-service ExternalName <none> 10.132.253.39 <none> 4m

この方法では、次の様に、ポッドのコンテナから sshで外部システムへログインすることもできます。

root@bash-57559bc848-xm6dk:/# ssh bm-service -l root

Password:
Welcome to Ubuntu 14.04.5 LTS (GNU/Linux 3.13.0-137-generic x86_64)

* Documentation: https://help.ubuntu.com/
Last login: Wed Dec 20 09:56:43 2017 from softbank126077118060.bbtec.net
root@test57:~# w
12:20:55 up 7 days, 16:28, 1 user, load average: 0.00, 0.01, 0.05
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
root pts/1 10.132.253.30 12:20 2.00s 0.01s 0.00s w


前述の2方法のDNS登録内容

kube-dnsに登録された情報をポッドのコンテナから nslookup コマンドを実行して確認した結果です。最初は、セレクター無しでサービスを定義して別途エンドポイントを設定したケースです。 IPアドレスがクラスタ・ネットワークの範囲になっており、外部システムへアクセスするために、プロキシが動作していることが伺えます。

root@bash-57559bc848-xm6dk:/# nslookup baremetal-svc

Server: 172.21.0.10
Address: 172.21.0.10#53

Name: baremetal-svc.default.svc.cluster.local
Address: 172.21.15.20

次は、ExternalName サービスを利用したケースです。このアドレス解決の結果では、k8sクラスタ外のプライベート・アドレスが設定されています。つまり、ExternalNameは、kube-DNSへエントリーを追加するだけのサービスとみなす事ができると思います。

root@bash-57559bc848-xm6dk:/# nslookup bm-service   

Server: 172.21.0.10
Address: 172.21.0.10#53

Name: bm-service.default.svc.cluster.local
Address: 10.132.253.39



仮想IPとサービス・プロキシ

Kubernetesクラスタ内のすべてのノードが kube-proxy を実行します。 kube-proxyはプロセスの起動時のオプションとして、「userspace」、「iptables」、「ipvs」のモードを選択でき、空白の場合には「iptables」が使用されます。 Kubernetes v1.0では、サービスは「レイヤ4」(TCP/UDP over IP)構造であり、プロキシ・モードは「userspace」でした。Kubernetes v1.2以降では「iptables」がデフォルトの動作モードになりました。 そして、Kubernetes v1.9-alphaで「ipvs proxy」が追加されました。

スクリーンショット 2017-12-21 18.59.08.png


プロキシーモード: userspace

このモードでは、kube-proxyは、KubernetesマスターがServiceオブジェクトとEndpointsオブジェクトの追加と削除を監視します。 各サービスに対して、ローカルノード上で(無作為に選ばれた)ポートを開きます。 この「プロキシポート」への接続は、サービスのバックエンドポッド(エンドポイントで報告されている)のいずれかにプロキシされます。 使用するバックエンドポッドは、サービスのSessionAffinityに基づいて決定されます。 最後に、iptablesルールをインストールして、サービスのclusterIP(仮想)およびPortへのトラフィックを収集し、そのトラフィックをバックエンドポッドをプロキシするプロキシポートにリダイレクトします。 デフォルトでは、バックエンドの選択はラウンドロビンです。

明らかに、iptablesはuserspaceとkernelspaceの間で戻ってくる必要はなく、userspaceプロキシよりも高速で信頼性が高いはずです。 しかし、ユーザ空間プロキシとは異なり、iptablesプロキシアは最初に選択したポッドが応答しない場合、自動的に別のポッドを再試行することができないため、作業準備プローブ(readiness probes)の使用に依存します。


プロキシーモード: iptables

このモードでは、kube-proxyはKubernetesマスターがServiceオブジェクトとEndpointsオブジェクトの追加と削除を監視します。 各サービスについて、iptablesルールをインストールして、サービスのクラスタIP(仮想)とポートへのトラフィックを取得し、そのトラフィックをサービスのバックエンドセットの1つにリダイレクトします。 各エンドポイントオブジェクトに対して、バックエンドポッドを選択するiptablesルールをインストールします。デフォルトでは、バックエンドの選択はランダムです。

明らかに、iptablesはuserspaceとkernelspaceの間で戻ってくる必要はなく、userspaceプロキシよりも高速で信頼性が高いはずです。 しかし、ユーザ空間プロキシとは異なり、iptablesプロキシアは最初に選択したポッドが応答しない場合、自動的に別のポッドを再試行することができないため、作業準備プローブ(readiness probes)の使用に依存します。


プロキシーモード: ipvs

このモードでは、kube-proxyはKubernetesサービスとエンドポイントを監視し、それに応じてnetlink interface create ipvsルールを呼び出し、定期的にipvsルールをKubernetesサービスとエンドポイントと同期させて、ipvsのステータスが期待通りであることを確認します。 サービスにアクセスすると、トラフィックはバックエンドポッドの1つにリダイレクトされます。

iptablesと同様に、Ipvsはnetfilterフック関数に基づいていますが、基になるデータ構造としてハッシュテーブルを使用し、カーネル状態で動作します。 これは、ipvsがトラフィックの転送をはるかに高速化できることを意味し、同期プロキシルールの方がパフォーマンスがはるかに優れています。 さらに、ipvsは次のような負荷分散アルゴリズムのためのより多くのオプションを提供します:


  • rr: round-robin

  • lc: least connection

  • dh: destination hashing

  • sh: source hashing

  • sed: shortest expected delay

  • nq: never queue

注:ipvsモードでは、kube-proxyを実行する前にノードにIPVSカーネルモジュールがインストールされていると想定しています。 kube-proxyがipvsプロキシモードで起動すると、kube-proxyはノードにIPVSモジュールがインストールされているかどうかを確認します。kube-proxyはiptablesプロキシモードに戻ります。



複数ポートのサービス

Kubernetesは、Serviceオブジェクトの複数のポートを公開する定義をサポートしています。 次の例の様に、複数のポートを使用する場合は、エンドポイントとの関連付けのため、すべてのポート名を指定する必要があります。

kind: Service

apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
- name: https
protocol: TCP
port: 443
targetPort: 9377



独自のIPアドレスの選定

独自のクラスタIPアドレスをサービス作成要求の一部として指定できます。 これを行うには、spec.clusterIP フィールドを設定します。 たとえば、既存のDNSエントリがすでに存在する場合や、特定のIPアドレス用に設定され再構成が困難なレガシーシステムがある場合などです。 ユーザが選択するIPアドレスは、有効なIPアドレスで、APIサーバにフラグで指定されたservice-cluster-ip-range CIDR範囲内である必要があります。 IPアドレスの値が無効な場合、apiserverは値が無効であることを示す422 HTTPステータスコードを返します。



サービスのディスカバリー

ポッドのアプリケーションが、サービスを発見するために、環境変数とDNSの二つの方法を提供しています。

次の例を見ながら、機能の理解を進めていきます。ここに、Cluster IP を取得しているサービス express-app-svcがあります。 このサービスが別のクライアントとなるポットで、どの様に取得できるか確認します。

$ kubectl get svc express-app-svc

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
express-app-svc ClusterIP 172.21.127.174 <none> 3000/TCP 1d


環境変数

上記サービスが稼働している状態で、新たに対話できるポッドを起動して、環境変数をリストします。 サービス名がexpress-app-svcですが、これに対応する環境変数は、EXPRESS_APP_SVCになります。

$ kubectl run -it --image ubuntu:latest bash

root@bash-57559bc848-xm6dk:/# env |grep EXPRESS |sort -r
EXPRESS_APP_SVC_SERVICE_PORT=3000
EXPRESS_APP_SVC_SERVICE_HOST=172.21.42.233
EXPRESS_APP_SVC_PORT_3000_TCP_PROTO=tcp
EXPRESS_APP_SVC_PORT_3000_TCP_PORT=3000
EXPRESS_APP_SVC_PORT_3000_TCP_ADDR=172.21.42.233
EXPRESS_APP_SVC_PORT_3000_TCP=tcp://172.21.42.233:3000
EXPRESS_APP_SVC_PORT=tcp://172.21.42.233:3000

環境変数によるサービスのディスカバリーでは、順番の考慮が必要になります。つまり、サービスが起動した後に、実行を開始したポッドは環境変数がセットされますが、ポッドの開始時に起動していないサービスは、環境変数としてセットされません。


DNS

DNSサーバーは、Kubernetes APIで新しいサービスを監視し、それぞれに対して一連のDNSレコードを作成します。 DNSがクラスタ全体で有効になっている場合、すべてのポッドはサービスの名前解決を自動的に行うことができます。

たとえば、Kubernetesネームスペース "my-ns"に "my-service"というサービスがある場合、 "my-service.my-ns"のDNSレコードが作成されます。 「my-ns」名前空間に存在するポッドは、単に「my-service」の名前検索を行うだけで見つけることができます。 他のネームスペースに存在するポッドは、その名前を "my-service.my-ns"として修飾する必要があります。 これらの名前検索の結果はクラスタIPです。

Kubernetesは、名前付きポート用のDNS SRV(サービス)レコードもサポートしています。 "my-service.my-ns"サービスにプロトコルTCPを持つ "http"という名前のポートがある場合は、 "_http._tcp.my-service.my-ns"のDNS SRVクエリを実行して結果を得る事ができます。

ここから、SRVレコードの振る舞いについて、動作の理解を進めていきます。

DNS SRVレコードを利用して、アクセスするべきポート番号を取得する事ができます。 SRVを利用するためには、サービスを定義するYAMLファイルで、下記の例の様に、ポート番号以外に、name と protocol フィールドの値のセットが必要です。 このYAMLファイルの定義をkubectlコマンドで適用すると、それと同時にk8sのDNSサービスに登録されます。

apiVersion: v1

kind: Service
metadata:
name: express-app-svc
spec:
selector:
app: express
ports:
- protocol: TCP
port: 3000
name: http

環境変数ケースと異なり、DNSサーバーへの反映結果は、稼働中の全てのポッドで、即時参照できる様になります。 下記は前述の環境変数取得時に起動した対話形のポッドです。 nslookupを起動して、SRVレコードを取得するために、set query=srvを実行しておきます。 そして、SRVの情報を取得するクエリー _http._tcp.express-app-svc を実行します。 _httpは上記の spec.ports.nameフィールドにセットされるで、_tcpは同プロトコルのフィールドです。 サービス名、プロトコル、ポート名がわかっていれば、アクセスするべき、ポート番号がDNSから取得できるという仕掛けです。

このクエリーの結果は、service = 10 100 3000 express-app-svc.default.svc.cluster.local.です。数字の意味は、順番にプライオリティ、ウェイト、ポート番号となります。

root@bash-57559bc848-xm6dk:/# nslookup

> set query=srv
> _http._tcp.express-app-svc
Server: 172.21.0.10
Address: 172.21.0.10#53

_http._tcp.express-app-svc.default.svc.cluster.local service = 10 100 3000 express-app-svc.default.svc.cluster.local.

この値をアプリケーションから取得する基礎的な例として、Pythonでのポート番号の取得例を以下に示します。 事前に dnspython というモジュールをpip インストールしておく必要がありますが、非常にシンプルなコードでポート番号を取得できる事が解ります。

root@bash-57559bc848-xm6dk:/# python

Python 2.7.12 (default, Nov 20 2017, 18:23:56)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import dns.resolver
>>> answers = dns.resolver.query('_http._tcp.express-app-svc','SRV')
>>> for rdata in answers:
... print "priority=", rdata.priority
... print "weight=", rdata.weight
... print "port=", rdata.port
...
priority= 10
weight= 100
port= 3000

参考資料

* DNS,Services,CONCEPT,Documentation,Kubernetes https://kubernetes.io/docs/concepts/services-networking/service/#dns

* DNS for Services and Pods https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/

* Connecting Applications with Services https://kubernetes.io/docs/concepts/services-networking/connect-applications-service/

* Configure DNS Service https://kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/



ヘッドレス・サービス

ロードバランシングと単一のサービスIPを必要としない場合、クラスタIPspec.clusterIPNoneを指定することで、「ヘッドレス」サービスを作成できます。 このサービスでは、クラスタIPは割り当てられず、kube-proxyはこれらのサービスを処理せず、プラットフォームによってロード・バランシングやプロキシ処理が行われません。 DNSの自動設定方法は、サービスにセレクタが定義されているかどうかによって異なります。


セレクター利用するケース

セレクタを定義するヘッドレスサービスの場合、エンドポイント・コントローラはAPIにエンドポイント・レコードを作成し、DNS設定を変更して、サービスをバックアップするポッドに直接ポイントするレコード(アドレス)を返します。

このセレクターを利用したヘッドレスの具体的な動作を確認したいと思います。 次のサービス定義のYAMLは、ClusterIP: Noneを設定してプロキシしない事を定義しています。


t019-headless.yml

apiVersion: v1

kind: Service
metadata:
name: express-app-hl-svc
spec:
selector:
app: express
ports:
- protocol: TCP
port: 3000
name: http
clusterIP: None

このYAMLでサービスを起動すると、次の様に CLUSTER-IPもEXTERNAL-IPも無い状態で実行されます。

$ kubectl get svc express-app-hl-svc

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
express-app-hl-svc ClusterIP None <none> 3000/TCP 1h

このサービスが繋げるエンドポイントは、セレクターで指定したポッドのレプリカ・セットになります。 クラスタのIPアドレス無し、プロキシ無しでどうやって、このサービスは機能するのでしょうか? DNSに精通した人であれば想像どおりです。

$ kubectl get ep express-app-hl-svc

NAME ENDPOINTS AGE
express-app-hl-svc 172.30.11.58:3000,172.30.170.239:3000,172.30.180.159:3000 1h

k8sの同じクラスタに起動したポッドから nslookupコマンドでサービス名をひいくと、次の様に、3つのポッドのIPアドレスがリストされます。つまり、DNSラウンド・ロビンを利用してポッドにアクセスできるという仕掛けです。

root@bash-57559bc848-xm6dk:/# nslookup express-app-hl-svc

Server: 172.21.0.10
Address: 172.21.0.10#53

Name: express-app-hl-svc.default.svc.cluster.local
Address: 172.30.11.58
Name: express-app-hl-svc.default.svc.cluster.local
Address: 172.30.170.239
Name: express-app-hl-svc.default.svc.cluster.local
Address: 172.30.180.159

ポッドのホスト・ネームを返すRESTサービスを呼び出して、DNSラウンド・ロビンがどの程度効くか、確認してみます。 リゾルバーのキャッシュやTTL時間が設定されるので、あまり期待できないかと思いきや、iptables モードのプロキシーと同じくらい均一に分散されている様です。

root@bash-57559bc848-xm6dk:/# curl http://express-app-hl-svc:3000/hostname

hostname = express-app-77d5f67b99-sb6kc
root@bash-57559bc848-xm6dk:/# curl http://express-app-hl-svc:3000/hostname
hostname = express-app-77d5f67b99-rnx9p
root@bash-57559bc848-xm6dk:/# curl http://express-app-hl-svc:3000/hostname
hostname = express-app-77d5f67b99-sb6kc
root@bash-57559bc848-xm6dk:/# curl http://express-app-hl-svc:3000/hostname
hostname = express-app-77d5f67b99-rnx9p
root@bash-57559bc848-xm6dk:/# curl http://express-app-hl-svc:3000/hostname
hostname = express-app-77d5f67b99-sb6kc
root@bash-57559bc848-xm6dk:/# curl http://express-app-hl-svc:3000/hostname
hostname = express-app-77d5f67b99-gfxw4


セレクターを利用しないケース

セレクタを定義しないヘッドレスサービスの場合、エンドポイントコントローラはエンドポイントレコードを作成しません。 ただし、DNSシステムは次のいずれかを設定します。


  • ExternalNameタイプのサービスのCNAMEレコード

  • 他のすべてのタイプの場合、サービスと名前を共有する任意のエンドポイントのレコード

ExternalNameについて具体的な動作をみていくため、以下のYAMLを準備しました。 spec.externalNameには、DNS名またはIPアドレスを設定できます。


t014-extenal-svc.yml

kind: Service

apiVersion: v1
metadata:
name: bm-service
namespace: default
spec:
type: ExternalName
externalName: 10.132.253.39

設定実施後に、確認したすると、CLUSTER-IPは無しで、EXTERNAL-IPにexternalNameに設定した値が入っています。

$ kubectl describe svc bm-service

Name: bm-service
Namespace: default
Labels: <none>
Annotations: <none>
Selector: <none>
Type: ExternalName
IP:
External Name: 10.132.253.39
Session Affinity: None
Events: <none>

ポッドからアクセスすると、サービス名でアドレスが解決できる様になっています。

root@bash-57559bc848-xm6dk:/# nslookup bm-service

Server: 172.21.0.10
Address: 172.21.0.10#53

Name: bm-service.default.svc.cluster.local
Address: 10.132.253.39



公開サービス サービスタイプ

アプリケーションの一部(フロントエンドなど)では、サービスを外部(クラスター外)のIPアドレスに公開したい場合があります。k8s ServiceTypesでは、必要なサービスの種類を指定できます。 デフォルトはClusterIPです。 型の値とその振る舞いは次のとおりです。


  • ClusterIP:クラスタ内のIPにサービスを公開します。 この値を選択すると、サービスはクラスタ内からのみ到達可能になります。 これがデフォルトのServiceTypeです。


  • NodePort:各ノードのIP上のサービスを静的ポート(NodePort)に公開します。 NodePortサービスがルーティングするClusterIPサービスが自動的に作成されます。 :を要求することにより、クラスタ外からNodePortサービスにアクセスできます。


  • LoadBalancer:クラウドプロバイダのロードバランサを使用して外部にサービスを公開します。 外部ロードバランサがルーティングするNodePortサービスとClusterIPサービスが自動的に作成されます。


  • ExternalName:その値でCNAMEレコードを返すことによって、サービスをexternalNameフィールドのコンテンツ(たとえばfoo.bar.example.com)にマッピングします。 どのような種類のプロキシも設定されていません。 これには、バージョン1.7以上のkube-dnsが必要です。



Type NodePort

タイプフィールドを "NodePort"に設定すると、Kubernetesマスターはフラグ設定された範囲(デフォルト:30000-32767)からポートを割り当て、各ノードは同じノードを開きポッドに転送します。

特定のポート番号が必要な場合は、nodePortフィールドに値を指定すると、システムがそのポートを割り当てます。指定する値は、ノードポートの設定範囲内にある必要があります。

次の図の様に、NodePortで開かれたポートは、k8sクラスタの全ノードで、同じポート番号が設定されます。そして、セレクタで指定する全てのポットに要求を分配します。

スクリーンショット 2017-12-24 12.43.31.png

例えば、Node#2のIPアドレスのNodePort番号にアクセスしても、Node#1他の上で動作するポッドへ分散されます。この時のデフォルトの分散アルゴリズムはランダムです・

スクリーンショット 2017-12-24 12.43.40.png


Type LoadBalancer

外部ロードバランサをサポートするクラウドプロバイダでは、タイプフィールドを "LoadBalancer"に設定すると、サービスのロードバランサがプロビジョニングされます。 ロードバランサの実際の作成は非同期で行われ、プロビジョニングされたバランサに関する情報は、サービスのstatus.loadBalancerフィールドに公開されます。

例えば、次の様にしてプロビジョニングできます。

kind: Service

apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
clusterIP: 10.0.171.239
loadBalancerIP: 78.11.24.19
type: LoadBalancer
status:
loadBalancer:
ingress:
- ip: 146.148.47.155

IBMクラウドで外部からのロードバランサーを指定する場合、次の例の様にYAMLを定義することができます。

apiVersion: v1

kind: Service
metadata:
name: express-app-lb-ext
spec:
type: LoadBalancer
selector:
app: express
ports:
- protocol: TCP
port: 3000
targetPort: 3000
loadBalancerIP: 169.56.7.58

外部からアクセスするIPアドレスの指定は、loadBalancerIPのフィールドに設定します。 この設定可能な範囲は、IBM CloudのInfrastructure の機能と連携しており、次の様にして、調べていく事ができます。

次のコマンド得られるクラスタのIDをメモします。

$ bx cs cluster-get mycluster3

Retrieving cluster mycluster3...
OK

Name: mycluster3
ID: 20bee482ba9d4a9687dea557b8b13271
State: normal

サブネットのリスクをクラスタIDでgrepすると、クラスタに割り当てられたサブネットがリストされます。

$ bx cs subnets |grep 20bee482ba9d4a9687dea557b8b13271

ID Network Gateway VLAN ID Type Bound Cluster
1085815 10.132.160.48/28 10.132.160.49 978115 private 20bee482ba9d4a9687dea557b8b13271
1498597 169.56.7.48/28 169.56.7.49 978113 public 20bee482ba9d4a9687dea557b8b13271

上記は見易くなる様に編集したものです。

上記から解ると通り、パブリック側では 169.56.7.48/28 の定義ですから、ipcalcの結果から、指定できるIPアドレスとしては、169.56.7.49 - 169.56.7.62が利用できます。

$ ipcalc 169.56.7.48/28

Address: 169.56.7.48 10101001.00111000.00000111.0011 0000
Netmask: 255.255.255.240 = 28 11111111.11111111.11111111.1111 0000
Wildcard: 0.0.0.15 00000000.00000000.00000000.0000 1111
=>
Network: 169.56.7.48/28 10101001.00111000.00000111.0011 0000
HostMin: 169.56.7.49 10101001.00111000.00000111.0011 0001
HostMax: 169.56.7.62 10101001.00111000.00000111.0011 1110
Broadcast: 169.56.7.63 10101001.00111000.00000111.0011 1111
Hosts/Net: 14 Class B

プライベート側も同様に求めて、10.132.160.49 〜 10.132.160.62であることがわかります。

IBM Cloud Infrastructure のセカンダリーサブネットから k8sクラスタのサービスへサブネットが割り当てられている事がわかります。

$ slcli subnet list --v4 -d tok02 --network-space PUBLIC

:.........:....................:...................:...............:............:.........:.....:....:
: id : identifier : type : network_space : datacenter : vlan_id : IPs : vs :
:.........:....................:...................:...............:............:.........:.....:....:
: 999167 : 161.202.132.80/28 : SECONDARY_ON_VLAN : PUBLIC : tok02 : 978113 : 16 : 0 :
: 1498597 : 169.56.7.48/28 : SECONDARY_ON_VLAN : PUBLIC : tok02 : 978113 : 16 : 0 :
: 1038909 : 161.202.142.192/28 : PRIMARY : PUBLIC : tok02 : 978113 : 16 : 4 :
:.........:....................:...................:...............:............:.........:.....:....:

$ slcli subnet list --v4 -d tok02 --network-space PRIVATE

:.........:..................:...................:...............:............:.........:.....:....:
: id : identifier : type : network_space : datacenter : vlan_id : IPs : vs :
:.........:..................:...................:...............:............:.........:.....:....:
: 758078 : 10.132.5.128/26 : SECONDARY_ON_VLAN : PRIVATE : tok02 : 978115 : 64 : 0 :
: 1085815 : 10.132.160.48/28 : SECONDARY_ON_VLAN : PRIVATE : tok02 : 978115 : 16 : 0 :
: 1038857 : 10.132.253.0/26 : PRIMARY : PRIVATE : tok02 : 978115 : 64 : 4 :
:.........:..................:...................:...............:............:.........:.....:....:


Internal load balancer

混在環境では、同じVPC内のサービスからトラフィックをルーティングする必要があることがあります。 スプリットホライゾンDNS環境では、外部トラフィックと内部トラフィックの両方をエンドポイントにルーティングできるようにするには、2つのサービスが必要です。

これは、クラウドプロバイダーに基づいて以下の注釈をサービスに追加することで実現できます。


GCP

GCP Kubernetes 1.7.2 以降で 次の様に定義します。

[...]

metadata:
name: my-service
annotations:
cloud.google.com/load-balancer-type: "Internal"
[...]

詳細は https://cloud.google.com/kubernetes-engine/docs/how-to/internal-load-balancing を参照してください。


AWS

AWSでは次の様に定義します。

[...]

metadata:
name: my-service
annotations:
service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
[...]

詳細 https://stackoverflow.com/questions/37869727/kubernetes-support-for-internal-load-balancers-in-aws

ソース https://github.com/kubernetes/kubernetes/blob/75dfb21018a7c36ceecf78a453683aa575b185fb/pkg/cloudprovider/providers/aws/aws_loadbalancer.go


Azure

Azureの場合

[...]

metadata:
name: my-service
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
[...]


IBM Cloud

次は、IBM Cloudeのプライベート・ロードバランサーのアノテーションです。

[...]

metadata:
name: my-service
annotations:
service.kubernetes.io/ibm-load-balancer-cloud-provider-ip-type: private
[...]

詳細説明は、https://console.bluemix.net/docs/containers/cs_apps.html#cs_apps_public_load_balancer にあります。

IBM Cloud Container Serviceでの実行例

次のYAMLファイルを適用することで、内部ロードバランサをスタートさせる事ができます。 IBM Cloud Infrastructure のアプライアンスのロードバランサーとは連動しないので、追加料金は発生しません。

apiVersion: v1

kind: Service
metadata:
name: express-app-lb-int
annotations:
service.kubernetes.io/ibm-load-balancer-cloud-provider-ip-type: private
spec:
type: LoadBalancer
selector:
app: express
ports:
- protocol: TCP
port: 3000
targetPort: 3000
loadBalancerIP: 10.132.160.51

IPアドレスを指定するフィールド loadBalancerIP に指定できるIPアドレス範囲は、前述のIBM Cloud InfrastructureのプライベートのセカンダリーIPとして割り当てられた範囲です。 この項目を省略すると、割り当て範囲から自動的にアサインします。

$ kubectl get svc express-app-lb-int

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
express-app-lb-int LoadBalancer 172.21.177.132 10.132.160.51 3000:30814/TCP 5s


ロードバランサー SSLサポート

AWSでは内部ロードバランサーと同様に、アノテーションに設定を追加する事で、SSLサポートを追加できる様です。

https://kubernetes.io/docs/concepts/services-networking/service/#ssl-support-on-aws

IBM Cloud Container Service のSSLサポートは、Engressサービスによって提供されます。 詳細は、Ingress を使用してアプリへのアクセスを構成する方法 を参考になります。



欠点

ユーザー空間プロキシをVIPに使用することは、小規模から中規模の規模では機能しますが、数千のサービスを持つ非常に大きなクラスターには適用されません。 詳細については、ポータルのオリジナル設計案を参照してください。

ユーザ空間プロキシを使用すると、サービスにアクセスするパケットのソースIPが隠されます。 これはいくつかの種類のファイアウォールを不可能にします。 iptablesプロキシは、クラスタ内のソースIPを隠すことはありませんが、ロードバランサまたはノードポートを経由してくるクライアントには影響します。

タイプフィールドはネストされた機能として設計されています。各レベルは前の機能に追加されます。 これは、すべてのクラウドプロバイダで厳密に必須ではありません(たとえば、Google Compute EngineはLoadBalancerを動作させるためにNodePortを割り当てる必要はありませんが、AWSは現在のAPIを必要とします)



参考資料