vclusterにCustom Controller/Admission Webhookをデプロイしてみる
vclusterを利用すると、マルチテナントKubernetes環境において一般ユーザーがCRDやOperatorをデプロイできるようになると説明されています。
しかし、Operator(Custom Controller)やAdmission Webhookをデプロイする手順のドキュメントやチュートリアルが見当たらないので、手探りで試してみます。
まずはkindでKubernetesクラスタを立てます。
$ kind version
kind v0.11.1 go1.16.4 linux/amd64
$ kind create cluster
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.21.1) 🖼
✓ Preparing nodes 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Have a nice day! 👋
公式ドキュメントの手順通りにvclusterをセットアップ。
$ curl -s -L "https://github.com/loft-sh/vcluster/releases/latest" | sed -nE 's!.*"([^"]*vcluster-linux-amd64)".*!https://github.com\1!p' | xargs -n 1 curl -L -o vcluster && chmod +x vcluster;
$ sudo mv vcluster /usr/local/bin;
$ vcluster --version
vcluster version 0.4.0
仮想クラスタを作成
$ vcluster create vcluster-1 -n host-namespace-1
[info] Creating namespace host-namespace-1
[info] execute command: helm upgrade vcluster-1 vcluster --repo https://charts.loft.sh --version 0.4.0 --kubeconfig /tmp/354278009 --namespace host-namespace-1 --install --repository-config='' --values /tmp/146627460
[done] √ Successfully created virtual cluster vcluster-1 in namespace host-namespace-1. Use 'vcluster connect vcluster-1 --namespace host-namespace-1' to access the virtual cluster
ではさっそく仮想クラスタにOperatorをデプロイと言いたいところですが、OperatorではAdmission Webhookを利用しているケースが多いため、まずはAdmission Webhookをセットアップするための手順を考えます。
仮想クラスタのAPIServerとAdmission Webhook間の通信には証明書が必要になります。
そのための証明書は、cert-managerを利用して発行するのが一般的でしょう。
CA Injectorという機能を利用すると、ValidatingWebhookConfiguration/MutatingWebhookConfigurationに発行した証明書を埋め込んでくれます。
というわけで、まずはcert-managerを仮想クラスタにデプロイします。
さて、cert-managerを仮想クラスタにデプロイする場合、cert-managerが利用するkubeconfigを仮想クラスタに向けてやる必要があります。
ソースコードを確認してみましょう。
controllerとwebhookに関しては、コマンドラインオプションで--kubeconfig
を指定してやればよさそうです。
cainjectorはctrl.GetConfigOrDie()
を利用しているので、環境変数KUBECONFIG
を指定すればよさそう。
では、そのkubeconfigはどこから持ってくればいいのでしょうか?
vclusterが生成したkubeconfigを確認してみます。
vclusterを作成したnamespaceのvc-vcluster-1
というSecretリソースにkubeconfigが入っているようです。
$ kubectl -n host-namespace-1 get secrets vc-vcluster-1 -o jsonpath={.data.config} | base64 -d
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: // 省略
server: https://localhost:8443
name: local
contexts:
// 以下省略
接続先がlocalhostになっちゃってますね。これでは仮想クラスタにつながりません。
out-kube-config-server
というオプションを指定するとよさそう。
仮想クラスタを作り直しましょう。まずは古いのを削除。
$ vcluster delete vcluster-1 -n host-namespace-1
values.yamlを用意
syncer:
extraArgs:
- --out-kube-config-server=https://vcluster-1
values.yamlを指定して仮想クラスタを作成。
$ vcluster create vcluster-1 -n host-namespace-1 --extra-values=./values.yaml
作成されたkubeconfigを再度確認。
$ kubectl -n host-namespace-1 get secrets vc-vcluster-1 -o jsonpath={.data.config} | base64 -d
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: // 省略
server: https://vcluster-1
name: local
contexts:
// 以下省略
今度は大丈夫そうですね。
作成されたkubeconfigを使って仮想クラスタに接続できるか確認してみましょう。
まずはPodを作成。
apiVersion: v1
kind: Pod
metadata:
name: kubectl
namespace: host-namespace-1
spec:
containers:
- name: kubectl
image: bitnami/kubectl:latest
command: [ "sleep", "infinity" ]
volumeMounts:
- mountPath: /etc/kubernetes
name: kubeconfig
volumes:
- name: kubeconfig
secret:
defaultMode: 420
secretName: vc-vcluster-1
kubectlを叩いてみる。
I have no name!@kubectl:/$ kubectl --kubeconfig=/etc/kubernetes/config get pod
Unable to connect to the server: x509: certificate is valid for kubernetes.default.svc.cluster.local, kubernetes.default.svc, kubernetes.default, kubernetes, localhost, not vcluster-1
どうやら証明書の作成時にaltNamesにvcluster-1が含まれてないみたいですね。
tls-san
というオプションを指定するとよさそう。
values.yamlを書き換えましょう。
syncer:
extraArgs:
- --out-kube-config-server=https://vcluster-1
- --tls-san=vcluster-1
再度仮想クラスタを作り直します。
$ vcluster delete vcluster-1 -n host-namespace-1
$ vcluster create vcluster-1 -n host-namespace-1 --extra-values=./values.yaml
もう一度、kubectlを叩いてみます。
I have no name!@kubectl:/$ kubectl --kubeconfig=/etc/kubernetes/config get pod -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-7448499f4d-mmfbz 1/1 Running 0 118s
今度は仮想クラスタにつながりました!
次にcert-managerを仮想クラスタにデプロイします。
今回はkustomizeを使って、cert-managerのマニフェストを生成してみましょう。
まずは、kustomization.yamlを用意します。
resources:
- https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml
patchesStrategicMerge:
- patch.yaml
patch.yamlは以下の通り。生成されたkubeconfigを利用するように環境変数やオプションを指定します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: cert-manager-cainjector
namespace: "cert-manager"
spec:
template:
spec:
serviceAccountName: default
containers:
- name: cert-manager
env:
- name: KUBECONFIG
value: /etc/kubernetes/config
volumeMounts:
- mountPath: /etc/kubernetes
name: kubeconfig
readOnly: true
volumes:
- name: kubeconfig
secret:
defaultMode: 420
secretName: vc-vcluster-1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cert-manager
namespace: "cert-manager"
spec:
template:
spec:
serviceAccountName: default
containers:
- name: cert-manager
args:
- --v=2
- --cluster-resource-namespace=$(POD_NAMESPACE)
- --leader-election-namespace=kube-system
- --kubeconfig=/etc/kubernetes/config
volumeMounts:
- mountPath: /etc/kubernetes
name: kubeconfig
readOnly: true
volumes:
- name: kubeconfig
secret:
defaultMode: 420
secretName: vc-vcluster-1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cert-manager-webhook
namespace: "cert-manager"
spec:
template:
spec:
serviceAccountName: default
containers:
- name: cert-manager
args:
- --v=2
- --secure-port=10250
- --dynamic-serving-ca-secret-namespace=$(POD_NAMESPACE)
- --dynamic-serving-ca-secret-name=cert-manager-webhook-ca
- --dynamic-serving-dns-names=cert-manager-webhook,cert-manager-webhook.cert-manager,cert-manager-webhook.cert-manager.svc
- --kubeconfig=/etc/kubernetes/config
volumeMounts:
- mountPath: /etc/kubernetes
name: kubeconfig
readOnly: true
volumes:
- name: kubeconfig
secret:
defaultMode: 420
secretName: vc-vcluster-1
さて仮想クラスタにcert-managerをデプロイするぞ、と思ったが
$ vcluster connect vcluster-1 -n host-namespace-1
[fatal] unexpected server in kubeconfig: https://vcluster-1
こっちはlocalhostじゃないとだめですね。うーん…
いったんkubeconfigをファイルに書き出します。
kubectl -n host-namespace-1 get secrets vc-vcluster-1 -o jsonpath={.data.config} | base64 -d > kubeconfig
server
をhttps://localhost:8443
に書き換えてしまいましょう。
そしてport-forwardしておきます。
kubectl -n host-namespace-1 port-forward svc/vcluster-1 8443:443
とりあえずこれでよさそう。
$ KUBECONFIG=$(pwd)/kubeconfig
$ export KUBECONFIG
$ kubectl get pod -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-7448499f4d-mmfbz 1/1 Running 0 18m
kustomizeで先ほど作成したkustomization.yamlを指定してマニフェストを生成し、仮想クラスタに適用します。
$ kustomize build ./cert-manager/ | kubectl apply -f -
しばらく待ってもPodが作成されないので原因を調べます。
$ kubectl describe pod -n host-namespace-1 cert-manager-6df7577f66-dd68v-x-cert-manager-x-vcluster-1
(中略)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 3m1s default-scheduler Successfully assigned host-namespace-1/cert-manager-6df7577f66-dd68v-x-cert-manager-x-vcluster-1 to kind-control-plane
Warning FailedMount 58s kubelet Unable to attach or mount volumes: unmounted volumes=[kubeconfig], unattached volumes=[kubeconfig kube-api-access-k59v5]: timed out waiting for the condition
Warning FailedMount 53s (x9 over 3m1s) kubelet MountVolume.SetUp failed for volume "kubeconfig" : secret "vc-vcluster-1-x-cert-manager-x-vcluster-1" not found
どうやら、仮想クラスタとhost-namespace-1の間でsecretリソースの名前が違うのがだめなようです。
syncするときに名前が変わってしまうんですね。
では、host-namespace-1からsecretリソースを書き出しましょう。
どちらのクラスタを触ってるのかややこしいので注意。
$ kubectl -n host-namespace-1 get secrets vc-vcluster-1 -o yaml > secret.yaml
namespaceとかを書き換えます。
apiVersion: v1
data:
config: // 省略
kind: Secret
metadata:
name: vc-vcluster-1
namespace: cert-manager
type: Opaque
今度は仮想クラスタの方に適用します。
$ kubectl apply -f secret.yaml
ホスト側でSecretリソースが作成されたことを確認。先ほど見つからなかったSecretと同じ名前ですね。
$ kubectl get secrets -n host-namespace-1
NAME TYPE DATA AGE
vc-vcluster-1-x-cert-manager-x-vcluster-1 Opaque 1 17s
Podがエラーで立ち上がらない。
$ kubectl logs -n host-namespace-1 cert-manager-cainjector-64c8557d4c-wqjnl-x-cert-mana-64da0d9912
I0917 11:15:19.970660 1 start.go:107] "starting" version="v1.5.3" revision="b26bd256f124d480fcb198e1464854f41b0d0d2c"
E0917 11:15:23.981515 1 cluster.go:160] cert-manager/controller-runtime/manager "msg"="Failed to get API Group-Resources" "error"="Get \"https://vcluster-1/api?timeout=32s\": dial tcp: lookup vcluster-1 on 10.96.132.113:53: server misbehaving"
Error: error creating manager: Get "https://vcluster-1/api?timeout=32s": dial tcp: lookup vcluster-1 on 10.96.132.113:53: server misbehaving
あ、もしかしてこれって、kubeconfigとか設定しなくてもよかったのか?
そのためにvclusterがCoreDNSを立ててくれてる!?
というわけで最初からやり直し。
$ kind create cluster
$ vcluster create vcluster-1 -n host-namespace-1
$ vcluster connect vcluster-1 -n host-namespace-1
$ export KUBECONFIG=./kubeconfig.yaml
$ kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml
余計なことしなくても普通に動いた。
vclusterよくできてますね!😌
cert-managerがデプロイできたので、いよいよ自前のCustom Controllerを動かすことができます。
今回はつくって学ぶKubebuilderのMarkdownView Controllerを動かしてみます。
仮想クラスタにMarkdownView Controllerをデプロイします。
$ git clone git@github.com:zoetrope/kubebuilder-training.git
$ cd kubebuilder-training/codes/markdown-view
$ make docker-build
$ kind load docker-image controller:latest
$ make install
$ make deploy
$ kubectl apply -f config/samples/view_v1_markdownview.yaml
DeploymentやServiceがちゃんと作られました。
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/viewer-markdownview-sample-6dcff589d6-mfpb2 1/1 Running 0 10m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.58.10 <none> 443/TCP 15h
service/viewer-markdownview-sample ClusterIP 10.96.59.255 <none> 80/TCP 10m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/viewer-markdownview-sample 1/1 1 1 10m
NAME DESIRED CURRENT READY AGE
replicaset.apps/viewer-markdownview-sample-6dcff589d6 1 1 1 10m
次に、ホスト側からこのServiceにPort Forwardして接続してみようとしたところ…
$ kubectl port-forward -n host-namespace-1 svc/viewer-markdownview-sample-x-default-x-vcluster-1 8080:80
error: cannot attach to *v1.Service: invalid service 'viewer-markdownview-sample-x-default-x-vcluster-1': Service is defined without a selector
vclusterのソースコードをみたところ、selectorはsyncしていません。
vclusterはDeploymentリソースをsyncしないので、Serviceにselectorを指定することもできないってわけですね。
まあこれは仕方ないか。
Custom Controllerが作成したサービスをロードバランサーで外部公開するケースを考えます。
(Custom ControllerがIngressやHTTPProxyなどのリソースを生成することを想定)
Ingressリソースを利用しているのであれば、vclusterがsyncしてくれるようです。
Contourなどの別のロードバランサーを利用している場合は、HTTPProxyリソースをsyncする必要がありますが、vclusterではサポートしていません。
仮想クラスタ上で作られたサービス名がviewer-markdownview-sample
だった場合、ホストのクラスタ上にはviewer-markdownview-sample-x-default-x-vcluster-1
という名前で同期されます。
このため、仮想クラスタからホストのクラスタに単純にリソースをコピーするだけでなく、サービス名の変換をおこなう必要があります。
任意のカスタムリソースをsyncする仕組みを別途用意するか、もしくはvclusterにプラグイン機構があるといいのかもしれない?