基于 Serverless Framework 的 Python HTTP API + DynamoDB 实战:Todo 增删改查全流程解析
基于 Serverless Framework 的 Python HTTP API + DynamoDB 实战:Todo 增删改查全流程解析
本文围绕开源仓库 examples 中的 aws-python-http-api-with-dynamodb 示例,系统讲解如何利用 Serverless Framework 在 AWS 上构建一个支持创建、查询、更新、删除(CRUD)Todo 的 HTTP API,并以 DynamoDB 作为数据存储后端。读完本文,你将掌握 serverless.yml 的完整部署配置、五个 Lambda 函数的实现思路与底层调用链、通过 curl 实测 API 的完整姿势,以及 Lambda 并发与 DynamoDB 容量规划的关键要点。
一、示例概览:HTTP API + DynamoDB 的 Todo 服务
该示例的核心目标是:搭建一个 HTTP API,允许对 Todo 进行 create(创建)、list(列出)、get(单个查询)、update(更新)、delete(删除)五种操作,数据统一存放在 DynamoDB 表中。示例作者在文档开头也特别说明:DynamoDB 只是存储后端的一个示例选择,实际项目中完全可以替换为任意数据存储。
示例明确列出的两类典型应用场景:
- Web 应用的 API 后端;
- 移动应用的 API 后端。
从架构上看,这个服务属于典型的"API Gateway(HTTP API)+ Lambda + DynamoDB"无服务器三层结构:客户端通过 HTTP 请求触发 API 事件,Serverless Framework 将请求路由到对应的 Lambda 函数,函数通过 boto3 访问 DynamoDB 完成数据读写。
二、目录结构与"一操作一文件"设计思想
整个服务的函数代码集中在 todos 目录下,其设计原则非常清晰:
- 每个操作对应一个文件,例如 todos/delete.py;
- 每个文件内只定义一个函数,例如
create函数、list函数等。
目录布局如下:
aws-python-http-api-with-dynamodb/
├── todos/
│ ├── __init__.py
│ ├── create.py # 创建 Todo
│ ├── list.py # 列出全部 Todo
│ ├── get.py # 按 id 获取单个 Todo
│ ├── update.py # 更新 Todo
│ ├── delete.py # 删除 Todo
│ └── decimalencoder.py # Decimal 序列化工具
├── serverless.yml # 部署配置
├── package.json
└── README.md
原文档特别解释了这一目录结构背后的扩展思路:todos 目录的用意是,如果你想在同一个服务里承载多个资源(例如 users、notes、comments),可以把它们放在同一个服务的不同目录下。虽然技术上完全可行,但作者也建议:每个资源单独拆分为一个服务可能更清晰,具体取决于实际用例和个人偏好。这是无服务器架构中"单体服务 vs 细粒度服务"的经典取舍,也是本示例在目录设计上想要传递的工程思想。
三、部署配置深度解析:serverless.yml 逐段拆解
部署配置位于 serverless.yml,它完整定义了服务名、运行时、函数事件映射、IAM 权限与 DynamoDB 表资源。下面逐段解读。
3.1 服务声明与框架版本
service: serverless-http-api-dynamodb
frameworkVersion: '4'
service:服务名,同时被用作函数名与 DynamoDB 表名的前缀,形成命名空间;frameworkVersion: '4':声明本项目基于 Serverless Framework v4 编写(当前仓库的示例均以此为目标框架版本)。
3.2 provider 配置:运行时、架构与环境变量
provider:
name: aws
runtime: python3.14
architecture: arm64
environment:
DYNAMODB_TABLE: ${self:service}-${sls:stage}
httpApi:
cors: true
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:${aws:region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
关键点说明:
runtime: python3.14:Lambda 函数运行环境为 Python 3.14;architecture: arm64:选择 ARM 架构(Graviton),通常比 x86 更经济;DYNAMODB_TABLE:通过变量引用self:service(服务名)与sls:stage(部署阶段)动态拼出表名,例如默认 dev 阶段下表名为serverless-http-api-dynamodb-dev。这样同一份配置在不同 stage(dev/prod)下会自动使用不同的表,避免数据串扰;httpApi.cors: true:一键为 HTTP API 开启 CORS 支持,方便浏览器端(Web/移动前端)直接跨域调用;iam.role.statements:为函数授予的最小权限集——Query、Scan、GetItem、PutItem、UpdateItem、DeleteItem 六项 DynamoDB 操作,且Resource通过 ARN 精确限定到当前服务对应的那张表,遵循最小权限原则。
3.3 functions:五个函数与 HTTP 事件映射
functions:
create:
handler: todos/create.create
events:
- httpApi:
path: /todos
method: post
list:
handler: todos/list.list
events:
- httpApi:
path: /todos
method: get
get:
handler: todos/get.get
events:
- httpApi:
path: /todos/{id}
method: get
update:
handler: todos/update.update
events:
- httpApi:
path: /todos/{id}
method: put
delete:
handler: todos/delete.delete
events:
- httpApi:
path: /todos/{id}
method: delete
事件映射遵循 REST 惯例:
| 函数 | Handler | 路径 | 方法 | 用途 |
|---|---|---|---|---|
| create | todos/create.py | /todos |
POST | 创建 |
| list | todos/list.py | /todos |
GET | 列出全部 |
| get | todos/get.py | /todos/{id} |
GET | 单个查询 |
| update | todos/update.py | /todos/{id} |
PUT | 更新 |
| delete | todos/delete.py | /todos/{id} |
DELETE | 删除 |
注意 handler 的写法:todos/create.create 表示模块路径 todos/create.py 中的 create 函数——这正是"一个文件一个函数"约定在部署层落地的方式。路径参数 {id} 会出现在函数收到的 event['pathParameters']['id'] 中(见第五、七节)。
3.4 resources:DynamoDB 表定义
resources:
Resources:
TodosDynamoDbTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: Retain
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
TableName: ${self:provider.environment.DYNAMODB_TABLE}
- 主键设计:以
id(字符串 S 类型)作为唯一的哈希键(HASH),所有 get/update/delete 都按该主键精确定位; BillingMode: PAY_PER_REQUEST:采用按需计费模式,表容量随流量自动伸缩,无需预设读写容量,适合流量波动大的场景;DeletionPolicy: Retain:删除 CloudFormation 堆栈时保留 DynamoDB 表及其数据,避免误删生产数据。
package.json(查看)只是最简单的元信息声明(MIT 许可),本示例不依赖任何 npm 包,纯 Python + boto3 即可运行。
四、环境准备与部署上线
4.1 安装 Serverless Framework
在本地安装全局 CLI(Node.js 环境需就绪):
npm install -g serverless
4.2 执行部署
在项目目录(即 aws-python-http-api-with-dynamodb)下运行:
serverless deploy
原文档给出了预期的部署输出,关键信息如下:
Service Information
service: serverless-http-api-dynamodb
stage: dev
region: us-east-1
endpoints:
POST - https://45wf34z5yf.execute-api.us-east-1.amazonaws.com/todos
GET - https://45wf34z5yf.execute-api.us-east-1.amazonaws.com/todos
GET - https://45wf34z5yf.execute-api.us-east-1.amazonaws.com/todos/{id}
PUT - https://45wf34z5yf.execute-api.us-east-1.amazonaws.com/todos/{id}
DELETE - https://45wf34z5yf.execute-api.us-east-1.amazonaws.com/todos/{id}
functions:
update: serverless-http-api-dynamodb-dev-update
get: serverless-http-api-dynamodb-dev-get
list: serverless-http-api-dynamodb-dev-list
create: serverless-http-api-dynamodb-dev-create
delete: serverless-http-api-dynamodb-dev-delete
从输出可以直观确认:服务自动进入 dev 阶段、部署到 us-east-1 区域(可在配置或部署参数中调整),API Gateway 生成了 5 个 REST 风格端点,5 个 Lambda 函数命名统一为 服务名-阶段-函数名 格式。部署完成后,即可用下文 curl 命令实测接口。
五、五个 Lambda 函数的源码实现剖析
所有函数都遵循统一模式:boto3.resource('dynamodb') 初始化客户端,从 os.environ['DYNAMODB_TABLE'] 读取表名,完成数据操作后返回带 statusCode 与 body 的标准 Lambda 响应结构。
5.1 create:写入新 Todo
实现见 todos/create.py,核心流程:
json.loads(event['body'])解析请求体;- 参数校验:若请求体中不含
text字段,记录错误日志并抛异常("Couldn't create the todo item."); - 以
uuid.uuid1()生成主键id; - 构造完整 item:
text(内容)、checked: False(默认未完成)、createdAt与updatedAt均为str(time.time())(字符串形式的秒级时间戳); table.put_item(Item=item)写入 DynamoDB;- 返回
statusCode: 200与json.dumps(item)。
值得注意:虽然原文档在"Create a Todo"一节标注为 "No output",但从源码看,create 函数实际会以 200 状态码返回新创建 Todo 的完整 JSON 内容——这是 README 描述与真实实现的一处差异,实测调用时请以实际返回为准。
5.2 list:Scan 全表
实现见 todos/list.py:直接执行 table.scan() 拉取全部条目,并以 json.dumps(result['Items'], cls=decimalencoder.DecimalEncoder) 序列化返回。
这里引入了本示例的关键工具类 DecimalEncoder(见 todos/decimalencoder.py):DynamoDB 返回的数字类型在 boto3 中表现为 decimal.Decimal,而标准 JSON 序列化无法直接处理它(这是 Python bug 16535 的经典场景),因此自定义编码器将 Decimal 转为 int 后再输出。这也解释了为什么 list 与 get 都要显式传入 cls=decimalencoder.DecimalEncoder。
5.3 get:按主键查询单个 Todo
实现见 todos/get.py:从 event['pathParameters']['id'] 取路径参数,执行 table.get_item(Key={'id': ...}),返回 result['Item'] 的 JSON 序列化结果。由于 id 正是表的主键(HASH 键),该查询按主键精确定位,效率最高。
5.4 update:UpdateExpression 局部更新
实现见 todos/update.py,流程比 create 更丰富:
- 解析请求体并校验
text、checked两个字段必须同时存在; - 时间戳改用
int(time.time() * 1000)(毫秒级整数)——与 create 的秒级字符串时间戳格式不同,这是本示例实现中两个函数时间戳格式不一致的细节,实际使用需自行统一; - 通过
update_item执行表达式更新:
table.update_item(
Key={'id': event['pathParameters']['id']},
ExpressionAttributeNames={'#todo_text': 'text'},
ExpressionAttributeValues={
':text': data['text'],
':checked': data['checked'],
':updatedAt': timestamp,
},
UpdateExpression='SET #todo_text = :text, '
'checked = :checked, '
'updatedAt = :updatedAt',
ReturnValues='ALL_NEW',
)
ExpressionAttributeNames中的#todo_text是text的占位别名(因为text可能触及保留字/特殊语义,使用别名更安全);UpdateExpression用表达式而非整条覆盖,实现真正的局部更新;ReturnValues='ALL_NEW'让更新操作直接返回更新后的完整条目,响应体即最新数据。
5.5 delete:按主键删除
实现见 todos/delete.py:执行 table.delete_item(Key={'id': ...}),返回仅含 statusCode: 200 的响应体,无内容输出——与原文档 "No output" 的描述一致。
5.6 小结:五个函数的分工对照
| 函数 | DynamoDB 操作 | 入参来源 | 响应内容 |
|---|---|---|---|
| create | PutItem | event['body'] |
创建的 item |
| list | Scan | — | 全部 items |
| get | GetItem | event['pathParameters']['id'] |
单个 item |
| update | UpdateItem | body + 路径 id | 更新后的 item(ALL_NEW) |
| delete | DeleteItem | event['pathParameters']['id'] |
仅状态码 |
六、API 使用指南:curl 实战
部署完成后,用下列命令对端点(https://XXXXXXX.execute-api.us-east-1.amazonaws.com 为你的实际部署地址)进行增删改查。
6.1 创建 Todo
curl -X POST https://XXXXXXX.execute-api.us-east-1.amazonaws.com/todos --data '{ "text": "Learn Serverless" }' -H "Content-Type: application/json"
如上文所述,实际会返回新创建的 Todo(含 id、checked: false、createdAt/updatedAt)。
6.2 列出全部 Todos
curl https://XXXXXXX.execute-api.us-east-1.amazonaws.com/todos
原文档给出的示例输出(注意末尾的 % 是终端回显,非响应内容):
[{"text":"Deploy my first service","id":"ac90feaa11e6-9ede-afdfa051af86","checked":true,"updatedAt":1479139961304},{"text":"Learn Serverless","id":"206793aa11e6-9ede-afdfa051af86","createdAt":1479139943241,"checked":false,"updatedAt":1479139943241}]
可以看到 list 返回的是 JSON 数组,包含多笔记录及各自的 text、id、checked、时间戳字段。
6.3 获取单个 Todo
# 将 <id> 替换为 todos 表中真实的 id
curl https://XXXXXXX.execute-api.us-east-1.amazonaws.com/todos/<id>
示例结果:
{"text":"Learn Serverless","id":"ee6490d0-aa11e6-9ede-afdfa051af86","createdAt":1479138570824,"checked":false,"updatedAt":1479138570824}
6.4 更新 Todo
# 将 <id> 替换为 todos 表中真实的 id
curl -X PUT https://XXXXXXX.execute-api.us-east-1.amazonaws.com/todos/<id> --data '{ "text": "Learn Serverless", "checked": true }' -H "Content-Type: application/json"
请求体必须同时携带 text 与 checked(源码校验),示例结果中 checked 已变为 true,updatedAt 同步刷新:
{"text":"Learn Serverless","id":"ee6490d0-aa11e6-9ede-afdfa051af86","createdAt":1479138570824,"checked":true,"updatedAt":1479138570824}
6.5 删除 Todo
# 将 <id> 替换为 todos 表中真实的 id
curl -X DELETE https://XXXXXXX.execute-api.us-east-1.amazonaws.com/todos/<id>
无输出内容,仅返回 200 状态码。
七、事件对象关键字段速查
无论使用哪个 API 客户端,Lambda 收到的 event 对象中与本示例直接相关的字段有:
| 字段 | 来源 | 用途 |
|---|---|---|
event['body'] |
请求体 | create/update 解析 JSON 负载 |
event['pathParameters']['id'] |
URL 路径 {id} 段 |
get/update/delete 的主键定位 |
os.environ['DYNAMODB_TABLE'] |
部署时注入的环境变量 | 定位目标表(如 serverless-http-api-dynamodb-dev) |
其中 DYNAMODB_TABLE 由 serverless.yml 中的 ${self:service}-${sls:stage} 变量在部署时自动解析,函数代码只需读取环境变量即可,无需硬编码表名,从而保证多环境复用。
八、容量规划与伸缩要点
8.1 AWS Lambda 并发限制
原文档指出:默认情况下,AWS Lambda 将某个区域内所有函数的总并发执行数限制为 1000。该默认限制是保护性安全阈值,用于防止开发和测试初期因失控或递归函数产生不可控成本。如需提升上限,需要按照 AWS 官方流程申请提升并发限制(参见 Lambda 文档中 "request a limit increase for concurrent executions" 相关章节)。
8.2 DynamoDB 容量模式:按需计费 vs 预置吞吐
原文档在缩放章节给出的预置吞吐示例为:
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
即在 serverless.yml 的 TodosDynamoDbTable.Properties 下配置预置读写容量单位(RCU/WCU),DynamoDB 会预留相应资源以保障一致、低延迟的性能,且支持随时增减。
需要特别说明:当前仓库版本的 serverless.yml 实际采用的是 BillingMode: PAY_PER_REQUEST(按需计费),即无需预设容量、随实际读写请求自动伸缩。两种模式各有适用场景:
- PAY_PER_REQUEST(按需):适合流量不可预测、波动明显的应用,按实际请求量计费,无需容量规划;
- ProvisionedThroughput(预置):适合流量可预测、需要精细成本控制的应用,可显式设置读写容量并随时调整。
若预期流量存在大幅波动,原文档还建议关注 DynamoDB 的自动扩缩容方案(即 AWS 官方博客介绍的 Dynamic DynamoDB 一类的做法),以在预置模式下按需动态调整容量。
九、小结
本示例用最精简的结构串联起了无服务器开发的核心链路:
- 配置层:一份 serverless.yml 同时完成 API 路由、IAM 最小权限与 DynamoDB 表资源声明;
- 实现层:
todos目录下五个独立文件对应五个函数,职责单一、易于扩展(可在同一服务中加入 users/notes/comments,或按资源拆分独立服务); - 调用层:标准的 REST 端点 + curl 即可完成全部增删改查,返回体统一为 JSON;
- 运维层:默认按需计费的表模式与最小权限 IAM 体现了成本与安全的最佳实践,Lambda 并发与表容量是上线前需要关注的伸缩边界。
无论是作为 Web 应用还是移动应用的 API 后端,这套"HTTP API + Python Lambda + DynamoDB"的样板都提供了可以直接复制改造的完整起点;后续在此基础上叠加鉴权、日志、前端托管等能力,即可演进为生产级无服务器应用。