使用 Crossplane 和 VCluster 在 Kubernetes 上快速構建新集群
Crossplane 是一個開源的 Kubernetes 插件,通過擴展 Kubernetes API 來解決云資源的供應問題。使用 Crossplane 時,你可以用聲明的方式定義需要創建哪些云資源才能讓你的應用程序正常工作,不需要編寫任何代碼。可以直接通過創建相關的 CRD 對象來完成這些云資源的定義,可以看成是一個云原生版本的 Terraform。
VCluster 是一個通過輕量級虛擬 Kubernetes 集群提供靈活性并節省成本的工具,使用 VCluster,你可以在 Kubernetes 集群內創建一個隔離的虛擬 Kubernetes 集群。這大大降低了創建和維護 Kubernetes 集群控制平面的復雜性。
下表比較了使用命名空間、vcluster 和 Kubernetes 集群的隔離級別和管理復雜性:
那么我們將 Crossplane 和 VCluster 這兩個工具一起來結合時候會產生怎樣的效果呢?接下來我們將通過一個示例來說明這二者的結合使用。
示例
在這個示例中我們想要實現如下所示的一些功能:
- 擁有一個集群可以接收請求來啟動一個新的集群環境
- 這些環境將可以使用 Helm 來安裝應用程序
- 請求新環境的團隊并不關心集群在哪里創建的,所以使用 VCluster 或在云提供商中創建一個 Kubernetes 集群應該為終端用戶提供類似的體驗。
這里我在本地環境使用 KinD 來進行演示,相關的資源清單可以在 https://github.com/salaboy/from-monolith-to-k8s/tree/main/platform/crossplane-vcluster 此處找到(需要自己提前安裝 kubectl、helm、kind)。
如上圖所示,我們只需要創建一個 KinD 集群(當然也可以是其他任何的 Kuberentes 集群),然后在集群上安裝 Crossplane 和 Crossplane Helm Provider,因為我們這里沒有創建任何云資源,所以我們不需要配置任何其他的 Crossplane Provider(比如 GCP、AWS、Azure 等)。
安裝 Crossplane
接下來我們可以先使用 KinD 創建一個 Kubernetes 集群。
$ kind create cluster
Creating cluster "kind" ...
? Ensuring node image (kindest/node:v1.23.4) ??
? 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 question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community ??
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready control-plane,master 56s v1.23.4
集群準備好后接下來我們可以將 Crossplane 和 Crossplane Helm Provider 安裝到我們的 KinD 集群中去,如下所示:
$ kubectl create ns crossplane-system
namespace/crossplane-system created
$ helm install crossplane --namespace crossplane-system crossplane-stable/crossplane
NAME: crossplane
LAST DEPLOYED: Tue Aug 9 15:20:22 2022
NAMESPACE: crossplane-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Release: crossplane
Chart Name: crossplane
Chart Description: Crossplane is an open source Kubernetes add-on that enables platform teams to assemble infrastructure from multiple vendors, and expose higher level self-service APIs for application teams to consume.
Chart Version: 1.9.0
Chart Application Version: 1.9.0
Kube Version: v1.23.4
安裝完成后會在 crossplane-system 命名空間下面運行如下所示的兩個 Pod:
$ kubectl get pods -n crossplane-system
NAME READY STATUS RESTARTS AGE
crossplane-c9b9fc9f9-4hn47 1/1 Running 0 11m
crossplane-rbac-manager-56c8ff5b65-8lgrp 1/1 Running 0 11m
接著需要安裝 Crossplane Helm Provider,直接使用 crossplane 的 kubectl 插件即可安裝:
$ kubectl crossplane install provider crossplane/provider-helm:v0.10.0
provider.pkg.crossplane.io/crossplane-provider-helm created
另外需要注意在安裝 Crossplane Helm Provider 的時候,我們需要為該 Provider 提供一個合適的 ServiceAccount 來創建新的 ClusterRoleBinding,以便該 Provider 可以安裝 Helm Charts。
$ SA=$(kubectl -n crossplane-system get sa -o name | grep provider-helm | sed -e 's|serviceaccount\/|crossplane-system:|g')
$ echo $SA
crossplane-system:crossplane-provider-helm-3d2f09bcd965
$ kubectl create clusterrolebinding provider-helm-admin-binding --clusterrole cluster-admin --serviceaccount="${SA}"
clusterrolebinding.rbac.authorization.k8s.io/provider-helm-admin-binding created
然后創建一個如下所示的 ProviderConfig 對象,用來聲明安裝 Helm Provider:
# helm-provider-config.yaml
apiVersion: helm.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: InjectedIdentity
---
# SA=$(kubectl -n crossplane-system get sa -o name | grep provider-helm | sed -e 's|serviceaccount\/|crossplane-system:|g')
# kubectl create clusterrolebinding provider-helm-admin-binding --clusterrole cluster-admin --serviceaccount="${SA}"
直接應用上面的資源清單即可:
$ kubectl apply -f helm-provider-config.yaml
providerconfig.helm.crossplane.io/default created
到這里我們就將 Crossplane 安裝完成了。最后我們還推薦安裝 vcluster 命令行工具來連接 Kubernetes 集群,可以參考文檔 https://www.vcluster.com/docs/getting-started/setup 進行安裝。
使用 Crossplane Composition 創建 VClusters
Crossplane 和 Crossplane Helm Provider 現在已經準備好了,接下來讓我們來看看 Crossplane Composition。Crossplane 提供了組合托管資源的機制,用戶可以在其中以聲明的方式創建自己的抽象。
- 組合資源(Composite Resource):組合資源(XR)是一種自定義資源,它由托管資源組成,允許你抽象基礎設施細節。CompositeResourceDefinition(XRD)定義了一種新型的組合資源,XRD 是集群范圍的,為了創建一個命名空間的 XR,相應的 XRD 可以提供一個組合資源聲明(XRC)。
- 組合(Composition):一個組合指定 XR 將由哪些資源組成,也就是當你創建 XR 時會發生什么,一個 XR 可以有多個組合。例如,對于 CompositeDatabase XR,你可以使用一個組合來創建 AWS RDS 實例、一個安全組和一個 MySQL 數據庫。另一種組合可以定義 GCP CloudSQL 實例和 PostgreSQL 數據庫。
- 配置(Configuration):配置是一個 XRD 和組合的包,然后可以使用 Crossplane CLI 將其發布到 OCI 鏡像注冊中心,并通過創建聲明性配置資源將其安裝到一個 Crossplane 集群中。
我們這里的 Crossplane Composition(XR) 定義了創建新環境資源時需要執行的相關操作,該對象會執行以下的一些操作:
- 使用我們安裝 Helm Provider 時配置的 Helm Provider Config 來安裝 VCluster Helm Chart,當我們安裝這個 Chart 時,就可以創建一個新的 VCluster,是不是非常簡單。
- VCluster 安裝會創建一個 Kubernetes Secret 對象,其中包含連接到 VCluster APIServer 的 tokens,我們可以使用該 Secret 來配置第二個 Helm Provider Config,它允許我們將 Helm Charts 安裝到新創建的集群中。
- 我們可以使用第二個 Helm Provider Config 來將應用程序安裝到創建的 VCluster 中。
接下來我們來看下這是如何實現的,首先我們需要將 Crossplane Composition 和 Environment CRD 應用到我們的集群中來,這樣我們就可以創建新的 Environment 資源了。
首先定義一個 Environment 的組合資源,對應的資源清單如下所示,該對象相當于 Kubernetes 集群中的 CRD:
# environment-resource-definition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: environments.fmtok8s.salaboy.com
spec:
group: fmtok8s.salaboy.com
names:
kind: Environment
plural: environments
claimNames:
kind: Cluster
plural: clusters
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties: {}
有了 XRD 組合資源聲明過后,接下來定義一個組合對象,資源清單文件內容如下所示,在該 Composition 組合對象中定義了多個資源,其中關聯了上面的 XRD 對象聲明的 Environment 對象:
# composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: environment.fmtok8s.salaboy.com
spec:
writeConnectionSecretsToNamespace: crossplane-system
compositeTypeRef:
apiVersion: fmtok8s.salaboy.com/v1alpha1
kind: Environment
resources:
- name: vcluster-helm-release
base:
apiVersion: helm.crossplane.io/v1beta1
kind: Release
metadata:
annotations:
crossplane.io/external-name: # patched
spec:
rollbackLimit: 3
forProvider:
namespace: # patched
chart:
name: vcluster
repository: https://charts.loft.sh
version: "0.10.2"
values:
syncer:
extraArgs: [] # patched
# - --out-kube-config-server=https://cluster-1.cluster-1.svc
providerConfigRef:
name: default
patches:
- fromFieldPath: metadata.name
toFieldPath: spec.forProvider.namespace
policy:
fromFieldPath: Required
- fromFieldPath: metadata.name
toFieldPath: metadata.annotations[crossplane.io/external-name]
policy:
fromFieldPath: Required
- fromFieldPath: metadata.name
toFieldPath: metadata.name
transforms:
- type: string
string:
fmt: "%s-vcluster"
- type: CombineFromComposite
combine:
variables:
- fromFieldPath: metadata.name
strategy: string
string:
fmt: "--out-kube-config-secret=%s-secret"
toFieldPath: spec.forProvider.values.syncer.extraArgs[0]
- type: CombineFromComposite
combine:
variables:
- fromFieldPath: metadata.name
- fromFieldPath: metadata.name
strategy: string
string:
fmt: "--out-kube-config-server=https://%s.%s.svc"
toFieldPath: spec.forProvider.values.syncer.extraArgs[1]
- type: CombineFromComposite
combine:
variables:
- fromFieldPath: metadata.name
- fromFieldPath: metadata.name
strategy: string
string:
fmt: "--tls-san=%s.%s.svc"
toFieldPath: spec.forProvider.values.syncer.extraArgs[2]
- name: helm-providerconfig
base:
apiVersion: helm.crossplane.io/v1alpha1
kind: ProviderConfig
spec:
credentials:
source: Secret
secretRef:
name: # patched
namespace: # patched
key: config
patches:
- fromFieldPath: metadata.name
toFieldPath: spec.credentials.secretRef.name
transforms:
- type: string
string:
fmt: vc-%s
- fromFieldPath: metadata.name
toFieldPath: spec.credentials.secretRef.namespace
- fromFieldPath: metadata.uid
toFieldPath: metadata.name
- name: helm-provider-vcluster
base:
apiVersion: helm.crossplane.io/v1beta1
kind: ProviderConfig
spec:
credentials:
source: Secret
secretRef:
namespace: #patched
key: config
patches:
- fromFieldPath: metadata.name
toFieldPath: metadata.name
- fromFieldPath: metadata.name
toFieldPath: spec.credentials.secretRef.namespace
policy:
fromFieldPath: Required
# This ProviderConfig uses the above VCluster's connection secret as
# its credentials secret.
- fromFieldPath: "metadata.name"
toFieldPath: spec.credentials.secretRef.name
transforms:
- type: string
string:
fmt: "%s-secret"
readinessChecks:
- type: None
- name: conference-chart-vcluster
base:
apiVersion: helm.crossplane.io/v1beta1
kind: Release
metadata:
annotations:
crossplane.io/external-name: conference
spec:
forProvider:
chart:
name: fmtok8s-conference-chart
repository: https://salaboy.github.io/helm/
version: "v0.1.1"
namespace: conference
providerConfigRef:
name: #patched
patches:
- fromFieldPath: metadata.name
toFieldPath: spec.providerConfigRef.name
我們為 VCluster 的 Chart 包設置了 3 個參數,以便與 Crossplane 一起使用:
- 在第 53 行配置了fmt: "--out-kube-config-secret=%s-secret"?,因為我們需要 VCluster 創建一個 Secret 對象,將kubeconfig 托管在里面,這樣我們就可以獲取它,與新創建的 APIServer 進行連接了。
- 在第 62 行配置了fmt: "--out-kube-config-server=https://%s.%s.svc"? ,因為我們需要kubeconfig? 從集群內指向新的 APIServer URL,默認情況下,生成的kubeconfig? 指向https://localhost:8443。
- 在第 71 行配置了fmt: "--tls-san=%s.%s.svc",表示需要將新的服務地址添加到到 APIServer 接受連接的主機列表中。
接著直接應用上面的兩個對象即可:
$ kubectl apply -f composition.yaml
composition.apiextensions.crossplane.io/environment.fmtok8s.salaboy.com created
$ kubectl apply -f environment-resource-definition.yaml
compositeresourcedefinition.apiextensions.crossplane.io/environments.fmtok8s.salaboy.com created
一旦集群內的組合和 CRD 可用后,我們就可以開始創建新的環境資源,在運行之前要檢查哪些 VClusters 目前是可用的。
$ vcluster list
NAME NAMESPACE STATUS CONNECTED CREATED AGE
No entries found
現在應該還沒有任何 VClusters,現在我們可以創建一個新的環境,比如現在我們定義一個 dev 的環境,只需要聲明一個如下所示的 Environment 對象即可:
# environment-resource.yaml
apiVersion: fmtok8s.salaboy.com/v1alpha1
kind: Environment
metadata:
name: dev-environment
spec: {}
直接應用該對象:
$ kubectl apply -f environment-resource.yaml
environment.fmtok8s.salaboy.com/dev-environment created
$ kubectl get environments
NAME READY COMPOSITION AGE
dev-environment False environment.fmtok8s.salaboy.com 57s
$ kubectl describe environments dev-environment
Name: dev-environment
Namespace:
Labels: crossplane.io/composite=dev-environment
Annotations: <none>
API Version: fmtok8s.salaboy.com/v1alpha1
Kind: Environment
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal PublishConnectionSecret 76s defined/compositeresourcedefinition.apiextensions.crossplane.io Successfully published connection details
Normal SelectComposition 19s (x7 over 76s) defined/compositeresourcedefinition.apiextensions.crossplane.io Successfully selected composition
Normal ComposeResources 18s (x7 over 76s) defined/compositeresourcedefinition.apiextensions.crossplane.io Successfully composed resources
現在我們可以像對待其他 Kubernetes 資源一樣對待你創建的環境資源了,你可以直接使用 kubectl get environments 列出它們,甚至可以描述它們以查看更多細節。
現在我們再去檢查 VCluster 正常就會發現一個新的資源了:
$ vcluster list
NAME NAMESPACE STATUS CONNECTED CREATED AGE
dev-environment dev-environment Running 2022-08-09 17:44:07 +0800 CST 56m38s
VCluster 將在一個新的命名空間 dev-environment 中安裝一個 APIServer(默認使用 K3s)、CoreDNS 實例和一個 Syncer,讓用戶能夠通過 kubectl 與 VCluster API Server 進行交互,就像與常規集群一樣,VCluster 將與負責調度工作負載的主機集群同步這些資源,這樣來實現了一個命名空間就是一個 Kubernetes 集群的功能。
一旦我們配置了 Crossplane 和 Crossplane Helm Provider,我們就可以通過創建一個新的 Helm Release 安裝一個 Helm Chart 來創建一個新的 VCluster,非常簡單。
一旦我們使用在 secret 中創建的正確 kubeconfig 創建了 VCluster,我們就可以配置第二個 Helm Provider 以將我們的應用程序安裝到新創建的 VCluster 中,上面 composition 對象中第 95 行定義的 helm-provider-vcluster 就是該描述。
然后在 composition 內部,我們配置使用了一個我們的會議應用的 Helm Chart 包。
配置完所有內容并創建新環境后,我們可以連接到 VCluster 并檢查應用程序是否已安裝:
$ vcluster connect dev-environment --server https://localhost:8443 -- bash
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
bash-3.2$ kubectl get ns
NAME STATUS AGE
default Active 32m
kube-system Active 32m
kube-public Active 32m
kube-node-lease Active 32m
conference Active 23m
bash-3.2$ kubectl get pods -n conference
NAME READY STATUS RESTARTS AGE
conference-fmtok8s-frontend-7cd5db8669-pv944 1/1 Running 0 23m
conference-fmtok8s-email-service-768bc88cbb-sklrg 1/1 Running 0 23m
conference-postgresql-0 1/1 Running 0 23m
conference-fmtok8s-c4p-service-7f56d7bd9d-2vjtx 1/1 Running 2 (19m ago) 23m
conference-redis-master-0 1/1 Running 0 23m
conference-redis-replicas-0 1/1 Running 0 23m
conference-fmtok8s-agenda-service-7db66c9568-xsh5m 1/1 Running 2 (16m ago) 23m
可以看到我們的應用在該集群中已經安裝成功了,而這些應用實際上就是部署在 KinD 這個原始集群的 dev-environment 命名空間下面的:
$ kubectl get pods -n dev-environment
NAME READY STATUS RESTARTS AGE
conference-fmtok8s-agenda-service-7db66c9568-xsh5m-x-08f9332627 1/1 Running 2 (18m ago) 25m
conference-fmtok8s-c4p-service-7f56d7bd9d-2vjtx-x-co-fc2c58eaec 1/1 Running 2 (21m ago) 25m
conference-fmtok8s-email-service-768bc88cbb-sklrg-x--c5d9594434 1/1 Running 0 25m
conference-fmtok8s-frontend-7cd5db8669-pv944-x-confe-2832ac1bef 1/1 Running 0 25m
conference-postgresql-0-x-conference-x-dev-environment 1/1 Running 0 25m
conference-redis-master-0-x-conference-x-dev-environment 1/1 Running 0 25m
conference-redis-replicas-0-x-conference-x-dev-environment 1/1 Running 0 25m
coredns-76dd5485df-6cbl7-x-kube-system-x-dev-environment 1/1 Running 0 34m
dev-environment-0 2/2 Running 0 63m
只是 VCluster 將一個命名空間進行了隔離,使用起來和 Kubernetes 集群體驗基本一致。
總結
這只是一個簡單的示例,介紹了如何使用 Crossplane 與 VCluster 來結合使用快速配置一套 Kubernetes 集群環境并在其中安裝應用,以使開發人員提高工作效率。當然還有很多可以優化的地方,比如:
- 在 VCluster 中安裝 ArgoCD 并使用作為環境參數提供的 GitHub URL 來實現 GitOps,這將避免對 VCluster 使用 kubectl。使用 composition 可以來創建 ArgoCD 資源以配置存儲庫和集群,而無需用戶干預。
- 在 VCluster 中安裝 Knative,以便開發人員可以依靠 Knative Functions、Knative Serving 和 Eventing 功能來設計他們的應用程序。
- 通過環境參數來決定 VCluster 使用哪個云資源,比如 GCP、AKS 和 EKS 實現等。
- 同樣 Crossplane 也是在本地 KinD 集群中進行測試使用的,我們也可以對接真實云資源,比如 GCP、AWS、Azure 等等。