跳到正文 ChatGPT 说 · Jimmy Huang's Blog
必做
Exercise 1 测试 GET 请求,获取全部任务或指定任务 必做 理解 Lambda event / HTTP method / path parameter
Exercise 2 修正 DELETE 和 PUT 逻辑 必做 理解 RESTful CRUD 和 DynamoDB 操作
Task 3 用 API Gateway 创建 REST API 必做 把 HTTP 请求暴露给外部用户
Task 4 用 S3 触发 Lambda 导入 CSV 到 DynamoDB 必做 理解 event-driven architecture
Task 5 设置 Cloud9 开发环境 Optional 云端 IDE / AWS CLI
Task 6 用 AWS SAM 部署 Hello World Serverless App Optional Serverless IaC / CloudFormation / SAM
1.3 整个实验的主线是什么
1.4 这周每个 AWS 服务分别负责什么
DynamoDB DynamoDB 是 AWS 托管的 NoSQL 数据库,用来保存 Todo 数据。
Lambda Lambda 是一个不用你管理服务器的函数运行环境。 服务器在哪
服务器怎么启动
操作系统怎么维护
扩容怎么做
API Gateway API Gateway 是应用的 HTTP 入口,负责把外部请求转发给 Lambda。 用户不会直接找 Lambda,而是通过 API Gateway 访问:
S3
IAM Role / LabRole 例如 Lambda 想写 DynamoDB,就必须有权限。
本 Lab 因为 AWS Academy 限制,不能自由创建 IAM Role,所以官方要求你选择:
SAM AWS SAM 是专门用来开发和部署 Serverless 应用的工具。 SAM 背后会使用 CloudFormation 创建资源。
2. PDF 中所有需要动手完成的任务清单
2.1 前置要求
2.2 Task 1: Data Persistence 打开 DynamoDB dashboard
点击 Create table
Primary key type 选择: String创建表
等表状态变成 Active
老师可能会演示: 手动创建 item
filter table
query table
2.3 Task 2: Lambda Business Logic 打开 Lambda dashboard
点击 Create function
Function name 填: todo_handlerExecution role 选择: Use existing roleRole 选择: LabRole / labRole创建 Lambda function
把官方给的 JavaScript 代码复制到 Lambda editor
点击 Deploy
创建 Test Event
用 POST 测试插入一条 Todo
Exercise 2: 找出 DELETE 和 PUT 代码问题
修改成能正常工作的版本
2.4 Task 3: API Gateway REST API 打开 API Gateway
选择 REST API
点击 Build
选择 New API
创建 API
在 /api 下创建 resource: /tasks对 /api/tasks 启用 CORS
在 /api/tasks 下创建 GET method
GET method 连接到 Lambda todo_handler
在 /api/tasks 下创建 POST method
POST method 连接到 Lambda todo_handler
测试 POST:
在 /api/tasks 下创建子 resource:
对 /api/tasks/{taskId} 启用 CORS
给 /api/tasks/{taskId} 创建 GET method
测试 GET 指定 task
Deploy API
创建 Stage,例如:
用浏览器或 curl 测试 deployed URL
2.5 Task 4: Importing Todo Tasks from CSV Files 创建一个新的 S3 bucket
这个 bucket 用来存放 CSV 文件
创建新的 Lambda function: todo_import_handlerRole 选择: LabRole / labRole在 Lambda 的 Function Overview / Designer 区域点击 Add trigger
Bucket 选择刚刚创建的 bucket
添加 trigger
替换 Lambda 模板代码为官方 Python 代码
创建 CSV 文件
CSV 每行 3 列:
上传 CSV 到 S3 bucket
验证 DynamoDB table 里出现新 records
如果没有立刻出现:
2.6 Task 5: Setup Cloud9 Development Environment 打开 Cloud9
Create Environment
填 name 和 optional description
默认配置即可
创建环境
等环境 ready
在 Cloud9 terminal 运行: aws sts get-caller-identity 如果 AWS Academy session 过期,需要重新: aws configure 官方文档说明 Cloud9 可能因为 AWS Academy Learner Lab 的 IAM 限制,需要你自己的 AWS free account。
2.7 Task 6: Create Hello-World Serverless Application with SAM 在 Cloud9 terminal 运行: sam init 选择: AWS Quick Start Templates给项目起名字
进入 SAM 创建的项目目录
根据提示配置 deployment
复制 OutputValue 到浏览器或用 curl 测试
删除 SAM 应用时,需要去 CloudFormation 删除 stack
3. Step-by-step 操作指导
Task 0:开始前检查环境
Step 0.1:登录 AWS Academy 打开 AWS Academy 登录页面
进入 Learner Lab
Start Lab
等 AWS 按钮变成可点击
点击 AWS 进入 AWS Console
AWS Academy 给你的不是永久 AWS 账号,而是一个有时间限制的 Lab session。
为什么本 Lab 使用 LabRole,而不是自己创建 IAM Role? 因为 AWS Academy learner account 通常限制 IAM 权限,不能自由创建完整 IAM role,所以 Lab 要求使用已有的 LabRole。
Step 0.2:统一 Region 进入 AWS Console 后,右上角检查 Region。
保证 DynamoDB、Lambda、API Gateway、S3 都在同一个区域。 如果你在 A 城市建数据库,却在 B 城市运行 Lambda,初学者很容易以为“表不见了”。
Task 1:创建 DynamoDB Table
Step 1.1:打开 DynamoDB 在 AWS Console 顶部搜索框搜索: DynamoDB进入 DynamoDB dashboard
进入数据库服务,准备创建保存 Todo 数据的表。
Step 1.2:创建 todos 表
Step 1.3:这一步背后的原理 DynamoDB 是一个 NoSQL database。
Step 1.4:哪些地方必须理解 todos 是表名
id 是每条任务的唯一标识
Lambda 后面会通过 table name 找到这个表
如果表名拼错,Lambda 会报错
为什么 DynamoDB table 需要 primary key? 因为数据库需要用 primary key 唯一识别每一条 item。这里每个 Todo task 都用 id 作为唯一标识。
Task 2:创建 Lambda 实现 Todo 业务逻辑
Step 2.1:创建 Lambda function 进入 Lambda dashboard
Function name 填: todo_handlerPermissions / Execution role 部分: 展开: Change default execution roleExisting role 选择: LabRole / labRole你会进入 Lambda function 编辑页面,函数名是:
Step 2.2:这一步背后的原理 这周它要根据不同 HTTP method 做不同事情:
Step 2.3:复制官方 Lambda 代码并 Deploy 在 Lambda 代码编辑器里,把原有内容删掉,粘贴官方代码:
Step 2.4:创建 POST Test Event Event name 可以填: postTodoTestEvent JSON 填:
然后去 DynamoDB todos 表里查看 items,应该能看到这条任务。
Step 2.5:这一步背后的原理 Lambda Test Event 是在模拟 API Gateway 以后会传给 Lambda 的 event。
Step 2.6:检查 DynamoDB 里是否真的写入 打开 DynamoDB
进入 Tables
查看是否出现刚刚创建的 task
不是只看 Lambda 返回成功,而是确认数据真的持久化进数据库。 你怎么证明 POST request 真的写入数据库了? 我可以在 DynamoDB 的 todos table 里查看 item,确认 Lambda 的 POST handler 调用了 dynamo.put() 并插入了记录。
Exercise 1:测试 GET 请求
Step 2.7:测试 GET 全部 tasks 返回 body 里应该包含 DynamoDB scan 的结果,例如:
模拟 GET /tasks,读取所有 Todo tasks。
Step 2.8:测试 GET 指定 task 先从 DynamoDB 复制一条 item 的 id。
返回 body 里只包含这个 id 对应的 task。
Step 2.9:这一步背后的原理 pathParameters 来自 URL 路径中的动态部分。
Exercise 2:修正 DELETE 和 PUT 的问题
Step 2.10:先理解官方代码的问题 官方代码中 DELETE 和 PUT 是这样写的:
它要求前端 / API 调用者直接把 DynamoDB SDK 需要的完整参数传进来。 也就是说,DELETE 如果按照原代码工作,body 可能得长这样:
然后 Lambda 自己组装 DynamoDB 参数:
Step 2.11:替换成推荐修正版 Lambda 代码 为了让 GET / POST / PUT / DELETE 都更符合 REST API 思路,建议把 Lambda 代码替换成下面这个完整版本。
回到 Lambda code editor
删除原代码
粘贴下面代码
点击 Deploy
Step 2.12:测试 PUT 然后回 DynamoDB 检查,对应 item 应该被更新。
Step 2.13:测试 DELETE 然后回 DynamoDB 检查,对应 item 应该消失。
Step 2.14:PUT / DELETE 背后的原理 REST API 负责表达“业务动作”,DynamoDB SDK 参数是后端内部实现细节。 原始代码直接把 event.body parse 后交给 dynamo.delete() 或 dynamo.update(),这要求 API 调用方传完整 DynamoDB 参数。更合理的方式是让调用方只传 task id 和更新内容,然后 Lambda 自己组装 TableName、 Key、 UpdateExpression 等 DynamoDB 参数。
Task 3:用 API Gateway 创建 REST API
Step 3.1:创建 REST API AWS Console 搜索: API Gateway进入 API Gateway
找到 REST API
这里要选 REST API,不是 HTTP API。
Step 3.2:创建 /api resource 在 API Gateway 的 Resources 页面
Resource Path 应该自动变成: /api
Step 3.3:创建 /api/tasks resource 并启用 CORS Actions -> Create Resource
勾选: Enable API Gateway CORS点击 Create Resource
Step 3.4:CORS 是什么 在本 Lab 里,先勾选 CORS 是为了让以后前端调用这个 API 时不容易被浏览器拦截。
curl 或浏览器直接打开 GET URL 时,CORS 通常不是最大问题。
Step 3.5:在 /api/tasks 下创建 GET method 点击 checkmark
Integration type 选择: Lambda Function建议勾选: Use Lambda Proxy integrationLambda Region 选择你 Lambda 所在的 Region
Lambda Function 填: todo_handler保存
如果弹出 Add Permission to Lambda Function,点击 OK
GET /api/tasks 会连接到 Lambda todo_handler。
Step 3.6:为什么建议使用 Lambda Proxy integration Lambda Proxy integration 的作用是:
API Gateway 会把 HTTP request 的 method、headers、body、path parameters 等信息打包成 event,直接交给 Lambda。 如果没有正确传入这些字段,Lambda 里面的 switch 可能不知道这是 GET 还是 POST。
Step 3.7:在 /api/tasks 下创建 POST method Actions -> Create Method
Integration type: Lambda Function勾选: Use Lambda Proxy integrationLambda function: todo_handler保存
Add Permission 时点击 OK
其中 OPTIONS 通常是 CORS 自动生成的。
Step 3.8:在 API Gateway 内部测试 POST Headers 填: Content-Type: application/jsonRequest Body 填:
Step 3.9:创建 /api/tasks/{taskId} resource Actions -> Create Resource
Resource Path 应该也是: /{taskId}勾选: Enable API Gateway CORS点击 Create Resource
Step 3.10:给 /api/tasks/{taskId} 创建 GET method Actions -> Create Method
Integration type: Lambda Function勾选: Use Lambda Proxy integrationLambda function: todo_handler保存
Add Permission 时点击 OK
Step 3.11:可选:创建 PUT 和 DELETE method 但因为 Exercise 2 要修正 PUT 和 DELETE,如果你想完整测试,可以额外创建:
Actions -> Create Method
Integration 都连接到: todo_handler勾选 Lambda Proxy integration
保存并允许 Add Permission
Step 3.12:Deploy API API Gateway 里创建 / 修改 methods 后,不 deploy 的话,外部 URL 不会更新。 Description 可选: Lab09 first deployment点击 Deploy
Step 3.13:用浏览器测试 GET
Step 3.14:用 curl 测试 GET 和 POST
Step 3.15:API Gateway 背后的原理 Lambda 不是设计成公开 HTTP URL 的 REST API 服务。API Gateway 提供正式的 HTTP endpoint,可以管理路径、方法、CORS、认证、部署 stage,并把请求转发给 Lambda。
Step 3.16:常见错误排查
错误 1:浏览器显示 Missing Authentication Token URL 路径写错
没有 deploy API
method 没创建
stage name 写错
访问了 /prod/tasks,但实际应该是 /prod/api/tasks
错误 2:POST 返回 500 Lambda 没 deploy 最新代码
Lambda 没权限访问 DynamoDB
DynamoDB table name 不叫 todos
Region 不一致
API Gateway 没使用 Lambda Proxy integration
先在 Lambda Test Event 里测 POST
Lambda 自己成功后,再测 API Gateway Test
API Gateway Test 成功后,再测 deployed URL
去 CloudWatch logs 看 Lambda 报错
错误 3:改了 API 但 URL 还是旧结果 API Gateway REST API 修改后,需要重新 Deploy API。
Task 4:S3 上传 CSV 自动导入 DynamoDB
Step 4.1:创建 S3 bucket 进入 S3
Bucket name 填一个全球唯一名字,例如: lab09-todo-import-你的名字或学号
Step 4.2:创建导入用 Lambda function 进入 Lambda dashboard
点击 Create function
Function name 填: todo_import_handlerExecution role 选择: Use existing roleRole 选择: LabRole / labRole点击 Create function
Step 4.3:给 Lambda 添加 S3 Trigger 打开 todo_import_handler
找到 Function Overview / Designer
Bucket 选择刚刚创建的 bucket
Event type 选择类似: All object create events点击 Add
Function Overview 里应该能看到:
告诉 AWS:只要这个 bucket 有新文件上传,就自动调用这个 Lambda。
Step 4.4:粘贴 Python Lambda 代码
Step 4.5:理解这段 Python 代码 S3 可能一次告诉 Lambda 多个新文件,所以代码逐个处理。
从 S3 event 里拿到 bucket 名字和文件名。
把这一行 CSV 转成 DynamoDB item 并插入 todos table。
Step 4.6:创建 CSV 文件 如果老师明确要求 CSV 第一行必须是 header,你可以保留 header,但要知道原始代码会把 header 也读进去。
为了验证功能,先用无 header 的 3 行数据。
Step 4.7:上传 CSV 到 S3 打开 S3
进入你的 bucket
点击 Upload
选择 tasks.csv
点击 Upload
Step 4.8:验证 DynamoDB 是否插入成功 打开 DynamoDB
进入 todos
点击 Explore table items
查看是否多了 CSV 中的 tasks
Step 4.9:如果没有插入成功,怎么排查
第一优先:检查 Lambda 有没有被触发 S3 trigger 没加成功
你上传到了错误 bucket
Event type 没选 ObjectCreated
Region 不一致
上传发生在 trigger 创建之前
第二优先:检查 CloudWatch Logs
第三优先:检查 CSV 格式
Step 4.10:Task 4 背后的核心原理 这一步是 event-driven architecture。
Serverless 不只是“不管服务器”,它也常常和 event-driven 架构一起使用。 S3 trigger 和 API Gateway trigger 有什么不同? API Gateway trigger 是用户发 HTTP request 后调用 Lambda;S3 trigger 是文件上传事件发生后自动调用 Lambda。前者是 request-driven,后者是 event-driven。
Task 5:设置 Cloud9 开发环境 Optional
Step 5.1:创建 Cloud9 Environment 官方文档说 Cloud9 可能因为 learner lab 的 IAM 限制,需要你自己的 AWS free account。
如果老师没有要求,Task 5 可以视为 optional。
打开 Cloud9 dashboard
Description 可选
下一页默认即可
点击 Create environment
等几分钟
Cloud9 IDE 打开,底部有 terminal。
Step 5.2:验证 AWS CLI Cloud9 里的 AWS CLI 已经能代表你的账号调用 AWS 服务。
Step 5.3:如果 session expired AWS Academy starter account 会过期。
然后根据 AWS Academy 提供的 access key / secret key / session token 重新配置。
这一步可能每个课程环境不一样,按老师提供的 Academy Instructions 为准。
Task 6:用 SAM 创建 Hello World Serverless App Optional
Step 6.1:初始化 SAM 项目
Step 6.2:进入项目目录 如果项目名是 sam-hello-world,运行:
Step 6.3:构建项目
Step 6.4:部署项目 如果 AWS Academy 不允许 IAM role creation,这一步可能失败。
这也是为什么官方说 Cloud9 / SAM 可能需要自己的 free AWS account。
Step 6.5:测试 HelloWorldApi 返回 Hello World 相关 JSON 或文字。
Step 6.6:SAM 背后的原理
4. 完成后清理资源
4.1 API Gateway
4.2 Lambda
4.3 DynamoDB
4.4 S3 bucket 进入 bucket
Empty bucket
Delete bucket
4.5 Cloud9 / SAM 去 CloudFormation 删除对应 stack,例如:
5. 本周容易出错的地方
5.1 Region 不一致
5.2 表名不一致
5.3 Primary key 不一致 所以 DynamoDB primary key 必须是:
5.4 Lambda 改完代码忘记 Deploy
5.5 API Gateway 改完忘记 Deploy API API Gateway Test 可以,但外部 URL 不行。 每次修改 API Gateway resources / methods 后:
5.6 没有使用 Lambda Proxy integration 创建 API Gateway method 时,建议勾选:
5.7 GET 指定 task 不知道 id 从哪来 POST 创建 task 后,Lambda 返回的 body 里有 id
DynamoDB table 里可以复制 id
5.8 CSV header 被当成数据插入
5.9 CSV 字段里不能随便带逗号
6. 本周必须学会的知识点
6.1 Serverless 是什么 Serverless 不是没有服务器,而是你不用管理服务器。
6.2 FaaS 是什么 FaaS = Function as a Service。
你把一个函数交给云平台,云平台在需要时帮你运行它。
6.3 Lambda event 是什么 Lambda 的代码不变,但不同 trigger 传进来的 event 格式不同。
6.4 API Gateway 和 Lambda 的关系 API Gateway 负责 HTTP 入口,Lambda 负责业务逻辑。
6.5 DynamoDB 和 Lambda 的关系 Lambda 通过 AWS SDK 读写 DynamoDB。
6.6 S3 Trigger 是什么 S3 trigger 是“文件上传后自动运行 Lambda”。
6.7 RESTful API 的 CRUD 对应关系
7. 老师可能会问的问题
Q1:什么是 Serverless? Serverless 不是没有服务器,而是开发者不用自己管理服务器。云平台负责底层运行环境、扩容和维护。开发者主要关注函数代码和服务之间的连接。本 Lab 中 Lambda、API Gateway、DynamoDB 和 S3 组合成了一个 Serverless Todo application。
Q2:Lambda 在这个应用里负责什么? Lambda 负责业务逻辑。它根据 event.httpMethod 判断请求是 GET、POST、PUT 还是 DELETE,然后调用 DynamoDB 完成读取、新建、更新或删除 Todo task。
Q3:API Gateway 在这个应用里负责什么? API Gateway 负责提供 HTTP endpoint。用户通过 URL 调用 API Gateway,API Gateway 再把请求转发给 Lambda。它相当于应用的 front door。
Q4:DynamoDB 在这个应用里负责什么? DynamoDB 负责数据持久化。Todo tasks 会作为 items 存在 todos table 中,每条 task 使用 id 作为 primary key。
Q5:为什么要使用 id 作为 primary key? 因为每个 Todo task 都需要一个唯一标识。Lambda 更新、查询或删除指定 task 时,都可以通过这个 id 找到对应 item。
Q6:POST 请求是怎么写入 DynamoDB 的? API Gateway 把 POST 请求传给 Lambda,Lambda 从 event.body 里解析出 title、desc、done,然后生成一个 id,最后调用 dynamo.put() 把 item 写入 todos table。
Q7:GET 全部 task 和 GET 指定 task 的区别是什么? GET 全部 task 不需要 path parameter,所以 Lambda 会 scan 整个 table。GET 指定 task 会通过 /api/tasks/{taskId} 传入 event.pathParameters.taskId,Lambda 可以根据 id 查找对应 item。
Q8:原始 DELETE / PUT 代码有什么问题? 原始代码直接把 event.body 解析后传给 dynamo.delete() 和 dynamo.update(),这要求 API 调用者传完整 DynamoDB SDK 参数。更合理的 REST API 设计是让调用者只传 task id 和要更新的业务字段,由 Lambda 自己组装 DynamoDB 的 TableName、Key、UpdateExpression 等参数。
Q9:S3 trigger 是怎么工作的? 当 CSV 文件上传到 S3 bucket 时,S3 会产生 ObjectCreated event,并自动调用 todo_import_handler Lambda。Lambda 从 event 中拿到 bucket 和 object key,读取 CSV 内容,然后逐行插入 DynamoDB。
Q10:SAM 和手动 Console 操作有什么区别? 手动 Console 是通过网页一步一步创建资源。SAM 是用项目文件和 template.yaml 声明 Serverless 应用,然后通过 sam build 和 sam deploy 自动创建 Lambda、API Gateway、S3 bucket、IAM roles 等资源。SAM 更适合真实项目和可重复部署。
8. 完成后自测题
题 1 DynamoDB table 名字错、Region 错,或者 table 还没有创建成功。
题 2 为什么 API Gateway 修改后要 Deploy API?
因为 REST API 的修改不会自动发布到外部 invoke URL。必须 deploy 到某个 stage,例如 prod,外部访问的 URL 才会使用最新配置。
题 3 如果 Lambda 由 API Gateway 触发,并且使用 Lambda proxy integration,API Gateway 会把 HTTP method 放进 Lambda event 里的 httpMethod 字段。
题 4 event.pathParameters.taskId 对应 URL 的哪一部分?
对应 /api/tasks/{taskId} 里的 {taskId}。例如访问 /api/tasks/abc-123,那么 event.pathParameters.taskId 就是 abc-123。
题 5 为什么 S3 上传 CSV 可以自动触发 Lambda?
因为给 Lambda 配置了 S3 trigger。当 S3 bucket 中发生 ObjectCreated 事件时,AWS 会自动调用 Lambda,并把上传文件的信息放进 event。
题 6 不是。服务器仍然存在,只是由 AWS 管理。开发者不用直接 provision、patch、scale 或维护服务器。
题 7 因为 AWS Academy learner account 通常限制 IAM 操作,不能自由创建完整角色和权限。LabRole 是学习环境预先配置好的 role,可以让 Lambda 访问所需 AWS 服务。
题 8 如果 CSV 上传后 DynamoDB 没变化,你第一步看哪里?
先看 todo_import_handler 的 Monitor tab 里的 Invocation count,确认 Lambda 有没有被 S3 触发。
9. 用更简单的话再解释一遍本周内容
10. 最终完成标准 Checklist 用户 / 浏览器 / curl
|
v
API Gateway
负责接收 HTTP 请求
|
v
Lambda
负责运行后端业务逻辑
|
v
DynamoDB
负责保存 Todo 数据上传 CSV 文件到 S3
|
v
S3 自动触发 Lambda
|
v
Lambda 读取 CSV
|
v
写入 DynamoDB第一阶段:先准备数据库
DynamoDB table: todos
第二阶段:写后端逻辑
Lambda function: todo_handler
第三阶段:把后端逻辑变成真正的 HTTP API
API Gateway REST API
第四阶段:实现自动化导入
S3 upload CSV -> trigger Lambda -> write to DynamoDB
第五阶段:了解更工程化的 Serverless 开发方式
Cloud9 + SAMDynamoDB = 云上的 Excel / 表格 / 数据抽屉收到什么请求?
要执行什么逻辑?
要返回什么结果?API Gateway = 餐厅前台 / 门口接待员
Lambda = 后厨
DynamoDB = 仓库GET /api/tasks
POST /api/tasks
GET /api/tasks/{taskId}tasks.csv 上传到 S3
|
v
触发 Lambda
|
v
读取 CSV 内容
|
v
写入 DynamoDBLambda = 员工
IAM Role = 员工工牌
权限 = 这个工牌允许员工做什么手动点 AWS Console = 一步一步手工搭积木
SAM = 用配置文件自动搭积木Lambda 在 us-east-1
DynamoDB 在 ap-southeast-2正确情况:
us-east-1
├── DynamoDB: todos
├── Lambda: todo_handler
├── API Gateway: todo-api
└── S3 bucket: todo-import-bucket
容易出错的情况:
us-east-1
├── Lambda: todo_handler
ap-southeast-2
└── DynamoDB: todos
结果:
Lambda 默认可能找不到你正在看的那个 table。Table name: todos
Partition key: id
Partition key type: StringDynamoDB table = 一个表格
id = 每一行的唯一编号
item = 表格里的一行数据
attribute = 每一行里的字段DynamoDB
└── Table: todos
├── Item 1
│ ├── id
│ ├── title
│ ├── desc
│ └── done
├── Item 2
│ ├── id
│ ├── title
│ ├── desc
│ └── done
└── Item 3
├── id
├── title
├── desc
└── doneGET -> 读取 tasks
POST -> 新建 task
PUT -> 更新 task
DELETE -> 删除 taskHTTP request
|
v
API Gateway
|
v
Lambda event
|
v
todo_handler 判断 event.httpMethod
|
├── GET -> dynamo.scan / dynamo.get
├── POST -> dynamo.put
├── PUT -> dynamo.update
└── DELETE -> dynamo.deleteevent.httpMethod = 告诉 Lambda 这是 GET / POST / PUT / DELETE
event.body = 请求正文
context.awsRequestId = AWS 自动给这次 Lambda 调用生成的唯一 ID如果请求方法是 POST
就把 body 里的 title / desc / done 组装成 data
然后 put 到 DynamoDB 的 todos 表如果 URL 路径里传来了 taskId
就只查这个 task
否则就查全部 tasksGET /api/tasks
|
v
event.httpMethod = "GET"
event.pathParameters = null / undefined
|
v
读取全部 tasks
GET /api/tasks/abc-123
|
v
event.httpMethod = "GET"
event.pathParameters.taskId = "abc-123"
|
v
读取指定 taskDELETE /api/tasks/abc-123前端 / API 用户:
只需要告诉你“我要删哪个 task”
Lambda:
负责把这个业务请求转换成 DynamoDB 能理解的操作参数找到某个 id 的 item
然后更新 title / desc / done / updatedAt如果前端网页来自 A 域名
但它想调用 B 域名的 API
浏览器会先检查 B 是否允许 A 调用GET /api/tasks
|
v
API Gateway
|
v
Lambda event:
{
"httpMethod": "GET",
"path": "/api/tasks",
"headers": {...},
"body": null
}/
└── /api
└── /tasks
└── /{taskId}GET /api/tasks
POST /api/tasks
GET /api/tasks/{taskId}PUT /api/tasks/{taskId}
DELETE /api/tasks/{taskId}/api/tasks
├── GET 获取全部 tasks
└── POST 创建 task
/api/tasks/{taskId}
├── GET 获取指定 task
├── PUT 更新指定 task
└── DELETE 删除指定 taskhttps://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prodhttps://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/api/taskshttps://你的-api-id.execute-api.你的-region.amazonaws.com/prod/api/tasks外部 HTTP 世界
|
v
API Gateway
|
v
Lambda
|
v
DynamoDBActions -> Deploy API -> prod只能小写字母、数字、短横线
不能用大写
不能用下划线
必须全球唯一lab09-todo-import-jihao-2026S3 -> todo_import_handlerUpload tasks.csv to S3
|
v
S3 ObjectCreated Event
|
v
todo_import_handler
|
v
Read CSV content
|
v
DynamoDB put_itemBuy milk,2L milk,false
Finish lab,Complete serverless lab,false
Submit report,Upload PDF,truetitle,desc,done
Buy milk,2L milk,falsetitle = title
desc = desc
done = doneLambda -> todo_import_handler -> MonitorLambda -> todo_import_handler -> Monitor -> View CloudWatch logs传统方式:
用户手动点按钮
|
v
后端运行
Event-driven 方式:
事件发生:S3 有新文件
|
v
自动触发 Lambda
|
v
自动处理文件AWS Quick Start Templatestemplate.yaml
hello-world/
README.mdStack Name: sam-hello-world
AWS Region: 当前 Region
Confirm changes before deploy: Y
Allow SAM CLI IAM role creation: Y
Disable rollback: N
Save arguments to configuration file: YLambda function
API Gateway
S3 deployment bucket
IAM roles / policies
CloudFormation stack你的 SAM 项目
|
v
template.yaml
|
v
sam build
|
v
sam deploy
|
v
CloudFormation stack
|
├── Lambda
├── API Gateway
├── S3 bucket
└── IAM rolestodo_handler
todo_import_handlerActions -> Deploy API -> prodUnsupported method undefinedUse Lambda Proxy integrationtitle,desc,done
Buy milk,2L milk,falsetitle = title
desc = desc
done = doneBuy milk,2L milk,false
Finish lab,Complete serverless lab,falseBuy milk,2L, full cream,falseevent = 触发 Lambda 的事件内容
context = AWS 提供的本次运行上下文curl / browser / frontend
|
v
API Gateway
解析 HTTP method / path / body
|
v
Lambda
执行代码
|
v
DynamoDB
读写数据手动上传 CSV 文件
|
v
S3 产生 ObjectCreated event
|
v
Lambda 自动被调用
|
v
读取 CSV
|
v
写入 DynamoDBGET /api/tasks 获取全部
POST /api/tasks 创建一个
GET /api/tasks/{id} 获取指定
PUT /api/tasks/{id} 更新指定
DELETE /api/tasks/{id} 删除指定ResourceNotFoundException: Requested resource not found买一台服务器
装 Node.js
写 Express API
连数据库
自己部署
自己维护不用自己买服务器
不用自己跑 Express
不用自己维护数据库机器API Gateway = 接待员
Lambda = 干活的人
DynamoDB = 存数据的柜子
S3 = 放文件的仓库
S3 Trigger = 仓库一有新文件就自动通知干活的人
SAM = 自动搭建这些服务的工具用户发请求
|
API Gateway 接收
|
Lambda 执行业务逻辑
|
DynamoDB 保存或读取数据你上传 CSV 到 S3
|
S3 自动触发 Lambda
|
Lambda 读取 CSV
|
写入 DynamoDB一个 Serverless 应用通常由多个云服务协作完成。
每个服务只负责一部分:
API Gateway 负责入口
Lambda 负责逻辑
DynamoDB 负责数据
S3 负责文件
Trigger 负责事件驱动{ " id " : " abc-123 " , " title " : " First Task " , " desc " : " sample description " , " done " : false, " updatedAt " : 1710000000000 } { " id " : " unique-id " , " title " : " First Task " , " desc " : " sample description " , " done " : false } import { DynamoDB } from ' @aws-sdk/client-dynamodb ' ; import { DynamoDBDocument } from ' @aws-sdk/lib-dynamodb ' ; const dynamo = DynamoDBDocument.from(new DynamoDB()); export const handler = async (event, context) => { let body; let statusCode = ' 200 ' ; const headers = { ' Content-Type ' : ' application/json ' ,}; const tableName = ' todos ' ; try { switch (event.httpMethod) { case ' DELETE ' : body = await dynamo.delete(JSON.parse(event.body)); break ; case ' GET ' : if (event.pathParameters && event.pathParameters.taskId) { let params = { TableName: tableName, FilterExpression: " id = :id " , ExpressionAttributeValues: { " :id " : event.pathParameters.taskId }, }; body = await dynamo.scan(params); } else body = await dynamo.scan({ TableName: tableName }); break ; case ' POST ' : var data = {}; data.desc = event.body && JSON.parse(event.body.trim()).desc || '' ; data.done = event.body && JSON.parse(event.body.trim()).done || false; data.title = event.body && JSON.parse(event.body.trim()).title || '' ; data.id = context.awsRequestId; data.updatedAt = new Date().getTime(); let params = {TableName: tableName, Item: data}; body = await dynamo.put(params); body.message = data; statusCode = ' 201 ' ; break ; case ' PUT ' : body = await dynamo.update(JSON.parse(event.body)); break ; default : throw new Error( ` Unsupported method " ${ event . httpMethod } " ` ); } } catch (err) { statusCode = ' 400 ' ; body = err.message; } finally { body = JSON.stringify(body); } return { statusCode, body, headers, }; }; { " body " : " {\n\"title\" :\"First Task\"\n,\"desc\" :\"sample description\"} " , " httpMethod " : " POST " } { " statusCode " : " 201 " , " body " : " ... " , " headers " : { " Content-Type " : " application/json " } } { " message " : { " desc " : " sample description " , " done " : false, " title " : " First Task " , " id " : " 某个自动生成的 request id " , " updatedAt " : 1710000000000 } } { " body " : " {...} " , " httpMethod " : " POST " } case ' POST ' : ... let params = {TableName: tableName, Item: data}; body = await dynamo.put(params); { " Items " : [ { " id " : " xxx " , " title " : " First Task " , " desc " : " sample description " , " done " : false } ], " Count " : 1, " ScannedCount " : 1 } { " httpMethod " : " GET " , " pathParameters " : { " taskId " : " 把这里替换成你的真实 id " } } { " httpMethod " : " GET " , " pathParameters " : { " taskId " : " abc-123 " } } if (event.pathParameters && event.pathParameters.taskId) { ... } { " pathParameters " : { " taskId " : " abc-123 " } } case ' DELETE ' : body = await dynamo.delete(JSON.parse(event.body)); break ; case ' PUT ' : body = await dynamo.update(JSON.parse(event.body)); break ; { " TableName " : " todos " , " Key " : { " id " : " abc-123 " } } { TableName: " todos " , Key: { id: " abc-123 " } } { " httpMethod " : " PUT " , " pathParameters " : { " taskId " : " 把这里替换成你的真实 id " }, " body " : " {\"title\":\"Updated Task\",\"desc\":\"updated description\",\"done\":true} " } { " statusCode " : 200, " body " : " {\"message\":{...更新后的 task...}} " } { " httpMethod " : " DELETE " , " pathParameters " : { " taskId " : " 把这里替换成你的真实 id " } } { " statusCode " : 200, " body " : " {\"message\":\"Task xxx deleted successfully\"} " } { TableName: ' todos ' , Key: { id: ' xxx ' }, UpdateExpression: ' set title = :title ' , ExpressionAttributeValues: { ' :title ' : ' new title ' } } { TableName: ' todos ' , Key: { id: ' xxx ' } } event.httpMethod event.body event.pathParameters { " title " : " newtask-from-gateway " } { " Items " : [ { " id " : " xxx " , " title " : " First Task " , " desc " : " sample description " , " done " : false } ], " Count " : 1, " ScannedCount " : 1 } curl -X GET " https://你的-api-id.execute-api.你的-region.amazonaws.com/prod/api/tasks " curl -X POST " https://你的-api-id.execute-api.你的-region.amazonaws.com/prod/api/tasks " \ -H " Content-Type: application/json " \ -d ' {"title":"newtask-from-curl","desc":"created from curl","done":false} ' curl -X GET " https://你的-api-id.execute-api.你的-region.amazonaws.com/prod/api/tasks/把这里换成真实id " curl -X PUT " https://你的-api-id.execute-api.你的-region.amazonaws.com/prod/api/tasks/把这里换成真实id " \ -H " Content-Type: application/json " \ -d ' {"title":"updated from curl","desc":"updated desc","done":true} ' curl -X DELETE " https://你的-api-id.execute-api.你的-region.amazonaws.com/prod/api/tasks/把这里换成真实id " import json import boto3 import os import sys import uuid from urllib.parse import unquote_plus s3_client = boto3.client( ' s3 ' ) dynamodb = boto3.client( ' dynamodb ' ) TABLE_NAME = ' todos ' def lambda_handler(event, context): for record in event[ ' Records ' ]: bucket = record[ ' s3 ' ][ ' bucket ' ][ ' name ' ] key = unquote_plus(record[ ' s3 ' ][ ' object ' ][ ' key ' ]) print( " File{0} uploaded to{1} bucket " .format(key, bucket)) csvfile = s3_client.get_object(Bucket=bucket, Key=key) csvcontent = csvfile[ ' Body ' ].read().decode( ' utf8 ' ).splitlines() for line in csvcontent: tokens = line.strip().split( " , " ) if len(tokens) > 2: data = {} data[ ' id ' ] = { ' S ' : str(uuid.uuid4())} data[ ' title ' ] = { ' S ' : tokens[0].strip()} data[ ' desc ' ] = { ' S ' : tokens[1].strip()} data[ ' done ' ] = { ' S ' : tokens[2].strip()} response = dynamodb.put_item(TableName=TABLE_NAME, Item=data) return { ' statusCode ' : 200, ' body ' : json.dumps( ' Records successfully inserted into database... ' ) } for record in event[ ' Records ' ]: bucket = record[ ' s3 ' ][ ' bucket ' ][ ' name ' ] key = unquote_plus(record[ ' s3 ' ][ ' object ' ][ ' key ' ]) csvfile = s3_client.get_object(Bucket=bucket, Key=key) csvcontent = csvfile[ ' Body ' ].read().decode( ' utf8 ' ).splitlines() tokens = line.strip().split( " , " ) tokens[0] = " Buy milk " tokens[1] = " 2L milk " tokens[2] = " false " dynamodb.put_item(TableName=TABLE_NAME, Item=data) aws sts get-caller-identity { " UserId " : " ... " , " Account " : " ... " , " Arn " : " ... " } curl " 你的-HelloWorldApi-URL " const tableName = ' todos ' ; event.httpMethod event.body event.pathParameters export const handler = async (event, context) => { ... }; { " httpMethod " : " POST " , " body " : " {\"title\":\"abc\"} " , " pathParameters " : { " taskId " : " xxx " } } { " Records " : [ { " s3 " : { " bucket " : { " name " : " bucket-name " }, " object " : { " key " : " tasks.csv " } } } ] } await dynamo.put({ TableName: ' todos ' , Item: item, }); import { DynamoDB } from ' @aws-sdk/client-dynamodb ' ; import { DynamoDBDocument } from ' @aws-sdk/lib-dynamodb ' ; const dynamo = DynamoDBDocument.from(new DynamoDB()); const tableName = ' todos ' ; function makeResponse(statusCode, body) { return { statusCode, headers: { ' Content-Type ' : ' application/json ' , }, body: JSON.stringify(body), }; } function parseBody(event) { if (!event.body) { return {}; } if (typeof event.body === ' object ' ) { return event.body; } return JSON.parse(event.body.trim()); } export const handler = async (event, context) => { try { const method = event.httpMethod; switch (method) { case ' GET ' : { const taskId = event.pathParameters?.taskId; if (taskId) { const result = await dynamo.get({ TableName: tableName, Key: { id: taskId, }, }); return makeResponse(200, result.Item || {}); } const result = await dynamo.scan({ TableName: tableName, }); return makeResponse(200, result); } case ' POST ' : { const data = parseBody(event); const item = { id: context.awsRequestId, title: data.title || '' , desc: data.desc || '' , done: data.done ?? false, updatedAt: new Date().getTime(), }; await dynamo.put({ TableName: tableName, Item: item, }); return makeResponse(201, { message: item, }); } case ' PUT ' : { const data = parseBody(event); const id = event.pathParameters?.taskId || data.id; if (!id) { throw new Error( ' Missing task id for PUT request ' ); } const result = await dynamo.update({ TableName: tableName, Key: { id: id, }, UpdateExpression: ' set title = :title, #desc = :desc, done = :done, updatedAt = :updatedAt ' , ExpressionAttributeNames: { ' #desc ' : ' desc ' , }, ExpressionAttributeValues: { ' :title ' : data.title || '' , ' :desc ' : data.desc || '' , ' :done ' : data.done ?? false, ' :updatedAt ' : new Date().getTime(), }, ReturnValues: ' ALL_NEW ' , }); return makeResponse(200, { message: result.Attributes, }); } case ' DELETE ' : { const data = parseBody(event); const id = event.pathParameters?.taskId || data.id; if (!id) { throw new Error( ' Missing task id for DELETE request ' ); } await dynamo.delete({ TableName: tableName, Key: { id: id, }, }); return makeResponse(200, { message: ` Task ${ id } deleted successfully ` , }); } default : throw new Error( ` Unsupported method " ${ method } " ` ); } } catch (err) { return makeResponse(400, { error: err.message, }); } };