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这个名字是约定俗成的几乎所有 IDEVS Code, PyCharm都能自动识别并激活它省去你手动source的麻烦。提示在 VS Code 中按CtrlShiftPWindows或CmdShiftPMac输入 “Python: Select Interpreter”然后选择你项目根目录下的.venv/Scripts/python.exeWindows或.venv/bin/pythonMac/Linux。这样编辑器里的代码补全、类型检查、调试器才能正确工作。2.2 AWS 凭据安全不是口号是具体到每一行代码的实践“用管理员账号”是教程里常见的妥协但在生产环境中这无异于把公司大门的钥匙挂在门把手上。我给你一个既安全又高效的方案使用 IAM Identity Center原 SSO配合aws-sso-util工具。它比传统的 Access Key 更安全因为凭据是临时的默认 1 小时过期自动失效不需要在本地磁盘上存储明文密钥可以精细控制到单个 AWS 账户、单个角色、甚至单个权限集。安装aws-sso-utilpip 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 ToolkitAmazon 官方这是灵魂。它能在侧边栏直接显示你所有已配置的 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 initmkdir 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-lib2.179.0 constructs10.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_namemy-first-cdk-bucket-2024, # 这是 S3 服务端的实际名称 versionedTrue, encryptions3.BucketEncryption.S3_MANAGED ) # 输出一个 CloudFormation Output方便后续引用 # 这会在部署后清晰地显示在 AWS 控制台的 Stack Outputs 标签页里 CfnOutput( self, BucketNameOutput, valueself.bucket.bucket_name, descriptionThe 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。层级名称特点何时使用我的实操经验L1CloudFormation 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 是“最后的手段”。L2AWS CDK-native Constructs (*)高层封装有合理的默认值、类型安全、内置便捷方法如grant_*、自动处理资源间依赖。1. 95% 的日常开发工作。2. 快速原型设计和 MVP 开发。3. 团队协作保证代码风格统一。这是我的主力武器。s3.Bucket,lambda_.Function,dynamodb.Table这些 L2 构造让我的代码行数减少了 60%且可读性极高。一个新同事看bucket.grant_read(my_lambda)就能立刻明白意图而不用去查CfnBucketPolicy的文档。L3Patterns (aws_s3_deployment,ecs_patterns)封装了多个 AWS 服务协同工作的完整模式开箱即用。1. 部署一个静态网站S3 CloudFront Route53。2. 创建一个 Fargate 服务ECS ALB Security Group。3. 在团队内推广最佳实践避免每个人重复造轮子。我们团队有一个WebAppPatternL3 构造它内部整合了s3.Bucket,cloudfront.Distribution,route53.ARecord。开发一个新前端项目时只需WebAppPattern(self, MyNewSite)5 分钟就搞定全套托管。这比让每个前端工程师自己研究 CloudFront 的缓存策略高效太多了。一个关键的实操心得永远从 L2 开始只在必要时降级到 L1。我见过太多人一上来就用CfnBucket结果花了三天时间才搞明白BucketEncryption的ServerSideEncryptionConfiguration结构体该怎么写。而用s3.Bucket(encryptions3.BucketEncryption.S3_MANAGED)一行代码秒级解决。3.3 为什么app.synth()是魔法的开关app.synth()这行代码是 CDK 的“编译”指令。它的执行过程远比你想象的复杂和精妙作用域解析Scope ResolutionCDK 会遍历整个对象树从app开始找到所有Stack实例再找到每个Stack下的所有Construct。它会严格检查父子关系确保没有Construct被创建在了错误的Stack之外。依赖图构建Dependency GraphCDK 会分析所有资源间的隐式依赖。例如当你调用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(fBucket name will be: {storage_stack.bucket.bucket_name}) print(fStack 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 envprod或配置文件动态注入环境变量。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_namef{domain_name.replace(., -)}-website-bucket, public_read_accessTrue, website_index_documentindex.html, website_error_documenterror.html, removal_policyRemovalPolicy.DESTROY, # 仅用于开发环境 auto_delete_objectsTrue # 清理桶内对象 ) # 2. 创建 CloudFront 分发作为全球 CDN 加速层 # 这是性能的关键。直接访问 S3 的速度远不如 CloudFront self.distribution cloudfront.Distribution( self, WebsiteDistribution, default_behaviorcloudfront.BehaviorOptions( origincloudfront.S3Origin(self.bucket), cache_policycloudfront.CachePolicy.CACHING_OPTIMIZED ), # 为自定义域名配置 SSL 证书需要提前在 ACM 中申请 domain_names[domain_name], certificatecloudfront.Certificate.from_certificate_arn( self, Certificate, certificate_arnarn:aws:acm:us-east-1:123456789012:certificate/xxxx-xxxx-xxxx ) ) # 3. 可选配置 Route53将域名指向 CloudFront # 这需要你拥有该域名的 Route53 托管区域 # hosted_zone route53.HostedZone.from_lookup( # self, # HostedZone, # domain_namedomain_name # ) # route53.ARecord( # self, # AliasRecord, # zonehosted_zone, # record_namedomain_name, # targetroute53.RecordTarget.from_alias( # targets.CloudFrontTarget(self.distribution) # ) # ) # 4. 输出关键信息方便后续使用 CfnOutput( self, WebsiteURL, valuefhttps://{domain_name}, descriptionThe URL of the deployed website ) CfnOutput( self, S3BucketName, valueself.bucket.bucket_name, descriptionThe 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_namedomain_name ) # 3. 创建 Lambda 函数用于处理动态请求 # 注意我们使用 Code.from_asset 从本地 lambda/ 目录加载代码 self.handler_function lambda_.Function( self, FormHandler, runtimelambda_.Runtime.PYTHON_3_11, handlerhandler.lambda_handler, # 指向 lambda/handler.py 中的函数 codelambda_.Code.from_asset(lambda), timeoutDuration.seconds(30), memory_size256 ) # 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, valueself.handler_function.function_arn, descriptionThe 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 fsubmissions/{uuid.uuid4().hex}.json # 3. 将数据写入 S3 桶 # 注意这里使用了 website.bucket.bucket_name 作为环境变量传入 s3_client.put_object( Bucketos.environ[BUCKET_NAME], Keyfile_key, Bodyjson.dumps({ name: name, email: email, timestamp: context.invoked_function_arn }), ContentTypeapplication/json ) return { statusCode: 200, body: json.dumps({message: Submission received!}) } except Exception as e: print(fError 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