Closed23

vclusterにCustom Controller/Admission Webhookをデプロイしてみる

zoetrozoetro

vclusterを利用すると、マルチテナントKubernetes環境において一般ユーザーがCRDやOperatorをデプロイできるようになると説明されています。
しかし、Operator(Custom Controller)やAdmission Webhookをデプロイする手順のドキュメントやチュートリアルが見当たらないので、手探りで試してみます。

zoetrozoetro

まずは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! 👋
zoetrozoetro

公式ドキュメントの手順通りに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
zoetrozoetro

仮想クラスタを作成

$ 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
zoetrozoetro

ではさっそく仮想クラスタにOperatorをデプロイと言いたいところですが、OperatorではAdmission Webhookを利用しているケースが多いため、まずはAdmission Webhookをセットアップするための手順を考えます。

仮想クラスタのAPIServerとAdmission Webhook間の通信には証明書が必要になります。
そのための証明書は、cert-managerを利用して発行するのが一般的でしょう。
CA Injectorという機能を利用すると、ValidatingWebhookConfiguration/MutatingWebhookConfigurationに発行した証明書を埋め込んでくれます。

というわけで、まずはcert-managerを仮想クラスタにデプロイします。

zoetrozoetro

さて、cert-managerを仮想クラスタにデプロイする場合、cert-managerが利用するkubeconfigを仮想クラスタに向けてやる必要があります。
ソースコードを確認してみましょう。

controllerとwebhookに関しては、コマンドラインオプションで--kubeconfigを指定してやればよさそうです。
cainjectorはctrl.GetConfigOrDie()を利用しているので、環境変数KUBECONFIGを指定すればよさそう。

zoetrozoetro

では、その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になっちゃってますね。これでは仮想クラスタにつながりません。

zoetrozoetro

out-kube-config-serverというオプションを指定するとよさそう。
https://github.com/loft-sh/vcluster/blob/96f662e5fd7992c93fda80b9bf586ae3df2b048d/cmd/vcluster/main.go#L96

仮想クラスタを作り直しましょう。まずは古いのを削除。

$ 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
zoetrozoetro

作成された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:
// 以下省略

今度は大丈夫そうですね。

zoetrozoetro

作成された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が含まれてないみたいですね。

zoetrozoetro

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
zoetrozoetro

もう一度、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

今度は仮想クラスタにつながりました!

zoetrozoetro

次に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
zoetrozoetro

さて仮想クラスタにcert-managerをデプロイするぞ、と思ったが

$ vcluster connect vcluster-1 -n host-namespace-1
[fatal]  unexpected server in kubeconfig: https://vcluster-1

こっちはlocalhostじゃないとだめですね。うーん…

zoetrozoetro

いったんkubeconfigをファイルに書き出します。

kubectl -n host-namespace-1 get secrets vc-vcluster-1 -o jsonpath={.data.config} | base64 -d > kubeconfig

serverhttps://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
zoetrozoetro

kustomizeで先ほど作成したkustomization.yamlを指定してマニフェストを生成し、仮想クラスタに適用します。

$ kustomize build ./cert-manager/ | kubectl apply -f -
zoetrozoetro

しばらく待っても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するときに名前が変わってしまうんですね。

zoetrozoetro

では、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
zoetrozoetro

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を立ててくれてる!?

zoetrozoetro

というわけで最初からやり直し。

$ 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よくできてますね!😌

zoetrozoetro

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
zoetrozoetro

次に、ホスト側からこの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していません。
https://github.com/loft-sh/vcluster/blob/96f662e5fd7992c93fda80b9bf586ae3df2b048d/pkg/controllers/resources/services/syncer.go#L91

vclusterはDeploymentリソースをsyncしないので、Serviceにselectorを指定することもできないってわけですね。
まあこれは仕方ないか。

zoetrozoetro

Custom Controllerが作成したサービスをロードバランサーで外部公開するケースを考えます。
(Custom ControllerがIngressやHTTPProxyなどのリソースを生成することを想定)

Ingressリソースを利用しているのであれば、vclusterがsyncしてくれるようです。
https://www.vcluster.com/docs/architecture/networking

Contourなどの別のロードバランサーを利用している場合は、HTTPProxyリソースをsyncする必要がありますが、vclusterではサポートしていません。

仮想クラスタ上で作られたサービス名がviewer-markdownview-sampleだった場合、ホストのクラスタ上にはviewer-markdownview-sample-x-default-x-vcluster-1という名前で同期されます。
このため、仮想クラスタからホストのクラスタに単純にリソースをコピーするだけでなく、サービス名の変換をおこなう必要があります。

任意のカスタムリソースをsyncする仕組みを別途用意するか、もしくはvclusterにプラグイン機構があるといいのかもしれない?

このスクラップは2021/10/23にクローズされました