使用 Terraform 创建多租户集群

Google Kubernetes Engine (GKE) Enterprise 版本中的多租户集群是由多个不同的团队或用户(称为租户)共享的 Kubernetes 集群。通常,每个租户在集群中都有自己的一组资源和应用。

通过本 Terraform 教程,您可以快速创建一个由两个团队(backendfrontend)共享的 GKE Enterprise 集群,这两个团队可以在该集群上部署特定于团队的工作负载。本教程假定您已熟悉 Terraform。如果还不熟悉,您可以使用以下资源来熟悉 Terraform 的基础知识:

准备工作

请按照以下步骤启用 Kubernetes Engine API:

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. Enable the GKE, GKE Hub, Cloud SQL, Resource Manager, IAM, Connect gateway APIs.

    Enable the APIs

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  6. Verify that billing is enabled for your Google Cloud project.

  7. Enable the GKE, GKE Hub, Cloud SQL, Resource Manager, IAM, Connect gateway APIs.

    Enable the APIs

  8. Make sure that you have the following role or roles on the project: roles/owner, roles/iam.serviceAccountTokenCreator

    Check for the roles

    1. In the Google Cloud console, go to the IAM page.

      Go to IAM
    2. Select the project.
    3. In the Principal column, find all rows that identify you or a group that you're included in. To learn which groups you're included in, contact your administrator.

    4. For all rows that specify or include you, check the Role column to see whether the list of roles includes the required roles.

    Grant the roles

    1. In the Google Cloud console, go to the IAM page.

      前往 IAM
    2. 选择项目。
    3. 点击 授予访问权限
    4. 新的主账号字段中,输入您的用户标识符。 这通常是 Google 账号的电子邮件地址。

    5. 选择角色列表中,选择一个角色。
    6. 如需授予其他角色,请点击 添加其他角色,然后添加其他各个角色。
    7. 点击 Save(保存)。
    8. 准备环境

      在本教程中,您将使用 Cloud Shell 来管理Google Cloud上托管的资源。Cloud Shell 预安装了本教程所需的软件,包括 TerraformkubectlGoogle Cloud CLI

      1. 通过点击 Cloud Shell 激活图标(激活 Cloud Shell激活 Shell 按钮,从 Google Cloud 控制台启动 Cloud Shell 会话。此操作会在 Google Cloud 控制台的底部窗格中启动会话。

        与此虚拟机关联的服务凭据会自动获取,因此您无需设置或下载服务账号密钥。

      2. 在运行命令之前,请在 gcloud CLI 中使用以下命令设置默认项目:

        gcloud config set project PROJECT_ID
        

        请将 PROJECT_ID 替换为您的项目 ID

      3. 克隆 GitHub 代码库:

        git clone https://github.com/terraform-google-modules/terraform-docs-samples.git --single-branch
        
      4. 切换到工作目录:

        cd terraform-docs-samples/gke/quickstart/multitenant
        

      查看 Terraform 文件

      Google Cloud 提供程序是一款插件,可让您使用 Terraform 管理和预配 Google Cloud 资源。它充当 Terraform 配置与Google Cloud API 之间的桥梁,可让您以声明方式定义基础设施资源,例如虚拟机和网络。

      1. 查看 main.tf 文件,该文件描述了 GKE Enterprise 集群资源:

        cat main.tf
        

        输出类似于以下内容:

        resource "google_container_cluster" "default" {
          name               = "gke-enterprise-cluster"
          location           = "us-central1"
          initial_node_count = 3
          fleet {
            project = data.google_project.default.project_id
          }
          workload_identity_config {
            workload_pool = "${data.google_project.default.project_id}.svc.id.goog"
          }
          security_posture_config {
            mode               = "BASIC"
            vulnerability_mode = "VULNERABILITY_ENTERPRISE"
          }
          depends_on = [
            google_gke_hub_feature.policycontroller,
            google_gke_hub_namespace.default
          ]
          # Set `deletion_protection` to `true` will ensure that one cannot
          # accidentally delete this instance by use of Terraform.
          deletion_protection = false
        }
        
        resource "google_gke_hub_membership_binding" "default" {
          for_each = google_gke_hub_scope.default
        
          project               = data.google_project.default.project_id
          membership_binding_id = each.value.scope_id
          scope                 = each.value.name
          membership_id         = google_container_cluster.default.fleet[0].membership_id
          location              = google_container_cluster.default.fleet[0].membership_location
        }

      创建集群和 SQL 数据库

      1. 在 Cloud Shell 中,运行以下命令以验证 Terraform 可用:

        terraform
        

        输出应类似如下所示:

        Usage: terraform [global options] <subcommand> [args]
        
        The available commands for execution are listed below.
        The primary workflow commands are given first, followed by
        less common or more advanced commands.
        
        Main commands:
          init          Prepare your working directory for other commands
          validate      Check whether the configuration is valid
          plan          Show changes required by the current configuration
          apply         Create or update infrastructure
          destroy       Destroy previously-created infrastructure
        
      2. 初始化 Terraform:

        terraform init
        
      3. 可选:规划 Terraform 配置:

        terraform plan
        
      4. 应用 Terraform 配置

        terraform apply
        

        出现提示时,输入 yes 以确认操作。此命令可能需要几分钟才能完成。输出类似于以下内容:

        Apply complete! Resources: 23 added, 0 changed, 0 destroyed.
        

      部署后端团队应用

      1. 查看以下 Terraform 文件:

        cat backend.yaml
        

        输出应类似如下所示:

        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: backend-configmap
          namespace: backend-team
          labels:
            app: backend
        data:
          go.mod: |
            module multitenant
        
            go 1.22
        
            require github.com/go-sql-driver/mysql v1.8.1
        
            require filippo.io/edwards25519 v1.1.0 // indirect
        
          go.sum: |
            filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
            filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
            github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
            github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
        
          backend.go: |
            package main
        
            import (
              "database/sql"
              "fmt"
              "log"
              "math/rand"
              "net/http"
              "os"
        
              _ "github.com/go-sql-driver/mysql"
            )
        
            func main() {
              mux := http.NewServeMux()
              mux.HandleFunc("/", frontend)
        
              port := "8080"
        
              log.Printf("Server listening on port %s", port)
              log.Fatal(http.ListenAndServe(":"+port, mux))
            }
        
            func frontend(w http.ResponseWriter, r *http.Request) {
              log.Printf("Serving request: %s", r.URL.Path)
        
              host, _ := os.Hostname()
              fmt.Fprintf(w, "Backend!\n")
              fmt.Fprintf(w, "Hostname: %s\n", host)
        
              // Open database using cloud-sql-proxy sidecar
              db, err := sql.Open("mysql", "multitenant-app@tcp/multitenant-app")
              if err != nil {
                fmt.Fprintf(w, "Error: %v\n", err)
                return
              }
        
              // Create metadata Table if not exists
              _, err = db.Exec("CREATE TABLE IF NOT EXISTS metadata (metadata_key varchar(255) NOT NULL, metadata_value varchar(255) NOT NULL, PRIMARY KEY (metadata_key))")
              if err != nil {
                fmt.Fprintf(w, "Error: %v\n", err)
                return
              }
        
              // Pick random primary color
              var color string
              randInt := rand.Intn(3) + 1
              switch {
              case randInt == 1:
                color = "red"
              case randInt == 2:
                color = "green"
              case randInt == 3:
                color = "blue"
              }
        
              // Set color in database
              _, err = db.Exec(fmt.Sprintf("REPLACE INTO metadata (metadata_key, metadata_value) VALUES ('color', '%s')", color))
              if err != nil {
                fmt.Fprintf(w, "Error: %v\n", err)
                return
              }
        
              fmt.Fprintf(w, "Set Color: %s\n", color)
            }
        
        ---
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: backendweb
          namespace: backend-team
          labels:
            app: backend
        spec:
          selector:
            matchLabels:
              app: backend
              tier: web
          template:
            metadata:
              labels:
                app: backend
                tier: web
            spec:
              containers:
              - name: backend-container
                image: golang:1.22
                command: ["go"]
                args: ["run", "."]
                workingDir: "/tmp/backend"
                volumeMounts:
                  - name: backend-configmap
                    mountPath: /tmp/backend/
                    readOnly: true
              - name: cloud-sql-proxy
                image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.4
                args:
                  - "--structured-logs"
                  - "--port=3306"
                  - "$(CONNECTION_NAME_KEY)"
                securityContext:
                  runAsNonRoot: true
                env:
                - name: CONNECTION_NAME_KEY
                  valueFrom:
                    configMapKeyRef:
                      name: database-configmap
                      key: CONNECTION_NAME
              volumes:
                - name: backend-configmap
                  configMap: { name: backend-configmap }
        ---
        apiVersion: v1
        kind: Service
        metadata:
          name: backendweb
          namespace: backend-team
          labels:
            app: backend
          annotations:
            networking.gke.io/load-balancer-type: "Internal" # Remove to create an external loadbalancer
        spec:
          selector:
            app: backend
            tier: web
          ports:
          - port: 80
            targetPort: 8080
          type: LoadBalancer

        此文件描述了以下资源:

        • 一个具有示例应用的 Deployment
        • 一个 LoadBalancer 类型的 Service。该 Service 会在端口 80 上公开 Deployment。如需将应用公开给互联网,请通过移除 networking.gke.io/load-balancer-type 注解来配置外部负载均衡器。
      2. 在 Cloud Shell 中,运行以下命令以模拟后端团队的服务账号:

        gcloud config set auth/impersonate_service_account backend@PROJECT_ID.iam.gserviceaccount.com
        

        请将 PROJECT_ID 替换为您的项目 ID

      3. 检索集群凭据:

        gcloud container fleet memberships get-credentials gke-enterprise-cluster --location us-central1
        
      4. 将后端团队的清单应用于集群:

        kubectl apply -f backend.yaml
        

      验证后端应用是否正常运行

      请执行以下操作,以确认集群运行正常:

      1. 前往 Google Cloud 控制台中的工作负载页面:

        转到“工作负载”

      2. 点击 backend 工作负载。系统会显示 Pod 详情页面。此页面显示有关 Pod 的信息,例如注解、Pod 上运行的容器、公开 Pod 的 Service,以及 CPU、内存和磁盘用量等指标。

      3. 点击 backend LoadBalancer Service。系统会显示 Service 详情页面。此页面显示有关 Service 的信息,例如与 Service 关联的 Pod 以及 Service 使用的端口。

      4. 端点部分,点击 IPv4 链接以在浏览器中查看您的 Service。输出类似于以下内容:

        Backend!
        Hostname: backendweb-765f6c4fc9-cl7jx
        Set Color: green
        

        每当用户访问后端端点时,Service 都会随机选择红色、绿色或蓝色中的一种颜色并将其存储在共享数据库中。

      部署前端团队应用

      1. 查看以下 Terraform 文件:

        cat frontend.yaml
        

        输出应类似如下所示:

        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: frontend-configmap
          namespace: frontend-team
          labels:
            app: frontend
        data:
          go.mod: |
            module multitenant
        
            go 1.22
        
            require github.com/go-sql-driver/mysql v1.8.1
        
            require filippo.io/edwards25519 v1.1.0 // indirect
        
          go.sum: |
            filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
            filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
            github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
            github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
        
          frontend.go: |
            package main
        
            import (
              "database/sql"
              "fmt"
              "log"
              "net/http"
              "os"
        
              _ "github.com/go-sql-driver/mysql"
            )
        
            func main() {
              mux := http.NewServeMux()
              mux.HandleFunc("/", frontend)
        
              port := "8080"
        
              log.Printf("Server listening on port %s", port)
              log.Fatal(http.ListenAndServe(":"+port, mux))
            }
        
            func frontend(w http.ResponseWriter, r *http.Request) {
              log.Printf("Serving request: %s", r.URL.Path)
        
              host, _ := os.Hostname()
              fmt.Fprintf(w, "Frontend!\n")
              fmt.Fprintf(w, "Hostname: %s\n", host)
        
              // Open database using cloud-sql-proxy sidecar
              db, err := sql.Open("mysql", "multitenant-app@tcp/multitenant-app")
              if err != nil {
                fmt.Fprint(w, "Error: %v\n", err)
                return
              }
        
              // Retrieve color from the database
              var color string
              err = db.QueryRow("SELECT metadata_value FROM metadata WHERE metadata_key='color'").Scan(&color)
              switch {
              case err == sql.ErrNoRows:
                fmt.Fprintf(w, "Error: color not found in database\n")
              case err != nil:
                fmt.Fprintf(w, "Error: %v\n", err)
              default:
                fmt.Fprintf(w, "Got Color: %s\n", color)
              }
            }
        
        ---
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: frontendweb
          namespace: frontend-team
          labels:
            app: frontend
        spec:
          selector:
            matchLabels:
              app: frontend
              tier: web
          template:
            metadata:
              labels:
                app: frontend
                tier: web
            spec:
              containers:
              - name: frontend-container
                image: golang:1.22
                command: ["go"]
                args: ["run", "."]
                workingDir: "/tmp/frontend"
                volumeMounts:
                  - name: frontend-configmap
                    mountPath: /tmp/frontend/
                    readOnly: true
              - name: cloud-sql-proxy
                image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.4
                args:
                  - "--structured-logs"
                  - "--port=3306"
                  - "$(CONNECTION_NAME_KEY)"
                securityContext:
                  runAsNonRoot: true
                env:
                - name: CONNECTION_NAME_KEY
                  valueFrom:
                    configMapKeyRef:
                      name: database-configmap
                      key: CONNECTION_NAME
              volumes:
                - name: frontend-configmap
                  configMap: { name: frontend-configmap }
        ---
        apiVersion: v1
        kind: Service
        metadata:
          name: frontendweb
          namespace: frontend-team
          labels:
            app: frontend
          annotations:
            networking.gke.io/load-balancer-type: "Internal" # Remove to create an external loadbalancer
        spec:
          selector:
            app: frontend
            tier: web
          ports:
          - port: 80
            targetPort: 8080
          type: LoadBalancer

        此文件描述了以下资源:

        • 一个具有示例应用的 Deployment
        • 一个 LoadBalancer 类型的 Service。该 Service 会在端口 80 上公开 Deployment。如需将应用公开给互联网,请通过移除 networking.gke.io/load-balancer-type 注解来配置外部负载均衡器。
      2. 在 Cloud Shell 中,运行以下命令以模拟前端团队的服务账号:

        gcloud config set auth/impersonate_service_account frontend@PROJECT_ID.iam.gserviceaccount.com
        

        请将 PROJECT_ID 替换为您的项目 ID

      3. 检索集群凭据:

        gcloud container fleet memberships get-credentials gke-enterprise-cluster --location us-central1
        
      4. 将前端团队的清单应用于集群:

        kubectl apply -f frontend.yaml
        

      验证前端应用是否正常运行

      请执行以下操作,以确认集群运行正常:

      1. 前往 Google Cloud 控制台中的工作负载页面:

        转到“工作负载”

      2. 点击 frontend 工作负载。系统会显示 Pod 详情页面。此页面显示有关 Pod 的信息,例如注解、Pod 上运行的容器、公开 Pod 的 Service,以及 CPU、内存和磁盘用量等指标。

      3. 点击 frontend LoadBalancer Service。系统会显示 Service 详情页面。此页面显示有关 Service 的信息,例如与 Service 关联的 Pod 以及 Service 使用的端口。

      4. 端点部分,点击 IPv4 链接以在浏览器中查看您的 Service。输出类似于以下内容:

        Frontend!
        Hostname: frontendweb-5cd888d88f-gwwtc
        Got Color: green
        

      清理

      为避免因本页中使用的资源导致您的 Google Cloud 账号产生费用,请按照以下步骤操作。

      1. 在 Cloud Shell 中,运行以下命令以取消设置服务账号模拟:

        gcloud config unset auth/impersonate_service_account
        
      2. 运行以下命令以删除 Terraform 资源:

        terraform destroy --auto-approve
        

      后续步骤