IaC、灰度发布与 PR 预览环境
# IaC、灰度发布与 PR 预览环境
可靠发布要能回答四个问题:这次改什么、谁批准、实际流量去了哪里、失败怎样恢复。CI 显示绿色只是证据之一。
# IaC 与 CI/CD 各自负责什么
IaC 是 Infrastructure as Code(基础设施即代码),用版本化文件描述云资源;CI/CD 负责测试、构建、交付与部署过程。CloudFormation 管理 Stack,SAM 为 Serverless 资源提供简写,CDK 用语言代码生成 CloudFormation 模板。它们不会自动把业务部署变得安全。
已有 CI/CD 基础复用 CI/CD 笔记。下面重点说明原项目的发布约束。
# CloudFormation、SAM 与 CDK 怎么选
IaC 是一种做法,不是某个工具,也不只包含这三种方式。例如要部署一个 Lambda,我们把运行时、内存、权限等要求写进文件,而不是每次在控制台重新配置。文件提交到 Git 后,就能审查谁改了什么,并用同一份定义部署不同环境。
# CloudFormation:直接声明 AWS 资源
CloudFormation (opens new window) 是 AWS 的基础设施部署服务。我们用 YAML 或 JSON 模板声明资源及其关系,它负责按依赖创建、更新或删除资源,并记录部署状态。
一个 Stack(资源栈)就是一起管理的一组资源,例如一个函数及其执行角色。修改模板后,CloudFormation 根据更新内容执行变更,不是每次都把整套环境重新建一遍;但部分属性变化可能要求替换资源,部署前要看变更集。
优点是资源定义直接、没有额外代码生成层;缺点是资源多时模板长,共用配置和重复结构不如编程语言组织方便。适合已有模板体系,或希望直接控制资源定义的团队。
# SAM:简化 Serverless 应用的资源声明
SAM (opens new window) 全称 Serverless Application Model。它在 CloudFormation 模板上提供简写,例如声明一个函数及其 HTTP 事件,就能生成对应函数、API 和调用权限,减少重复配置。
SAM 有两部分:模板语法负责描述资源,SAM CLI负责辅助构建、本地调用、打包和部署。部署时,模板中的 Transform 告诉 CloudFormation 按 SAM 规则展开简写,再创建实际资源。
优点是 Lambda、API 和事件驱动应用写起来简洁,本地调试工具也方便;缺点是大量共享配置和复杂复用仍要组织模板。SAM 不是只能部署 Lambda,同一模板里可以直接写普通 CloudFormation 资源,所以不能仅凭项目有数据库或消息队列,就认为 SAM 不适用。
# CDK:用熟悉的编程语言定义资源
CDK (opens new window) 全称 Cloud Development Kit,可以用 TypeScript、Python 等语言定义 AWS 资源。一个 Construct(构造组件)可以表示单个资源,也可以组合函数、队列、权限等多个资源,供不同环境复用。
cdk synth 运行这些定义并生成 CloudFormation 模板,cdk deploy 再把部署交给 CloudFormation。因此,CDK 主要改变的是资源定义的写法,不是绕过 CloudFormation 直接部署。
优点是可以用函数、条件和组件组织复杂配置,复用代码并编写测试;代价是多了依赖和生成层,一行代码可能生成多个资源。升级依赖、修改默认值时仍要审查生成的模板、权限和变更清单。
常见命令和含义:
cdk synth:执行资源定义代码,生成 CloudFormation 模板;合成成功不等于部署成功,也不代表权限和配置已经通过生产验证。cdk diff:比较本地定义与已部署的 Stack,检查资源新增、修改、替换、删除及 IAM 权限变化。cdk deploy:按需上传 Lambda 部署包等资源文件,再通过 CloudFormation 创建或更新真实云资源。cdk destroy:删除 Stack;资源是否保留取决于删除策略(Removal Policy)等配置,可能造成数据丢失,执行前必须审查。
L1、L2、L3:CDK 的三个封装层级
L 是 Level(层级),不是版本号,也不是三个执行步骤。
- L1:配置资源细节。直接对应 CloudFormation 资源,例如
lambda.CfnFunction,需要自己指定执行角色等配置。 - L2:简化资源使用。提供默认配置和便利方法,例如
lambda.Function,未指定角色时可以自动创建执行角色。 - L3:组合一套方案。把多个资源封装在一起,例如
ApplicationLoadBalancedFargateService,组合 Fargate 服务和应用负载均衡器等资源。
通常优先用 L2,需要现成组合时用 L3,细节配置不足时再用 L1;三者可以混用。封装越方便,越要看清自动创建的资源、权限和费用。
# 三者的关系与选择
| 方式 | 我们编写什么 | 怎样部署 | 更适合什么情况 |
|---|---|---|---|
| CloudFormation | YAML / JSON 资源模板 | CloudFormation 直接处理模板 | 希望直接控制资源定义,或已有成熟模板 |
| SAM | 带 Serverless 简写的模板 | SAM 声明展开后,由 CloudFormation 部署 | 主要是函数、API 和事件处理,偏好简洁模板 |
| CDK | TypeScript、Python 等资源定义代码 | 先生成模板,再由 CloudFormation 部署 | 资源组合较多,需要代码复用、校验和测试 |
它们是不同的编写入口,共用 CloudFormation 作为部署基础,不是从初级到高级必须依次升级的三个阶段。选择时主要看团队熟悉的写法、资源组合和复用需求,不是哪个名字更新就用哪个。
# 用同一个 Lambda 对比三种写法
下面都只定义一个返回 hello 的 Python Lambda,不创建公网 API,也不连接数据库。重点看函数和执行角色分别需要写多少配置,不是三套一起部署。示例使用 AWS 的基础日志权限策略;生产应按实际资源进一步收紧权限。这里只展示文件,不执行云端部署。
# CloudFormation 模板
保存为 cloudformation.yaml。函数与角色都显式声明;角色是 Lambda 运行时使用的身份,不是部署者的身份。
AWSTemplateFormatVersion: '2010-09-09'
Resources:
FunctionRole:
Type: AWS::IAM::Role
Properties:
# 信任策略允许 Lambda 服务使用这个角色。
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
# 只给函数基础日志能力,不授予管理员权限。
ManagedPolicyArns:
- Fn::Sub: 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
HelloFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: python3.12
Handler: index.handler
Timeout: 10
# 引用上面角色的 ARN,同时建立资源依赖。
Role:
Fn::GetAtt: [FunctionRole, Arn]
Code:
# CloudFormation 将内联 Python 代码放入 index.py。
ZipFile: |
def handler(event, context):
return {"message": "hello"}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# SAM 模板
保存为 sam.yaml。没有指定 Role 时,SAM 会生成函数执行角色和基础日志权限;模板并不是省掉了权限,而是由工具生成。
AWSTemplateFormatVersion: '2010-09-09'
# 声明 SAM 转换规则,部署时展开为 CloudFormation 资源。
Transform: AWS::Serverless-2016-10-31
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.12
Handler: index.handler
Timeout: 10
# 用内联代码展示写法;真实项目通常通过 CodeUri 引用代码目录。
InlineCode: |
def handler(event, context):
return {"message": "hello"}
2
3
4
5
6
7
8
9
10
11
12
13
14
# CDK 代码
在已初始化、安装 aws-cdk-lib 的 CDK v2 TypeScript 项目中,可用下面内容作为 bin 下的应用入口。这里省略角色定义,也是因为 Lambda 构造组件会生成默认执行角色和基础日志权限。
import { App, Duration, Stack } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
// App 是资源定义的入口,Stack 对应一组一起部署的资源。
const app = new App();
const stack = new Stack(app, 'HelloStack');
new lambda.Function(stack, 'HelloFunction', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'index.handler',
timeout: Duration.seconds(10),
// 为了对比模板,这里内联代码;真实项目通常打包独立代码目录。
code: lambda.Code.fromInline([
'def handler(event, context):',
' return {"message": "hello"}',
].join('\n')),
});
// 生成本地模板,不在此代码中执行云端部署。
app.synth();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这个例子里三种方式都能完成任务。CloudFormation 写得最直接;SAM 用约定减少 Serverless 配置;CDK 则方便把相同的函数配置封装成组件,复用于多个服务。配置变短不代表资源变少,最终仍要检查生成的角色和权限。
# Terraform 与 Pulumi 又有什么不同
除了 AWS 原生工具,还可以用支持多种云平台的 IaC 工具。它们通过 Provider(服务适配插件)调用云平台 API,不以生成 CloudFormation 模板作为常规部署路径。
# Terraform 是什么
Terraform (opens new window) 是 HashiCorp 提供的 IaC 工具,主要用 HCL(HashiCorp Configuration Language)配置语言描述想要的资源。比如在 .tf 文件里声明一个 S3 存储桶,Terraform 会结合配置、状态记录和云上实际情况,判断需要创建或修改什么,再通过 AWS Provider 调用 AWS API 执行。
常见流程是:terraform init 初始化工作目录和 Provider,terraform plan 预览变更,审查后再用 terraform apply 执行。它适合偏好声明式配置、需要统一管理多个平台资源的团队;代价是要学习 HCL,并妥善管理部署状态。
# Pulumi 是什么
Pulumi (opens new window) 也是 IaC 工具,但可以用 TypeScript、Python 等熟悉的语言定义资源。比如用 TypeScript 声明 S3 存储桶,再把名称、标签和权限规则封装成可复用组件;Pulumi 引擎会根据资源定义计算变更,通过 Provider 执行。
常见流程是:编写资源代码,用 pulumi preview 查看变化,审查后用 pulumi up 部署。它适合希望用现有编程语言组织基础设施的团队。它与 AWS CDK 的关键区别不是都能写 TypeScript,而是 Pulumi 通常由自己的引擎和 Provider 部署,AWS CDK 则生成 CloudFormation 模板后交给 AWS 部署。
# 两者怎么选
| 工具 | 怎么写与执行 | 优点 | 需要承担的复杂度 |
|---|---|---|---|
| Terraform (opens new window) | 主要用 HCL 配置语言;plan 预览变更,apply 执行 | Provider 生态广,便于用统一工作流管理多个平台 | 配置语言、模块和状态文件管理 |
| Pulumi (opens new window) | 用 TypeScript、Python 等语言;preview 预览,up 执行 | 熟悉的编程语言、组件复用和多平台支持 | 语言依赖、资源依赖表达和部署状态管理 |
状态记录保存代码中的资源与云上实际资源的对应关系。Terraform 和 Pulumi 团队协作时,要配置受控的远程状态存储、并发更新保护和访问权限,不能把可能含敏感信息的状态文件随便提交到 Git。CloudFormation 也有资源状态,只是由 AWS 服务管理,不需要团队自行维护同样的本地状态文件。
“支持多云” 不等于一套资源代码能原样部署到所有云。AWS 的 Lambda、IAM、VPC 与其他云的资源类型和权限模型不同,跨云迁移仍要调整定义。同一个云资源也应明确由哪套工具管理,避免两边反复覆盖配置。
# 发布必须留下同一版本的证据
确认 commit → 测试与构建 → 不可变产物
→ 审查 Change Set → 执行并等待 Stack 完成
→ 服务健康 → 小流量验证 → 扩大流量
└→ 失败则恢复旧流量与运行版本
2
3
4
镜像用完整 SHA 标识,部署时记录 digest;Lambda 发布不可变 version。不要反复覆盖 latest,否则同一个标签可能在不同时间指向不同代码。
Change Set 是计划变更清单:检查新增、修改、删除和 Replacement(替换资源),重点看数据库、API ID、存储与 IAM。execute-change-set 被接受不代表部署完成,必须等待相应创建或更新状态,失败再看最早的具体错误。
CDK 的 synth 生成模板,diff 比较变化,deploy 才应用变更;Context lookup、bootstrap 和 deploy 的影响不同,不能把一串命令都当作本地无副作用操作。执行前先确认账号与权限。
# Lambda 灰度为什么可能没有生效
原项目发布新 version,再通过 live alias 将少量流量送到新版本。API Gateway integration 和调用权限必须指向 alias;如果入口仍调用 $LATEST 或别的 ARN,改 alias 权重不会改变那条流量。
验证时按版本看请求与错误,不只看全函数总量。少量流量下,配置 10% 不表示每十次请求必有一次新版本。成功后把 alias 全部切到新版本并清理附加权重;失败回到旧版,再核对真实请求。
观察时长和比例根据请求量、风险及业务指标决定,原项目的 10%/五分钟只是当时策略,不是适用于所有产品的安全标准。
# ECS 灰度:路由与服务必须一起考虑
原项目使用 Stable/Canary 两个 Service 与 Target Group,ALB 按权重分流。先验证新组有健康实例,再开放权重;失败时同时恢复旧镜像、稳定流量和新组容量。
ALB 加权 Target Group 不是自动故障切换。某组全部不健康时,不能假定它的流量会自动转给另一组。还要检查内部 Cloud Map 调用是否绕过 ALB;如果内部提前进入新版,公网灰度比例就不能代表整个系统的升级范围。
# 数据库变化怎样配合回滚
代码退回旧版,不会自动把新数据变回旧格式。采用 expand-contract:先新增兼容字段或表,让新旧代码都能运行;迁移数据并验证,再下线旧逻辑,最后删除旧结构。破坏性变更不能和首次新代码发布捆在一个不可逆步骤里。
数据库迁移使用专门的有界任务,只运行一次;应用启动只验证所需结构,不让每个副本自行修改 schema。
# 用一个字段变更理解可回滚发布
假设订单金额原来存在 amount_text 文本字段,要改成整数最小单位 amount_minor。直接删旧字段再部署新版,会让尚未更新的旧实例立即报错,代码回滚也找不回旧结构。
更稳妥的顺序是:先新增可空字段 → 发布兼容读写逻辑 → 按币种精度迁移旧数据并校验 → 新版切换读取 → 确认不再需要旧代码 → 最后删除旧字段。回填期间必须明确谁继续写旧字段、如何同步新字段及冲突怎么处理,不能只跑一次 SQL 就假定数据不会再变化。
灰度阶段尤其要覆盖旧实例写、新实例读,以及新实例写、旧实例读这两种交叉情况。数据库回滚不是默认执行反向迁移;已经产生的新业务数据可能无法无损转换,必要时修复向前而不是强行倒退。
# 放量看哪些证据
| 检查 | 为什么有用 |
|---|---|
| 实际版本标识与请求数量 | 先证明新版本真的收到流量,避免零请求假通过 |
| 新旧版错误率及 P95 | 总平均值可能掩盖小流量版本的问题 |
| 数据库连接、队列年龄 | 新代码即使返回 200,也可能把压力转嫁下游 |
| 一次关键业务回读 | 健康接口无法证明写入、异步处理和查询一致 |
| 回滚后同一条业务路径 | 证明恢复生效,而不是只看 alias 修改成功 |
发布前定义失败门槛与观察窗口;样本少时延长观察或补受控验证,不能把五分钟无告警等同于充分验证。回滚也要检查新版本产生的队列消息能否被旧消费者解析。
# PR Preview 隔离什么
| 层 | 共享以减少成本 | 必须独立或受控 |
|---|---|---|
| 网络与计算基础 | VPC、Cluster、ALB 等 | PR 的 Service、Task Definition 与路由 |
| 产物 | ECR 仓库 | 不同 PR/commit 的镜像标识 |
| 数据 | 可共享非生产数据库实例 | 独立凭据、数据库或 schema;不接生产资料 |
| 权限 | 经设计的预览角色 | 不能读取生产 Secret,也不能自动执行不可信 PR |
原项目让前端与后端都从同一 commit 派生 preview key,通过请求头匹配路由。这个 key 是选环境的标识,不是密码或认证凭据。同一个数据库用户下分 schema 也不是恶意租户之间的强隔离。
预览环境的安全顺序:先创建不可路由、容量为 0 的资源 → 建立测试 schema 和虚构种子数据 → 校验退出码 → 启动服务并开放路由 → 冒烟测试。重试也从安全态开始,不能让上一次半成品继续接流量。
关闭 PR 后按精确资源清单清理,并设置 TTL 兜底;删除前核对 PR、commit、Stack 与数据范围。关闭事件不应被普通 “代码路径未变更” 过滤规则吞掉。
# 面试时可以这样回答
IaC 是什么?它和 CI/CD 有什么区别?参考答案
IaC 是把云资源及其配置写成代码或模板,让环境可以复现、变更可以审查。CI/CD 则决定什么时候测试、构建和部署。比如我用 CDK 定义函数、队列和权限,再让 CI/CD 在审批通过后执行部署;两者配合,而不是互相替代。
CloudFormation、SAM、CDK 怎么选?参考答案
三者最终都依赖 CloudFormation 部署,主要区别是怎么定义资源。直接写 CloudFormation 模板最直观;SAM 用简写减少函数、API 和事件配置;CDK 用 TypeScript 等语言组织资源,方便组件复用和测试。我会根据团队习惯和复用需求选择,不会因为资源多就认定 SAM 做不了,也不会因为用了 CDK 就跳过模板和权限审查。
CDK 和 Pulumi 都能写 TypeScript,有什么区别?参考答案
AWS CDK 主要生成 CloudFormation 模板,由 CloudFormation 管理 AWS 资源;Pulumi 通过自身引擎和 Provider 管理资源,也能接入其他云平台。因此不只是语言选择,还涉及部署引擎、状态管理和团队现有工具。跨云时 Pulumi 可以统一工作流,但不能让不同云的资源定义自动通用。
怎样设计一次能回滚的发布?参考答案
我先用不可变产物绑定代码版本,审查基础设施变化,保留当前稳定版本。新实例健康后才放少量流量,按版本观察错误和业务指标;异常时恢复流量和运行版本。数据库采用兼容性迁移,因为回滚代码不会自动回滚数据。最后用真实请求验证恢复,不以部署脚本退出为结束。
为什么有灰度和自动回滚,还要考虑数据库兼容?参考答案
灰度期间新旧代码同时运行,回滚也只恢复代码,不会自动撤销数据变化。我会先扩展 schema,让两个版本兼容读写,再回填验证,最后才删除旧结构。消息格式同样要兼容,否则新版本发出的事件可能把回滚后的旧消费者卡住。
PR 预览环境最容易忽略什么风险?参考答案
预览会运行 PR 的代码,不能默认可信。我会限制哪些来源能触发部署,隔离生产凭据和数据,并绑定确定的 commit。关闭 PR 时按资源清单清理,再用 TTL 防止漏清理。环境路由 key 只是选择环境,不是访问授权,不能拿它保护敏感数据。
# 复用来源与参考
整理自原项目 Production CI/CD、PR Preview、稳定性与灰度发布章节。