使用 Terraform 建立多用戶群叢集

Google Kubernetes Engine (GKE) Enterprise 版的多租戶叢集是多個不同團隊或使用者 (稱為租戶) 共用的 Kubernetes 叢集。每個用戶群通常在叢集內都有自己的資源和應用程式。

本 Terraform 教學課程會引導您快速建立 GKE Enterprise 叢集,供兩個團隊 (backendfrontend) 共用,並在叢集上部署團隊專屬工作負載。本教學課程假設您已熟悉 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. 在「New principals」(新增主體) 欄位中,輸入您的使用者 ID。 這通常是 Google 帳戶的電子郵件地址。

    5. 在「Select a role」(選取角色) 清單中,選取角色。
    6. 如要授予其他角色,請按一下 「新增其他角色」,然後新增每個其他角色。
    7. 按一下 [Save]
    8. 準備環境

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

      1. 在 Google Cloud 控制台中,按一下 Cloud Shell 啟用圖示「啟用 Cloud 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 資源 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 類型的服務。這項服務會將 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. 在「Endpoints」(端點) 部分中,按一下「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. 在「Endpoints」(端點) 部分中,按一下「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
        

      後續步驟