Skip to content

Instantly share code, notes, and snippets.

@fortune
Last active July 13, 2022 13:54
Show Gist options
  • Save fortune/dddbe12270d71530a3092d6dfdd2ca64 to your computer and use it in GitHub Desktop.
Save fortune/dddbe12270d71530a3092d6dfdd2ca64 to your computer and use it in GitHub Desktop.
Kubernetes クラスタ上のサービスディスカバリ

Kubernetes クラスタ上のサービスディスカバリ

Kubernetes クラスタ上にデプロイしたアプリケーションを利用する場合、最終的には、Pod 内のコンテナが Listen するポートに対してリクエストすることになる。つまり、IP アドレスで Pod を指定し、ポート番号で Pod 内のコンテナを指定するわけだが、これらを直接指定するのでは管理しにくいし、スケールもしにくくなる。

そこで、Kubernetes では Service リソースオブジェクトによって、この問題、すなわちサービスディスカバリの問題を解決する。

ここでは、O'Reilly 『入門 Kubernetes』 の7章の内容を試してみる。

Deployment リソースオブジェクトの作成

テキストに載っている 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 リソースオブジェクトの作成

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 DNS

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 へ接続できるので、

http://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 アドレスになるはず。

http://localhost:8080/

でアクセスできる。NodePort の 30553 ではない。NodePort では接続できない。

GKE で試す。

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

http://34.71.24.45:8080/

でアクセスできた。type を LoadBalance に変えたのに、NodePort が 30757 になっている。 おそらく、LoadBalancer からノードのいずれかの 30757/TCP へとリクエストが転送され、そこから alpaca-prod の Endpoints のいずれかにいくはず。

apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
creationTimestamp: "2022-03-06T12:57:10Z"
generation: 1
labels:
#app: alpaca-prod
app: alpaca
ver: "1"
env: prod
name: alpaca-prod
namespace: default
resourceVersion: "2617080"
uid: 37dd343f-7b4b-4c7c-a5bb-90e40789cba0
spec:
progressDeadlineSeconds: 600
replicas: 3
revisionHistoryLimit: 10
selector:
matchLabels:
#app: alpaca-prod
app: alpaca
ver: "1"
env: prod
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
#app: alpaca-prod
app: alpaca
ver: "1"
env: prod
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
imagePullPolicy: IfNotPresent
name: kuard-amd64
ports:
- containerPort: 8080
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status:
availableReplicas: 3
conditions:
- lastTransitionTime: "2022-03-06T12:57:11Z"
lastUpdateTime: "2022-03-06T12:57:11Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
- lastTransitionTime: "2022-03-06T12:57:10Z"
lastUpdateTime: "2022-03-06T12:57:11Z"
message: ReplicaSet "alpaca-prod-5946f5599" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
observedGeneration: 1
readyReplicas: 3
replicas: 3
updatedReplicas: 3
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
creationTimestamp: "2022-03-06T13:05:09Z"
generation: 1
labels:
#app: bandicoot-prod
app: bandicoot
ver: "2"
env: "prod"
name: bandicoot-prod
namespace: default
resourceVersion: "2617767"
uid: 360907f2-5423-40af-8dd2-9fe92cc41519
spec:
progressDeadlineSeconds: 600
replicas: 2
revisionHistoryLimit: 10
selector:
matchLabels:
#app: bandicoot-prod
app: bandicoot
ver: "2"
env: "prod"
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
#app: bandicoot-prod
app: bandicoot
ver: "2"
env: "prod"
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:2
imagePullPolicy: IfNotPresent
name: kuard-amd64
ports:
- containerPort: 8080
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status:
availableReplicas: 2
conditions:
- lastTransitionTime: "2022-03-06T13:05:16Z"
lastUpdateTime: "2022-03-06T13:05:16Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
- lastTransitionTime: "2022-03-06T13:05:09Z"
lastUpdateTime: "2022-03-06T13:05:16Z"
message: ReplicaSet "bandicoot-prod-7cc586bc77" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
observedGeneration: 1
readyReplicas: 2
replicas: 2
updatedReplicas: 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment