AWS CDK Python 实战:从基础设施代码化到生产级 CI/CD
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):
- 在 AWS 控制台,进入 IAM Identity Center > Settings ,复制你的
Start URL(如https://your-org.awsapps.com/start)。 - 在终端运行:
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 - 浏览器会自动弹出登录页,用你的企业账号登录,选择你要访问的 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 的“编译”指令。它的执行过程,远比你想象的复杂和精妙:
-
作用域解析(Scope Resolution) :CDK 会遍历整个对象树,从
app开始,找到所有Stack实例,再找到每个Stack下的所有Construct。它会严格检查父子关系,确保没有Construct被创建在了错误的Stack之外。 -
依赖图构建(Dependency Graph) :CDK 会分析所有资源间的隐式依赖。例如,当你调用
bucket.grant_read_write(lambda_func)时,CDK 不仅会生成 IAM Policy,还会在内部记录一条LambdaFunction依赖于Bucket的边。这确保了在 CloudFormation 模板中,AWS::IAM::Policy资源一定会在AWS::Lambda::Function之后创建,从而避免了“资源不存在”的错误。 -
模板合成(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 :
更多推荐

所有评论(0)