AWS CDK实战:用Python构建Serverless应用的现代工程实践
对于习惯了Python的开发者来说,第一次接触云基础设施部署,往往会被那些冗长、重复且难以调试的JSON或YAML模板劝退。你是否有过这样的经历:为了部署一个简单的API后端,需要在控制台反复点击,或者在一个上千行的CloudFormation模板里小心翼翼地修改某个参数,生怕一个缩进错误就让整个部署失败?这种体验,与我们在应用开发中追求的敏捷、可测试和可维护性背道而驰。
AWS Cloud Development Kit的出现,正是为了解决这种割裂感。它不是一个全新的工具,而是一种思维方式的转变——将基础设施视为真正的应用程序代码。这意味着,你可以用自己最熟悉的Python语法,去定义和编排AWS上的资源。想象一下,用面向对象的方式去创建一个DynamoDB表,用函数调用的方式去配置Lambda函数的权限,甚至可以将一整套复杂的微服务架构封装成一个可复用的“积木块”。这不仅仅是效率的提升,更是工程实践的一次升级。
本文面向那些希望快速上手AWS CDK,并专注于Serverless架构的Python开发者。我们将抛开那些抽象的概念,直接从一个包含API Gateway、Lambda函数和DynamoDB的完整Serverless应用入手。通过一步步的代码构建和原理解析,你将不仅学会“怎么做”,更能理解CDK背后的设计哲学,从而能够自信地将其应用到更复杂的生产场景中。
1. 环境准备与项目初始化
在开始编写任何基础设施代码之前,一个稳定且配置正确的开发环境是基石。与传统的Python项目不同,CDK项目对环境和工具有着特定的要求,但设置过程本身并不复杂。
首先,你需要确保本地已经安装了Node.js(>= 10.13.0)和Python(>= 3.6)。Node.js是CDK命令行工具(CLI)的运行环境,而Python则是我们编写构造代码的语言。你可以通过以下命令快速检查:
node --version
python --version
pip --version
接下来,通过npm全局安装AWS CDK命令行工具。这是你与CDK交互的主要入口,用于合成模板、部署栈、比较变更等。
npm install -g aws-cdk
安装完成后,运行 cdk --version 来验证安装是否成功。同时,你需要配置好AWS CLI的访问凭证,确保CDK有权限在你的AWS账户中创建资源。通常,这通过 aws configure 命令来完成,设置好你的Access Key、Secret Key、默认区域和输出格式。
注意:在生产环境中,强烈建议使用IAM角色或临时凭证,而非将长期访问密钥硬编码在本地。对于CI/CD场景,可以使用OpenID Connect或AWS Secrets Manager来管理凭证。
环境就绪后,我们就可以创建第一个CDK项目了。CDK CLI提供了项目初始化的脚手架功能。打开终端,进入你的工作目录,执行:
mkdir my-first-serverless-cdk && cd my-first-serverless-cdk
cdk init app --language python
这个命令会生成一个标准的Python CDK项目结构。让我们花点时间理解一下这个结构,这对于后续的开发和团队协作至关重要:
my-first-serverless-cdk/
├── app.py # 应用的入口点,定义栈(Stack)
├── cdk.json # CDK工具配置文件
├── requirements.txt # Python依赖声明
├── source.bat # Windows环境激活脚本
├── README.md
│
├── my_first_serverless_cdk/ # 你的主要构造库目录
│ ├── __init__.py
│ └── my_first_serverless_cdk_stack.py # 主栈的定义
│
└── tests/ # 单元测试目录
└── unit/
└── test_my_first_serverless_cdk_stack.py
关键文件解读:
- app.py: 这是应用的起点。它实例化一个
core.App对象,并在这个应用中创建我们的栈。你可以在这里为不同的环境(开发、测试、生产)创建不同的栈实例。 - my_first_serverless_cdk_stack.py: 这是我们主要的工作文件。
Stack类是CDK中的核心概念,代表一组在AWS上作为一个单元进行部署和管理的资源集合。我们所有的Lambda、API Gateway、DynamoDB等资源都将在这个类中定义。 - cdk.json: 告诉CDK工具如何执行你的应用。例如,它指定了运行
app.py的命令。 - requirements.txt: 列出了项目所需的Python包,主要是
aws-cdk-lib和相关的构造库。
初始化完成后,建议立即创建一个Python虚拟环境来隔离项目依赖,这是一个良好的Python开发习惯。
python -m venv .venv
# 在Windows上激活:.venv\Scripts\activate
# 在macOS/Linux上激活:source .venv/bin/activate
pip install -r requirements.txt
至此,你的开发环境已经准备就绪,一个干净的CDK项目骨架也已搭建完成。接下来,我们就可以将注意力转向真正的业务逻辑——用代码定义我们的Serverless应用。
2. 核心概念:从Construct到Stack的认知模型
在动手编写资源代码前,理解CDK的抽象层次模型至关重要。这能帮助你在未来设计出更清晰、更可复用的架构。CDK的核心抽象可以看作一个三层模型,从最基础的组件到完整的部署单元。
第一层:L1 Constructs(Level 1,CFN资源)
这是最底层的抽象,与CloudFormation资源几乎是一一对应的关系。每个L1构造都直接映射到CloudFormation模板中的一个资源类型(如 AWS::Lambda::Function)。当你使用L1构造时,你需要配置所有属性,就像直接编写CloudFormation模板一样。它的优势是功能完整、控制力强,但代码会显得冗长。
from aws_cdk import aws_lambda as _lambda
from aws_cdk import core as cdk
# 使用L1构造定义Lambda函数(不推荐日常使用,仅作示例)
# 你需要手动指定Runtime、Handler、Code等所有属性
# 代码可读性和简洁性较差
第二层:L2 Constructs(Level 2,经过Curated的抽象) 这是CDK的“甜点”,也是我们最常使用的层次。L2构造在L1的基础上提供了更高级别的、符合最佳实践的默认值,并封装了复杂的关联资源创建逻辑。例如,创建一个DynamoDB表时,L2构造会自动为你配置加密、点播容量模式等安全且经济的默认设置。它极大地减少了样板代码,并提升了安全性。
from aws_cdk import aws_dynamodb as dynamodb
# 使用L2构造定义DynamoDB表
table = dynamodb.Table(
self, "MyTable",
partition_key=dynamodb.Attribute(
name="id",
type=dynamodb.AttributeType.STRING
),
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, # 推荐的按需计费模式
encryption=dynamodb.TableEncryption.AWS_MANAGED, # 默认启用加密
)
# 短短几行,就创建了一个安全、生产就绪的表,无需关心底层CloudFormation的复杂属性。
第三层:L3 Constructs(Patterns,解决方案模式)
这是最高级别的抽象,通常代表一个完整的、经过验证的架构模式。例如,一个 LambdaRestApi 构造会一次性创建API Gateway和一个将其与Lambda函数集成的代理。Patterns旨在解决特定场景,让你用极少的代码快速搭建出复杂架构。
理解了构造的层次,我们再来看它们是如何被组织的。Construct 是所有资源的基础类,代表云组件。Stack 是Construct的子类,是一个部署单元,其内部的所有资源会一起被创建、更新或删除。一个CDK应用(App)则可以包含多个Stack,这让你能够按环境、按功能模块来逻辑隔离资源。
| 概念 | 类比 | 作用 | 示例 |
|---|---|---|---|
| Construct | 乐高积木块 | 云组件的抽象基类,可以是单个资源或一组资源 | aws_lambda.Function, aws_dynamodb.Table |
| Stack | 乐高模型(如一辆车) | 一个可部署的单元,包含一组相关的Constructs | MyServerlessBackendStack |
| App | 乐高套装说明书 | 定义了所有Stack的组织方式,是CDK程序的入口 | core.App |
这种层次化的设计带来了巨大的灵活性。你可以将几个相关的L2 Construct(比如一个VPC加上其中的安全组和子网)封装成一个自定义的、可复用的L3 Construct,然后在多个Stack中像使用标准库一样引用它。这实现了真正的基础设施即代码(IaC)的“代码复用”,是传统模板方式难以企及的。
3. 实战:构建一个完整的Serverless REST API
现在,让我们运用前面学到的知识,来构建一个具体的Serverless应用:一个简单的待办事项(Todo)API。这个API将提供创建、读取、更新和删除待办事项的功能。我们将使用API Gateway作为HTTP接口,Lambda函数处理业务逻辑,DynamoDB作为数据存储。
首先,打开 my_first_serverless_cdk_stack.py 文件。我们将从定义数据存储层开始。
3.1 定义数据存储层(DynamoDB)
在 __init__ 方法中,我们首先创建一个DynamoDB表。我们将使用按请求付费的模式,这对于访问模式不确定的Serverless应用来说是最经济的选择。
from aws_cdk import (
aws_dynamodb as dynamodb,
RemovalPolicy,
Stack,
)
from constructs import Construct
class MyFirstServerlessCdkStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# 创建DynamoDB表
todo_table = dynamodb.Table(
self, "TodoTable",
partition_key=dynamodb.Attribute(
name="id",
type=dynamodb.AttributeType.STRING
),
# 按请求付费,无需预置容量
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
# 删除栈时自动删除表(仅用于开发环境!生产环境慎用)
removal_policy=RemovalPolicy.DESTROY,
)
提示:
RemovalPolicy.DESTROY会在执行cdk destroy时删除该资源,非常适合开发和测试环境,可以避免产生残留资源。但在生产环境中,你很可能希望将其设置为RemovalPolicy.RETAIN以防止误删数据。
3.2 创建业务逻辑层(Lambda函数)
接下来,我们创建处理API请求的Lambda函数。我们将函数的代码放在项目根目录下的 lambda 文件夹中。首先创建这个文件夹和函数文件。
mkdir -p lambda
touch lambda/todo_handler.py
编辑 lambda/todo_handler.py,写入一个简单的处理函数。为了简化,我们在这里处理所有HTTP方法。
import json
import os
import boto3
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb')
table_name = os.environ['TODO_TABLE_NAME']
table = dynamodb.Table(table_name)
def lambda_handler(event, context):
http_method = event['httpMethod']
path = event['path']
if http_method == 'GET' and path == '/todos':
# 获取所有待办事项
response = table.scan()
items = response.get('Items', [])
return {
'statusCode': 200,
'body': json.dumps(items)
}
elif http_method == 'POST' and path == '/todos':
# 创建新的待办事项
item = json.loads(event['body'])
item['id'] = context.aws_request_id # 使用请求ID作为简单ID
table.put_item(Item=item)
return {
'statusCode': 201,
'body': json.dumps(item)
}
else:
return {
'statusCode': 400,
'body': json.dumps({'message': 'Unsupported operation'})
}
现在,回到Stack定义文件,添加Lambda函数的构造。我们使用 aws_lambda.Function 这个L2构造,并指定代码路径和运行时。
from aws_cdk import aws_lambda as _lambda
from aws_cdk import aws_lambda_python_alpha as lambda_python
import os
# 在 __init__ 方法内,创建DynamoDB表之后
# 创建Lambda函数
todo_handler = _lambda.Function(
self, "TodoHandler",
runtime=_lambda.Runtime.PYTHON_3_9,
# 指定代码目录。使用Code.from_asset指向包含代码的文件夹。
code=_lambda.Code.from_asset("lambda"),
handler="todo_handler.lambda_handler",
environment={
"TODO_TABLE_NAME": todo_table.table_name
}
)
这里,我们通过 environment 参数将DynamoDB表的名称传递给Lambda函数,这样函数代码就能知道要操作哪张表。但仅仅传递名称还不够,Lambda函数还需要被授予访问这张表的权限。
3.3 配置权限与集成(IAM与API Gateway)
CDK的一个强大特性是它能够以声明式的方式管理权限。我们不需要去手动编写复杂的IAM策略文档,而是直接调用资源对象上的 grant 方法。
# 授予Lambda函数对DynamoDB表的读写权限
todo_table.grant_read_write_data(todo_handler)
这一行代码背后,CDK会自动生成并附加一个精确的、遵循最小权限原则的IAM策略到Lambda函数的执行角色上。这比手动管理策略安全且高效得多。
最后,我们创建API Gateway,并将其与Lambda函数集成。这里我们使用 LambdaRestApi 这个L3构造(Pattern),它能快速创建一个代理所有请求到单个Lambda函数的REST API。
from aws_cdk import aws_apigateway as apigateway
# 创建API Gateway并与Lambda集成
api = apigateway.LambdaRestApi(
self, "TodoApi",
handler=todo_handler,
proxy=False, # 设置为False以便自定义资源和方法
)
# 定义 /todos 资源
todos_resource = api.root.add_resource("todos")
# 为 /todos 添加GET和POST方法
todos_resource.add_method("GET")
todos_resource.add_method("POST")
将以上所有代码片段组合起来,你的 MyFirstServerlessCdkStack 类就完整了。现在,整个Serverless后端的基础设施已经用不到50行的Python代码定义完毕。我们可以预览将要生成的CloudFormation模板。
4. 部署、测试与进阶运维
代码编写完成后,真正的价值在于将其部署到云端并验证其功能。CDK CLI提供了一系列命令来支持这个流程。
4.1 合成与部署
首先,运行 cdk synth。这个命令会执行你的CDK应用,生成对应的CloudFormation模板,但不会进行任何实际的部署。这是检查你的代码是否正确的第一步,也是查看CDK为你生成了哪些AWS资源的好方法。
cdk synth
输出结果是一个庞大的YAML文件,这就是最终会被提交到CloudFormation的模板。你可以看到里面包含了我们定义的DynamoDB表、Lambda函数、IAM角色、API Gateway等所有资源的详细配置。
接下来,在首次部署前,你需要引导(bootstrap)你的AWS环境和区域。CDK需要一个S3存储桶来存储部署过程中生成的资产(如Lambda函数代码包)。每个账户和区域只需引导一次。
cdk bootstrap
引导完成后,就可以进行首次部署了。使用 cdk deploy 命令,CDK会显示一个变更集,列出将要创建、修改或删除的资源,确认无误后即可执行。
cdk deploy
部署成功后,命令行会输出API Gateway的调用URL,格式类似于 https://xxxxxxxxxx.execute-api.region.amazonaws.com/prod/。
4.2 测试与验证
拿到URL后,我们可以使用 curl 或 Postman 等工具来测试API。
- 创建待办事项:
curl -X POST https://your-api-id.execute-api.region.amazonaws.com/prod/todos \ -H "Content-Type: application/json" \ -d '{"task": "Learn AWS CDK", "completed": false}' - 获取所有待办事项:
curl https://your-api-id.execute-api.region.amazonaws.com/prod/todos
如果一切正常,你应该能收到成功的响应。这标志着你已经成功地用代码定义并部署了一个完整的、可工作的Serverless应用。
4.3 进阶运维与最佳实践
当应用进入迭代开发阶段,你会频繁地修改代码和基础设施。CDK提供了强大的工具来管理这些变更。
- 预览变更 (
cdk diff):在部署前,始终运行cdk diff。它会比较当前已部署的栈和本地代码生成的栈之间的差异,清晰地告诉你哪些资源会被添加、修改或删除。这是防止意外变更的安全网。 - 基础设施测试:CDK构造也是代码,理应被测试。你可以编写单元测试来验证你创建的Construct是否具有预期的属性。CDK提供了
@aws-cdk/assertions模块来辅助测试。# 示例测试:验证DynamoDB表是否被创建且加密开启 def test_dynamodb_table_created(self): stack = MyFirstServerlessCdkStack(app, "TestStack") template = Template.from_stack(stack) template.has_resource_properties("AWS::DynamoDB::Table", { "SSESpecification": {"SSEEnabled": True}, "BillingMode": "PAY_PER_REQUEST" }) - 多环境管理:一个常见的模式是为开发、预发布和生产环境使用不同的配置。你可以通过向Stack传递参数来实现。
class TodoServiceStage(cdk.Stage): def __init__(self, scope, id, *, env_name=None, **kwargs): super().__init__(scope, id, **kwargs) TodoServiceStack(self, f"TodoService-{env_name}", env_name=env_name) # 在app.py中定义不同环境 app = cdk.App() TodoServiceStage(app, "Dev", env_name="dev") TodoServiceStage(app, "Prod", env_name="prod") app.synth() - 使用CDK Pipelines进行CI/CD:对于团队协作和自动化部署,可以配置CDK Pipeline。它本身也是一个用CDK定义的基础设施,能够监听代码仓库的变更,自动执行合成、测试和部署到多个环境,实现真正的基础设施持续交付。
在项目规模增长后,一个清晰的项目结构至关重要。我个人的习惯是将不同的服务或功能模块拆分到不同的Stack或Construct中,并通过Stack之间的输出来传递ARN、ID等引用信息。例如,将网络(VPC)、数据库、无服务器API分别放在不同的Stack里,这样在更新时的影响范围更小,也更容易进行权限隔离和成本核算。
从在控制台手动点击,到编写上千行的YAML模板,再到用几十行富有表达力的Python代码定义整个应用架构,这种体验的飞跃是实实在在的。CDK不仅仅是一个工具,它更是一种将开发实践带入运维领域的桥梁。当你下次需要为一个新功能添加一个SQS队列或一个EventBridge规则时,你会发现自己更愿意去更新几行清晰的Python代码,而不是在晦涩的JSON中挣扎。这种开发体验上的根本性改善,才是AWS CDK带给开发者最宝贵的礼物。
1320



被折叠的 条评论
为什么被折叠?



