• 使用 Crossplane 和 VCluster 在 Kubernetes 上快速构建新集群


    Crossplane 是一个开源的 Kubernetes 插件,通过扩展 Kubernetes API 来解决云资源的供应问题。使用 Crossplane 时,你可以用声明的方式定义需要创建哪些云资源才能让你的应用程序正常工作,不需要编写任何代码。可以直接通过创建相关的 CRD 对象来完成这些云资源的定义,可以看成是一个云原生版本的 Terraform

    图片

    VCluster 是一个通过轻量级虚拟 Kubernetes 集群提供灵活性并节省成本的工具,使用 VCluster,你可以在 Kubernetes 集群内创建一个隔离的虚拟 Kubernetes 集群。这大大降低了创建和维护 Kubernetes 集群控制平面的复杂性。

    图片

    下表比较了使用命名空间、vcluster 和 Kubernetes 集群的隔离级别和管理复杂性:

    图片

    那么我们将 CrossplaneVCluster 这两个工具一起来结合时候会产生怎样的效果呢?接下来我们将通过一个示例来说明这二者的结合使用。

    示例

    在这个示例中我们想要实现如下所示的一些功能:

    • 拥有一个集群可以接收请求来启动一个新的集群环境

    • 这些环境将可以使用 Helm 来安装应用程序

    • 请求新环境的团队并不关心集群在哪里创建的,所以使用 VCluster 或在云提供商中创建一个 Kubernetes 集群应该为终端用户提供类似的体验。

    这里我在本地环境使用 KinD 来进行演示,相关的资源清单可以在 https://github.com/salaboy/from-monolith-to-k8s/tree/main/platform/crossplane-vcluster 此处找到(需要自己提前安装 kubectl、helm、kind)。

    图片

    如上图所示,我们只需要创建一个 KinD 集群(当然也可以是其他任何的 Kuberentes 集群),然后在集群上安装 CrossplaneCrossplane 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    集群准备好后接下来我们可以将 CrossplaneCrossplane 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    安装完成后会在 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接着需要安装 Crossplane Helm Provider,直接使用 crossplane 的 kubectl 插件即可安装:

    $ kubectl crossplane install provider crossplane/provider-helm:v0.10.0
    provider.pkg.crossplane.io/crossplane-provider-helm created
    
    
    • 1
    • 2
    • 3

    另外需要注意在安装 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后创建一个如下所示的 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}"
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    直接应用上面的资源清单即可:

    $ kubectl apply -f helm-provider-config.yaml
    providerconfig.helm.crossplane.io/default created
    
    
    • 1
    • 2
    • 3

    到这里我们就将 Crossplane 安装完成了。最后我们还推荐安装 vcluster 命令行工具来连接 Kubernetes 集群,可以参考文档 https://www.vcluster.com/docs/getting-started/setup 进行安装。

    使用 Crossplane Composition 创建 VClusters

    CrossplaneCrossplane Helm Provider 现在已经准备好了,接下来让我们来看看 Crossplane CompositionCrossplane 提供了组合托管资源的机制,用户可以在其中以声明的方式创建自己的抽象。

    • 组合资源(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 CompositionEnvironment 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: {}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    有了 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142

    我们为 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    一旦集群内的组合和 CRD 可用后,我们就可以开始创建新的环境资源,在运行之前要检查哪些 VClusters 目前是可用的。

    $ vcluster list
    
     NAME   NAMESPACE   STATUS   CONNECTED   CREATED   AGE
     No entries found
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    现在应该还没有任何 VClusters,现在我们可以创建一个新的环境,比如现在我们定义一个 dev 的环境,只需要声明一个如下所示的 Environment 对象即可:

    # environment-resource.yaml
    apiVersion: fmtok8s.salaboy.com/v1alpha1
    kind: Environment
    metadata:
      name: dev-environment
    spec: {}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    直接应用该对象:

    $ 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:  
    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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    现在我们可以像对待其他 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
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    可以看到我们的应用在该集群中已经安装成功了,而这些应用实际上就是部署在 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    只是 VCluster 将一个命名空间进行了隔离,使用起来和 Kubernetes 集群体验基本一致。

    总结

    这只是一个简单的示例,介绍了如何使用 CrossplaneVCluster 来结合使用快速配置一套 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 等等。

    参考文档

    • https://www.vcluster.com/
    • https://crossplane.io/
    • https://salaboy.com/2022/08/03/building-platforms-on-top-of-kubernetes-vcluster-and-crossplane/
  • 相关阅读:
    Tomcat 下部署 jFinal
    爬取tx视频
    PLC设备相关常用英文单词(一)
    python中.npy文件转换为.txt或.csv文件
    国产智多晶FPGA使用Modelsim仿真RTL设计方法
    Mybatis-Plus最优化持久层开发
    【面试题】前端面试真题 年前端面试
    计算机毕业设计Java山西工艺美术馆(源码+系统+mysql数据库+lw文档)
    史上最全的Redis基础+进阶项目实战总结笔记
    2020携程java面试题整理,开发实习一面面经
  • 原文地址:https://blog.csdn.net/weixin_38261043/article/details/126262857