freeCodeCamp.org

How to Migrate an Application from AWS to GKE [Full Book]

7.1内容质量
How to Migrate an Application from AWS to GKE [Full Book]

TL;DR · AI 摘要

How to Migrate an Application from AWS to GKE Full Book October 16, 2026 / Devops Osomudeya Zudonu This guided lab walks...

核心要点

  • 主题聚焦:How to Migrate an Application from AWS to GKE Fu
  • 来源:freeCodeCamp.org,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#编程#前端#后端#云计算
打开原文

如何将应用从 AWS 迁移到 GKE [完整指南]

2026年10月16日

/

#Devops

Osomudeya Zudonu

本实践教程将带您逐步完成一个小型支付应用的迁移过程,该应用包含一个 React 用户界面和一个运行在 AWS 单个 EC2 实例上的 Express API,而 PostgreSQL 数据库仍保留在 AWS 上。

您将把用户界面和 API 迁移到 Google Kubernetes Engine,同时不改变客户已经使用的公共 URL,逐步将流量从 AWS 以百分比形式转移到 GKE。

最终,您将本地运行该应用,搭建 AWS 环境,通过八阶段 Terraform 构建 GKE,测试新环境,将生产流量转移到 GKE,完成迁移并清理不再需要的组件。

下图展示了迁移过程的中间阶段,两个云平台同时运行,客户仍使用同一个 URL,数据库保留在 AWS。

围绕该请求路径,还有三个系统协同工作:

目录

  • 迁移前准备 需要安装的工具 需要掌握的技能 成本估算 本书使用方式 团队介绍
  • 第一部分:如何运行 ClearPay 并部署到 AWS 第1章:在笔记本电脑上运行 ClearPay 第2章:阅读关键代码 第3章:验证重试机制不会重复收费 第4章:使用 Terraform 构建 AWS 服务器 第5章:将 ClearPay 部署到 AWS 服务器 第6章:记录 AWS 基线数据
  • 第二部分:如何规划迁移 第7章:阅读迁移任务说明 第8章:梳理 ClearPay 的依赖关系 第9章:理解 GCP 架构设计
  • 第三部分:如何使用 Terraform 构建 Google Cloud 第10章:设置 gcloud 并选择项目 第11章:通过八阶段 Terraform 构建 GCP
  • 第四部分:如何将 ClearPay 部署到 GKE 预发布环境 第12章:检查 API 镜像 第13章:为 ClearPay 准备集群 第14章:构建 CI/CD 流水线 第15章:检查预发布环境并运行发布门禁
  • 第五部分:如何使 GKE 具备生产就绪性 第16章:运行安全审查 第17章:使用 Prometheus 和 Grafana 监控 ClearPay(可选) 第18章:故意对预发布环境进行负载测试
  • 第六部分:如何将客户迁移到 GKE 第19章:部署生产环境和 GKE 网关 第20章:执行迁移前检查清单 第21章:将 1% 流量导向 GKE 第22章:将流量提升至 100% 第23章:当数据指标出现异常时
  • 第七部分:如何完成迁移 第24章:通过数据指标验证迁移 第25章:在 GKE 上运行并设定 AWS 停用日期 第26章:停用 AWS 服务器 第27章:拆除 GCP 和数据库 第28章:编写迁移记录
  • 项目后续 本项目遗留的关键问题
  • 最终应具备的成果
  • 如何在面试中讨论本项目

迁移前准备

需要安装的工具

在对应章节需要时安装每个工具。第一天不需要安装所有工具。

章节 | 工具 --- | --- 1 | Docker Desktop(或 Linux 上带 Compose 的 Docker Engine)、Git 和<br>``python3` 4 | AWS 账户、AWS CLI(运行一次 `aws configure`)、Terraform 1.5 或更新版本、`rsync` 和 SSH 密钥 6 | 笔记本电脑上的 Node.js 20 或更新版本。`scripts/` 中的检查脚本使用 `node` 读取 JSON 10 | 开启计费功能的 GCP 项目、`gcloud` CLI 和 `kubectl` 14 | GitHub 账户和您在 GitHub 上的本仓库副本 19 | `kustomize`(在 Mac 上通过 `brew install kustomize`` 安装,或参见 kubectl.docs.kubernetes.io)

在 Windows 上,使用 Git Bash 或 WSL 执行本书中的所有命令。这些命令是为 Bash shell 编写的。

需要掌握的技能

无需具备 Kubernetes、Terraform、GCP 或负载均衡器的相关经验。无需购买域名。在 AWS 上,你只需打开 https:// 并添加弹性 IP 地址。浏览器会提示存在自签名证书,点击通过后即可获得与笔记本电脑上相同的 ClearPay 登录界面。在 GCP 上,只需添加一行 /etc/hosts 配置,使 https://api.clearpay.test 指向负载均衡器,除非你启用了 Cloud DNS。

成本说明

AWS 的成本约为每天 1.25 美元。GCP 启动后成本约为每天 1.60 至 3 美元。运行整本书内容一个周末的成本仅需几美元。

两个环境都不应持续运行一个月。如果停止超过几天,请彻底关闭:

code
./scripts/teardown-gcp.sh
cd infra/aws/terraform && terraform destroy

两个文件包含详细成本数据:infra/aws/ COST.md 和 infra/gcp/ COST.md。本书会引导你查看这两个文件,而不会指向其他文档。

本书的使用方式

克隆代码库并从其根目录运行所有命令(除非某步骤要求切换目录):

code
git clone https://github.com/Osomudeya/migrate-api-aws-gke-devops.git

cd migrate-api-aws-gke-devops

代码库中已包含应用代码、Terraform 配置、Kubernetes 文件、流水线配置和脚本。

你无需编写新代码。你的任务是运行每一步,阅读系统输出内容,并判断是否继续执行。

本书的阅读体验如同一个故事:你将在第一周加入一家小型支付公司,每一章都是团队成员分配给你的任务。

每章以时间戳和场景开头,然后遵循相同流程:

#### 最重要规则

不要运行你尚未理解的命令。如果本书尚未解释原因,请继续阅读,因为"为什么"正是本书的重点。

本书包含两种类型的步骤。

第一类仅询问步骤是否成功,例如文件是否复制成功或容器是否启动。如果成功,请继续。

第二类要求你思考发生了什么。命令执行完成后,请阅读输出内容并理解其含义,再进入下一章。

#### 各章内容

按顺序阅读所有章节,不要跳过任何章节。第一章不会要求你安装 Kubernetes 或开通 GCP,这些内容会在后续章节出现。

| 部分 | 章节 | 内容 | |------|------|------| | 1. 运行 ClearPay 并部署到 AWS | 1–6 | 你将在笔记本电脑上运行应用,部署到 AWS 服务器,并记录其性能表现 | | 2. 制定迁移计划 | 7–9 | 你将阅读任务说明,分析应用依赖项,并阅读 GCP 架构设计 | | 3. 使用 Terraform 构建 Google Cloud | 10–11 | 你将通过 8 个阶段逐步构建 GCP 环境 | | 4. 将 ClearPay 部署到 GKE 测试环境 | 12–15 | 流水线将 ClearPay 部署到 GKE 测试环境,晋升门禁会进行验证 | | 5. 使 GKE 生产环境就绪 | 16–18 | 你将审查安全设置,搭建仪表盘,并故意破坏测试环境 | | 6. 将客户迁移到 GKE | 19–23 | 你将部署生产环境,并将流量从 1% 逐步提升至 100% | | 7. 完成迁移 | 24–28 | 你将验证迁移效果,停用 AWS,拆除所有环境,并撰写总结报告 |

#### 如果某步骤执行失败

留在当前章节,使用该章节的"如果失败"步骤处理问题。不要跳到后续章节。向前传递的问题会越来越难定位,因为下一章会在现有问题基础上叠加新的云环境。

任务不是记忆 Kubernetes 或 Terraform。而是学习如何处理真实的迁移:理解当前运行情况,根据发现的内容做出决策,验证结果,并在出现问题时知道如何应对。

#### 所需时间

这可能需要超过一整天的时间。如果你需要离开超过一两天,销毁 AWS 和 GCP 的资源,以免在闲置期间被计费。相关命令请参见上方“成本说明”部分。

团队

这个实验以 Zee 开始,他是 ClearPay 团队的一名初级 DevOps 工程师,第一周。你将扮演 Zee。

  • Maya 是工程经理。她要求证据。
  • Daniel 是高级工程师。他会在服务器、Kubernetes 和容量规划方面与你协作。
  • Amara 负责维护支付 API。她只关心一件事:确保客户不会被重复收费。
  • Chris 负责客户支持。他是第一个得知转账失败的人。

你的任务是在不造成客户可见中断的情况下,将支付 API 从 AWS EC2 服务器迁移到 GKE,同时 PostgreSQL 保留在 AWS。

第 1 部分:如何运行 ClearPay 并部署到 AWS

迁移需要一个你信任的起点。在本部分中,你将在自己的笔记本电脑上运行 ClearPay,以公司当前的方式将其部署到 AWS,并记录其运行速度。后续所有章节都会将 GKE 的表现与这些数据进行对比。

第 1 章:在笔记本电脑上运行 ClearPay

第一周,星期一,上午 9:05

你的笔记本电脑是新的。Wi-Fi 密码写在便签纸上。收件箱里的第一条消息来自工程经理 Maya,内容简短。

Maya: “首先在你的笔记本电脑上运行 ClearPay。登录后发送 20 美元,然后我们再讨论将 API 迁移到 GKE 的事情。”

代码在代码库中:api/、ui/ 和 db/。目前还没有任何服务在运行。

#### 为什么这很重要

后续章节需要一张良好登录界面和成功发送 20 美元的示例图。当 AWS 或 GKE 出现故障时,你将用这张图作为对比基准。

#### 你即将运行的内容

ClearPay 由三个部分组成,每个部分都有特定职责。

  • UI 是客户看到并操作的部分:登录页面、余额和发送按钮。UI 代表用户界面。
  • API 是执行实际工作的部分。它没有界面。UI 会向其发送消息,例如“给 Bob 发送 20 美元”。API 会验证消息,执行操作并返回回复,例如“完成”。API 代表应用程序编程接口。可以将其想象成一扇有规则的门。UI 以固定方式提问,API 以固定方式回答。
  • 数据库是 ClearPay 记录所有信息的地方:每个账户、余额和支付记录。PostgreSQL 是 ClearPay 使用的数据库程序。
  • 服务器是一台全天候运行并等待请求的计算机。API 运行在这样的服务器上。

咖啡馆的工作方式与此类似。菜单是 UI,服务员是 API,厨房及其记录是数据库。顾客永远不会走进厨房。顾客会告诉服务员自己的需求。

本章将在你的笔记本电脑上运行这三个部分。后续章节会将 UI 和 API 迁移到 Google Cloud,而数据库保留在 AWS。

#### 步骤

启动 Docker Desktop。本章不需要安装 Node 或 PostgreSQL,它们会在容器内运行。

从包含 docker-compose.yml 的文件夹中执行:

code
docker compose up --build

首次构建需要几分钟时间。终端会持续输出日志,直到你按下 Ctrl+C 才会返回提示符。让它持续运行。

等待直到出现以下三行。左侧的名称可能以你的文件夹名称开头:

code
Container <folder>-postgres-1 健康
api-1       | {"message":"api_started", ...}
ui-1        |   ➜  本地:   http://localhost:5173/

"Healthy" 表示 Postgres 已启动。"api_started" 表示 API 已启动。"localhost:5173" 表明 UI 已启动。

在相同文件夹中打开第二个终端,创建两个演示账户:

code
docker compose exec api node db/seed.js

这会直接运行种子脚本。API 镜像故意没有安装 npm,因为第 14 章的安全扫描会将其标记出来。

预期输出:

code
seeded: [email protected] (密码: password123)
seeded: [email protected] (密码: password123)

如果看到 skip (already exists): [email protected],表示之前运行已创建过账户,这没有问题。

现在正在运行的内容如下:

#### 验证

在第二个终端中执行:

code
curl localhost:4000/health

预期输出如下。你的实例值会是容器 ID:

code
{"status":"ok","served_by":{"platform":"aws","version":"1.0.0","instance":"14b8d5d767ea","commit":"local","rollback_active":false}}

"status":"ok" 表示验证通过。served_by 会标识响应请求的平台。在你的笔记本电脑上显示 aws 是因为 docker-compose.yml 设置了 PLATFORM: aws。后续在 GKE 上运行时会显示 gke,这个字段能告诉你每个请求由哪个云平台处理。

现在打开 http://localhost:5173 并登录。种子脚本已创建 Alice 账户,所以不要重复注册她:

向 [email protected] 发送 $20。转账会显示在"最近活动"中,显示为"已发送"并带有简短 ID,例如"已发送 · a1b2c3d4"。

在仪表板上关注两个位置。后续章节会持续观察这两个位置:

  • "最近活动"中每条记录末尾都带有 served by aws/1.0.0。这表示写入转账的 API 进程的平台和版本。该信息会与转账一起存储在数据库中。
  • 顶部的黑色条状区域在 /health/ready 验证通过时显示"All systems operational"。这表示 API 进程已启动且能连接到 Postgres。这个状态与登录验证无关。密码错误时表单会显示红色错误,但状态栏仍会显示"All systems operational"。

第 5 章后,你会在 AWS 地址上打开相同的仪表板。使用相同登录名,关注相同两个位置。

#### 如果失败:

  • failed to connect to the docker API or docker.sock:Docker Desktop 未运行。启动它,等待就绪后再次运行 docker compose up --build。
  • 注册页面显示 email already registered:Alice 账户已存在。返回登录页面。
  • invalid credentials:密码应为完整字符串 password123。如果仍然失败,请再次运行种子命令。出现 skip (already exists) 表示账户已存在。
  • docker compose up 立即退出:在第二个终端运行 docker compose logs api。首次启动较慢时,API 可能在 Postgres 接受连接前启动。请再次运行 docker compose up。
  • 4000 或 5173 端口已被占用:机器上的其他程序占用了该端口。停止该程序,或修改 docker-compose.yml 中的端口映射。

#### 决策

如果转账成功,应用已正常工作。继续后续步骤。

如果失败,请在本机修复问题。AWS 和 GKE 的调试更复杂。在此机器上,只需运行 docker compose logs 命令即可定位问题。

#### 完成标准:

  • docker compose up 无错误运行
  • 种子脚本报告创建或跳过两个账户
  • 在 UI 中发送 $20 并在"最近活动"中看到记录

第 2 章. 阅读关键代码

星期一,上午 11:30

code

$20的转账已经到账。你仍然盯着转账记录时,一把椅子滑到你的旁边。丹尼尔,坐在隔壁桌的高级工程师,手握咖啡,神情从容。

丹尼尔朝你的屏幕瞥了一眼。"你不需要记住每一行代码。要了解每个文件的作用,这样在迁移时能快速找到对应文件。"

后续每一章都会涉及这四个文件中的某一个。现在花五分钟了解,之后在高压环境下就不用浪费时间搜索了。

按以下顺序打开这些文件:

- api/src/index.js 启动API并连接所有路由:健康检查、/api/info、登录和交易。

- api/src/servedBy.js 仅有17行代码。它从环境变量中读取PLATFORM、APP_VERSION和ROLLBACK_ACTIVE。仪表板使用它来显示served by aws/1.0.0。

- api/src/routes/transactions.js 处理转账功能。在文件顶部附近能找到idempotency_key检查。该检查会在其他任何操作之前执行。

- api/src/db.js 连接API到PostgreSQL数据库。查找DB_POOL_SIZE || '10'。

丹尼尔在最后一个文件前拦住你。

"数据库连接就是通往数据库的开放通道。一个API进程最多可以同时保持十个连接。记住这个数字。"

#### 现在你已掌握的知识

index.js → 启动API并连接路由 servedBy.js → 显示哪个云平台响应(aws或gke) routes/transactions.js → 处理转账,优先检查idempotency key db.js → 连接PostgreSQL,每个进程最多10个连接

code

servedBy.js 填充了仪表板上served by aws/1.0.0的显示内容。这表示响应的云平台,而非登录用户。

目前只有一个API进程,因此十个连接就是整个连接池。在GKE上,多个API副本会同时运行。Kubernetes将每个副本称为一个Pod。每个Pod拥有独立的十个连接池,但所有Pod共享同一个数据库。第22章会再次讨论这个计算方式。

- 不看文档你也能说出哪个文件设置了连接限制,哪个文件指定了平台名称

### 第3章 证明重试不会重复扣款

星期一,下午2:10

午餐后,你收到一条来自陌生人的消息。阿玛拉维护支付API。玛雅告诉她你正在笔记本电脑上运行ClearPay,因此她跳过了寒暄。

阿玛拉:"用相同的密钥重复发送两次$20。爱丽丝的余额应减少$20,而非$40。"

爱丽丝向鲍勃转账$20。她点击发送后屏幕冻结。她不确定支付是否成功,于是再次点击发送。

没有保护机制时,API会看到两次请求并重复扣款两次。爱丽丝为一次支付损失了$40。

idempotency_key阻止了这种情况。"Idempotent"意味着重复操作是安全的:重复执行与执行一次的结果相同。

这个密钥是应用为每次支付添加的标签,例如round-a。

- 第一次请求带着round-a标签到达。API扣款$20并保存该标签。

- 第二次请求带着相同标签到达。API发现round-a已存在。它知道这是同一笔支付,因此不扣款,只返回第一次的结果。

相同标签代表相同支付。

在迁移期间这一点尤为重要。有时第一次点击会发送到AWS,重试会发送到GKE。这两个是不同服务器,但使用同一个数据库。GKE在数据库中找到round-a后不会重复扣款。共享数据库确保用户在两个云平台都得到保护。

本章使用curl而非"发送资金"按钮。UI为每次点击生成新密钥,因此每次点击都是独立转账。

第4章 使用Terraform构建AWS服务器

星期二,上午9:15

Maya在去开会的路上停在你的办公桌前。昨天你告诉她ClearPay可以在你的笔记本电脑上运行,并且重试支付只会收取一次费用。

Maya:"很好。现在,localhost不能作为基准。把ClearPay部署到AWS上,并证明客户可以在那里完成交易。之后我们再讨论离开AWS的事情。Daniel会和你一起处理这个任务。"

Daniel从隔壁的办公桌滑过来椅子。

Daniel:"在开始之前有一个规则。不要跳过terraform plan步骤。"

本书从许多小型公司采用的初始配置开始:一台没有Docker的Linux服务器。

systemd(Linux服务管理器)在服务器启动时启动Node API。nginx位于其前面,并在443端口处理HTTPS。PostgreSQL运行在Amazon RDS(托管数据库服务)上。

没有使用 AWS 负载均衡器。负载均衡器的作用是将流量分发到多个服务器,但当前架构只使用了一台服务器。添加负载均衡器会导致每小时产生额外费用却没有任何收益。具体费用数据请参见 infra/aws/COST.md 文件。

api/Dockerfile 文件确实存在,但仅在你的笔记本电脑上使用。AWS 服务器从不运行 Docker。

Daniel 说明:"EC2 服务器不会安装 Docker。systemd 会在系统启动时同时启动 Node.js 和 nginx。这就是你将继承的服务器配置。"

上图展示了客户请求如何通过 AWS 到达 ClearPay。弹性 IP 为 EC2 服务器提供固定的公网地址。Nginx 在 443 端口处理 HTTPS 流量,提供前端文件,并将 API 请求转发到由 systemd 管理的 Node.js 进程(运行在 4000 端口)。API 通过 Amazon RDS 上的 PostgreSQL 数据库读写支付数据,目前该数据库没有公网端点。

本章涉及两个 AWS 术语:

- VPC 是服务器所在的私有网络。AWS 为每个账户提供默认 VPC。本实验使用默认 VPC 而非自建网络。

- 安全组是 AWS 的防火墙。它定义哪些端口可以访问服务器。本实验仅允许从你的 IP 地址进行 SSH 访问,允许任意来源的 HTTPS 访问,以及仅允许 API 服务器访问的 PostgreSQL 数据库。

首先确认你的笔记本电脑已准备就绪。以下命令必须全部正常运行:

docker --version # 后续仍会用到 aws sts get-caller-identity terraform version # 1.5 或更新版本 ssh-keygen -t ed25519 -f ~/.ssh/clearpay-deployer -N ""

code

aws sts get-caller-identity 命令会输出你的 AWS 账户信息。如果出现错误,请使用具有创建 EC2、RDS、VPC 和 CloudWatch 报警权限的 IAM 用户的访问密钥运行 aws configure。使用临时账户的管理员用户也可以。

从仓库根目录执行:

cd infra/aws/terraform cp terraform.tfvars.example terraform.tfvars

code

打开 terraform.tfvars 文件并修改两个参数:

- ssh_allowed_cidr : 你的公网 IP 地址后加 /32 。可通过 curl ifconfig.me 获取你的 IP 地址。

- ssh_public_key_path : 你的密钥文件的 .pub 文件路径。

然后执行:

terraform init terraform plan terraform apply

code

在输入 yes 之前请仔细阅读执行计划。首次运行应仅创建资源:EC2、RDS 和安全组。如果计划显示将销毁任何资源,请立即停止并不要执行。

这是 Terraform 构建的基础设施。服务器在第 5 章之前是空的:

该图展示了 Terraform 在 AWS 上创建的基础设施:一个带有弹性 IP 的 Ubuntu EC2 服务器,通过安全组控制 SSH 和 HTTPS 访问,一个仅允许 API 服务器访问的 RDS PostgreSQL 数据库,以及一个 CloudWatch 状态检查。这些资源都位于默认 VPC 内。EC2 服务器目前仍是空的,第 5 章将向其中部署 ClearPay。

关于成本的说明:即使服务器运行,弹性 IP 每天仍会产生约 0.12 美元的费用,因为 AWS 对每个公网 IPv4 地址收费(AWS 公网 IPv4 收费)。

terraform output instance_public_ip

code

该命令会输出一个公网 IPv4 地址。这就是你访问服务器的方式。由于不需要域名,因此 Route 53 保持关闭状态(manage_dns = false)。

- 错误:InvalidKeyPair.Duplicate :名为 clearpay-deployer 的密钥对已在该 AWS 区域存在,可能是之前尝试创建的。请在 AWS 控制台中删除它,或在 infra/aws/terraform/ec2.tf 中修改名称。

- 错误:UnauthorizedOperation :你的 AWS 用户缺少某个资源类型的权限。错误信息会指出具体操作。请添加该特定权限而非直接申请管理员权限。

基础设施已创建。但目前服务器上没有运行任何服务,还需要继续。请继续阅读第 5 章。

- terraform apply 已完成且没有错误

- terraform output instance_public_ip 输出真实的 IP 地址

### 第5章 将 ClearPay 部署到 AWS 服务器

星期二,下午1:40

午餐前 Terraform 应用已完成。AWS 现在显示了一台服务器和一个数据库,但还没有任何服务在为客户提供功能。

Terraform 创建了一个空的服务器和数据库。目前还没有任何服务在为客户提供功能。

你打开终端开始部署。Daniel 拉过椅子坐在你旁边。

Daniel:"Terraform 完成部署只说明服务器存在。让我们把应用部署上去并发送一个真实的请求。"

一个空的服务器无法作为迁移源。第6章将探讨真实的客户请求是否能在 AWS 上正常运行。在登录页面和 API 启动之前,这个问题没有答案。

从仓库根目录开始。如果你仍在 infra/aws/terraform 目录,先运行 cd ../../..。

首先在服务器上安装 Node、nginx 和 certbot:

cd infra/aws/deploy INSTANCE_IP=$(terraform -chdir=../terraform output -raw instance_public_ip) echo "$INSTANCE_IP"

记下这个 IP 地址。新终端窗口不会有 INSTANCE_IP 环境变量

ssh -o StrictHostKeyChecking=accept-new \ -i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP 'bash -s' < bootstrap.sh

code

你应该看到 == ClearPay AWS bootstrap == ,然后是几分钟的 apt-get 输出。bootstrap.sh 脚本很短,可以阅读它。

当输出以以下内容结尾时说明步骤成功:

== Bootstrap 完成 == Node: v20.20.2 自签名 TLS 证书位于 /etc/letsencrypt/live/clearpay,使用 curl -k

code

你的 Node 版本可能不同。以 debconf: 无法初始化前端 开头的行是无害的,这是由于 SSH 会话没有交互式界面导致的。由 + 和 * 组成的行来自 OpenSSL 创建自签名证书的过程。

如果完全没有任何输出,说明 SSH 连接失败。自第4章以来你的家庭 IP 地址可能已改变,因此安全组仍然允许旧的 IP 地址。使用 curl -s ifconfig.me; echo 检查你的 IP 地址,将新 IP 地址加上 /32 写入 terraform.tfvars 的 ssh_allowed_cidr 字段,然后在 infra/aws/terraform 目录运行 terraform apply,再重试。

接下来编写 API 需要的 .env 文件并部署应用。数据库 URL 来自 Terraform。JWT 密钥用于签署登录令牌,会生成新的密钥:

DATABASE_URL=$(terraform -chdir=../terraform output -raw database_url)

cat > /tmp/clearpay.env <<EOF DATABASE_URL=$DATABASE_URL JWT_SECRET=$(openssl rand -hex 32) PLATFORM=aws APP_VERSION=1.0.0 GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo local) ROLLBACK_ACTIVE=false PORT=4000 EOF

./deploy.sh "$INSTANCE_IP" ~/.ssh/clearpay-deployer /tmp/clearpay.env rm /tmp/clearpay.env

code

deploy.sh 会构建 React 用户界面并将其复制到 /opt/clearpay-ui。它会将 API 复制到 /opt/clearpay-api,安装依赖包,写入 .env 文件,安装 systemd 服务和 infra/aws/deploy/ 目录中的 nginx 配置,然后重启相关服务。

现在加载数据库模式并初始化 Alice 和 Bob。这些命令在服务器上运行,而不是你的笔记本电脑。RDS 还没有公共端点(infra/aws/terraform/rds.tf 中的 publicly_accessible = false),所以你的笔记本电脑无法直接访问它。

你的笔记本电脑无法直接访问数据库。它需要通过 EC2 服务器作为中介:

上图展示了为什么你的笔记本电脑无法直接连接到私有 RDS 数据库。允许 SSH 访问 EC2 服务器,且服务器被允许在 AWS 网络内连接到 RDS。你的笔记本电脑直接访问数据库的连接被阻止。你使用 EC2 服务器作为数据库访问的入口点。

以下每个命令均以 `ssh ... ubuntu@$INSTANCE_IP "..."` 开头。这意味着:登录到 EC2 服务器并在服务器上运行引号中的部分。因此 psql 和初始化脚本在 EC2 上运行,EC2 会代表你与 RDS 通信。稍后你将在浏览器中看到结果:你以 Alice 身份登录,EC2 会从 RDS 读取她的账户信息。

scp -i ~/.ssh/clearpay-deployer ../../../db/schema.sql \ ubuntu@$INSTANCE_IP:/tmp/schema.sql

ssh -i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP \ "sudo apt-get install -y postgresql-client -qq && psql '$DATABASE_URL' -f /tmp/schema.sql"

ssh -i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP \ "cd /opt/clearpay-api && sudo -u clearpay npm run seed"

code

初始化脚本与第一章中的脚本相同。这次它会写入 RDS。

ssh -i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP 'sudo systemctl is-active clearpay-api'

code

预期输出:`active` 。

然后从你的笔记本电脑执行:

curl -k https://$INSTANCE_IP/health

code

查找 `"status":"ok"` 和 `"platform":"aws"` 。`-k` 参数告诉 curl 接受 bootstrap.sh 创建的自签名证书。

在浏览器中打开 https://<你的实例IP>。点击 "Not Secure" 警告。该警告来自自签名证书。你应该看到与笔记本电脑上相同的 ClearPay 登录界面。以 Alice 身份使用密码 `password123` 登录。仪表盘会显示她的余额和 `aws/1.0.0` 服务标识。

Daniel:"健康检查只能告诉我们应用正在运行。在写下基准线之前,我想看到这个 IP 上有一次转账成功。"

在浏览器中向 Bob 发送 $20。

- 如果 `systemctl is-active` 返回 `failed`:通过 SSH 登录并运行 `sudo journalctl -u clearpay-api -n 50`。常见原因是 DATABASE_URL 与 RDS 不匹配。逐字符与 `terraform output database_url` 进行比对。

- 如果初始化失败提示 `SELF_SIGNED_CERT_IN_CHAIN`:psql 已经成功运行,说明数据库没有问题。新版 Node pg 库将 `sslmode=require` 视为完整证书验证。将当前的 `api/src/pgConfig.js`、`api/src/db.js` 和 `api/db/seed.js` 复制到服务器,重启 clearpay-api,再次仅运行初始化命令。不要再次运行 schema.sql。

- 如果 nginx 返回 502 Bad Gateway:nginx 正在运行但后端 API 没有运行。先检查上面的 systemd 状态。

- 浏览器显示 `Cannot GET /`:nginx 仍然使用旧的仅 API 配置。运行 `./deploy.sh "$INSTANCE_IP" ~/.ssh/clearpay-deployer` 不带第三个参数。这样可以保留服务器上已有的 .env 文件。不要再次运行初始化。

- Alice 凭证无效:浏览器可能自动填充了保存的密码。清空输入框,输入 `password123` 后重试。

- `systemctl is-active clearpay-api` 输出 `active`

- `curl -k https://$INSTANCE_IP/health` 返回 `"status":"ok"`

- 你已在 AWS 地址的浏览器中成功发送了 $20

### 第6章 记录 AWS 基准线

星期三 上午10:00

你给 Maya 发消息:"健康检查正常,且 Alice 已在弹性 IP 上登录。" 她在一分钟内回复了。

Maya:"很好。现在对这个 IP 执行五项检查,把终端输出发给我,不要截图。"

随后又收到一条消息:"然后进行负载测试,把 p95 数据发给我。我无法将 GKE 与 '它工作' 进行对比。"

本章将为你提供两样东西。你将在本书其余部分使用这两样东西。

1. 通过晋升门的测试。晋升门是一个简短的脚本。它测试支付应用必须执行的五项内容:应用正在运行、能够连接数据库、用户可以登录、重复支付仅收取一次费用、余额减少正确的金额。如果五项测试全部通过,说明应用可以正常工作。

在第15章中,你会在GKE上运行相同的脚本。如果GKE通过了与AWS相同的测试,说明GKE同样可以胜任这项工作。

2. 性能评分表。负载测试会同时发送大量请求,就像许多用户同时使用应用一样。然后它会记录应用响应的速度以及失败的请求数量。这就是AWS当前的表现。之后你会在GKE上运行相同的测试并进行对比。

想象在运动员换鞋前计时。没有第一次的基准数据,第二次的测试结果毫无意义。本章将记录首次测试的数据。

本章的脚本使用你电脑上的Node.js读取JSON文件。如果你尚未安装,请现在安装Node.js 20或更新版本。

从仓库根目录开始。如果你当前在infra/aws/deploy目录,请先运行cd ../../..。

在AWS上运行晋升门测试:

export INSTANCE_IP=$(terraform -chdir=infra/aws/terraform output -raw instance_public_ip)

./scripts/promotion-gate.sh https://$INSTANCE_IP \ [email protected] password123 [email protected]

code

必须使用https://。如果使用http://,nginx会进行重定向,脚本会显示卡顿。

门测试会按顺序检查以下五项内容:

- 进程正在运行(/health)。
- 能够连接数据库(/health/ready)。
- Alice可以登录。
- 重复支付密钥返回replayed=true。
- Alice的余额仅减少一次。

PASS: health端点返回ok PASS: readiness端点确认数据库连接性 PASS: 登录成功并返回令牌 PASS: 重复使用幂等性密钥返回replayed=true PASS: 余额准确扣减一次(100美分),而非两次

5项通过,0项失败

GATE PASSED: 可以安全地部署到生产环境。

code

最后一行提到了生产环境。目前还没有需要部署的生产环境。这与之后在GKE上运行的相同脚本中的那行代码含义一致。

现在对AWS进行负载测试。脚本会登录并访问/api/accounts/me和/api/transactions接口,这两个接口都会查询Postgres数据库。不要对/health接口进行负载测试。API会立即直接响应/health接口,而不会查询数据库。真实的客户请求必须查询数据库并等待响应,这个等待时间占据了大部分时间:

/health 电脑 → API → 响应 (快速,无需数据库) /api/accounts/me 电脑 → API → 数据库 → API → 响应 (实际工作)

code

对/health接口进行负载测试只会测量快速的部分。想象向店员询问"你们营业吗?"与"你们有42码的鞋吗?"。只有第二个问题会迫使店员去仓库查看。

./scripts/load-test.sh https://$INSTANCE_IP \ [email protected] password123 10 30

code

最后两个数字分别是并发数(同时10个请求)和持续时间(30秒)。

脚本会输出性能评分表。这是本实验生成的评分表:

== 数据库负载测试: https://184.194.120.157 并发数=10 持续时间=30秒 == 接口: GET /api/accounts/me GET /api/transactions

成功=372 失败=0

迁移评分表,本次运行

p50延迟: 673 毫秒 p95延迟: 745 毫秒 p99延迟: 1326 毫秒 吞吐量: 12.4 请求/秒 4xx错误率: 0.0 % 5xx错误率: 0.0 % 交易成功率: 100.0 %

code

你的评分表会有所不同。你的 IP 地址、请求数量和毫秒数取决于你所在的位置。负载测试在你的笔记本电脑上运行,因此每个请求都需要从你的家庭网络传输到 AWS us-east-1 区域,然后再返回。

距离该区域较近的用户看到的数字会比位于其他大洲的用户低得多。两者都没有错。重要的是,你要将 GKE 的表现与你自己在同一位置测量的评分表进行对比。

当看到 failed=0 、5xx 错误率为 0.0% 且事务成功率为 100.0% 时,测试通过。

- p95 延迟表示 95% 的请求完成时间都快于这个数值。它展示了慢速用户的实际体验,而平均值会掩盖这一问题。

- 4xx 表示客户端发送了错误请求,例如缺少令牌。

- 5xx 表示服务器端发生故障。

将完整的评分表复制到笔记文件中,然后将相同的内容发送给 Maya。她很快回复道:"不要为了看起来更整洁而四舍五入。我会将 GKE 与这一页进行对比。"

#### 现在设置你的阈值

在评分表下方添加以下两行,填写具体数值:

Outage: 5xx 错误率连续超过 1% 超过 2 分钟 Failure: p95 延迟连续超过你 AWS p95 值的 3 倍以上超过 5 分钟

code

失败阈值使用你自己的 AWS p95 值,因为每个用户的基准线都不同。以本实验中的 745 毫秒为例,阈值约为 2235 毫秒。这些是示例阈值。请自行选择,但要在 GKE 存在之前就确定下来。在看到 GKE 数据后再设定的阈值,实际上是为适应数据而妥协的阈值。

从以下内容开始成本记录:

成本记录,条目 1: AWS 约 1.25 美元/天(infra/aws/COST.md)

code

在此停留,直到所有五个检查项都通过。如果你在迁移过程中携带问题,当涉及两个云平台时,问题会变得更难发现。

- 准备就绪检查失败:API 无法连接到 RDS。返回第 5 章的 "如果失败" 清单。

- node: command not found:在你的笔记本电脑上安装 Node.js。

门禁测试通过。这台 EC2 服务器是基准。本书中所有 GKE 的数据都会与这个评分表进行对比。

一分钟之后,Maya 又添加了一行:"很好。这些现在就是我的数据。"

- 门禁测试显示对 AWS IP 的检查通过了 5 项,0 项失败

- 负载测试显示 failed=0,评分表已记录在你的笔记中

- 你的停机和失败阈值已用具体数值写入记录

## 第二部分:如何规划迁移

告诉 Maya 门禁测试通过,并将评分表发送给她。

她回复道:"下一步是 GKE。我需要在那里运行 API,并且在你迁移过程中,需要确保客户持续支付费用。将 Postgres 保留在 AWS 上。数据库不在本次任务范围内。"

在本部分中,不会接触任何云平台。你只需阅读、映射并做出决策。

### 第 7 章:阅读迁移任务

星期三 下午4:00

Maya 在任务顶部添加了一行:"在打开 Terraform 之前,请阅读全部内容。"

#### 任务:将 ClearPay API 从 AWS EC2 迁移到 GKE

ClearPay 目前运行在一台 EC2 服务器上。它目前可以正常工作,但没有自动扩展、没有分阶段部署,且部署流程只存在于一个人的脑海中。你在第 5 章手动执行过这个流程,因此你知道它的不足之处。

公司希望将 API 迁移到 Kubernetes:需要一个测试环境、自动化部署以及扩展空间。再次强调,数据库不在本次任务范围内。

#### 当前架构

ClearPay 位于一个 AWS 账户中,使用第 4 章提到的默认 VPC:包含一台 EC2 服务器和一个 RDS 数据库。

#### 无法更改的内容

- 数据库。迁移 Postgres 是一个独立的后续项目。

- API 协议。相同的路径返回相同的 JSON:/health、/api/auth/login、/api/transactions。在 AWS 上能正常工作的浏览器或脚本,在 GKE 上无需修改也能正常工作。

- 在整个迁移过程中,客户不会看到任何持续性的服务中断。

#### 接受标准

"完成"意味着按以下顺序达成:

- GKE 承载所有生产流量,并且持续保持这种状态。错误率和延迟保持在第 6 章定义的限制范围内。

- 在 AWS 仍存在期间,回滚到 AWS 的操作仍然有效。

- 在 GKE 证明其稳定性后,再退役 AWS。

#### 迁移内容与保留内容

该图示展示了从 AWS 迁移到 GCP 的过程。ClearPay 的用户界面和 API 从 EC2 服务器迁移到 Google Kubernetes Engine(GKE),其中测试环境和生产环境均运行在 GKE 上。PostgreSQL 仍保留在 AWS 的 Amazon RDS 上,因此在迁移期间两个环境使用相同的数据库。

全书遵循以下路径:

仅 AWS → AWS 与 GKE 共存 → 仅 GKE,随后 EC2 退役

code

中间阶段是实际操作阶段。两个云平台运行相同的应用,均连接到相同的数据库,流量通过逐步转移在两者之间分配。

#### 流量转移方式

目前,客户通过弹性 IP 地址访问服务,所有请求都发送到 AWS。

- 在前端部署 Google 负载均衡器,客户只需切换一次地址即可使用新地址,后续迁移过程均使用该地址。

- 负载均衡器将部分请求发送到 AWS,其余发送到 GKE。初始配置为 100% AWS 和 0% GKE。

- 通过脚本 shift-traffic.sh 手动逐步提高 GKE 的流量占比,每次调整后检查相关指标。

- 如果 GKE 出现故障,可通过 shift-traffic.sh 100 0 将 100% 流量回退到 AWS。回滚操作仅需数秒。

上图展示了客户通过单一公共地址访问 ClearPay 的方式。在迁移期间,用户界面和 API 同时运行在 AWS 和 GKE 上,两个环境均连接到 AWS 上的 Amazon RDS 的相同 PostgreSQL 数据库。将数据库保留在原位置可避免在迁移过程中同步移动支付数据。

为什么不使用 DNS 分割流量?DNS 是将域名转换为 IP 地址的查询过程。笔记本电脑和互联网服务商会缓存查询结果。

修改 DNS 记录后,部分客户可能在数分钟甚至数小时内仍访问旧地址。耗时一小时的回滚操作不能算作真正的回滚。负载均衡器可在数秒内完成流量分配调整。

#### 回滚要求

在迁移的每个阶段,回退到 100% AWS 必须快速且安全。这不是重新部署,也不是需要一小时传播的 DNS 修改。第 20 章会在首次迁移前专门测试这一点。

#### 此处"零停机时间"的含义

这并不意味着永远不会有请求失败。没有真实系统能保证这一点。

这意味着没有任何客户会看到持续性的服务中断。如果失败请求数量控制在第 6 章定义的限制范围内,少量失败请求是可以接受的。

#### 为什么 API 首先迁移

如果 API 和数据库同时迁移,出现问题时故障可能出现在任一系统或它们的交互中。仅迁移 API 可将问题范围缩小:这个 API 能否在 GKE 上运行而不影响客户?

这是一个可回答的问题。"能否一次性安全迁移所有内容?"这个问题要困难得多,现场调试也会复杂得多。

#### 为什么数据库保留在 AWS

第 13 章展示了跨云连接的复杂性。目前:PostgreSQL 在本项目中不会迁移。

一个未来的独立项目将负责处理此事,很可能会使用 Google 的数据库迁移服务、持续复制以及自身的切换操作。这个项目最终会以 GKE 上的 API 和 AWS 上的数据库结束。这是计划,而不是未完成的遗留问题。

#### 为何两个云平台必须同时运行

回滚计划依赖于此。你将少量流量发送到 GKE 并保持 AWS 处于就绪状态。如果 GKE 失败,AWS 会接管所有流量。这只有在 AWS 仍然存在时才能实现。

#### 在接触 GCP 之前设定迁移预算

现在就做这件事,在第 9 章之前。在笔记中写下四个数字。它们决定了这次迁移可能花费你的金额。

像规划旅行资金一样规划支出:

- 当前 AWS 的成本。大约每天 1.25 美元,来自 infra/aws/COST.md。

- GCP 每天的成本。你还不知道。写上 "TBD"(待定)。第 9 章会计算这个数值。infra/gcp/COST.md 的预估是每天 1.60 至 5 美元。

- 当两个云平台同时运行时迁移的成本。迁移期间你同时支付 AWS 和 GCP 的费用。这等于每天 1.25 美元加上 GCP 的每日成本。

- 你的预算。整个迁移过程中你最多愿意花费的金额。在开始任何支出之前,现在就确定这个数值。

本实验的数值:

当前 AWS 成本: 1.25 美元/天 预期 GCP 成本: 约 3 美元/天(第 9 章会验证) 双平台运行成本: 1.25 + 3 = 4.25 美元/天(两个平台同时运行时) 迁移预算: 5 天 × 4.25 美元 = 约 21 美元。向上取整为 25 美元。

code

在迁移过程中持续记录总支出。第 19 至 25 章会增加实际的双平台运行天数。第 28 章会问一个问题:你是否控制在预算范围内?如果迁移因回滚而延长,总支出会准确显示额外成本。

#### 迁移阶段

- 你可以用两句话向 Maya 解释清楚哪些内容会迁移、哪些保持不变,以及如何撤销错误的操作。

### 第 8 章。绘制 ClearPay 所依赖的内容

星期四,上午 9:00

在任何人启动 GKE 之前,团队需要一份书面的 ClearPay 依赖关系图。这个系统自星期二以来一直由人工运行。纸上没有任何内容说明迁移期间必须继续运行的组件。

Maya:“在启动另一个云平台之前,请写下这个系统所依赖的内容。”

没有依赖图的迁移可能会迁移错误的内容,或遗漏在切换当天导致故障的组件。请根据代码库回答以下每个问题,而不是依赖记忆。

#### 1. 应用是否已准备好容器化?

是的。api/Dockerfile 自第 1 章以来一直正常工作。变化的是它的角色。在你的笔记本电脑上它只是一个便利工具,而在 GKE 上它将成为生产环境运行的核心。

#### 2. 密钥存储在哪里?

打开 infra/aws/deploy/systemd/clearpay-api.service。它加载 EnvironmentFile=/opt/clearpay-api/.env。该文件不在 Git 中。它位于 EC2 服务器上,第 5 章的 deploy.sh 已将其复制到该位置。

API 所需的所有密钥都存储在一个服务器上的一个纯文本文件中。请注意这一点。不要打开它来打印密钥。GKE 将从 Google Secret Manager 读取相同的值。

#### 3. 它调用了哪些外部系统?

一个:PostgreSQL。连接仍然在 AWS 默认 VPC 内,且 RDS 没有公共端点。没有第三方 API。通过阅读 api/src/ 确认这一点。

#### 4. 服务器能否在请求之间记住用户?

不能。打开 api/src/index.js 和 api/src/routes/transactions.js。每个请求都携带 JWT(一个签名的登录令牌),所有账户数据都存储在 Postgres 中。服务器不会在内存中保存任何内容。

这正是AWS和GKE能够后续拆分请求的原因。用户可以在AWS上登录并在GKE上转账,因为两个云平台都验证相同的令牌密钥并读取相同的数据库。

#### 5. 重试会重复收费吗?

不会。第3章已证明幂等性密钥有效。当首次请求落在AWS而重试落在GKE时,该机制必须继续有效。

#### 信息图

在笔记中写下这五个答案:

容器就绪: 是,api/Dockerfile 密钥: EC2服务器上的一个.env文件 外部系统: 仅PostgreSQL 服务器状态: 无,JWT + 数据库 重试安全: 幂等性密钥,第3章已证明

code

#### 实际需要迁移的内容

仅需迁移UI和API:从EC2服务器迁移到GKE。PostgreSQL保留在AWS。

浏览器和脚本仍然访问相同的URL并获取相同的JSON响应。重试仍然必须避免重复扣款。这些规则不会改变。

#### 完成标准

- 五个答案已全部记录,每个答案都指向对应的文件

### 第9章 理解GCP设计

星期四,下午2:30

Google Cloud的Terraform配置已经完成。在你加入之前,有人做出了两个重大决策。Maya想知道你是否能为这些决策辩护。她在团队频道中写道:

Maya:"打开infra/gcp/COST.md和infra/gcp/terraform/03-gke。然后告诉我为什么这个仓库使用GKE Standard而非Autopilot。我需要一个能辩护的理由。"

坐在旁边办公桌的Daniel正打开infra/gcp/terraform/03-gke/main.tf。他指着名为primary的代码块。这是节点池:GKE运行应用的虚拟机组。这个节点池包含1到3台e2-medium机器。

Daniel:"如果我们保留这些机器,它们一次最多能运行多少个API副本?"

本章不会执行Terraform。文件已经写好。你只需阅读其中的决策内容。

#### Standard与Autopilot

GKE是Google的托管Kubernetes,运行在两种模式下:

- Standard:你选择虚拟机,按节点池分组。Google无论应用是否繁忙都会对这些机器计费。

- Autopilot:Google隐藏机器。你声明每个容器所需的CPU和内存,Google按这些资源计费。

本仓库使用Standard模式。原因不是成本。Standard模式使节点池、机器规格和自动扩缩可见,而这正是本书要教授的内容。Autopilot会隐藏这些信息。

自行计算两种模式的成本。Standard按机器计费。infra/gcp/COST.md中显示,一台e2-medium机器全天运行约1.44美元,加上Cloud NAT费用。

Autopilot按每个Pod预留的CPU和内存计费。k8s/base/deployment.yaml中每个API Pod预留250m CPU(一个CPU核心的四分之一)和256Mi内存。根据infra/gcp/COST.md中的Autopilot费率,三个API Pod的费用约为:

0.25 vCPU × 3 × $0.0445/小时 × 24 ≈ $0.80 / 天 0.25 GiB × 3 × $0.0049/小时 × 24 ≈ $0.09 / 天

code

三个API Pod每天约0.89美元(不包括UI Pod)。Autopilot的成本可能低于一个Standard机器。实验室仍使用Standard模式,以便你能看到节点池。

告诉Maya这两个数字及原因。

#### API进程数量

AWS运行一个API进程。在GKE中,HorizontalPodAutoscaler(HPA)根据CPU使用情况添加或删除API Pod。每个Pod是一个API进程,拥有自己独立的10个数据库连接池。

本实验集群使用三台小型e2-medium机器。生产环境中的HPA(k8s/overlays/production/patch-hpa.yaml)在1到3个API Pod之间运行,因为这种规格的三台机器无法与GKE自身的系统Pod并行运行更多Pod。

Answer Daniel: "GKE 上运行 1 到 3 个 API Pod,AWS 上同时运行 1 个。每个 Pod 可以打开 10 个连接。"

拥有更大机器的公司会设置更高的上限,例如 3 到 8 个 Pod。第 22 章将解释为什么在流量开始传输前需要将这个上限与数据库限制进行核对。

#### 设计的网络

在 AWS 上,这个实验使用了 AWS 为你创建的默认 VPC,这个网络没有经过专门设计。

在 GCP 上,infra/gcp/terraform/02-network 会构建自己的 VPC,包含命名子网和 Cloud NAT。集群中的机器仅获得私有地址,它们通过 Cloud NAT 访问互联网和后续的 AWS 数据库,所有机器共享一个公共 IP。

#### 区域

01-foundation/variables.tf 将区域设置为 us-east4,这是 Google 与 AWS us-east-1 最接近的区域。所有从 GKE 发往 AWS 数据库的查询都需要跨云传输,因此短距离传输有助于减少延迟。如果你的 AWS 部署不在 us-east-1,构建任何内容前请修改此设置。

左侧是当前已有的内容,右侧是第三部分将构建的内容。

上方的图表展示了从 AWS 迁移到 GCP 的过程。客户使用由 GCP 负载均衡器管理的单个公共地址,该地址将流量分发到现有的 AWS EC2 服务器和 GKE。GKE 在私有节点上运行 UI 和 API,通过 Cloud NAT 提供互联网出站访问。两个环境都连接到 AWS 上的同一 PostgreSQL 数据库。流量逐步转移到 GKE,而数据库保持原位。

你将依次遇到三个公共 IP 地址:

- 弹性 IP。这是 EC2 服务器,客户目前使用它。
- GKE 网关 IP。负载均衡器将 GKE 的流量份额发送到该地址,客户从不手动输入它。
- 来自 08-edge 的负载均衡器 IP。客户只需切换一次到这个地址,流量分配将在其后发生。

#### 如果需要暂停 AWS

第三部分不需要 AWS 持续运行。AWS 在第 11 章(将数据库 URL 复制到 Secret Manager)和第 13 章(让 GKE 访问数据库)时再次需要。

不要停止 EC2 服务器但让 RDS 运行。RDS 占据大部分每日 AWS 费用,停止的 RDS 会在七天后自动重启,弹性 IP 在两种情况下都会产生费用。

如果在一天或两天内继续使用,保持 AWS 运行。如果暂停数天,请销毁它:

cd infra/aws/terraform && terraform destroy

code

要恢复,运行该文件夹中的 terraform apply 并重复第 5 章。你将获得新的弹性 IP 和新的 DATABASE_URL。第 6 章的评分表仍作为基准。

- 你告诉 Maya 标准和 Autopilot 的每日数值,以及实验为何使用标准模式
- 你告诉 Daniel 可以运行多少个 API 进程,每个进程可以打开多少个连接

## 第三部分:如何使用 Terraform 构建 Google Cloud

目前已有运行中的 AWS 应用和评分表。Google Cloud 尚未设置,没有网络、集群和注册表,直到你构建它们。

UI 和 API 后续将在该集群上运行,PostgreSQL 仍保留在 AWS。因此顺序是:首先在 GCP 上构建一个空白环境,然后将应用部署到该环境,最后教应用如何通过互联网连接到 RDS。

本部分仅涉及空白环境的构建。客户尚未迁移。

### 第 10 章:设置 gcloud 并选择项目

星期五,上午 8:45

Google Cloud 是一张白纸。在构建任何内容之前,你的笔记本电脑需要知道在哪个项目中构建,以及你的身份。

将 gcloud、kubectl 和 Terraform 指向正确的 Google 项目,并确保启用计费。

gcloud 是 Google Cloud 的命令行工具。安装完成后,关闭终端并打开一个新的终端窗口。

在 Mac 上:

brew install --cask google-cloud-sdk

code

在 Windows 上,从 [Install the Google Cloud CLI](https://cloud.google.com/sdk/docs/install) 下载安装程序,勾选将 gcloud 添加到 PATH 的选项,然后重新打开 Git Bash。在 Linux 上,请遵循同一页的 Linux 选项卡中的说明。运行 `gcloud version` 应该会输出版本号。

如果 gcloud 安装完成后运行 `kubectl version --client` 失败,请安装 kubectl 及其用于登录 GKE 的插件:

gcloud components install kubectl gcloud components install gke-gcloud-auth-plugin

code

插件必须位于你的 PATH 中。如果之后运行 `kubectl get nodes` 报错 `gke-gcloud-auth-plugin not found`,请运行以下命令并打开新的终端窗口:

echo 'export PATH="$(gcloud info --format="value(installation.sdk_root)")/bin:$PATH"' >> ~/.zshrc

code

这行代码适用于 zsh,这是 Mac 的默认 shell。在使用 Bash 的 Linux 系统上,请将 `~/.zshrc` 改为 `~/.bashrc`。在 Windows 上,安装程序通常会自动处理 PATH。安装完成后重新打开 Git Bash。

现在登录并选择项目:

gcloud auth login --update-adc gcloud projects list gcloud config set project YOUR_PROJECT_ID gcloud billing projects describe YOUR_PROJECT_ID terraform version kubectl version --client

code

`--update-adc` 参数非常重要。普通的 `gcloud auth login` 只能让 gcloud 使用你的账户。Terraform 是一个独立的程序,它会查找名为 Application Default Credentials 的文件。该标志会在浏览器登录时生成该文件。在 Google 页面上,点击允许,并且不要提前关闭标签页。

#### 使用哪个 ID

`gcloud projects list` 会输出两列看似相似的字段:

- NAME 是一个标签,许多项目都共享默认标签 My First Project。
- PROJECT_ID 是唯一的,例如 my-first-project-123456。

复制 PROJECT_ID,不要复制名称。在命令中需要 YOUR_PROJECT_ID 的地方粘贴 PROJECT_ID。

如果你有多个项目,请选择一个启用了计费的项目。对每个 ID 运行 `gcloud billing projects describe`,直到输出包含 `billingEnabled: true`。

你也可以在 Google Cloud Console 中找到 ID:点击页面顶部的项目名称,然后阅读 ID 列。

gcloud config get-value project

code

该命令会输出你的项目 ID。如果输出为 `[None]`,请再次运行 `gcloud config set project` 命令。

- 如果之后 Terraform 报错 `could not find default credentials`:你没有使用标志登录。运行:

gcloud auth application-default login

code

- 如果该命令提示 `cloud-platform scope is required but not consented`:浏览器在你点击允许之前就关闭了。再次运行 `gcloud auth login --update-adc` 并点击允许。

今天只需要完成 gcloud 登录。名称 `api.clearpay.test` 会在第 8 阶段构建负载均衡器时出现,直到那时可以跳过。

- `gcloud config get-value project` 输出你的项目 ID
- `gcloud billing projects describe` 显示 `billingEnabled: true`
- `kubectl version --client` 输出版本号

给 Maya 发消息:"gcloud 已登录。项目已设置。计费已启用。准备好开始 GCP 了。"

Maya:"一次构建一个阶段。每完成一个阶段就告诉我。如果你一次性运行所有八个阶段,而第三个阶段出错,你将不知道该修复哪一个。"

### 第 11 章:用八阶段 Terraform 构建 GCP

星期五上午 10:30,到第二周的星期一

GCP 是通过八个 Terraform 阶段构建的。每个阶段创建目标环境的一个层级。

按顺序运行这些阶段。在进入下一个阶段之前,验证每个阶段的结果。

构建迁移的 Google 端:API、网络、集群、镜像仓库、身份、密钥、错误告警和公共负载均衡器。

#### 为什么是八个阶段?

Terraform 代码分布在 01-foundation 到 08-edge 共八个文件夹中。每个文件夹都有自己的状态文件,仅读取其需要的早期阶段内容。如果 03-gke 阶段失败,问题出在集群本身,而不是某个 1500 行的文件中。

这并不是“运行八次 Terraform”。工作内容是构建八个独立组件,并在下一个组件需要时证明其已成功创建。

状态文件默认保存在你的笔记本电脑本地,这对单人使用是可行的。如果是团队协作,应在第二个人执行 apply 之前,将每个阶段的状态文件迁移到 Google Cloud Storage 存储桶中。

上方的图示展示了 Terraform 如何通过八个阶段构建 GCP 基础设施。它依次启用所需 API、创建网络和私有 GKE 集群(含 Cloud NAT)、配置镜像仓库、身份、密钥、错误监控和公共负载均衡器。Cloud NAT 为私有节点提供出站连接,包括后续迁移中访问 AWS RDS 的能力。

#### 每个阶段的执行方式

每个阶段的文件夹内都使用相同的三个命令:

在 apply 之前先查看计划。每个阶段都会列出预期的计划数量,例如 Plan: 7 to add, 0 to change, 0 to destroy。如果数量不符,或计划包含销毁操作,请先停止并阅读计划内容,再输入 yes 确认。

执行 terraform apply 时会提示 Do you want to perform these actions?。输入 yes 并按回车。

下方的文件夹路径均以仓库根目录为起点。如果你当前在 infra/gcp/terraform/0X-... 目录,可通过 cd ../0Y-... 命令切换到下一阶段。

关于“在控制台检查”的说明:在页面顶部选择项目,当页面包含区域菜单时,将区域设置为 us-east4。如果出现空列表,通常表示菜单仍停留在 us-central1。

#### 第一阶段:启用 Google APIs(01-foundation)

新项目默认关闭所有 Google Cloud 服务。此阶段将启用其中的十个服务:GKE、Artifact Registry、Secret Manager、IAM、日志记录等。如果跳过此步骤,后续的 03-gke 阶段会因权限错误而失败,且错误信息无法定位具体问题。

cd infra/gcp/terraform/01-foundation cp terraform.tfvars.example terraform.tfvars

code

如果 terraform.tfvars 已经存在并包含你的项目 ID,请跳过复制操作。重复复制会覆盖你之前的修改。

打开 terraform.tfvars 文件,将 clearpay-migration-demo 替换为你的项目 ID。然后运行:

预期计划:google_project_service 会执行 10 次,且不涉及集群。

Plan: 10 to add, 0 to change, 0 to destroy.

Changes to Outputs: + project_id = "YOUR_PROJECT_ID" + region = "us-east4"

code

如果计划输出显示 clearpay-migration-demo,请先修正 terraform.tfvars 文件。Secret Manager 和 Service Networking 在 apply 过程中可能会短暂显示 Still creating 状态,持续约半分钟。

检查:

gcloud services list --enabled --filter='config.name:container.googleapis.com'

code

NAME TITLE container.googleapis.com Kubernetes Engine API

code

如果此命令未输出任何内容,说明当前 gcloud 查看的项目未启用该 API。不要继续进入第二阶段。请按以下顺序检查三个可能原因:

- gcloud 指向了不同的项目。运行 gcloud config get-value project。输出结果必须与你在 terraform.tfvars 中设置的项目 ID 一致。如果不一致,请运行 gcloud config set project YOUR_PROJECT_ID 并再次检查。

- 应用未完成。再次在 01-foundation 中运行 terraform plan。完成的阶段会显示 No changes。如果计划仍然想要添加资源,请运行 terraform apply 并输入 yes。

- Google 还未同步完成。新启用的 API 可能需要一分钟才会显示在列表中。等待一分钟后再重新运行检查。

Maya 消息:"第一阶段已应用,Kubernetes 引擎 API 显示为已启用。"

Maya:"很好。接下来是网络,没有其他内容。"

#### 阶段 2:构建网络(02-network)

VPC 是您 Google 项目内的私有网络。可以将其视为集群所在的社区,子网则是其中的一条街道。此阶段创建以下内容:

- clearpay-vpc,私有网络
- clearpay-gke-subnet,集群机器使用的地址范围,包含两个额外的范围 pods 和 services,供阶段 3 使用
- clearpay-router 和 clearpay-nat,Cloud NAT 配置
- clearpay-nat-ip,集群机器在调用外部服务时共享的公共 IP
- 两条防火墙规则,使内部机器可以互相通信,同时允许 Google 的健康检查访问它们

此文件夹中没有 terraform.tfvars。它会从阶段 1 的状态文件中读取您的项目 ID。

cd infra/gcp/terraform/02-network

code

运行:

预期计划:

Plan: 7 to add, 0 to change, 0 to destroy.

code

预期应用结束:

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Outputs:

nat_ip_address = "x.x.x.x" network_name = "clearpay-vpc" subnet_name = "clearpay-gke-subnet"

code

gcloud compute networks describe clearpay-vpc terraform output nat_ip_address

code

查找 name: clearpay-vpc,subnetworks 行以 clearpay-gke-subnet 结尾,以及 autoCreateSubnetworks: false。最后一行表示该网络是设计创建的,而非自动生成。

记录 NAT IP。第 13 章会将 AWS 数据库开放给这个地址。

在控制台中:VPC networks 显示 clearpay-vpc。Cloud NAT 显示 clearpay-nat 在 clearpay-router 上。IP addresses 显示 clearpay-nat-ip。

如果计划无法读取 ../01-foundation/terraform.tfstate,说明阶段 1 尚未完成。

Maya 消息:"clearpay-vpc 和 Cloud NAT 已存在。NAT IP 已记录在您的笔记中。"

Daniel 查看了您的笔记中的 NAT IP。

Daniel:"我们 Pod 向外部世界发出的所有调用都会从这个地址发出。这包括对 AWS 上数据库的调用。稍后我们会告诉 AWS 数据库只接受来自这个地址的连接,不接受其他地址的连接。"

这将在第 13 章实现。在那之前,请一直保留 NAT IP 的记录。

#### 阶段 3:创建 GKE 集群(03-gke)

此阶段创建名为 clearpay 的 Standard 集群,位于 us-east4-a 区域,包含一个名为 primary 的节点池,包含 1 至 3 台 e2-medium 机器。这些机器仅使用私有 IP,并通过 Cloud NAT 访问外部网络。控制平面(Google 管理的集群 API 服务器)具有公共端点,以便您笔记本电脑上的 kubectl 可以访问它。

GKE 首先创建默认节点池,然后 Terraform 会将其删除并创建 primary 节点池,该节点池由您控制。

cd infra/gcp/terraform/03-gke

code

预期计划:集群和 primary 节点池。

Plan: 2 to add, 0 to change, 0 to destroy.

code

应用过程会在 Still creating 状态下持续 7 到 10 分钟。不要按下 Ctrl+C。

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

cluster_name = "clearpay" get_credentials_command = "gcloud container clusters get-credentials clearpay --zone us-east4-a --project YOUR_PROJECT_ID"

code

cluster_endpoint 输出 <sensitive>。您不需要它。

将 kubectl 指向新集群:

$(terraform output -raw get_credentials_command)

code

成功运行后会为 clearpay 生成 kubeconfig 条目。

kubectl get nodes

code

NAME STATUS ROLES AGE VERSION gke-clearpay-primary-........ Ready <none> 2m v1.xx

code

Ready 表示机器已启动并加入集群。ClearPay 尚未在其上运行。

如果 apply 提到 container.googleapis.com,请返回第1阶段。如果提到子网或Pods或服务范围,请返回第2阶段。不要编辑 03-gke 来隐藏这些错误。

如果 kubectl 报错 gke-gcloud-auth-plugin 未找到,集群是正常的。不要销毁它。安装插件并为当前终端修复 PATH:

gcloud components install gke-gcloud-auth-plugin export PATH="$(gcloud info --format='value(installation.sdk_root)')/bin:$PATH" kubectl get nodes

code

在控制台中:Kubernetes 集群显示 clearpay 正在运行。VM 实例显示一个名为 gke-clearpay-primary- 的机器,具有内部 IP 且没有外部 IP。

向 Maya 发送消息:"集群已启动。一个节点处于 Ready 状态。目前还没有我们的服务在上面运行。"

apply 操作耗时九分钟。你全程都在关注。

#### 第4阶段:创建镜像仓库(04-registry)

容器镜像是应用程序的打包副本。GKE 从镜像仓库下载镜像。本阶段在 Artifact Registry 中创建一个名为 clearpay 的私有 Docker 仓库,位于 us-east4 区域。直到第14章的流水线推送内容之前,它将保持为空。

cd infra/gcp/terraform/04-registry

code

这两个资源分别是仓库本身和一个允许集群机器从中拉取镜像的规则。

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

repository_url = "us-east4-docker.pkg.dev/YOUR_PROJECT_ID/clearpay"

code

gcloud artifacts repositories describe clearpay --location=us-east4

code

查找 format: DOCKER、名称以 /repositories/clearpay 结尾,以及 Repository Size: 0.000MB。今天一个空的仓库是正确的。你还会看到 SCANNING_DISABLED。流水线将在第14章使用 Trivy 代替扫描镜像。

向 Maya 发送消息:"镜像仓库已创建。它是空的。"

#### 第5阶段:创建无密钥文件的身份(05-identity)

稍后需要 Google 权限的两个对象:GKE 上的 ClearPay Pod,以及推送镜像和部署时的 GitHub Actions。

旧方法是下载服务账户密钥文件。密钥文件就像磁盘上的密码。如果泄露,任何人都可以使用。本实验从不创建此类文件。

相反,本阶段创建三个 Google 服务账户(具有权限的身份),并允许命名调用者在短时间内借用它们:

- clearpay-api-staging:由 staging 命名空间中的 Pod 借用
- clearpay-api-production:由 production 命名空间中的 Pod 借用
- clearpay-ci:由通过名为 clearpay-github-pool 的 Workload Identity 池的 GitHub Actions 借用

Workload Identity 是 Google 的功能,可将 Kubernetes 服务账户或 GitHub 仓库与 Google 服务账户关联,而无需密钥。

可以将每个服务账户想象成一个徽章。目前没有任何实体佩戴徽章。稍后,Google 允许命名的 Pod 或命名的仓库在短时间内佩戴一个徽章,且永远不会存在密钥文件。

命名空间是集群内部的独立区域。预发布环境和生产环境共享集群的机器,但不共享身份信息,因此预发布环境的 Pod 无法读取生产环境的密钥。真正的公司通常会运行独立的集群,通常位于不同的项目中。本实验使用一个集群以保持账单金额较小。

cd infra/gcp/terraform/05-identity cp terraform.tfvars.example terraform.tfvars

code

文件中唯一需要设置的值是 github_repo 。如果您已经拥有该仓库的 GitHub 副本,请在此处填写其 owner/name ,注意不要以 .git 结尾。如果没有,请保留 YOUR_GITHUB_USER/YOUR_REPO 。第 14 章会告诉您何时设置该值并重新应用此阶段。

Plan: 10 to add, 0 to change, 0 to destroy.

code

这 10 个资源包括三个服务账户、GitHub 池及其提供者,以及它们之间的权限链接。

Apply complete! Resources: 10 added, 0 changed, 0 destroyed.

Outputs:

ci_service_account_email = "clearpay-ci@YOUR_PROJECT_ID.iam.gserviceaccount.com" service_account_emails = { "production" = "clearpay-api-production@YOUR_PROJECT_ID.iam.gserviceaccount.com" "staging" = "clearpay-api-staging@YOUR_PROJECT_ID.iam.gserviceaccount.com" }

code

gcloud iam service-accounts list --filter='email~clearpay-'

code

查找三个邮箱:clearpay-api-staging、clearpay-api-production 和 clearpay-ci。

在控制台中:服务账户列表显示这三个账户。工作负载身份联邦显示 clearpay-github-pool,其提供者为 clearpay-github-provider。

如果因 Error 409: Requested entity already exists on the pool 导致应用失败,说明您之前运行过此阶段。Google 会保留已删除的池 30 天并阻止其名称的重复使用。使用 gcloud iam workload-identity-pools undelete clearpay-github-pool --location=global 恢复池,然后运行 terraform import google_iam_workload_identity_pool.github projects/YOUR_PROJECT_ID/locations/global/workloadIdentityPools/clearpay-github-pool 并重新应用。

如果出现相同错误并提示提供者名称,使用 gcloud iam workload-identity-pools providers undelete clearpay-github-provider --workload-identity-pool=clearpay-github-pool --location=global 恢复提供者,然后运行 terraform import google_iam_workload_identity_pool_provider.github projects/YOUR_PROJECT_ID/locations/global/workloadIdentityPools/clearpay-github-pool/providers/clearpay-github-provider 并重新应用。

向 Maya 发送消息:"三个服务账户已创建。目前还没有任何服务使用它们。"

Maya:"没有密钥文件?"

"没有。"

#### 第 6 阶段:创建密钥 (06-secrets)

GKE 上的 ClearPay 需要两个密钥:数据库 URL 和 JWT 签名密钥。这两个密钥在 AWS 上已经存在。本阶段将在 Secret Manager 中创建四个密钥,每个命名空间一对:

- clearpay-staging-jwt-secret
- clearpay-staging-database-url
- clearpay-production-jwt-secret
- clearpay-production-database-url

预发布服务账户只能读取预发布密钥对,生产服务账户只能读取生产密钥对。

可以将每个密钥想象为一个上锁的抽屉。第 5 阶段生成的每个徽章只能打开对应的抽屉。

本实验在两个密钥对中使用相同的值。两个环境使用相同的 RDS,且 AWS 上的登录功能在请求转移到 GKE 时仍需正常工作。真正的公司会为每个环境分配独立的值。

Terraform 使用占位符值 REPLACE_ME 创建密钥。Terraform 被允许创建密钥但不能自动生成其值。因此操作顺序是:先应用,然后手动复制真实值。

cd infra/gcp/terraform/06-secrets

code

Terraform 创建了十四项资源:

- 四个密钥
- 四个初始版本
- 四个 API 服务账户的访问规则
- 两个允许 pipeline 的 clearpay-ci 账户读取 staging pair 的规则

计划:添加 14 项,修改 0 项,销毁 0 项。

code

应用完成!资源:新增 14 项,修改 0 项,销毁 0 项。

code

现在将 AWS 值复制进去。AWS 必须正在运行。

从仓库根目录运行这些命令,即项目顶层文件夹,而不是从 06-secrets 或 infra/aws/terraform 文件夹运行。你仍然在 06-secrets 文件夹中,所以需要先向上移动四层目录:

cd ../../../..

code

此时 ls 应该显示 api、infra、k8s 和 scripts 四个文件夹。

你不需要进入 AWS 文件夹。terraform -chdir=infra/aws/terraform 会从当前位置读取 AWS 值。每个命令都会直接将值传入 Secret Manager,因此屏幕上不会显示任何内容。

每个命名空间的数据库 URL:

terraform -chdir=infra/aws/terraform output -raw database_url \ | gcloud secrets versions add clearpay-staging-database-url --data-file=- terraform -chdir=infra/aws/terraform output -raw database_url \ | gcloud secrets versions add clearpay-production-database-url --data-file=-

code

一次成功的执行会显示 Created version [2],而不会显示 URL。

JWT 密钥从 AWS 服务器的 .env 文件中读取:

INSTANCE_IP=$(terraform -chdir=infra/aws/terraform output -raw instance_public_ip) ssh -i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP \ "sudo grep '^JWT_SECRET=' /opt/clearpay-api/.env | cut -d= -f2-" \ | gcloud secrets versions add clearpay-staging-jwt-secret --data-file=- ssh -i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP \ "sudo grep '^JWT_SECRET=' /opt/clearpay-api/.env | cut -d= -f2-" \ | gcloud secrets versions add clearpay-production-jwt-secret --data-file=-

code

此文件夹中稍后的 terraform apply 不会重新插入 REPLACE_ME。Terraform 文件会告诉它在第一次 apply 后保留该值。

gcloud secrets list --filter='name~clearpay-'

code

然后验证是否仍有密钥包含占位符。此循环会打印 ok 或 STILL_PLACEHOLDER,且从不打印密钥内容:

for s in clearpay-staging-database-url clearpay-staging-jwt-secret \ clearpay-production-database-url clearpay-production-jwt-secret; do val=$(gcloud secrets versions access latest --secret="$s") if [ "$val" = "REPLACE_ME" ]; then echo "STILL_PLACEHOLDER $s"; else echo "ok $s"; fi done

code

寻找四条 ok 的输出。如果某条显示 STILL_PLACEHOLDER,请重新运行对应的复制命令。

在控制台中:Secret Manager 列出四个密钥,每个密钥的最新版本都是版本 2。不要在共享屏幕上点击 "Show secret value"。

给 Maya 发消息:"四个密钥已包含 AWS 值。没有内容在屏幕上打印或粘贴到聊天中。"

#### 阶段 7:统计 API 错误(07-observability)

每次 API 发生事件时,都会写入一条简短的记录,称为日志条目。登录会写入一条,支付也会写入一条。当出现问题时,记录会显示 "level":"error"。支付失败会写入如下内容:

{"level":"error","message":"transaction_failed","timestamp":"2026-09-29T10:15:02.000Z","error":"..."}

code

在 GKE 上,Google Cloud 会收集所有这些记录。本阶段基于这些记录构建两样东西:

- 一个计数器,名为 clearpay-api-error-count。每次出现错误记录时,计数器会增加 1。

- 一个名为 ClearPay API - 错误率升高的警报。当计数器在五分钟内增加超过五次时,警报会触发。一次错误可能是运气不好。五分钟内出现五次错误意味着某些地方出了问题。

该警报目前没有关联电话号码或电子邮件。当警报触发时,Google Cloud 控制台会显示它,但没有人会收到消息。之后可以在控制台中添加电子邮件。

目前计数器保持在零,因为 ClearPay 还没有在 GKE 上运行。今天零是正确的结果。

cd infra/gcp/terraform/07-observability

code

应用完成!资源:2 个新增,0 个更改,0 个销毁。

输出:

log_metric_name = "clearpay-api-error-count"

code

如果 apply 停止并出现错误 404:找不到匹配类型的指标,这并不表示有问题。Google 需要几分钟时间来注册一个全新的计数器,之后警报才能监视它。计数器已经创建。等待五分钟,再次运行 terraform apply。这次计划显示 1 个新增,仅是警报。

gcloud logging metrics describe clearpay-api-error-count

code

查找名称:clearpay-api-error-count 和一个提及 clearpay-api 以及 jsonPayload.level="error" 的过滤器。

消息 Maya:“错误计数器存在且读数为零。目前还没有应用在 GKE 上运行。”

Maya:“最后一个了。这是客户将使用的地址,所以请花点时间。”

#### 第 8 阶段:构建公共负载均衡器(08-edge)

第 1 至 7 阶段建造了房屋。本阶段建造的是正门:客户在整个迁移过程中将保留的地址。它是一个 Google 全球负载均衡器,后面有一个流量调节器:将一定百分比的流量导向 AWS,其余导向 GKE。今天它将 100% 的流量发送到 AWS。

本阶段创建以下内容:

- clearpay-api-ip,客户将使用的公共地址

- 一个自签名证书,使 HTTPS 正常工作

- 流量分配,设置为 AWS 100%,GKE 0%

- clearpay-gke-edge-ip,GKE 一侧的第二个 IP。负载均衡器使用它。客户不会直接使用这个 IP。

下面的图表展示了 GCP 负载均衡器如何通过可配置的流量百分比在 AWS 和 GKE 之间分配客户流量。客户通过一个公共 IP 连接。AWS 通过其弹性 IP 接收流量,而 GKE 通过一个客户无法直接访问的独立边缘 IP 接收流量。两个环境都连接到 AWS 上的同一 PostgreSQL 数据库。在本阶段,GKE 集群是空的。

cd infra/gcp/terraform/08-edge cp terraform.tfvars.example terraform.tfvars

code

如果文件已存在,可以跳过复制操作。将你的弹性 IP 放入 aws_backend_ip。从仓库根目录的第二个终端获取它:

terraform -chdir=infra/aws/terraform output -raw instance_public_ip

code

保持权重设置为 100 和 0:

aws_traffic_weight = 100 gke_traffic_weight = 0

code

应用过程大约需要两分钟。

计划:17 个新增,0 个更改,0 个销毁。

code

应用完成!资源:17 个新增,0 个更改,0 个销毁。

输出:

current_traffic_split = "AWS: 100% / GKE: 0%" static_ip_address = "x.x.x.x" gke_edge_ip_address = "y.y.y.y" hosts_file_line = "x.x.x.x api.clearpay.test"

code

记录下两个 IP 地址。static_ip_address 是客户地址。gke_edge_ip_address 不是。

#### 为地址命名

你没有拥有域名,因此你可以在计算机的 hosts 文件中将名称 api.clearpay.test 指向负载均衡器。

你已经在 apply 之后进入 08-edge,所以可以跳过下面的 cd 命令。仅当你在仓库根目录打开了一个新的终端时才运行它:

cd infra/gcp/terraform/08-edge LINE=$(terraform output -raw hosts_file_line) echo "LINE is: [$LINE]"

code

如果LINE为空,请停止。如果内容类似x.x.x.x api.clearpay.test,请添加到主机文件中。

echo "$LINE" | sudo tee -a /etc/hosts sudo dscacheutil -flushcache sudo killall -HUP mDNSResponder

code

在Linux系统中只需运行第一行。在Windows系统中,请以管理员身份打开编辑器,将相同的IP和api.clearpay.test添加到C:\Windows\System32\drivers\etc\hosts文件中。

不要运行sudo cd。cd是shell内置命令,sudo无法更改当前目录。

确认该行已存在:

grep api.clearpay.test /etc/hosts

code

域名以.test结尾是有意为之。.test域名保留用于测试,因此Chrome允许点击通过自签名证书警告。不要将lab_hostname改为.dev域名。Chrome会对所有.dev网站强制使用严格HTTPS,并隐藏继续按钮。

curl -k https://api.clearpay.test/health

code

{"status":"ok","served_by":{"platform":"aws","version":"1.0.0","instance":"ip-172-31-64-73","commit":"504e0cd","rollback_active":false}}

code

您的实例和提交记录值会不同。"platform":"aws"是验证点。请求已发送到新的Google地址,负载均衡器将其转发到AWS,因为权重设置为100和0。

新的Google负载均衡器启动可能需要几分钟时间。如果尚未看到JSON响应,可能会先看到以下内容:

- 连接重置:负载均衡器仍在启动中。等待一分钟,再次运行相同的curl命令。

- 标题为404 Not Found的Google页面:负载均衡器已响应但尚未连接到AWS。确认AWS本身是否正常:

curl -k https://YOUR_ELASTIC_IP/health

code

如果AWS有响应,请等待几分钟后再次尝试访问api.clearpay.test。即使此时速度仍然较慢,阶段8也已完成。第20章会再次检查该内容。

当JSON响应出现时,在浏览器中打开https://api.clearpay.test。在Chrome中,点击高级,然后选择“继续前往api.clearpay.test(不安全)”。您将看到由AWS通过新地址提供的ClearPay登录界面。如果浏览器显示NXDOMAIN,说明主机文件中缺少该行或缓存未清除。

在控制台中:IP地址显示clearpay-api-ip和clearpay-gke-edge-ip。负载均衡显示clearpay-migration-forwarding-rule,后端使用您的弹性IP。

- 阶段1:container.googleapis.com已启用

- 阶段2:terraform output nat_ip_address输出一个IP,且您已记录下来

- 阶段3:kubectl get nodes显示至少一个Ready节点

- 阶段4:clearpay仓库已存在

- 阶段5:存在三个clearpay-服务账户

- 阶段6:循环打印四条ok信息

- 阶段7:错误指标已存在

- 阶段8:/etc/hosts包含api.clearpay.test,权重为100/0

告诉Maya:“八个阶段均已验证。权重为100和0。GKE上尚未运行任何服务。”

Daniel:“集群是空的。下一步是将ClearPay部署到该集群。”

## 第4部分:如何将ClearPay部署到GKE预发布环境

Maya顺便查看您浏览器中的新地址。她阅读仪表板信息后露出微笑。

Maya:“您在前端门的处理得很好。但看,它仍然显示aws。客户可以访问新地址,但每个请求仍然到达旧服务器。GKE上的集群是空的,因此目前还无法将$20发送到那里。”

她指向您的终端。

Maya:“下一步是将ClearPay部署到该集群。我特别关注的一点是:让流水线自动完成,而不是人工输入命令。流水线每次都能以相同方式执行,而且我们可以精确查看它执行了哪些操作。”

https://api.clearpay.test 已经可以在浏览器中正常访问,因此很容易让人误以为 ClearPay 已部署在 GKE 上。实际上并非如此。该地址是 Google 新的前端入口,来自 Stage 8 的负载均衡器。通过权重设置为 100% 和 0% 的配置,前端入口会将所有请求发送到 AWS。仪表板上显示的 "由 aws/1.0.0 提供服务" 这一行证明了这一点。GKE 集群本身仍然处于空状态。

code
浏览器 → api.clearpay.test (Google 负载均衡器) → 100% → AWS EC2 → RDS
                                                  →   0% → GKE (空)

在这一部分,流水线会将 ClearPay 部署到 GKE 预发布环境,而第 6 章提到的发布网关会决定预发布环境是否达标。预发布环境是集群内部的一个私有应用副本,不会被任何客户访问到。

第12章 检查API镜像

第二周,星期二,上午9:00

集群已构建完成且处于空状态。在任何内容部署到集群之前,Daniel 会将 API 的 Dockerfile 显示在你的屏幕上。

Daniel:"我们即将把应用部署到集群。请证明这个容器不是以 root 身份运行的。"

验证 API 镜像的大小、不包含任何敏感信息且不以 root 身份运行。

如果攻击者入侵容器,以 root 身份运行的进程会拥有不必要的权限。生产环境镜像应该以普通用户身份运行。

打开 api/Dockerfile 文件,它包含两个阶段:

  • 第一阶段 FROM node:22-slim AS deps 使用 npm install --omit=dev 仅安装生产环境依赖。
  • 第二阶段从 node:22-slim 重新开始。它应用了 Debian 安全更新并删除了 npm 和 yarn,因为运行 API 只需要 node src/index.js。它创建了用户 ID 为 1001 的 appuser 用户,并复制了 node_modules、src 和 db 目录。最后几行切换到了 USER appuser。

注意镜像中没有包含的内容:DATABASE_URL 和 JWT_SECRET。这些信息会在运行时从 Secret Manager 获取。

Docker Desktop 必须处于运行状态。从仓库根目录执行以下命令:

code
docker build -t clearpay-api:test ./api
docker run --rm clearpay-api:test whoami

第二个命令输出:

code
appuser
  • 如果输出 root:说明缺少 USER appuser 这一行。请修复 api/Dockerfile 并重新运行两个命令。
  • Docker 无法连接:请启动 Docker Desktop 后重试。集群本身没有问题。
  • whoami 输出 appuser

第13章 为 ClearPay 准备集群

星期二,上午11:00

镜像检查已完成。你打开 GitHub,准备运行流水线。Daniel 看着你的屏幕,摇了摇头。

Daniel:"首次部署会失败两次,原因与流水线无关。先解决这些问题。"

在首次部署之前,集群需要具备两个当前尚未拥有的功能:

  • 一种让 Pod 从 Secret Manager 读取密钥的方式。
  • 到 AWS 上数据库的网络连接路径。

缺少第一个功能时,Kubernetes 会拒绝加载预发布环境文件。缺少第二个功能时,Pod 虽然能启动但永远不会变为就绪状态。

#### A部分:启用 Secret Manager 插件

Daniel:"Pod 应该从 Stage 6 的 Secret Manager 读取值。如果它们没有这么做,请记录下为什么仍然使用文件。"

k8s/overlays/ 目录中的预发布和生产环境文件会从 Secret Manager 读取数据库 URL 和 JWT 密钥。它们使用了两个 GKE 插件:

  • Secret Manager CSI 驱动程序:将密钥以文件形式挂载到 Pod 内部。
  • Secret Sync:将密钥复制到名为 clearpay-api-secrets 的普通 Kubernetes Secret 中。Deployment 会将该 Secret 加载到 API 的环境变量中。

通过 Workload Identity,Pod 使用第 5 阶段的服务账户来证明自己的身份。任何地方都没有密钥文件。API 代码无需更改,仍然像在 AWS 上一样读取普通环境变量。

启用这两个插件。gcloud 需要两个独立的命令,每个命令需要几分钟时间:

code
gcloud container clusters update clearpay --zone us-east4-a \
  --enable-secret-manager
gcloud container clusters update clearpay --zone us-east4-a \
  --enable-secret-sync

这些命令在此处执行,而不是在 03-gke 中,因此集群阶段保持较小规模,您可以观察到添加密钥访问权限的时刻。

保留 k8s/overlays/staging/secretproviderclass.yaml 和 patch-serviceaccount.yaml 中的 PROJECT_ID。流水线在部署时会将其替换为您的真实项目 ID。不要将您的 ID 输入这些文件中。

#### 第 B 部分:将 AWS 数据库开放给 GKE

不要将此过程描述为“将 GKE 连接到数据库”。这种表述暗示数据库也会迁移,但实际上不会。这一步让 GKE 安全地访问和使用未迁移的真实 AWS 数据库。

集群中的机器没有公网 IP。它们的出站流量通过 Cloud NAT 以一个公网地址(第 2 阶段的 NAT IP)发出。RDS 将只接受来自该地址的 5432 端口连接,且连接字符串已要求 TLS(sslmode=require)。

code
GKE Pod  (clearpay-api)
   │
   ▼
私有节点  (无公网 IP)
   │
   ▼
Cloud NAT  (clearpay-nat-ip)
   │
   │  公网出站,一个 /32
   ▼
AWS 安全组  (additional_db_ingress_cidrs)
   │  5432 端口 + SSL
   ▼
RDS PostgreSQL 16

这是连接两个云的三种方式之一:

| 方案 | 配置 | 持续成本 | 说明 | |------|------|----------|------| | 公共端点 + IP 白名单 | 几个 Terraform 变量 | $0(正常互联网出站) | RDS 获取公网 IP,安全组仅允许 Cloud NAT 的 /32 IP。TLS 必须启用。 | | 云 VPN(站点到站点) | 几小时:两个云的网关 | GCP 每月约 $36 + AWS 每月约 $36(每条隧道) | 私有路径。RDS 没有公网端点。不在本仓库中。 | | AWS 合作伙伴跨云互联 | 实际配置时间 | 固定 10Gbps 或 100Gbps 等级 | 专用私有链路用于高流量。不在本仓库中。 |

为您的系统进行比较时:

code
(预期每月 GB) × (该路径每 GB 价格) + 该方案的固定月成本

ClearPay 发送小查询,因此使用 VPN 每月成本会超过数据价值。本实验使用白名单。更繁忙的系统可能会选择其他方案。

获取 NAT IP:

code
cd infra/gcp/terraform/02-network
terraform output nat_ip_address

将其放入 infra/aws/terraform/terraform.tfvars:

code
db_publicly_accessible      = true
additional_db_ingress_cidrs = ["<the NAT IP>/32"]

然后应用 AWS 堆栈:

code
cd infra/aws/terraform
terraform plan
terraform apply

该计划会添加一条允许 NAT IP 的新安全组规则,并使 RDS 可公开访问。必须保留允许 EC2 服务器访问 RDS 的规则。在 Terraform 中,这两条规则是分开的:aws_security_group_rule.db_from_nat(用于 GKE)和 aws_security_group_rule.db_from_api(用于 EC2)。

AWS 必须仍能访问其数据库。从仓库根目录:

code
export INSTANCE_IP=$(terraform -chdir=infra/aws/terraform output -raw instance_public_ip)
curl -k https://$INSTANCE_IP/health/ready
code
{"status":"ready", ... "platform":"aws" ...}

如果输出为 not_ready,或 AWS 登录页面显示 internal error,说明数据库已停止接受 EC2 服务器连接。请在此处暂停并重新阅读 Terraform 计划。在触碰 GKE 之前请先修复 AWS 问题,因为 AWS 仍为所有客户提供服务。

#### 继续之前请检查

第 14 章将推送代码并启动流水线。如果上述任何步骤被跳过,两章之后流水线会以未明确提示缺失步骤的错误失败。请现在运行此检查。它会为流水线所需每个项目输出 ok 或 MISSING。请从仓库根目录运行:

code
export INSTANCE_IP=$(terraform -chdir=infra/aws/terraform output -raw instance_public_ip)
NAT_IP=$(terraform -chdir=infra/gcp/terraform/02-network output -raw nat_ip_address)
DB_HOST=$(terraform -chdir=infra/aws/terraform output -raw db_endpoint | cut -d: -f1)

grep -q '^db_publicly_accessible *= *true' infra/aws/terraform/terraform.tfvars \
  && echo "ok   RDS 公共设置" \
  || echo "MISSING db_publicly_accessible = true 在 infra/aws/terraform/terraform.tfvars"
grep -q "$NAT_IP/32" infra/aws/terraform/terraform.tfvars \
  && echo "ok   NAT IP 在允许列表中" \
  || echo "MISSING $NAT_IP/32 在 additional_db_ingress_cidrs"
kubectl get csidriver secrets-store-gke.csi.k8s.io >/dev/null 2>&1 \
  && echo "ok   Secret Manager 驱动" \
  || echo "MISSING Secret Manager 驱动: 运行 Part A 中的两个 gcloud 命令"
kubectl get crd secretsyncs.secret-sync.gke.io >/dev/null 2>&1 \
  && echo "ok   Secret Sync" \
  || echo "MISSING Secret Sync: 运行 Part A 中的两个 gcloud 命令"
curl -sk https://$INSTANCE_IP/health/ready | grep -q '"status":"ready"' \
  && echo "ok   AWS 仍能连接数据库" \
  || echo "MISSING AWS 无法连接数据库: 重新阅读 Terraform 计划"

最后一个检查测试 GKE 到数据库的真实路径。它会启动一个临时 Pod,询问数据库端口是否接受连接,然后删除 Pod。首次运行需要约 1 分钟,因为 Kubernetes 需要下载镜像:

code
kubectl run dbcheck --rm -i --restart=Never --image=postgres:16 -- \
  pg_isready -h "$DB_HOST" -p 5432 -t 10 \
  && echo "ok   GKE 能连接数据库" \
  || echo "MISSING GKE 无法连接数据库"

你需要看到 6 行 ok 且没有 MISSING。每行 MISSING 都会指出需要修复的内容。修复后请重新运行检查。在所有 6 行都显示 ok 之前不要开始第 14 章。

如果最后一行显示 MISSING 但前两行显示 ok,RDS 可能仍在应用更改。请等待 2 分钟后重新运行该命令。

  • 两个 gcloud container clusters update 命令已执行完毕
  • RDS 允许 NAT IP,且 curl -k https://$INSTANCE_IP/health/ready 仍输出 ready
  • 上述检查已输出 6 行 ok 且没有 MISSING

第 14 章 构建 CI/CD 流水线

星期二,下午 3:00

集群已准备好首次部署。Maya 查看团队频道并添加了一条规则。

Maya:"从现在起,我们不再手动在 staging 环境运行 kubectl apply。流水线应该以相同方式每次构建镜像、扫描、推送并部署。"

工作流文件已写好:.github/workflows/deploy.yml。你的任务是理解它,将其连接到 Google Cloud,并观察它部署 staging 环境。

#### 为什么需要流水线?

当人工部署时,每次部署都有细微差别。有人因为时间紧迫跳过扫描,有人从包含未提交更改的笔记本电脑部署。当出现问题时,没人能说清楚到底运行了什么。

流水线消除了这些差异。每个镜像都源自某个提交,通过相同的扫描流程,并通过相同的步骤到达集群。第15章提到的“提升门”具有实际意义,因为它会检查以这种方式构建的镜像。

#### 流水线的工作原理

该工作流包含两个任务:

在你的仓库中打开 .github/workflows/deploy.yml 文件,并将其与本图示并排打开。该图示展示了工作流的两个任务及其执行顺序。该文件包含实际的 GitHub Actions 配置。在阅读触发条件、权限、镜像构建与扫描、预发布部署和生产环境提升等内容时,请结合文件和图示一起阅读。

请按照此图示从上到下阅读文件。

触发条件:on: push: branches: [main] 在每次向 main 分支推送时运行任务1。workflow_dispatch 在 GitHub 中添加了一个“运行工作流”按钮,包含一个复选框 promote_to_production。任务1仅在该复选框未勾选时运行(if: ${{ !inputs.promote_to_production }})。任务2仅在勾选时运行。任何推送操作都无法直接到达生产环境。

权限:id-token: write 允许任务向 GitHub 请求一个短期的 OIDC 令牌,这是一个声明“此运行属于仓库 X”的签名文件。Google 将该声明与第5阶段的池进行比对,并返回 clearpay-ci 的临时凭证。GitHub Secrets 中没有密钥文件,因此不存在密钥泄露的风险。

镜像标签:标签是 git rev-parse --short HEAD 生成的短提交 SHA。每个镜像都指向一个确切的提交。流水线还会推送一个动态标签 staging-latest,但部署使用的是 SHA。

扫描:Trivy 是一个开源扫描工具,它将镜像中的软件包与已知漏洞列表进行比对。每个已知漏洞都有一个类似 CVE-2026-29786 的 ID。工作流设置如下:

  • severity: CRITICAL,HIGH:仅统计严重漏洞
  • ignore-unfixed: true:即使没有可用补丁也不会阻断构建
  • exit-code: "1":任何剩余漏洞都会导致任务失败

这些设置的含义是:如果严重漏洞有补丁但镜像未包含,构建将失败。扫描失败时不会推送任何内容。

推送:任务会为每个镜像打上完整仓库路径标签 us-east4-docker.pkg.dev/<project>/clearpay/api:<sha> 并推送。clearpay-ci 拥有第5阶段的 artifactregistry.writer 权限,因此推送操作可以成功。

部署到预发布环境:该步骤在 k8s/overlays/staging 中执行:

  • 它将 Secret Manager 中的两个预发布密钥写入运行器上的 secrets.staging.env 文件。umask 077 确保文件仅对运行器用户可读。该文件被 Git 忽略,并在运行器终止时删除。
  • sed 将 patch-serviceaccount.yaml 和 secretproviderclass.yaml 中的 PROJECT_ID 占位符替换为实际项目 ID。
  • kustomize edit set image 将覆盖层指向当前提交 SHA 对应的镜像。
  • kubectl apply -k . 将覆盖层发送到集群。
  • kubectl rollout status 等待最多 180 秒直至 API 和 UI 容器就绪。若始终未就绪,任务将失败。

clearpay-ci 拥有 container.developer 权限,允许部署工作负载但不能修改集群本身。

提升到生产环境:任务2是流水线唯一接触生产环境的方式:

  • environment: production 会暂停任务,直到指定审核人在 GitHub 上点击“批准”。
  • 它不会构建任何内容,而是读取预发布环境当前运行的镜像字符串,并将该镜像精确部署到生产环境。

该规则被称为“一次构建,推广相同制品”。通过第15章验证的镜像与客户获得的镜像在字节层面完全一致。重新构建可能会拉取更新的软件包并推送未经测试的内容。

Kustomize在两个部署步骤中均被使用,它是kubectl apply -k命令背后的工具。它会将k8s/base/目录中的共享文件与k8s/overlays/staging/或k8s/overlays/production/目录中的环境特定修改(称为overlays)进行合并。预发布环境和生产环境共享同一个基础配置,仅在必须时存在差异。

首先,将此仓库托管在GitHub上,使用您的个人账户,并将main分支设为默认分支。

然后告知Google哪些仓库可以使用clearpay-ci。打开infra/gcp/terraform/05-identity/terraform.tfvars文件,将github_repo设置为您的仓库所有者/仓库名,例如Osomudeya/migrate-api-aws-gke-devops。不要粘贴完整的https://github.com/...地址,结尾不要带.git。再次应用第5阶段:

code
cd infra/gcp/terraform/05-identity
terraform plan
terraform apply

该计划会修改池的属性条件和clearpay-ci链接,但不会影响集群。如果您已在第11章设置了真实名称,计划将显示无变更。

在GitHub上,打开您的仓库并点击设置。在左侧菜单中点击“Secrets and variables”,然后点击“Actions”。打开“Variables”标签页,为以下三个变量分别点击“New repository variable”。请使用“Repository”部分,而非“Environment”。

名称

值

code
GCP_PROJECT_ID

您的GCP项目ID

code
GCP_WORKLOAD_IDENTITY_PROVIDER

执行以下命令的输出:

code
terraform -chdir=infra/gcp/terraform/05-identity output -raw github_actions_workload_identity_provider
code
GCP_CI_SERVICE_ACCOUNT
code
terraform -chdir=infra/gcp/terraform/05-identity output -raw ci_service_account_email

这些是变量而非密钥,其中没有任何一个是密码。整个流水线完全不需要GitHub密钥。

复制两个terraform output -raw命令的输出时,不要复制末尾的%符号。在Mac上,终端会在输出后无换行符时显示%,但该符号不属于值的一部分。提供者值必须以clearpay-github-provider结尾,服务账户必须以.iam.gserviceaccount.com结尾。

然后,仍在设置页面中,点击“Environments”,然后点击“New environment”。将其命名为production并点击“Configure environment”。勾选“Required reviewers”,搜索您的GitHub用户名,选择它,然后点击“Save protection rules”。

原因:Job 2是通往生产的唯一路径。设置必审人可以让GitHub暂停该任务,直到有人点击批准,从而防止意外内容到达客户。Job 2会在第19章等待该批准。

您会在此页面上看到“Manage environment secrets”按钮。不要点击它。生产环境不需要密钥,因为流水线通过Workload Identity而非存储的密码获取Google访问权限。如果您的仓库已保存其他项目的密钥,请保留它们在原处。

如果GitHub未提供“Required reviewers”功能,您的仓库可能在免费计划下是私有的。为本实验将仓库设为公开,或跳过审核人设置。没有审核人时,生产任务会在您触发后立即运行。

接下来,在推送前进行检查。失败的流水线会耗费3分钟并生成长日志,而检查仅需10秒。首先再次运行第13章末尾的检查命令。所有六行必须输出ok。然后检查您提供给Google的仓库名称:

code
REPO=$(grep '^github_repo' infra/gcp/terraform/05-identity/terraform.tfvars | cut -d'"' -f2)
case "$REPO" in
  https*|*.git|YOUR_GITHUB_USER/*) echo "MISSING github_repo 必须为 owner/name。当前值为: $REPO" ;;
  */*) echo "ok   github_repo $REPO" ;;
  *)   echo "MISSING github_repo 必须为 owner/name。当前值为: $REPO" ;;
esac

输出结果必须显示 ok 并包含你自己的 owner/name 。

现在触发流水线:

code
git commit --allow-empty -m "trigger pipeline"
git push

打开 Actions 标签页并点击正在运行的工作流。观察 build-scan-push 步骤依次完成,每个步骤都有勾选标记:build, 扫描 API 镜像, 扫描 UI 镜像, 认证到 GCP, push, 部署到 staging 。

如果某个步骤显示红色,此处会提示你重新运行工作流:

红色步骤

前往

扫描 API 镜像

或

扫描 UI 镜像

"如果扫描失败"

认证到 GCP

"如果认证到 GCP 失败"

部署到 staging

"如果部署到 staging 失败"

修复后重新运行,打开 Actions 标签页,点击失败的运行记录,右上角点击 Re-run all jobs。无需再次推送。

第二个任务 promote-to-production 显示灰色圆圈加斜杠,这是正确的。GitHub 会在每次推送时跳过该任务,只有在第19章手动启动时才会运行。

当部署到 staging 成功后,检查集群:

code
kubectl -n staging get pods

API 和 UI 的 Pod 显示 Running 状态且 READY 1/1 。

staging 环境没有公网地址,负载均衡器仍然将所有客户流量导向 AWS。不要在浏览器中打开 staging 环境。第15章会通过私有隧道访问该环境。

#### 如果扫描失败

永远不要将 exit-code: "1" 改为 "0" 。这会关闭安全网关。

Dockerfile 已经修复,扫描应该通过。后续可能出现新的安全漏洞导致失败。打开红色任务,Trivy 会输出表格。找到 Target 列并匹配:

Target 列

含义

处理方式

操作系统,如

code
debian 12.x

,Fixed 列包含版本号

基础镜像版本滞后

重新构建。如果仍显示红色,将

code
FROM node:22-slim

更新到最新

code
22.x-slim

标签,

在

Docker Hub

上。

code
usr/local/lib/node_modules/npm

npm 包含该软件包

Dockerfile 删除了 npm。检查删除语句是否仍然存在。

code
app/node_modules
code
package-lock.json

ClearPay 的某个包版本滞后

在

code
api/package.json
code
ui/package.json

中升级版本号。

Alpine 软件包,在

,包含 Fixed 版本号

UI 基础镜像版本滞后

向 nginx 阶段添加

code
RUN apk upgrade --no-cache

到

code
ui/Dockerfile.prod

每次修改 Dockerfile 后,本地重新构建并确认用户仍然是 appuser :

提交代码,推送至 main 分支,再次观察 Actions 流水线。

#### 如果认证到 GCP 失败

错误信息类似:

code
给定的凭证被属性条件拒绝。

Google 接受了 GitHub 令牌但随后拒绝了。第5阶段会指定不同的仓库,通常是占位符 YOUR_GITHUB_USER/YOUR_REPO 。值必须严格为 owner/name ,例如 Osomudeya/migrate-api-aws-gke-devops 。完整 URL 不匹配,他人的仓库也不匹配。使用你的精确 owner/name 重复上述第2步。然后不修改工作流直接重试:

code
git commit --allow-empty -m "retry pipeline after WIF repo lock"
git push

#### 如果部署到 staging 失败

code

两个错误信息含义相同:error: timed out waiting for the condition 和 error: deployment "clearpay-api" exceeded its progress deadline。配置文件已应用,但 Pod 从未变为就绪状态。向集群询问原因:

kubectl -n staging get pods kubectl -n staging describe pod -l app=clearpay-api

code

然后根据实际看到的内容进行匹配。前两行出现在作业日志中,其余内容出现在 Pod 输出中:

你看到的内容

no matches for kind "SecretProviderClass"

code

"SecretSync"

code

Secret Manager 插件已关闭

运行第13章A部分的两个命令。重新运行工作流。

Permission denied

code

在

gcloud secrets versions access

code

流水线账户无法读取 staging 密钥

重新应用第6阶段(下方修复A)

ImagePullBackOff

code

伴随

403 Forbidden

code

集群机器无法读取镜像仓库

重新应用第4阶段,然后重启 Pod(下方修复B)

Insufficient cpu

code

集群正在扩展或已满

运行

。少于三台机器:等待几分钟。三台机器:将 staging 缩容为一个 API Pod(下方修复C)。

FailedMount

code

driver ... not found

code

或

secret "clearpay-api-secrets" not found

code

新机器仍在启动中,或 Secret Sync 尚未写入密钥

等待两分钟。Pod 会自动重试。使用以下命令检查:

kubectl -n staging get secret clearpay-api-secrets

code

Running

code

但

READY 0/1

code

伴随

Connection terminated due to connection timeout

code

Readiness probe failed

code

Pod 无法连接到 PostgreSQL

运行第13章末尾的检查。以下

MISSING

code

行显示未完成的操作。第13章B部分通常是根本原因。Pod 会在下一次健康检查时自动变为就绪状态。

修复A:为流水线授予访问 staging 密钥的权限:

cd infra/gcp/terraform/06-secrets terraform plan terraform apply

code

修复B:允许集群读取镜像仓库。机器以默认 Compute Engine 服务账户拉取镜像,该账户需要 artifactregistry.reader 权限。计划添加了该规则:

cd infra/gcp/terraform/04-registry terraform plan terraform apply

code

然后重启 staging Pod 并等待:

kubectl -n staging rollout restart deployment/clearpay-api deployment/clearpay-ui kubectl -n staging rollout status deployment/clearpay-api --timeout=180s kubectl -n staging rollout status deployment/clearpay-ui --timeout=180s

code

修复C:在 staging 中运行一个 API Pod:小型实验室集群无法容纳两个。缩容后重新运行工作流:

kubectl -n staging scale deployment/clearpay-api --replicas=1

code

在不打印密钥的情况下读取 API 日志:

kubectl -n staging logs deployment/clearpay-api --tail=20

code

- 你可以解释 deploy.yml 中两个作业各自的作用  
- 生产环境已存在,你作为所需审核人  
- staging 部署已通过  
- `kubectl -n staging get pods` 显示 API 和 UI 处于 Running 状态,READY 1/1  

流水线在此处停止。它不会运行晋升门。你将在第15章执行此操作。

### 第15章。检查 staging 环境并运行晋升门

第二周,星期三,上午10:00

流水线在昨晚6:40无错误完成。你截取了屏幕截图。ClearPay 的任何人都不关心截图。

Daniel 在你桌前停下并查看通过的流水线。

Daniel:"所以 GKE 是健康的?"

你回答:"Pod 是健康的。但这不能证明客户可以发送二十美元。"

Maya 已经阅读了该线程。她补充了一行。

Maya:"客户只有在推广脚本通过后才能获得这个构建版本。仅运行Pods对我不够。"

Kubernetes在Pod的健康检查响应后将其标记为就绪状态。这表示进程已启动且能连接到数据库。但它并未涉及登录、支付或重复收费等问题。推广门检查这些内容。

推广门是一个在你笔记本电脑上运行的脚本。流水线不会运行它。它也不是你在GitHub上看到的灰色"推广到生产环境"任务。那个任务会在第19章出现,并且只有在通过这个门检后才会执行。

#### 预发布环境的运行方式

GKE集群 "clearpay" 区域 us-east4-a │ ├── 命名空间 staging │ 服务 clearpay-api :80 │ └── API Pod (HPA 1–3) │ └── 命名空间 production (第19章创建) 网关 clearpay-gke-edge ← clearpay-gke-edge-ip HTTPRoute → 服务 :80 └── API Pod (HPA 1–3) 每个Pod: DB_POOL_SIZE=10

code

Daniel查看相同的图表。

"生产环境能运行多少个API进程?"

"在这个集群上最多三个。这是patch-hpa.yaml中的HPA上限。"

"每个能打开多少个数据库连接?"

"十个。和周一的db.js一样。"

"那么数据库能看到什么?"

你停顿了一下。30个来自GKE。加上AWS的10个,当两者都运行时。在一个能扩展到8个Pod的集群中,仅GKE自身就会有80个连接。

"这就是我让你记住的数字,"Daniel说。

现在有三个文件值得关注:

- k8s/base/deployment.yaml : 健康检查调用/health/ready,每个API Pod保留250m CPU和256Mi内存。

- k8s/base/networkpolicy.yaml : 限制哪些Pod可以与API通信。

- k8s/overlays/staging/patch-deployment.yaml : 预发布环境的副本数和滚动设置。

#### 第1步:检查GKE到数据库的路径

在预发布API Pod中运行一行Node脚本。它打开数据库连接并打印connected:

kubectl -n staging exec deployment/clearpay-api -- node -e \ "require('./src/db').checkConnection().then(() => { console.log('connected'); process.exit(0); }).catch(e => { console.error(e); process.exit(1); })"

code

connected

code

#### 第2步:检查密钥是否到达

Kubernetes保存了包含两个密钥的同步Secret。describe列出密钥名称和大小,而非具体值:

kubectl -n staging get secret clearpay-api-secrets kubectl -n staging describe secret clearpay-api-secrets

code

API进程拥有该值,但不会打印出来进行检查:

kubectl -n staging exec deployment/clearpay-api -- node -e 'if (!process.env.DATABASE_URL) process.exit(1); console.log("DATABASE_URL is set")'

code

DATABASE_URL is set

code

脚本特意使用单引号。默认的Mac shell zsh会将双引号内的!视为历史命令,并报错zsh: event not found。

CSI挂载通过名称显示密钥文件:

kubectl -n staging exec deployment/clearpay-api -- ls /mnt/secrets-store

code

查找DATABASE_URL和JWT_SECRET。

#### 第3步:运行推广门检查

预发布环境是私有的。kubectl port-forward创建隧道,使你的笔记本电脑localhost:8080连接到预发布API。&让隧道在后台运行。从仓库根目录:

kubectl -n staging port-forward svc/clearpay-api 8080:80 &

code

在继续之前等待出现这行:

Forwarding from 127.0.0.1:8080 -> 4000

code

Amara已经在该线程:"运行回放。我想看到一次扣款。"

运行第6章中针对AWS执行的相同网关检查。四个参数分别是隧道URL、Alice的电子邮件、她的密码以及Bob的电子邮件:

./scripts/promotion-gate.sh http://localhost:8080 \ [email protected] password123 [email protected]

code

使用 `kill %1` 停止隧道。

由于GKE读取与AWS相同的RDS数据库,因此Alice在GKE上已存在且不需要新种子。

#### 第4步:确认客户仍连接至AWS

预发布环境没有公网地址:

kubectl -n staging get svc clearpay-api

code

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE clearpay-api ClusterIP 10.x.x.x <none> 80/TCP 12m

code

ClusterIP显示为 `<none>` 表示只有集群内部可以访问该服务。

客户地址仍然从AWS响应:

export INSTANCE_IP=$(terraform -chdir=infra/aws/terraform output -raw instance_public_ip) curl -sk https://$INSTANCE_IP/health

code

{"status":"ok","served_by":{"platform":"aws",...}}

code

如果此处显示 `gke`,请停止操作。如果 `curl` 命令失败,说明AWS已宕机,这与GKE接管流量是不同问题。

因此两个云平台都在运行,但只有AWS对客户开放。预发布环境在GKE内部独立运行。网关不会影响其运行,它仅通过你的隧道进行测试,这是唯一的入口。

浏览器 → api.clearpay.test → 100% → AWS EC2 (为所有人提供服务)

GKE预发布环境 ← 你的隧道(端口转发) ← 网关和负载测试 GKE生产环境 (尚未存在,见第19章)

code

客户仅在第21章之后才会连接到GKE,届时前端门将开始发送真实流量。

#### 如果检查失败

不要推进此次构建。

- **JSON输入意外结束或连接被拒绝**:脚本在隧道开始监听前运行。如果终端中已有 `Forwarding from 127.0.0.1:8080`,只需重新运行脚本。不要启动第二个端口转发。

- **健康检查通过但就绪检查失败**:Pod无法连接到PostgreSQL。返回第13章B部分。

- **登录失败**:检查输入的值是否与种子一致:`[email protected]`、`password123`、`[email protected]`。

- **checkConnection打印TLS或超时错误**:与就绪检查失败相同。参考第13章B部分。

- **`ls` 显示无文件,或Pod卡在ContainerCreating状态**:运行 `kubectl -n staging describe pod <pod-name>` 并查找Secret Manager权限错误。预发布环境 `clearpay-api` 服务账户的注解必须与第5步中的预发布电子邮件逐字匹配。这是第6步允许读取密钥的电子邮件地址。

修复问题后,如果代码或文件有更改,请通过流水线推送,并重新运行网关。

Maya:"我们准备好了吗?"

"网关检查通过了。健康检查、就绪检查、登录、重放和一次扣款都通过了。Amara的重放检查也通过了。客户仍然连接在AWS上。"

Maya:"那么我们可以讨论生产环境了。在此之前不行。"

她再次看向终端:"预发布环境运行正常。现在告诉我,我们是否应该继续将GKE连接到同一台PostgreSQL数据库?"

- **checkConnection显示来自预发布Pod的连接**
- **DATABASE_URL已设置,且/mnt/secrets-store列出了两个密钥名称**
- **网关通过隧道打印出5项通过、0项失败**
- **预发布环境为ClusterIP且没有外部IP,而弹性IP仍然响应aws**

这是关键检查点。预发布环境通过了网关检查,且GKE能访问真实数据库。

下一步你要证明其安全性,然后在任何客户流量迁移前证明其在负载下的稳定性。

## 第5部分:如何使GKE具备生产环境就绪能力

预发布环境运行正常。发布门验证了五个关键点:应用可以运行、能够连接到数据库、用户可以登录、重复支付只会扣款一次、余额会正确减少。这是一个良好的结果,但并不等同于对客户绝对安全。

想象一辆新车通过了首次试驾。它能启动、能转向。但在允许任何人乘坐之前,你仍会提出三个问题:车门是否上锁?能否看到仪表盘?在陡峭的山坡上,或者轮胎没气时会发生什么?

这部分对ClearPay提出同样的三个问题:

问题  
检查内容  

是否有暴露的风险?  
16  

谁可以读取敏感信息?数据库连接是否加密?每个账户是否仅拥有必要的权限?  

能否观察到运行状态?  
17(可选)  

在笔记本电脑上运行的实践仪表盘,显示应用响应速度和失败请求数量。它教会你使用工具。在流量切换期间,你可以通过命令行检查进行监控。  

在高压环境下会发生什么?  
18  

同时接收大量请求,如同繁忙的下午。某个Pod在工作过程中突然崩溃。在用户使用应用时进行重启。  

这些问题是有意在此时提出的。客户目前仍在使用AWS,因此如果这一阶段出现故障,没有人会察觉,也不会造成经济损失。在此阶段修复问题只需花费你一小时。而如果在客户到达后才修复,你将失去客户的信任,所有修复操作都将在用户注视下进行。

### 第16章。执行安全审查

星期三,下午2:00

午餐后,Maya发送了下一个任务。她已经阅读了流水线日志。

Maya:"在任何客户接触GKE之前,我想要进行安全检查。我不想在发布后才发现文件中存在敏感信息。"

之前的所有检查都在问"是否能正常工作?"。而这个问题在问"是否暴露了敏感信息?"。对于支付API来说,发布后的安全审查为时已晚。

#### 检查1:数据库通信加密

数据库连接需要在云之间穿越公共互联网。确认实时预发布流程需要TLS加密,且不打印URL:

kubectl -n staging exec deployment/clearpay-api -- node -e "const u=process.env.DATABASE_URL||''; console.log(u.includes('sslmode=require') ? 'sslmode=require is set' : 'sslmode=require missing')"

code

sslmode=require is set

code

#### 检查2:理解密钥轮换机制

api/src/routes/auth.js生成的登录令牌有效期为12小时。如果你轮换JWT_SECRET,所有使用旧密钥签名的令牌将失效,用户必须重新登录。对于本实验来说,这是可以接受的。将这句话写入你的笔记作为轮换策略。此处不要自动化轮换。

#### 检查3:存在审计日志

Google Cloud会记录谁读取了密钥、谁更改了权限,这些记录独立于应用自身的日志。确认这些审计记录存在:

gcloud logging read 'protoPayload.serviceName=("secretmanager.googleapis.com" OR "iam.googleapis.com")' --limit=5 --freshness=7d --format='table(timestamp,protoPayload.methodName)'

code

查找包含SecretManager或iam的Method名称。仅看到iam相关名称(如SetIAMPolicy)是正常的。这证明Google正在记录权限变更。时间戳会有所不同。

#### 检查4:每个身份仅拥有必要权限

预发布环境的账户只能读取预发布密钥。生产环境的账户只能读取生产密钥。流水线可以推送镜像和部署,但不能修改集群。

PROJECT_ID=$(gcloud config get-value project) gcloud secrets get-iam-policy clearpay-staging-database-url gcloud secrets get-iam-policy clearpay-production-database-url gcloud projects get-iam-policy "$PROJECT_ID" --flatten='bindings[].members' --filter='bindings.members:serviceAccount:clearpay-ci@' --format='table(bindings.role)'

code

需要关注的事项:

- 预发布环境密钥列出了 clearpay-api-staging 和 clearpay-ci。流水线在部署期间会读取预发布环境配对。

- 生产环境密钥只列出 clearpay-api-production。

- clearpay-ci 拥有两个项目角色:artifactregistry.writer 和 container.developer。

如果生产环境密钥列出了预发布环境账户,或流水线拥有 owner 或 editor 这类角色,请停止并立即在阶段5或阶段6进行修复。

- 所有四个检查项都应记录在您的笔记中,使用您实际看到的输出,而不是"可能没问题"的推测。

### 第17章. 使用Prometheus和Grafana监控ClearPay(可选)

星期四,上午9:30

安全审查已完成。Daniel阅读了您的笔记,然后将椅子转向您。

Daniel:"一旦开始流量迁移,我希望能查看图表而不是猜测。您会关注哪些指标?"

请注意本章内容为可选章节。本章不涉及AWS或GKE,后续章节也不依赖本章内容。如果您想跳过,可以直接进入第18章。如果您想学习Prometheus和Grafana的工作原理,可以继续阅读本章。

API在/metrics端点暴露了指标数据:请求次数、延迟和数据库连接池使用情况。Prometheus每隔几秒收集这些数据。Grafana负责将这些数据可视化。

本章将在您的笔记本电脑上使用Docker Compose运行Prometheus和Grafana,监控第1章的API。这将验证仪表盘功能正常。本章不会监控GKE或AWS。observability/targets/目录中的目标文件以.example格式存在,但本实验不会使用这些文件。在流量迁移期间,第21和22章的命令行检查才是真实数据来源。

Google Cloud在阶段7有自己的错误计数器。这是GCP中的Cloud Logging指标,与本Grafana独立。

现在 localhost:4000 → Prometheus → Grafana :3000 之后 AWS, GKE, 负载均衡器 (observability/targets/*.example, 可选)

code

Prometheus读取host.docker.internal:4000,这是第1章的API。如果执行curl http://localhost:4000/health失败,请启动服务:

docker compose up -d api postgres

code

然后启动监控组件:

cd observability docker compose up

code

打开 http://localhost:3000 。此仅限本地的监控堆栈启用了匿名管理员访问。打开 ClearPay - 迁移仪表盘 。

向本地API发送几个请求:

curl http://localhost:4000/health

code

"按环境划分的请求延迟p95"面板显示一个名为local的系列。空面板表示Prometheus无法访问4000端口。

- 仪表盘显示本地系列

### 第18章. 有意对预发布环境进行负载测试和破坏性测试

星期四,下午1:00

预发布环境通过了关卡测试,您感觉任务已经完成。Daniel将您的第6章评分表和关卡测试结果并排显示在屏幕上。

Daniel:"关卡测试只验证了一个登录操作。当二十个请求同时到达,或者当Pod崩溃时会发生什么?"

对预发布环境运行第6章的负载测试,将两个评分表并排显示,然后在负载运行时杀死一个Pod并重启Deployment。

客户可能会同时向GKE发送大量请求,Kubernetes在更新和机器维护期间会重启Pod。您现在希望在没有客户参与的情况下同时观察这两种情况发生。

被测系统运行在Stage 3的正常节点池上。如果你在云中运行负载生成器,廉价的Spot VM是理想选择,因为它被中断时不会造成损害。你测量的系统绝不能运行在Spot实例上,因为中断会导致数据失效。

#### 步骤1:负载测试预发布环境

使用两个终端,都位于仓库根目录。

终端A用于本章的隧道连接。保持它持续运行:

kubectl -n staging port-forward svc/clearpay-api 8080:80

code

查找Forwarding from 127.0.0.1:8080的提示。

终端B:首先确认已连接到GKE且Postgres响应正常:

curl -s http://localhost:8080/health curl -s http://localhost:8080/health/ready

code

health必须包含"platform":"gke"。ready必须包含"status":"ready"。

然后运行30秒20个并发请求的负载测试:

./scripts/load-test.sh http://localhost:8080 \ [email protected] password123 20 30

code

将评分表记录到笔记中,与AWS的记录并列。本实验的指标:

AWS(第6章) GKE预发布环境(本章) p95延迟: 745 ms 655 ms 错误率: 0.0 % 0.0 % Pod终止后的恢复时间 n/a (步骤3后填写)

code

AWS使用10个并发请求,ok=372。GKE使用20个并发请求,ok=686,failed=0,每秒22.9个请求。

GKE的p95值比AWS略低,这可能让你意外。这两个指标大部分来自你的笔记本到云的传输,两者传输量相近。GKE上的每个数据库查询还会通过Cloud NAT从Google的us-east4传输到AWS的us-east-1,但这只增加了一小部分延迟。

两次测试使用了不同的并发数(10和20),因此应将数值视为相近而非竞争关系。需要保持干净的指标是failed=0。记住这两个延迟数值,它们会在书的结尾再次出现。

#### 步骤2:查看由哪些实例处理的交易记录

在api.clearpay.test上打开ClearPay仪表盘,查看最近活动。预期看到大部分是aws记录,可能有1-2条gke记录。本实验显示如下:

-$1.00 served by aws/1.0.0 ← 你在浏览器中发起的交易 -$1.00 served by gke/0.0.0-unset ← 第15章的促销网关 -$5.00 served by aws/1.0.0 ← 更早的交易

code

每条记录显示处理该转账的进程。两个云都写入同一数据库,UI读取所有记录。为什么gke记录这么少?

- 你的浏览器发送请求到AWS,因为前端门仍然将100%流量导向那里。

- 只有促销网关通过GKE处理了$1.00的支付(通过隧道)。负载测试只读取数据,因此不会新增记录。

gke记录并不表示负载均衡器将你的页面发送到GKE。它表示GKE的Pod处理了该转账。

这两个名称来自环境变量。aws/1.0.0来自EC2的.env文件。gke/0.0.0-unset来自k8s/base/deployment.yaml,其中APP_VERSION是流水线不会修改的占位符。占位符是预期行为,不是故障。

顶部的黑色条形显示当前正在响应的进程。最近活动显示历史记录。

#### 步骤3:终止Pod并测量恢复时间

删除一个API Pod并测量Kubernetes替换它所需的时间:

kubectl -n staging get pods -l app=clearpay-api POD=$(kubectl -n staging get pod -l app=clearpay-api -o jsonpath='{.items[0].metadata.name}') echo "deleting $POD" START=$(date +%s) kubectl -n staging delete pod "$POD" kubectl -n staging wait --for=condition=Ready pod -l app=clearpay-api --timeout=120s echo "recovery $(( $(date +%s) - START )) s" kubectl -n staging get pods -l app=clearpay-api

code

kubectl delete pod 会等待旧的 Pod 被删除,包括 k8s/base/deployment.yaml 中定义的 5 秒 preStop 暂停。这个暂停允许正在进行的请求完成后再终止进程。恢复时间是从删除到新 Pod 就绪的时间。之前运行这个实验时显示 42 秒。你的结果会不同。将你的数字写入上方的表格。

如果等待超时,运行 kubectl -n staging get pods 和 kubectl -n staging describe pod 查看新 Pod 的信息。

当 Terminal A 隧道指向的 Pod 被删除时,隧道可能会断开。如果 Terminal A 退出,请在第 4 步前重新启动端口转发。

#### 步骤 4. 在负载下重启部署

这一步需要修改 scripts/load-test.sh 脚本。如果不做修改,即使请求失败,failed= 仍会显示 0。该脚本只统计获得响应的请求。当 Pod 重启时,隧道断开,请求得不到响应,脚本会停止该工作线程但不会统计失败。

在编辑器中打开 scripts/load-test.sh。将整个 hit() 函数替换为以下内容。新增了两处内容:curl 行的 2>/dev/null || true,以及最后的 if 语句。

hit() { local path="$1" out code secs out=$(curl "${CURL_OPTS[@]}" -o /dev/null -w '%{http_code} %{time_total}' \ -H "Authorization: Bearer $TOKEN" \ "$BASE_URL$path" 2>/dev/null || true) code="${out%% *}" secs="${out##* }" echo "$code" >> "$CODES" node -e "process.stdout.write(String(Math.round(Number(process.argv[1]) * 1000)) + '\n')" "$secs" >> "$TIMES" if [ "$code" = "000" ]; then sleep 0.5; fi }

code

当 curl 没有收到响应时会报告 000。短暂的暂停可以防止失败的工作线程每秒循环数千次。

现在在 Terminal B 中运行负载测试。它运行 40 秒,因此重启操作可以包含在内:

./scripts/load-test.sh http://localhost:8080 \ [email protected] password123 5 40

code

在按下该命令的 Enter 键前,先在第三个终端输入重启命令并保持准备。在负载测试开始后的三秒内,按下第三个终端的 Enter 键:

kubectl -n staging rollout restart deployment/clearpay-api kubectl -n staging rollout status deployment/clearpay-api kubectl -n staging get events --sort-by=.lastTimestamp | tail

code

事件显示旧 Pod 停止和新 Pod 启动,每次只处理一个 Pod。

Staging 环境允许在替换 Pod 启动前让一个 Pod 下线(k8s/overlays/staging/patch-deployment.yaml 中的 maxUnavailable: 1 和 maxSurge: 0)。小型实验室集群没有空间容纳额外 Pod。因此在此重启期间,请求会短暂失败,failed= 大于 0 是正常现象。记录失败次数。Terminal A 可能显示 lost connection to pod。用 Ctrl+C 停止它,并在测试结束后重新启动隧道。

生产环境更安全。它的基础配置会在旧 Pod 停止前启动新 Pod(maxSurge: 1 和 maxUnavailable: 0)。

通过:30秒测试中失败次数为0,删除后替换的Pod成功就绪,且滚动状态完成。只要GKE的p95值不超过AWS的3倍,即使其p95值高于AWS,在本路径上仍视为通过,这符合你在第6章设置的失败阈值。

失败:30秒测试中出现5xx错误,登录时出现internal_error错误,或新Pod始终无法就绪。需要修复预发布环境,推送至流水线,并重新运行本章内容。

  • 两个评分表并排显示在你的笔记中
  • Pod恢复有时间限制
  • 在负载运行期间滚动重启完成,且你记录了failed=的状态

在终端A中使用Ctrl+C停止端口转发。

这是关键检查点。GKE已成功承受负载和Pod终止。到目前为止的所有检查均在无客户流量的路径下完成。

现在情况将发生变化。

第6部分:如何将客户迁移至GKE

这是客户可以迁移到GKE的部分。两个云平台均可处理请求。从这里开始的每一步都提出一个问题:什么证据能让你迁移更多流量?

第19章:部署生产环境与GKE网关

第三周,星期一,上午9:00

这是客户可以迁移到GKE的阶段。没有人会公开讨论这件事。办公室比平时安静一些。

到目前为止,GKE仅承载预发布环境。预发布环境是通过隧道访问的私有副本。客户将使用一个名为生产环境的独立副本。本章将构建这个副本,并连接客户将调用的地址。

目前缺少两样东西:

  • 生产环境命名空间。命名空间是集群内的独立区域。预发布环境已有命名空间,但生产环境尚未创建,因此目前没有生产环境Pod。你将创建该命名空间,并将通过预发布关卡的相同镜像部署到其中。
  • GKE边缘IP后端的内容。在第8阶段,你为GKE保留了一个公共地址,称为边缘IP。它就像你预留但从未连接的电话号码:电话会响,但没人接听。你将将其连接到网关。网关会响应每个请求,并将其转发给生产环境Pod。

完成部署后,GKE将能够响应真实请求。但此时仍不会迁移任何流量。直到第21章,前端仍将把100%的客户流量发送至AWS。

只有当GKE上有能响应的组件时,负载均衡器才能将流量发送至GKE。这个组件就是网关:一个Kubernetes对象,它告诉Google在服务前构建负载均衡器。此处它监听clearpay-gke-edge-ip,并将请求转发至生产环境API和UI。

08-edge负载均衡器通过互联网NEGs(指向外部IP地址的网络端点组)连接两个云平台。这种类型没有健康检查。如果后端宕机,负载均衡器仍会持续发送流量,客户将收到502错误。你的防护措施包括synthetic-health.sh脚本、决策网关和rollback.sh脚本。

#### 步骤1:启用网关API

第3阶段的集群尚未启用该功能:

code
gcloud container clusters update clearpay --zone us-east4-a \
  --gateway-api=standard

等待完成,然后执行:

code
kubectl get gatewayclass

查找gke-l7-global-external-managed。这是k8s/gateway/gke-gateway.yaml使用的类。

#### 步骤2:为节点池腾出空间

三个e2-medium机器在GKE系统Pod占用后,每台约剩940m CPU。预发布和生产环境无法同时容纳。需要缩减预发布规模以腾出空间。其镜像会保留在Artifact Registry,网关测试结果会保留在你的笔记中。

首先记录预发布环境运行的精确镜像。生产环境必须运行相同的字符串:

code
kubectl -n staging get deploy clearpay-api \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
kubectl -n staging get deploy clearpay-ui \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

然后将 staging 缩放至零:

code
kubectl -n staging scale deploy/clearpay-api --replicas=0
kubectl -n staging scale deploy/clearpay-ui --replicas=0

在流量切换期间不要推送至 main 分支。推送操作会以完整规模重新部署 staging 环境,并重新占用 CPU 资源。

如果稍后生产环境 API Pod 因 CPU 不足而保持 Pending 状态,说明 staging 环境仍在占用 CPU 资源。除非你接受增加另一台机器的成本,否则不要提升节点池的最大容量。

#### 第 3 步:部署生产环境

使用流水线。这是第 14 章为当前场景构建的路径。

通过流水线操作:

  • 在 GitHub 上打开 Actions,然后在左侧列表中选择 Build、scan 和 deploy。从工作流页面开始操作,而不是从已完成的运行记录开始。如果你正在查看单个运行记录,请点击其名称旁边的返回箭头。旧运行记录中的 promote-to-production 作业会显示 "This job was skipped"(此作业已跳过),并且会保持跳过状态。你需要一个新的运行记录。
  • 点击 Run workflow(运行工作流)。保持分支在 main,勾选 promote_to_production,然后点击 Run workflow(运行工作流)。
  • promote-to-production 作业会停留在 "Waiting for review"(等待审核)状态。点击 Review deployments(审核部署),勾选 production(生产环境),然后点击 Approve and deploy(批准并部署)。
  • 观察 Find the currently-deployed staging images and Deploy those exact images to production(找到当前部署的 staging 镜像,并将这些精确镜像部署到生产环境)直到出现勾选标记完成。

该作业会从 staging Deployment 中读取镜像字符串,即使 staging 现在运行零个 Pod,也会部署这些精确的镜像。

如果流水线未设置:

这些步骤与作业执行的步骤相同。文件中的 PROJECT_ID 是占位符,需要像流水线那样替换它。在 Mac 上,sed -i 需要一个空的 '' 作为参数:

code
cd k8s/overlays/production
PROJECT_ID=$(gcloud config get-value project)
sed -i '' "s/@PROJECT_ID.iam/@${PROJECT_ID}.iam/" patch-serviceaccount.yaml
sed -i '' "s#projects/PROJECT_ID/#projects/${PROJECT_ID}/#" secretproviderclass.yaml

在 Linux 上,-i 后面不需要 ''。

将 overlay 指向你在第 2 步中记录的两个镜像。永远不要使用 latest:

code
kustomize edit set image \
  REGION-docker.pkg.dev/PROJECT_ID/clearpay/api=STAGING_API_IMAGE
kustomize edit set image \
  REGION-docker.pkg.dev/PROJECT_ID/clearpay/ui=STAGING_UI_IMAGE
kubectl apply -k .
kubectl -n production rollout status deployment/clearpay-api --timeout=180s
kubectl -n production rollout status deployment/clearpay-ui --timeout=180s

将 STAGING_API_IMAGE 和 STAGING_UI_IMAGE 替换为第 2 步中得到的完整字符串,格式为 us-east4-docker.pkg.dev/.../api:<tag> 。

不要提交 sed 和 kustomize edit 操作后的文件。它们现在包含你的项目 ID 和真实标签。部署完成后,从仓库根目录运行 git checkout k8s/overlays/production 来撤销这些更改。

#### 检查生产环境

返回仓库根目录。等待 Pod 准备就绪:

code
kubectl -n production get pods

API 和 UI Pod 显示 Running 状态且 READY 1/1。然后从生产环境 Pod 而非 staging 环境检查数据库:

code
kubectl -n production exec deployment/clearpay-api -- node -e \
  "require('./src/db').checkConnection().then(() => { console.log('connected'); process.exit(0); }).catch(e => { console.error(e); process.exit(1); })"

#### 第 4 步:应用 GKE 网关

第 8 阶段保留了 clearpay-gke-edge-ip 和证书 clearpay-gke-edge-cert。现在为它们分配服务目标:

code
kubectl apply -f k8s/gateway/gke-gateway.yaml
kubectl apply -f k8s/gateway/httproute-production.yaml
kubectl apply -f k8s/gateway/healthcheck-api.yaml
kubectl -n production get gateway clearpay-gke-edge

等待直到 PROGRAMMED 显示为 True。这个过程可能需要几分钟,因为 Google 正在构建负载均衡器。

这三个文件分别负责三项任务:

  • gke-gateway.yaml 在保留的 IP 上创建 Gateway 并配置证书。
  • httproute-production.yaml 将请求路由到生产环境的 Service。
  • healthcheck-api.yaml 告诉 Google 对 /health 路径执行健康检查。如果没有这个配置,Google 会检查根路径 /,API 会返回 404,导致请求失败并显示没有健康的上游服务。

#### 检查 Gateway

通过 GKE 的边缘 IP 直接访问 GKE。--resolve 参数会让 curl 在连接到 GKE IP 时使用证书中的主机名:

code
HOST=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname)
EDGE=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw gke_edge_ip_address)
curl -k --resolve "$HOST:443:$EDGE" "https://$HOST/health"

查找输出中的 "platform":"gke"。如果 curl 显示 no healthy upstream,等待一分钟后再重试。健康检查需要时间标记 Pod 为健康状态。

不加 --resolve 参数时,相同主机名仍会路由到 AWS,因为权重是 100 / 0。

  • kubectl get gatewayclass 会列出 gke-l7-global-external-managed
  • 生产环境 API 和 UI 的 Pod 处于 Running 状态,READY 1/1,使用 staging 镜像标签
  • checkConnection 会显示从生产环境 Pod 的连接状态
  • Gateway 显示 PROGRAMMED True,边缘 IP 返回 "platform":"gke"

第20章。执行割接前的检查清单

星期一,下午3:00

生产环境已上线。Maya 手持笔记本电脑走到你桌前。

Maya:"在切换流量之前,请告诉我具体如何将客户重新路由到 AWS,然后演示它确实可行。"

#### 回滚演练的重要性

scripts/rollback.sh 按以下顺序执行两个操作:

  • 运行 shift-traffic.sh 100 0,这会立即将所有流量重新路由到 AWS。
  • 通过 SSH 连接到 AWS 服务器,在其 .env 文件中设置 ROLLBACK_ACTIVE=true 并重启 API。UI 随后会显示回滚横幅。

仪表盘特意显示两种不同消息:

  • 新版本消息是右下角的小弹窗,显示 "Noticed a change: this request ran on gke/... (previously aws/...)",五秒后消失。当不同版本响应时会出现。
  • 回滚横幅是红色的横贯顶部的条带,显示 "Rolled back to AWS, traffic has been restored while the GKE issue is investigated." 直到清除标志才会消失。

任何查看应用的人都能区分 "新版本已发布" 和 "出现问题并已恢复"。

第一次执行回滚不应该是在真实故障发生时,此时客户正在失败,希望 SSH 密钥有效。现在在一切正常时执行回滚测试。当前权重已经是 100 / 0,因此流量切换步骤不会产生任何变化。需要测试的是 SSH 步骤和横幅显示。

#### 检查清单

用你实际执行的命令勾选每一项,而不是凭记忆。使用你自己的弹性 IP 和主机名。

目前只需四条命令:GKE 健康、回滚测试、AWS 健康、合成测试运行。其他五项使用你已有的结果,只需阅读并勾选。第21章和第22章不会重复这个检查清单。它们只复用合成测试,你需要让它在自己的终端中持续运行。

code
割接前检查清单

 /think

[ ] GKE 状态正常  
    kubectl -n production get pods  
    (全部处于 Running 状态,READY)  

[ ] 数据库状态正常  
    生产环境的 checkConnection(第 19 章)输出 connected  

[ ] 监控服务已启动(可选,仅当你完成了第 17 章)  
    Grafana 在 http://localhost:3000 可加载  
    它不会监控 GKE。不要因 GKE 系列为空而阻塞  

[ ] 回滚已测试  
    YOUR_ELASTIC_IP 是你的 AWS 服务器的公网 IP。获取方式:  
    terraform -chdir=infra/aws/terraform output -raw instance_public_ip  
    ./scripts/rollback.sh YOUR_ELASTIC_IP ~/.ssh/clearpay-deployer  
    打开 https://YOUR_ELASTIC_IP,登录后查看仪表盘顶部的红色条状提示  
    ("Rolled back to AWS ...")。然后清除它:  
    ./scripts/clear-rollback-flag.sh YOUR_ELASTIC_IP ~/.ssh/clearpay-deployer  

[ ] AWS 状态正常  
    ./scripts/promotion-gate.sh https://YOUR_ELASTIC_IP \  
      [email protected] password123 [email protected]  
    5 项通过,0 项失败  

[ ] 合成测试正在运行  
    ./scripts/synthetic-health.sh https://YOUR_PUBLIC_HOSTNAME  
    整个轮班期间让它在独立终端中持续运行  
    YOUR_PUBLIC_HOSTNAME 是 api.clearpay.test。从仓库根目录确认:  
    terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname  
    每秒输出一行:时间、状态码、延迟  

[ ] 限速指标已确认  
    第 6 章的限速指标,加上第 18 章的表格:  
    AWS p95 745 毫秒,GKE 预发布环境 p95 655 毫秒,均 0.0% 错误,加上第 18 章的 Pod 恢复时间  

[ ] 安全审查通过  
    第 16 章的四项检查均已记录  

[ ] 成本账单已更新  
    infra/aws/COST.md 和 infra/gcp/COST.md,两个云平台现已开始计费  

#### 回滚演练的输出内容

流量权重已为 100 / 0,因此第一步不会产生任何变化。第二步才是真正的测试,最终输出如下:

code
== 流量分配已更新 ==
"AWS: 100% / GKE: 0%"

== 第二步:将 AWS 标记为回滚目标 ==
AWS ROLLBACK_ACTIVE=true,服务已重启。

== 回滚完成 ==

脚本最后一行显示的旧章节编号可以忽略。真正的回滚步骤在第 23 章。

如果第二步卡在 ssh: connect to host ... port 22: Operation timed out,说明你的家庭 IP 已变更,但 AWS 防火墙仍允许旧 IP。通过 curl -s ifconfig.me; echo 获取当前 IP,将其与 /32 一起写入 infra/aws/terraform/terraform.tfvars 中的 ssh_allowed_cidr。在 infra/aws/terraform 执行 terraform apply,然后重新运行脚本。

所有项目均标记完成,且 GKE 边缘 IP 返回 "platform":"gke":进入第 21 章。

有任何未标记项:立即停止。当 GKE 边缘 IP 为空时,不要运行 ./scripts/shift-traffic.sh 99 1。1% 的用户将访问没有后端服务的地址。

告知 Maya:"回滚只需一个脚本。它能在数秒内切换流量并显示横幅。我五分钟后执行过一次。"

  • 每个检查项均通过,且输出与你实际看到的一致

第 21 章. 将 1% 流量导向 GKE

星期二,上午 10:00

整个团队在一次会议中。Chris 在第二块屏幕上打开了支持邮箱。Amara 在自己的屏幕上打开了交易表。Daniel 坐在你旁边,打开了负载均衡器页面。

Maya:"将 1% 流量导向 GKE。关注错误和转账情况。如果发现异常,立即切换回全部流量。"

#### 为什么是 1%?

1%的请求。一个请求是从浏览器到API的一个消息:登录、余额查询、支付。每100个请求中,大约99个会发送到AWS,约1个会发送到GKE。负载均衡器是根据每个请求进行路由选择,而不是针对每个客户。因此,一个客户在单次访问中可能同时被路由到两个云平台。

1%的流量足以发现合同中的问题,例如JSON结构不一致、缺失的路由或单边登录失败。但这个比例太小,无法测试容量。第22章将处理容量测试。

code
./scripts/shift-traffic.sh 99 1

该脚本会重新应用08-edge配置,但会使用新的权重更新负载均衡器的URL映射。这个操作不需要DNS变更,也不需要重新部署。

现在将相同的权重复制到infra/gcp/terraform/08-edge/terraform.tfvars文件中:

code
aws_traffic_weight = 99
gke_traffic_weight = 1

脚本通过命令行参数传递权重值。在08-edge目录下的下一个普通terraform apply命令会读取该文件。如果文件中仍然显示100和0,那么这个apply操作会静默地将流量切换回去。

确认文件内容和实时状态一致:

code
grep traffic_weight infra/gcp/terraform/08-edge/terraform.tfvars
terraform -chdir=infra/gcp/terraform/08-edge output current_traffic_split

你应该看到aws_traffic_weight = 99,gke_traffic_weight = 1,以及"AWS: 99% / GKE: 1%"。如果文件中仍然显示100和0,请立即修正。

#### 检查:是否有请求到达GKE?

在1%的流量比例下,几乎每个请求仍然会发送到AWS。连续40个请求都显示aws是正常现象。循环直到出现gke:

code
HOST=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname)
IP=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw static_ip_address)
while true; do
  curl -sk --resolve "$HOST:443:$IP" "https://$HOST/health"
  echo
  sleep 0.2
done

当看到"platform":"gke"时,按Ctrl+C停止。本实验中的一次运行需要200次请求:197次aws和3次gke。

为了确认GKE仍然能正常响应而无需等待随机事件,直接调用其边缘IP地址。这不是客户请求:

code
HOST=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname)
EDGE=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw gke_edge_ip_address)
curl -sk --resolve "$HOST:443:$EDGE" "https://$HOST/health"

查找"status":"ok"和"platform":"gke"。本实验输出:

code
{"status":"ok","served_by":{"platform":"gke","version":"0.0.0-unset","instance":"clearpay-api-6f9448c857-mj2j6","commit":"unset","rollback_active":false}}

你的实例信息会有所不同。如果platform显示aws,或者curl命令失败,说明GKE没有响应。不要提高权重。

#### 检查:通过公共地址的登录是否有效?

从现在开始,一个客户可能在一次请求时被路由到AWS,下一次请求时被路由到GKE。两者都必须接受相同的请求并给出相同的响应。如果它们的响应不一致,现在在1%的流量比例下就应该发现,而不是等到50%时再发现。通过公共地址发送登录请求:

code
HOST=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname)
IP=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw static_ip_address)
curl -sk --resolve "$HOST:443:$IP" \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"password123"}' \
  "https://$HOST/api/auth/login"

在响应中查找"token"和"user"。如果看到internal_error,请停止并不要提高权重。

#### 检查:像客户一样使用应用

打开 https://api.clearpay.test 并点击证书警告提示。登录后查看余额,发送小额资金。'Recent activity'下方的每一行都显示为由 aws/1.0.0 或 gke/0.0.0-unset 提供服务。只有最新一行会显示当前请求信息,旧记录保留其写入时的时间戳。

顶部的黑色条形图几乎每次都会显示 AWS,占比约 1%。关注新活动记录和流量循环,而非条形图本身。

快速查看 synthetic-health.sh 终端。每行都应该显示状态 200 。

  • 流量循环至少一次显示 gke,登录返回令牌,合成健康检查仅显示 200:前往第 22 章。
  • 其他任何情况:停止并调查。不要提升权重。如果影响客户,运行 rollback.sh 并前往第 23 章。
  • 文件和 current_traffic_split 的权重均为 99 / 1
  • 流量循环至少一次打印了 "platform":"gke"
  • 通过公网地址登录返回令牌

第 22 章。将流量提升至 100%

星期二上午 11:30 至星期三下午

1% 的流量已验证。这意味着每 100 个请求中有 1 个发送到 GKE。现在你需要逐步提升流量比例。没有自动机制可以完成这个操作,必须手动调整,并在每一步操作前检查数据。

每次都要检查以下三项:

  • 评分表:应用响应速度和失败请求数量
  • 副本数量:正在运行的 API 实例数量
  • 连接计算:数据库是否仍有足够连接容量
code
99 / 1     AWS ████████████████████░    GKE ░
90 / 10    AWS ██████████████████░░     GKE ██
75 / 25    AWS ███████████████░░░░░     GKE █████
50 / 50    AWS ██████████░░░░░░░░░░     GKE ██████████
25 / 75    AWS █████░░░░░░░░░░░░░░░     GKE ███████████████
10 / 90    AWS ██░░░░░░░░░░░░░░░░░░     GKE ██████████████████
 0 / 100   AWS                          GKE ████████████████████

#### 在达到 50 / 50 之前:进行连接计算

在你到达中点前,正在通话的 Daniel 插话提问:"如果 GKE 完全扩容时 AWS 仍处于运行状态,RDS 最多能承受多少数据库连接?"

每个 API 进程最多可打开 DB_POOL_SIZE 个连接,默认为 10。连接池根据负载需求动态开启连接,因此在流量低时连接数保持较低。在满负荷时,每个进程可维持完整的 10 个连接。数据库有硬性限制 max_connections,当总连接数超过该限制时,新连接会被拒绝,请求等待,客户将看到超时。

获取数据库的限制值。这在第 5 章已从 AWS 服务器运行 psql 获取:

code
export INSTANCE_IP=$(terraform -chdir=infra/aws/terraform output -raw instance_public_ip)
DATABASE_URL=$(terraform -chdir=infra/aws/terraform output -raw database_url)
ssh -i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP "psql '$DATABASE_URL' -c 'SHOW max_connections;'"

从 k8s/overlays/production/patch-hpa.yaml 中的 maxReplicas 获取 GKE 的上限。然后用你的数值填写下表:

code
RDS max_connections:          ___
AWS, 1 process × 10:          10
GKE, maxReplicas × 10:        ___
Worst case, both at once:     10 + ___ = ___

本实验的填表示例如下,供你核对:

code
RDS max_connections:          81
AWS, 1 process × 10:          10
GKE, maxReplicas × 10:        30    (3 pods × 10)
Worst case, both at once:     10 + 30 = 40

40 小于 81,因此可以增加流量。如果你的 RDS 实例大小不同,max_connections 的值可能会有所不同。关键在于确保最坏情况下的连接数始终低于该限制。

现在为更大的部署运行相同的计算。一家使用更大机器的公司可能将 HPA 从 3 个 Pod 扩展到 8 个 Pod:10 + 80 = 90 连接,而小型 RDS 实例的限制约为 87。

单独来看,这两种配置都没有错误。连接池大小是在 API 代码中设置的,Pod 上限是在 Kubernetes 文件中设置的,不同的人在不同时间设置的。只有当两个云平台同时满负荷运行时,它们才会发生冲突,而这正是渐进迁移所创造的场景。

如果你的最坏情况下的连接数超过 max_connections,请停止并阅读第 23 章的修复方案,然后再增加权重。

每个阶段都有相同的四个步骤。完成所有四个步骤后,再用下一组数字重复操作。

  • 转移:使用新数值对运行 ./scripts/shift-traffic.sh。第一个数字是 AWS 的占比,第二个是 GKE 的占比。
  • 保存:将相同的两个数字写入 08-edge 目录下的 terraform.tfvars。
  • 检查:运行下一节中的六个检查项。没有任何一个脚本能完成所有检查项。你需要使用一些命令,加上两个脚本:synthetic-health.sh(已经在运行)和 load-test.sh。
  • 决策:所有检查项都通过:进入下一阶段。有任何一个检查项失败:停止。

阶段顺序如下:

阶段 | 命令 | AWS 获取 | GKE 获取 --- | --- | --- | --- ./scripts/shift-traffic.sh 90 10 | 90% | 10% ./scripts/shift-traffic.sh 75 25 | 75% | 25% ./scripts/shift-traffic.sh 50 50 | 50% | 50% ./scripts/shift-traffic.sh 25 75 | 25% | 75% ./scripts/shift-traffic.sh 10 90 | 10% | 90% ./scripts/shift-traffic.sh 0 100 | 0% | 100%

一次运行一行,永远不要同时运行所有行。

#### 每个阶段的检查项

  1. 指针已移动。脚本的输出是 Terraform 的设置,不是证明。确认文件内容匹配:
  1. 实际请求遵循指针。在转移后等待三分钟,然后统计响应情况:
code
HOST=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname)
IP=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw static_ip_address)

for i in $(seq 1 40); do
  curl -sk --resolve "$HOST:443:$IP" "https://$HOST/health"
  echo
done | tee /tmp/clearpay-health.txt

grep -c '"platform":"aws"' /tmp/clearpay-health.txt
grep -c '"platform":"gke"' /tmp/clearpay-health.txt

随着 GKE 权重的增加,GKE 的响应数量应该增加。40 次请求只是一个粗略的样本,并非百分比证明。Google 需要几分钟时间将新的分流应用到所有地方,因此过早统计会显示旧的分流。该实验观察到:

code
90 / 10   80 次请求:  75 aws,  5 gke
50 / 50   40 次请求:  34 aws,  6 gke
25 / 75   40 次请求:  17 aws, 23 gke
10 / 90   40 次请求:   8 aws, 31 gke
 0 / 100  40 次请求, 1 分钟后:    3 aws, 37 gke
 0 / 100  40 次请求, 90 秒后:  0 aws, 40 gke

如果 10 分钟后计数仍然不符合指针,询问 Google 当前的分流情况:

code
gcloud compute url-maps describe clearpay-migration-map | grep -B2 -A2 weight

在 0 / 100 阶段,clearpay-gke-backend 行显示 weight: 100,AWS 行则完全不显示 weight,因为 Google 会省略零值。如果权重与文件中的值不一致,请在 08-edge 中再次运行 terraform apply。

  1. GKE 状态正常。
code
kubectl -n production get pods -l app=clearpay-api
kubectl -n production get hpa clearpay-api

Pods 正在运行且处于就绪状态。HPA 界面显示当前 CPU 使用率与 70% 的目标值对比,以及当前副本数量。本实验在整个 25/75 的过程中保持 1 个副本,CPU 使用率在 4% 到 7% 之间。

  1. 登录功能仍然正常。在每个阶段再次运行第 21 章的登录 curl 命令。只有在特定流量组合下才可能出现合同问题。连续 5 次失败意味着停止,而不是扩容。
  1. 错误保持在限制范围内。synthetic-health.sh 终端应仅显示 200 状态码。如果某行显示其他内容,请停止并不要扩容。
  1. 用户界面。登录 https://api.clearpay.test,发送资金,并阅读最新服务记录。

#### 每个阶段的决策

Maya 每次都会问同样的问题:"我们准备好进行下一步了吗?"

用证据回答:"仪表盘与文件一致,GKE 的请求占比已提升。Pods 健康,登录功能正常,合成健康检查全部显示 200。最坏情况下的连接数仍在数据库限制范围内。"

如果任何检查失败,请不要扩容。如果影响到客户,请立即前往第 23 章。

  • 文件和当前流量分配中的权重为 0/100
  • 连续 40 次请求返回 "platform":"gke"
  • 连接计算使用了你自己的数字进行记录
  • 合成健康检查在所有阶段均未出现持续错误

第23章 当数据变糟时

星期三,下午4:12

最后一班岗已完成。所有客户现已迁移至 GKE,健康检查持续输出 200,这意味着一切正常。支持邮箱在过去一小时保持安静。Amara 再次检查交易表。

"看起来很无聊,"她说。这是她能说的最好的话。

你等待着有人宣布这次迁移成功。

Daniel 仍在查看部署情况。他统计 GKE 中运行的 API 实例数量,然后查看数据库指标。

"我能跟你讲讲我上一份工作中最糟糕的下午吗?"

Chris 把椅子转过来。

Daniel 向后靠了靠。

"那是另一家公司的迁移,但基本问题是一样的。我们当时正在将客户从一个云平台迁移到另一个云平台,到周五下午时已经迁移了大约一半的客户。流量开始上升,新环境开始增加更多 API 实例。"

他停顿了一下。

"旧环境仍在运行。它还在为尚未迁移的客户提供服务。"

你看着屏幕上的部署情况。

"所以两个环境都在使用数据库?""是的。而且每个 API 实例都需要数据库连接。"

他指着图表中的数据库。

"这就是我们遇到麻烦的地方。"Daniel 拿起一支笔。

"想象一家座位数固定的餐厅。餐厅有足够的座位容纳一定数量的人,但座位数量不会因为来的人更多而增加。"

他在白板上画出两组人群。

"AWS 是一组,GKE 是另一组。两组人都在使用同一家餐厅。"

他在 GKE 旁边又画了一组。

"然后流量增加。GKE 增加了更多 API 实例,每个新实例都需要数据库连接。旧的 AWS 实例仍然保持着它们的连接。"

他放下笔。

"数据库有连接限制。我们不断增加进入数据库的连接数量。"

"当达到限制时发生了什么?"你问。

"请求开始等待连接。然后开始超时。"

他看着你。

"应用程序本身看起来并没有明显损坏。如果容器正在运行且健康检查通过,问题就出在容量上。我们的数据库连接需求超过了数据库能够提供的上限。"

克里斯回想起那次部署。

"所以你当时在监控新环境,但数据库却要同时处理两个环境的请求。"

"正是如此。"

丹尼尔再次查看图表。

"这就是我们之前忽略的部分。我们只计算了新环境的需求,却没计算两个环境同时运行时数据库的需求。"

他坐回座位。

"最终我们把流量重新切换回旧环境并恢复了服务。但这次事件给我留下了深刻印象。"

你看着GKE部署。"现在你会怎么做?"

"在切换流量之前,我会先理清所有数据。"

丹尼尔把笔记本转向你。

"你的实验环境是安全的。你运行了3个副本,而数据库连接上限是81,工作负载从未接近耗尽这个限制。"

他打开另一份笔记。

"我的事故情况不同。我们最多运行了8个副本,而数据库允许87个连接。"

他看着你。"那我们用这些数字来推演吧。"

你把椅子往前挪了挪。丹尼尔继续说道。

"我会按事件发生的顺序给你提供信息。你现在是值班人员,不要假设你知道结局。当每个事件发生时,告诉我你会检查什么,以及你认为发生了什么。"

他打开终端。

这次推演从两个环境共8个API副本、数据库连接上限87的场景开始。

这次,数字将变得至关重要。

你在这章需要运行操作吗?把它当作一个带有教训的故事读一遍。你的实验环境没有发生事故,因此无需修复。如果你想确认自己确实没问题,运行第2步中的两个命令,并与"你的集群"进行对比。如果日志中没有错误且HPA保持在1个副本,那么你已经完成。继续阅读第24章吧。

#### 现象表现

50/50流量切换后的最初几分钟一切正常。随后响应时间开始缓慢上升。

阿玛拉首先在团队频道注意到异常。

"有几笔转账刚刚超时了。"

丹尼尔打开生产环境仪表盘。API已扩展到7个副本,p95延迟正在上升。

克里斯发布了两份关于转账缓慢的客户报告。

丹尼尔再次查看图表。"这是影响所有请求了吗?"

"还没有,"克里斯说,"到目前为止只有少数几笔转账。"

阿玛拉检查支付记录。"我们得确认这些超时没有导致重复扣款。"

丹尼尔查看数据库指标。"可能是数据库的问题。"

你还不确定。

日志中没有出现"数据库连接耗尽"的提示。第一个信号仅仅是请求耗时比几分钟前变长了。

没有错误信息显示"数据库连接耗尽"。你首先看到的只是最慢响应(p95延迟)变得越来越慢。只有在连续多次健康检查失败后,应用中的状态栏(ui/src/components/StatusRibbon.jsx)才会切换到"降级中。正在调查",因此单个缓慢请求不会触发这个状态。

#### 你已知的信息

流量分布在两个云平台。延迟比AWS基准值更差。状态栏是在多次检查失败后切换的,而不是单次波动。

#### 你尚未掌握的信息

问题可能出在负载均衡器、容器、数据库或糟糕的部署上。因此不要假设。先收集信息。

不要在未理解发生问题之前再次运行 shift-traffic.sh 指向 GKE。记录你注意到问题的时间。

在采取任何其他操作之前,记录你看到的内容:实际的错误信息、实际的副本数量和实际的时间戳。

每花一小时诊断卡顿百分比的问题,就相当于增加了一小时的双运行成本。将其添加到成本账目中。不要因为大家的注意力在别处而让总成本减少。

#### 第2步:收集证据

阅读 API 的错误信息:

code
kubectl -n production logs deployment/clearpay-api --tail=100 | grep error

查找与数据库相关的词汇:连接错误、"太多连接",或在从池中等待客户端时出现超时。没有数据库相关词汇的 5xx 错误可能指向其他位置,例如网关或部署问题。

检查 Kubernetes 正在运行的 Pod 数量:

code
kubectl -n production get hpa clearpay-api

将 REPLICAS 与 maxReplicas 进行比较。副本数上升意味着负载增加。这是一个线索,但尚未确定原因。

然后用实时数据填写第22章表格:

code
RDS max_connections:          87
AWS, 1 process × 10:          10
GKE, 7 replicas × 10:         70
Right now:                    80, close to the limit
GKE at maxReplicas 8:         80, worst case 90, over the limit

#### 你自己的集群在 50 / 50 情况下

在假设你处于 Daniel 的情境之前,先在你的集群上运行相同的两个命令。这个 AWS 50 / GKE 50 的实验显示:

code
HPA:       cpu 4%/70%, MINPODS 1, MAXPODS 3, REPLICAS 1
pods:      1 Running
logs:      最近100条日志中没有错误信息
RDS:       max_connections=81
           数据库 clearpay 上有2个会话
status:    没有降级

在此情况下最坏情况是达到40,低于81。除非你的数值超过第6章的限制,否则不要运行 rollback.sh。

如果 HPA 仍然为1且日志没有异常,请记录下来。这是证据。坚持使用这些数据。

#### 故障如何形成

单独的 AWS 不会达到这个程度。单独的 GKE 也不会达到这个程度。这种情况只会在两者同时满负荷运行时发生,而迁移计划有意让两者同时运行。

告诉 Daniel:"副本数量上升。池计算使我们接近数据库限制。这是容量问题,不是 Kubernetes 的问题。"

#### 第3步:回滚

Maya:"你建议怎么做?"

将这些数值与你在第6章中写的限制进行比较。现在不要发明新的限制。

"将客户重新放回 AWS。错误已经超过了我们的限制,客户受到影响,且回滚已经过测试。"

Maya:"好的。执行吧。"

code
./scripts/rollback.sh <aws-instance-ip> ~/.ssh/clearpay-deployer

上面的图表显示了 rollback.sh 执行的两个操作。首先,shift-traffic.sh 100 0 将所有客户流量重新发送到 AWS 并移除 GKE 的流量。其次,脚本通过 SSH 连接到 AWS EC2 服务器,在 API 的环境文件中设置 ROLLBACK_ACTIVE=true,然后重启 API 服务。API 随后报告 AWS 为活动平台,并在 ClearPay UI 中启用独特的“已回滚到 AWS”横幅。这些步骤恢复了流量,并使回滚对操作员可见。

将 08-edge 中的 terraform.tfvars 设置回 100 和 0,以防止后续的 apply 操作撤销回滚。

#### 第4步:确认回滚成功

code
curl -k https://api.clearpay.test/api/info

platform 读取 aws 且 rollback_active 读取 true。刷新 UI:红色的“已回滚到 AWS”横幅出现,当足够多的健康检查通过后,状态栏将恢复为“所有系统正常运行”。

然后从证据而非假设中检查以下内容:

  • 请求停止失败(synthetic-health.sh 再次显示 200 状态码)
  • 延迟已恢复到接近第 6 章 AWS 的数值
  • 数据库保持一致性:回滚前的转账记录显示恰好一次借记,而非零次或两次

Amara:"我不关心流量是否已回到 AWS。转账真的只发生了一次吗?"

通过数据库的交易历史记录而非负载均衡器来确认。第 3 章提到的幂等性密钥正是让重试转账在此场景下安全的关键,即使首次尝试发送到 GKE,重试发送到 AWS 也不会导致问题。

为事件回顾记录一个数字:从"注意到问题"到"流量回到 AWS"所用的分钟数。

#### 第 5 步:修复根本原因

Daniel:"你将做出什么改变以防止这种情况再次发生?"

有四个实际选项,每个都有其成本:

  • 每个 pod 降低 DB_POOL_SIZE:每个进程的连接数减少。成本低且实施快。每个 pod 同时处理的数据库查询会减少。
  • 在 k8s/overlays/production/patch-hpa.yaml 中降低 HPA 的 maxReplicas。限制总数。GKE 在真实流量激增时无法扩展到以前的程度。
  • 使用自定义参数组提高 RDS 的 max_connections。每个连接都会占用数据库内存,如果设置过高,像 db.t3.micro 这样的小型实例可能无法启动。首先检查 AWS 对应实例尺寸的指导方针。
  • 在 Postgres 前面添加连接池器,如 PgBouncer 或 RDS Proxy。多个应用连接共享少量数据库连接。这是最长期的解决方案,但设置工作量最大。

本书不会为你选择一个方案。写下你选择的方案以及你为此放弃的内容。

对于最小的实际更改,降低连接池大小。每个 pod 6 个连接的计算:8 × 6 = 48 来自 GKE,加上 10 来自 AWS,总计 58。即使在满负荷情况下,这个数值也低于 87 的上限。

在紧急情况下,可以实时设置:

code
kubectl -n production set env deployment/clearpay-api DB_POOL_SIZE=6

实时更改不会留下记录,下次部署时会被覆盖。通过流水线进行真实更改:在 k8s/base/deployment.yaml 中设置 DB_POOL_SIZE,提交并推送到 main 分支。流水线会构建、扫描并部署到 staging 环境。使用第 18 章的步骤,以更高的并发量对 staging 进行充分的负载测试。仅在轻负载下通过的修复证明不了任何问题。然后运行发布门禁,使用第 19 章的流水线任务进行发布。

#### 第 6 步:恢复

Maya:"我们准备好处理 10% 的流量了吗?"

"是的。API 健康,合成检查无异常,错误率在限制内,连接池计算在上限时成立,且回滚已测试。"

Daniel:"同意。"

Maya:"你的决定。"

首先清除回滚标志:

code
./scripts/clear-rollback-flag.sh <aws-instance-ip> ~/.ssh/clearpay-deployer

刷新 UI。回滚横幅已消失。如果保留标志,恢复后横幅仍会显示,这会让人习惯性忽略它。

然后从头开始重复第 21 和 22 章的内容:

code
./scripts/shift-traffic.sh 99 1
# 观察、比较、记录、决定

./scripts/shift-traffic.sh 90 10
# 观察、比较、记录、决定

./scripts/shift-traffic.sh 75 25
./scripts/shift-traffic.sh 50 50
./scripts/shift-traffic.sh 25 75
./scripts/shift-traffic.sh 10 90
./scripts/shift-traffic.sh 0 100

在上次事件发生时的阶段密切观察 HPA 和错误行。正是在那个阶段,修复方案会证明其有效性。

添加一行到成本账本。第二次处理迁移意味着两个云平台同时计费的小时数增加。保持成本的可见性,而不是将其合并为一个“迁移成本”数字。

#### 本章所需技能

上图展示了从观察到恢复的事件响应流程。首先检查延迟、状态栏和错误日志。形成假设,用数据库连接限制进行验证,并使用实时测量数据作为证据。如果客户受到影响,停止流量切换并回滚。通过管道修复根本原因,在最大预期负载下进行测试,只有在证据支持时才恢复迁移。

事件不是寻找答案的狩猎,而是减少不确定性直至能够做出安全决策的工作。如果客户出现故障,安全决策是停止并回滚。如果客户没有受到影响,安全决策是继续观察。不要因为某个数字看起来异常就回滚,也不要因为Pod状态看起来健康就继续迁移。

这是检查点。GKE正在处理100%的流量。

AWS现在是按需付费的安全网,而不是被遗忘的遗留项。

第7部分:如何完成迁移

GKE为每个客户提供服务。AWS仍然运行并计费,作为安全网。本部分将切换操作转化为证据,为安全网标注日期,并移除不再需要的部分。

第24章:用数据证明迁移切换

Maya在上午十点与公司领导层开会。她带着笔记本电脑走到你桌前,屏幕上是一张空白页面。

Maya:"领导们没有参加我们的任何会议,他们会问一个问题:客户是否注意到迁移?我需要一个能放在屏幕上的简短摘要。需要知道通过了多少请求、失败了多少、与AWS相比应用响应速度如何,以及是否有人注意到停机。能在十点前准备好吗?"

你点头。她要求的每个数字都来自你已经掌握的两个脚本。

现在运行两个检查脚本,针对当前100% GKE的公网地址生成硬性数据:

code
HOST=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname)
./scripts/promotion-gate.sh "https://$HOST" \
  [email protected] password123 [email protected]

然后使用第6章的设置,对https://$HOST运行load-test.sh,并复制评分表。将第22章循环统计的请求数和synthetic-health.sh日志数据加入其中。

#### 证据

以下是本实验的表格,你的表格数字会有所不同:

code
生产环境请求(计数检查,非完整仪表盘总数):
  40 /health 在0/100后一分钟:3 aws, 37 gke
  40 /health 九十秒后:0 aws, 40 gke
  load-test.sh 10 × 20s:ok=310  failed=0
  promotion-gate.sh:5通过, 0失败
成功请求数:310 + 40 + 5(来自这些运行)
失败请求数:0
5xx错误:0.0 %
p95延迟(GKE, 100%):647 ms
  第6章AWS基线为164 ms
  第18章GKE预发布(端口转发)为1157 ms
持续可见客户停机时间:0秒

本地Prometheus未监控GKE,因此此表使用计数检查。不要编造从未测量过的仪表盘总数。

将此表与GKE出现前在第6章中设定的限制进行核对:

  • 停机限制:5xx错误没有超过你的阈值。本实验失败请求数为零。
  • 故障限制:p95延迟低于你的上限。本实验的647 ms低于示例上限1500 ms,因此通过。

通过测试并不能说明全部问题。647 毫秒大约是 AWS 164 毫秒的四倍。客户从未经历服务中断,但每个请求的响应时间都变慢了。请将这些事实如实记录,而不是刻意隐藏。本书的最后一章将解释原因。

#### 为什么表格中没有写着“从未发生过失败请求”

任何真实系统在数小时的运行过程中,面对流量变化和可能的回滚操作,都无法诚实地承诺零失败请求。声称这一点会削弱唯一重要的承诺:客户不会看到持续的服务中断。

在你提前设定的限制范围内,少量失败请求才是“这次运行良好”的诚实表述。标准是可控且可衡量的,而非完美无缺。

  • 表格中的每一行都记录了你实际测量的数值
  • 每一行都会与你在第 6 章设定的限制进行核对,包括失败的请求

第 25 章:在 GKE 上运行并为 AWS 设定停用日期

星期五 上午 10:00

Maya 从与领导的会议回来后提出了一个问题。

Maya:"那么客户这边发生了什么变化?"

你查看数据。流量 100% 来自 GKE。数据传输成功。合成检查全部通过。PostgreSQL 仍然运行在 AWS 上。EC2 服务器没有服务任何请求。

"地址保持不变,没有人看到服务中断。"

Maya:"那么这个系统现在归你了。我想要一个停用旧服务器的具体日期。"

#### 第一步:观察运行状态

在做出任何承诺之前,让生产环境实际运行一个完整窗口期。对真正的公司而言,这意味着几天时间,而不是最后一次轮班后的几分钟。

这个实验的 100% 窗口期只持续了几分钟,通过了 5/5 的门禁检查且失败数为 0。请如实记录这一点。周末的实验不等于一周的生产环境运行。

#### 第二步:检查告警

第 7 章创建了告警策略 "ClearPay API - elevated error rate",当 5 分钟内出现超过 5 条错误记录时触发。该策略没有通知渠道,因此没有人会在告警触发时收到通知。一个无法通知到任何人的告警,本质上就不是告警。

对真实系统而言,应在 Cloud Monitoring 中添加通知渠道,并在预发布环境进行测试,而非通过向客户发送错误来触发。对于这个实验,请记录下差距:该策略已存在,但没有人会收到告警通知。

#### 第三步:审查容量

查看窗口期内的 HPA 数据:

这个实验在 100% 负载测试期间,即使达到每秒 15.5 次请求,仍保持 1 个副本和 4% 到 7% 的 CPU 使用率。一个 Pod 就足够了。为一个 API Pod 配置三个 e2-medium 机器,远超当前流量需求。

Google 提供承诺使用折扣(Committed Use Discounts,CUD):以承诺使用一定机器容量 1 或 3 年为条件,换取更低的价格。

仅在拥有稳定使用历史记录后才进行承诺。基于你规划的架构而非实际测量的使用情况做出的承诺,是带有合同约束的猜测。这个实验没有购买任何 CUD,因为几个小时的 GKE 使用时间不足以构成使用历史记录。

#### 第四步:撰写交接文档

为未参与迁移的人员撰写这份文档。这个实验的版本:

code
日志:       kubectl -n production logs deployment/clearpay-api
仪表盘:    http://localhost:3000(如果 observability/docker-compose.yml 已启动)
           GKE 系列可能为空。
告警:      Cloud Monitoring "ClearPay API - elevated error rate"
           没有通知渠道。
回滚:      scripts/rollback.sh,仅在 EC2 仍然存在时可用。
主机名:    terraform output public_hostname
           terraform output static_ip_address
           (请使用你自己的,而不是从本书复制的主机名)

#### 第五步:为 AWS 设定停用日期

scripts/rollback.sh 仅在 EC2 服务器存在时有效,且服务器会按存在的每一天计费。决定其保留时长,记录原因并计算成本:

code
AWS 每日成本:              ~$1.25/天(infra/aws/COST.md)
保留窗口:                  ___ 天
退役日期:                  ___
总安全余量成本:            ___ 天 × $1.25 = $___

本实验未使用任何额外天数,且在 GKE 达到 100% 可用性验证当天即退役 AWS 计算资源,因为这是一个拆除实验环境。

真实企业可能会保留 AWS 一到两周。较短的保留窗口可节省成本,较长的窗口则能获得更多信心。错误的做法是保持 AWS 开启作为“以防万一”的备用资源,这会每天产生费用且永远不会被重新评估。

在日期到来之前就制定退役规则,避免在截止期限压力下仓促决策:

  • 在 GKE 达到 100% 可用性且无回滚操作的情况下保留固定天数
  • 在此窗口期内不因 GKE 问题触发任何告警
  • 由指定人员签署确认

本实验的规则:权重 0/100,门限 5/5,负载测试失败=0,HPA 设置为 1,无回滚。

最后,检查是否仍有其他内容直接指向弹性 IP:书签、其他系统中硬编码的 URL,或负载均衡器创建前设置的外部运行状态监控。

  • 您已对生产环境进行了书面且实时的观察窗口
  • 告警间隙已被记录
  • 容量已审查且 CUD 决策已记录
  • 交接说明已存在
  • 保留窗口、退役日期、成本和规则已记录

第26章。退役 AWS 服务器

星期五,下午3:00

日期已到,且满足第25章的规则。移除 EC2 服务器并保留数据库。数据库从未参与此次迁移。

从仓库根目录执行最终检查。GKE 必须通过公共地址的门限检查:

然后确认 Terraform 跟踪的是您要移除的服务器:

code
cd infra/aws/terraform
terraform state show aws_instance.api   # 确认您选择了正确的资源

接下来,使用 cd ../../.. 返回仓库根目录,然后仅移除 AWS 计算资源:

code
./scripts/teardown-aws-compute.sh
# 读取计划后使用 --yes 参数添加 -auto-approve

完成此步骤后,AWS 回滚功能将被移除。这就是您在第25章做出决策的目的。

#### 阅读销毁计划

该脚本仅销毁以下内容:

  • EC2 实例
  • 弹性 IP
  • 允许 API 访问数据库的安全组规则
  • API 安全组
  • SSH 密钥对
  • CloudWatch 告警

RDS 保留。

数据库规则是独立的 Terraform 资源 aws_security_group_rule.db_from_api,脚本会首先移除它。如果该规则位于数据库安全组内部,AWS 会在数据库仍引用它时拒绝删除 API 安全组。

infra/aws/terraform 中的完整 terraform destroy 还会删除 PostgreSQL。此处不要使用该命令。

#### 清理凭证

  • AWS 中的 SSH 密钥对已通过脚本移除。如果您仅将 ~/.ssh/clearpay-deployer 用于此服务器,请删除本地私钥。
  • 删除所有指向旧服务器的内容,例如保存的主机或包含旧 IP 的脚本。
  • 删除所有引用旧服务器的 CI/CD 密钥或变量。

#### 更新图表

第7章的双云图表现已过时。新的图表应显示 GKE 独立位于负载均衡器后方,数据库仍保留在 AWS,如第7章所述项目结束状态。

  • 拆除前网关已通过公共地址
  • AWS 计算资源已被销毁,RDS 仍然存在
  • 本地部署密钥被有意移除或保留
  • 架构图与实际运行情况一致

第27章 拆除GCP和数据库

星期五,下午5:30

办公室正在清空。两个云服务仍在按小时计费。

这是一个实验环境。一旦完成笔记,请移除所有仍在计费的资源:网关负载均衡器、GKE 机器、Cloud NAT、镜像仓库和 RDS 数据库。

在拆除任何资源之前,请先完成第28章的迁移记录。它要求你记录最终架构、迁移结果、性能指标和总成本。如果还需要从运行系统中获取证据,请立即收集。一旦摧毁基础设施,这些实时测量数据将不再可用。

code
./scripts/teardown-gcp.sh
# 使用 --yes 参数跳过提示并自动批准每个阶段

脚本执行步骤:

  • 删除网关以及生产环境和预发布环境的工作负载。Kubernetes 会为网关创建 Google 负载均衡器组件,而 Terraform 不知道这些资源。先删除网关可以让 Google 移除这些组件。如果跳过此步骤,Terraform 可能会卡在处理未创建的资源上。
  • 按照逆序执行 terraform destroy:08-edge → 07-observability → 06-secrets → 05-identity → 04-registry → 03-gke → 02-network → 01-foundation。后续阶段依赖于早期阶段,因此先拆除依赖项。

在脚本提示时输入 yes,然后逐一阅读并确认每个 Terraform 计划。

命名空间可能在 Terminating 状态停留几分钟。脚本会为每个删除操作等待最多 180 秒,之后继续执行。即使不删除集群,这些命名空间最终也会被移除。

然后删除数据库。迁移时特意将其保留在 AWS 上,但实验环境不应继续支付每天约 0.8 美元的费用:

code
cd infra/aws/terraform
terraform destroy

最后,从 /etc/hosts 文件中删除 api.clearpay.test 行。

本书从未创建或删除你的 GCP 项目。除非该项目仅包含本次实验,否则不要删除项目。

  • kubectl get ns 不再显示预发布或生产环境,或者集群已消失
  • 所有 infra/gcp/terraform/0*-* 阶段均已被销毁
  • infra/aws/terraform 中的 terraform destroy 已完成
  • GCP 和 AWS 的计费记录显示本实验环境已停止运行

这是检查点。工作已经完成。

在遗忘之前,写下实际发生的情况,包括账单记录。

第28章 编写迁移记录

第四周,星期一,上午9:00

在细节淡忘之前,写下发生了什么、架构设计、账单记录以及你会做出哪些改变。

#### 第一部分 架构记录

迁移前:

code
用户 → nginx(TLS) → systemd 管理的 Node 进程 → RDS Postgres

一个 EC2 服务器、一个数据库、一个无人设计的默认网络、明文 .env 文件存储的密钥、没有自动扩展,以及手动执行的部署流程。

迁移过程中:

迁移后:

为每个模型写一个简短段落:

  • 流量:通过全局外部应用负载均衡器的加权后端,由 shift-traffic.sh 脚本调整。不使用 DNS,因为缓存的 DNS 响应会使回滚变慢。
  • 部署:GitHub Actions 构建每个提交到预发布环境,生产环境仅在通过网关审核并获得审核人批准后获取预发布环境的精确镜像。
  • 数据库:从未迁移过。第4章的 RDS 实例始终是唯一的真相来源。
  • 安全性:为容器和流水线均采用工作负载身份。在任何时刻都不存在下载的服务账户密钥。密钥来自密钥管理器。
  • 可观测性:API自身的JSON日志和/metrics端点,在AWS和GKE上表现一致,同时Cloud Logging提供基于日志的错误指标。
  • 回滚:一个用于调整流量权重并设置标志的脚本,在首次迁移前已进行测试。

#### 第二部分:事后分析

你们四人一起查看笔记。Maya率先提出一个大家半是预料的问题。

Maya:"Kubernetes是问题所在吗?"

"不是。它只是执行了我们告诉它的操作。真正需要注意的是数据库。每个API副本可以打开10个连接,GKE可以运行多个副本,而AWS同时还在为客户提供服务。把这些加起来,就接近数据库的限制。

在我们这个小集群中,40个连接与81个的限制值相比还有差距,所以没有出现故障。但在一个可以扩展到8个副本的集群中,同样的总和就会超过限制。"

Daniel点头:"这正是我在上一家公司错过的。所有数据都存在,但没人把它们并排对比。你们在流量迁移前就完成了这项分析。"

Amara:"那支付部分呢?每笔转账是否只被收取一次?"

"是的。第三章提到的幂等性密钥在每次迁移中都有效。我们甚至在100% GKE负载下重放了一笔支付,也没有出现重复扣款。"

Maya合上笔记本电脑:"那么下次我们应该怎么做才能改进?"

请撰写以下部分:

成功之处:

  • 幂等性密钥在GKE出现前就已实现,使客户重试请求即使落在不同云平台也能安全处理。
  • 分阶段的Terraform配置使每个失败点都指向特定阶段。
  • 流水线确保生产环境运行的是通过关卡检测的精确镜像。
  • 回滚方案在第20章就已测试,早于实际需要时完成。

本次实验的失误:

  • 本地Prometheus从未监控GKE,因此第24章使用了计数检查而非仪表板总数。
  • 告警策略没有配置通知渠道。
  • 小型节点池无法运行真实企业会使用的Pod数量,因此连接限制风险是通过纸面计算而非实际观察发现的。
  • GKE的p95性能指标约为AWS的四倍。

根本原因:两个合理的配置参数DB_POOL_SIZE=10和HPA的maxReplicas在不同位置被设置。直到迁移同时运行两个云平台时,才有人检查这些参数与数据库限制的匹配性。一旦发现问题,修复并不复杂。但找到问题需要将第23章当作真正的调查来处理,而非直接跳到答案。

决策与权衡:

  • 选择GKE标准版而非Autopilot:获得对节点池和自动扩展的完全控制权,代价是需要自行理解和配置它们。本次实验为此支付了三台基本闲置的机器费用。Autopilot可能在自己的调度机制中隐藏了连接限制风险,这可能带来好处也可能带来隐患。
  • 使用两个环境而非五个:减少构建和维护成本,代价是测试环境与生产环境共享数据库。在当前规模下是可行的,但不适合所有企业。
  • 使用带有IP白名单的公共数据库端点而非VPN:无需配置时间且无需支付月度VPN费用。代价是RDS必须暴露公共端点,即使只接受单一IP并要求TLS。
  • 保留数据库在AWS:实现更简单安全的迁移,代价是所有查询都需要跨云传输。

所以第二次迁移会带来哪些变化?在任何流量迁移之前,计算所有共享数据库的环境中最坏情况下的连接数。将其与实际限制进行对比,将其作为检查清单项,而非实时发现。

#### 第三部分:成本事后分析

本次实验的版本:

预算:约25美元,用于五天的GCP使用。

实际双运行:总计约4.30美元,其中AWS 1.25美元 + GCP 3.05美元。GCP的3.05美元由免费试用信用额度覆盖,因此实际GCP费用为0美元。

迁移按计划进行。未发生任何事故、无需回滚或第二班次操作。

集群被过度配置:三个GKE节点支持一个API Pod,CPU使用率仅为4-7%。

选择标准GKE是为了教学价值。对于此工作负载,Autopilot会更便宜。

未购买任何CUD。

结论:迁移成本远低于预算,但GKE的配置使用的资源容量超过了工作负载的实际需求。

请从您的云账单中填写自己的数据。本书无法为您完成这部分工作,而猜测的数字也会带来与本项目整体避免的相同虚假精度。

  • 迁移前、中、后的图表与实际运行情况一致
  • 事后分析明确指出了根本原因
  • 成本事后分析保留了真实数据

本项目之后的下一步

本项目留下的开放问题

第13章特意指出并跳过了这个问题:在什么时间点,继续在AWS上维护PostgreSQL的成本(包括风险和工程时间)会超过迁移的成本?跨云迁移、对两个供应商的依赖,以及第23章中提到的连接池压力,这些因素都位于同一侧的天平上。

将您笔记中的两个数字并排对比:

code
AWS,第6章:             p95 164 毫秒  
GKE 100%,第24章:      p95 647 毫秒  

API迁移后,客户从未经历任何停机。但每个请求的响应时间变慢了约四倍。

代码没有变化,负载测试也没有变化。变化的是距离。在AWS上,API和数据库位于同一数据中心。在GKE上,每个查询都会离开Google的us-east4区域,通过Cloud NAT传出,穿越互联网到达AWS的us-east-1区域,然后再返回。单次传输会运行多个查询,因此需要多次支付这段往返的费用。Google的负载均衡器还会在前面增加一个额外的跳转。

Maya查看了表格,问道:“为什么变慢了?”

“数据库仍然留在AWS。每个查询都需要跨云传输。”

“这是个错误吗?”

“不是。第7章决定只迁移API,因为同时迁移两件事会使任何故障都更难追踪。问题可能来自API、网络,或数据库迁移本身。但这个决定是有代价的,现在我们可以量化它。”

Maya:“那么这就是下一个项目。”

本项目并未回答这个开放性问题。它从未打算这样做。下一个项目很可能会将PostgreSQL迁移到Google Cloud,最有可能使用Google的数据库迁移服务,该服务持续复制数据,使最终切换仅需几分钟而非数小时。它将采用本书相同的方法:先建立基准,设定书面限制,采取小步骤,进行测试回滚,并在每个阶段保留证据。完成之后,运行相同的负载测试并对比相同的两个数字。

再次强调,这是另一个独立的项目,需要另一本书来描述。本书在此结束:ClearPay的API运行在GKE上,数据库仍留在AWS,正如第7章所预测的那样。

最终应具备的内容

  • 可运行的金融科技API和最小客户界面(api/、ui/)
  • AWS部署(infra/aws/)
  • GCP 架构,分阶段的 Terraform 配置( infra/gcp/terraform/ )
  • Docker 镜像和 CI/CD 流水线,用于构建、扫描、部署预发布环境并推动已测试镜像( .github/workflows/deploy.yml )
  • Kubernetes 清单文件,双环境 Kustomize 覆盖配置( k8s/base/ , k8s/overlays/ )
  • 数据库连接,已验证且从未迁移(第13章)
  • 密钥管理,从 .env 文件升级到 Secret Manager( k8s/overlays/*/secretproviderclass.yaml , secretsync.yaml )
  • 位于加权负载均衡器后的 GKE Gateway( k8s/gateway/ , infra/gcp/terraform/08-edge/ )
  • 如果完成了可选的第17章,可在笔记本电脑上查看可观测性仪表板( observability/ )
  • 负载测试、发布门控和合成健康检查脚本( scripts/ )
  • 迁移评分卡(第6章、第18章、第24章)
  • 成本账单,从开始到结束(第6章至第28章)
  • 流量转移流程( scripts/shift-traffic.sh )
  • 回滚流程( scripts/rollback.sh , scripts/clear-rollback-flag.sh )
  • 架构图(第4章、第7章、第9章、第11章、第13章、第14章、第19章、第28章)
  • 迁移手册(本书)
  • 技术事后分析(第28章)
  • 成本事后分析(第28章)

ClearPay API 从一个 AWS EC2 服务器迁移至 GKE。出于特定原因,PostgreSQL 仍保留在 AWS。在迁移过程中,客户保持一个地址不变,同时进行流量转移、回滚演练并完成最终切换。

Maya 询问客户在迁移过程中发生了什么变化。地址没有变化,余额也没有变化,没有人注意到服务中断。现在你明白为什么这是最难的部分,以及为什么 647 毫秒将成为下一个挑战。

实验部分涵盖了如何验证每个阶段、如何解读证据以及如何撤销错误的流量转移。销毁你仍在付费的任何资源。

订阅我的电子报,获取新的技术文章、项目实践指南和即将发布的内容。

如何在面试中讨论该项目

我参与了一个名为 ClearPay 的支付应用的云迁移项目。该应用原本运行在 AWS 上,目标是将其迁移至 Google Cloud 而不中断服务访问。

主要挑战是安全地完成迁移。我没有一次性切换所有流量,而是设计了一个受控迁移方案,可以逐步将流量转移到新环境。如果出现问题,可以将流量重新导向原始系统。同时,我将数据库保留在 AWS 上,以缩小迁移范围并避免引入不必要的风险。

我设置了部署流程、安全检查和验证步骤,以确保应用在完成切换前正常运行。

随后我测试了最终的迁移。测试过程中没有请求失败,应用在最终检查中也没有产生服务器错误。我还发现新旧环境之间存在性能差异,需要进一步调查才能确认迁移是否完全优化。

该项目证明了我能够规划并执行云迁移,管理相关风险,验证结果,并识别仍需关注的问题,而不仅仅是因应用正常运行就宣称成功。

面试技巧:准备好解释为什么使用加权流量路由、如何在迁移切换期间验证应用、回滚的触发条件,以及为什么 PostgreSQL 保留在 AWS 上。

合作伙伴推荐课程

(在新标签页中打开)

ADVERTISEMENT

  • Udemy终极DevOps项目实践 Abhishek Veeramalla 4.7(5,672人评分) 11小时47分钟 初学者 证书 查看课程(在新标签页中打开)
  • Udemy Google Cloud上DevOps与AI:AI驱动的CI/CD Faisal Memon(EmbarkX)和EmbarkX官方 5(2人评分) 4小时 初学者 证书 查看课程(在新标签页中打开)
  • Udemy SAP S/4 Hana数据迁移 - Hana迁移仪表板 Sastrageek Solutions 4.5(1,870人评分) 6小时11分钟 初学者 证书 查看课程(在新标签页中打开)
  • Coursera REST Assured Java - 创建书店应用程序 EDUCBA 5(11人评分) 3小时14分钟 初学者 证书 查看课程(在新标签页中打开)

更多Terraform课程请访问Class Central

→

DevOps和云工程师。我将Kubernetes、多云基础设施分解为适合初学者、职业转换者和进阶工程师的实践指南。

如果你读到这里,请感谢作者以表达你的支持。说声谢谢

免费学习编程。freeCodeCamp的开源课程已帮助超过40,000人成为开发者。立即开始