Kubernetes クラスタ上にデプロイしたアプリケーションを利用する場合、最終的には、Pod 内のコンテナが Listen するポートに対してリクエストすることになる。つまり、IP アドレスで Pod を指定し、ポート番号で Pod 内のコンテナを指定するわけだが、これらを直接指定するのでは管理しにくいし、スケールもしにくくなる。
そこで、Kubernetes では Service リソースオブジェクトによって、この問題、すなわちサービスディスカバリの問題を解決する。
ここでは、O'Reilly 『入門 Kubernetes』 の7章の内容を試してみる。
テキストに載っている kubectl run コマンドでは Deployment リソースは作成できないので、次のようにする。
$ kubectl create deployment alpaca-prod --image=gcr.io/kuar-demo/kuard-amd64:1 --replicas=3 --port=8080 # --labels="ver=1,app=alpaca,env=prod"
$ kubectl create deployment bandicoot-prod --image=gcr.io/kuar-demo/kuard-amd64:2 --replicas=2 --port=8080 # --labels="ver=2,app=bandicoot,env=prod"
次に kubectl edit
コマンドでラベルの変更をしたいのだが、Immutable だというエラーが出てしまう。そこで、
$ kubectl get deployment alpaca-prod -o yaml >alpaca-prod.yml
を実行して、マニフェストファイルを取得し、その中の
metadata.labels.{app, ver, env} spec.selector.matchLabels.{app, ver, env} spec.template.metadata.labels.{app, ver, env}
の項目を目的の値に書き換えた。bandicoot-prod Deployment オブジェクトも同様にする。この2つのファイル、alpaca-prod.yml
, bandicoot-prod.yml
を使って、再度 Deployment オブジェクトをデプロイし直す。
$ kubectl apply -f alpaca-prod.yml
$ kubectl apply -f bandicoot-prod.yml
Service リソースを他のリソースから作成することができる。ここでは、先に作成した Deployment リソースを基に作成する。
$ kubectl expose deployment alpaca-prod
service/alpaca-prod exposed
$ kubectl expose deployment bandicoot-prod
service/bandicoot-prod exposed
$ kubectl get services -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
alpaca-prod ClusterIP 10.96.233.137 <none> 8080/TCP 48s app=alpaca,env=prod,ver=1
bandicoot-prod ClusterIP 10.99.154.163 <none> 8080/TCP 32s app=bandicoot,env=prod,ver=2
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 46d <none>
CLUSTER-IP 列の IP アドレスの PORT(S) 列のポートへのトラフィックが SELECTOR 列でセレクトされる Pod へと流される。
$ kubectl get service alpaca-prod -o yaml
とすれば、alpaca-prod Service オブジェクトのマニフェストが出力されるのでより詳細な情報がわかる。その中で
spec.ports
は
port: 8080
protocol: TCP
targetPort: 8080
となっているが、これは、8080 番ポートを待ち受けて、これをターゲットとなる Pod の 8080 番ポートへと経路をつくるということだ。転送先となる Pod を決めるラベルやポートはすべて、Service を作成するときに使用した Deployment リソースのマニフェストから引用している。
Service リソースオブジェクトがもつ Cluster IP アドレスを直に使うこともできるが、Kubernetes クラスタは、DNS サービスを提供しているので、Service オブジェクトの名前を使える。
$ kubectl get services --namespace="kube-system"
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 46d
これが、Kubernetes クラスタが提供する DNS のための Service オブジェクトだ。これを試すのに、別の Pod をクラスタ上にデプロイして実験する。
$ kubectl run test-dns -it --restart=Never --image="fortunefield/linux-gcc:ubuntu-20.04" --command -- /bin/bash
root@test-dns:/#
root@test-dns:/#
root@test-dns:/# cat /etc/resolv.conf
nameserver 10.96.0.10 # kube-dns の ClusterIP になっている。
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
root@test-dns:/#
root@test-dns:/#
root@test-dns:/#
root@test-dns:/# telnet alpaca-prod 8080
Trying 10.96.233.137...
Connected to alpaca-prod.default.svc.cluster.local.
Escape character is '^]'.
^]
telnet> quit
Connection closed.
root@test-dns:/#
root@test-dns:/# telnet bandicoot-prod 8080
Trying 10.99.154.163...
Connected to bandicoot-prod.default.svc.cluster.local.
Escape character is '^]'.
^]
telnet> quit
Connection closed.
root@test-dns:/#
root@test-dns:/# curl http://alpaca-prod:8080
.....
.....
root@test-dns:/# curl http://bandicoot-prod:8080
.....
.....
root@test-dns:/# curl http://alpaca-prod.default:8080
.....
.....
root@test-dns:/# curl http://bandicoot-prod.default.svc.cluster.local:8080
....
....
以上のように、クラスタ内から {Service オブジェクト名}
を使えば、デフォルトで使用可能になっている DNS サーバが名前解決して、対応する Service オブジェクトの ClusterIP アドレスを返してくれる。名前空間が異なる場合は、{Service オブジェクト名}.{名前空間名}.svc.cluster.local}
を使う。
Deployment
リソースオブジェクト、Pod
リソースオブジェクトを再度確認。
$ kubectl get deployment -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
alpaca-prod 3/3 3 3 20h kuard-amd64 gcr.io/kuar-demo/kuard-amd64:1 app=alpaca,env=prod,ver=1
bandicoot-prod 2/2 2 2 20h kuard-amd64 gcr.io/kuar-demo/kuard-amd64:2 app=bandicoot,env=prod,ver=2
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
alpaca-prod-7b7cc74d7-9x25n 1/1 Running 0 20h 10.1.0.69 docker-desktop <none> <none>
alpaca-prod-7b7cc74d7-th88q 1/1 Running 0 20h 10.1.0.71 docker-desktop <none> <none>
alpaca-prod-7b7cc74d7-wfxkz 1/1 Running 0 20h 10.1.0.70 docker-desktop <none> <none>
bandicoot-prod-849677dcb8-6qrpg 1/1 Running 0 20h 10.1.0.74 docker-desktop <none> <none>
bandicoot-prod-849677dcb8-fhdl8 1/1 Running 0 20h 10.1.0.75 docker-desktop <none> <none>
alpaca-prod Deployment オブジェクトには3つの Pod がある。それらの Pod は、
- alpaca-prod-7b7cc74d7-9x25n
- alpaca-prod-7b7cc74d7-th88q
- alpaca-prod-7b7cc74d7-wfxkz
であり、それらのクラスタ上での IP もわかる。alpaca-prod.yml マニフェストファイルで定義したように、alpaca-prod Deployment オブジェクトにセットしたラベルと同じラベル
- app=alpaca
- env=prod
- ver=1
が、これら3つの Pod にもついている。
Service
オブジェクトを見ると、
$ kubectl get services -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
alpaca-prod ClusterIP 10.96.233.137 <none> 8080/TCP 20h app=alpaca,env=prod,ver=1
bandicoot-prod ClusterIP 10.99.154.163 <none> 8080/TCP 20h app=bandicoot,env=prod,ver=2
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 47d <none>
である。alpaca-prod, bandicoot-prod Service は、alpaca-prod Deployment オブジェクトから kubectl expose
コマンドにより、作成したのだった。この中の alpaca-prod Service オブジェクトの詳細を確認してみる。
$ kubectl describe service alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
env=prod
ver=1
Annotations: <none>
Selector: app=alpaca,env=prod,ver=1
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.233.137 # Service の ClusterIP
IPs: 10.96.233.137
Port: <unset> 8080/TCP # Service の対象となるポート
TargetPort: 8080/TCP # 対象となるポートを結びつける Endpoint のポート
Endpoints: 10.1.0.69:8080,10.1.0.70:8080,10.1.0.71:8080 # Selector のラベルで選択された Pod が Endpoint になる。これらは、alpaca-prod Deployment オブジェクト中の Pod だ。
Session Affinity: None
Events: <none>
上の出力で確認できる Endpoint を見てみる。
$ kubectl get endpoints alpaca-prod
NAME ENDPOINTS AGE
alpaca-prod 10.1.0.69:8080,10.1.0.70:8080,10.1.0.71:8080 20h
Kubernetes 上のヘルスチェックの Readiness Probe で失敗回数が閾値に達すると、その Pod の該当ポートに対応する Endpoint が Service
から削除され、復活すると、元に戻るというわけだ。
alpaca-prod Service オブジェクトの Type は、ClusterIP なので、クラスタ内からしかアクセスできない。クラスタ外からアクセス可能にするには、 NotePort にすればいい。
$ kubectl edit service alpaca-prod
として、動的に type を NodePort に変更する。
$ kubectl get service alpaca-prod -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
alpaca-prod NodePort 10.96.233.137 <none> 8080:30533/TCP 20h app=alpaca,env=prod,ver=1
$ kubectl describe service alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
env=prod
ver=1
Annotations: <none>
Selector: app=alpaca,env=prod,ver=1
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.233.137
IPs: 10.96.233.137
LoadBalancer Ingress: localhost
Port: <unset> 8080/TCP
TargetPort: 8080/TCP
NodePort: <unset> 30533/TCP # ノードの Port は 30533/TCP が割り当てられた。
Endpoints: 10.1.0.69:8080,10.1.0.70:8080,10.1.0.71:8080
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
これで、K8S クラスタ上の各ノードの 30533/TCP へのパケットが、alpaca-prod Service オブジェクトの 8080 ポートへと向かうようになる。 K8S クラスタ外であっても、クラスタと同じネットワークにあるホストからなら、どのノードの 30533/TCP へつなぐこともできる。また、クラスタの前に LoadBalancer を置いてそれと連携させることもできる。Docker for Mac のローカルな K8S クラスタを使っているなら、localhost の 30533 へ接続できるので、
でいい。
ローカル K8S で動かしているのだが、Service の type を LoadBalancer に変更してみる。
$ kubectl edit service alpaca-prod
で type を LoadBalancer
に変更する。
$ kubectl get service alpaca-prod -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
alpaca-prod LoadBalancer 10.96.233.137 localhost 8080:30533/TCP 20h app=alpaca,env=prod,ver=1
EXTERNAL-IP のとこが None から localhost に変わった。
$ kubectl describe service alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
env=prod
ver=1
Annotations: <none>
Selector: app=alpaca,env=prod,ver=1
Type: LoadBalancer
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.233.137
IPs: 10.96.233.137
LoadBalancer Ingress: localhost
Port: <unset> 8080/TCP
TargetPort: 8080/TCP
NodePort: <unset> 30533/TCP
Endpoints: 10.1.0.69:8080,10.1.0.70:8080,10.1.0.71:8080
Session Affinity: None
External Traffic Policy: Cluster
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Type 9m20s service-controller NodePort -> LoadBalancer
これらの情報から推察するに、
- K8S クラスタの外に LoadBalancer が作成され、それに外部からアクセスできる IP アドレスが EXTERNAL-IP だ。
ローカル K8S なので、EXTERNAL-IP が localhost になっているが、パブリッククラウドだと、そのプロバイダが提供する LoadBalancer サービスが使われ、その LoadBalancer に与えられた IP アドレスになるはず。
でアクセスできる。NodePort の 30553 ではない。NodePort では接続できない。
GKE 上にクラスタを作成し、そこでやってみる。
自分のアカウントで作成済みのプロジェクト内に my-cluster という K8S クラスタを作成。
gcloud
コマンドで、対象とするプロジェクトをセットし、my-cluster の情報を kubectl の設定情報へと
セットする。そして
$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
docker-desktop docker-desktop docker-desktop
* gke_todo-kube-343008_us-central1-c_my-cluster gke_todo-kube-343008_us-central1-c_my-cluster gke_todo-kube-343008_us-central1-c_my-cluster
としてカレントコンテキストが my-cluster になっていることを確認。
$ kubectl apply -f alpaca-prod.yml
deployment.apps/alpaca-prod created
$ kubectl apply -f bandicoot-prod.yml
deployment.apps/bandicoot-prod created
$ kubectl expose deployment alpaca-prod
service/alpaca-prod exposed
$ kubectl expose deployment bandicoot-prod
service/bandicoot-prod exposed
$ kubectl describe service alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
env=prod
ver=1
Annotations: cloud.google.com/neg: {"ingress":true}
Selector: app=alpaca,env=prod,ver=1
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.80.0.131
IPs: 10.80.0.131
Port: <unset> 8080/TCP
TargetPort: 8080/TCP
Endpoints: 10.76.0.4:8080,10.76.1.4:8080,10.76.2.8:8080
Session Affinity: None
Events: <none>
type を NodePort
に変更。
$ kubectl edit service alpaca-prod
$ kubectl get service alpaca-prod -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
alpaca-prod NodePort 10.80.0.131 <none> 8080:30757/TCP 4m26s app=alpaca,env=prod,ver=1
$ kubectl describe service alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
env=prod
ver=1
Annotations: cloud.google.com/neg: {"ingress":true}
Selector: app=alpaca,env=prod,ver=1
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.80.0.131
IPs: 10.80.0.131
Port: <unset> 8080/TCP
TargetPort: 8080/TCP
NodePort: <unset> 30757/TCP
Endpoints: 10.76.0.4:8080,10.76.1.4:8080,10.76.2.8:8080
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
各ノードの 30757/TCP へ接続すれば alpaca-prod Service オブジェクトの 8080 ポートへとつながるようになった。 だが、GKE の各ノード(3つある)の外部アドレスは知らないし、同じネットワーク上のマシンもないので無意味。
そこで、type を LoadBalancer に変えてみよう。
$ kubectl edit service alpaca-prod
を実行して変更する。次に確認する。
$ kubectl get service alpaca-prod -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
alpaca-prod LoadBalancer 10.80.0.131 <pending> 8080:30757/TCP 9m26s app=alpaca,env=prod,ver=1
$ kubectl get service alpaca-prod -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
alpaca-prod LoadBalancer 10.80.0.131 34.71.24.45 8080:30757/TCP 10m app=alpaca,env=prod,ver=1
EXTERNAL-IP が割り当てられた!
$ kubectl describe service alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
env=prod
ver=1
Annotations: cloud.google.com/neg: {"ingress":true}
Selector: app=alpaca,env=prod,ver=1
Type: LoadBalancer
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.80.0.131
IPs: 10.80.0.131
LoadBalancer Ingress: 34.71.24.45
Port: <unset> 8080/TCP
TargetPort: 8080/TCP
NodePort: <unset> 30757/TCP
Endpoints: 10.76.0.4:8080,10.76.1.4:8080,10.76.2.8:8080
Session Affinity: None
External Traffic Policy: Cluster
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Type 106s service-controller NodePort -> LoadBalancer
Normal EnsuringLoadBalancer 106s service-controller Ensuring load balancer
Normal EnsuredLoadBalancer 67s service-controller Ensured load balancer
でアクセスできた。type を LoadBalance に変えたのに、NodePort が 30757 になっている。 おそらく、LoadBalancer からノードのいずれかの 30757/TCP へとリクエストが転送され、そこから alpaca-prod の Endpoints のいずれかにいくはず。