使用 Terraform 创建多租户集群

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

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

准备工作

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

  1. 登录您的 Google Cloud 账号。如果您是 Google Cloud新手,请 创建一个账号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

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

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

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

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

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

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

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

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  8. 确保您在项目中拥有以下一个或多个角色: roles/owner、roles/iam.serviceAccountTokenCreator

    检查角色

    1. 在 Google Cloud 控制台中,前往 IAM 页面。

      转到 IAM
    2. 选择项目。
    3. 主账号列中,找到标识您或您所属群组的所有行。如需了解您属于哪些群组,请与您的管理员联系。

    4. 对于指定或包含您的所有行,请检查角色列以查看角色列表是否包含所需的角色。

    授予角色

    1. 在 Google Cloud 控制台中,前往 IAM 页面。

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

    5. 点击选择角色,然后搜索相应角色。
    6. 如需授予其他角色,请点击 添加其他角色,然后添加其他各个角色。
    7. 点击 Save(保存)。

准备环境

在本教程中,您将使用 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 集群资源:

    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
    

后续步骤