如何将应用程序从AWS迁移到GKE [完整指南]
这个指导性实验将带你了解如何迁移一个小型支付应用。该应用包含使用React开发的用户界面以及运行在AWS EC2实例上的Express API,而数据库则仍保存在AWS上。 你需要将用户界面和API迁移到Google Kubernetes Engine上,但无需更改客户目前使用的公共URL地址,而是逐步将流量从AWS转移至GKE,每次转移的比例为几百分比。 完成整个迁移过程后,你将能够在本地运行该应用,在AWS上搭建基础设施,通过八个Terraform步骤在GKE上构建环境,测试新环境是否正常工作,然后将生产环境的流量全部转移到GKE上,完成切换流程,并拆除不再需要的资源。 下图展示了迁移过程
这个指导性实验将带你了解如何迁移一个小型支付应用。该应用包含使用React开发的用户界面以及运行在AWS EC2实例上的Express API,而数据库则仍保存在AWS上。
你需要将用户界面和API迁移到Google Kubernetes Engine上,但无需更改客户目前使用的公共URL地址,而是逐步将流量从AWS转移至GKE,每次转移的比例为几百分比。
完成整个迁移过程后,你将能够在本地运行该应用,在AWS上搭建基础设施,通过八个Terraform步骤在GKE上构建环境,测试新环境是否正常工作,然后将生产环境的流量全部转移到GKE上,完成切换流程,并拆除不再需要的资源。
下图展示了迁移过程中的中间状态:此时两种云服务都在正常运行,客户仍然使用同一个URL地址,数据库也依然保存在AWS上。
在那个请求路径周围,还有另外三个系统在各自执行相应的功能。
目录
在开始之前
需要安装的工具
请在相应章节中安装所需的工具。第一天并不需要安装所有工具。
| 章节 | 所需工具 |
|---|---|
| 1 | Docker Desktop(或在Linux系统中使用Docker Engine与Compose),Git以及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://加上Elastic IP地址即可。浏览器会提示证书为自签名证书,但点击确认后,您仍然可以使用与笔记本电脑上相同的ClearPay登录方式。在GCP系统中,您需要在/etc/hosts文件中添加一条记录,使https://api.clearpay.test指向负载均衡器;除非您选择了使用Cloud DNS。
成本估算
在AWS系统中,每日使用费用约为1.25美元。在GCP系统中,一旦环境搭建完成,每日费用约为1.60至3美元。如果整个流程只在周末进行,所需费用也会仅为几美元而已。
这两种开发环境都不应连续运行超过一个月。如果您停止使用它们超过几天,就需要分别执行以下命令来销毁这些环境:
./scripts/teardown-gcp.sh
cd infra/aws/terraform && terraform destroy
详细费用信息记录在两个文件中:infra/aws/COST.md以及infra/gcp/COST.md。本书仅推荐您参考这两个文件,其他文档无需查阅。
本书的使用方法
请克隆该仓库,然后从其根目录开始执行所有命令,除非有特别说明需要切换到其他目录:
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章 | 你将通过八个步骤逐步使用Terraform来构建GCP环境。 |
| 4>将ClearPay部署到GKE测试环境中 | 12–15章 | 通过自动化流程将ClearPay部署到GKE测试环境中,并进行相应的测试。 |
| 5>使GKE环境准备好投入生产使用 | 16–18章 | 你需要检查安全设置,配置监控仪表盘,并故意引发一些问题以验证系统的稳定性。 |
| 6>将客户数据迁移至GKE环境中 | 19–23章 | 你需要完成生产环境的部署工作,并逐步将所有流量切换到GKE环境上。 |
| 7>完成整个迁移过程 | 24–28章 | 你需要验证迁移结果的准确性,关闭AWS环境,拆除所有的临时配置,最后整理相关文档以记录整个迁移过程。 |
如果出现问题怎么办
如果某个步骤出现了问题,请留在当前章节中,按照其中的“如果操作失败”部分提供的指导进行排查。千万不要跳过当前步骤继续前进,因为遗留下来的问题会变得越来越难以解决,因为下一章的内容会引入更多的复杂因素。
学习的目标并不是死记硬背Kubernetes或Terraform的相关知识,而是要掌握如何实际完成一次迁移任务:了解各个组件的运行情况,根据观察到的结果做出决策,验证最终的结果,以及知道在遇到问题时该采取什么措施。
整个过程需要多长时间
<这可能需要一天以上的时间来完成。如果你需要离开一段时间(超过一两天),请删除AWS和GCP上的相关资源,这样在它们处于闲置状态时就不会产生费用了。相关的操作命令可以在上方的“费用说明”部分找到。>团队成员
在第一周,你将扮演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/文件夹里。目前这些代码还没有被运行起来。
为什么这很重要
后续章节会需要参考你在笔记本电脑上成功运行ClearPay时的操作过程,以及成功发送20美元款项的实例。当AWS或GKE出现故障时,你可以将这些实际情况与之前的记录进行对比。
你即将运行的内容
ClearPay由三个部分组成,每个部分都有特定的功能。
用户界面是客户能够看到的部分,包括登录页面、余额显示以及“发送款项”按钮等。UI代表“user interface”。
应用程序编程接口则是负责执行具体操作的部分。它没有屏幕界面,用户界面会向它发送指令,比如“向Bob发送20美元”,API接收到指令后会执行相应的操作,并返回结果,例如“操作已完成”。API代表“application programming interface”。
数据库用于存储所有数据,包括用户的账户信息、余额以及交易记录。ClearPay使用的是PostgreSQL作为数据库程序。
服务器是一台全天候运行的计算机,它会等待来自用户端的请求。API程序就是运行在服务器上的。
咖啡馆的运作方式也与此类似:菜单就是用户界面,服务员则相当于应用程序编程接口,厨房及其记录则构成了数据库。顾客永远不会进入厨房,他们只需要告诉服务员自己想要什么就行了。
在本章中,你可以在笔记本电脑上同时运行这三部分内容。后续章节会将用户界面和API迁移到Google Cloud平台上,而数据库则继续保留在AWS上。
操作步骤
首先启动Docker Desktop。在本章中不需要安装Node.js或PostgreSQL,因为它们会运行在容器内部。
在包含docker-compose.yml文件的文件夹中,执行以下命令:
docker compose up --build
第一次构建过程需要几分钟时间。此时终端会持续输出日志信息,直到你按下Ctrl+C键才会停止输出。请让这个进程继续运行。
等待直到出现以下三行输出。左侧显示的名称可能会以你的文件夹名开头:
Container -postgres-1 Healthy
api-1 | {"message":"apistarted", ...}
ui-1 | ➜ Local: http://localhost:5173/
Healthy表示Postgres数据库已经启动;api_started表示API服务已启动;而localhost:5173则表示用户界面已经可用。
在同一文件夹中打开另一个终端窗口,然后创建两个演示账户:
docker compose exec api node db/seed.js
这条命令会直接运行种子脚本。故意没有在API容器中安装npm,因为第14章中的安全扫描会检测到这一设置。
预期的输出结果如下:
seeded: alice@clearpay.dev (password: password123)
seeded: bob@clearpay.dev (password: password123)
如果看到skip (already exists): alice@clearpay.dev这样的提示,说明这些账户是在之前的运行过程中已经创建的,这属于正常情况。
目前系统运行的组件如下:
验证运行状态
在第二个终端窗口中,执行以下命令:
curl localhost:4000/health
预期的输出结果中,instance字段会显示一个容器ID。
输出示例:
{"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创建好了账户,因此无需再次注册她。
电子邮件:
alice@clearpay.dev密码:
password123
向bob@clearpay.dev转账20美元。这笔转账会以“已发送”为状态显示在“近期活动”栏目中,同时会附带一个简短的识别码,例如Sent · a1b2c3d4。
请关注这个控制面板上的两个区域。在本书的后续内容中,你还会经常用到这两个区域:
“近期活动”栏目下的每一条记录都会以
served by aws/1.0.0结尾。这一信息表示处理这笔转账的API所使用的平台及其版本。这些信息会与转账记录一起存储在数据库中。当
/health/ready命令执行成功时,顶部的黑色条会显示“所有系统均正常运行”。这意味着API进程已经启动,并且能够与Postgres数据库进行通信。不过这条信息与登录过程无关。如果输入的密码错误,表单上会显示红色错误提示,但黑色条仍然会显示“所有系统均正常运行”。
在阅读第5章之后,你可以在AWS地址上打开同一个控制面板。使用相同的登录信息,依然可以找到那两个需要关注的区域。
如果出现故障:
如果出现“无法连接到docker API”或“docker.sock错误”,说明Docker Desktop没有运行。请先启动它,等待其准备好后再重新执行
docker compose up --build命令。password123。如果这个问题仍然存在,请再次执行seed命令。如果系统中显示“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命令能够无错误地运行时,说明任务已经完成。当seed工具报告系统中存在两个账户(无论是新创建的还是被跳过的账户)时,也说明任务完成。
当你通过应用程序成功完成了20美元的转账操作,并且能在“近期活动”栏目中看到这笔转账记录时,也说明任务完成。
第2章. 阅读关键代码
星期一,上午11:30
20美元已经转到了账户里。你还在看着转账界面,这时一把椅子滚到了你的旁边。坐在隔壁办公桌上的高级工程师丹尼尔正喝着咖啡,他并不着急。Daniel看了看你的屏幕,说道:“你不需要记住每一行代码。只要知道哪个文件负责做什么功能就行了,这样在迁移过程中就能快速打开相应的文件。”
为什么这很重要
后续的每一章都会涉及到这四个文件中的某个。现在花五分钟记住这些信息,以后在紧张的工作环境中就能节省大量的搜索时间。
操作步骤
按照以下顺序打开这些文件:
api/src/index.js负责启动API并连接所有的路由路径,包括健康检查、/api/info接口、登录功能以及交易处理。api/src/servedBy.js这个文件只有17行代码。它会从环境变量中读取PLATFORM、APP_VERSION和ROLLBACK_ACTIVE这些值,然后 dashboard会使用这些信息来显示“由aws/1.0.0提供支持”这样的提示信息。api/src/routes/transactions.js用于处理资金转账操作。在文件的开头部分可以找到idempotency_key相关的检查逻辑,这个代码会在其他所有操作之前被执行。api/src/db.js负责将API与Postgres数据库连接起来。在这个文件中可以找到DB_POOL_SIZE || '10'这条配置信息。
Daniel在提到最后一个文件时停了下来,说道:
“数据库连接就相当于通往数据库的‘通道’。一个API进程最多可以同时建立10个这样的连接。请记住这个数字。”
你现在掌握了什么
index.js → 启动API并连接各个路由路径
servedBy.js → 显示是由哪种云服务提供支持(aws或gke)
routes/transactions.js → 处理资金转账操作,会先检查idempotency_key
db.js → 与Postgres数据库进行交互,每个进程最多可建立10个连接
servedBy.js负责在dashboard上显示“由aws/1.0.0提供支持”这样的提示信息。这表示是哪种云服务完成了请求处理,而不是哪位用户进行了登录操作。
目前只有一个API进程在运行,因此整个系统最多只能建立10个连接。而在GKE环境中,会同时运行多个API实例。Kubernetes将每个这样的实例称为一个“pod”,每个pod都会拥有自己的10个连接资源,而且所有pod都会共享同一个数据库。第22章会再次讨论这个相关内容。
什么时候算完成
- 你不需要查看文件内容,就能立刻判断出是哪个文件设置了连接限制,以及哪个文件用于指定所使用的平台版本。
第3章:证明重试机制能够避免重复收费
星期一,下午2:10
午饭后,你收到了一条来自一个你还未曾见过面的人的消息。Amara负责维护支付相关的API系统,Maya知道你的笔记本电脑上安装了ClearPay软件,因此她没有在消息中添加问候语。
Amara说:“用同一个密钥分别发送两次20美元的转账请求。Alice的账户余额应该减少20美元,而不是40美元。”
为什么这很重要
Alice向Bob转了20美元,但她点击“发送”按钮后屏幕就卡住了。由于不确定交易是否成功,她又重新点击了一次“发送”按钮。
如果没有相应的保护机制,API会将这两个请求视为独立的操作,并因此收取两次费用。结果Alice反而损失了40美元。
idempotency_key这一机制可以防止这种情况的发生。“幂等”意味着重复执行某项操作也不会产生不同的结果:两次执行该操作会得到与一次执行相同的结果。这个“标签”其实是应用程序为每笔交易指定的标识符,例如round-a。
第一个请求会使用标签
round-a进行发送。API会收取20美元的费用,并将这个标签保存下来。第二个请求也会使用相同的标签
round-a。API再次看到这个标签后,会意识到这是同一笔交易,因此不会再收费,而是会返回之前的处理结果。
相同的标签意味着是同一笔交易。
在系统迁移过程中,这一点尤为重要。有时,第一次支付请求会发送到AWS服务器,而重试请求则会发送到GKE服务器。虽然这是两台不同的服务器,但它们使用的是同一个数据库。GKE服务器会在该数据库中查找round-a这个标签,从而避免重复收费。正是这个共享的数据库确保了客户在两种云环境中都能得到一致的服务。
在本章中,请使用curl命令来进行操作,而不是点击“汇款”按钮。因为用户界面会在每次点击时生成一个新的标识符,这意味着每次点击都会被视为一笔新的转账请求。
如果想更深入地了解幂等性的概念,可以参考PayFlow:一个用于学习真实DevOps技术的金融科技系统这篇文章。
操作步骤
请继续在第二个终端中,进入包含docker-compose.yml文件的文件夹。每次只运行一条命令,并在执行下一条命令之前先查看之前的输出结果。
以Alice的身份登录系统。系统会将她的登录令牌存储在一个名为TOKEN的shell变量中。这个令牌证明了Alice已经成功登录,后续的命令都会通过$TOKEN变量来传递这个令牌。
TOKEN=$(curl -s -X POST localhost:4000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@clearpay.dev","password":"password123"}' \
| python3 -c "import sys, json; print(json.load(sys.stdin')[‘token’])")
如果操作成功,这条命令不会输出任何内容。你可以运行echo $TOKEN来确认令牌的值,应该会看到一串长字符。
接下来,查询Alice的账户信息,并记下balance_cents这个数值。
curl -s localhost:4000/api/accounts/me -H "Authorization: Bearer $TOKEN"
查询结果应该符合预期的格式。不过你的账户编号和id数值可能会有所不同:
{"account":{"id":"...","balance_cents":"94000","currency":"USD"},"served_by":{"platform":"aws","version":"1.0.0","instance":"...","commit":"local","rollback_active":false}}
“94000”表示940美元。如果你还没有进行过任何交易,那么查询结果中的balance_cents数值应该是“100000”,即1000美元。如果命令提示“missing_token”或“invalid_token”,请重新执行第一步操作。
现在,请选择一个你尚未使用过的标签:
LABEL=round-a
向Bob汇款20美元。2000表示20美元的金额,以分为单位。
curl -s -X POST localhost:4000/api/transactions \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"to_email\":\"bob@clearpay.dev\",\"amount_cents\":2000,\"idempotency_key\":\"$LABEL\"}"
在回复中查找"replayed"这一字段:
"replayed":false表示这次交易花费了20美元,可以继续进行下一步操作。
"replayed":true表示这个标签之前已经被使用过,可能是在之前的某次交易中。因此不会再次收取费用。请将LABEL设置为round-b,然后重新发送请求,直到看到"replayed":false为止。
请再次发送完全相同的请求,不要更改LABEL的值。
curl -s -X POST localhost:4000/api/transactions \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"to_email\":\"bob@clearpay.dev\",\"amount_cents\":2000,\"idempotency_key\":\"$LABEL\"}"
现在你应该会看到"replayed":true,而且这次交易的id应该与第一次发送时的ID相同。
再次查看你的账户信息:
curl -s localhost:4000/api/accounts/me -H "Authorization: Bearer $TOKEN"
检查结果:
balance_cents的值应该比你之前记录的数字少2000,而不是4000。如果你最初的账户余额是94000,那么现在应该看到92000。
如果操作失败:
如果执行echo $TOKEN后没有输出任何内容,说明令牌无效,请重新运行登录命令。
如果第二次发送请求时仍然显示"replayed":false,说明两次请求使用的LABEL不同。请先执行echo $LABEL来确认标签值是否正确,然后再重新发送请求。
如果两次发送请求都显示"replayed":true,说明账户余额不会发生变化。此时请选择一个新的LABEL,然后重复步骤3和4。
操作完成的条件:
当某次请求显示"replayed":false,而下一次请求显示"replayed":true时,说明操作成功。
当balance_cents的值减少了2000而不是4000时,说明操作成功。
第4章:使用Terraform搭建AWS服务器
周二上午9:15
Maya在去开会的路上经过了你的办公桌。昨天你告诉她,ClearPay是在你的笔记本电脑上运行的,而且重复支付的操作只会收取一次费用。
Maya说:“很好。不过现在本地服务器并不能作为基准环境。我们需要把ClearPay部署到AWS上,证明客户可以在那里完成交易。之后我们再讨论是否需要移除AWS这个平台。Daniel会和你一起处理这个问题。”
Daniel从旁边的办公桌走过来。
Daniel说:“在开始之前有两条规则需要遵守:千万不要跳过terraform plan这一步。”
为什么这一点很重要
这本书介绍的是许多小型企业常用的系统架构:一台没有安装Docker的Linux服务器。
当服务器启动时,Linux的服务管理器systemd会自动启动Node API;nginx负责处理443端口上的HTTPS请求;PostgreSQL则运行在Amazon RDS这个托管型数据库服务上。
这套系统中并没有使用AWS负载均衡器。虽然负载均衡器可以将流量分散到多台服务器上,但在这种架构中添加负载均衡器并不会带来任何实际好处,反而会增加额外的费用。相关详细信息可以在infra/aws/COST.md文件中找到。
文件 `api/Dockerfile` 确实存在,但只有你的笔记本电脑在使用它。AWS 服务器从未运行过 Docker。
Daniel 说:“EC2 服务器上并不安装有 Docker。systemd 会在系统启动时同时启动 Node.js 和 Nginx。你即将使用的就是这种服务器配置。”
上图说明了客户请求是如何到达 ClearPay 在 AWS 上的服务器的。Elastic IP 为 EC2 服务器提供了固定的公共地址。Nginx 负责处理端口 443 上的 HTTPS 流量,提供前端文件,并将 API 请求转发到由 systemd 管理的 Node.js 进程,该进程运行在端口 4000 上。API 进程会与 Amazon RDS 中的 PostgreSQL 16 数据库进行交互,而这个数据库目前并没有公共访问端点。
在本章中会提到两个 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 ""
aws sts get-caller-identity 命令会显示你的 AWS 账户信息。如果执行过程中出现错误,请使用具有创建 EC2、RDS、VPC 以及 CloudWatch 警报权限的 IAM 用户的访问密钥来运行 aws configure 命令。使用临时账户中的管理员用户也可以完成这个操作。
在仓库根目录下执行以下命令:
cd infra/aws/terraform
cp terraform.tfvars.example terraform.tfvars
打开文件 terraform.tfvars,并修改其中的两个值:
ssh_allowed_cidr:你的公共 IP 地址后加上 /32。你可以使用命令 curl ifconfig.me 来获取自己的 IP 地址。
ssh_public_key_path:你的密钥的 .pub 文件所在的路径。
接下来执行以下命令:
terraform init
terraform plan
terraform apply
在输入 yes 之前,请先仔细阅读生成的配置计划。首次运行 Terraform 时,系统只会创建 EC2 服务器、RDS 数据库以及安全组等资源。如果配置计划中提到了需要删除某些资源,请立即停止操作,不要继续执行。
在第五章之前,这个服务器环境实际上是空的,没有任何预先配置好的服务或数据。

该图展示了Terraform创建的AWS基础设施:一台配置了Elastic IP的EC2 Ubuntu服务器,以及用于控制SSH和HTTPS访问的安全组;还有一个只能通过API服务器访问的RDS PostgreSQL数据库,同时还配置了CloudWatch监控功能。所有这些资源都位于默认的VPC环境中。目前这台EC2服务器还为空置状态,第5章会向其中部署ClearPay系统。
关于费用的问题:即使服务器处于运行状态,Elastic IP的费用也仅为每天约0.12美元,因为AWS会对每一个公共IPv4地址收取费用(详见AWS公共IPv4地址收费政策)。
检查结果:
terraform output instance_public_ip
这条命令会输出一个公共IPv4地址,这就是访问该服务器所需的地址。由于不需要使用域名,因此也不需要配置Route 53服务(manage_dns = false)。
如果出现错误:
Error: InvalidKeyPair.Duplicate:说明在这个AWS区域内,已经存在名为clearpay-deployer的密钥对。请在AWS控制台中删除该密钥对,或修改infra/aws/terraform/ec2.tf文件中的相关配置。
Error: UnauthorizedOperation:说明您的AWS账户缺乏访问某些资源的权限。错误信息会明确指出具体是哪个操作需要授权。只需为该操作添加相应的权限即可,无需申请完整的管理员权限。
下一步行动:
基础设施已经搭建完成,但服务器目前还为空置状态,因此还需要继续进行后续操作。请进入第5章继续学习。
何时算完成:
当terraform apply命令执行完毕且没有出现任何错误时。
当terraform output instance_public_ip命令输出一个有效的IP地址时。
第5章:在AWS服务器上部署ClearPay系统
周二下午1:40
在午餐之前,terraform apply命令就已经执行完毕。此时AWS系统中确实显示有一台服务器和一个数据库,但它们目前还无法为客户提供服务。
Terraform只是创建了一台空置的服务器和一个空的数据库,这些资源暂时还不能用于实际服务。
你打开终端准备开始部署过程,这时Daniel走过来坐在你旁边。
Daniel说:“仅仅因为Terraform执行完毕,并不意味着服务器已经可以正常使用了。我们还需要在服务器上安装应用程序,并发送一次真实的请求来测试系统是否能够正常运行。”
为什么这很重要:
如果服务器还是空置状态,那么它就无法作为迁移的起点。第6章会测试真实的客户请求在AWS环境中是否能够正常处理,而在这个问题得到解决之前,我们无法进行后续的测试工作。
操作步骤:
首先从仓库的根目录开始操作。如果你当前还在infra/aws/terraform目录中,需要先执行cd ../../..命令来切换到正确的目录。
接下来在服务器上安装Node.js、nginx和certbot软件:
cd infra/aws/deploy
INSTANCE_IP=$(terraform -chdir=../terraform output -raw instance_public_ip)
echo "$INSTANCE_IP"
# 记下这个IP地址,因为新的终端环境中可能没有这个变量。
ssh -o StrictHostKeyChecking=accept-new \
-i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP 'bash -s' < bootstrap.sh
你应该会看到== ClearPay AWS bootstrap ==这一行,随后会是几分钟的apt-get命令执行输出。文件bootstrap.sh很短,建议仔细阅读它。
当输出内容以以下这几行结尾时,说明操作成功:
== Bootstrap完成 ==
Node: v20.20.2
自签名TLS证书位于/etc/letsencrypt/live/clearpay,可以使用curl -k命令进行访问。
你的Node版本可能与上述示例不同。但那些以debconf: unable to initialize frontend开头的行并不影响操作,它们出现的原因是SSH会话没有交互式界面。而那些包含+或*的行,则是OpenSSL在生成自签名证书时产生的。
如果完全没有输出信息,说明SSH连接失败了。可能是因为从第4章之后你的家庭IP地址发生了变化,导致安全组设置仍然允许旧的IP地址访问。你可以使用curl -s ifconfig.me; echo命令查看当前IP地址,然后将其与/32组合起来,保存到terraform.tfvars文件中的ssh_allowed_cidr字段里,接着运行terraform apply命令重新尝试连接。
接下来,需要编写API所需的.env文件并部署应用程序。数据库地址信息来自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
deploy.sh脚本会构建React用户界面并将其复制到/opt/clearpay-ui目录中,同时将API相关文件复制到/opt/clearpay-api目录并安装相关依赖包,生成.env>配置文件,然后安装systemd服务及nginx配置文件,并重新启动这两个服务。
接下来需要加载数据库结构并初始化Alice和Bob账户。这些操作需要在服务器端执行,而不是在笔记本电脑上。由于RDS目前还没有公开访问接口(在infra/aws/terraform/rds.tf文件中设置了publicly_accessible = false),因此笔记本电脑无法直接访问RDS数据库。
笔记本电脑实际上是通过EC2服务器来间接访问RDS数据库的:
从示意图中可以看出,笔记本电脑之所以不能直接访问RDS数据库,是因为虽然允许通过SSH连接到EC2服务器,但该服务器也被允许在AWS私有网络内连接RDS数据库,因此笔记本电脑无法绕过这一中间环节直接访问数据库。你必须使用EC2服务器作为访问RDS数据库的入口点。
以下每个命令都以 `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"
这次使用的初始化脚本与第 1 章中的相同,只不过它会将数据写入 RDS 中。
检查
ssh -i ~/.ssh/clearpay-deployer ubuntu@$INSTANCE_IP 'sudo systemctl is-active clearpay-api'
预期的输出结果是:`active`。
然后,在你的笔记本电脑上执行以下命令:
curl -k https://$INSTANCE_IP/health
检查是否出现 `“status”: “ok”` 和 `“platform”: “aws”` 这些信息。`-k` 标志告诉 curl 接受 `bootstrap.sh` 文件生成的自我签名证书。
在浏览器中打开 `https://<你的实例 IP>`,忽略“不安全”的警告提示。这个警告是由于使用了自我签名证书导致的。你应该能看到与在笔记本电脑上相同的 ClearPay 登录界面,使用 `password123` 以 Alice 的身份登录。此时,控制面板会显示她的余额信息,以及 “由 aws/1.0.0 提供服务” 这一提示。
Daniel 说:“健康检查只能告诉我们应用程序是否正在运行。在确定基准数据之前,我需要先测试一下通过这个 IP 地址能否完成转账操作。”
你可以在浏览器中向 Bob 转账 20 美元。
如果出现故障:
如果 `systemctl is-active` 的返回结果是 `failed`,那么请登录到 EC2 服务器,然后运行 `sudo journalctl -u clearpay-api -n 50` 命令。通常造成这种问题的原因是 `DATABASE_URL` 的值与 RDS 中设置的地址不一致,请将这两个值逐个字符进行对比。
如果出现 `seed_failed` 并且 `SELF_SIGNED_CERT_INCHAIN` 的值为 `true`,说明数据库本身没有问题,可能是 Node.js 中的 `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 已通过 Elastic IP 登录。”她不到一分钟就回复了。
Maya 说:“很好。现在用那个 IP 地址进行五项测试,请将终端输出结果发给我,不要截图。”
随后又发来一条消息:“然后再进行负载测试,并把 p95 值发给我。否则我无法将 GKE 的性能与 ‘正常运行’ 的标准进行比较。”
为什么这很重要
本章会为您提供两样非常重要的内容,在本书的后续部分中,您都会用到它们。
1. 通过“推广测试”这一关。 “推广测试”其实是一个简短的脚本,用于检测每个支付应用都必须满足的五项条件:应用程序正在运行中、能够连接数据库、用户可以正常登录、重复支付的操作只会执行一次、用户的余额会按照预期减少。如果这五项都符合要求,那么说明该应用程序是正常工作的。
在第 15 章中,您会在 GKE 上运行同样的测试脚本。如果 GKE 能够通过 AWS 所通过的那些测试,那就说明 GKE 也能完成相应的任务。
2. 一份评估报告。 负载测试会同时发送大量请求,模拟许多用户同时使用该应用程序的情况。然后系统会记录应用程序响应这些请求的速度以及有多少请求失败了。这样我们就能了解当前 AWS 系统的实际性能。之后,您会在 GKE 上再次进行同样的测试,并将结果与 AWS 的数据进行比较。
可以把这想象成在运动员更换鞋子之前测量他们的速度——如果没有第一次的测量数据,第二次的测量结果就毫无意义。本章的作用就是记录下这个“第一次测量”的数据。
本章中的脚本会使用您笔记本电脑上的 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 \
alice@clearpay.dev password123 bob@clearpay.dev
请使用 `https://` 协议,因为如果使用 `http://`,Nginx 会进行重定向,导致脚本无法正常执行。
该测试会依次检查以下五项内容:
应用程序进程是否正在运行(通过检查 `/health` 端点)。
应用程序能否成功连接数据库(通过检查 `/health/ready` 端点)。
Alice 是否能够正常登录。
重新使用的支付密钥是否会返回 “replayed=true” 的结果。
Alice 的余额是否确实减少了一次(减少 100 美分),而不是两次。
预期的测试结果如下:
PASS: health endpoint returns ok
PASS: readiness endpoint confirms DB connectivity
PASS: login succeeds and returns a token
PASS: replaying an idempotency key returns replayed=true
PASS: balance debited exactly once (100 cents), not twice
5 passed, 0 failed
GATE PASSED: safe to promote to production.
最后一行提到了生产环境。目前还没有需要推广的生产环境版本。后来在GKE上运行的脚本中,这一行的含义也是一样的。
现在来对AWS进行负载测试。该脚本会登录系统并访问/api/accounts/me以及/api/transactions这两个接口,而这些接口都会查询Postgres数据库。请不要对/health这个接口进行负载测试。因为API本身会立即回复/health的请求,而不会去询问数据库。但是,真实的客户请求才会触发数据库查询并需要等待响应,而这种等待时间通常占整个处理流程的大部分:
/health 笔记本电脑 → API → 回复 (快速,无需查询数据库)
/api/accounts/me 笔记本电脑 → API → 数据库 → API → 回复 (实际需要执行的操作)
对/health这个接口进行负载测试时,实际上只测量了其中“快速响应”的那部分过程。可以把这想象成问店员“你们开门了吗?”和“你们有42码的这种鞋子吗?”的区别——只有后一个问题才会让店员去仓库查找。
./scripts/load-test.sh https://$INSTANCE_IP \
alice@clearpay.dev password123 10 30
最后两个数字分别表示并发请求的数量(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%
你的评分结果可能会有所不同。因为你的IP地址、请求次数以及延迟时间都会受到你所处位置的影响。由于负载测试是在你的笔记本电脑上进行的,所以每个请求都需要从你的所在地传输到位于us-east-1区域的AWS服务器,然后再返回。
如果测试地点靠近该区域,那么测试结果中的数值会比位于其他大陆的用户的测试结果要低一些。不过这两种结果都没有错。关键是要将GKE的测试结果与你自己在相同地点测得的评分结果进行对比。
当看到失败次数=0、5xx错误率=0.0%以及交易成功率=100.0%时,说明测试通过了。
p95延迟时间表示有95%的请求在比这个数值更短的时间内完成。这一指标能够反映出那些响应速度较慢的客户所遇到的问题,而平均值往往无法体现这些细节。
4xx错误表示客户端发送了错误的请求信息,例如缺少必要的认证令牌。
5xx错误表示服务器出现故障,无法正常处理请求。
将这份完整的评分报告复制到笔记文件中,然后把其中的相关内容发给Maya。她很快就回复说:“不要为了让数据看起来更整齐而四舍五入这些数值。我会直接用GKE的测试结果与这个报告进行对比。”
现在就设定你的限制条件吧
在评分报告下方,请写下以下这两行内容,并填写相应的数字:
“中断”是指:5xx错误率连续超过2分钟,其数值高于1%;“故障”则指:p95错误率连续超过5分钟,其数值超过您所在AWS环境的p95阈值的3倍以上。
失败阈值是根据你自己的AWS p95值来设定的,因为每个用户的基准情况都不相同。以这个实验中测得的745毫秒为基准,那么失败阈值大约就是2,235毫秒。这些只是示例数值,你可以自行选择合适的阈值,但必须现在就确定下来,因为在GKE出现之后再设置阈值的话,就会被迫去调整那些数值以使其符合新的标准了。
用以下一行来开始记录成本信息:
成本记录,条目1:AWS费用约为每天1.25美元 (infra/aws/COST.md)
如果失败了:
请一直等待,直到所有五项检查都通过为止。如果在迁移过程中存在问题,那么当涉及到两种云服务时,这些问题会变得更加难以排查。
如果“准备就绪”这项检查失败,说明API无法连接到RDS。此时请返回到第5章中关于失败情况的处理方法。
如果出现node: command not found这样的错误,请在您的笔记本电脑上安装Node.js。
决策
所有检查都通过了。这台EC2服务器就成为了基准值。本书中提到的所有与GKE相关的数值都会与这个基准值进行对比。
一分钟后,Maya又添加了一条记录:“好了,这些数值现在就是我的标准了。”
何时算完成:
当系统显示“针对AWS IP地址,有5项检查通过,0项失败”时,说明迁移工作已经完成。
当负载测试结果显示“失败次数为0”,并且所有的评估数据都已经被记录下来时,也表示迁移工作结束。
当你把所有的中断时间和失败阈值都以具体的数值形式记录下来时,也就意味着迁移工作彻底完成了。
第2部分:如何规划迁移计划
请告诉Maya迁移工作已经完成,并将评估结果发送给她。
她回复说:“下一步的任务是部署GKE。我需要确保API能在GKE上正常运行,同时在整个迁移过程中,客户也需要继续支付相关费用。请把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
中间阶段是整个迁移过程的核心:两种云环境都会运行相同的应用程序,都会访问同一个数据库,流量会逐步地在这两个环境之间进行转移。
流量如何迁移
目前,客户需要输入Elastic IP地址才能访问相关服务,所有请求都会被发送到AWS。
首先在前面添加一个Google负载均衡器,客户只需将访问地址更改为该负载均衡器的地址即可。在整个迁移过程中,他们都需要继续使用这个新的地址。
负载均衡器会将部分请求发送到AWS,其余的请求则发送到GKE。初始状态下,100%的流量都会被发送到AWS。
之后可以通过`scripts/shift-traffic.sh`脚本逐步增加发送到GKE的流量比例,每完成一步后都需要检查相关数据是否正常。
如果GKE环境中出现任何问题,可以立即使用`shift-traffic.sh 100 0`命令将100%的流量重新发送回AWS,系统恢复过程只需几秒钟即可完成。

上图展示了客户如何通过一个公共地址访问ClearPay;在迁移过程中,UI和API同时运行在AWS和GKE上。这两种环境都连接到亚马逊AWS中的同一台PostgreSQL数据库。将数据库放在同一个地方,可以避免在迁移应用程序的同时也迁移支付数据。
为什么不用DNS来分配流量呢?DNS就是将域名转换为IP地址的机制,笔记本电脑和互联网服务提供商都会缓存这些查询结果。
在更改DNS记录后,有些客户仍会在几分钟或几小时内继续访问旧地址。如果需要花费一小时才能完成回滚操作,那么这种回滚措施就毫无意义了;而负载均衡器可以在几秒钟内完成流量分配的调整。
回滚要求
在迁移的每一个阶段,迅速且安全地恢复到100%依赖AWS的状态是必须的。这不能通过重新部署系统来实现,也不能通过需要一小时才能生效的DNS设置变更来达成。第20章会在首次进行迁移之前,专门测试这些要求是否得到满足。
这里的“零停机时间”意味着什么
这并不意味着永远不会有任何请求失败。没有任何真正的系统能够保证这一点。
它的意思是:没有任何客户会遇到持续的服务中断。只要失败的请求数量低于你在第6章中规定的限度,那么就是可以接受的。
为什么先迁移API
如果API和数据库同时进行迁移,而其中某个环节出现了问题,那么故障可能出在其中一个组件上,也可能出在它们之间的交互过程中。因此,只迁移API可以让问题变得更容易解决:这个API能够在GKE上正常运行,而不会影响到客户的体验吗?这是一个可以给出明确答案的问题。“能否安全地一次性完成所有迁移工作?”这个问题则要难回答得多,而且在实际操作中调试起来也会更加麻烦。
为什么数据库仍留在AWS上
第13章已经说明了跨云环境进行数据传输所涉及的技术细节。目前来说,在这个项目中,PostgreSQL数据库是不会被迁移的。
未来会有一个单独的项目来处理数据库的迁移工作,很可能会使用谷歌的数据库迁移服务、持续复制技术以及专门的切换流程。而当前这个项目的目标仅仅是让API运行在GKE上,而数据库仍然留在AWS上。这就是我们的计划,并不是一个未完成的任务。
为什么必须同时运行两个云平台
回滚机制的实施就依赖于这一点。你可以先向GKE发送少量流量,同时确保AWS处于准备状态;如果GKE出现故障,AWS可以立即恢复所有服务。不过,这种方案只有在AWS仍然存在的情况下才能发挥作用。
在开始使用GCP之前,先制定迁移预算
现在就去做这件事吧,在阅读第9章之前。在你的笔记中写下四个数字,它们将决定这次迁移会让你花费多少钱。
规划你的开支,就像规划旅行费用一样:
目前AWS的使用成本是多少?大约每天1.25美元,具体数据详见infra/aws/COST.md文件。
未来使用GCP每天会花费多少费用?这个数字你还不知道,现在先写上“待定”。第9章会为你计算出具体数值。根据infra/gcp/COST.md文件,GCP的每日费用大约在1.60美元到5美元之间。
在两个云平台同时运行的期间,这次迁移总共需要花费多少费用?
在迁移过程中,你需要同时支付AWS和GCP的费用,也就是每天1.25美元加上GCP的每日使用成本。
你的预算是多少?
这是你在整个迁移过程中最多可以花费的金额。在开始任何操作之前,先确定这个预算吧。
本实验的相关数据:
当前的AWS使用成本:每天1.25美元
预计的GCP使用成本:大约每天3美元(具体数值会在第9章中进行核实)
同时运行两者的总成本:1.25美元 + 3美元 = 每天4.25美元
迁移预算:5天 × 4.25美元 = 大约21美元,四舍五入后为25美元。
在实验进行过程中,请随时记录累计成本。第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章得到验证。
实际上需要迁移哪些内容?
只有用户界面和API需要从EC2服务器迁移到GKE上。Postgres数据库则继续留在AWS上。
浏览器和脚本访问相同的URL后,仍然会得到相同的JSON响应数据。重试机制也必须确保不会导致费用被重复收取,这些规则是不会改变的。
什么时候可以完成任务?
- 需要将所有五个相关答案都记录下来,且每个答案都要对应到具体的文件。
第9章:了解GCP的设计原理
星期四下午2:30
Terraform脚本针对Google Cloud平台已经编写完成。在你加入团队之前,已经有其他人做出了两个重要的设计决策。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模式,原因并非出于成本考虑。实际上,这种配置方式能够让用户清楚地看到节点池的构成、虚拟机的规格以及自动扩展机制的运作原理,而这些正是本书所强调的内容。如果使用Autopilot模式,这些信息就会被隐藏起来。
你自己计算一下这两项费用吧。标准机器的费用是固定的。e2-medium型机器如果全天运行,根据infra/gcp/COST.md中的数据,其费用约为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美元/天
所以,三个API Pod每天的总费用约为0.89美元,这还不包括UI Pod的费用。实际上,Autopilot带来的成本可能还低于使用一台标准机器的成本。不过这个实验室仍然选择使用标准机器,这样你们就可以看到节点池的配置情况了。
把这两个数字以及相关的计算依据告诉Maya吧。
需要多少个API进程?
AWS会运行一个API进程。而在GKE上,HorizontalPodAutoscaler会根据CPU使用情况来自动增加或减少API Pod的数量。每个Pod都代表一个API进程,而且每个Pod都会拥有自己的10个数据库连接资源。
这个实验室使用的集群是由三台小型e2-medium型机器组成的。在k8s/overlays/production/patch-hpa.yaml文件中配置的生产环境下的HPA机制,会根据需求运行1到3个API Pod,因为三台这种规模的机器无法与GKE自带的系统Pod们共同容纳更多的Pod。
给Daniel的回答是:“在GKE上会运行1到3个API Pod,在AWS上也会有一个API Pod在运行。每个Pod都可以建立10个数据库连接。”
如果一家公司的机器规模更大,那么它们可能会设置更高的上限,比如3到8个Pod。第22章会解释为什么在增加Pod数量之前,需要先检查这些上限是否与数据库的容量限制相符。
精心设计的网络架构
在AWS上,这个实验室使用了AWS默认提供的VPC。这个VPC并不是由任何人专门设计的。
而在GCP上,infra/gcp/terraform/02-network文件会创建一个自定义的VPC,其中包含一个命名的子网以及Cloud NAT功能。集群中的机器们只能获得私有地址,但它们可以通过Cloud NAT访问互联网,进而连接到AWS上的数据库。所有这些机器都共享同一个公共IP地址。
区域设置
01-foundation/variables.tf文件将区域设置为us-east4,这个区域位于谷歌云平台中,距离AWS的us-east-1区域最近。从GKE发起的任何数据库查询都会在这两个云平台之间进行传输,因此选择距离较近的区域可以显著减少数据传输的时间。如果你的AWS环境并不位于us-east-1区域,请在开始构建任何系统之前先更改这个设置。
整体架构图
左侧显示的是当前已经存在的架构,右侧则展示了第三部分内容中将会添加的组件。

上图展示了从AWS迁移到GCP的过程。客户使用一个由GCP负载均衡器管理的公共地址,该地址会将流量分配到现有的AWS EC2服务器和GKE之间。GKE在私有节点上运行用户界面和API,而Cloud NAT则负责提供出站互联网访问功能。这两种环境都会连接到AWS上的同一个PostgreSQL数据库。随着迁移的逐步进行,流量会逐渐从AWS转移到GKE上,而数据库本身则保持不变。
在这个迁移过程中,您会遇到三个公共IP地址,它们的顺序如下:
弹性IP地址。这就是当前的EC2服务器所使用的IP地址,客户目前仍在使用它。
GKE网关IP地址。负载均衡器会将分配给GKE的流量发送到这个地址上,但客户通常不需要手动输入这个地址。
来自08-edge区域的负载均衡器IP地址。客户只需将流量切换到这个地址上,后续的流量分配工作就会在背后自动完成。
如果您需要暂停使用AWS……
在第3部分中,并不需要让AWS一直处于运行状态。但在第11章(将数据库URL复制到Secret Manager中)和第13章(将数据库开放给GKE使用)时,还是需要使用AWS的。
请不要关闭EC2服务器,同时要让RDS继续运行。RDS通常会占据AWS日常账单的大部分费用;即使停止运行,RDS也会在7天后自动重新启动,而弹性IP地址的费用则不会因此发生变化。
如果您在一两天内就会继续使用AWS,请让它保持运行状态;但如果您要停用它几天,那么请执行以下命令来彻底删除相关资源:
cd infra/aws/terraform && terraform destroy
如果以后需要重新开始使用AWS,只需在该文件夹中运行terraform apply命令,然后按照第5章的步骤进行操作即可。您会得到一个新的弹性IP地址和新的数据库URL;而第6章中的各项指标仍然可以作为参考基准。
完成迁移的条件:
您已经向Maya说明了标准配置模式与自动配置模式的每日数据统计情况,以及为什么实验室选择使用标准配置模式。
您也已经向Daniel解释了可以同时运行多少个API进程,以及每个进程能够建立多少个连接。
第3部分:如何使用Terraform构建Google Cloud
目前,AWS应用程序和相应的评估指标体系已经搭建完成,但Google Cloud的相关环境尚未配置完毕——此时还没有网络、集群或注册表等组件。
后续,用户界面和API将会在那个GKE集群上运行;而Postgres数据库则仍然保留在AWS上。因此,正确的操作顺序是:首先在GCP中创建一个空白的基础设施环境,然后把应用程序部署到那里,最后让应用程序学会如何通过互联网连接到RDS数据库。
目前这个阶段只是完成了基础设施环境的搭建,客户的数据和应用程序还没有被迁移过去。
第10章:配置gcloud>并选择您的项目
周五上午8:45
在开始任何操作之前,您的笔记本电脑需要知道应该使用哪个项目来进行配置,同时也需要确认您的身份信息。
确保将 gcloud、kubectl 以及 Terraform 配置为指向正确的 Google 项目,并且 billing 功能已启用。
操作步骤
gcloud 是 Google Cloud 的命令行工具。请先安装它,然后关闭终端并打开一个新的终端。
在 Mac 上:
brew install --cask google-cloud-sdk
在 Windows 上,请从 下载 Google Cloud CLI 安装程序,勾选将 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
该插件必须被添加到你的 PATH 中。如果之后执行 kubectl get nodes 时出现 “gke-gcloud-auth-plugin not found” 的错误,请运行以下命令并重新打开终端:
echo 'export PATH="$(gcloud info --format="value(installation.sdk_root)")/bin:$PATH"' >>> ~/.zshrc
这条命令是针对 Mac 的默认 shell zsh 编写的。在使用 Bash 的 Linux 系统上,应将文件名改为 ~/.bashrc。在 Windows 上,安装程序通常会自动处理 PATH 的配置问题。安装完成后,请重新打开 Git Bash。
现在请登录并选择相应的项目:
gcloud auth login --update-adc
gcloud projects list
gcloud config set project YOURPROJECT_ID
gcloud billing projects describe YOUR_PROJECT_ID
terraform version
kubectl version --client
--update-adc 这个参数非常重要。仅使用 gcloud auth login 时,gcloud 只能使用你的账户信息;而 Terraform 是一个独立的工具,它会查找名为 “Application Default Credentials” 的文件。通过设置这个参数,Terraform 可以使用与浏览器登录相同的凭据进行操作。在 Google 页面上,请点击 “Allow” 按钮,不要提前关闭该标签页。
应该使用哪个项目 ID
执行 gcloud projects list 命令后,会看到两列内容,它们看起来很相似:
NAME 是项目的名称,很多项目都会使用默认的名称 “My First Project”。
PROJECT_ID 是每个项目的唯一标识符,例如 “my-first-project-123456”。
请复制 PROJECT_ID,而不是项目名称。在所有需要输入 YOUR_PROJECT_ID 的地方,都使用这个 ID。
如果你有多个项目,请选择那些已经启用了 billing 功能的项目。对于每个项目,分别执行 gcloud billing projects describe 命令,直到输出结果中包含 “billingEnabled: true” 这一行。
你也可以在 Google Cloud Console 中查找项目 ID:点击页面顶部的项目名称,然后查看 ID 列。
检查完成
gcloud config get-value project
这条命令会显示你的项目ID。如果输出的结果是[None],请再次运行gcloud config set project命令。
如果操作失败:
- 如果Terraform提示“无法找到默认凭据”,说明你在登录时没有使用相应的标志。请运行以下命令:
gcloud auth application-default login
- 如果该命令显示“需要cloud-platform权限,但尚未获得许可”,则可能是你在点击“允许”之前浏览器已经关闭了。请再次运行
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后,如果能看到版本信息,说明配置也已成功。
消息提示:「gcloud已登录成功。项目配置已完成,计费功能也已开启。现在可以开始使用GCP了。」
Maya提醒:“请一次只构建一个阶段。每个阶段完成后,请及时通知我。如果你一次性运行全部八个阶段,而第三个阶段出现了问题,你就无法判断是哪个环节出了故障。”
第11章:通过八个Terraform阶段构建GCP
时间安排:周五上午10:30,持续到第二周的周一
GCP的构建过程分为八个Terraform阶段,每个阶段都会创建目标环境中的某一部分。
请按顺序依次执行这些阶段。在每个阶段完成后,请先验证结果,确认一切正常后再进入下一阶段。
需要构建的内容包括:API、网络架构、集群系统、镜像注册服务、身份认证机制、密钥管理功能、错误报警系统,以及公共负载均衡器等。
为什么要分八个阶段进行构建?
Terraform的相关配置文件被保存在八个文件夹中,分别是01-foundation到08-edge。每个文件夹都包含自己的状态文件,而且只会读取前几个阶段所需的配置信息。如果03-gke阶段出现故障,问题出在集群系统上,而不会出现在某个长达1,500行的配置文件中。
这并不是说需要运行Terraform八次,而是指需要分别完成八个不同的构建任务,并且在开始下一个阶段之前,要确保前一个阶段的配置已经正确无误。
对于个人用户来说,这些状态文件可以保存在本地笔记本电脑上;但如果是一个团队进行协作,建议将每个阶段的配置状态转移到Google Cloud Storage桶中,然后再由其他成员执行apply命令。

上图展示了Terraform是如何分八个阶段来构建GCP基础设施的。它首先会启用所需的API,然后使用Cloud NAT创建网络及私有GKE集群,接着设置镜像注册表、身份认证机制、密钥管理配置、错误监控系统以及公共负载均衡器。Cloud NAT为私有节点提供了出站连接能力,在迁移过程中的后续步骤中,这些私有节点还能访问AWS RDS。
各阶段的执行流程
每个阶段都会在其对应的文件夹中使用相同的三个命令:
terraform init
terraform plan
terraform apply
在执行操作之前,请先查看相应的计划内容。下表列出了每个阶段预期会执行的操作数量,例如“计划:添加10个资源,修改0个资源,删除0个资源”。如果实际数量与预期不同,或者计划中包含删除资源的操作,在输入“yes”之前请务必仔细阅读相关内容。
当执行`terraform apply`时,系统会询问“是否要执行这些操作?”请输入“yes”并按回车键。
下面的文件夹路径都是从仓库根目录开始的。如果你已经处于`infra/gcp/terraform/0X-...`这个目录中,可以使用`cd ../0Y-...`来进入下一个阶段。
对于那些需要在控制台进行的检查:请在页面顶部选择你的项目,并且当页面出现地区选择菜单时,将地区设置为`us-east4`。如果菜单列表为空,说明当前地区仍设置为`us-central1`。
阶段1:启用Google APIs(01-foundation)
在新创建的项目中,Google Cloud的各项服务默认都是处于关闭状态的。这个阶段会启用其中10项服务,包括GKE、Artifact Registry、Secret Manager、IAM以及日志记录功能等。如果你跳过这个阶段,后续的`03-gke`阶段就会因为权限问题而失败,而且这些权限错误信息并不会提供任何有用的线索。
cd infra/gcp/terraform/01-foundation
cp terraform.tfvars.example terraform.tfvars
如果`terraform.tfvars`文件中已经存在你的项目ID,就可以直接跳过这个复制步骤。因为再次复制会覆盖你之前所做的修改。
打开`terraform.tfvars`文件,将其中的`clearpay-migration-demo`替换为你的项目ID,然后运行以下命令:
terraform init
terraform plan
terraform apply
预期的执行结果应该是:会创建10个与Google项目服务相关的资源,但不会删除任何集群。
计划:添加10个资源,修改0个资源,删除0个资源。
输出参数的变化:
+ project_id = "YOUR_PROJECT_ID"
+ region = "us-east4"
如果执行结果中显示了`clearpay-migration-demo`这个选项,请先检查`terraform.tfvars`文件的内容是否正确。在应用配置的过程中,Secret Manager和服务网络相关设置可能会暂时显示为“Still creating”,这种情况属于正常现象,通常会在几分钟后自动完成。
检查完成后,可以运行以下命令来验证各项服务是否已经成功启用:
gcloud services list --enabled --filter='config.name:container.googleapis.com'
名称 标题
container.googleapis.com Kubernetes Engine API
如果此时没有任何输出信息,说明 `gcloud` 所查询的项目对应的 API 尚未启用。请不要进入第二阶段,请先检查以下三个原因:
gcloud 指向的是另一个项目。请运行命令 `gcloud config get-value project`,查看显示出的项目 ID 是否与你在 `terraform.tfvars` 中设置的项目 ID 一致。如果不一致,请运行 `gcloud config set project YOURPROJECT_ID`,然后再检查一次。
`terraform apply` 操作尚未完成。请在 `01-foundation` 目录下再次运行 `terraform plan` 命令。如果操作已完成,屏幕上会显示 “No changes”;但如果系统仍提示需要添加资源,请运行 `terraform apply` 并输入 “yes” 以继续操作。
Google 尚未更新相关配置。某些新启用的 API 可能需要一段时间才能在列表中显示出来。请等待一分钟后再重新检查。
消息来自 Maya:“第一阶段已经完成,Kubernetes Engine 的 API 也已显示为已启用状态。”
Maya:“很好,接下来进行网络配置设置。没有其他需要检查的内容了。”
第二阶段:构建网络架构(02-network)
VPC 是你 Google 项目中的私有网络,可以将其想象成集群所在的区域,而子网则相当于该区域内的具体街道。这一阶段会创建以下资源:
clearpay-vpc:私有网络本身。
clearpay-gke-subnet:集群中的机器使用的地址范围;另外还预留了两个子网范围,分别用于第三阶段的 `pods` 和 `services` 服务。
clearpay-router 和 clearpay-nat:云 NAT 设置相关资源。
clearpay-nat-ip:所有集群机器在对外发送请求时都会使用的公共 IP 地址。
两条防火墙规则,用于确保内部机器之间能够相互通信,同时让 Google 的健康检查机制能够访问这些机器。
此文件夹中不存在 `terraform.tfvars` 文件,系统会从第一阶段的配置文件中读取项目 ID。
cd infra/gcp/terraform/02-network
接下来请运行以下命令:
terraform init
terraform plan
terraform apply
预期的执行结果如下:
计划:需添加 7 个资源,无需修改任何资源,也无需删除任何资源。
`terraform apply` 操作完成后的预期输出结果为:
检查步骤:
gcloud compute networks describe clearpay-vpc
terraform output nat_ip_address
需要查找以下内容:`name: clearpay-vpc`,以`clearpay-gke-subnet`结尾的`subnetworks`这一行,以及`autoCreateSubnetworks: false`这一项。最后这一行说明该网络是人工设计的,而非自动生成的。
把NAT IP地址记下来。在第13章中,会使用这个地址来连接AWS数据库。
在控制台中:VPC网络选项下可以看到`clearpay-vpc`;Cloud NAT选项下,`clearpay-router`对应的NAT IP地址是`clearpay-nat`;IP地址选项下也能找到`clearpay-nat-ip`。
如果系统无法读取`../01-foundation/terraform.tfstate`文件,说明第1阶段的操作还没有完成。
给Maya发消息:「`clearpay-vpc`和Cloud NAT已经创建好了。NAT IP地址记录在你的笔记里了。」
Daniel会查看你的笔记,确认NAT IP地址是否已被记录下来。
Daniel解释道:「我们所有的Pod与外部世界的通信都是通过这个IP地址进行的,包括与AWS数据库的连接。之后我们会设置AWS数据库只接受来自这个地址的连接。」
这些操作会在第13章中进行。在那一章之前,请务必把NAT IP地址记清楚。
第3阶段:创建GKE集群(`03-gke`)
在这个阶段,我们会在`us-east4-a`区域创建一个名为`clearpay`的标准集群,并设置一个名为`primary`的节点池,该节点池由1到3台`e2-medium`型机器组成。这些机器只拥有私有IP地址,它们会通过Cloud NAT与外部进行通信。而用于管理集群的控制平面服务器则具有公共端点,因此你可以在笔记本电脑上使用`kubectl`来远程操作它。
cd infra/gcp/terraform/03-gke
接下来运行以下命令:
terraform init
terraform plan
terraform apply
预期的结果应该是创建一个集群以及名为`primary`的节点池。
计划内容:需要添加2个资源,无需修改任何资源,也不需要删除任何资源。
在执行`terraform apply`命令后,系统会显示“Still creating”状态,这个过程通常会持续7到10分钟。请不要提前按下Ctrl+C键终止操作。
cluster_endpoint会输出<sensitive>这一信息,但你并不需要它。
将kubectl指向新的集群:
$(terraform output -raw get_credentials_command)
如果操作成功,系统会输出“kubeconfig entry generated for clearpay”这一信息。
检查一下:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-clearpay-primary-........ Ready 2m v1.xx
Ready表示该机器已经启动并加入了集群,不过ClearPay应用程序尚未在该机器上运行。
如果配置文件中提到了container.googleapis.com,请返回到第1阶段;如果提到了子网、pods或services的相关信息,请返回到第2阶段。切勿为了掩盖这些错误而修改03-gke文件。
如果kubectl提示“gke-gcloud-auth-plugin not found”,说明集群本身没有问题,无需销毁它。请安装该插件,并调整终端的PATH环境变量:
gcloud components install gke-gcloud-auth-plugin
export PATH="$(gcloud info --format='value(installation.sdk_root)')/bin:$PATH"
kubectl get nodes
在控制台中,Kubernetes集群页面会显示clearpay集群正在运行中;而虚拟机实例页面则会显示名为gke-clearpay-primary-...的机器,该机器拥有内部IP地址,但没有外部IP地址。
Maya发来消息:“集群已经启动,其中一个节点处于Ready状态,不过我们的应用程序还没有在该节点上运行。”
整个配置应用过程耗时9分钟,你全程都见证了这一过程。
第4阶段:创建镜像仓库(04-registry)
容器镜像是应用程序的打包版本。GKE会从镜像仓库中下载这些镜像。在这个阶段,我们会在Artifact Registry中创建一个私有的Docker镜像仓库,其名称为clearpay,位置位于us-east4区域。在第14章中,当管道系统向该仓库推送数据时,这个仓库才会被使用到。
cd infra/gcp/terraform/04-registry
执行以下命令:
terraform init
terraform plan
terraform apply
需要创建的两个资源是一个镜像仓库,以及一条允许集群中的机器从该仓库中下载镜像的规则。
计划操作:新增2个资源,无需修改任何资源,也不会删除任何资源。
检查步骤:
gcloud artifacts repositories describe clearpay --location=us-east4
需要查找以下内容:`format: DOCKER`、以`/repositories/clearpay`结尾的`name`,以及`Repository Size: 0.000MB`。目前,一个空的注册表是正常的。你还会看到`SCANNING_DISABLED`这一字段。在第14章中,管道会使用Trivy来扫描图像。
消息提示:“图像注册表存在,但它是空的。”
阶段5:创建无需密钥文件的身份账户(05-identity)
后续会有两项操作需要使用Google的权限:在GKE上运行的ClearPay pods,以及GitHub Actions在推送图像和进行部署时的操作。
传统的方法是下载服务账户密钥文件。这种密钥文件实际上就是存储在磁盘上的密码,如果泄露的话,任何人都可以使用它。但在本实验中,我们不会采用这种方法。
相反,在这个阶段会创建三个具有相应权限的Google服务账户,然后允许特定的调用者在短时间内借用这些账户:
clearpay-api-staging,由staging命名空间中的Pods借用
clearpay-api-production,由production命名空间中的Pods借用
clearpay-ci,通过名为clearpay-github-pool的工作负载身份池,由GitHub Actions借用
工作负载身份是Google提供的一项功能,它允许将Kubernetes服务账户或GitHub仓库与Google服务账户关联起来,而无需使用密钥文件。
可以把每个服务账户想象成一种“徽章”。现在还没有人会佩戴这种徽章,但后来Google会允许特定的Pod或仓库在短时间内使用这种“徽章”,而且根本不需要生成任何密钥文件。
命名空间是一个集群内部的不同区域。staging和production命名空间共享同一台服务器,但它们的身份信息是相互独立的,因此staging命名空间中的Pod无法访问production命名空间中的敏感信息。在实际的企业环境中,通常会为不同的项目分别部署不同的集群。不过在本实验中,为了简化操作流程,我们使用同一个集群。
cd infra/gcp/terraform/05-identity
cp terraform.tfvars.example terraform.tfvars
该文件中唯一需要设置的值是`github_repo`。如果你已经拥有这个GitHub仓库的副本,那么现在就可以将其owner/name内容填写到这里,注意不要在后面加上`.git`后缀;如果没有这样的副本,那就保留`YOUR_GITHUB_USER/YOUR_REPO`这一格式。第14章会告诉你何时需要设置这个值,并且会说明如何重新执行这个阶段的操作。
运行命令:
terraform init
terraform plan
terraform apply
预期计划:
计划:增加10个资源,不修改任何资源,也不删除任何资源。
这10个资源指的是3个服务账户、GitHub身份池及其提供者,以及它们之间的权限关联关系。
检查结果:
gcloud iam service-accounts list --filter='email~clearpay-'
需要查找以下三个电子邮件地址:clearpay-api-staging、clearpay-api-production和clearpay-ci。
在控制台中,通过服务账户选项可以查看这三个账户;而通过工作负载身份联盟选项,则可以看到名为clearpay-github-pool的身份池及其提供者clearpay-github-provider。
如果申请失败,并且出现“错误409:请求的实体已存在”这样的提示,说明你之前已经执行过这个步骤。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-poolproviders/clearpay-github-provider进行申请。
Maya的消息:“已经存在3个服务账户,但目前还没有任何系统在使用它们。”
Maya又问:“没有密钥文件吗?”
“没有。”
阶段6:创建密钥文件(06-secrets)
在GKE环境中,ClearPay需要两份密钥文件:数据库地址和JWT签名密钥。这两份文件已经存在于AWS上。在这个阶段中,我们需要在Secret Manager中创建四份新的密钥文件,每个命名空间对应一对密钥文件:
clearpay-staging-jwt-secret
clearpay-staging-database-url
clearpay-production-jwt-secret
clearpay-production-database-url
staging服务账户只能访问与自身命名空间对应的密钥文件对;production服务账户也只能访问与自身命名空间对应的密钥文件对。
可以把每一份密钥文件想象成一个上锁的抽屉,第5阶段中获得的每个“钥匙”只能打开属于自己的那个抽屉。
在这个实验中,两组配置中都使用了相同的值。这两种环境都使用同一个RDS数据库,因此当下一个请求被发送到GKE时,在AWS上的登录操作仍然应该能够正常进行。在实际的公司环境中,每个环境通常都会使用不同的配置参数。
Terraform会使用占位符REPLACE_ME来创建这些配置文件。虽然Terraform可以生成这些文件,但它无法为这些文件指定具体的值。因此,正确的操作顺序是:先执行Terraform命令进行配置应用,然后再手动将实际的参数值复制到这些文件中。
cd infra/gcp/terraform/06-secrets
接下来运行以下命令:
terraform init
terraform plan
terraform apply
Terraform会创建十四个资源文件:
四个配置文件
四个初始版本的配置项
为API服务账户设置的四条访问规则
两条允许管道中的clearpay-ci账户读取测试环境的配置规则
计划创建14个资源,0个资源需要修改,0个资源需要删除。
cd ../../../..
此时执行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=-
如果配置成功,命令会输出“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=-
之后再次在这个文件夹中执行terraform apply命令时,Terraform不会再将REPLACE_ME替换回配置文件中。因为之前的配置应用已经成功地将正确的参数值保存了下来。
检查步骤:
gcloud secrets list --filter='name~clearpay-'
然后确认没有哪个秘密仍然包含占位符。这个脚本会输出“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
需要找到四行“ok”输出。如果其中有一行显示“STILL PLACEHOLDER”,则需要重新执行相应的复制命令。
在控制台中,Secret Manager会列出这四个秘密,每个秘密的版本号都是2(表示最新版本)。请注意不要点击共享屏幕上的“Show secret value”按钮。
消息确认:这四个秘密中都存储了AWS相关的信息。没有任何信息会被显示在屏幕上,也不会被粘贴到聊天记录中。
阶段7:统计API错误次数(07-observability)
每当API中发生某些操作时,系统都会生成一条称为“日志记录”的简短信息。登录操作会生成一条日志,支付操作也会生成一条日志。当出现错误时,日志中会标注“level”: “error”。例如,一次支付失败时,生成的日志内容如下:
{"level":"error","message":"transaction_failed","timestamp":"2026-09-29T10:15:02.000Z","error":"..."}
在GKE环境中,Google Cloud会收集所有这些日志记录。在这个阶段,我们会基于这些日志记录构建两样功能:
一个计数器,名为“clearpay-api-error-count”。每当有错误日志出现时,这个计数器的值就会增加1。
一个警报机制,名为“ClearPay API - elevated error rate”。当计数器在5分钟内增加了5次以上时,该警报就会触发。一次错误可能是偶然现象,但如果5分钟内出现5次错误,那就说明系统确实存在问题。
目前这个警报机制还没有关联任何电话号码或电子邮件地址。当警报触发时,Google Cloud控制台会显示相关信息,但不会有人收到通知。你可以在后续的操作中为该警报添加相应的联系信息。
由于ClearPay尚未在GKE环境中运行,因此当前计数器的值为0。今天来说,这个值是正确的。
cd infra/gcp/terraform/07-observability
执行以下命令:
terraform init
terraform plan
terraform apply
预期的执行结果如下:
计划操作:新增2个资源,不修改任何资源,不删除任何资源。
如果执行 `terraform apply` 时出现错误 “Error 404: Cannot find metric(s) that match type”,说明并没有什么问题发生。Google需要几分钟的时间来注册这个新的计数器,之后警报系统才能开始监控它。计数器已经创建好了,请等待五分钟后再重新运行 `terraform apply`。这次命令执行后,计划中会显示 “1 to add”,这意味着只有警报规则需要被添加。
检查方法:
gcloud logging metrics describe clearpay-api-error-count
查找名为 `clearpay-api-error-count` 的计数器,以及其中包含 “clearpay-api” 和 “jsonPayload.level="error""” 这些关键信息的过滤条件。
Maya的消息是:“错误计数器已经存在,其值为0。目前还没有任何应用程序在 GKE 上运行。”
阶段 8:构建公共负载均衡器(08-edge)
前七个阶段已经完成了基础设施的搭建,这个阶段的主要任务是构建“前端入口”——也就是客户在后续迁移过程中会使用的地址。我们将会使用 Google 的全球负载均衡器,并设置流量分配规则:其中 100% 的流量会被发送到 AWS,剩下的则会被发送到 GKE。目前来说,所有流量都会被直接发送到 AWS。
这个阶段会创建以下内容:
clearpay-api-ip:客户将使用的公共地址
- 一个自签名证书,以确保 HTTPS 连接能够正常工作
- 流量分配规则:AWS 接收 100% 的流量,GKE 接收 0% 的流量
clearpay-gke-edge-ip:GKE 端使用的备用 IP 地址。负载均衡器会使用这个地址,但客户本身并不会直接访问它
下图展示了 GCP 负载均衡器是如何根据可配置的流量分配规则,在 AWS 和 GKE 之间引导客户流量的。客户们是通过一个公共 IP 地址与负载均衡器连接的;AWS 通过其弹性 IP 接收流量,而 GKE 则通过另一个专用 IP 地址接收流量。这两种环境都会连接到 AWS 上的同一个 PostgreSQL 数据库。目前,GKE 集群还为空。
cd infra/gcp/terraform/08-edge
cp terraform.tfvars.example terraform.tfvars
如果文件已经存在,就可以跳过这个复制步骤。请将你的弹性 IP 地址设置到 `aws_backend_ip` 变量中。你可以在仓库根目录下的另一个终端窗口中获取该地址:
terraform -chdir=infra/aws/terraform output -raw instance_public_ip
请保持 `aws_traffic_weight` 和 `gke_traffic_weight` 的值分别为 100 和 0:
aws_traffic_weight = 100
gkeTraffic_weight = 0
执行步骤:
terraform init
terraform plan
terraform apply
“apply”命令的执行大约需要两分钟时间。
计划操作结果:需添加17个资源,无需修改0个资源,无需删除0个资源。
请将这两个IP地址记下来。static_ip_address是客户专用地址,而gke_edge_ip_address则不是。
为这个地址起个名字吧
由于你还没有自己的域名,因此需要将名称api.clearpay.test添加到电脑的hosts文件中,从而将其指向相应的负载均衡器。
在应用操作完成后,你已经处于08-edge环境中了,所以下面的cd命令可以跳过。只有当你在仓库根目录下打开了新的终端窗口时,才需要执行这条命令:
cd infra/gcp/terraform/08-edge
LINE=$(terraform output -raw hosts_file_line)
echo "LINE是:[$LINE]"
如果LINE为空,说明操作成功;如果其内容为x.x.x.x api.clearpay.test,那么就需要将其添加到hosts文件中。
在Mac上执行如下命令:
echo "$LINE" | sudo tee -a /etc/hosts
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
在Linux系统中,只需执行第一行命令即可;在Windows系统中,需要以管理员权限打开编辑器,然后将相同的IP地址及api.clearpay.test添加到C:\Windows\System32\drivers\etc\hosts文件中。
请不要执行sudo cd命令。因为cd是Shell内置的功能,所以使用sudo也无法改变当前的工作目录。
确认该地址确实已被添加到hosts文件中:
grep api.clearpay.test /etc/hosts
之所以将地址名称设置为.test,是因为.test是专门用于测试目的的域名。这样,Chrome浏览器在遇到自签名证书警告时也会允许用户继续访问页面。请不要将lab_hostname改为.dev格式,因为Chrome会强制要求所有.dev网站使用HTTPS协议,并且会隐藏“继续访问”按钮。
进行检查
执行以下命令来验证配置是否正确:
curl -k https://api.clearpay.test/health
预期的输出结果如下:
{"status":"ok","served_by":{"platform":"aws","version":"1.0.0","instance":"ip-172-31-64-73","commit":"504e0cd","rollback_active":false}}
你的instance值和commit值可能会有所不同,但"platform":"aws"这一项可以用来确认请求是否真的发送到了新的Google负载均衡器,并且被正确地转发给了AWS。
新的Google负载均衡器可能需要几分钟时间才能启动完毕。如果你还没有看到预期的JSON响应结果,可能会先看到以下两种情况之一:
出现“连接重置”提示。这说明负载均衡器仍在启动中,请稍等片刻后再尝试访问。
看到一个标题为“404未找到”的Google页面。这说明负载均衡器已经响应了请求,但数据尚未传送到AWS。请先确认AWS本身是否处于正常运行状态。
curl -k https://YOUR_ELASTIC_IP/health
如果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;负载均衡设置会显示以你的Elastic IP作为后端的clearpay-migration-forwarding-rule配置。
完成条件:
第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上的集群目前还是空的,所以还没有人能够将资金汇入那里。”
她指了指你的终端。
玛雅说:“下一步就是将ClearPay部署到那个集群上。有一点我很在意:必须让自动化流程来完成这项工作,而不是让人手动输入命令。自动化流程每次都会以相同的方式执行操作,这样我们就能清楚地知道它具体做了什么。”
https://api.clearpay.test这个地址在你的浏览器中已经可以正常使用了,因此人们很容易以为ClearPay已经被部署到了GKE上。但实际上并非如此。这个地址其实是谷歌新的入口点,也就是第8阶段使用的负载均衡器。由于配置权重分别为100和0,所以所有请求都会被发送到AWS上去。控制面板上的served by aws/1.0.0这一行就是明证。而GKE集群本身仍然为空。
浏览器 → google负载均衡器 → AWS EC2 → RDS
→ GKE(空)
在这个阶段,自动化流程会将ClearPay部署到GKE的测试环境中,而第6章中提到的升级机制将会决定这个测试环境是否满足使用要求。测试环境其实就是集群内部的一个私有副本,没有任何客户能够直接访问它。
第12章:检查API镜像
第二周,周二,上午9:00
集群已经搭建完成,但目前还为空。在开始任何操作之前,丹尼尔将API相关的Dockerfile显示在了你的屏幕上。
丹尼尔说:“我们马上就要把这个镜像部署到集群上了。请确保容器不是以root权限运行的。”
我们需要确认这个API镜像的大小是否合适,其中是否包含任何敏感信息,以及它是否确实是以非root权限运行的。
为什么这很重要
如果攻击者成功入侵了容器,那么以root权限运行的进程将会拥有远超其实际需要的权限。因此,在生产环境中,应用程序的镜像应该始终以普通用户的权限来运行。
操作步骤
首先打开api/Dockerfile文件。这个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已经处于运行状态。接下来,在仓库的根目录下执行以下命令:
docker build -t clearpay-api:test ./api
docker run --rm clearpay-api:test whoami
检查结果
第二个命令执行完成后,会输出appuser这个用户名,这说明程序确实是以非root权限运行的。
如果出现错误
如果输出的是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中获取数据库地址和JWT密钥信息的。为此,它们使用了两个GKE插件:
Secret Manager CSI驱动程序会将配置信息以文件的形式加载到Pod内部。
Secret Sync插件会将这些配置信息复制到一个名为clearpay-api-secrets的Kubernetes Secret中,然后部署脚本会将这些信息加载到API的环境变量中。

Pod们会通过Workload Identity机制来识别自己的身份,此时它们会使用Stage 5中配置的服务账户。实际上,并不需要在任何地方保存密钥文件,API代码也不会发生任何变化——它仍然像在AWS上那样,直接读取环境变量中的信息。
gcloud命令需要执行两条独立的命令,每条命令都需要几分钟才能完成:
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环境中运行的,因此集群的配置过程不会受到影响,你也能立刻看到Secret访问功能被添加到的效果。
请保持PROJECT_ID的值不变,它原本就存在于k8s/overlays/staging/secretproviderclass.yaml和patch-serviceaccount.yaml文件中。在部署时,管道脚本会自动将这个值替换为你真正的Project ID,请不要手动在这些文件中输入该值。
B部分:将AWS数据库接入GKE
不要把这一步描述为“将GKE连接到数据库”。这种说法容易让人误以为数据库本身也会被移动到GKE环境中,但实际上并非如此。这一步骤的作用仅仅是让GKE能够安全地访问和使用位于AWS上的原始数据库而已。
该集群中的机器没有公共IP地址。它们的出站流量是通过Cloud NAT使用一个公共地址进行传输的,这个公共地址就是第二阶段配置中设置的NAT IP地址。RDS会接受来自该地址、端口5432上的连接请求,并且连接字符串本身就已经要求使用TLS协议进行加密(sslmode=require)。GKE Pod (clearpay-api)
│
▼
私有节点 (没有公共IP地址)
│
▼
Cloud NAT (clearpay-nat-ip)
│
│ 公共出站连接,范围为1/32网段
▼
AWS安全组 (additional_db_ingress_cidrs)
│ 端口5432,支持SSL加密
▼
RDS PostgreSQL 16
这是连接两种云环境的三种方法之一:
方法
设置步骤
>运行成本
>工作原理
公共端点 + IP地址允许列表
需要配置一些Terraform变量
额外费用为0美元(使用常规互联网出站连接)
RDS会获得一个公共IP地址;安全组仅允许来自Cloud NAT的1/32网段流量;必须使用TLS加密
Cloud VPN(点对点连接)
需要配置两朵云平台上的网关
在GCP上每月费用约为36美元,在AWS上每条隧道每月费用也约为36美元GCP VPN价格信息
采用私有连接路径;RDS没有公共端点
AWS的合作伙伴提供的跨云互联服务
实际配置所需时间较长
提供10 Gbps或100 Gbps两种带宽等级跨云互联服务详情
专为高流量场景设计的专用私有链接
如果您需要为自己的系统选择合适的连接方式,可以按照以下公式进行计算:
(预期的每月数据量以GB为单位) × (该连接方式的每GB费用) + 该选项的固定月费
由于ClearPay发送的数据量较小,因此使用VPN会导致每月的费用远高于实际数据传输所消耗的成本。在这个实验环境中,我们选择了使用IP地址允许列表的方法;但对于数据流量较大的系统来说,可能需要选择其他方案。
cd infra/gcp/terraform/02-network
terraform output nat_ip_address
将获取到的NAT IP地址添加到文件infra/aws/terraform/terraform.tfvars中:
db_publicly_accessible = true
additional_db_ingress_cidrs = ["/32"]
然后应用AWS相关的配置文件:
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服务器仍然需要能够访问数据库。在仓库根目录下执行以下命令:
export INSTANCE_IP=$(terraform -chdir=infra/aws/terraform output -raw instance_public_ip)
curl -k https://$INSTANCE_IP/health/ready
如果响应内容中显示“status: ready”,或者AWS登录页面显示“internal error”,说明数据库正常工作。但如果出现其他错误,请重新检查Terraform配置文件,并先解决AWS相关的问题,因为AWS是所有客户都能使用的基础设施。
在继续之前请先进行检查
第14章会开始执行相关代码并启动管道流程。如果前面有任何步骤被跳过,那么在后续的两个章节中,管道流程会因为错误而失败,但这些错误信息并不会指出具体是哪一步被忽略了。因此,请现在就运行这个检查工具。该工具会为管道流程所需的各项内容输出“ok”或“MISSING”作为结果。请从仓库的根目录处运行以下命令:
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_publiclyaccessible *= *true' infra/aws/terraform/terraform.tfvars \
&& echo "ok RDS公共配置设置正确" \
|| echo "MISSING:infra/aws/terraform/terraform.tfvars文件中未找到db_publicly_accessible = true这一项"
grep -q "$NAT_IP/32" infra/aws/terraform/terraform.tfvars \
&& echo "ok NAT IP地址已在允许列表中" \
|| echo "MISSING:additional_db_ingress_cidrs文件中未找到$NAT_IP/32这一项"
kubectl get csidriver secrets-store-gke.csi.k8s.io >/dev/null 2>&1 \
&∓ echo "ok Secret Manager驱动程序配置正确" \
|| echo "MISSING:请按照A部分中的说明运行相应的gcloud命令"
kubectl get crd secretsyncs.secret-sync.gke.io >/dev/null 2>&1 \
&∓ echo "ok Secret Sync配置正确" \
|| echo "MISSING:请按照A部分中的说明运行相应的gcloud命令"
curl -sk https://$INSTANCE_IP/health/ready | grep -q '"status":"ready"' \
&∓ echo "ok AWS能够正常访问其数据库" \
|| echo "MISSING:AWS无法访问其数据库,请重新查看Terraform配置文件"
最后的检查会验证从GKE到数据库的实际连接路径是否正常。该检查会创建一个临时Pod,然后测试数据库端口是否允许接收连接请求,最后删除这个临时Pod。第一次运行这个检查可能需要大约一分钟的时间,因为Kubernetes需要下载相应的镜像文件:
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无法访问数据库"
你需要看到六行“ok”的输出结果,而不能有任何“MISSING”这样的提示。如果出现了“MISSING”这样的提示,就需要立即进行相应的修复操作,然后再重新运行检查。在所有六项检查结果都显示为“ok”之前,请不要开始执行第14章的内容。
如果最后的检查结果显示为“MISSING”,但前两项的检查结果都是“ok”,那么可能说明RDS仍在处理这些变更请求。请等待两分钟后再重新运行该检查命令。
完成条件:
两条“gcloud container clusters update”命令都已完成执行
RDS已经允许该NAT IP地址通过连接,且执行命令“curl -k https://$INSTANCE_IP/health/ready”后仍能得到“ready”的返回结果
上述检查步骤全部完成后,应该会看到六行“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配置信息。在阅读关于触发条件、权限设置、镜像构建与扫描、staging环境中的部署以及生产环境中的升级机制等内容时,请务必同时参考这两者。
请按照这张图从上到下依次阅读文件内容。
触发条件: 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的秘密存储区中,因此不存在泄露风险。
镜像标签: 镜像标签其实就是git rev-parse --short HEAD命令返回的简短提交哈希值。每个镜像都对应着某个特定的提交版本。自动化流程还会推送一个动态变化的标签staging-latest,但实际进行部署时使用的还是那个具体的提交哈希值。
扫描环节: Trivy是一款开源工具,它可以用来检查镜像中所包含的软件包是否存在已知的漏洞。每个已知漏洞都有一个唯一的编号,比如CVE-2026-29786。工作流程会配置相应的扫描规则来检测这些漏洞。
severity: CRITICAL,HIGH,因此只有严重的安全问题才会被考虑在内。
ignore-unfixed: true,这意味着如果某个安全问题尚未有对应的修复方案,也不会阻止构建过程的进行。
exit-code: "1",只要还存在任何未解决的安全问题,整个构建流程就会失败。
综合这些设置,其含义就是:如果存在严重的安全漏洞且尚未有修复方案,那么构建过程将会失败;如果扫描失败,任何代码都不会被推送上去。
推送代码:该流程会为每个构建生成的镜像添加完整的注册路径标签,格式为us-east4-docker.pkg.dev//clearpay/api: ,然后将其推送到指定的仓库。clearpay-ci在第五阶段配置了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命令,等待API和UI相关的Pod准备好。如果它们始终无法启动,构建流程就会失败。
clearpay-ci拥有container.developer>权限,因此它只能用于部署工作负载,而不能直接修改集群本身的配置。
推广到生产环境:只有通过第二步操作,整个流程才能进入生产环境:
environment: production:此设置会暂停构建过程,直到有指定的审核人员在GitHub上点击“批准”按钮。
该步骤不会重新构建代码,而是直接使用测试环境中使用的镜像将其部署到生产环境。
这种做法被称为“一次构建,多次发布”——只有通过第15章的所有检测的镜像才会被正式推送到生产环境。如果重新构建代码,可能会引入新的软件包,从而导致未经测试的新问题出现。
kustomize>工具在两个部署步骤中都被使用,它是kubectl apply -k命令背后的实现机制。该工具会读取k8s/base/目录中的基础配置文件,然后根据k8s/overlays/staging/或k8s/overlays/production/目录中针对特定环境的修改配置文件,对基础配置进行相应的调整。测试环境和生产环境共享同一个基础配置文件,只有在必要的地方才会存在差异。
步骤说明
首先,将这个仓库添加到你的GitHub账户下,并设置main为默认分支。
然后,需要告知Google哪些仓库可以使用clearpay-ci。打开文件infra/gcp/terraform/05-identity/terraform.tfvars,将github_repo设置为你的仓库的用户名和域名组合,例如Osomudeya/migrate-api-aws-gke-devops。注意不要复制完整的https://github.com/...地址,也不要在地址末尾添加.git后缀。最后再次执行第五阶段的配置流程。
cd infra/gcp/terraform/05-identity
terraform plan
terraform apply
该脚本会修改池的属性条件以及clearpay-ci链接,但不会影响集群本身。如果你在第11章中已经设置了相关参数,那么这个脚本就不会显示任何变化。
在GitHub上,打开你的仓库并点击“设置”。在左侧菜单中选择“秘密与变量”,然后再点击“操作”。打开“变量”标签页,分别为这三个参数创建新的仓库变量。请使用“仓库”选项卡进行配置,而不是“环境”选项卡。
名称
值
GCP_PROJECT_ID
你的GCP项目ID
GCP_WORKLOAD_IDENTITY_PROVIDER
执行`terraform -chdir=infra/gcp/terraform/05-identity output -raw github_actions_workload_identity_provider`命令后的输出结果
GCP_CI_SERVICEACCOUNT
执行`terraform -chdir=infra/gcp/terraform/05-identity output -raw ci_service_account_email`命令后的输出结果
这些都是变量,而不是秘密信息,其中没有任何一个是密码。因此,这个管道流程根本不需要使用GitHub的秘密配置。
在复制这两个`terraform output -raw`命令的输出结果时,请不要复制末尾的`%`符号。在Mac系统中,当输出内容后面没有换行符时,终端会显示`%`符号,但这并不是输出值的一部分。提供者的值必须以`clearpay-github-provider`结尾,服务账户的地址必须以`.iam.gserviceaccount.com`结尾。
接下来,在“设置”中点击“环境”,然后选择“新建环境”。将环境名称设置为`production`,再点击“配置环境”。选中“需要审核人”选项,搜索你的GitHub用户名并选择它,最后点击“保存保护规则”。
为什么这样做呢?因为作业2是进入生产环境的唯一途径。设置“需要审核人”选项后,GitHub会阻止该作业的执行,直到有人点击“批准”按钮,这样就可以确保没有任何内容会意外地发送给客户。在第19章中会详细介绍如何等待审批流程。
你会看到这个页面上有一个“管理环境秘密”的按钮,但不要点击它。因为`production`环境并不需要任何秘密信息,因为该管道流程是通过Workload Identity来获取Google访问权限的,并不需要存储密码。如果你的仓库中已经包含了其他项目的秘密信息,请让它们保持原样。
如果GitHub没有提供“需要审核人”的功能,那么在你的免费账户下,仓库可能是私有的。为了完成这个实验,请将仓库设置为公共模式,或者跳过审核人设置。如果没有审核人,那么一旦你触发作业2,它就会立即开始执行。
在推送代码之前,请先进行检查。如果管道流程失败,会耗费3分钟的时间,并生成冗长的日志文件;而进行简单检查只需要10秒钟而已。首先,请再次运行第13章末尾提供的检查命令,确保所有六行输出结果都是`ok`。然后,检查你提供给Google的仓库名称是否正确:
REPO=$(grep '^github_repo' infra/gcp/terraform/05-identity/terraform.tfvars | cut -d'"' -f2)
case "$REPO" in
https*|*.git|YOUR_GITHUB_USER/*) echo "缺少github_repo字段,它应该等于仓库所有者或名称。当前值为:$REPO" ;;
*/*) echo "ok github_repo $REPO" ;;
*) echo "缺少github_repo字段,它应该等于仓库所有者或名称。当前值为:$REPO" ;;
esac
它必须输出“ok”以及你的用户名。
现在,触发这个处理流程:
git commit --allow-empty -m "触发处理流程"
git push
检查结果
打开“操作”选项卡,点击正在运行的工作流。观察“build-scan-push”这一系列步骤的完成过程,每个步骤完成后都会显示对勾:构建、扫描API镜像、扫描UI镜像、登录GCP、推送代码、部署到测试环境。
如果某个步骤显示为红色,请在此处找到它。每个出现红色的步骤都会提示你需要重新运行整个工作流:
显示为红色的步骤
对应的操作
扫描API镜像或扫描UI镜像
"如果扫描失败"
登录GCP
"如果登录GCP失败"
部署到测试环境
"如果部署失败"
在修复问题后,只需打开“操作”选项卡,点击失败的运行记录,然后点击右上角的“重新运行所有任务”即可。无需再次执行推送操作。
第二个任务“promote-to-production”会显示为一个带有斜线的灰色圆圈,这是正常的。每次进行代码推送时,GitHub都会跳过这个步骤;只有当你在第19章中手动触发它时,才会真正执行这个任务。
当“部署到测试环境”的步骤成功完成之后,请检查集群状态:
kubectl -n staging get pods
API镜像和UI镜像对应的Pod状态应显示为“Running”,且“READY 1/1”。
测试环境没有公共地址,因此负载均衡器仍然会将所有请求发送到AWS。请不要直接在浏览器中访问测试环境;第15章中介绍了通过私有隧道访问测试环境的方法。
如果扫描失败
千万不要将“exit-code: "1"`修改为“"0""",因为这样会关闭安全检查机制。
Dockerfile文件已经经过了相应的修补,因此扫描应该能够通过。不过以后可能会出现新的安全漏洞,导致某些步骤显示为红色。此时请打开这些显示为红色的任务记录,Trivy会生成一个表格,你需要在其中找到“目标”列,并根据提示进行相应的操作:
“目标”列
其含义
应对措施
操作系统,例如debian 12.x,具体版本信息位于“Fixed”列中
基础镜像尚未下载完成
需要重新构建Dockerfile。如果相关步骤仍然显示为红色,请将FROM node:22-slim修改为Docker Hub上最新的22.x-slim标签。
usr/local/lib/node_modules/npm
npm用于安装相关包
Dockerfile中包含删除npm的指令,请确认这条指令仍然存在。
app/node_modules或package-lock.json
ClearPay提供的某些包尚未下载完成
需要在api/package.json或ui/package.json文件中更新这些包的版本信息。
在“扫描UI镜像”步骤中,如果显示为Alpine版本的软件包
对应的UI基础镜像尚未下载完成
需要在ui/Dockerfile.prod文件的nginx阶段添加RUN apk upgrade --no-cache指令。
在任何对 Dockerfile 的修改之后,都需要在本地重新构建应用程序,并确认用户身份仍然为 appuser:
docker build -t clearpay-api:test ./api
docker run --rm clearpay-api:test whoami
完成修改后,将代码提交到 main 分支,并再次观察相关操作结果。
如果尝试使用 GCP 进行身份验证失败
出现错误的提示信息如下:
提供的凭证不符合系统要求。
Google 先接受了 GitHub 提供的令牌,但随后又拒绝了它。在第五步中,系统会指定另一个仓库地址,这个地址通常是一个占位符,格式为 YOUR_GITHUB_USER/YOUR_REPO。实际使用的仓库地址必须与 owner/name 的组合完全一致,例如 Osomudeya/migrate-api-aws-gke-devops。如果使用完整的 URL 或其他人的仓库地址,都会导致操作失败。请使用正确的 owner/name 重新执行上述步骤,然后再次尝试部署。
git commit --allow-empty -m "在 WIF 仓库锁定结束后重新尝试部署流程"
git push
如果尝试将应用程序部署到 staging 环境失败
有两种错误信息其实表示的是同一回事:error: waiting for the condition timed out 和 error: deployment "clearpay-api" exceeded its progress deadline。这意味着文件已经上传到了目标服务器,但相应的 Pod 仍未准备好运行。可以尝试通过以下命令查询集群的状态:
kubectl -n staging get pods
kubectl -n staging describe pod -l app=clearpay-api
根据查询结果采取相应的处理措施:前两行信息通常会出现在作业日志中,其余信息则会显示在 Pod 的运行输出中。
出现的错误信息
含义
>解决方法
no matches for kind "SecretProviderClass" 或 "SecretSync"
Secret Manager 插件未启用
请按照第 13 章 A 部分的说明重新运行相关命令,然后再次尝试部署。
Permission denied on gcloud secrets versions access
当前账户没有权限访问 staging 环境中的秘密信息
请重新执行第 6 步的操作(参见下面的解决方法 A)。
ImagePullBackOff with 403 Forbidden
集群中的机器无法访问注册中心
请重新执行第 4 步的操作,然后重启相关 Pod(参见下面的解决方法 B)。
Insufficient cpu
集群的资源不足,或者已经达到容量上限
运行 kubectl get nodes 命令查看节点数量。如果节点数量少于 3 个,请等待几分钟;如果节点数量为 3 个,那么请将 staging 环境中的 Pod 规模调整为一个
FailedMount with driver ... not found 或 secret "clearpay-api-secrets" not found
可能有新的机器正在启动中,或者 Secret Sync 还没有完成秘密信息的写入操作
请等待 2 分钟,Pod 会自动尝试重新连接。也可以通过 kubectl -n staging get secret clearpay-api-secrets 命令来检查秘密信息是否已经上传成功。
Running but READY 0/1, with Connection terminated due to connection timeout 或 Readiness probe failed
Pod 无法连接到 PostgreSQL 数据库
请参考第 13 章的说明,检查是否缺少某些配置或步骤。通常情况下,问题出在 B 部分的内容中。Pod 会在下一次健康检查时自动恢复正常状态。
修复措施A:让管道能够访问用于测试环境的配置信息:
cd infra/gcp/terraform/06-secrets
terraform plan
terraform apply
修复措施B:允许集群读取配置注册表。这些机器会以默认的Compute Engine服务账户来下载镜像,而该账户需要具备artifactregistry.reader权限。相应的配置规则如下:
cd infra/gcp/terraform/04-registry
terraform plan
terraform apply
之后,请重启用于测试环境的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
修复措施C:在测试环境中仅运行一个API Pod。由于小型实验室集群的容量有限,因此无法同时运行两个API Pod。请缩小集群规模,然后再重新执行相关操作:
kubectl -n staging scale deployment/clearpay-api --replicas=1
如果需要查看API的日志而不让敏感信息被显示出来,可以使用以下命令:
kubectl -n staging logs deployment/clearpay-api --tail=20
完成条件:
你能够解释deploy.yml文件中定义的这两项任务各自的作用。
作为指定的审核人员,你已经确保了production环境已经准备就绪。
“将配置部署到测试环境”的操作已经成功完成。
执行命令kubectl -n staging get pods后,会看到API Pod和UI Pod的状态均为“Running”,且Ready值为1/1。
至此,管道流程就已经结束。接下来需要手动执行“推广门”验证步骤,具体操作方法将在第15章中介绍。
第15章:检查测试环境并执行推广门验证
时间:第二周周三上午10点
昨晚6:40,管道流程顺利完成,且没有出现任何错误。你当时还截取了屏幕截图,不过ClearPay的同事们其实并不关心这个截图。
Daniel走到你的办公桌前,查看了刚刚完成的管道操作过程。
Daniel问道:“那么GKE集群运行状况正常吗?”
你回答说:“Pods目前运行正常,但这并不能证明客户能够成功进行20美元的支付操作。”
Maya也看到了这个讨论记录,她补充道:“在推广脚本通过验证之前,客户是无法使用这些部署好的服务的。仅仅让Pods运行起来是不够的。”
为什么这很重要
Kubernetes会将那些健康检查结果正常的Pod标记为“Ready状态”,这意味着这些进程已经启动并且能够访问数据库。然而,这一状态并不能说明用户是否能够成功完成登录、支付等操作,也无法保证不会出现重复收费等问题。而“推广门”验证步骤正是用来检测这些问题的。
“推广门”实际上是一个需要在笔记本电脑上运行的脚本。管道流程本身并不会自动执行这个脚本。另外,它也不是你在GitHub上看到的那个灰色的promote-to-production任务——那个任务会在第19章中才会执行,而且只有在“推广门”验证通过之后才会启动。
测试环境的运行机制
GKE集群“clearpay”,所在区域为us-east4-a
│
├── 名称空间staging
│ 服务clearpay-api,端口为80
│ └── API容器(HPA设置为1–3)
│
└── 名称空间production(在第19章中创建)
Gateway clearpay-gke-edge ← clearpay-gke-edge-ip
HTTPRoute → 服务clearpay-api,端口为80
└── API容器(HPA设置为1–3)
每个容器的DB_POOL_SIZE值为10
丹尼尔也在查看同一张图。
“这个生产环境集群最多可以运行多少个API进程?”
“在这个集群上最多可以运行三个。这个限制值就在patch-hpa.yaml文件中。”
“那么每个API进程最多可以建立多少个数据库连接?”
“也是10个。和周一时db.js文件中的设置一样。”
“那么数据库能看到哪些信息呢?”
你停下来想了想:GKE集群最多可以运行3个API进程;而当AWS的相应服务也处于活跃状态时,总共就可以有6个进程在运行。如果这个集群能够扩展到可以容纳8个容器的规模,那么仅GKE集群就能建立80个数据库连接。
“这就是我让你记住的这个数字。”丹尼尔说道。
现在有三份文件值得我们仔细研究:
k8s/base/deployment.yaml:这个文件规定了健康检查的调用路径为/health/ready,同时每个API容器会预留250MB的CPU资源和256MiB的内存。
k8s/base/networkpolicy.yaml:这个文件用于限制哪些容器可以与API服务进行通信。
k8s/overlays/staging/patch-deployment.yaml:这个文件包含了staging环境下的副本数量以及部署相关的设置。
步骤1:检查GKE集群中的数据库连接路径
可以在staging环境的API容器中运行一段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); })"
connected
步骤2:确认Secrets数据已经上传到集群中
Kubernetes会以两种密钥的形式存储这些Secret数据。describe命令可以列出这些Secret的键名和大小,但不会显示它们的实际值:
kubectl -n staging get secret clearpay-api-secrets
kubectl -n staging describe secret clearpay-api-secrets
API进程已经获取到了这些Secret的值,但我们并不需要直接打印出来:
kubectl -n staging exec deployment/clearpay-api -- node -e 'if (!process.env.DATABASE_URL) process.exit(1); console.log("DATABASE_URL is set")'
DATABASE_URL is set
为什么这里的脚本要使用单引号呢?因为默认的Mac Shell工具zsh会将双引号中的!解读为历史命令执行指令,从而导致错误。
CSI挂载功能会按名称显示这些隐藏文件:
kubectl -n staging exec deployment/clearpay-api -- ls /mnt/secrets-store
请查找DATABASE_URL和JWT_SECRET这两个字段。
步骤3:运行促销测试程序
“Staging”环境是私有的。使用kubectl port-forward命令可以建立一条隧道,这样你的笔记本电脑上的localhost:8080就可以连接到“staging”环境中的API服务器。&符号用于使该隧道在后台持续运行。在仓库根目录下执行以下命令:
kubectl -n staging port-forward svc/clearpay-api 8080:80 & &
请等待直到出现以下这条消息再继续下一步操作:
Forwarding from 127.0.0.1:8080 -> 4000
在Amara线程中已经有人提到了:“需要运行回放测试,我想看看一次借记操作的效果。”
请使用与第6章中针对AWS环境时相同的命令来运行这个促销测试程序。该命令的四个参数分别是隧道地址、Alice的电子邮件地址、她的密码以及Bob的电子邮件地址:
./scripts/promotion-gate.sh http://localhost:8080 \
alice@clearpay.dev password123 bob@clearpay.dev
PASS: 健康检查端点返回正常结果。
PASS: 准备就绪状态端点确认数据库连接成功。
PASS: 登录操作成功,获得了访问令牌。
PASS:重新执行操作后,系统确认该操作是幂等的,返回结果为“replayed=true”。
PASS:余额确实只被借记了一次(100美分),没有出现两次借记的情况。
5项测试均通过,0项失败。
测试成功:可以安全地将系统推广到生产环境。
要关闭这条隧道,请使用kill %1命令。
由于GKE使用的数据库与AWS相同,因此Alice在GKE环境中并不需要重新设置密码。
步骤4:确认客户仍在AWS环境下运行
“Staging”环境没有公共地址:
kubectl -n staging get svc clearpay-api
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
clearpay-api ClusterIP 10.x.x.x 80/TCP 12m
CLUSTER-IP字段显示为,说明只有该集群内部才能访问这个服务。
客户的地址仍然可以通过AWS环境进行访问:
export INSTANCE_IP=$(terraform -chdir=infra/aws/terraform output -raw instance_public_ip)
curl -sk https://$INSTANCE_IP/health
{"status":"ok","served_by":{"platform":"aws",...}}
如果在这里看到“gke”这个选项,说明测试成功。但如果curl请求失败,那就说明AWS环境出现了故障,这与GKE是否能够正常处理请求是两个不同的问题。
总之,虽然两个云平台都在运行中,但只有AWS环境对客户开放。而“Staging”环境则独立运行在GKE内部,那个用于测试的程序并不是维持其运行的关键因素,它只是通过你建立的隧道来执行测试任务而已。
浏览器 → api.clearpay.test → AWS EC2(为所有用户提供服务)
GKE staging环境 → 你的隧道(port-forward功能)→ 测试程序及负载测试工具
GKE生产环境(尚未建立,详见第19章)
客户只有在阅读完第21章之后,当前置服务器开始向GKE发送真实流量时,才能真正使用到GKE。
如果出现故障……
在这种情况下,请不要继续推广这个版本。
如果出现“JSON输入突然结束”或“连接被拒绝”的错误,说明脚本在隧道尚未启动监听之前就被执行了。如果终端中已经输入了“Forwarding from 127.0.0.1:8080”这一命令,请重新运行脚本即可,无需再次开启端口转发功能。
即使健康检查通过,但如果“准备就绪”状态仍然显示为失败,说明Pod无法连接到PostgreSQL数据库。此时请返回到第13章B部分进行排查。
如果登录失败,请核对您输入的用户名“alice@clearpay.dev”和密码“password123”是否正确。
如果“checkConnection”命令显示TLS连接错误或超时异常,说明问题与“准备就绪”状态失败的原因相同。请参考第13章B部分进行解决。
如果执行“ls”命令后没有看到任何文件,或者Pod的状态仍停留在“ContainerCreating”阶段,请运行命令“kubectl -n staging describe pod ”,检查是否存在与Secret Manager相关的权限问题。staging环境的“clearpay-api”服务账户上的注释内容必须与第5阶段中设置的电子邮件地址完全一致,只有这样才能确保第6阶段能够正常读取秘密信息。
请找出问题的根本原因,如果代码或文件发生了变化,请先通过测试流程进行验证,然后再重新运行检测程序。
决策...
Maya问道:“我们准备好了吗?”
“检测已经通过。健康检查、准备就绪状态、登录功能、重复支付测试以及一次借记交易测试也都成功了。Amara的重复支付验证也通过了,目前客户们仍在使用AWS环境。”
Maya接着说:“那么我们现在就可以讨论将系统部署到生产环境的事宜了……不过在此之前还需要做一些准备。”
她再次查看终端界面,确认“staging环境运行正常”后,问道:“接下来我们应该继续通过互联网将GKE连接到同一个PostgreSQL数据库上吗?”
完成条件:
从staging环境的Pod中执行“checkConnection”命令后,必须能够得到“connected”的响应。
必须确保“DATABASE_URL”配置正确,并且“/mnt/secrets-store”目录中列出了所有的秘密信息名称。
通过隧道传输数据时,检测结果应为“5 passed, 0 failed”。
staging环境应使用“ClusterIP”地址进行连接,且不应有任何外部IP地址;同时,Elastic IP地址仍然需要指向“aws”服务器。
目前我们已经达到了这个检查点:staging环境的测试已经通过,GKE也能够成功连接到真实的数据库了。
接下来,我们需要证明该系统在正常负载下依然能够稳定运行,只有在确认这一点之后,才能允许客户开始使用它。
第5部分:如何让GKE具备生产环境适用性
staging环境的测试已经成功。这些测试验证了五项关键内容:应用程序能够正常运行、能够连接到数据库、用户可以成功登录、重复支付功能能够正常执行、且账户余额会按照预期减少。虽然这些结果很不错,但还不能说明系统已经完全适合客户使用。
想象有一辆刚刚通过首次试驾的新汽车。它已经能够启动并正常转弯了,但在有人真正乘坐之前,你仍然会再问三个问题:车门是否锁好了?能否看到仪表盘?在陡坡上行驶时会发生什么?或者当某个轮胎漏气时又会怎样呢?
对于ClearPay来说,也需要问这三个同样的问题:
问题
章节
需要检查的内容
是否有任何敏感信息被暴露了出来?
16
是否有人能够读取这些敏感信息?数据库连接是否经过了加密处理?每个账户是否只拥有它所需要的权限?
我们能否了解应用程序的实际运行情况?
17(可选)
可以在笔记本电脑上使用测试仪表盘来观察应用程序的响应速度以及有多少请求会失败。这样可以帮助我们熟悉这些工具。在流量高峰期间,还可以通过命令行检查来监控系统的运行状况。
在面临压力时,系统会如何表现?
18
比如在业务量很大的下午,系统中是否会同时收到大量请求?如果某个服务组件在工作过程中出现故障,系统是否会自动重启?当客户还在使用该应用程序时,是否需要进行重启操作?
提出这些问题是有意为之的。因为客户目前仍在使用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')
sslmode_require is set
检查2:是否了解秘密信息的轮换机制
在`api/src/routes/auth.js`文件中,生成的登录令牌的有效期为12小时。如果更换了`JWT_SECRET`密钥,那么所有使用旧密钥生成的令牌都会失效,用户必须重新登录。对于这个实验来说,这种设计是可行的。请将这一规定记录在笔记中作为秘密信息轮换的政策。不过在这里不要自动执行密钥的轮换操作。
检查3:是否存在审计日志
gcloud logging read 'protoPayload.serviceName=("secretmanager.googleapis.com" OR "iam.googleapis.com")' --limit=5 --freshness=7d --format='table(timestamp,protoPayload.methodName)'
查找那些包含SecretManager或iam字样的方法名称。如果只看到像SetIAMPolicy这样的iam相关方法名称,那也是正常的——这说明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)'
需要检查的内容如下:
测试环境中的秘密配置应该包含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无关,后续的章节也不需要用到这一章的内容。如果你想了解Prometheus和Grafana的工作原理,可以直接跳到第18章;但如果你愿意花时间学习这些工具,那么继续阅读这一章也是可以的。
该API会在/metrics路径下提供各种数值数据,包括请求次数、延迟时间以及数据库池的使用情况。Prometheus会每隔几秒收集一次这些数据,而Grafana则会将这些数据可视化展示出来。
在这一章中,我们会在你的笔记本电脑上使用Docker Compose来运行Prometheus和Grafana,并让它们监控第1章中提到的API接口。这样就可以验证仪表盘的功能是否正常了。需要注意的是,这一章并不涉及对GKE或AWS的监控;相关的配置文件保存在observability/targets/目录下,文件扩展名为.example,但在这个实验环境中并不会使用到这些文件。在正式转移流量的过程中,第21章和第22章中介绍的命令行检查方法才是最终确定系统是否正常运行的依据。
从第7阶段开始,Google Cloud就自己拥有独立的错误计数器。这是GCP中的Cloud Logging指标,并与这个Grafana指标是分开的。
当前 localhost:4000 → Prometheus → Grafana :3000
之后 AWS、GKE、负载均衡器 (可选:observability/targets/*.example)
步骤
Prometheus会读取host.docker.internal:4000这个地址,这就是第1章中提到的API接口。如果执行curl http://localhost:4000/health失败,请先启动相关服务:
docker compose up -d api postgres
接着再启动监控堆栈:
cd observability
docker compose up
打开http://localhost:3000。这个仅适用于本笔记本电脑的监控系统允许匿名管理员访问,请进入ClearPay - 迁移控制面板。
检查
向本地API发送几条请求:
curl http://localhost:4000/health
“按环境划分的请求延迟分布”面板中会显示一个名为local的统计项。如果该面板为空,说明Prometheus无法连接到端口4000。
完成条件:
- 当控制面板中出现
local这个统计项时,表示测试已完成。
第18章:有目的的负载测试与阶段环境破坏测试
星期四下午1:00
阶段测试已经完成,你认为一切都很顺利。但Daniel将你的第6章评估结果与阶段测试的结果并排显示在屏幕上。
Daniel说:“阶段测试只模拟了一次登录操作。那么当有20个请求同时到达时,或者某个Pod崩溃了,会发生什么呢?”
针对阶段环境运行第6章的负载测试,将两种测试结果进行对比;然后故意终止一个Pod,在负载测试仍在进行的过程中重新启动整个部署环境。
为什么这很重要
客户在实际使用中会同时向GKE发送大量请求,而Kubernetes在更新系统或维修机器时也会重启相关Pod。你需要在这些情况发生之前就了解它们的行为,而且这一切都不需要让客户参与其中。
本次测试所使用的系统运行在第3阶段配置的普通节点池上。如果在云环境中使用负载生成工具进行测试,使用价格低廉的Spot虚拟机是个不错的选择,因为这类虚拟机的中断不会对测试结果造成影响。不过,用于正式测量的系统绝对不能使用Spot虚拟机,因为任何中断都会导致测量数据失真。
步骤1:阶段环境的负载测试
需要使用两个终端,这两个终端都必须位于仓库的根目录下。
终端A用于建立整个测试过程中的隧道连接,请保持它处于运行状态:
kubectl -n staging port-forward svc/clearpay-api 8080:80
请注意查看是否出现了Forwarding from 127.0.0.1:8080这一提示信息。
终端B:首先确认能否成功连接到GKE,并且Postgres服务是否能够正常响应:
curl -s http://localhost:8080/health
curl -s http://localhost:8080/health/readyhealth字段必须包含"platform":"gke"这一项。ready字段则必须包含"status":"ready"这一项。
接下来,运行负载测试,设置20个并发请求,持续30秒:
./scripts/load-test.sh http://localhost:8080 \
alice@clearpay.dev password123 20 30
将测试结果记录在笔记中,与AWS的测试结果放在同一位置。本次实验的具体数据如下:
AWS (第6章) GKE staging (本章)
p95延迟: 745毫秒 655毫秒
错误率: 0.0 % 0.0 %
Pod被终止后的恢复时间:n/a (在第3步之后填写)
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这一项始终为0。同时,请记住这两个延迟数值,它们会在本书的最后部分再次出现。
步骤2:查看served by字段所显示的信息
打开api.clearpay.test地址上的ClearPay控制面板,查看“近期活动”栏目。你会看到大部分记录都是与AWS相关的,只有零星几条是与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 ← 这是一笔较早进行的支付请求
每条记录都会显示处理该交易的具体服务进程。虽然这两种云服务都写入同一个数据库,但用户界面会显示所有相关的记录。那么为什么与GKE相关的记录如此少呢?
这是因为你的浏览器仍然会将所有的请求发送到AWS,目前而言,100%的流量都是通过AWS处理的。
只有第15章中的促销活动相关请求是通过GKE来处理的,这笔1.00美元的支付就是通过GKE完成的。而负载测试只是用于读取数据,并不会生成任何新的记录。
“gke/0.0.0-unset”这一条记录并不意味着负载均衡器将请求发送到了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 "正在删除$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 "恢复时间:$(( $(date +%s) - START )) 秒"
kubectl -n staging get pods -l app=clearpay-api
kubectl delete pod会等待旧的Pod完全被删除,其中包含k8s/base/deployment.yaml中规定的5秒preStop暂停时间。这个暂停机制可以确保正在处理的请求在进程终止之前完成。而recovery所表示的时间是从Pod被删除到新的Pod准备好开始提供服务为止所经过的时间。在上次实验中,记录下来的恢复时间为42秒,但你的结果可能会有所不同,请将你的测量结果填写在上面的表格中。
如果wait命令超时,请重新运行kubectl -n staging get pods,然后使用新的Pod名称再次执行kubectl -n staging describe pod命令。
当指向某个Pod的Terminal A通道被删除时,该通道可能会中断。如果Terminal A已经关闭,在进行步骤4之前,请重新启动端口转发功能。
步骤4. 在负载测试环境下重启Deployment
在这个步骤中,需要对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 "$TIMES"
if [ "$code" = "000" ]; then sleep 0.5; fi
}
000表示curl请求没有收到任何响应。设置这个短暂的延迟可以防止出现失败的Worker进程每秒执行成千上万的次循环操作。
现在在Terminal B中运行负载测试。该测试会持续40秒钟,因此重新启动Pod的操作完全可以在这个测试时间内完成:
./scripts/load-test.sh http://localhost:8080 \
alice@clearpay.dev password123 5 40
在输入该命令并按回车键之前,请在第三个终端中准备好重新启动Pod的命令。在负载测试开始后的三秒内,在第三个终端中按回车键执行以下命令:
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
这些现象表明,旧Pod会逐一停止运行,而新Pod也会依次开始启动。
在“ staging”模式下,系统会允许一个Pod先停止运行,然后再开始替换它(在《k8s/overlays/staging/patch-deployment.yaml》文件中,`maxUnavailable`的值为1,`maxSurge`的值为0)。由于这个小型实验室集群没有空间容纳额外的Pod,因此在重启期间会有一小段时间请求会失败,此时`failed=`的值大于0是正常的。请记录下这个数值。在终端A中,可能会出现“与Pod连接丢失”的提示;此时可以按Ctrl+C终止该操作,在测试结束后再重新启动隧道。
在生产环境中,系统的设置更为安全:它会在新Pod开始运行之前让旧Pod先停止(同样在《k8s/overlays/staging/patch-deployment.yaml》文件中,`maxSurge`的值为1,`maxUnavailable`的值为0)。
决策标准
如果满足以下条件,则视为通过测试:在30秒的测试时间内,`failed=`的值为0;替换用的新Pod在被删除旧Pod后能够立即准备好运行;并且“滚动部署流程”也已完成。只要GKE的p95值低于你在第6章中设定的失败阈值(即AWS p95值的3倍以下),那么即使GKE的p95值略高于AWS,也算是通过测试。
如果出现以下情况,则视为测试失败:在30秒的测试时间内出现5xx错误、登录过程中出现“internal_error”错误,或者新创建的Pod始终无法准备好运行。此时需要修复“ staging”环境中的问题,重新推动流程进行测试,然后再重复这一系列操作。
何时可以认为测试完成?
当你的笔记中同时记录下了这两份评估结果时。
当Pod的恢复过程已经完成,并且其状态已经被准确记录下来时。
当滚动重启操作在负载测试进行期间顺利完成,并且你已经记录下了`failed=`的值时。
在终端A中,按Ctrl+C终止端口转发功能。
此时就达到了一个关键节点:GKE成功经受住了负载测试以及Pod被删除带来的影响。到目前为止,所有的测试步骤都是在没有客户请求参与的情况下进行的。但从现在开始,情况会发生变化。
第6部分:如何将客户迁移至GKE
这一阶段就是将客户请求引导至GKE的过程。这两种云服务环境都可以处理客户的请求。从现在开始,每一个步骤都会引出一个问题:究竟有哪些证据表明你可以让更多的流量被引导到GKE上呢?
第19章:部署生产环境及GKE网关
第三周,星期一,上午9:00。
这一周,客户请求将开始被引导至GKE。虽然没有人明确说出来,但办公室里的气氛确实比平时要安静一些。
目前,GKE仅用于“ staging”环境。而“ staging”环境其实是一个通过隧道访问的私有副本。客户们将会使用另一个名为“ production”的副本。这一章的内容就是专门用来构建这个“ production”副本,并将其与客户们会使用的地址连接起来。
目前还缺少两样东西:
“ production”命名空间。命名空间是集群内部的一个独立区域。“ staging”环境已经有了自己的命名空间,但“ production”环境还没有,因此目前还不存在任何“ production”Pod。你需要创建这个命名空间,并将那些已经通过“ staging”测试的Pod放入其中。
位于GKE边缘IP后面的某个服务组件。在第八阶段中,你为GKE预留了一个公共地址,也就是所谓的边缘IP。这个地址就像一个你预留了但从未使用过的电话号码:它会响起,但却没有人接听。你需要将这个地址连接到某个网关上,由这个网关来接收所有的请求,并将其转发给“ production”环境中的Pod们。
当你完成这些配置后,GKE才能开始处理真实的请求。不过,在这个阶段,任何流量仍然不会被转发到GKE;直到第21章之前,前置接口会继续将所有客户请求发送到AWS。
为什么这很重要
只有当GKE上的某个组件能够响应请求时,负载均衡器才会开始将流量转发给GKE。这个负责响应请求的组件就是“网关”:一个Kubernetes对象,它会让Google在相应的服务前面创建一个负载均衡器。在这个例子中,该网关会监听clearpay-gke-edge-ip地址,并将请求转发到生产环境的API和UI接口。
08-edge负载均衡器通过互联网NEG(指向外部IP地址的网络端点组)与两个云环境进行连接。这种类型的负载均衡器不支持健康检查功能;如果后端服务出现故障,负载均衡器会继续向该服务发送请求,从而导致客户收到502错误。你的保护措施包括synthetic-health.sh脚本、决策机制以及rollback.sh脚本。
步骤1:启用网关API
在第3阶段构建的集群中,网关API尚未被启用:
gcloud container clusters update clearpay --zone us-east4-a \
--gateway-api=standard
等待操作完成之后,再执行以下命令:
kubectl get gatewayclass
查找名为gke-l7-global-external-managed的网关类别;这个类别正是k8s/gateway/gke-gateway.yaml配置文件所使用的。
步骤2:为节点池腾出空间
在部署了GKE系统组件之后,三台e2-medium型服务器每台大约只剩下940兆字节的CPU资源可用。如果同时运行测试环境和生产环境,这些资源就会不够用。因此,需要减少测试环境的节点数量来腾出空间。测试环境使用的镜像文件会保存在Artifact Registry中,相关的配置信息也会记录在你的笔记中。
首先,记下测试环境所使用的具体镜像版本;生产环境也必须使用相同的镜像版本:
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"}'
接下来,将测试环境的节点数量调整为零:
kubectl -n staging scale deploy/clearpay-api --replicas=0
kubectl -n staging scale deploy/clearpay-ui --replicas=0
在转移流量的过程中,千万不要将配置推送到main环境中;因为这样做会导致测试环境重新被部署为满配状态,从而占用更多的CPU资源。
如果之后生产环境的API Pod仍处于“Pending”状态,并且显示“Insufficient cpu”的错误信息,那说明测试环境仍然在消耗CPU资源。在这种情况下,除非你愿意额外购买一台服务器来增加资源容量,否则不要调整节点池的最大规模。
步骤3:部署生产环境
请使用之前在第14章中搭建好的管道来进行部署操作。
操作步骤如下:
在 GitHub 上,依次点击“Actions”→“Build”,然后从左侧列表中选择“scan”和“deploy”。请从这个工作流程页面开始操作,而不是从已经完成的运行记录开始。如果你正在查看某次具体的运行记录,可以点击其名称旁边的后退箭头。在那次旧运行记录中,“promote-to-production”任务会显示“此任务被跳过”,并且确实会被跳过。因此你需要重新进行一次运行。
点击“Run workflow”。保持分支设置为
main,勾选“promote-to-production”选项,然后再次点击“Run workflow”。“promote-to-production”任务会显示“Waiting for review”的状态。点击“Review deployments”,勾选
production选项,然后点击“Approve and deploy”。最后,确认系统已经将暂存环境中的镜像部署到了生产环境中,并且相关操作已经完成。
尽管暂存环境目前没有运行任何Pod,但该任务还是会从暂存环境的配置中读取镜像信息,并将这些镜像部署到生产环境中。
如果手动执行这些操作,且没有设置相应的管道流程,步骤也是相同的。文件中的PROJECT_ID只是一个占位符,你需要根据实际的管道配置来替换它。在Mac系统上,使用sed -i命令时,需要在命令后面加上空字符串'':
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后面的空字符串''即可。
将你在第二步中记录下来的两个镜像路径设置为覆盖环境的镜像源。绝对不要使用latest选项:
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_UIIMAGE替换为第二步中得到的完整镜像路径,格式应为us-east4-docker.pkg.dev/.../api:<tag>。
在使用sed和kustomize edit命令修改文件后,请不要立即提交这些文件。因为这些文件中包含了你的项目ID和具体的镜像标签,所以在部署完成后,需要先执行git checkout k8s/overlays/production命令来恢复原来的配置。
检查生产环境
返回到仓库的根目录。等待Pod们准备好:
kubectl -n production get pods
当API和UI相关的Pod显示为“Running”状态,并且状态值为“READY 1/1”时,说明部署已经成功。接下来,请从生产环境的Pod中检查数据库状态,而不是暂存环境中的Pod:
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 Gateway
保留了地址`clearpay-gke-edge-ip`以及证书`clearpay-gke-edge-cert`。现在需要为它们配置相应的服务:
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是否正常工作
可以直接通过其边缘IP地址来测试Gateway的功能。使用`--resolve`选项可以让curl在连接GKE IP时使用证书中的主机名:
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,导致请求始终会被路由到AWS。
完成条件:
kubectl get gatewayclass的命令输出结果中应包含`gke-l7-global-external-managed`这一项。
- 生产环境的API和UI Pod应该处于`Running`状态,且显示`READY 1/1`,同时使用的是预发布版本的镜像。
- 从生产环境中的Pod执行`checkConnection`命令后,应得到`connected`的返回结果。
- Gateway的状态应为`PROGRAMMED True`,并且边缘IP地址返回的信息中必须包含`"platform":"gke"`这一字段。
第20章:预切换检查清单
周一下午3:00
生产环境已经启动。Maya拿着笔记本电脑来到你的办公桌前。
Maya:“在将流量切换回去之前,请先详细告诉我如何把客户数据重新迁回AWS系统,然后让我确认这个流程是否能够正常运行。”
为什么进行回滚测试如此重要
scripts/rollback.sh脚本会按以下顺序执行两项操作:
shift-traffic.sh 100 0命令会在几秒钟内将所有流量重新路由回AWS。
- 通过SSH连接到AWS服务器,将`ROLLBACK_ACTIVE=true`这一设置添加到服务器的`.env`文件中,然后重启API服务。此时UI界面会显示回滚提示信息。
仪表板上有意显示了两条不同的信息:
新版本通知会以弹窗形式出现在右下角,上面会显示“检测到变化:此次请求是在gke环境下执行的……(之前是在aws环境下执行)”,五秒后会自动消失。当系统切换到新版本时,这条通知就会出现。
回滚提示信息则会以红色条状标签的形式显示在仪表板顶部,上面会写明“系统已回滚至AWS环境,流量已恢复正常,目前正在调查GKE环境中出现的问题”。直到你手动清除这条提示信息,它才会消失。
任何查看该应用的人都能立刻分辨出“这是新版本已经上线”还是“出现了问题但已经得到解决”。
首次进行回滚操作时,绝对不要在真正发生故障、客户正在遇到问题的情况下进行,要确保此时SSH密钥能够正常使用。最好在一切正常的情况下先进行测试。由于权重配置已经是100/0,因此进行回滚操作并不会影响流量数据;你需要测试的是SSH连接流程以及相应的提示信息显示情况。
检查清单
请根据你实际执行的操作在每项后面打勾,不要凭记忆填写。请使用你自己分配的Elastic IP地址和主机名。
目前只有四项需要执行具体的命令:GKE环境正常、回滚功能测试完成、AWS环境正常以及合成测试正在运行中。其余五项只需要使用你已经获得的结果即可,因此请仔细阅读这些内容并相应地打勾。第21章和第22章并不会重复这个检查清单,它们只会再次使用合成测试这一项。
切换前的检查清单
[ ] GKE环境正常
kubectl -n production get pods
(所有Pod都在运行中,状态为READY)
[ ] 数据库正常
根据第19章中的checkConnection命令,确认数据库能够正常连接
[ ] 监控系统已开启(可选,仅在你完成了第17章的操作后执行)
打开http://localhost:3000访问Grafana,确认它能够正常显示数据。注意:Grafana并不负责监控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登录系统,注意仪表板顶部的红色提示信息“系统已回滚至AWS环境……”。完成后请执行以下命令清除提示信息:
./scripts/clear-rollback-flag.sh YOUR_ELASTIC_IP ~/.ssh/clearpay-deployer
[ ] AWS环境正常
执行以下命令验证AWS环境的正常性:
./scripts/promotion-gate.sh https://YOUR_ELASTIC_IP \
alice@clearpay.dev password123 bob@clearpay.dev
如果所有测试都通过,输出结果应为“5 passed, 0 failed”。
[ ] 合成测试正在运行中
让合成测试在单独的终端中持续运行整个工作时段。YOUR_PUBLIC_HOSTNAME应该是api.clearpay.test。你可以在仓库根目录下使用以下命令确认这个主机名:
terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname
这个命令会每秒输出一行数据,包括时间、状态码和延迟时间。
[ ] 系统限制已确认
需要检查第6章中列出的系统限制,以及第18章中的相关数据:
AWS环境的p95值是745毫秒,GKE staging环境的p95值是655毫秒,两者的错误率都为0.0%;同时还需要根据第18章中的内容确认Pod恢复所需的时间。
[ ] 安全审查通过
需要完成第16章中列出的所有安全检查项。
[ ] 费用记录更新完毕
确保infra/aws/COST.md和infra/gcp/COST.md文件中的费用信息是最新的,此时两种云服务的计费系统应该都已经开始生效。
回滚测试会生成哪些输出信息
当前的流量分配比例已经是100/0,因此第一步并不会产生任何变化。第二步才是真正的测试环节,其执行结果如下:
== 流量分配比例已更新 ==
"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点
整个团队都参加了这次电话会议。Chris在第二个屏幕上打开了支持请求邮箱,Amara则打开了交易记录表格,Daniel则坐在你旁边,打开了负载均衡器的页面。
Maya说:“将1%的流量分配给GKE,密切关注出现的错误和数据传输情况。如果发现任何异常,立即将所有流量重新分配回AWS。”
为什么选择1%的流量?
之所以选择1%的流量进行测试,是因为这样就可以轻松地发现一些潜在的问题,比如JSON数据格式不正确、路由设置错误,或者某侧系统的登录功能出现故障等等。而使用1%的流量完全不足以用来测试系统容量,这部分内容将在第22章中讨论。
操作步骤
./scripts/shift-traffic.sh 99 1
该脚本会重新应用负载均衡器URL映射中的配置,从而调整流量分配比例。这个过程中不会涉及DNS地址的更改,也不会进行任何部署操作。
接下来,请将新的流量分配比例复制到infra/gcp/terraform/08-edge/terraform.tfvars文件中:
aws_traffic_weight = 99
gke_traffic_weight = 1
该脚本会通过命令行参数传递这些权重值。在`08-edge`环境中,接下来的`terraform apply`命令会读取这个文件。如果文件中显示的权重值仍然是100和0,那么执行这个命令后,流量就会被重新分配回AWS。
请确认文件中的配置与实际运行状态是否一致:
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`、`gketraffic_weight = 1`,以及`"AWS: 99% / GKE: 1%"`。如果文件中仍然显示100和0,那么请立即进行修改。
检查:是否有请求到达了GKE?
当权重配置仅为1%时,几乎所有的请求都会被发送到AWS。连续40次请求都显示“aws”作为处理平台也是正常的。继续执行这个测试,直到有一次请求的处理平台显示为“gke”为止:
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地址发送测试请求。这个操作并非基于客户的实际需求:
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。在这个实验中,我们得到的响应结果是:
{"status":"ok","served_by":{"platform":"gke","version":"0.0.0-unset","instance":"clearpay-api-6f9448c857-mj2j6","commit":"unset","rollback_active":false}}
你的系统中“instance”的具体值可能会有所不同。但如果响应结果中显示的“platform”是AWS,或者curl请求失败了,那就说明GKE并没有正常工作。在这种情况下,千万不要调整权重配置。
检查:通过公共地址进行的登录操作是否能够成功?
从现在开始,可以模拟这样一个场景:某个客户的第一次请求会被发送到AWS,而第二次请求则会被发送到GKE。这两种处理方式都必须能够正确地接收并回复同一条请求。如果出现差异,最好在权重配置仅为1%的时候就发现这个问题,而不是等到权重配置达到50%之后才去排查。现在就通过公共地址尝试进行登录操作:
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":"alice@clearpay.dev","password":"password123"}' \
"https://$HOST/api/auth/login"
在回复中查找"token"和"user"这两个关键词。如果看到了internal_error这个提示,就立即停止操作,不要继续增加权重值。检查:像普通客户一样使用该应用
打开https://api.clearpay.test,忽略证书警告并登录系统。查看余额,然后发送一笔小额交易。在“最近活动”列表中,每条新记录都会显示“由aws/1.0.0提供服务”或“由gke/0.0.0-unset提供服务”。只有最新的记录才能反映当前的请求情况;较旧的记录会保留其生成时的信息。
顶部的黑色条形几乎每次都会显示“AWS”以及相应的百分比数值(例如1%)。请关注新出现的活动记录,而不是那个条形图。
快速查看synthetic-health.sh终端窗口中的内容,每条记录都应该显示状态码“200”。
决策
如果循环中至少出现过一次“gke”这一选项,登录后能够获取到token,并且系统检测结果显示状态码为“200”,则进入第22章继续操作。
如果出现其他异常情况,请立即停止当前操作并进行调查。切勿擅自调整配置参数。如果客户因此受到影响,请执行rollback.sh脚本,然后进入第23章进行处理。
完成条件:
在文件以及current_traffic_split配置中,权重比例必须为99/1。
循环系统中至少应有一次显示“platform:gke”这一信息。
通过公共地址登录系统后,能够成功获取到token。
第22章:将流量比例提升至100%
时间安排:周二上午11:30至周三下午
之前将流量比例设定为1%时,意味着每100个请求中只有1个会被路由到GKE。现在你需要逐步增加这个比例——每次只调整一点点,并在每一步操作之前仔细检查相关数据。
每次检查以下三项内容:
性能指标:应用响应速度以及失败请求的数量。
副本数量:当前正在运行的API副本个数。
连接资源状况:数据库是否还有足够的资源来处理所有的连接请求。
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仍然处于正常运行状态,那么数据库能够同时处理多少个连接请求呢?”
每个API进程最多可以建立DB_POOL_SIZE个连接,默认值为10个。连接池会根据负载需求动态分配连接数,因此在流量较少的情况下,这个数字会保持在较低水平。当系统达到满负荷状态时,每个进程都可以使用全部10个连接。数据库本身也设有一个上限值max_connections,一旦总连接数超过这个数值,新的连接请求就会被拒绝,现有请求也会出现等待现象,用户会遇到超时错误。
要了解自己数据库的具体限制,请在AWS服务器上运行psql命令。这一操作的方法在第5章中已经介绍过:
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;'"
对于GKE环境,其连接上限可以通过文件k8s/overlays/production/patch-hpa.yaml中的maxReplicas字段来获取。然后请根据实际情况填写下表:
RDS最大连接数: ___
AWS,1个进程×10个连接: 10
GKE,最大副本数×10个连接: ___
最坏情况(两者同时运行): 10 + ___ = ___
以下是本实验中填写好的示例表格,你可以根据自己的实际情况进行对照:
RDS最大连接数: 81
AWS,1个进程×10个连接: 10
GKE,最大副本数×10个连接: 30 (3个Pod×10个连接)
最坏情况(两者同时运行): 10 + 30 = 40
由于40小于81,因此你的系统还有提升处理能力的空间。如果你的RDS实例规模不同,max_connections的值也会相应变化。关键在于,即使在最坏的情况下,系统的连接数也不应超过这个上限。
现在,请对规模更大的环境进行同样的计算。对于那些拥有更强大服务器的公司来说,他们可能会使用3到8个Pod来运行HPA机制,此时总连接数将为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获得的分配比例
1
./scripts/shift-traffic.sh 90 10
90%
10%
2
./scripts/shift-traffic.sh 75 25
75%
25%
3
./scripts/shift-traffic.sh 50 50
50%
50%
4
./scripts/shift-traffic.sh 25 75
25%
75%
5
./scripts/shift-traffic.sh 10 90
10%
90%
6
./scripts/shift-traffic.sh 0 100
0%
100%
请每次只运行一行命令,切勿同时执行所有命令。
每个阶段都需要进行的检查
1. 分配比例是否发生了变化。脚本的输出结果仅代表Terraform的设置情况,并不能作为实际分配比例的证明。请确认文件内容是否符合以下要求:
grep traffic_weight infra/gcp/terraform/08-edge/terraform.tfvars
terraform -chdir=infra/gcp/terraform/08-edge output current_traffic_split
2>实际请求量是否与分配比例一致。在分配比例调整后等待三分钟,然后统计有多少请求被处理:
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的分配比例增加,其处理的请求数量也应相应增加。40个请求只是一个大致样本,并不能代表实际的百分比分布情况。谷歌也需要几分钟时间才能在所有系统中应用新的分配规则,因此如果过早进行统计,得到的结果仍会是旧的比例分配情况。在本实验中观察到的数据如下:
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
如果十分钟后统计结果仍然与预期不符,请联系谷歌咨询当前的实际分配比例。
gcloud compute url-maps describe clearpay-migration-map | grep -B2 -A2 weight
当分配比例为0 / 100时,clearpay-gke-backend对应的重量值为100,而AWS对应的重量值显示为“0”,因为谷歌在计算时会忽略这个值。如果实际分配比例与文件中的设置不符,请在08-edge环境中再次运行terraform apply命令。
3>确认GKE服务是否正常运行。
kubectl -n production get pods -l app=clearpay-api
kubectl -n production get hpa clearpay-api
Pod们目前处于运行中状态,已经准备就绪。HPA图表显示了当前的CPU使用率与70%的目标值之间的对比情况,同时还展示了当前的副本数量。在这个实验环境中,副本数量始终为1个,而CPU使用率则介于4%到7%之间。
4. 登录功能仍然可以正常使用。在每个测试阶段,都请再次运行第21章中提到的登录测试脚本。只有在对特定类型的流量组合进行测试时,才可能出现异常情况;如果连续出现5次失败,应立即停止测试,而不要继续上报错误信息。
5>出现的错误都必须保持在规定的范围内。synthetic-health.sh脚本运行后的输出结果应该仅为200。如果出现其他数值,应立即停止测试,并且不要上报错误。
6>关于用户界面部分……请登录https://api.clearpay.test,尝试进行资金转账操作,然后查看最新的served by信息。
每个阶段都需要做出的决策
每次遇到这种情况时,Maya都会问同一个问题:“我们是否已经准备好进入下一个步骤?”
请用具体的数据来回答这个问题: “文件中设置的权重值与实际情况相符;GKE所处理的请求量也有所增加;Pod们运行正常,登录功能可以正常使用,合成健康检测的结果也是200;即使在最坏的情况下,连接数也不会超过数据库允许的最大限度。”
如果有任何检查项失败,请不要上报错误。但如果因此影响了客户的使用体验,那么请立即进入第23章进行进一步的处理。
完成测试的条件:
文件中以及current_traffic_split配置中的权重值应为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副本也仍然保持着它们的连接。”
他放下了笔。
“数据库是有容量限制的,但我们不断增加接入它的连接数。”
“当达到这个限制时,会发生什么?”你问道。
“请求开始等待连接,然后就会超时。”
他看着你。
“应用程序本身并没有出现故障。如果Pod在运行中,健康检查也通过的话,问题就出在容量上——我们对数据库连接的需求超出了它的供应能力。”
克里斯又看了看部署情况。
“所以你们在监控新环境,但实际上数据库却在同时处理两个环境的数据。”
“没错。”
丹尼尔再次看了看那张图。
“这就是我们忽略的地方。我们计算了新环境所需要的资源,但却没有考虑当两个环境都在运行时,数据库实际上需要多少资源。”
他重新坐了下来。
“最终,我们把流量重新调回了旧环境,问题也得到了解决。但这次经历一直萦绕在我的心头。”
你看着GKE的部署情况问道:“现在如果你再遇到这种情况,你会怎么做?”
“在调整流量之前,我会先仔细计算各项数据。”
丹尼尔把笔记本电脑转向了你。
“你们的实验室环境很安全。你们使用了3个副本,而数据库的连接限制只有81个,所以工作负载根本不会达到使数据库不堪重负的程度。”
他打开了另一份笔记。
“但我的情况不同。我们当时使用了多达8个副本,而数据库允许的最大连接数是87个。”
他看着你说道:“那么,我们就用这些数据来进行分析吧。”
你把椅子拉得更近了一些,丹尼尔继续解释。
“我会按照事件发生的顺序来描述整个过程。你们现在处于值班状态,不要以为自己已经知道最终的结果。每当有新的事件发生时,请告诉我你们会检查哪些内容,以及你们认为当时发生了什么。”
他打开了终端窗口。
这次测试开始时,两个环境中都有8个API副本可用,而数据库的连接限制仍然是87个。
这一次,这些数字就显得非常重要了。
如果你正在使用这个环境进行实验,请先阅读这一章。你的实验室没有遇到任何问题,所以也没有什么需要修改的地方。但如果你想确认自己确实没有问题,那就按照第2步中的指令运行这两条命令,并将结果与“你自己的集群”进行比较。如果日志中没有显示任何错误,且HPA的值设置为1个副本,那么你就没问题了。接下来可以继续阅读第24章吧。系统当前的表现
在开始进行50/50流量分配后的最初几分钟内,系统运行情况看起来还是正常的。但随后响应时间开始逐渐增加。
Amara首先在团队频道注意到了这一现象。
“有几次数据传输操作出现了超时现象。”
Daniel打开了生产监控界面。此时API已经扩展到了7个副本节点,而p95延迟值也在不断上升。
Chris还收到了两封客户反馈,称数据传输速度很慢。
Daniel再次查看图表:“这种情况是不是影响到了所有的请求呢?”
“还不确定,”Chris回答,“目前只有少数几次传输出现了问题。”
Amara检查了支付记录,以确保这些超时现象没有导致重复收费的情况发生。
Daniel观察数据库指标,心想:“也许问题出在数据库上……”
但现在还无法确定具体原因。日志中并没有显示“数据库连接数不足”这样的信息。最初的迹象只是请求处理时间比几分钟前变长了而已。
也没有任何错误提示表明“数据库出现故障”。首先能看到的现象仅仅是响应速度最慢的那些请求的处理时间进一步延长了。只有当连续多次健康检查都失败后,应用程序的状态栏才会显示“系统性能下降,正在排查中”,因此一次缓慢的请求并不足以触发这种提示。
你已经了解的信息
当前流量被分配到了两朵云服务器上,但延迟值却超过了AWS规定的基准水平。状态栏是在多次检测失败后才发生变化的,并非突然出现异常信号的。
你还不知道的事情
目前还不清楚问题是出在负载均衡器、Pod容器、数据库上,还是部署过程中出现了错误。因此不要妄下结论,先收集更多信息再进行分析。
步骤1:停止当前的流量分配操作
在弄清楚问题原因之前,不要再运行shift-traffic.sh脚本来调整流量分配。请记下你发现这一现象的具体时间。
在采取任何行动之前,请先记录下当前看到的所有信息:具体的错误提示、实际的副本节点数量以及准确的时间戳。
每花费一个小时来进行诊断,就意味着要额外承担双倍的系统运行成本。这个费用必须被计入总成本中。千万不要因为大家的注意力都集中在其他地方,而忽略这一项成本。
步骤2:收集相关证据
查看API产生的错误信息:
kubectl -n production logs deployment/clearpay-api --tail=100 | grep error
重点关注与数据库相关的错误信息,比如“连接失败”、“连接数过多”,或者在从数据库池中获取数据时出现超时现象。如果错误代码是5xx系列,且没有提到数据库的问题,那么问题可能出在网关设备或部署配置上。
查看Kubernetes当前正在运行的Pod容器数量:
kubectl -n production get hpa clearpay-api
将REPLICAS的值与maxReplicas的值进行对比。如果副本节点数量在不断增加,那就说明系统负载正在增加——这只是一个线索,还不是根本原因。
最后,请根据当前的实际数据填写第22章中的表格。
RDS的最大连接数:87
AWS:1个进程×10个连接:10
GKE:7个副本×10个连接:70
当前数值:80,接近上限值
当GKE的副本数量为8时:80;最坏情况下为90,超过了上限值
自建集群:50/50的比例配置
在认为自己的集群已经达到理想状态之前,请先在自己的集群上运行以下两条命令。这个在AWS和GKE各使用50%资源的实验表明:
HPA设置:CPU使用率为4%/70%,最小Pod数量为1个,最大Pod数量为3个,副本数量为1个;
当前运行的Pod数量为1个;过去100条日志中没有出现任何错误信息;
RDS的最大连接数为81个,clearpay数据库上同时有2个会话在运行,系统状态正常,未出现降级现象。
最坏的情况下,CPU使用率也会降至40%左右,但仍然低于81%的限值。除非你的配置数据确实超出了第6章中规定的限制,否则不要执行rollback.sh脚本。
如果HPA设置仍然是1,且日志中没有异常信息,请将这一情况记录下来。这就是证明系统正常运行的证据,请继续保持这种配置状态。
故障是如何逐渐形成的
单独使用AWS或GKE是不可能出现这种情况的。只有当两者都处于满负荷运行状态,且迁移方案刻意让它们继续同时运行时,才会发生这种问题。
请告诉Daniel:“副本数量增加了,资源分配情况已经接近数据库的限值了。这个问题与Kubernetes本身的配置无关,而是由整体资源容量决定的。”
步骤3:执行回滚操作
Maya问:“你有什么建议?”
请将当前的参数值与第6章中规定的限值进行比较。现在不要随意设定新的限制值。
“把客户数据重新迁回AWS吧。当前出现的错误已经超出了我们的容忍范围,客户也会受到影响,而且我们已经测试过回滚操作是否可行。”
Maya同意道:“好的,那就这么做吧。”
./scripts/rollback.sh ~/.ssh/clearpay-deployer
上面的示意图展示了rollback.sh脚本执行的两个步骤:首先,shift-traffic.sh命令会将所有客户流量重新导向AWS,同时切断与GKE的连接;其次,该脚本会通过SSH连接到AWS的EC2服务器,在API的环境配置文件中设置ROLLBACK_ACTIVE=true,然后重启API服务。这样一来,系统就会将AWS识别为当前的活动平台,并在ClearPay的用户界面中显示“已回滚至AWS”的提示信息。这些操作能够恢复正常的流量传输,并让操作人员清楚地看到回滚操作已经成功完成。
请将08-edge配置文件中的terraform.tfvars值重新设置为100和0,这样后续的配置应用就不会撤销之前的回滚操作了。
步骤4:确认回滚操作是否成功
curl -k https://api.clearpay.test/api/info
查询结果显示platform的值为aws,rollback_active的值为true。刷新用户界面后,会看到红色的“已回滚至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个连接为例:GKE提供的8个连接加上AWS提供的10个连接,总共是18个连接,这个数字仍然在87的范围内。
在紧急情况下,你可以立即执行以下命令来修改设置:
kubectl -n production set env deployment/clearpay-api DB_POOL_SIZE=6
不过,这种即时修改不会留下任何记录,下一次部署时会自动恢复原来的设置。因此,建议通过正常的流程来进行更改:在文件k8s/base/deployment.yaml中设置DB_POOL_SIZE的值,然后提交并推送到main分支。接下来,使用管道工具来构建、测试并部署新的配置。在进行压力测试时,要确保负载足够大,以便让HPA达到其最大处理能力。如果某种解决方案只在轻负荷情况下才能正常工作,那么它就没有任何实际意义。最后,通过第19章中描述的流程来完成最终的部署工作。
步骤6:恢复操作
Maya:“我们准备好执行10%的流量转移了吗?”
“准备好了。API运行正常,模拟测试结果也符合要求,错误数值都在允许范围内,资源池的配置也处于上限状态,回滚机制也已经过测试。”
Daniel:“同意。”
Maya:“最终决定权在你。”
./scripts/clear-rollback-flag.sh ~/.ssh/clearpay-deployer
刷新用户界面,回滚提示会消失。如果保留该标志,恢复操作后这个提示仍然会显示,这样人们就会忽略它。
./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指标以及错误日志。正是在这个阶段,修复措施的效果才能得到验证。
./scripts/add-to-cost-ledger.sh
需要将这次流量转移操作所产生的费用单独记录下来。因为再次进行流量转移意味着两朵云都会产生相应的费用,所以应该将这些费用分开统计,而不能将其合并为“迁移成本”这个数字。
本章所涉及的技术技能
上图展示了从观察问题到恢复正常的整个事故响应流程。首先检查延迟、状态栏和错误日志,然后提出假设并通过数据库连接限值来进行验证,同时用实时数据作为证据。如果客户受到了影响,就必须立即停止流量转移并执行回滚操作;通过部署流程解决根本问题,在最大预期负载下测试修复措施的有效性,只有当所有证据都表明修复成功时,才能继续进行迁移操作。
处理事故并不是在寻找答案,而是要不断减少不确定性,直到能够做出安全的决策。如果客户的服务出现了故障,那么最安全的做法就是立即停止操作并执行回滚;如果客户的服务没有受到影响,那么就继续观察情况。不要因为某个数值看起来不正常就执行回滚操作,也不要因为Pod的状态看起来正常就继续进行流量转移操作。
GKE目前已经能够提供100%的服务能力。
AWS如今已经是一种有定价机制的“安全网”,而不是你遗忘掉的东西。第7部分:如何完成迁移工作
GKE能够满足所有客户的需求。而AWS仍然在运行中,也会继续产生费用,作为一种备用方案。这一部分会将迁移完成的各项数据转化为有形的证据,为这一“备用方案”设定一个明确的截止时间,并移除那些不再需要的内容。
第24章:用数据证明迁移工作的完成
星期四上午9:00
玛雅在10点与公司的高层领导们开会。她路过你的办公桌时,笔记本电脑的屏幕上显示着空白页面。
玛雅:“领导们没有参加我们的任何会议,但他们肯定会问一个问题:客户是否注意到了这次系统迁移?我需要一份简短的总结报告,可以展示在屏幕上。请告诉我:共有多少请求成功处理了,有多少失败了?与AWS相比,应用程序的响应速度如何?有没有人发现任何故障现象?能在10点之前把报告准备好吗?”
你点了点头。她要求的所有数据其实都可以从你已经编写好的两个脚本中获取到。
操作步骤
首先运行这两个测试脚本,获取在GKE环境完全接管后、针对公共地址的实际测试结果:
HOST=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname)
./scripts/promotion-gate.sh "https://$HOST" \
alice@clearpay.dev password123 bob@clearpay.dev
然后使用第6章中设定的参数,运行load-test.sh脚本对https://$HOST进行测试,并将测试结果保存到报告文件中。同时,还需要将第22章中的统计数据以及synthetic-health.sh日志中的信息也添加到报告中。
证据
以下是本次实验得出的测试结果表格。你的实际数据可能会有所不同:
生产环境下的请求处理情况(统计的是具体测试次数,而非仪表盘显示的总量):
0分钟时:40次请求,其中3次在AWS上成功处理,37次在GKE上成功处理;
90秒后:40次请求,全部在GKE上成功处理;
运行load-test.sh脚本10次,每次测试20秒,结果为:成功310次,失败0次;
运行promotion-gate.sh脚本后,有5次请求成功处理,0次失败。
因此,总共共有310 + 40 + 5 = 355次请求成功处理,失败次数为0。
5xx错误率:0.0%;
GKE环境下的平均响应延迟为647毫秒,而第6章中给出的AWS基准值是164毫秒;GKE的测试环境(使用端口转发功能)下的平均响应延迟为1157毫秒。
客户可见的系统故障时间:0秒。
由于本实验中没有使用本地Prometheus监控工具来收集数据,因此这个表格中显示的是具体的测试次数。请不要凭空编造一些从未测量过的统计数据。
在将这些数据与第6章中规定的指标进行对比之前,请先确认GKE环境出现之前的各项限制条件是否仍然有效:
故障限制:5xx错误率绝对不能超过规定的上限。本次实验中,没有一次请求失败。
响应延迟限制:GKE环境的平均响应延迟必须低于规定的上限。本次实验中测得的647毫秒低于1500毫秒的上限,因此符合要求。
仅仅达到标准并不意味着一切都成功了。647毫秒这个数值大约是AWS所记录的164毫秒的四倍。虽然客户并没有感受到任何服务中断,但每个请求的处理速度确实变慢了。这一点应该被如实记录下来,而不是被隐藏起来。本书的最后一部分会解释其中的原因。
为什么统计表格上从未显示“零次失败请求”
任何真正能够连续运行数小时、在不同的业务高峰时段正常运作,且有可能发生系统回滚的系统,都不可能诚实地做出这样的承诺。如果声称系统永远不会出现故障,那么就会削弱那个最重要的承诺:即客户不会感受到任何服务中断。
在您事先设定的容忍范围内出现少量的失败请求,实际上就意味着“系统运行得很好”。这种标准是可以被监控和测量的,但并不完美。
何时可以认为任务完成:
表格中的每一行数据都应该代表您实际测量得到的数值。
每一行数据都需要与第6章中规定的各项标准进行对比,包括那些失败的交易记录。
第25章:在GKE上运行系统,并为AWS的使用设定具体日期
周五上午10点
Maya从会议中回来后,领导们向她提出了一个问题。
Maya问道:“那么,对于客户来说,究竟发生了什么变化呢?”
你仔细查看了各项数据:所有请求都通过GKE平台进行处理,传输过程也一切正常,各种测试也都通过了。PostgreSQL数据库仍然运行在AWS上,而EC2服务器则根本没有被使用。
“地址没有发生变化,也没有任何人感受到服务中断。”
Maya说:“那么这个系统现在就属于你们了。我需要知道什么时候会关闭那台旧服务器。”
第一步:观察系统的实际运行情况
在做出任何决定之前,先让系统在实际环境中运行一段时间。对于一家真正的企业来说,这个“一段时间”至少应该是几天,而不仅仅是最后一个工作班次结束后的几分钟。
在这个实验中,系统仅在实际环境中运行了短短的几分钟,各项指标也都符合标准(失败请求数为0)。这一点应该被记录下来。毕竟,周末进行的实验并不能代表系统在正常工作环境中的表现。
第二步:检查警报机制
在第7阶段中,我们创建了名为ClearPay API - elevated error rate的警报规则。当五分钟内出现超过五条错误记录时,这个规则就会被触发。但由于没有设置通知渠道,因此没有人会收到这条警报。如果警报根本无法到达相关人员手中,那它就失去了存在的意义。
对于真正的生产环境来说,应该在Cloud Monitoring中添加通知渠道,并在测试环境中触发警报机制,而绝不能让错误信息真正发送给客户。在这个实验中,我们需要记录下这个缺陷:虽然警报规则确实存在,但没有人会收到任何通知。
第三步:审查系统容量配置
请查看这段时间内系统的HPA配置情况:
kubectl -n production get hpa clearpay-api
在这个实验中,即使在100%的负载测试环境下,系统也只使用了1个副本,CPU利用率仅为4%到7%。实际上,只需要1个Pod就足够了。为了一个API Pod使用3台e2-medium型服务器,这样的配置显然过于浪费资源了。
谷歌提供了“承诺使用折扣”政策:用户如果承诺在一年内或三年内持续使用一定量的计算资源,就可以享受更低的价格优惠。
只有在你拥有稳定的使用记录之后,才应该进行正式的部署。如果基于你规划好的架构而不是实际的使用情况来决定是否进行部署,那么这种决策其实只是一种带有合同约束的猜测。在这个实验中,我们没有购买任何相关服务,因为仅仅使用GKE几个小时根本算不上是真正的使用记录。
步骤4:编写交接文档
这份交接文档应该是为那些没有参与迁移过程的人准备的。以下是本实验版本的交接文档示例:
日志记录:kubectl -n production logs deployment/clearpay-api
控制面板地址:http://localhost:3000 if observability/docker-compose.yml
如果该服务处于运行状态,GKE相关的信息可能会显示为空。
警报信息:Cloud Monitoring会提示“ClearPay API的错误率升高”,但此时没有通知渠道可供使用。
回滚方案:仅当EC2服务器仍然存在时,才执行scripts/rollback.sh脚本。
主机名:terraform output public_hostname
以及terraform output static_ip_address
(请使用你自己的实际主机名,而不是从本书中复制的名称)
步骤5:在AWS上标注具体日期
scripts/rollback.sh脚本仅在EC2服务器存在的情况下才会被执行,而且该服务器每天都会产生相应的费用。你需要确定服务器应该保留多长时间,明确保留的原因,并计算相关的成本:
AWS的每日费用:约1.25美元/天(详见infra/aws/COST.md文件)
保留期限:___天
退役日期:___
总成本:___天 × 1.25美元/天 = $___
在这个实验中,我们没有额外延长服务器的保留时间,而是在确认GKE服务能够100%正常运行后的同一天就立即退役了AWS服务器,因为这是一个用于测试和清理环境的实验。
在实际工作中,企业可能会选择将AWS服务器保留一到两周。较短的保留期限可以节省成本,而较长的保留期限则能增加系统的可靠性。但错误的做法是仅仅因为“以防万一”而让服务器持续运行,这样不仅会每天产生费用,而且也不会有人再去重新评估是否真的需要继续使用它。
请在规定的日期之前就制定好退役方案,这样才能避免在最后关头迫于时间压力而做出错误的决定:
服务器必须保持100%正常运行指定的天数数,且期间不能执行任何回滚操作
在规定的保留期内,如果因为GKE导致任何问题发生,也不允许触发任何警报
必须有指定的人员对退役方案进行最终确认并签字
本实验的规则:权重值为0/100,通过率为5/5,负载测试结果为失败次数=0,自动扩展机制的设置为1,且不允许执行任何回滚操作。
最后,请检查是否还有其他地方直接指向Elastic IP地址,比如书签、其他系统中硬编码的URL地址,或者在负载均衡器启用之前就已经设置好的外部监控工具。
完成标准:
你已经对生产环境进行了为期指定的实时监控
所有相关的警报设置都已经记录下来
- 已经对系统容量进行了评估,并且决定了最终的处置方案
- 交接文档已经编写完成
- 保留期限、退役日期、成本以及相关规则都已经详细记录下来
第26章:退役AWS服务器
星期五,下午3:00
规定的日期已经到来,并且所有规则也都得到了满足。现在可以删除EC2服务器了,但数据库需要保留下来——因为它从来就不是这次迁移的一部分。
步骤
请从仓库根目录开始进行最终检查。GKE必须能够通过使用公共地址进行的检测:
HOST=$(terraform -chdir=infra/gcp/terraform/08-edge output -raw public_hostname)
./scripts/promotion-gate.sh "https://$HOST" \
alice@clearpay.dev password123 bob@clearpay.dev
然后确认Terraform确实记录了您想要删除的服务器:
cd infra/aws/terraform
terraform state show aws_instance.api # 确认您选对了目标资源
接下来,使用cd ../../..返回到仓库根目录,然后仅删除AWS计算资源:
./scripts/teardown-aws-compute.sh
# 在阅读计划内容后,输入--yes以启用自动批准功能。
完成这一步之后,AWS上的相关资源就会被彻底删除。这就是您在第25章中所做的决策所带来的结果。
阅读销毁计划
该脚本只会删除以下内容:
EC2实例
Elastic 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
办公室里已经没有人了,但还有两项服务仍在按小时计费。
这里是一个实验环境。等你的记录完成后,把所有还在产生费用的服务都移除:包括Gateway负载均衡器、GKE实例、Cloud NAT、注册服务以及RDS数据库。
在拆除任何基础设施之前,请先完成第28章中的迁移记录填写工作。该章节要求你记录最终的架构配置、迁移结果、性能数据以及总成本。如果你还需要从正在运行的系统中获取相关数据,现在就赶紧收集吧,因为一旦销毁了这些基础设施,那些实时数据就无法再获取到了。
操作步骤
在仓库根目录下运行以下命令:
./scripts/teardown-gcp.sh
# 输入“--yes”可以跳过提示信息,并自动执行所有步骤。
脚本的功能如下:
删除Gateway以及生产环境和测试环境中的相关工作负载。Kubernetes会为Gateway创建相应的负载均衡器资源,但Terraform并不知道这些资源的存在。因此,先删除Gateway可以让Google系统自动处理这些资源,否则Terraform可能会在尝试删除它自己并不创建的资源时遇到问题。
以相反的顺序执行`terraform destroy`命令:08-edge → 07-observability → … → 01-foundation。由于后续步骤依赖于前面的步骤,因此必须先删除前面的资源。
在脚本的提示界面输入“yes”,然后仔细阅读并确认每一条Terraform命令。
命名空间可能会在“Terminating”状态中停留几分钟,脚本会为每个删除操作等待最多180秒,之后才会继续执行下一项任务。无论如何,最终都会删除这些命名空间和相关的资源。
接下来删除数据库。虽然我们在迁移过程中有意将数据库迁移到AWS上,但实验环境不应该继续为此支付每天约0.80美元的费用:
cd infra/aws/terraform
terraform destroy
最后,从`/etc/hosts`文件中删除api.clearpay.test这条记录。
本书并不会自动创建或删除你的GCP项目,请只有在该项目仅用于实验目的时才进行删除操作。
完成标准
运行`kubectl get ns`后,系统中不再显示staging或production命名空间,或者相关集群已经完全被删除。
所有infra/gcp/terraform/0*-*阶段的操作都已经完成。
`terraform destroy`命令在`infra/aws/terraform`目录中执行完毕。
GCP和AWS的计费系统中不再显示与这个实验环境相关的任何费用。
至此,所有工作都已经完成。
在忘记之前,请把实际发生的情况,包括各项费用的详细信息记录下来。
第28章:编写迁移记录
第四周,周一,上午9:00。
在细节逐渐被遗忘之前,把发生的事情、架构设计、相关配置以及你认为应该做出的修改记录下来。
第一部分:架构设计记录
迁移前的情况:
用户 → nginx (TLS) → 由systemd管理的Node进程 → RDS Postgres数据库
只有一台EC2服务器,一个数据库,使用的是默认设置的网络配置,敏感信息被保存在普通的.env文件中,没有自动扩展机制,部署过程也是手动完成的。
迁移过程中的变化:
迁移后的结果:
接下来,针对每个部分再简单说明一下变化情况:
流量分配:通过全局外部应用负载均衡器来分配请求流量,具体配置由shift-traffic.sh脚本控制。没有使用DNS进行路由,因为缓存过的DNS响应会导致回滚操作变慢。
部署流程:每次提交代码后,都会通过GitHub Actions进行构建、测试并部署到预发布环境。只有在通过审核并获得批准后,才会将预发布环境的镜像推送到生产环境。
数据库:没有进行任何迁移操作。第4章中使用的那台RDS实例始终作为数据的唯一存储来源。
安全性
:无论是Pod还是部署流程,都使用了Workload Identity机制来管理身份认证信息。任何时候都不会下载服务账户密钥,所有敏感信息都通过Secret Manager进行管理。
监控与日志记录
:API本身会生成JSON格式的日志,同时/metrics接口也可以用来获取相关数据。在AWS和GKE环境中,监控方式是相同的;此外,Cloud Logging还提供了基于日志的错误统计功能。
回滚机制
:有一个专门的脚本用于调整流量分配权重,并在AWS上设置相应的标志位。这个脚本在首次进行流量切换之前就已经经过了测试。
第二部分:事后总结
你们四个人一起回顾了这些记录。Maya首先提出了一个大家都预料到的问题:
Maya:“问题出在Kubernetes上吗?”
“不完全是。Kubernetes确实按照我们的要求进行了操作。真正需要关注的是数据库。每个API实例最多可以建立10个连接,GKE可以运行多个这样的实例,而AWS也能同时为大量客户提供服务。把这些因素加在一起,就会接近数据库的承载极限。
在这个小型集群中,我们最终建立的连接数是40个,而数据库的最大允许连接数为81个,所以没有出现任何问题。但如果将集群规模扩大到可以运行8个实例的程度,同样的计算结果就会超过限制。”
Daniel点了点头:“我在上一家公司就是忽略了这一点。所有数据其实都在那里,但没有人把它们放在一起对比分析。而你们在开始调整流量分配之前就做了这样的分析。”
阿玛拉:“那么这些支付操作呢?每次转账都是收取一次费用吗?”
“是的。第三章中提到的幂等性机制在整个迁移过程中都得到了有效应用。我们甚至用100%的GKE环境重新进行了测试,结果也没有出现第二次收费的情况。”
玛雅关上了她的笔记本电脑。“那么下次我们应该做哪些不同的事情呢?”
请记录以下内容:
成功的方面:
在GKE出现之前就设计好的幂等性机制,确保了客户重新尝试迁移时能够使用不同的云环境,而不会遇到问题。
分阶段进行的Terraform部署使得每次失败都能准确地定位到问题的发生环节。
通过构建自动化测试流程,确保生产环境始终使用那些通过所有验证阶段的代码版本。
在第20章中我们就对回滚机制进行了测试,这样在实际需要使用时就能顺利应用它。
这次实验中出现的问题:
本地的Prometheus监控工具并未实时监测GKE的运行状况,因此第24章中的统计数据是基于单独的检查结果得出的,并非总体的汇总数值。
当前的警报机制并没有设置任何通知渠道,导致问题发生时无法及时得到提醒。
由于使用的节点池规模较小,无法模拟真实企业会使用的负载情况,因此连接限制风险仅通过理论分析进行了验证,并未在实际环境中进行测试。
GKE的延迟值大约是AWS的四倍。
根本原因:两个看似合理的配置选项——《DB_POOL_SIZE=10》以及HPA设置中的《maxReplicas》——被分别应用于不同的系统模块中。直到开始同时在两种云环境中进行测试之前,没有人真正检查过这些配置是否超出了数据库的允许范围。一旦找到了问题所在,解决方法其实并不复杂。但找到问题的关键在于要像进行真正的调查一样仔细分析每一处细节,而不能直接跳到答案上去。
所做的决策及其带来的权衡:
选择使用GKE Standard而非Autopilot:虽然可以完全控制节点池和自动扩展功能,但同时也需要自己负责配置这些参数。这次实验的结果是浪费了三台几乎处于闲置状态的机器资源。如果使用Autopilot,或许这种连接限制风险会被其调度机制掩盖起来,不过这也会带来其他未知的后果。
仅建立两个测试环境而非五个:这样可以减少开发和维护的成本,但缺点是测试环境需要共享生产环境的数据库。对于这种规模的项目来说这个方案还可以接受,但对于某些企业来说可能并不合适。
使用公共数据库端点并设置IP允许列表,而不是使用VPN:这样无需进行复杂的配置操作,也不会产生每月的VPN费用。不过缺点是RDS的公共端点虽然只允许一个IP地址连接,并且需要使用TLS协议进行加密通信,但这种方式确实更加简单方便。
将数据库留在AWS平台上:这种迁移方案更为简单、安全,但所有查询操作都必须在两种云环境之间进行传输,从而增加了网络延迟和成本。
那么在第二次进行迁移时,我们应该做哪些改变呢?在开始任何数据传输之前,首先需要计算出所有共享数据库的环境中可能出现的最大连接数,并将其与数据库的实际限制值进行对比。这个步骤应该被纳入到迁移计划中,而不是等到实验过程中才去发现这些问题。
第三部分:成本分析
这次实验的花费:
预算:大约25美元,用于五天的GCP使用费用。
实际支出:总共约4.30美元,其中AWS费用为1.25美元,GCP费用为3.05美元。由于使用了免费试用额度,GCP的实际花费为0美元。迁移工作按计划进行,没有出现任何意外情况,也不需要回滚操作或增加额外的工作量。
集群的资源配置过于充足:三个GKE节点被用来支持一个API Pod,而这些节点的CPU使用率仅为4%到7%而已。
之所以选择标准的GKE方案,是因为它具有较高的教学价值;对于这种工作负载来说,使用Autopilot反而会更便宜一些。
没有购买任何额外的计算资源。
结论:迁移工作的成本完全控制在预算范围内,但GKE环境的配置所消耗的资源确实超出了实际工作需要的量。
请根据你的云服务账单填写相应的数值。这本书无法帮你完成这部分工作,而随意猜测数字同样会导致不准确的结果——而这正是整个项目一直努力避免的情况。
完成时间:
迁移前的准备、迁移过程中的操作以及迁移后的结果都与实际执行情况相符。
事后分析清楚地指出了问题的根本原因。
成本核算也使用了真实的数据。
这个项目之后会做什么?
这个项目留下了哪些未解决的问题?
第13章特意提到了这个问题,但并未深入探讨:在什么情况下,在AWS上继续使用PostgreSQL反而会带来更高的成本、更大的风险,以及更多的开发工作量呢?跨云平台的迁移方案、对两个不同供应商的依赖,以及第23章中提到的连接池压力,都属于需要进一步考虑的因素。
请将你的笔记中记录的两个相关数值并列出来:
AWS环境下的测试结果:第6章数据,p95百分位时耗为164毫秒
GKE环境下满负荷运行时的测试结果:第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后端部署了带有加权负载均衡器的网关系统(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上,这是有意为之的。在完成流量迁移、进行回滚测试并最终完成系统切换的过程中,客户使用的地址和账户余额都没有发生任何变化,也没有任何人遇到任何服务中断的情况。现在你应该明白,为什么这个过程会如此复杂,以及为什么647毫秒这样的时间间隔对于确保迁移的顺利进行来说是如此关键了。
实验内容中还介绍了如何验证每个迁移阶段的完成情况、如何分析相关数据,以及如何在发现错误时及时撤销之前的操作。对于那些仍然在使用的系统资源,必须采取相应的措施来避免造成损失。
如果你想了解最新的技术文章、项目实施过程介绍以及即将发布的新的技术资源,请订阅我的时事通讯。
如何在面试中讨论这个项目
我参与过一个名为ClearPay的支付应用程序的云迁移项目。该应用程序原本运行在AWS上,我们的目标是在不影响用户正常使用服务的情况下,将其迁移到Google Cloud平台上。
<主要挑战在于如何安全地完成这一迁移过程。我没有选择一次性全部进行切换,而是设计了一个循序渐进的迁移方案,让流量能够逐步转移到新环境中。如果出现任何问题,我都可以将流量重新引导回原来的系统。此外,我还把数据库部署在AWS上,这样既能降低迁移的风险范围,也能避免带来不必要的麻烦。
<为了确保应用程序在迁移完成后能够正常运行,我制定了相应的部署流程、安全检查步骤以及验证机制。
<随后,我对最终的迁移方案进行了测试。所有测试都顺利完成,没有出现任何错误;在最后的检测过程中,应用程序也没有产生任何服务器故障。不过我也发现新旧环境之间存在性能差异,这些差异还需要进一步研究,才能确认迁移方案已经达到了最佳优化效果。
<这个项目证明了我具备规划并执行云迁移任务的能力、能够有效管理相关风险、验证迁移结果,并能及时发现那些仍需要解决的问题——而不仅仅是因为应用程序能够正常运行就简单地宣布迁移成功。
<<面试提示:>在面试时,请准备好解释自己为何选择使用加权流量路由机制,在系统切换过程中是如何对应用程序进行测试的,是什么导致了系统回滚操作,以及为什么PostgreSQL数据库仍然被部署在AWS上。> 相关文章
如何使用 GitHub Actions、Ingress 以及自动 TLS 功能,在 GKE 上搭建临时性的 PR 预览环境
每一个发布软件的团队最终都会搭建一个测试环境。而每一个搭建了测试环境的团队,最终都会遇到同样的问题。 测试环境是共享的。一个环境,却有大量的拉取请求。有人会部署一个尚未完成的功能来进行测试,其他人会在其基础上继续进行开发,还有第三个人会执行数据库迁移操作。到了周四下午,就没人能弄清楚到底哪个分支的哪个版本正在运行中,“在测试环境中可以正常使用”这句话已经完全失去了任何参考意义。 常见的解决办法包括:创建一个Slack频道,让大家把测试环境当作会议室来使用;制定轮换部署计划;或者再搭建第二个、甚至第三个测试环境,但每个测试环境的配置都会与生产环境越来越偏离。 其实有更好的解决方案。像Vercel
阅读全文
如何阻止人工智能代理生成尼日利亚的邮政编码
2026年10月1日,尼日利亚的邮政编码从六位数字改为由11个字符组成的代码,这种编码能够唯一标识某栋建筑,例如 EK-01-A03-FK-01 。 在此之前训练的语言模型仍然会自信地使用旧格式来处理邮政编码。这会影响两种类型的程序:一种是负责生成验证代码的编程程序,另一种是在运行时从客户信息中读取邮政编码或填写配送表格的程序。 本文将介绍如何解决这个问题。对于那些生成验证代码的程序,你需要检测它们目前生成的代码是否存在错误,并为它们提供相应的规则来纠正这些错误;而对于在运行时处理邮政编码的程序,则需要为它们设定明确的边界:当模型提出一个邮政编码时,由特定的程序来判断该编码是否符合规范、是否已
阅读全文
如何在数据传输流程中对个人身份信息进行加密,同时确保这些信息仍可被检索到
在Intuit,我的团队构建了一个数据处理流程,用于处理TurboTax电子申报系统中产生的数据。这些数据需要被用于制作仪表板、进行分析,以及满足其他后续使用需求。 其中最大的挑战之一就是如何保护那些属于 个人身份信息 的数据,比如社会安全号码。这类信息具有极高的敏感性,因此在数据处理的每一个环节都必须得到妥善保护。与此同时,分析团队有时也需要进行一些正常的业务操作,例如识别与特定客户相关的记录,或者进行精确匹配查询。 那么,如何在保证这些敏感信息不被泄露的同时,允许人们对这些数据进行受控的搜索呢?在本文中,我将解释解决这一问题所涉及的加密技术原理,包括以下内容: 概率加密与确定性加密 对加密
阅读全文
API漏洞的“工程学分析”:深入探讨OWASP列出的十大最常见API安全风险
大多数安全文章读起来都像威胁报告。它们从外部角度描述各种漏洞:攻击者会采取什么行动、这些漏洞会造成什么影响,以及全球范围内有多少系统会受到影响。这样的信息确实很有参考价值,但它们并不能帮助工程师构建更安全的系统。 但这篇文章有所不同。它从内部角度分析OWASP API安全十大漏洞中的每一个:是哪些工程决策导致了这些漏洞的出现,违反了哪些架构原则,以及应该采取什么样的工程标准才能防止这类漏洞被应用到生产环境中。 我在受监管的金融环境中从事过多年大规模生产级应用程序的开发工作。这份列表中提到的漏洞并非理论上的概念,我在实际系统中见过其中的大部分。有些漏洞在我将其投入生产之前就发现了;而有些则在我开
阅读全文