部署至 Compute Engine

本指南說明如何使用 Cloud Build 和 Terraform,在 Compute Engine 代管執行個體群組 (MIG) 上執行藍綠部署,確保服務不中斷。

Cloud Build 可自動執行各種開發人員程序,包括建構應用程式,以及將應用程式部署至各種 Google Cloud 執行階段,例如 Compute Engine、Google Kubernetes EngineGKE EnterpriseCloud Run 函式

Compute Engine MIG 可讓您在多個相同的虛擬機器 (VM) 上執行應用程式。您可以利用自動調度資源、自動修復、區域 (多可用區) 部署以及自動更新等自動化 MIG 服務,讓工作負載具有可擴充性和高可用性。您將使用藍/綠持續部署模型,瞭解如何逐步將使用者流量從一個 MIG (藍色) 轉移至另一個 MIG (綠色),這兩個 MIG 都會在正式環境中執行。

事前準備

  • Enable the Cloud Build, Cloud Run, Artifact Registry, and Resource Manager 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

  • 準備好應用程式原始碼。原始碼必須儲存在 GitHub 或 Bitbucket 等存放區。

  • 如要在本頁面執行 gcloud 指令,請安裝 Google Cloud CLI

必要的「身分與存取權管理」權限

  1. 在 Google Cloud 控制台中,前往「Cloud Build 權限」頁面:

    前往「Permissions」(權限)

  2. 針對指定的 Cloud Build 服務帳戶預設 Cloud Build 服務帳戶,將下列角色的狀態設為「已啟用」

    • Compute 執行個體管理員 v1 (roles/compute.instanceAdmin):允許 Cloud Build 將新執行個體部署至 Compute Engine。
    • Storage 管理員 (roles/storage.admin):可從 Cloud Storage 讀取及寫入資料。
    • Artifact Registry 寫入者 (roles/artifactregistry.writer):允許從 Artifact Registry 提取映像檔,以及寫入 Artifact Registry。
    • 記錄寫入者 (roles/logging.logWriter):允許將記錄項目寫入 Cloud Logging。
    • Cloud Build 編輯者 (roles/cloudbuild.builds.editor):允許服務帳戶執行建構作業。

設計總覽

下圖顯示本文所述程式碼範例使用的藍綠部署模型:

藍綠模型

這個模型大致包含下列元件:

  • 兩個 Compute Engine VM 集區:藍色和綠色。
  • 三個外部 HTTP(S) 負載平衡器:
    • 藍/綠負載平衡器,可將使用者流量導向藍色或綠色 VM 執行個體集區。
    • 藍色負載平衡器,可將 QA 工程師和開發人員的流量導向藍色 VM 執行個體集區。
    • 綠色負載平衡器,可將 QA 工程師和開發人員的流量轉送至綠色執行個體集區。
  • 兩組使用者:
    • 可存取藍綠負載平衡器的使用者,系統會將他們導向藍色或綠色執行個體集區。
    • 需要存取兩組集區的品質確保工程師和開發人員,以進行開發和測試。他們可以存取藍色和綠色負載平衡器,分別將他們導向藍色執行個體集區和綠色執行個體集區。

藍色和綠色 VM 集區是透過 Compute Engine MIG 實作,外部 IP 位址則會透過外部 HTTP(S) 負載平衡器,轉送至 MIG 中的 VM。本文所述的程式碼範例使用 Terraform 設定這項基礎架構。

下圖說明部署期間發生的開發人員作業:

開發人員營運流程

在上圖中,紅箭頭代表首次設定部署基礎架構時發生的啟動程序流程,藍箭頭則代表每次部署時發生的 GitOps 流程。

如要設定這項基礎架構,請執行設定指令碼,啟動啟動程序並設定 GitOps 流程的元件。

設定指令碼會執行 Cloud Build 管道,執行下列作業:

  • Cloud Source Repositories 中建立名為 copy-of-gcp-mig-simple 的存放區,並將 GitHub 範例存放區的原始碼複製到 Cloud Source Repositories 的存放區。
  • 建立兩個名為 applydestroyCloud Build 觸發條件

apply 觸發條件會附加至 Cloud Source Repositories 中名為 main.tfvars 的 Terraform 檔案。這個檔案包含代表藍色和綠色負載平衡器的 Terraform 變數。

如要設定部署作業,請更新 main.tfvars 檔案中的變數。 apply 觸發程序會執行 Cloud Build 管道,該管道會執行 tf_apply 並執行下列作業:

  • 建立兩個 Compute Engine MIG (分別用於綠色和藍色)、四個 Compute Engine VM 執行個體 (綠色 MIG 和藍色 MIG 各兩個)、三個負載平衡器 (藍色、綠色和分配器),以及三個公用 IP 位址。
  • 列印 IP 位址,您可以使用這些位址,在藍色和綠色執行個體中查看已部署的應用程式。

系統會手動觸發 destroy 觸發條件,刪除 apply 觸發條件建立的所有資源。

立即體驗

  1. 從 Google 程式碼範例存放區執行設定指令碼:

    bash <(curl https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-build-samples/main/mig-blue-green/setup.sh)
    
  2. 當設定指令碼要求使用者同意時,請輸入「yes」

    指令碼會在幾秒內完成執行。

  3. 在 Google Cloud 控制台中,開啟 Cloud Build 的「建構記錄」頁面:

    開啟「建構記錄」頁面

  4. 按一下最新版本。

    您會看到「建構詳細資料」頁面,其中顯示 Cloud Build 管道,包含三個建構步驟:第一個建構步驟會在 Cloud Source Repositories 中建立存放區;第二個步驟會將 GitHub 中範例存放區的內容複製到 Cloud Source Repositories;第三個步驟則會新增兩個建構觸發條件。

  5. 開啟 Cloud Source Repositories:

    開啟 Cloud Source Repositories

  6. 在存放區清單中,按一下 copy-of-gcp-mig-simple

    在頁面底部的「History」(記錄) 分頁中,您會看到一個由 Cloud Build 建立的提交,說明為「created a repository named copy-of-gcp-mig-simple」(建立名為「copy-of-gcp-mig-simple」的存放區)。A copy of https://github.com/GoogleCloudPlatform/cloud-build-samples.git

  7. 開啟 Cloud Build 的「Triggers」(觸發條件) 頁面:

    開啟「觸發條件」頁面

  8. 您會看到兩個名為 applydestroy 的建構觸發條件。apply 觸發程序會附加至 main 分支中的 infra/main.tfvars 檔案。每當檔案更新時,就會執行這項觸發條件。destroy 觸發條件是手動觸發條件。

  9. 如要開始部署程序,請更新 infra/main.tfvars 檔案:

    1. 在終端機視窗中,建立並前往名為 deploy-compute-engine 的資料夾:

      mkdir ~/deploy-compute-engine
      cd ~/deploy-compute-engine
      
    2. 複製 copy-of-gcp-mig-simple 存放區:

      gcloud source repos clone copy-of-mig-blue-green
      
    3. 前往複製的目錄:

      cd ./copy-of-mig-blue-green
      
    4. 更新 infra/main.tfvars,將藍色換成綠色:

      sed -i'' -e 's/blue/green/g' infra/main.tfvars
      
    5. 新增更新後的檔案:

      git add .
      
    6. 修訂檔案:

      git commit -m "Promote green"
      
    7. 推送檔案:

      git push
      

      變更 infra/main.tfvars 會觸發 apply 觸發程序,進而啟動部署作業。

  10. 開啟 Cloud Source Repositories:

    開啟 Cloud Source Repositories

  11. 在存放區清單中,按一下 copy-of-gcp-mig-simple

    頁面底部的「記錄」分頁會顯示含有 Promote green 說明的提交。

  12. 如要查看 apply 觸發條件的執行情況,請在 Google Cloud 控制台中開啟「Build history」(建構記錄) 頁面:

    開啟「建構記錄」頁面

  13. 按一下第一個建構作業,開啟「Build details」(建構作業詳細資料) 頁面。

    您會看到 apply 觸發管道,其中包含兩個建構步驟。第一個建構步驟會執行 Terraform apply,為部署作業建立 Compute Engine 和負載平衡資源。第二個建構步驟會列印出 IP 位址,您可以在該位址查看執行中的應用程式。

  14. 在瀏覽器中開啟與綠色 MIG 對應的 IP 位址。您會看到類似下方的螢幕截圖,顯示部署作業:

    部署作業

  15. 前往 Compute Engine 的「Instance group」(執行個體群組) 頁面,查看藍色和綠色執行個體群組:

    開啟「執行個體群組」頁面

  16. 開啟「VM instances」(VM 執行個體) 頁面,查看四個 VM 執行個體:

    開啟「VM Instance」(VM 執行個體) 頁面

  17. 開啟「外部 IP 位址」頁面,查看三個負載平衡器:

    開啟「外部 IP 位址」頁面

瞭解程式碼

這個程式碼範例的原始碼包括:

  • 與設定指令碼相關的原始碼。
  • 與 Cloud Build 管道相關的原始碼。
  • 與 Terraform 範本相關的原始碼。

設定指令碼

setup.sh 是設定指令碼,可執行啟動程序並建立藍綠部署的元件。指令碼會執行下列作業:

  • 啟用 Cloud Build、Resource Manager、Compute Engine 和 Cloud Source Repositories API。
  • roles/editor 身分與存取權管理角色授予專案中的 Cloud Build 服務帳戶。Cloud Build 必須具備這個角色,才能建立及設定部署作業所需的 GitOps 元件。
  • roles/source.admin 身分與存取權管理角色授予專案中的 Cloud Build 服務帳戶。Cloud Build 服務帳戶必須具備這個角色,才能在專案中建立 Cloud Source Repositories,並將範例 GitHub 存放區的內容複製到 Cloud Source Repositories。
  • 產生名為 bootstrap.cloudbuild.yaml 的內嵌 Cloud Build 管道,該管道會執行下列作業:

    • 在 Cloud Source Repositories 中建立新的存放區。
    • 將範例 GitHub 存放區中的原始碼複製到 Cloud Source Repositories 中的新存放區。
    • 建立套用和刪除建構觸發條件。
set -e

BLUE='\033[1;34m'
RED='\033[1;31m'
GREEN='\033[1;32m'
NC='\033[0m'

echo -e "\n${GREEN}######################################################"
echo -e "#                                                    #"
echo -e "#  Zero-Downtime Blue/Green VM Deployments Using     #"
echo -e "#  Managed Instance Groups, Cloud Build & Terraform  #"
echo -e "#                                                    #"
echo -e "######################################################${NC}\n"

echo -e "\nSTARTED ${GREEN}setup.sh:${NC}"

echo -e "\nIt's ${RED}safe to re-run${NC} this script to ${RED}recreate${NC} all resources.\n"
echo "> Checking GCP CLI tool is installed"
gcloud --version > /dev/null 2>&1

readonly EXPLICIT_PROJECT_ID="$1"
readonly EXPLICIT_CONSENT="$2"

if [ -z "$EXPLICIT_PROJECT_ID" ]; then
    echo "> No explicit project id provided, trying to infer"
    PROJECT_ID="$(gcloud config get-value project)"
else
    PROJECT_ID="$EXPLICIT_PROJECT_ID"
fi

if [ -z "$PROJECT_ID" ]; then
    echo "ERROR: GCP project id was not provided as parameter and could not be inferred"
    exit 1
else
    readonly PROJECT_NUM="$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')"
    if [ -z "$PROJECT_NUM" ]; then
        echo "ERROR: GCP project number could not be determined"
        exit 1
    fi
    echo -e "\nYou are about to:"
    echo -e "  * modify project ${RED}${PROJECT_ID}/${PROJECT_NUM}${NC}"
    echo -e "  * ${RED}enable${NC} various GCP APIs"
    echo -e "  * make Cloud Build ${RED}editor${NC} of your project"
    echo -e "  * ${RED}execute${NC} Cloud Builds and Terraform plans to create"
    echo -e "  * ${RED}4 VMs${NC}, ${RED}3 load balancers${NC}, ${RED}3 public IP addresses${NC}"
    echo -e "  * incur ${RED}charges${NC} in your billing account as a result\n"
fi

if [ "$EXPLICIT_CONSENT" == "yes" ]; then
  echo "Proceeding under explicit consent"
  readonly CONSENT="$EXPLICIT_CONSENT"
else
    echo -e "Enter ${BLUE}'yes'${NC} if you want to proceed:"
    read CONSENT
fi

if [ "$CONSENT" != "yes" ]; then
    echo -e "\nERROR: Aborted by user"
    exit 1
else
    echo -e "\n......................................................"
    echo -e "\n> Received user consent"
fi

#
# Executes action with one randomly delayed retry.
#
function do_with_retry {
    COMMAND="$@"
    echo "Trying $COMMAND"
    (eval $COMMAND && echo "Success on first try") || ( \
        echo "Waiting few seconds to retry" &&
        sleep 10 && \
        echo "Retrying $COMMAND" && \
        eval $COMMAND \
    )
}

echo "> Enabling required APIs"
# Some of these can be enabled later with Terraform, but I personally
# prefer to do all API enablement in one place with gcloud.
gcloud services enable \
    --project=$PROJECT_ID \
    cloudbuild.googleapis.com \
    cloudresourcemanager.googleapis.com \
    compute.googleapis.com \
    sourcerepo.googleapis.com \
    --no-user-output-enabled \
    --quiet

echo "> Adding Cloud Build to roles/editor"
gcloud projects add-iam-policy-binding \
    "$PROJECT_ID" \
    --member="serviceAccount:$PROJECT_NUM@cloudbuild.gserviceaccount.com" \
    --role='roles/editor' \
    --condition=None \
    --no-user-output-enabled \
    --quiet

echo "> Adding Cloud Build to roles/source.admin"
gcloud projects add-iam-policy-binding \
    "$PROJECT_ID" \
    --member="serviceAccount:$PROJECT_NUM@cloudbuild.gserviceaccount.com" \
    --condition=None \
    --role='roles/source.admin' \
    --no-user-output-enabled \
    --quiet

echo "> Configuring bootstrap job"
rm -rf "./bootstrap.cloudbuild.yaml"
cat <<'EOT_BOOT' > "./bootstrap.cloudbuild.yaml"
tags:
- "mig-blue-green-bootstrapping"
steps:
- id: create_new_cloud_source_repo
  name: "gcr.io/cloud-builders/gcloud"
  script: |
    #!/bin/bash
    set -e

    echo "(Re)Creating source code repository"

    gcloud source repos delete \
        "copy-of-mig-blue-green" \
        --quiet || true

    gcloud source repos create \
        "copy-of-mig-blue-green" \
        --quiet

- id: copy_demo_source_into_new_cloud_source_repo
  name: "gcr.io/cloud-builders/gcloud"
  env:
    - "PROJECT_ID=$PROJECT_ID"
    - "PROJECT_NUMBER=$PROJECT_NUMBER"
  script: |
    #!/bin/bash
    set -e

    readonly GIT_REPO="https://github.com/GoogleCloudPlatform/cloud-build-samples.git"

    echo "Cloning demo source repo"
    mkdir /workspace/from/
    cd /workspace/from/
    git clone $GIT_REPO ./original
    cd ./original

    echo "Cloning new empty repo"
    mkdir /workspace/to/
    cd /workspace/to/
    gcloud source repos clone \
        "copy-of-mig-blue-green"
    cd ./copy-of-mig-blue-green

    echo "Making a copy"
    cp -r /workspace/from/original/mig-blue-green/* ./

    echo "Setting git identity"
    git config user.email \
        "$PROJECT_NUMBER@cloudbuild.gserviceaccount.com"
    git config user.name \
        "Cloud Build"

    echo "Commit & push"
    git add .
    git commit \
        -m "A copy of $GIT_REPO"
    git push

- id: add_pipeline_triggers
  name: "gcr.io/cloud-builders/gcloud"
  env:
    - "PROJECT_ID=$PROJECT_ID"
  script: |
    #!/bin/bash
    set -e

    echo "(Re)Creating destroy trigger"
    gcloud builds triggers delete "destroy" --quiet || true
    gcloud builds triggers create manual \
        --name="destroy" \
        --repo="https://source.developers.google.com/p/$PROJECT_ID/r/copy-of-mig-blue-green" \
        --branch="master" \
        --build-config="pipelines/destroy.cloudbuild.yaml" \
        --repo-type=CLOUD_SOURCE_REPOSITORIES \
        --quiet

    echo "(Re)Creating apply trigger"
    gcloud builds triggers delete "apply" --quiet || true
    gcloud builds triggers create cloud-source-repositories \
        --name="apply" \
        --repo="copy-of-mig-blue-green" \
        --branch-pattern="master" \
        --build-config="pipelines/apply.cloudbuild.yaml" \
        --included-files="infra/main.tfvars" \
        --quiet

EOT_BOOT

echo "> Waiting API enablement propagation"
do_with_retry "(gcloud builds list --project "$PROJECT_ID" --quiet && gcloud compute instances list --project "$PROJECT_ID" --quiet && gcloud source repos list --project "$PROJECT_ID" --quiet) > /dev/null 2>&1" > /dev/null 2>&1

echo "> Executing bootstrap job"
gcloud beta builds submit \
    --project "$PROJECT_ID" \
    --config ./bootstrap.cloudbuild.yaml \
    --no-source \
    --no-user-output-enabled \
    --quiet
rm ./bootstrap.cloudbuild.yaml

echo -e "\n${GREEN}All done. Now you can:${NC}"
echo -e "  * manually run 'apply' and 'destroy' triggers to manage deployment lifecycle"
echo -e "  * commit change to 'infra/main.tfvars' and see 'apply' pipeline trigger automatically"

echo -e "\n${GREEN}Few key links:${NC}"
echo -e "  * Dashboard: https://console.cloud.google.com/home/dashboard?project=$PROJECT_ID"
echo -e "  * Repo: https://source.cloud.google.com/$PROJECT_ID/copy-of-mig-blue-green"
echo -e "  * Cloud Build Triggers: https://console.cloud.google.com/cloud-build/triggers;region=global?project=$PROJECT_ID"
echo -e "  * Cloud Build History: https://console.cloud.google.com/cloud-build/builds?project=$PROJECT_ID"

echo -e "\n............................."

echo -e "\n${GREEN}COMPLETED!${NC}"

Cloud Build 管道

apply.cloudbuild.yamldestroy.cloudbuild.yaml 是設定指令碼用來設定 GitOps 流程資源的 Cloud Build 設定檔。apply.cloudbuild.yaml 包含兩個建構步驟:

  • tf_apply build 建構步驟,呼叫 tf_install_in_cloud_build_step 函式來安裝 Terraform。tf_apply ,用於建立 GitOps 流程中使用的資源。函式 tf_install_in_cloud_build_steptf_apply 定義於 bash_utils.sh,建構步驟會使用 source 指令呼叫這些函式。
  • describe_deployment 建構步驟,呼叫 describe_deployment 函式,輸出負載平衡器的 IP 位址。

destroy.cloudbuild.yaml 呼叫 tf_destroy,刪除 tf_apply 建立的所有資源。

函式 tf_install_in_cloud_build_steptf_applydescribe_deploymenttf_destroy 定義於 bash_utils.sh 檔案中。建構設定檔會使用 source 指令呼叫函式。

steps:
  - id: run-terraform-apply
    name: "gcr.io/cloud-builders/gcloud"
    env:
      - "PROJECT_ID=$PROJECT_ID"
    script: |
      #!/bin/bash
      set -e
      source /workspace/lib/bash_utils.sh
      tf_install_in_cloud_build_step
      tf_apply

  - id: describe-deployment
    name: "gcr.io/cloud-builders/gcloud"
    env:
      - "PROJECT_ID=$PROJECT_ID"
    script: |
      #!/bin/bash
      set -e
      source /workspace/lib/bash_utils.sh
      describe_deployment

tags:
  - "mig-blue-green-apply"
steps:
  - id: run-terraform-destroy
    name: "gcr.io/cloud-builders/gcloud"
    env:
      - "PROJECT_ID=$PROJECT_ID"
    script: |
      #!/bin/bash
      set -e
      source /workspace/lib/bash_utils.sh
      tf_install_in_cloud_build_step
      tf_destroy

tags:
  - "mig-blue-green-destroy"

下列程式碼顯示 bash_utils.sh 中定義的 tf_install_in_cloud_build_step 函式。建構設定檔會呼叫這個函式,即時安裝 Terraform。這會建立 Cloud Storage 值區,用於記錄 Terraform 狀態。

function tf_install_in_cloud_build_step {
    echo "Installing deps"
    apt update
    apt install \
        unzip \
        wget \
        -y

    echo "Manually installing Terraform"
    wget https://releases.hashicorp.com/terraform/1.3.4/terraform_1.3.4_linux_386.zip
    unzip -q terraform_1.3.4_linux_386.zip
    mv ./terraform /usr/bin/
    rm -rf terraform_1.3.4_linux_386.zip

    echo "Verifying installation"
    terraform -v

    echo "Creating Terraform state storage bucket $BUCKET_NAME"
    gcloud storage buckets create \
        "gs://$BUCKET_NAME" || echo "Already exists..."

    echo "Configure Terraform provider and state bucket"
cat <<EOT_PROVIDER_TF > "/workspace/infra/provider.tf"
terraform {
  required_version = ">= 0.13"
  backend "gcs" {
    bucket = "$BUCKET_NAME"
  }
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 3.77, < 5.0"
    }
  }
}
EOT_PROVIDER_TF

    echo "$(cat /workspace/infra/provider.tf)"
}

下列程式碼片段顯示 bash_utils.sh 中定義的 tf_apply 函式。這個函式會先呼叫 terraform init 載入所有模組和自訂程式庫,然後執行 terraform applymain.tfvars 檔案載入變數。

function tf_apply {
    echo "Running Terraform init"
    terraform \
        -chdir="$TF_CHDIR" \
        init

    echo "Running Terraform apply"
    terraform \
        -chdir="$TF_CHDIR" \
        apply \
        -auto-approve \
        -var project="$PROJECT_ID" \
        -var-file="main.tfvars"
}

下列程式碼片段顯示 bash_utils.sh 中定義的 describe_deployment 函式。這個指令會使用 gcloud compute addresses describe,依名稱擷取負載平衡器的 IP 位址,然後列印出來。

function describe_deployment {
    NS="ns1-"
    echo -e "Deployment configuration:\n$(cat infra/main.tfvars)"
    echo -e \
      "Here is how to connect to:" \
      "\n\t* active color MIG: http://$(gcloud compute addresses describe ${NS}splitter-address-name --region=us-west1 --format='value(address)')/" \
      "\n\t* blue color MIG: http://$(gcloud compute addresses describe ${NS}blue-address-name --region=us-west1 --format='value(address)')/" \
      "\n\t* green color MIG: http://$(gcloud compute addresses describe ${NS}green-address-name --region=us-west1 --format='value(address)')/"
    echo "Good luck!"
}

下列程式碼片段顯示 bash_utils.sh 中定義的 tf_destroy 函式。這個指令會呼叫 terraform init,載入所有模組和自訂程式庫,然後執行 terraform destroy,卸載 Terraform 變數。

function tf_destroy {
    echo "Running Terraform init"
    terraform \
        -chdir="$TF_CHDIR" \
        init

    echo "Running Terraform destroy"
    terraform \
        -chdir="$TF_CHDIR" \
        destroy \
        -auto-approve \
        -var project="$PROJECT_ID" \
        -var-file="main.tfvars"
}

Terraform 範本

您可以在 copy-of-gcp-mig-simple/infra/ 資料夾中找到所有 Terraform 設定檔和變數。

  • main.tf:這是 Terraform 設定檔
  • main.tfvars:這個檔案會定義 Terraform 變數。
  • mig/splitter/:這些資料夾包含定義負載平衡器的模組。mig/ 資料夾包含 Terraform 設定檔,用於定義藍色和綠色負載平衡器的 MIG。藍色和綠色 MIG 完全相同,因此只需定義一次,並針對藍色和綠色物件例項化。分割器負載平衡器的 Terraform 設定檔位於 splitter/ 資料夾中。

以下程式碼片段顯示 infra/main.tfvars 的內容。其中包含三個變數:兩個變數用於決定要將哪個應用程式版本部署到藍色和綠色集區,另一個變數則用於決定有效顏色:藍色或綠色。變更這個檔案會觸發部署作業。

MIG_VER_BLUE     = "v1"
MIG_VER_GREEN    = "v1"
MIG_ACTIVE_COLOR = "blue"

以下是 infra/main.tf 的程式碼片段。這個程式碼片段:

  • 為專案定義變數。 Google Cloud
  • Google 設為 Terraform 供應商。
  • 系統會為命名空間定義變數。Terraform 建立的所有物件都會加上這個變數的前置字元,因此應用程式的多個版本可以部署在同一個專案中,且物件名稱不會彼此衝突。
  • 變數 MIG_VER_BLUEMIG_VER_BLUEMIG_ACTIVE_COLORinfra/main.tfvars 檔案中變數的繫結。
variable "project" {
  type        = string
  description = "GCP project we are working in."
}

provider "google" {
  project = var.project
  region  = "us-west1"
  zone    = "us-west1-a"
}

variable "ns" {
  type        = string
  default     = "ns1-"
  description = "The namespace used for all resources in this plan."
}

variable "MIG_VER_BLUE" {
  type        = string
  description = "Version tag for 'blue' deployment."
}

variable "MIG_VER_GREEN" {
  type        = string
  description = "Version tag for 'green' deployment."
}

variable "MIG_ACTIVE_COLOR" {
  type        = string
  description = "Active color (blue | green)."
}

下列 infra/main.tf 程式碼片段顯示了分割器模組的例項化。這個模組會接收有效顏色,讓分割器負載平衡器知道要將應用程式部署到哪個 MIG。

module "splitter-lb" {
  source               = "./splitter"
  project              = var.project
  ns                   = "${var.ns}splitter-"
  active_color         = var.MIG_ACTIVE_COLOR
  instance_group_blue  = module.blue.google_compute_instance_group_manager_default.instance_group
  instance_group_green = module.green.google_compute_instance_group_manager_default.instance_group
}

infra/main.tf 中的下列程式碼片段會為 Blue 和 Green MIG 定義兩個相同的模組。這個函式會接收顏色、網路和子網路,這些項目是在分割器模組中定義。

module "blue" {
  source                               = "./mig"
  project                              = var.project
  app_version                          = var.MIG_VER_BLUE
  ns                                   = var.ns
  color                                = "blue"
  google_compute_network               = module.splitter-lb.google_compute_network
  google_compute_subnetwork            = module.splitter-lb.google_compute_subnetwork_default
  google_compute_subnetwork_proxy_only = module.splitter-lb.google_compute_subnetwork_proxy_only
}

module "green" {
  source                               = "./mig"
  project                              = var.project
  app_version                          = var.MIG_VER_GREEN
  ns                                   = var.ns
  color                                = "green"
  google_compute_network               = module.splitter-lb.google_compute_network
  google_compute_subnetwork            = module.splitter-lb.google_compute_subnetwork_default
  google_compute_subnetwork_proxy_only = module.splitter-lb.google_compute_subnetwork_proxy_only
}

檔案 splitter/main.tf 會定義為分割器 MIG 建立的物件。以下是 splitter/main.tf 的程式碼片段,其中包含在綠色和藍色 MIG 之間切換的邏輯。這項服務以 google_compute_region_backend_service 為後端,可將流量導向兩個後端區域:var.instance_group_bluevar.instance_group_greencapacity_scaler:定義要轉送的流量比例。

以下程式碼會將 100% 的流量導向指定顏色,但您可以更新此程式碼,進行 Canary 部署,將流量導向部分使用者。

resource "google_compute_region_backend_service" "default" {
  name                  = local.l7-xlb-backend-service
  region                = "us-west1"
  load_balancing_scheme = "EXTERNAL_MANAGED"
  health_checks         = [google_compute_region_health_check.default.id]
  protocol              = "HTTP"
  session_affinity      = "NONE"
  timeout_sec           = 30
  backend {
    group           = var.instance_group_blue
    balancing_mode  = "UTILIZATION"
    capacity_scaler = var.active_color == "blue" ? 1 : 0
  }
  backend {
    group           = var.instance_group_green
    balancing_mode  = "UTILIZATION"
    capacity_scaler = var.active_color == "green" ? 1 : 0
  }
}

mig/main.tf 檔案會定義與 Blue 和 Green MIG 相關的物件。這個檔案中的下列程式碼片段定義了用於建立 VM 集區的 Compute Engine 執行個體範本。請注意,這個執行個體範本的 Terraform 生命週期屬性已設為 create_before_destroy。這是因為更新集區版本時,如果舊版集區仍在使用範本,您就無法使用範本建立新版集區。但如果先毀損舊版集區,再建立新範本,集區就會有一段時間無法運作。為避免這種情況,我們將 Terraform 生命週期設為 create_before_destroy,以便先建立較新版本的 VM 集區,再刪除舊版。

resource "google_compute_instance_template" "default" {
  name = local.l7-xlb-backend-template
  disk {
    auto_delete  = true
    boot         = true
    device_name  = "persistent-disk-0"
    mode         = "READ_WRITE"
    source_image = "projects/debian-cloud/global/images/family/debian-10"
    type         = "PERSISTENT"
  }
  labels = {
    managed-by-cnrm = "true"
  }
  machine_type = "n1-standard-1"
  metadata = {
    startup-script = <<EOF
    #! /bin/bash
    sudo apt-get update
    sudo apt-get install apache2 -y
    sudo a2ensite default-ssl
    sudo a2enmod ssl
    vm_hostname="$(curl -H "Metadata-Flavor:Google" \
    http://169.254.169.254/computeMetadata/v1/instance/name)"
    sudo echo "<html><body style='font-family: Arial; margin: 64px; background-color: light${var.color};'><h3>Hello, World!<br><br>version: ${var.app_version}<br>ns: ${var.ns}<br>hostname: $vm_hostname</h3></body></html>" | \
    tee /var/www/html/index.html
    sudo systemctl restart apache2
    EOF
  }
  network_interface {
    access_config {
      network_tier = "PREMIUM"
    }
    network    = var.google_compute_network.id
    subnetwork = var.google_compute_subnetwork.id
  }
  region = "us-west1"
  scheduling {
    automatic_restart   = true
    on_host_maintenance = "MIGRATE"
    provisioning_model  = "STANDARD"
  }
  tags = ["load-balanced-backend"]

  # NOTE: the name of this resource must be unique for every update;
  #       this is wy we have a app_version in the name; this way
  #       new resource has a different name vs old one and both can
  #       exists at the same time
  lifecycle {
    create_before_destroy = true
  }
}