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"}
1
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"}
1
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();
1
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 完成
  → 服务健康 → 小流量验证 → 扩大流量
                      └→ 失败则恢复旧流量与运行版本
1
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、稳定性与灰度发布章节。