🌟

kindとMulticluster-SchedulerでつくるマルチK8sクラスター

2020/11/21に公開

※本記事はブログからの移行記事のため、一部内容が古くなっている場合があります(また時間があるときに改稿するかもしれません)


つい先日kind(Kubernetes in Docker)のv0.8.0がリリースされました。
以下は一例ですが、今バージョンでいくつかの機能追加や改修が行われています。

  • 「kind」という名前のユーザー定義ネットワークがデフォルトでアタッチ
  • Kubernetes 1.18対応
  • Podmanの試験的サポート開始[1]
  • kind delete clusters --allの導入でクラスター整理が楽に

個人的には特に最初の

  • 「kind」という名前のユーザー定義ネットワークがデフォルトでアタッチ

という点に注目しました。
理由としては、元々デフォルトのDockerのブリッジネットワークを利用している場合、
以下のようにノード間で名前解決はできなかったため、kindクラスター間を跨いだ検証が面倒だったためです。

# コンテナ名で名前解決できない
root@kind-control-plane:/# ping -c 1 kind2-control-plane
ping: kind2-control-plane: Name or service not known

# ネットワーク疎通自体は取れる
root@kind-control-plane:/# ping -c 1 172.17.0.3
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.221 ms

--- 172.17.0.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.221/0.221/0.221/0.000 ms

その点はユーザー定義ネットワークを利用すれば、Dockerエンジン内蔵のDNSサーバーによってディスカバリできるようになるのですが、
v0.8.0未満のkindに対応する機能がないのがネックでした[2]

今バージョンからその点は先述のように改善されているので、いちいちdocker inspectしてIPを確認しなくても、比較的楽にノードやクラスターを横断した検証ができるようになります。
よって、今回はぼんやり触りたいと思っていたMulticluster-Schedulerを絡めて、ローカル環境でマルチクラスター構築をしていきます。

Multicluster-Scheduler

Multicluster-Schedulerとは、Virtual Kubeletを応用したOSSのマルチクラスター用スケジューラーです。

https://github.com/admiraltyio/multicluster-scheduler

こちらを利用すると、Source ClusterにデプロイしたPodが
Target Clusterの各ノードにもスケジュールされるようになるので、クラスターレベルの高い冗長性を得ることができます。
公式にもありますが、導入の大まかな流れは以下となります。

  • Source Cluster(デプロイ先のクラスター)、Target Cluster(実際にPodが稼働するクラスター)の両方にMulticluster-Schedulerをインストール
  • Podに専用のアノテーションを付与しつつデプロイ
    • multicluster.admiralty.io/elect=""
  • アノテーションが付与されたPodは状態管理用のダミーPodにMutateされ、実際に稼働するPodはTarget Clusterに作成される
  • 稼働した後はFeedback Loopによって定期的に監視され、稼働中のPodのステータスをダミーPodに反映し続ける

ドキュメントはまだ少ないのですが、詳細は以下の資料が参考になります。

  • Argo Workflowとの応用例

https://itnext.io/multicluster-scheduler-argo-workflows-across-kubernetes-clusters-ea98016499ca

  • シーケンス図

https://github.com/admiraltyio/multicluster-scheduler/blob/master/docs/multicluster-scheduler-sequence-diagram.svg

今回のゴール

こんな感じの構成をkindとMulticluster-Schedulerを使って作っていきます。
なお、図中のProxy Pod、Delegate Podに関しては後述します。

検証

今回説明しないこと

  • クラスターを横断したトラフィックのルーティング
    • Cilium CNIのCluster Meshをkind上で実現する手順については、今現在以下のIssueで進行中なので、このドキュメントがリリースされ次第試していきます

https://github.com/cilium/cilium/pull/11157

環境

  • MacOS: Catalina
  • kind: v0.8.1
  • Kubernetes: v1.18.2
  • Multicluster-Scheduler: v0.8.0
  • Helm: v3.0.3
    • Multicluster-Schedulerの推奨はv3

構築の流れ

  • kindで二系統のKubernetesクラスターを構築

c1,c2という名前でそれぞれ構築します。

$ kind create cluster --name c1
Creating cluster "c1" ...
 ✓ Ensuring node image (kindest/node:v1.18.2) 🖼
 ✓ Preparing nodes 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
Set kubectl context to "kind-c1"
You can now use your cluster with:

kubectl cluster-info --context kind-c1

Thanks for using kind! 😊

$ kind create cluster --name c2
Creating cluster "c2" ...
 ✓ Ensuring node image (kindest/node:v1.18.2) 🖼
 ✓ Preparing nodes 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
Set kubectl context to "kind-c2"
You can now use your cluster with:

kubectl cluster-info --context kind-c2

Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
  • クラスターのコンテキスト名をシェル変数に定義
$ CLUSTER1=kind-c1
$ CLUSTER2=kind-c2
  • cert-manager(=>v0.11)をインストール

Multicluster-Schedulerの要件となっているので、淡々とHelmでインストールします。

$ helm repo add jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
 $ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "flagger" chart repository
...Successfully got an update from the "admiralty" chart repository
...Successfully got an update from the "jetstack" chart repository
Update Complete. ⎈ Happy Helming!⎈ 
$ for CONTEXT in $CLUSTER1 $CLUSTER2
> do
>   kubectl --context $CONTEXT apply --validate=false -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.12/deploy/manifests/00-crds.yaml
>   kubectl --context $CONTEXT create namespace cert-manager
>   helm install cert-manager \
>     --kube-context $CONTEXT \
>     --namespace cert-manager \
>     --version v0.12.0 \
>     --wait \
>     jetstack/cert-manager
> done
customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created
namespace/cert-manager created
NAME: cert-manager
LAST DEPLOYED: Sun May  3 22:32:36 2020
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager has been deployed successfully!

In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).

More information on the different types of issuers and how to configure them
can be found in our documentation:

https://docs.cert-manager.io/en/latest/reference/issuers.html

For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:

https://docs.cert-manager.io/en/latest/reference/ingress-shim.html
customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created
namespace/cert-manager created
NAME: cert-manager
LAST DEPLOYED: Sun May  3 22:33:17 2020
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager has been deployed successfully!

In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).

More information on the different types of issuers and how to configure them
can be found in our documentation:

https://docs.cert-manager.io/en/latest/reference/issuers.html

For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:

https://docs.cert-manager.io/en/latest/reference/ingress-shim.html
  • Multicluster-Schedulerをインストール
$ helm repo add admiralty https://charts.admiralty.io
"admiralty" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "admiralty" chart repository
...Successfully got an update from the "flagger" chart repository
...Successfully got an update from the "jetstack" chart repository
Update Complete. ⎈ Happy Helming!⎈ 

$ kubectl --context "$CLUSTER1" create namespace admiralty
namespace/admiralty created

$ helm install multicluster-scheduler admiralty/multicluster-scheduler \
>   --kube-context "$CLUSTER1" \
>   --namespace admiralty \
>   --version 0.8.0 \
>   --set clusterName=c1 \
>   --set targetSelf=true \
>   --set targets[0].name=c2
NAME: multicluster-scheduler
LAST DEPLOYED: Sun May  3 22:37:49 2020
NAMESPACE: admiralty
STATUS: deployed
REVISION: 1
TEST SUITE: None

$ kubectl --context "$CLUSTER2" create namespace admiralty
namespace/admiralty created
$ helm install multicluster-scheduler admiralty/multicluster-scheduler \
>   --kube-context "$CLUSTER2" \
>   --namespace admiralty \
>   --version 0.8.0 \
>   --set clusterName=c2
NAME: multicluster-scheduler
LAST DEPLOYED: Sun May  3 22:37:53 2020
NAMESPACE: admiralty
STATUS: deployed
REVISION: 1
TEST SUITE: None
  • サービスアカウントの作成

Source ClusterからTarget Clusterに接続する際に使用するサービスアカウントを作成します。
普通に作成しても良いのですが、今回は公式ドキュメントにもあるklum(Kubernetes Lazy User Manager)で作成していきます。
本旨から少し外れますが、klumを使うと、(Cluster)RoleBindingの設定やサービスアカウントに対応したkubeconfigのダウンロードなどを通常よりも非常に楽に行うことができます。

[https://github.com/ibuildthecloud/klum:embed:cite]

# klumインストール
$ kubectl --context "$CLUSTER2" apply -f https://raw.githubusercontent.com/ibuildthecloud/klum/v0.0.1/deploy.yaml
namespace/klum created
deployment.apps/klum created
serviceaccount/klum created
clusterrolebinding.rbac.authorization.k8s.io/klum-cluster-admin created
 
# ユーザー作成
$ cat <<EOF | kubectl --context "$CLUSTER2" apply -f -
> kind: User
> apiVersion: klum.cattle.io/v1alpha1
> metadata:
>   name: c1
> spec:
>   clusterRoles:
>     - multicluster-scheduler-source
> EOF
user.klum.cattle.io/c1 created

# 確認
$ kubectl --context "$CLUSTER2" get sa -A|grep c1
klum                 c1                                       1         9m46s
$ kubectl --context "$CLUSTER2" get clusterrolebinding -A|grep c1
klum-c1-multicluster-scheduler-source-406318a4         ClusterRole/multicluster-scheduler-source                                          9m57s
  • kubemcsaのインストール

kubemcsaとは、Admiraltyが公開している別のOSSのMulticluster-Service-Account用のCLIツールです。
今回はMulticluster-Service-Accountには触れませんが、kubemcsaを利用すると、先ほどklumで作成したサービスアカウントへの接続情報を容易にSecretに書き出すことができます。

$ MCSA_RELEASE_URL=https://github.com/admiraltyio/multicluster-service-account/releases/download/v0.6.1
# linux,armの時は変える
$ OS=darwin
$ ARCH=amd64
$ curl -Lo kubemcsa "$MCSA_RELEASE_URL/kubemcsa-$OS-$ARCH"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   634  100   634    0     0   1209      0 --:--:-- --:--:-- --:--:--  1207
100 33.5M  100 33.5M    0     0  1356k      0  0:00:25  0:00:25 --:--:-- 4851k
$ chmod +x kubemcsa
$ mv kubemcsa /usr/local/bin/

# 確認
$ kubemcsa --version
0.6.1
  • サービスアカウントのエクスポート

対向クラスターから見た時はもう一方への接続情報となるので、
Target Cluster(c2)から抜いたSecretを--as c2しつつ保存します。

$ kubemcsa export --context "$CLUSTER2" -n klum c1 --as c2 >> c2.yaml
  • エクスポートしたSecretを編集

通常のクラスターであればエクスポートしたSecretをそのままSource Cluster(c1)にapplyすればOKですが、
kindの場合だとアクセス元がDockerホストかDockerネットワーク内の別コンテナからなのかで疎通可能なkube-apiserverのエンドポイントが異なる[3]ので、
編集する必要があります。

具体的にはsecret.data.configとsecret.data.serverの2つを編集していきます。

# secret.data.config
$ echo '${元々のsecret.data.config}'|base64 -d >> c2-kubeconfig.yaml
c2-kubeconfig.yamlのconfig.clusters.cluster.serverを編集
- server: https://127.0.0.1:xxxxx
+ server: https://c2-control-plane:6443
$ cat c2-kubeconfig.yaml |base64
エンコードした結果をc2.yamlのsecret.data.configにペースト

# secret.data.server
$ echo 'https://c2-control-plane:6443'|base64
エンコードした結果をc2.yamlのsecret.data.serverにペースト
  • c2.yamlの編集が終わったらSource Cluster(c1)にapply
$ kubectl --context "$CLUSTER1" -n admiralty apply -f c2.yaml
secret/c2 created
  • Multicluster-Schedulerの各コンポーネントを再起動

Secretを読み直します。

$ kubectl --context "$CLUSTER1" -n admiralty rollout restart deploy/multicluster-scheduler-candidate-scheduler deploy/multicluster-scheduler-controller-manager deploy/multicluster-scheduler-proxy-scheduler
deployment.apps/multicluster-scheduler-candidate-scheduler restarted
deployment.apps/multicluster-scheduler-controller-manager restarted
deployment.apps/multicluster-scheduler-proxy-scheduler restarted

# Runningするまで待機
$ kubectl --context "$CLUSTER1" -n admiralty get po -w
NAME                                                         READY   STATUS    RESTARTS   AGE
multicluster-scheduler-candidate-scheduler-dbb5bbbcc-pt4dx   1/1     Running   0          10s
multicluster-scheduler-controller-manager-665dcdb7cb-b8zvq   1/1     Running   0          9s
multicluster-scheduler-proxy-scheduler-6d95bcbbc8-btmhn      1/1     Running   0          9s
  • Virtual Kubelet確認

以下のようにadmiralty-c1とc2がReadyしていればOKです。

$ kubectl --context "$CLUSTER1" get node
NAME               STATUS   ROLES    AGE    VERSION
admiralty-c1       Ready    agent    94s    
admiralty-c2       Ready    agent    94s    
c1-control-plane   Ready    master   155m   v1.18.2

動作確認

  • default namespaceでMulticluster-Schedulerを有効化
$ kubectl --context "$CLUSTER1" label namespace default multicluster-scheduler=enabled
namespace/default labeled
$ kubectl --context "$CLUSTER1" get ns --show-labels
NAME                 STATUS   AGE    LABELS
admiralty            Active   137m   <none>
cert-manager         Active   142m   <none>
default              Active   159m   multicluster-scheduler=enabled
kube-node-lease      Active   159m   <none>
kube-public          Active   159m   <none>
kube-system          Active   159m   <none>
local-path-storage   Active   159m   <none>
  • アノテーションを付与したNginxをデプロイ
$ cat <<EOF | kubectl --context "$CLUSTER1" apply -f -
> apiVersion: apps/v1
> kind: Deployment
> metadata:
>   name: nginx
> spec:
>   replicas: 10
>   selector:
>     matchLabels:
>       app: nginx
>   template:
>     metadata:
>       labels:
>         app: nginx
>       annotations:
>         multicluster.admiralty.io/elect: ""
>     spec:
>       containers:
>       - name: nginx
>         image: nginx
>         resources:
>           requests:
>             cpu: 100m
>             memory: 32Mi
>         ports:
>         - containerPort: 80
> EOF
deployment.apps/nginx created
  • 確認
$ kubectl --context "$CLUSTER1" get pods -o wide 
NAME                           READY   STATUS    RESTARTS   AGE   IP            NODE               NOMINATED NODE   READINESS GATES
nginx-5466f8ddf7-4knlk         1/1     Running   0          52s   10.244.0.15   admiralty-c1       <none>           <none>
nginx-5466f8ddf7-4knlk-c7jxn   1/1     Running   0          50s   10.244.0.15   c1-control-plane   <none>           <none>
nginx-5466f8ddf7-4m589         1/1     Running   0          52s   10.244.0.18   admiralty-c1       <none>           <none>
nginx-5466f8ddf7-4m589-vn2l2   1/1     Running   0          44s   10.244.0.18   c1-control-plane   <none>           <none>
nginx-5466f8ddf7-8wwhb         1/1     Running   0          52s   10.244.0.12   admiralty-c2       <none>           <none>
nginx-5466f8ddf7-d29d2         1/1     Running   0          52s   10.244.0.17   admiralty-c1       <none>           <none>
nginx-5466f8ddf7-d29d2-rtxtc   1/1     Running   0          47s   10.244.0.17   c1-control-plane   <none>           <none>
nginx-5466f8ddf7-gp2gl         1/1     Running   0          52s   10.244.0.16   admiralty-c2       <none>           <none>
nginx-5466f8ddf7-lwj7k         1/1     Running   0          52s   10.244.0.14   admiralty-c2       <none>           <none>
nginx-5466f8ddf7-msnc7         1/1     Running   0          52s   10.244.0.16   admiralty-c1       <none>           <none>
nginx-5466f8ddf7-msnc7-6qb8p   1/1     Running   0          49s   10.244.0.16   c1-control-plane   <none>           <none>
nginx-5466f8ddf7-pc7qk         1/1     Running   0          52s   10.244.0.19   admiralty-c1       <none>           <none>
nginx-5466f8ddf7-pc7qk-nksc2   1/1     Running   0          42s   10.244.0.19   c1-control-plane   <none>           <none>
nginx-5466f8ddf7-vqsgk         1/1     Running   0          52s   10.244.0.15   admiralty-c2       <none>           <none>
nginx-5466f8ddf7-xrtvt         1/1     Running   0          52s   10.244.0.13   admiralty-c2       <none>           <none>

$ kubectl --context "$CLUSTER2" get pods -o wide
NAME                           READY   STATUS    RESTARTS   AGE   IP            NODE               NOMINATED NODE   READINESS GATES
nginx-5466f8ddf7-8wwhb-6xrhs   1/1     Running   0          70s   10.244.0.12   c2-control-plane   <none>           <none>
nginx-5466f8ddf7-gp2gl-2fmzb   1/1     Running   0          59s   10.244.0.16   c2-control-plane   <none>           <none>
nginx-5466f8ddf7-lwj7k-5xx5b   1/1     Running   0          65s   10.244.0.14   c2-control-plane   <none>           <none>
nginx-5466f8ddf7-vqsgk-kbhdf   1/1     Running   0          64s   10.244.0.15   c2-control-plane   <none>           <none>
nginx-5466f8ddf7-xrtvt-r8zmk   1/1     Running   0          67s   10.244.0.13   c2-control-plane   <none>           <none>

上手く設定されていれば、上記のように計20個のPodが2つのクラスター上でRunningします。
ここで、replicasは10なのに何故20個?と思った方もいるかと思いますが、半分はProxy Pod、もう半分はDelegate Podです。
Proxy Podとはプレースホルダー的な状態管理用のPodのことで、Delegate Podとは実際に稼働するPodのことを指します。
例えば以下のように名前が似ているPodがそれぞれのクラスターで動作していますが、
この名前が短く、クラスターと対応したVirtual NodeにスケジュールされているのがProxy Podであり、
実Nodeにスケジュールされ、Suffixが付与されているのがDelegate Podです。

nginx-5466f8ddf7-vqsgk         1/1     Running   0          52s   10.244.0.15   admiralty-c2       <none>           <none>
nginx-5466f8ddf7-vqsgk-kbhdf   1/1     Running   0          64s   10.244.0.15   c2-control-plane   <none>           <none>

このように、各Targetクラスターを抽象化した仮想ノードにダミーPodをスケジュールさせ、対応したTargetクラスターに実Podの作成をHookさせる形で、
マルチクラスターなスケジューリングを可能にしています。

まとめ

今回はkind上でクラスターワイドなスケジューリングを行うところまでを確認していきました。
もしリソースに余裕があれば、マルチノードのクラスターを複数系統たてて障害テストを行うと、より動きが分かって面白いかもしれません。

次回以降は複数クラスターへのトラフィックの振り分けをはじめ、
NodeSelectorやAffinity、クラスターレベルのdrainなどもまとめていく予定です。

※内容に誤りがあったらご指摘いただけたら嬉しいです

脚注
  1. kinp? ↩︎

  2. 一応docker network connectすれば行けたかもしれない(未検証)が、いちいち手動でやるのは面倒 ↩︎

  3. kindではクラスター構築を行うと、kube-apiserverからの疎通確保のため、docker run -p ${ホストの空いているhigh port}:6443相当の処理を行いますが、このエンドポイントはDockerネットワーク内の別コンテナからでは疎通が通りません。 ↩︎

Discussion