Skip to main content
Rapsberry Pi

冬休みの自由研究: EdgeX Foundry (7) Kubernetes 上で Fuji リリースを動かす

EdgeX Foundry 関連エントリ

EdgeX Foundry と Kubernetes

これまでの EdgeX Foundry 関連エントリでは、一貫してその動作を Docker Compose に任せていました。公式でも Docker Compose や Snaps を利用して動作させる手順が紹介されています。

が、最近よく Kubernetes(や OpenShift)を触っていることもあり、コンテナなら Kubernetes でも動かせるよね! という気持ちになったので、やってみました。

なお、現段階では、Kubernetes 上で動作させるためのマニフェストファイルは、公式には用意されていません。また、そもそも EdgeX Foundry は HA 構成を明確にはサポートしておらず、実装上も考慮されていないようです。つまり、仮に Kubernetes で動作させられたとしても、レプリカを増やしてロードバランスするような状態での正常な動作は何の保証もないことになります。

というわけで、現状ではあくまで実験程度に捉えておくのがよいと思います。

今回の目標

後述しますが、EdgeX Foundry の Kubernetes 上での動作は、過去にすでに試みられているので、今回はそれらの先行実装で解決されていない以下の 2 点を解消できる実装を目標としました。

  1. 現時点での Fuji リリースを動作させられること
  2. マルチノード構成の Kubernetes で動かせること

幸いなことに、自宅の vSphere 環境では Cluster API を使ってマルチノードの Kubernetes 環境がすぐに作れるようになっている ので、この環境を使います。

できあがったマニフェスト

紆余曲折を経てひとまず動くものができたので、リポジトリ に置いてあります。

以下、使い方を簡単に紹介します。リポジトリはあらかじめクローンしておきます。

$ git clone https://github.com/kurokobo/edgex-on-kubernetes.git
$ cd edgex-on-kubernetes

Persistent Volume の用意

PV を 4 つ使うので、PV の実体となるものを用意します。今回は QNAP を NFS ストレージとして使いました。

エクスポートされた NFS 領域を適当な作業用の Linux 端末からマウントして、PV の実体となるサブディレクトリを作っておきます。

$ mount -t nfs 192.168.0.200:/k8s /mnt/k8s
$ for i in $(seq -f %03g 100); do sudo mkdir /mnt/k8s/pv$i; done
$ umount /mnt/k8s

続けて、これらの NFS 領域を Kubernetes の PersistentVolume として定義します。マニフェストの中身はこんな感じです。

$ cat persistentvolumes/nfs/pv001-persistentvolume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001
spec:
  capacity:
    storage: 512Mi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    server: 192.168.0.200
    path: /k8s/pv001

当然ですが、これはぼくの環境に合わせたものなので、それ以外の環境で動かす場合は適宜修正は必要です。今回は nfs で書いていますが、hostPath でも gcePersistentDisk でも好きなようにしてください。

逆にいえば、Deployment ではボリュームを PVC で割り当てているので、環境に合わせて編集するのは PV だけでどうにかなる気がします。

マニフェストができたら、kubectl create します。

$ ls persistentvolumes/nfs/*.yaml | xargs -n 1 kubectl create -f
persistentvolume/pv001 created
persistentvolume/pv002 created
persistentvolume/pv003 created
persistentvolume/pv004 created

$ kubectl get pv
NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
pv001   512Mi      RWX            Recycle          Available                                   22s
pv002   128Mi      RWX            Recycle          Available                                   22s
pv003   128Mi      RWX            Recycle          Available                                   22s
pv004   128Mi      RWX            Recycle          Available                                   22s

Persistent Volume Claim の用意

PV ができたら、PVC を作ります。

$ ls persistentvolumeclaims/*.yaml | xargs -n 1 kubectl create -f
persistentvolumeclaim/consul-config created
persistentvolumeclaim/consul-data created
persistentvolumeclaim/db-data created
persistentvolumeclaim/log-data created

$ kubectl get pv,pvc
NAME                     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS   REASON   AGE
persistentvolume/pv001   512Mi      RWX            Recycle          Bound    default/db-data                                 11m
persistentvolume/pv002   128Mi      RWX            Recycle          Bound    default/consul-data                             11m
persistentvolume/pv003   128Mi      RWX            Recycle          Bound    default/log-data                                11m
persistentvolume/pv004   128Mi      RWX            Recycle          Bound    default/consul-config                           11m

NAME                                  STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/consul-config   Bound    pv004    128Mi      RWX                           8s
persistentvolumeclaim/consul-data     Bound    pv002    128Mi      RWX                           8s
persistentvolumeclaim/db-data         Bound    pv001    512Mi      RWX                           8s
persistentvolumeclaim/log-data        Bound    pv003    128Mi      RWX                           7s

PV の STATUSBound になって、PVC の VOLUME で PV が対応付けられていれば大丈夫です。

Service の作成

続けて SVC を作ります。マニフェストファイル内で、外部から触りそうなヤツだけ type: LoadBalancer を書いて外部 IP アドレスを持たせています。

$ ls services/*.yaml | xargs -n 1 kubectl create -f
service/edgex-app-service-configurable-rules created
service/edgex-core-command created
service/edgex-core-consul created
service/edgex-core-data created
service/edgex-core-metadata created
service/edgex-device-virtual created
service/edgex-mongo created
service/edgex-support-logging created
service/edgex-support-notifications created
service/edgex-support-rulesengine created
service/edgex-support-scheduler created
service/edgex-sys-mgmt-agent created
service/edgex-ui-go created

$ kubectl get svc
NAME                                   TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                          AGE
edgex-app-service-configurable-rules   ClusterIP      100.67.112.145   <none>         48100/TCP                        8s
edgex-core-command                     LoadBalancer   100.69.10.125    192.168.0.50   48082:31022/TCP                  8s
edgex-core-consul                      LoadBalancer   100.71.56.117    192.168.0.51   8400:30675/TCP,8500:32711/TCP    8s
edgex-core-data                        LoadBalancer   100.70.131.230   192.168.0.52   48080:31879/TCP,5563:31195/TCP   8s
edgex-core-metadata                    LoadBalancer   100.68.152.47    192.168.0.53   48081:30265/TCP                  8s
edgex-device-virtual                   ClusterIP      100.70.115.198   <none>         49990/TCP                        8s
edgex-mongo                            LoadBalancer   100.69.209.129   192.168.0.54   27017:30701/TCP                  7s
edgex-support-logging                  ClusterIP      100.65.29.76     <none>         48061/TCP                        7s
edgex-support-notifications            ClusterIP      100.65.127.176   <none>         48060/TCP                        7s
edgex-support-rulesengine              ClusterIP      100.64.125.224   <none>         48075/TCP                        7s
edgex-support-scheduler                ClusterIP      100.70.168.47    <none>         48085/TCP                        7s
edgex-sys-mgmt-agent                   LoadBalancer   100.68.113.150   192.168.0.55   48090:32466/TCP                  7s
edgex-ui-go                            LoadBalancer   100.64.153.22    192.168.0.56   4000:32252/TCP                   7s
kubernetes                             ClusterIP      100.64.0.1       <none>         443/TCP                          14m

外部からの触りっぷりをもうちょっときれいにしたい場合は、Ingress でも作るとよいと思います。

Deployment の作成と Consul の初期化

ここまでできたら、あとは順次マイクロサービスを起動していきます。

最初に、edgex-files を起動させて、PV 内にファイルを配置します。その後、Consul(edgex-core-consul)を起動させます。

$ kubectl create -f pods/edgex-files-pod.yaml
pod/edgex-files created

$ while [[ $(kubectl get pods edgex-files -o "jsonpath={.status.phase}") != "Succeeded" ]]; do sleep 1; done

$ kubectl create -f deployments/edgex-core-consul-deployment.yaml
deployment.apps/edgex-core-consul created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-core-consul
deployment.apps/edgex-core-consul condition met

Consul が起動したら、edgex-core-config-seed を動作させて、Consul に初期設定値を登録させます。

$ kubectl create -f pods/edgex-core-config-seed-pod.yaml
pod/edgex-core-config-seed created

$ while [[ $(kubectl get pods edgex-core-config-seed -o "jsonpath={.status.phase}") != "Succeeded" ]]; do sleep 1; done

edgex-filesedgex-core-config-seed は、それぞれ edgex-files はファイルのコピーが終わったら、edgex-core-config-seed は Consul に値を登録し終わったら不要になります。そのため、ここではこれらは Deployment リソースとしてではなく Pod リソースとして定義しています。

残りの Deployment の作成

残りのマイクロサービスを起動していきます。

$ kubectl create -f deployments/edgex-mongo-deployment.yaml
deployment.apps/edgex-mongo created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-mongo
deployment.apps/edgex-mongo condition met

$ kubectl create -f deployments/edgex-support-logging-deployment.yaml
deployment.apps/edgex-support-logging created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-support-logging
deployment.apps/edgex-support-logging condition met

$ kubectl create -f deployments/edgex-support-notifications-deployment.yaml
deployment.apps/edgex-support-notifications created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-support-notifications
deployment.apps/edgex-support-notifications condition met

$ kubectl create -f deployments/edgex-core-metadata-deployment.yaml
deployment.apps/edgex-core-metadata created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-core-metadata
deployment.apps/edgex-core-metadata condition met

$ kubectl create -f deployments/edgex-core-data-deployment.yaml
deployment.apps/edgex-core-data created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-core-data
deployment.apps/edgex-core-data condition met

$ kubectl create -f deployments/edgex-core-command-deployment.yaml
deployment.apps/edgex-core-command created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-core-command
deployment.apps/edgex-core-command condition met

$ kubectl create -f deployments/edgex-support-scheduler-deployment.yaml
deployment.apps/edgex-support-scheduler created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-support-scheduler
deployment.apps/edgex-support-scheduler condition met

$ kubectl create -f deployments/edgex-support-rulesengine-deployment.yaml
deployment.apps/edgex-support-rulesengine created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-support-rulesengine
deployment.apps/edgex-support-rulesengine condition met

$ kubectl create -f deployments/edgex-app-service-configurable-rules-deployment.yaml
deployment.apps/edgex-app-service-configurable-rules created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-app-service-configurable-rules
deployment.apps/edgex-app-service-configurable-rules condition met

$ kubectl create -f deployments/edgex-device-virtual-deployment.yaml
deployment.apps/edgex-device-virtual created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-device-virtual
deployment.apps/edgex-device-virtual condition met

$ kubectl create -f deployments/edgex-sys-mgmt-agent-deployment.yaml
deployment.apps/edgex-sys-mgmt-agent created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-sys-mgmt-agent
deployment.apps/edgex-sys-mgmt-agent condition met

$ kubectl create -f deployments/edgex-ui-go-deployment.yaml
deployment.apps/edgex-ui-go created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-ui-go
deployment.apps/edgex-ui-go condition met

完成

完成です。

$ kubectl get all,pv,pvc
NAME                                                        READY   STATUS      RESTARTS   AGE
pod/edgex-app-service-configurable-rules-6c794d754d-xj4v4   1/1     Running     0          92s
pod/edgex-core-command-cc8465b6f-kgwhm                      1/1     Running     0          98s
pod/edgex-core-config-seed                                  0/1     Completed   0          2m13s
pod/edgex-core-consul-5b8cdbbfc8-g7j6t                      1/1     Running     0          2m26s
pod/edgex-core-data-c7775c56b-t8rdf                         1/1     Running     0          100s
pod/edgex-core-metadata-66f86d86b-mfqfg                     1/1     Running     0          102s
pod/edgex-device-virtual-597f9d77f-vqlln                    1/1     Running     0          90s
pod/edgex-files                                             0/1     Completed   0          6m13s
pod/edgex-mongo-99c7d8957-fsz6c                             1/1     Running     0          110s
pod/edgex-support-logging-8566649bf8-dj8fw                  1/1     Running     0          107s
pod/edgex-support-notifications-586cf544f-n8md7             1/1     Running     0          104s
pod/edgex-support-rulesengine-6cfc49f89b-xn8fc              1/1     Running     0          94s
pod/edgex-support-scheduler-7658f985bb-9bmz2                1/1     Running     0          96s
pod/edgex-sys-mgmt-agent-5dd56c65bb-tnqth                   1/1     Running     0          88s
pod/edgex-ui-go-54668dcc94-mdx2d                            1/1     Running     0          86s

NAME                                           TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                          AGE
service/edgex-app-service-configurable-rules   ClusterIP      100.67.11.126    <none>         48100/TCP                        7m33s
service/edgex-core-command                     LoadBalancer   100.71.84.78     192.168.0.50   48082:32108/TCP                  7m33s
service/edgex-core-consul                      LoadBalancer   100.66.136.131   192.168.0.51   8400:31442/TCP,8500:31806/TCP    7m32s
service/edgex-core-data                        LoadBalancer   100.64.210.255   192.168.0.52   48080:31808/TCP,5563:30664/TCP   7m32s
service/edgex-core-metadata                    LoadBalancer   100.67.207.51    192.168.0.53   48081:32379/TCP                  7m32s
service/edgex-device-virtual                   ClusterIP      100.69.130.2     <none>         49990/TCP                        7m32s
service/edgex-mongo                            LoadBalancer   100.66.8.30      192.168.0.54   27017:31973/TCP                  7m32s
service/edgex-support-logging                  ClusterIP      100.68.171.73    <none>         48061/TCP                        7m32s
service/edgex-support-notifications            ClusterIP      100.67.242.204   <none>         48060/TCP                        7m32s
service/edgex-support-rulesengine              ClusterIP      100.67.203.199   <none>         48075/TCP                        7m32s
service/edgex-support-scheduler                ClusterIP      100.65.53.193    <none>         48085/TCP                        7m31s
service/edgex-sys-mgmt-agent                   LoadBalancer   100.65.122.248   192.168.0.55   48090:31476/TCP                  7m31s
service/edgex-ui-go                            LoadBalancer   100.70.107.81    192.168.0.56   4000:30852/TCP                   7m31s
service/kubernetes                             ClusterIP      100.64.0.1       <none>         443/TCP                          47m

NAME                                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/edgex-app-service-configurable-rules   1/1     1            1           92s
deployment.apps/edgex-core-command                     1/1     1            1           98s
deployment.apps/edgex-core-consul                      1/1     1            1           2m26s
deployment.apps/edgex-core-data                        1/1     1            1           100s
deployment.apps/edgex-core-metadata                    1/1     1            1           102s
deployment.apps/edgex-device-virtual                   1/1     1            1           90s
deployment.apps/edgex-mongo                            1/1     1            1           110s
deployment.apps/edgex-support-logging                  1/1     1            1           107s
deployment.apps/edgex-support-notifications            1/1     1            1           104s
deployment.apps/edgex-support-rulesengine              1/1     1            1           94s
deployment.apps/edgex-support-scheduler                1/1     1            1           96s
deployment.apps/edgex-sys-mgmt-agent                   1/1     1            1           88s
deployment.apps/edgex-ui-go                            1/1     1            1           86s

NAME                                                              DESIRED   CURRENT   READY   AGE
replicaset.apps/edgex-app-service-configurable-rules-6c794d754d   1         1         1       92s
replicaset.apps/edgex-core-command-cc8465b6f                      1         1         1       98s
replicaset.apps/edgex-core-consul-5b8cdbbfc8                      1         1         1       2m26s
replicaset.apps/edgex-core-data-c7775c56b                         1         1         1       100s
replicaset.apps/edgex-core-metadata-66f86d86b                     1         1         1       102s
replicaset.apps/edgex-device-virtual-597f9d77f                    1         1         1       90s
replicaset.apps/edgex-mongo-99c7d8957                             1         1         1       110s
replicaset.apps/edgex-support-logging-8566649bf8                  1         1         1       107s
replicaset.apps/edgex-support-notifications-586cf544f             1         1         1       104s
replicaset.apps/edgex-support-rulesengine-6cfc49f89b              1         1         1       94s
replicaset.apps/edgex-support-scheduler-7658f985bb                1         1         1       96s
replicaset.apps/edgex-sys-mgmt-agent-5dd56c65bb                   1         1         1       88s
replicaset.apps/edgex-ui-go-54668dcc94                            1         1         1       86s

NAME                     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS   REASON   AGE
persistentvolume/pv001   512Mi      RWX            Recycle          Bound    default/db-data                                 8m11s
persistentvolume/pv002   128Mi      RWX            Recycle          Bound    default/consul-config                           8m11s
persistentvolume/pv003   128Mi      RWX            Recycle          Bound    default/consul-data                             8m10s
persistentvolume/pv004   128Mi      RWX            Recycle          Bound    default/log-data                                8m10s

NAME                                  STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/consul-config   Bound    pv002    128Mi      RWX                           8m4s
persistentvolumeclaim/consul-data     Bound    pv003    128Mi      RWX                           8m4s
persistentvolumeclaim/db-data         Bound    pv001    512Mi      RWX                           8m4s
persistentvolumeclaim/log-data        Bound    pv004    128Mi      RWX                           8m4s

動作確認

標準で edgex-device-virtual が動いているので、edgex-core-data のログなどを除けば値が取り込まれていることが観察できます。

$ kubectl logs edgex-core-data-c7775c56b-t8rdf --tail 4
level=INFO ts=2020-03-01T07:41:47.454970177Z app=edgex-core-data source=event.go:240 msg="Putting event on message queue"
level=INFO ts=2020-03-01T07:41:47.455410868Z app=edgex-core-data source=event.go:258 msg="Event Published on message queue. Topic: events, Correlation-id: 4d142e84-f3b2-49cd-a1d4-8be7dc7e5d36 "
level=INFO ts=2020-03-01T07:41:47.47515949Z app=edgex-core-data source=event.go:240 msg="Putting event on message queue"
level=INFO ts=2020-03-01T07:41:47.47523845Z app=edgex-core-data source=event.go:258 msg="Event Published on message queue. Topic: events, Correlation-id: 6e6f7f2a-da7f-4fc7-ac1f-85e5918c31ed "

適当に MQTT トピックにエクスポートさせると、実際に値が届いていることが確認できます。エクスポート先の MQTT ブローカは、マニフェストファイルで環境変数として定義しているので、適宜修正してください。

$ docker run -d --rm --name broker -p 1883:1883 eclipse-mosquitto
598878ed584699c61b4ccb95cdaeb53cc70b3a56793b53c83db1511483dc037a

$ kubectl create -f deployments/edgex-app-service-configurable-mqtt-export-deployment.yaml
deployment.apps/edgex-app-service-configurable-mqtt-export created

$ kubectl wait --for=condition=available --timeout=60s deployment edgex-app-service-configurable-mqtt-export
deployment.apps/edgex-app-service-configurable-mqtt-export condition met

$ docker run --init --rm -it efrecon/mqtt-client sub -h 192.168.0.100 -t "#" -v
edgex-handson-topic {"id":"5a79e954-6c3f-4635-9e60-d93379a2a7ec","device":"Random-UnsignedInteger-Device","origin":1583049398041588772,"readings":[{"id":"529f5003-47b6-45ab-a743-7b4a21d937f4","origin":1583049398026423142,"device":"Random-UnsignedInteger-Device","name":"Uint8","value":"80"}]}
edgex-handson-topic {"id":"2cd0b56e-28c8-4035-9351-75028b37afef","device":"Random-UnsignedInteger-Device","origin":1583049398427444993,"readings":[{"id":"0a041b11-76f9-426c-9098-75affe58ea89","origin":1583049398412848451,"device":"Random-UnsignedInteger-Device","name":"Uint64","value":"2542183823530459689"}]}
edgex-handson-topic {"id":"00aaffb1-27ca-43ff-8150-ace2eefe2a32","device":"Random-Boolean-Device","origin":1583049398815297954,"readings":[{"id":"7ecc8518-ae56-4c78-8527-dd3442b14c6d","origin":1583049398800419572,"device":"Random-Boolean-Device","name":"Bool","value":"false"}]}
...

残されている問題

そんなわけで、とりあえずは動いていますが、

  • 起動・停止が面倒
    • マイクロサービス間に依存関係があり、起動順序に配慮が必要(依存関係自体は Docker Compose でも定義されているし起動順序も公式ドキュメントに書いてあるので無視できない)
    • Kubernetes では起動順序を制御しにくい(実装が検討されているっぽい のでそれに期待)
    • 現状では、コマンド群をシェルスクリプトにするか、InitContianers などでがんばるのが精々。Kustomize とか Helm でも細かい順序制御はしんどい気がする
  • 設定ファイルの流し込みがしにくい
    • configuration.toml などをコンテナに渡しにくい
    • PV を新しく作って InitContainers で git clone するなどの工夫が必要
  • ヘルスチェックをしていない
    • いわゆる Liveness Probe、Rediness Probe の追加をサボった
  • edgex-sys-mgmt-agent が部分的に動かないかも(未確認)
    • /var/run/docker.sock を消したが影響を確認していない

あたりに本気で使うにはハードルがありそうです。将来的には公式に参考実装やベストプラクティスが出てくるといいですね。

マニフェストの作成過程

いきなりできあがったモノを紹介しましたが、作る過程をざっくり書いておきます。

とりあえず Kompose で変換する

Docker Compose ファイルを Kubernetes 用のマニフェストに変換できる Kompose というものがあります。おもむろにこれに突っ込めば完成!

$ kompose convert -f docker-compose.yml
INFO Network edgex-network is detected at Source, shall be converted to equivalent NetworkPolicy at Destination
...
INFO Kubernetes file "app-service-rules-service.yaml" created
...
INFO Kubernetes file "edgex-network-networkpolicy.yaml" created

となるはずもなく、このままだと全然動きません。この作業で、

  • Services(*-service.yaml
  • Deployments(*-deployment.yaml
  • Persistent Volyme Claims(*-persistentvolumeclaim.yaml
  • Network Policy(*-networkpolicy.yaml

の 4 種類のファイルができあがっています。

このままだと動かないとは言いつつ、ベースとしては充分使えるので、これを編集していく形で実装を考えます。

アーキテクチャを考える

この段階で、テストだからがばがばでよしとすることにして、Network Policy のマニフェストは消しました。

残りのマニフェストで定義された構成を、以下のように考えて組み替えていきます。

  • 共有ボリュームの持たせ方
    • Docker Compose ファイルだと、ボリュームが無駄に広い範囲で共有されすぎている気がする
    • 最新の Nightly Build リリースの Docker Compose ファイルをみると、最小限に絞られていそうだ
    • Nightly Build 版での共有っぷりを参考に、今回も共有範囲は最小限に絞ることにする
  • 共有ボリュームへのファイルの配置の仕方
    • edgex-files のコンテナイメージ内に共有ボリューム上に置くべきファイルが保存されている
    • Docker Compose と同じようにマウントしてしまうと、既存のファイルがオーバライドされて見えなくなる
    • 別のパスにマウントして、commandargs で必要なファイルをコピーさせるようにする
    • 一度だけ動けばよいので、restartPolicyNever にした Pod にする
  • 設定の配り方
    • 先行実装では edgex-config-seed を消し去っていた(ローカルの設定ファイルを使わせていた)例がある
    • とはいえ EdgeX Foundry は Consul ありきでできているフシもあるので、できれば Docker Compose のときと同じようにしたい
    • 一度だけ動けばよいので、restartPolicyNever にした Pod にする
  • Docker ならではの実装の排除
    • Portainer は Docker 前提なので消すことにする
    • /var/run/docker.sock のマウントも Docker 前提なので消すことにする
  • PV の作り方
    • Deployment に直接 PV を持たせるのではなく、お作法通り PVC を使ってマウントさせるようにする

マニフェストの整形

方針が決まったら、Kompose で変換されたマニフェストファイルを実際にきれいにしていきます。

  • Services
    • metadata を削除、または修正
    • ほかのマイクロサービスから正しく名前解決できるように name を各マイクロサービスのホスト名に修正
    • selector を Deployment に合わせて修正
    • 外部からアクセスしそうなサービスに type: LoadBalancer を追加
  • Deployments
    • API バージョンを修正、併せて必須パラメータ(selector)追加
    • metadata を削除、または修正
    • Deployment と Pod の name を各マイクロサービスのホスト名に統一
    • 不要なボリュームの削除
    • edgex-core-config-seededgex-files の削除
  • Pods
    • edgex-core-config-seededgex-files を新規に追加
    • restartPolicy やボリュームの変更、commandargs の追加
  • Persistent Volume Claims
    • metadata を削除、または修正
    • accessModesReadWriteMany に変更
    • サイズを変更(値は このとき の実使用量を基に決定)
  • Persistent Volumes
    • 新規に追加

こまごまトラブルシュート

で、だいたいできたと思ったら、一部のマイクロサービスで、REST エンドポイントとして必要な HTTP サーバが起動しない状態になりました。以下のような具合で、起動しようとした直後に停止してしまっています。

$ kubectl logs edgex-support-logging-66fc4f6c98-28bk4
...
level=INFO ts=2020-02-25T12:47:50.622662811Z app=edgex-support-logging source=httpserver.go:72 msg="Web server starting (edgex-support-logging:48061)"
level=INFO ts=2020-02-25T12:47:50.624106767Z app=edgex-support-logging source=httpserver.go:80 msg="Web server stopped"
...

いろいろ調べたら、以下のような状態のようでした。

  • edgex-go リポジトリで管理されているマイクロサービスで起きる
  • HTTP サーバは、マイクロサービスの起動時に <ホスト名>:<ポート番号> で待ち受けを指示される
    • 上記のログの例だと edgex-support-logging:48061
    • 待ち受けのホスト名とポート番号は Consul から受け取る
  • HTTP サーバは、指示された <ホスト名> の名前解決を試みる
    • コンテナ内の /etc/hosts を無視して、Kubernetes による名前解決が行われる
    • 名前解決したいホスト名が Service の名前と一致しているため、Kubernetes の ClusterIP に解決される
  • Pod 内のプロセスからは ClusterIP は利用できない(Pod の IP アドレスしか認識されない)ため、待ち受けできずに死ぬ

Consul 上のホスト名を書き換えるのも、Pod と Service でホスト名を別にするのも、副作用がわからないのでできれば避けたいところです。そもそも Kubernetes の仕様上、コンテナ内の /etc/hosts には Pod の IP アドレスと Pod のホスト名が正しく書いてあるわけですから、これを参照してくれさえすればよいだけの話です。

が、Go 言語のデフォルトの名前解決の仕組みでは、/etc/nsswitch.conf が存在しない(または存在していても中で files が指定されていない)場合に、/etc/hosts は無視されるようです。この挙動の修正は 1.15 のロードマップに乗っている ものの、要するに、現時点ではそういう仕様ということになります。

そして、問題が起きているマイクロサービスのコンテナイメージは、scratchalpine などがベースになっているので、/etc/nsswitch.conf が存在しません。

というわけで、いろいろな仕様が重なった結果として、こういう問題が引き起こされています。がんばりましょう。

この解決にあたって、以下の 3 案を考えました。今回はいちばんマニフェストへの取り込みがラクな最初の案を採用しています。

  • 環境変数 GODEBUG に、値 netdns=cgo を与える
    • 名前解決に Go 標準ではなく CGO の仕組みを使うことを強制するオプション
    • CGO では /etc/nsswitch.conf が存在していなくても /etc/hosts が読み取られる
  • /etc/nsswitch.conf(中身は hosts: files dns)をコンテナ内に配置する
    • コンテナイメージを自製するか、PV や InitContainer などを使ってがんばる必要がある
  • Consul の K/V ストアの /edgex/core/1.0/*/Service/Host を全部 0.0.0.0 にする
    • Consul を起動させて、手で書き換えてから残りのサービスを起動するようにする
    • 副作用が不明

先行実装

冒頭で書いた通り、EdgeX Foundry の Kubernetes 上での動作は、すでに過去に試みられています。現状で確認できているものを簡単に紹介します。

EdgeX on Kubernetes

EdgeX Foundry のブログの 2018 年のエントリで、当時のリリースを Kubernetes で動作させる例が紹介されています。

2018 年当時なので、使われているイメージは古く、どうやら Barcelona ベースのようです。

また、共有ボリュームとして利用する PV が hostPath で定義されているので、シングルノード上での動作が前提になっているものと推測されます。

edgex-kubernetes-support

GitHub の edgexfoundry-holding 配下のリポジトリとして用意されています。比較的新しめです。

こちらは Edinburgh をベースにしたものと Nightly Build をベースにしたものがありました。Nightly Build とはいえ、半年以上前のそれなので、実際は Fuji 相当に近そうです。Helm チャートも用意されています。

K8s だけでなく K3s も対象としている点はうれしいですが、これも PV が hostPath で定義されているので、シングルノードでの動作を前提としているようです。

まとめ

EdgeX Foundry の Fuji リリースを、マルチノード構成の Kubernetes 上で動作させるためのマニフェストファイルを作成して、動作を確認しました。

Kompose は便利ですが、やはりそれなりの規模の Docker Compose ファイルだと完全に無編集でそのまま使えるというわけにはいかないですね。よい経験になりました。

EdgeX Foundry も Kubernetes も、本当に日々更新されていっているので、追従し続けるのは相当しんどそうです。このブログの EdgeX Foundry 関連エントリも、来月に予定されている次期バージョン(Geneva)のリリースですぐに時代遅れになるわけです。がんばりましょう。

EdgeX Foundry 関連エントリ


Cluster API で vSphere 上の Kubernetes クラスタを管理する

きっかけ

実験したいことが出てきてしまい、自宅で Kubernetes を触りたくなりました。

これまで Kubernetes を触る場合は Google Kubernetes Engine(GKE)ばかりを使っていたのですが、 今回実験したいのは IoT の世界でいうエッジ側の話なので、できればオンプレミス相当の Kuberentes クラスタが欲しいところです。

そんなわけで、これ幸いと自宅の vSphere 環境で Cluster API を叩いてゲストクラスタを作ることにしました。Cluster API は、2019 年に VMware から発表された VMware Tanzu や Project Pacific の実装でも使われているそうで、そういう意味でも興味のあるところです。

VMware Tanzu や Project Pacific は、エントリの本筋ではないので細かくは書きませんが、めっちゃ雑に書くと、vSphere と Kubernetes がイケてる感じにくっついたヤツです。

Cluster API とは

Cluster API は、Kubernetes っぽいお作法で Kubernetes クラスタそれ自体を管理できる仕組みです。Kubernetes の SIG のプロジェクト として開発が進められています。

最新バージョンは 2019 年 9 月にリリースされた v1alpha2 で、現在は v1alpha3 が開発中です。バージョン名を見てもわかる通り、現段階ではいわゆるアルファ版ですし、ドキュメントでも『プロトタイプだよ』『どんどん変わるからね』と記載されているので、ごりごりに使い込むのはまだちょっと待ったほうがよさそうですね。

Cluster API is still in a prototype stage while we get feedback on the API types themselves. All of the code here is to experiment with the API and demo its abilities, in order to drive more technical feedback to the API design. Because of this, all of the codebase is rapidly changing.

https://github.com/kubernetes-sigs/cluster-api/blob/master/README.md

Kubernetes クラスタを構成するノードは、多くの場合は仮想マシンです。その仮想マシンは、パブリッククラウド上だったりオンプレミスの vSphere 上や OpenStack 上だったりで動いているわけですが、Cluster API では、そうしたプラットフォームごとに Provider なる実装が用意されており、環境差異を抽象化してくれます。これにより、異なる環境でも同一の操作感で Kubernetes クラスタを管理できます。

今回は vSphere 環境上の Kubernetes クラスタの構成が目的なので、vSphere 用の Provider を使って作業します。

構成要素と構成の流れ

最終的には、いわゆる Kubernetes らしく業務や開発で様々なアプリケーションを動作させることになる Kubernetes クラスタと、それらを管理するためだけの Kubernetes クラスタ、の大きく二種類の Kubernetes クラスタができあがります。

前者がゲストクラスタ(ワークロードクラスタ)、後者がマネジメントクラスタなどと呼ばれるようです。Cluster API はこのうちのマネジメントクラスタに組み込まれており、この Cluster API によってゲストクラスタのライフサイクルを簡単に管理できるということです。

ざっくりイメージ

もう少し具体的にいえば、例えば Kubernetes クラスタ自体は cluster リソースとして、あるいはそれを構成するノードの仮想マシンは machine リソースとして扱えるようになり、通常の pod リソースや deployment リソースと同じように、自分以外の Kubernetes クラスタの構成が管理できるということです。

構築の観点では、つまりマネジメントクラスタができさえすれば Cluster API 環境はほぼ完成と言えるわけですが、実際にはマネジメントクラスタ自身もマネジメントクラスタで管理するため、手順はちょっと複雑です。

マネジメントクラスタの作り方はいくつかあるようですが、今回は vSphere 用 Provider の Getting Started の通り、以下のような流れで構成を進めます。

作業の流れのイメージ
  1. 作業用端末(図中 Workstation)に Docker や kuberctl など必要なモノを揃えて、マニフェストファイルを生成する
  2. Docker 上に作業用の Kubernetes クラスタ(ブートストラップクラスタ)を作り、Cluster API を導入する
  3. 導入した Cluster API を使って、マニフェストファイルに従って本当のマネジメントクラスタを作る
  4. マネジメントクラスタに Cluster API 環境を移行(Pivoting)して、ブートストラップクラスタを消す

最終的なマネジメントクラスタを作るためにさらに別の Kubernetes クラスタ(ブートストラップクラスタ)が必要なあたりがわりとややこしいですが、そういうものみたいです。

ここまでできたら、Cluster API の本来の利用方法の通り、マニフェストファイルに従ってゲストクラスタを作ったり消したり拡張したり縮小したりできます。

ここまでの作業はこれをやるためのただのお膳立てです

構築の準備

前述した流れの通り、マネジメントクラスタを構築するには、ブートストラップクラスタが動作できる環境が必要です。また、マネジメントクラスタが動作する vSphere 環境でも、少し準備が必要です。

作業端末の整備

作業前提を整えます。作業用の端末は、ブートストラップクラスタの動作と、それを用いたマネジメントクラスタの構築ができる必要があり、このために、

が必要です。Windows でもおそらく動くとは思いますが、今回は作業用の Ubuntu 端末を別に用意して使っています。

Docker は適当に入れます。今回の端末は Ubuntu 19.10 なので、19.04 用のバイナリを無理やり入れます。

$ sudo apt update
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu disco stable"
$ sudo apt update
$ sudo apt install docker-ce docker-ce-cli containerd.io
$ sudo usermod -aG docker ${USER}

clusterctl と Kind、kubectl はバイナリをダウンロードするだけです。

$ curl -Lo ./clusterctl https://github.com/kubernetes-sigs/cluster-api/releases/download/v0.2.9/clusterctl-darwin-amd64
$ chmod +x ./clusterctl
$ mv ./clusterctl ~/bin
$ curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.6.1/kind-$(uname)-amd64
$ chmod +x ./kind
$ mv ./kind ~/bin
$ curl -Lo ./kubectl https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
$ chmod +x ./kubectl
$ mv ./kubectl ~/bin

が、clusterctl は v1alpha2 の時点ですでに DEPRECATED 扱いでした。

しかしながら代替手段がよくわかっていないし、Cluster API のドキュメント 通りではなにやらうまく動かない(kubeconfig 用の secret ができあがらない)ので、v1alpha3 が出て情報が増えてきたらどうにかします。

あと、Kind の最新リリースは現時点で 0.7.0 ですが、それを使うと後続の作業がうまく動かなかったので、ひとつ古い 0.6.1 を指定しています。

vSphere 環境での OVA テンプレートの展開

vSphere 環境用の Provider を使って Kubernetes クラスタを作った場合、最終的にできあがる Kubernetes のノードは vSphere 環境上の仮想マシンです。

この仮想マシンの元になるテンプレートが用意されているので、これをあらかじめデプロイして、テンプレートに変換しておきます。

Ubuntu 版と CentOS 版がありますが、今回は Ubuntu 版の最新の 1.16.3 を使いました。

その他のイメージのリンクは リポジトリに一覧 されています。

vSphere 環境でのフォルダとリソースプールの作成

マネジメントクラスタとゲストクラスタを入れるフォルダ(仮想マシンインベントリのヤツ)とリソースプールを作ります。入れ物としてただあればよいので、設定は適当で大丈夫です。

マネジメントクラスタの構成

では、実際の構築を進めます。ブートストラップクラスタを作り、そこで Cluster API を動作させて、それを通じて最終的なマネジメントクラスタを作ります。

はじめに、構成に必要な環境変数を、envvars.txt にまとめて定義します。内容はそれぞれの環境に依存するので書き換えが必要です。

$ cat envvars.txt
# vCenter config/credentials
export VSPHERE_SERVER='192.168.0.201'                  # (required) The vCenter server IP or FQDN
export VSPHERE_USERNAME='administrator@vsphere.local'  # (required) The username used to access the remote vSphere endpoint
export VSPHERE_PASSWORD='my-secure-password'           # (required) The password used to access the remote vSphere endpoint

# vSphere deployment configs
export VSPHERE_DATACENTER='kuro-dc01'                                 # (required) The vSphere datacenter to deploy the management cluster on
export VSPHERE_DATASTORE='nfs-ds01'                                   # (required) The vSphere datastore to deploy the management cluster on
export VSPHERE_NETWORK='ext-vm'                                       # (required) The VM network to deploy the management cluster on
export VSPHERE_RESOURCE_POOL='k8s'                                    # (required) The vSphere resource pool for your VMs
export VSPHERE_FOLDER='k8s'                                           # (optional) The VM folder for your VMs, defaults to the root vSphere folder if not set.
export VSPHERE_TEMPLATE='template_ubuntu-1804-kube-v1.16.3'           # (required) The VM template to use for your management cluster.
export VSPHERE_DISK_GIB='50'                                          # (optional) The VM Disk size in GB, defaults to 20 if not set
export VSPHERE_NUM_CPUS='2'                                           # (optional) The # of CPUs for control plane nodes in your management cluster, defaults to 2 if not set
export VSPHERE_MEM_MIB='2048'                                         # (optional) The memory (in MiB) for control plane nodes in your management cluster, defaults to 2048 if not set
export SSH_AUTHORIZED_KEY='ssh-rsa AAAAB...6Ix0= kuro@kuro-ubuntu01'  # (optional) The public ssh authorized key on all machines in this cluster

# Kubernetes configs
export KUBERNETES_VERSION='1.16.3'     # (optional) The Kubernetes version to use, defaults to 1.16.2
export SERVICE_CIDR='100.64.0.0/13'    # (optional) The service CIDR of the management cluster, defaults to "100.64.0.0/13"
export CLUSTER_CIDR='100.96.0.0/11'    # (optional) The cluster CIDR of the management cluster, defaults to "100.96.0.0/11"
export SERVICE_DOMAIN='cluster.local'  # (optional) The k8s service domain of the management cluster, defaults to "cluster.local"

続けて、このファイルを使って、マネジメントクラスタの構成を定義したマニフェストファイル群を生成します。この作業のための専用のコンテナイメージが用意されているので、これに食べさせます。

$ docker run --rm \
  -v "$(pwd):/out" \
  -v "$(pwd)/envvars.txt":/envvars.txt:ro \
  gcr.io/cluster-api-provider-vsphere/release/manifests:latest \
  -c management-cluster
Checking 192.168.0.201 for vSphere version
Detected vSphere version 6.7.1
Generated ./out/management-cluster/addons.yaml
Generated ./out/management-cluster/cluster.yaml
Generated ./out/management-cluster/controlplane.yaml
Generated ./out/management-cluster/machinedeployment.yaml
Generated /build/examples/pre-67u3/provider-components/provider-components-cluster-api.yaml
Generated /build/examples/pre-67u3/provider-components/provider-components-kubeadm.yaml
Generated /build/examples/pre-67u3/provider-components/provider-components-vsphere.yaml
Generated ./out/management-cluster/provider-components.yaml
WARNING: ./out/management-cluster/provider-components.yaml includes vSphere credentials

これで、./out/management-cluster 配下にマニフェストファイル群が出力されました。

$ ls -l ./out/management-cluster/
total 268
-rw-r--r-- 1 kuro kuro  19656 Feb 11 08:54 addons.yaml
-rw-r--r-- 1 kuro kuro    933 Feb 11 08:54 cluster.yaml
-rw-r--r-- 1 kuro kuro   3649 Feb 11 08:54 controlplane.yaml
-rw-r--r-- 1 kuro kuro   2576 Feb 11 08:54 machinedeployment.yaml
-rw-r--r-- 1 kuro kuro 240747 Feb 11 08:54 provider-components.yaml

で、あとはこれらを clusterctl に食べさせるだけです。

$ clusterctl create cluster \
  --bootstrap-type kind \
  --bootstrap-flags name=management-cluster \
  --cluster ./out/management-cluster/cluster.yaml \
  --machines ./out/management-cluster/controlplane.yaml \
  --provider-components ./out/management-cluster/provider-components.yaml \
  --addon-components ./out/management-cluster/addons.yaml \
  --kubeconfig-out ./out/management-cluster/kubeconfig
NOTICE: clusterctl has been deprecated in v1alpha2 and will be removed in a future version.
I0211 08:55:09.414116    2332 createbootstrapcluster.go:27] Preparing bootstrap cluster
I0211 08:55:59.745617    2332 clusterdeployer.go:82] Applying Cluster API stack to bootstrap cluster
...
I0211 08:56:02.302440    2332 clusterdeployer.go:87] Provisioning target cluster via bootstrap cluster
...
I0211 08:56:02.454960    2332 applymachines.go:46] Creating machines in namespace "default"
I0211 08:59:12.512879    2332 clusterdeployer.go:105] Creating target cluster
...
I0211 08:59:13.394821    2332 clusterdeployer.go:123] Pivoting Cluster API stack to target cluster
...
I0211 08:59:56.665878    2332 clusterdeployer.go:164] Done provisioning cluster. You can now access your cluster with kubectl --kubeconfig ./out/management-cluster/kubeconfig
I0211 08:59:56.666586    2332 createbootstrapcluster.go:36] Cleaning up bootstrap cluster.

主要なログだけ抜粋していますが、これだけで、

  1. Kind を使って、作業端末の Docker 上にブートストラップクラスタを構築する
  2. ブートストラップクラスタに Cluster API を導入する
  3. その Cluster API を使って、マニフェスト通りに vSphere 環境上にマネジメントクラスタをデプロイする
    • 仮想マシンをテンプレートからデプロイする
    • 仮想マシンをクラスタのコントロールプレーンとして構成する
  4. ブートストラップクラスタ上に構成していた Cluster API の環境をマネジメントクラスタに移行する(Pivoting)

が実行され、最終的な形でマネジメントクラスタができあがります。

完成したマネジメントクラスタへの接続に必要な情報は、./out/management-cluster/kubeconfig に保存されています。kubectl の設定ファイルをこれに切り替えてコマンドを実行すると、マネジメントクラスタ自体の情報や、machine リソースとしての自分自身の存在が確認できます。

$ export KUBECONFIG="$(pwd)/out/management-cluster/kubeconfig"

$ kubectl cluster-info
Kubernetes master is running at https://192.168.0.27:6443
KubeDNS is running at https://192.168.0.27:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

$ kubectl get machines
NAME                                       PROVIDERID                                       PHASE
management-cluster-controlplane-0          vsphere://42069f1f-21ac-45be-69b9-e94702d3062b   running

vSphere 環境では、仮想マシン management-cluster-controlplane-0 の存在が確認できるはずです。

ここまででマネジメントクラスタができたので、Cluster API 環境は完成です。あとは好きなように Cluster API を使ってゲストクラスタを作ったり消したり拡張したり縮小したりできます。

ゲストクラスタの構成

実際に、Cluster API を使って、新しくゲストクラスタを構成します。

Cluster API は、Kubernetes のお作法で Kubernetes クラスタ自体が管理できるので、つまり、クラスタの構成情報も、Pod や Deployment などほかの Kubernetes リソースと同じように、マニフェストファイルで定義されます。よって、ゲストクラスタを作るには、その構成を定義したマニフェストファイルが必要です。

本来はきちんと中身を書くべきっぽいですが、マネジメントクラスタ用のマニフェストファイルを作ったのと同じ方法でゲストクラスタ用のマニフェストファイル群も作れるので、ここではそれを利用します。

$ docker run --rm \
  -v "$(pwd):/out" \
  -v "$(pwd)/envvars.txt":/envvars.txt:ro \
  gcr.io/cluster-api-provider-vsphere/release/manifests:latest \
  -c workload-cluster-1
Checking 192.168.0.201 for vSphere version
Detected vSphere version 6.7.1
Generated ./out/workload-cluster-1/addons.yaml
Generated ./out/workload-cluster-1/cluster.yaml
Generated ./out/workload-cluster-1/controlplane.yaml
Generated ./out/workload-cluster-1/machinedeployment.yaml
Generated /build/examples/pre-67u3/provider-components/provider-components-cluster-api.yaml
Generated /build/examples/pre-67u3/provider-components/provider-components-kubeadm.yaml
Generated /build/examples/pre-67u3/provider-components/provider-components-vsphere.yaml
Generated ./out/workload-cluster-1/provider-components.yaml
WARNING: ./out/workload-cluster-1/provider-components.yaml includes vSphere credentials

ゲストクラスタの定義と、コントロールプレーンの定義が、

  • ./out/workload-cluster-1/cluster.yaml
  • ./out/workload-cluster-1/controlplane.yaml

に含まれます。また、ワーカノードの定義は、

  • ./out/workload-cluster-1/machinedeployment.yaml

です。自分でマニフェストをいじる場合は、この辺をどうにかする必要があるということですね。

実際にデプロイするには、kubectl の接続先をマネジメントクラスタに切り替えてから、先の 3 つのファイルをマネジメントクラスタに突っ込みます。

$ export KUBECONFIG="$(pwd)/out/management-cluster/kubeconfig"

$ kubectl apply -f ./out/workload-cluster-1/cluster.yaml
cluster.cluster.x-k8s.io/workload-cluster-1 created
vspherecluster.infrastructure.cluster.x-k8s.io/workload-cluster-1 created

$ kubectl apply -f ./out/workload-cluster-1/controlplane.yaml
kubeadmconfig.bootstrap.cluster.x-k8s.io/workload-cluster-1-controlplane-0 created
machine.cluster.x-k8s.io/workload-cluster-1-controlplane-0 created
vspheremachine.infrastructure.cluster.x-k8s.io/workload-cluster-1-controlplane-0 created

$ kubectl apply -f ./out/workload-cluster-1/machinedeployment.yaml
kubeadmconfigtemplate.bootstrap.cluster.x-k8s.io/workload-cluster-1-md-0 created
machinedeployment.cluster.x-k8s.io/workload-cluster-1-md-0 created
vspheremachinetemplate.infrastructure.cluster.x-k8s.io/workload-cluster-1-md-0 created

この作業によって、まずコントロールプレーンがデプロイされ、続けてワーカノードがデプロイされます。vSphere 環境でも順次仮想マシンがデプロイされパワーオンされていく様子が観察できるでしょう。

デプロイが完了すると、以下のように、マネジメントクラスタが Kubernetes クラスタ自体を cluster リソースや machine リソースとして管理できている状態になります。

$ kubectl get clusters
NAME                 PHASE
management-cluster   provisioned
workload-cluster-1   provisioned

$ kubectl get machines
NAME                                       PROVIDERID                                       PHASE
management-cluster-controlplane-0          vsphere://42069f1f-21ac-45be-69b9-e94702d3062b   running
workload-cluster-1-controlplane-0          vsphere://4206f835-1e8d-3473-943f-0e2cc4b04319   running
workload-cluster-1-md-0-78469c8cf9-fr22j   vsphere://4206661b-9be8-cede-eeee-68ddbbdeb872   running
workload-cluster-1-md-0-78469c8cf9-jnqnw   vsphere://4206861f-e133-4a53-2008-3346c81ed8e3   running

ゲストクラスタへの接続情報は、secret リソースとして保持されています。これを kubeconfig として保存することで、kubectl でゲストクラスタに接続できるようになります。

$ kubectl get secrets
NAME                            TYPE                                  DATA   AGE
...
workload-cluster-1-kubeconfig   Opaque                                1      
...

$ kubectl get secret workload-cluster-1-kubeconfig -o=jsonpath='{.data.value}' | \
  { base64 -d 2>/dev/null || base64 -D; } >./out/workload-cluster-1/kubeconfig

実際に接続すれば、当たり前ですがゲストクラスタやノードの情報が確認できます。

$ export KUBECONFIG="$(pwd)/out/workload-cluster-1/kubeconfig"

$ kubectl cluster-info
Kubernetes master is running at https://192.168.0.28:6443
KubeDNS is running at https://192.168.0.28:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

$ kubectl get nodes
NAME                                       STATUS     ROLES    AGE     VERSION
workload-cluster-1-controlplane-0          NotReady   master   3m14s   v1.16.3
workload-cluster-1-md-0-78469c8cf9-fr22j   NotReady   <none>   90s     v1.16.3

表示されている通り、この段階ではノードは NotReady です。これは CNI プラグインが構成されていないからで、雑にいえば、この Kubernetes クラスタ内のコンテナネットワークに使う実装を明示する必要があるということです。

コンテナネットワークの実装には Flannel とか Calico とかいろいろありますが、ゲストクラスタ用マニフェスト群の中にある ./out/workload-cluster-1/addons.yaml で Calico を構成できるので、今回はこれを使います。

適用してしばらく待つと、ノードが Ready になり、ゲストクラスタの完成です。

$ kubectl apply -f ./out/workload-cluster-1/addons.yaml 
configmap/calico-config created
...
serviceaccount/calico-kube-controllers created

$ kubectl get nodes
NAME                                       STATUS   ROLES    AGE     VERSION
workload-cluster-1-controlplane-0          Ready    master   4m22s   v1.16.3
workload-cluster-1-md-0-78469c8cf9-fr22j   Ready    <none>   2m38s   v1.16.3

ゲストクラスタの拡張

Kubernetes って、アプリケーションのスケールアウトが kubectl scale だけでできてめっちゃラクですよね。

というのと同じノリで、ゲストクラスタもめっちゃラクにスケールアウトできるので、やってみましょう。

ノードの管理はマネジメントクラスタから行うので、接続先を切り替えて、machinedeployment をスケールさせます。

$ export KUBECONFIG="$(pwd)/out/management-cluster/kubeconfig"

$ kubectl scale md workload-cluster-1-md-0 --replicas=3
machinedeployment.cluster.x-k8s.io/workload-cluster-1-md-0 scaled

$ kubectl get machines
NAME                                       PROVIDERID                                       PHASE
management-cluster-controlplane-0          vsphere://42069f1f-21ac-45be-69b9-e94702d3062b   running
workload-cluster-1-controlplane-0          vsphere://4206f835-1e8d-3473-943f-0e2cc4b04319   running
workload-cluster-1-md-0-78469c8cf9-fr22j   vsphere://4206661b-9be8-cede-eeee-68ddbbdeb872   running
workload-cluster-1-md-0-78469c8cf9-jnqnw   vsphere://4206861f-e133-4a53-2008-3346c81ed8e3   running
workload-cluster-1-md-0-78469c8cf9-nxvht   vsphere://4206ec88-6607-1699-8051-e1447a448983   running

これだけでノードが 3 台になりました。仮想マシンも増えています。

ノードが増えた様子

簡単ですね。

ゲストクラスタのロードバランサの構成

おまけです。

vSphere が組み込みでロードバランサを持っていないから仕方ないですが、 現状、Cluster API でゲストクラスタを構成するときには、ロードバランサは構成できません。

このままだと、ゲストクラスタで kubectl expose--type=LoadBalancer しても EXTERNAL-IP が永遠に pending のままで、外部に公開できません。

GitHub でも issue があります し、めっちゃがんばると多分オンプレミス環境でも NSX-T とかでどうにかできるとは思いますが、その域に達していないので、とりあえず MetalLB を突っ込んで解決します。

MetalLB とは

ロードバランサが使えずにサービスを外部に公開できない、というのは、ベアメタル環境で Kubernetes を使うときによく遭遇する話題のようで、そういうヒト向けの仮想ロードバランサの実装です。

実際には正しい負荷分散にはならないみたいですが、ものすごく気軽に使えますし、アクセス経路の提供という意味では充分機能するので便利です。

MetalLB の構成と初期設定

公式のドキュメント に従います。インストール用のマニフェストファイルを突っ込んだあと、

$ kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.3/manifests/metallb.yaml
namespace/metallb-system created
...
deployment.apps/controller created

ドキュメントの Layer 2 Configuration の通りに設定用マニフェストファイルを作って突っ込みます。IP アドレスのレンジは任意で修正します。

$ cat metallb.yaml 
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 192.168.0.50-192.168.0.99

$ kubectl apply -f metallb.yaml 
configmap/config created

これだけです。

動作確認

Kubernetes のチュートリアルのヤツ を作って、

$ kubectl apply -f https://k8s.io/examples/service/load-balancer-example.yaml
deployment.apps/hello-world created

$ kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
hello-world-f9b447754-6rrt9   1/1     Running   0          68s
hello-world-f9b447754-b6psq   1/1     Running   0          68s
hello-world-f9b447754-ml5w8   1/1     Running   0          68s
hello-world-f9b447754-nprbn   1/1     Running   0          68s
hello-world-f9b447754-wpgv5   1/1     Running   0          68s

expose します。

$ kubectl expose deployment hello-world --type=LoadBalancer
service/hello-world exposed

$ kubectl get services
NAME          TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)          AGE
hello-world   LoadBalancer   100.69.189.221   192.168.0.50   8080:32015/TCP   6s
kubernetes    ClusterIP      100.64.0.1       <none>         443/TCP          4h55m

$ kubectl describe services hello-world
...
Endpoints:                100.113.190.67:8080,100.113.190.68:8080,100.97.109.3:8080 + 2 more...
...

EXTERNAL-IP に MetalLB で設定したレンジの IP アドレスが振られていますし、エンドポイントも 5 つです。

実際にこの IP アドレスにアクセスすると、正しく表示が返ってきます。無事に動いているようです。

$ curl http://192.168.0.50:8080/
Hello Kubernetes!

まとめ

vSphere 環境で Cluster API を使って Kubernetes クラスタを管理できる状態を整えました。

最初の作業はちょっとだけ手間ですが、いちどできてしまうとスケールも相当楽なので使いやすそうです。ロードバランサ周りが NSX-T などふくめいい感じにできるようになってくると、活用の幅も広がりそうですね。

VMware Tanzu や Project Pacific でも Kubernetes クラスタのライフサイクル管理は謳われているので、この手の作業が導入作業も含めて GUI や API 経由でポチポチ簡単にできるようになるのだろうと勝手に思っています。GA になったら触ってみます。