使用 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. 按一下「Grant access」(授予存取權)
    4. 在「New principals」(新增主體) 欄位中,輸入您的使用者 ID。 這通常是指 Google 帳戶的電子郵件地址。

    5. 按一下「選取角色」,然後搜尋角色。
    6. 如要授予其他角色,請按一下「Add another role」(新增其他角色),然後新增其他角色。
    7. 按一下「Save」(儲存)

準備環境

在本教學課程中,您將使用 Cloud Shell 管理Google Cloud上託管的資源。Cloud Shell 已預先安裝本教學課程所需的軟體,包括 TerraformkubectlGoogle Cloud CLI

  1. 在 Google Cloud 控制台中,按一下「啟用 Cloud Shell」圖示 「Activate Shell」(啟用 Shell) 按鈕,啟動 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 設定與 API 之間的橋樑,讓您以宣告方式定義基礎架構資源,例如虛擬機器和網路。Google Cloud

  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 類型的服務。這項服務會將 Deployment 公開至通訊埠 80。如要將應用程式公開到網際網路,請移除 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 控制台的「Workloads」(工作負載) 頁面:

    前往「Workloads」(工作負載)

  2. 按一下 backend 工作負載。系統隨即會顯示 Pod 詳細資料頁面。這個頁面會顯示 Pod 的相關資訊,例如註解、在 Pod 上執行的容器、公開 Pod 的服務,以及 CPU、記憶體和磁碟用量等指標。

  3. 按一下 backend LoadBalancer 服務。系統會顯示「服務詳細資料」頁面。 這個頁面會顯示服務的相關資訊,例如與服務相關聯的 Pod,以及服務使用的通訊埠。

  4. 在「端點」部分中,按一下「IPv4 連結」,即可在瀏覽器中查看服務。輸出結果會與下列內容相似:

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

    每當使用者存取後端端點時,服務會從紅色、綠色或藍色中隨機挑選並儲存顏色到共用資料庫。

部署前端團隊應用程式

  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 類型的服務。這項服務會將 Deployment 公開至通訊埠 80。如要將應用程式公開到網際網路,請移除 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 控制台的「Workloads」(工作負載) 頁面:

    前往「Workloads」(工作負載)

  2. 按一下 frontend 工作負載。系統隨即會顯示 Pod 詳細資料頁面。這個頁面會顯示 Pod 的相關資訊,例如註解、在 Pod 上執行的容器、公開 Pod 的服務,以及 CPU、記憶體和磁碟用量等指標。

  3. 按一下 frontend LoadBalancer 服務。系統會顯示「服務詳細資料」頁面。 這個頁面會顯示服務的相關資訊,例如與服務相關聯的 Pod,以及服務使用的通訊埠。

  4. 在「端點」部分中,按一下「IPv4 連結」,即可在瀏覽器中查看服務。輸出結果會與下列內容相似:

    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
    

後續步驟