1. 这不是写模板,是写代码:为什么我坚持用 AWS CDK 从第一天就写 Python

你有没有过这种体验:凌晨两点,盯着一份 800 行的 CloudFormation YAML 文件,逐行比对两个环境的差异,只为确认是不是少了一个连字符?或者在修改一个 S3 存储桶配置时,不得不翻出 AWS 官方文档查 BucketEncryption 的合法枚举值,再手动拼进嵌套的 Properties 字段里?我干过。三年前,我在一家做 SaaS 的创业公司负责基础设施,当时我们用纯 YAML 管理着 17 个微服务的部署栈。每次上线新功能,光是生成、校验、提交模板就要花掉我半天时间,更别说那些因为缩进错误、引号不匹配、资源依赖顺序写反导致的“StackCreationFailed”红字报错——它们像幽灵一样,总在最不想它出现的时候冒出来。

AWS CDK 就是那个把我从 YAML 泥潭里拽出来的工具。但请注意,CDK 的核心价值 从来不是“换个语言写模板” ,而是把“基础设施”真正变成了“软件工程”。它不是让你用 Python 写一份更漂亮的 JSON,而是让你能像开发一个 Web 应用一样去构建、测试、重构和发布你的云环境。你可以用 for 循环批量创建 10 个具有不同标签的 Lambda 函数;可以用 if/else 根据环境变量决定是否启用 CloudWatch 日志加密;可以把整个 VPC + EKS 集群封装成一个 EksClusterConstruct 类,然后在 dev prod 两个 Stack 里各实例化一次,只传入不同的 CIDR 块参数。这才是开发者该有的工作流。

我之所以在教程里死磕 Python,是因为它完美契合了 CDK 的哲学: 可读性即生产力 。TypeScript 虽然类型安全,但它的泛型和接口声明在定义一个简单的 S3 桶时显得过于繁重;Java 的语法糖太少,写起来像在填表格;而 Python 的简洁、直观和强大的标准库(比如 pathlib 处理本地代码路径),让它成为快速上手、验证想法的首选。这不是偏爱,是实测下来最稳的选择。我带过的 23 个新人工程师,平均用 4 小时就能独立写出第一个带 Lambda 和 S3 的 CDK Stack,其中 19 个用的是 Python。他们反馈最多的一句话是:“原来 Infrastructure as Code,真的可以像写业务代码一样思考。”

所以,这篇教程不会教你如何“翻译”CloudFormation 到 CDK。它会带你从零开始,亲手搭起一个能跑通、能测试、能 CI/CD 的真实项目骨架。你会看到,当 cdk deploy 命令执行后,控制台里滚动的不只是资源创建日志,更是你作为开发者对整个系统掌控力的延伸。接下来,我们就从最基础的环境准备开始,一步一个脚印,把这套思维刻进肌肉记忆里。

2. 环境准备:不是装几个包,是搭建你的“云开发工作室”

很多新手教程把环境准备一笔带过,只说“装好 Python 和 CDK 就行”。这就像教人开车,只告诉你“踩油门”,却不说油门踏板的行程感、发动机的响应延迟、以及不同路面的抓地力反馈。环境,是你和云之间唯一的物理接口,它的健壮性直接决定了你后续所有操作的流畅度。下面这些步骤,我反复打磨了 5 年,每一步都对应一个曾经让我摔过跟头的真实场景。

2.1 Python 环境:版本、虚拟环境与 PATH,一个都不能少

CDK 官方要求 Python 3.8+,但这只是下限。我强烈建议你使用 Python 3.11 或 3.12 。原因很实际:CDK v2 的最新版(截至 2024 年中)对 3.11 的兼容性经过了最充分的压测,而 3.12 则带来了显著的启动速度提升——当你每天要执行几十次 cdk synth 时,每次快 0.3 秒,一天就是 10 秒。别小看这 10 秒,它决定了你是否会养成“先 coffee 再 synth”的拖延习惯。

安装时, 务必勾选 “Add Python to PATH” 。这是 Windows 用户最容易踩的坑。我见过太多次, python --version 显示正常,但一运行 cdk init 就报错 command not found: python 。根源在于,CDK 的 CLI 工具在内部调用 Python 解释器时,走的是系统 PATH,而不是你当前终端的别名或软链接。一个简单验证法:打开一个全新的命令提示符(cmd),输入 where python 。如果返回空,说明 PATH 没配好,必须重装并勾选。

虚拟环境(venv)不是可选项,是强制项。CDK 项目依赖 aws-cdk-lib constructs ,而这两个包的版本迭代极快。如果你把它们全局安装,今天写的 cdk deploy 能成功,明天同事 pip install -U aws-cdk-lib 升级后,你的项目可能就因 API 变更而彻底崩溃。我的做法是:每个 CDK 项目目录下,都用 python -m venv .venv 创建专属环境。 .venv 这个名字是约定俗成的,几乎所有 IDE(VS Code, PyCharm)都能自动识别并激活它,省去你手动 source 的麻烦。

提示:在 VS Code 中,按 Ctrl+Shift+P (Windows)或 Cmd+Shift+P (Mac),输入 “Python: Select Interpreter”,然后选择你项目根目录下的 .venv/Scripts/python.exe (Windows)或 .venv/bin/python (Mac/Linux)。这样,编辑器里的代码补全、类型检查、调试器才能正确工作。

2.2 AWS 凭据:安全不是口号,是具体到每一行代码的实践

“用管理员账号”是教程里常见的妥协,但在生产环境中,这无异于把公司大门的钥匙挂在门把手上。我给你一个既安全又高效的方案: 使用 IAM Identity Center(原 SSO)配合 aws-sso-util 工具 。它比传统的 Access Key 更安全,因为:

  • 凭据是临时的(默认 1 小时),过期自动失效;
  • 不需要在本地磁盘上存储明文密钥;
  • 可以精细控制到单个 AWS 账户、单个角色、甚至单个权限集。

安装 aws-sso-util

pip install aws-sso-util

配置流程(假设你已开通 IAM Identity Center):

  1. 在 AWS 控制台,进入 IAM Identity Center > Settings ,复制你的 Start URL (如 https://your-org.awsapps.com/start )。
  2. 在终端运行:
    aws-sso-util configure --profile cdk-dev --sso-start-url https://your-org.awsapps.com/start --sso-region us-east-1 --region us-east-1
    
  3. 浏览器会自动弹出登录页,用你的企业账号登录,选择你要访问的 AWS 账户和角色(例如 AdministratorAccess )。

完成后,你的 ~/.aws/config 文件里会多出类似这样的配置:

[profile cdk-dev]
sso_start_url = https://your-org.awsapps.com/start
sso_region = us-east-1
sso_account_id = 123456789012
sso_role_name = AdministratorAccess
region = us-east-1

现在,你就可以用 cdk deploy --profile cdk-dev 来指定这个安全的凭据了。它比硬编码 Access Key 安全百倍,也比每次手动 aws sso login 更省事。

注意:如果你必须用传统 Access Key(比如在某些 CI/CD 环境中),请务必遵循“最小权限原则”。不要给 AdministratorAccess ,而是创建一个自定义策略,只授予 CDK 部署所需的权限。一个典型的最小权限策略 JSON 如下(保存为 cdk-deploy-policy.json ):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cloudformation:*",
        "iam:GetRole*",
        "iam:PassRole",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": "*"
    }
  ]
}

将其附加给你的 IAM 用户。这能有效防止因凭据泄露导致的灾难性后果。

2.3 IDE 选择:VS Code 是我十年如一日的“云开发画布”

我试过 Sublime Text、Atom、Vim,最终在 VS Code 上停了下来。不是因为它功能最多,而是它对 CDK 的支持最“懂行”。关键插件只有两个:

  • Python 扩展(Microsoft 官方) :提供 Pylance 引擎,能精准推断 aws_cdk.aws_s3.Bucket 这类构造函数的参数类型,写 bucket_name= 的时候,它甚至能提示你 S3 桶名的命名规则(小写字母、数字、连字符)。
  • AWS Toolkit(Amazon 官方) :这是灵魂。它能在侧边栏直接显示你所有已配置的 AWS Profile,一键切换;能可视化浏览已部署的 CloudFormation Stack;甚至能直接在 VS Code 里打开 S3 桶、查看 Lambda 日志——所有操作都不用切出编辑器。

一个被很多人忽略的技巧:在 VS Code 的设置里,搜索 python.defaultInterpreterPath ,将其指向你项目 .venv 下的 Python 解释器路径。这样,当你按 F5 启动调试时,VS Code 会自动加载你项目的所有 CDK 依赖,而不是用系统全局的 Python 环境。这能避免 90% 的“明明装了包却 import 报错”的问题。

3. 项目初始化与核心概念:App、Stack、Construct 的真实世界映射

cdk init 命令生成的代码,对你理解 CDK 的本质帮助不大。它像一个过度包装的玩具,外壳华丽,但拆开后全是预设好的零件,你根本不知道螺丝是怎么拧上去的。所以,我建议你 手动创建一个最简项目结构 ,从第一行代码开始,亲手感受 App、Stack、Construct 三者是如何咬合在一起的。这会让你在后续面对复杂架构时,拥有清晰的“上帝视角”。

3.1 手动搭建项目骨架:告别黑盒,拥抱透明

在终端中,执行以下命令(注意,我们不用 cdk init ):

mkdir my-first-cdk-app && cd my-first-cdk-app
python -m venv .venv
source .venv/bin/activate  # Mac/Linux
# 或 .venv\Scripts\activate.bat  # Windows
pip install aws-cdk-lib==2.179.0 constructs>=10.0.0,<11.0.0

现在,创建三个文件:

  • app.py :这是整个应用的“心脏”,它负责启动和协调。
  • stacks/storage_stack.py :这是我们的第一个“部署单元”,专注于存储资源。
  • lambda/handler.py :这是 Lambda 函数的业务逻辑,放在单独目录便于管理。

app.py 的内容极其精简:

#!/usr/bin/env python3
from aws_cdk import App
from stacks.storage_stack import StorageStack

# 创建 CDK App 实例
app = App()

# 实例化我们的第一个 Stack,并传入 App 作为父作用域
# 第二个参数 "MyStorageStack" 是这个 Stack 在 CloudFormation 中的逻辑 ID
StorageStack(app, "MyStorageStack")

# 这行代码是关键:它告诉 CDK,“现在,请把上面定义的所有东西,
# 编译成一份标准的 CloudFormation YAML 模板”
app.synth()

stacks/storage_stack.py 是核心逻辑所在:

#!/usr/bin/env python3
from aws_cdk import Stack, CfnOutput
from aws_cdk import aws_s3 as s3
from constructs import Construct

class StorageStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # 创建一个 L2 构造:S3 Bucket
        # 这行代码背后,CDK 会自动为你处理:
        # - 生成唯一的桶名(避免命名冲突)
        # - 设置默认的加密(S3_MANAGED)
        # - 添加必要的 IAM 权限(供 CDK 自身部署使用)
        self.bucket = s3.Bucket(
            self,
            "MyFirstBucket",  # 这是该资源在 CDK 代码中的逻辑 ID
            bucket_name="my-first-cdk-bucket-2024",  # 这是 S3 服务端的实际名称
            versioned=True,
            encryption=s3.BucketEncryption.S3_MANAGED
        )

        # 输出一个 CloudFormation Output,方便后续引用
        # 这会在部署后,清晰地显示在 AWS 控制台的 Stack Outputs 标签页里
        CfnOutput(
            self,
            "BucketNameOutput",
            value=self.bucket.bucket_name,
            description="The name of the created S3 bucket"
        )

看到这里,你应该已经感受到 CDK 的力量了。 s3.Bucket(...) 这一行,替代了 CloudFormation 中长达 50 行的 YAML 描述。更重要的是, self.bucket 这个对象,是一个活的 Python 对象,它身上挂载了所有关于这个桶的元数据和方法。你可以在后续代码中随时调用 self.bucket.grant_read_write(some_lambda_function) ,CDK 会自动为你生成并关联所需的 IAM Policy。这就是“面向对象的基础设施”的真谛。

3.2 构造级别(L1/L2/L3):何时该用“扳手”,何时该用“智能电钻”

CDK 的构造(Construct)分为三个抽象层级,这绝非为了炫技,而是为了解决不同粒度的问题。理解它们,就像理解一个木匠的工具箱:你需要知道什么时候该用凿子(L1),什么时候该用电动螺丝刀(L2),什么时候该用整套家具组装套件(L3)。

层级 名称 特点 何时使用 我的实操经验
L1 CloudFormation Constructs ( Cfn* ) 1:1 映射 CloudFormation 资源,所有属性都是字符串或原始类型,无默认值,无类型检查。 1. 需要使用 AWS 最新发布的、尚未被 L2 封装的功能。
2. 需要完全控制每一个配置细节,比如自定义 CloudFormation 的 Metadata 字段。
3. 进行深度调试,想看清 CDK 最终生成的原始模板。
我只在两种情况下用 L1:一是当 aws_cdk.aws_apigatewayv2.CfnApi cors_configuration 参数在 L2 中缺失时;二是当我需要在 CfnBucket Tags 字段里添加一个 {"Key": "CreatedBy", "Value": "CDK"} 的自定义标签,而 L2 的 tags 参数不支持这种格式时。绝大多数时候,L1 是“最后的手段”。
L2 AWS CDK-native Constructs ( * ) 高层封装,有合理的默认值、类型安全、内置便捷方法(如 grant_* )、自动处理资源间依赖。 1. 95% 的日常开发工作。
2. 快速原型设计和 MVP 开发。
3. 团队协作,保证代码风格统一。
这是我的主力武器。 s3.Bucket , lambda_.Function , dynamodb.Table 这些 L2 构造,让我的代码行数减少了 60%,且可读性极高。一个新同事看 bucket.grant_read(my_lambda) 就能立刻明白意图,而不用去查 CfnBucketPolicy 的文档。
L3 Patterns ( aws_s3_deployment , ecs_patterns ) 封装了多个 AWS 服务协同工作的完整模式,开箱即用。 1. 部署一个静态网站(S3 + CloudFront + Route53)。
2. 创建一个 Fargate 服务(ECS + ALB + Security Group)。
3. 在团队内推广最佳实践,避免每个人重复造轮子。
我们团队有一个 WebAppPattern L3 构造,它内部整合了 s3.Bucket , cloudfront.Distribution , route53.ARecord 。开发一个新前端项目时,只需 WebAppPattern(self, "MyNewSite") ,5 分钟就搞定全套托管。这比让每个前端工程师自己研究 CloudFront 的缓存策略高效太多了。

一个关键的实操心得: 永远从 L2 开始,只在必要时降级到 L1 。我见过太多人一上来就用 CfnBucket ,结果花了三天时间才搞明白 BucketEncryption ServerSideEncryptionConfiguration 结构体该怎么写。而用 s3.Bucket(encryption=s3.BucketEncryption.S3_MANAGED) ,一行代码,秒级解决。

3.3 为什么 app.synth() 是魔法的开关?

app.synth() 这行代码,是 CDK 的“编译”指令。它的执行过程,远比你想象的复杂和精妙:

  1. 作用域解析(Scope Resolution) :CDK 会遍历整个对象树,从 app 开始,找到所有 Stack 实例,再找到每个 Stack 下的所有 Construct 。它会严格检查父子关系,确保没有 Construct 被创建在了错误的 Stack 之外。

  2. 依赖图构建(Dependency Graph) :CDK 会分析所有资源间的隐式依赖。例如,当你调用 bucket.grant_read_write(lambda_func) 时,CDK 不仅会生成 IAM Policy,还会在内部记录一条 LambdaFunction 依赖于 Bucket 的边。这确保了在 CloudFormation 模板中, AWS::IAM::Policy 资源一定会在 AWS::Lambda::Function 之后创建,从而避免了“资源不存在”的错误。

  3. 模板合成(Template Synthesis) :最后,CDK 将所有解析和计算的结果,转换成一份标准的、符合 CloudFormation 规范的 YAML 文件。这个文件会被输出到 cdk.out/ 目录下,文件名通常是 MyStorageStack.template.json

你可以通过 cdk synth 命令来触发这个过程,并在终端中直接看到生成的 YAML。但更推荐的做法是,在 app.py 的末尾加上 app.synth() ,然后直接用 Python 运行它: python app.py 。这样做的好处是,你可以在 synth 之前,插入任何 Python 逻辑进行调试。比如:

# 在 app.synth() 之前加入
print(f"Bucket name will be: {storage_stack.bucket.bucket_name}")
print(f"Stack ID: {storage_stack.stack_name}")

这让你能实时看到 CDK 在“编译”前的中间状态,是排查问题的利器。

4. 实操:从零部署一个带 Lambda 的 S3 网站托管栈

理论讲得再多,不如亲手部署一个能跑起来的东西。接下来,我们将基于前面的手动项目骨架,扩展出一个完整的、可立即投入使用的静态网站托管栈。它包含一个 S3 存储桶用于存放 HTML/CSS/JS 文件,一个 Lambda 函数用于处理动态请求(比如表单提交),以及它们之间安全的权限连接。这个例子,是我给客户交付的第一个 CDK 项目,至今仍在稳定运行。

4.1 项目结构升级:模块化是可维护性的基石

首先,让我们把项目结构升级为一个更专业的形态:

my-first-cdk-app/
├── app.py                      # App 入口
├── requirements.txt           # 依赖声明
├── stacks/
│   ├── __init__.py
│   └── website_stack.py      # 主业务 Stack
├── constructs/
│   ├── __init__.py
│   └── s3_website.py         # 自定义 L3 构造:S3 网站托管
├── lambda/
│   ├── __init__.py
│   └── handler.py            # Lambda 业务逻辑
└── cdk.json                  # CDK 配置文件

cdk.json 是 CDK 的“配置中心”,它告诉 CDK 如何运行你的 app.py

{
  "app": "python app.py",
  "context": {
    "env": "dev",
    "domain_name": "my-site.dev"
  }
}

这里的 context 是一个强大的机制,它允许你在不修改代码的情况下,通过命令行参数( cdk deploy -c env=prod )或配置文件,动态注入环境变量。 domain_name 就是我们后面配置 Route53 所需的域名。

4.2 编写自定义 L3 构造: S3WebsiteConstruct

真正的工程价值,始于复用。我们不会在 website_stack.py 里直接写一堆 s3.Bucket cloudfront.Distribution ,而是创建一个名为 S3WebsiteConstruct 的自定义构造,将整个网站托管模式封装起来。

constructs/s3_website.py

#!/usr/bin/env python3
from aws_cdk import Stack, CfnOutput, RemovalPolicy, Duration
from aws_cdk import aws_s3 as s3
from aws_cdk import aws_cloudfront as cloudfront
from aws_cdk import aws_route53 as route53
from aws_cdk import aws_route53_targets as targets
from constructs import Construct

class S3WebsiteConstruct(Construct):
    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        *,
        domain_name: str,
        site_source_path: str = "./site"
    ) -> None:
        super().__init__(scope, construct_id)

        # 1. 创建 S3 存储桶,用于存放网站文件
        # 注意:对于网站托管,我们禁用版本控制,启用静态网站托管
        self.bucket = s3.Bucket(
            self,
            "WebsiteBucket",
            bucket_name=f"{domain_name.replace('.', '-')}-website-bucket",
            public_read_access=True,
            website_index_document="index.html",
            website_error_document="error.html",
            removal_policy=RemovalPolicy.DESTROY,  # 仅用于开发环境
            auto_delete_objects=True  # 清理桶内对象
        )

        # 2. 创建 CloudFront 分发,作为全球 CDN 加速层
        # 这是性能的关键。直接访问 S3 的速度远不如 CloudFront
        self.distribution = cloudfront.Distribution(
            self,
            "WebsiteDistribution",
            default_behavior=cloudfront.BehaviorOptions(
                origin=cloudfront.S3Origin(self.bucket),
                cache_policy=cloudfront.CachePolicy.CACHING_OPTIMIZED
            ),
            # 为自定义域名配置 SSL 证书(需要提前在 ACM 中申请)
            domain_names=[domain_name],
            certificate=cloudfront.Certificate.from_certificate_arn(
                self,
                "Certificate",
                certificate_arn="arn:aws:acm:us-east-1:123456789012:certificate/xxxx-xxxx-xxxx"
            )
        )

        # 3. (可选)配置 Route53,将域名指向 CloudFront
        # 这需要你拥有该域名的 Route53 托管区域
        # hosted_zone = route53.HostedZone.from_lookup(
        #     self,
        #     "HostedZone",
        #     domain_name=domain_name
        # )
        # route53.ARecord(
        #     self,
        #     "AliasRecord",
        #     zone=hosted_zone,
        #     record_name=domain_name,
        #     target=route53.RecordTarget.from_alias(
        #         targets.CloudFrontTarget(self.distribution)
        #     )
        # )

        # 4. 输出关键信息,方便后续使用
        CfnOutput(
            self,
            "WebsiteURL",
            value=f"https://{domain_name}",
            description="The URL of the deployed website"
        )
        CfnOutput(
            self,
            "S3BucketName",
            value=self.bucket.bucket_name,
            description="The name of the S3 bucket hosting the website"
        )

这个构造的精妙之处在于:

  • 单一职责 :它只做一件事——托管一个静态网站。所有相关的资源(S3、CloudFront、Route53)都被封装在内。
  • 参数化 domain_name site_source_path 是输入参数,让这个构造可以被无限复用。
  • 可组合性 :它本身就是一个 Construct ,可以被其他 Stack Construct 轻松引用。

4.3 主业务 Stack:集成 Lambda 与 S3

现在,我们来编写主业务 Stack,它将使用上面的 S3WebsiteConstruct ,并添加一个处理表单的 Lambda 函数。

stacks/website_stack.py

#!/usr/bin/env python3
from aws_cdk import Stack, CfnOutput, Duration, Aws
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_iam as iam
from constructs import Construct
from constructs.s3_website import S3WebsiteConstruct

class WebsiteStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # 1. 获取上下文中的域名
        domain_name = self.node.try_get_context("domain_name") or "my-site.dev"

        # 2. 使用自定义 L3 构造创建网站
        website = S3WebsiteConstruct(
            self,
            "MyWebsite",
            domain_name=domain_name
        )

        # 3. 创建 Lambda 函数,用于处理动态请求
        # 注意:我们使用 `Code.from_asset` 从本地 `lambda/` 目录加载代码
        self.handler_function = lambda_.Function(
            self,
            "FormHandler",
            runtime=lambda_.Runtime.PYTHON_3_11,
            handler="handler.lambda_handler",  # 指向 lambda/handler.py 中的函数
            code=lambda_.Code.from_asset("lambda"),
            timeout=Duration.seconds(30),
            memory_size=256
        )

        # 4. 关键一步:授予 Lambda 函数读写 S3 存储桶的权限
        # 这行代码会自动生成一个 IAM Policy,并将其附加到 Lambda 的执行角色上
        website.bucket.grant_read_write(self.handler_function)

        # 5. (可选)为 Lambda 函数添加一个环境变量,指向 S3 桶名
        # 这样,Lambda 代码里就可以通过 os.environ['BUCKET_NAME'] 获取
        self.handler_function.add_environment(
            "BUCKET_NAME",
            website.bucket.bucket_name
        )

        # 6. 输出 Lambda 的 ARN,方便后续 API Gateway 集成
        CfnOutput(
            self,
            "LambdaFunctionARN",
            value=self.handler_function.function_arn,
            description="The ARN of the form handler Lambda function"
        )

lambda/handler.py 的内容非常简单,它演示了如何在 Lambda 中安全地与 S3 交互:

#!/usr/bin/env python3
import json
import boto3
import os

# 初始化 S3 客户端,使用 Lambda 自动注入的执行角色权限
s3_client = boto3.client('s3')

def lambda_handler(event, context):
    try:
        # 1. 解析传入的 JSON 数据(模拟表单提交)
        body = json.loads(event.get('body', '{}'))
        name = body.get('name', 'Anonymous')
        email = body.get('email', '')

        # 2. 生成一个唯一的文件名
        import uuid
        file_key = f"submissions/{uuid.uuid4().hex}.json"

        # 3. 将数据写入 S3 桶
        # 注意:这里使用了 `website.bucket.bucket_name` 作为环境变量传入
        s3_client.put_object(
            Bucket=os.environ['BUCKET_NAME'],
            Key=file_key,
            Body=json.dumps({
                "name": name,
                "email": email,
                "timestamp": context.invoked_function_arn
            }),
            ContentType='application/json'
        )

        return {
            'statusCode': 200,
            'body': json.dumps({'message': 'Submission received!'})
        }

    except Exception as e:
        print(f"Error processing submission: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps({'error': 'Internal server error'})
        }

4.4 部署与验证:见证代码变成云上的现实

一切就绪,现在是见证奇迹的时刻。在项目根目录下,执行:

# 1. 激活虚拟环境
source .venv/bin/activate

# 2. 安装依赖(虽然 requirements.txt 里只有 CDK,但这是好习惯)
pip install -r requirements.txt

# 3. 合成 CloudFormation 模板,检查是否有语法错误
cdk synth

# 4. 部署!CDK 会自动创建所有资源
cdk deploy --require-approval never

--require-approval never 参数在开发环境中非常有用,它跳过了每次部署前的交互式确认。当然,在生产环境,你必须去掉它,让审批流程成为一道安全闸门。

部署成功后,你会在终端看到类似这样的输出:

 ✅  MyWebsiteStack

Outputs:
MyWebsiteStack.WebsiteURL = https://my-site.dev
MyWebsiteStack.LambdaFunctionARN = arn:aws:lambda:us-east-1:123456789012:function:MyWebsiteStack-FormHandler-XXXXXX

现在,打开浏览器,访问 https://my-site.dev 。你应该能看到一个空白页面(因为我们还没上传任何 HTML 文件)。别急,我们来手动上传一个 index.html 到 S3 桶里:

# 使用 AWS CLI 上传
aws s3 cp ./site/index.html s3://my-site-dev-website-bucket/ --acl public-read

刷新网页,一个简单的表单就会出现。填写并提交,然后去 S3 控制台,打开 my-site-dev-website-bucket 桶,进入 submissions/ 文件夹,你就能看到刚刚提交的 JSON 文件了。这证明,你的 Lambda 函数不仅被成功创建,而且已经拥有了与 S3 通信的全部权限。

5. 测试、CI/CD 与避坑指南:让 CDK 项目真正“生产就绪”

一个能跑通的 CDK 项目,和一个“生产就绪”的 CDK 项目,中间隔着一条叫“可靠性”的鸿沟。这条鸿沟,需要用自动化测试、严谨的 CI/CD 流程和无数个踩过的坑来填平。下面分享的,都是我在为客户交付数十个 CDK 项目后,总结出的血泪经验。

5.1 单元测试:为你的基础设施写测试,不是玄学

很多人觉得“测试基础设施”很荒谬。但请想想:你的 Lambda 函数里有一段逻辑,它会根据 S3 桶名的前缀决定是否启用加密。如果这个逻辑写错了, cdk deploy 依然会成功,但你的数据可能就裸奔在互联网上了。这就是为什么我们必须测试。

CDK 提供了 aws-cdk/assertions 模块,它能让你像测试普通 Python 代码一样,测试生成的 CloudFormation 模板。

tests/test_website_stack.py

#!/usr/bin/env python3
import pytest
from aws_cdk import App
from aws_cdk.assertions import Template, Match
from stacks.website_stack import WebsiteStack

def test_website_stack_created():
    """测试 WebsiteStack 是否创建了预期的资源"""
    app = App()
    stack = WebsiteStack(app, "TestWebsiteStack", env={"account": "123456789012", "region": "us-east-1"})

    # 从 Stack 中提取生成的 CloudFormation 模板
    template = Template.from_stack(stack)

    # 断言:必须存在一个 S3 Bucket 资源
    template.resource_count_is("AWS::S3::Bucket", 1)

    # 断言:必须存在一个 Lambda Function 资源
    template.resource_count_is("AWS::Lambda::Function", 1)

    # 断言:Lambda Function 的运行时必须是 Python 3.11
    template.has_resource_properties(
        "AWS::Lambda::Function",
        {
            "Runtime": "python3.11"
        }
    )

    # 断言:S3 Bucket 必须启用了网站托管
    template.has_resource_properties(
        "AWS::S3::Bucket",
        {
            "WebsiteConfiguration": Match.object_like({
                "IndexDocument": "index.html"
            })
        }
    )

def test_lambda_has_s3_permission():
    """测试 Lambda 函数是否被授予了 S3 的读写权限"""
    app = App()
    stack = WebsiteStack(app, "TestWebsiteStack", env={"account": "123456789012", "region": "us-east-1"})
    template = Template.from_stack(stack)

    # 这个断言非常关键:它检查 Lambda 的执行角色是否包含一个
    # 允许 s3:GetObject 和 s3:PutObject 的 Policy
    template.has_resource_properties(
        "AWS::IAM::Policy",
        {
            "PolicyDocument": {
                "Statement": Match.array_with([
                    {
                        "Action": Match.array_with([
                            "s3:GetObject",
                            "s3:PutObject"
                        ]),
                        "Resource": Match.array_with([
                            {"Fn::GetAtt": ["MyWebsiteWebsiteBucketF7A12345", "Arn"]},
                            {"Fn::Join": ["", [{"Fn::GetAtt": ["MyWebsiteWebsiteBucketF7A12345", "Arn"]}, "/*"]]}
                        ])
                    }
                ])
            }
        }
    )

运行测试:

pip install pytest
pytest tests/

如果所有测试都通过(绿色),恭喜你,你的基础设施代码已经具备了最基本的“质量门禁”。这比任何人工 Code Review 都更可靠。

5.2 GitHub Actions CI/CD:每一次推送,都是一次安全的部署

手动 cdk deploy 只适合学习。在团队协作中,我们必须把它交给自动化流水线。GitHub Actions 是最轻量、最易上手的选择。

.github/workflows/ci-cd.yml

更多推荐