部署到 Compute Engine

本指南介绍了如何使用 Cloud Build 和 Terraform 在 Compute Engine 代管式实例组 (MIG) 上执行零停机蓝绿部署。

Cloud Build 可让您自动执行各种开发者流程,包括将应用构建和部署到各种 Google Cloud 运行时(例如 Compute Engine、Google Kubernetes Engine、GKE Enterprise 和 Cloud 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

所需的 Identity and Access Management 权限

  1. 在 Google Cloud 控制台中,前往 Cloud Build 权限页面:

    前往权限

  2. 对于指定的 Cloud Build 服务账号默认的 Cloud Build 服务账号,请将以下角色的状态设置为已启用

    • Compute Instance Admin v1 (roles/compute.instanceAdmin) | 使 Cloud Build 能够将新实例部署到 Compute Engine。
    • Storage Admin (roles/storage.admin) | 允许从 Cloud Storage 读取和写入数据。
    • Artifact Registry Writer (roles/artifactregistry.writer) | 允许从 Artifact Registry 拉取映像以及向 Artifact Registry 写入映像。
    • 日志写入者 (roles/logging.logWriter) | 允许将日志条目写入 Cloud Logging。
    • Cloud Build Editor (roles/cloudbuild.builds.editor) | 允许您的服务账号运行 build。

设计概览

下图展示了本文档中所述代码示例使用的蓝绿部署模型:

蓝/绿模型

从宏观层面来看,此模型包含以下组件:

  • 两个 Compute Engine 虚拟机池:蓝色和绿色。
  • 三个外部 HTTP(S) 负载平衡器:
    • 一种蓝绿负载均衡器,可将最终用户的流量路由到蓝色或绿色虚拟机实例池。
    • 一个蓝色负载均衡器,用于将流量从质量保证工程师和开发者路由到蓝色虚拟机实例池。
    • 一个绿色负载均衡器,用于将来自 QA 工程师和开发者的流量路由到绿色实例池。
  • 两组用户:
    • 有权访问蓝绿负载均衡器的最终用户,该负载均衡器会将他们定向到蓝色或绿色实例池。
    • 需要同时访问这两组池的质量检查工程师和开发者,以便进行开发和测试。他们可以访问蓝色和绿色负载平衡器,这些负载平衡器会将他们分别路由到蓝色实例池和绿色实例池。

蓝色和绿色虚拟机池以 Compute Engine MIG 的形式实现,外部 IP 地址通过外部 HTTP(s) 负载平衡器路由到 MIG 中的虚拟机。本文档中描述的代码示例使用 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 虚拟机实例(两个用于绿色 MIG,两个用于蓝色 MIG)、三个负载平衡器(蓝色、绿色和拆分器)以及三个公共 IP 地址。
  • 输出可用于查看蓝色和绿色实例中已部署应用的 IP 地址。

销毁触发器会手动触发,以删除应用触发器创建的所有资源。

测试

  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. 点击最新 build。

    您会看到构建详情页面,其中显示了一个包含三个构建步骤的 Cloud Build 流水线:第一个构建步骤在 Cloud Source Repositories 中创建一个代码库,第二个步骤将 GitHub 中示例代码库的内容克隆到 Cloud Source Repositories,第三个步骤添加两个构建触发器。

  5. 打开 Cloud Source Repositories:

    打开 Cloud Source Repositories

  6. 在代码库列表中,点击 copy-of-gcp-mig-simple

    在页面底部的历史记录标签页中,您会看到一个提交,其说明为 A copy of https://github.com/GoogleCloudPlatform/cloud-build-samples.git,这是由 Cloud Build 创建的,用于创建名为 copy-of-gcp-mig-simple 的代码库。

  7. 打开 Cloud Build 的触发器页面:

    打开“触发器”页面

  8. 您会看到两个名为 applydestroy 的 build 触发器。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 控制台中打开构建记录页面:

    打开“构建历史记录”页面

  13. 点击第一个 build,打开build 详情页面。

    您将看到包含两个构建步骤的 apply 触发流水线。第一个 build 步骤会执行 Terraform apply,以创建部署所需的 Compute Engine 和负载均衡资源。第二个 build 步骤会输出您可以在其中查看应用运行情况的 IP 地址。

  14. 在浏览器中打开与绿色 MIG 对应的 IP 地址。您将看到类似于以下屏幕截图的界面,其中显示了部署情况:

    部署

  15. 前往 Compute Engine 的实例组页面,查看蓝色实例组和绿色实例组:

    打开“实例组”页面

  16. 打开虚拟机实例页面,查看以下四个虚拟机实例:

    打开“虚拟机实例”页面

  17. 打开外部 IP 地址页面,查看三个负载平衡器:

    打开“外部 IP 地址”页面

了解代码

此代码示例的源代码包括:

  • 与设置脚本相关的源代码。
  • 与 Cloud Build 流水线相关的源代码。
  • 与 Terraform 模板相关的源代码。

设置脚本

setup.sh 是运行引导过程并为蓝绿部署创建组件的设置脚本。该脚本执行以下操作:

  • 启用 Cloud Build、Resource Manager、Compute Engine 和 Cloud Source Repositories API。
  • 向您项目中的 Cloud Build 服务账号授予 roles/editor IAM 角色。Cloud Build 需要此角色才能创建和设置部署所需的 GitOps 组件。
  • 向您项目中的 Cloud Build 服务账号授予 roles/source.admin IAM 角色。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 构建步骤,用于调用安装 Terraform 的函数 tf_install_in_cloud_build_steptf_apply 用于创建 GitOps 流程中使用的资源。函数 tf_install_in_cloud_build_steptf_applybash_utils.sh 中定义,构建步骤使用 source 命令来调用它们。
  • describe_deployment build 步骤,用于调用可输出负载平衡器 IP 地址的函数 describe_deployment

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。build config 文件会调用此函数来动态安装 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 和绿色 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 中的以下代码段为蓝色和绿色 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 和蓝色 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 定义了与蓝色和绿色 MIG 相关联的对象。此文件中的以下代码段定义了用于创建虚拟机池的 Compute Engine 实例模板。请注意,此实例模板的 Terraform 生命周期属性设置为 create_before_destroy。 这是因为,在更新池的版本时,如果模板仍被旧版池使用,您就无法使用该模板创建新版池。但是,如果在创建新模板之前销毁了旧版本的池,则会有一段时间池处于关闭状态。为避免这种情况,我们将 Terraform 生命周期设置为 create_before_destroy,以便先创建较新版本的虚拟机池,然后再销毁较旧版本。

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
  }
}