跳到正文 Lab 09: Serverless Architecture in AWS · Jimmy Huang's Blog
API Gateway 如何把 HTTP 请求转发给 Lambda
DynamoDB 如何作为 Serverless NoSQL 数据库使用
S3 文件上传如何触发 Lambda 自动处理数据
SAM 如何用代码方式创建和部署 Serverless 应用
如何测试、验证和排查一个 Serverless Todo API
Task 0: 准备 AWS Academy 环境 本 Task 主要是确认你可以进入 AWS Academy,并且后面所有服务都在同一个 AWS Region 下创建。
需要使用 AWS Academy Learner Lab
后面会用到 DynamoDB、Lambda、API Gateway、S3
注意所有资源尽量放在同一个 Region,例如 us-east-1
AWS Academy 环境通常没有完整 IAM 权限,所以创建 Lambda 时要选择已有的 LabRole
实验结束后要删除或停止资源,避免产生额外费用
Step 0.1:登录 AWS Academy 进入 Learner Lab 后,启动 Lab 环境,并点击进入 AWS Console。
你是在进入学校提供的 AWS 实验账号。这个账号已经预配置了一些权限和角色,例如本周会用到的 LabRole。
本周要创建真实 AWS 资源,例如 Lambda、DynamoDB、S3 和 API Gateway。没有启动 Learner Lab 的话,后面的 AWS Console 可能没有权限或无法创建资源。
Step 0.2:确认 Region 进入 AWS Console 后,右上角确认当前 Region。
AWS 的很多资源是按 Region 隔离的。比如你在 us-east-1 创建了 DynamoDB 表,但 Lambda 在另一个 Region,就可能找不到表。
为了避免后面出现“明明创建了资源但 Lambda/API Gateway 找不到”的问题,所有服务最好统一放在同一个 Region。
Task 1: Data Persistence - 创建 DynamoDB 表 本 Task 要先创建 Todo 应用的数据存储层。
Step 1.1:进入 DynamoDB Dashboard 你正在创建一个 NoSQL 数据库表,用来保存 Todo 应用里的任务数据。
Serverless 架构里,Lambda 本身不适合长期保存数据。Lambda 每次调用可能是新的运行环境,所以持久化数据要交给数据库。这里使用 AWS 托管的 DynamoDB。
Step 1.2:创建 todos 表 你创建了一个 DynamoDB 表,表名是 todos,每条记录通过 id 唯一标识。
Todo 应用中的每个任务都需要一个唯一 ID。后面 Lambda 创建任务时,会自动生成一个 ID 并写入 DynamoDB。
创建完成后,你应该能在 DynamoDB 的 Tables 列表中看到:
Step 1.3:理解 DynamoDB 表结构 你需要知道后面 Lambda 写入 DynamoDB 的数据大概长什么样。
后面测试 POST、GET、PUT、DELETE 的时候,你需要回到 DynamoDB 表里检查数据是否真的被写入或修改。
Task 2: Implement Business Logic Using Lambda 本 Task 要用 AWS Lambda 实现 Todo 应用的业务逻辑。
Step 2.1:创建 Lambda Function 进入 Lambda Dashboard 后,点击:
你创建了一个 Lambda function,它会接收 API Gateway 转发过来的 HTTP 请求,并根据请求方法执行不同逻辑。
在 Serverless 架构里,Lambda 就相当于后端业务逻辑代码。你不需要管理服务器,只需要写函数。
Step 2.2:粘贴并部署 Lambda 代码 进入 todo_handler 的代码编辑器,将原有代码替换为下面这个版本。
这个版本保留了 Lab 的核心逻辑,并补好了 Exercise 中提到的 DELETE 和 PUT 问题。
GET:查询所有 tasks,或者根据 taskId 查询一个具体 task
POST:创建一个新的 task,并写入 DynamoDB
PUT:根据 task id 更新已有 task
DELETE:根据 task id 删除 task
headers:返回 JSON,并允许基本 CORS 请求
tableName = 'todos':指定连接到前面创建的 DynamoDB 表
这段 Lambda 相当于 Todo 应用的后端 API 逻辑。API Gateway 收到 HTTP 请求后,会把请求转发给 Lambda,Lambda 再根据 event.httpMethod 判断要执行哪种操作。
Step 2.3:创建 POST 测试事件 你是在模拟 API Gateway 发来的 POST 请求。这个请求会创建一条新的 Todo task。
Lambda 返回的 statusCode 应该是:
返回 body 中应该包含新创建 task 的信息,例如:
回到 DynamoDB 的 todos 表,在 Items 里查看是否出现新记录。
Step 2.4:创建 GET 全部任务测试事件 你是在模拟 GET 请求,查询 DynamoDB 中所有 Todo tasks。
返回结果里应该包含 DynamoDB scan 的结果,例如:
Step 2.5:创建 GET 指定任务测试事件 先从 DynamoDB 里复制某一条 task 的 id。
Lab 的 Exercise 1 问的是:如何获取一个 specific task,而不是所有 tasks。关键就是通过 path parameter 把 taskId 传给 Lambda。
Step 2.6:测试 PUT 更新任务 Lab 的 Exercise 2 提到原始代码中 PUT 的处理有问题。原始写法只是把 JSON.parse(event.body) 直接丢给 dynamo.update(),但 DynamoDB update 需要明确提供:
回到 DynamoDB 表中查看该 task 的 title、 desc 或 done 是否被更新。
Step 2.7:测试 DELETE 删除任务 先确认你有一条可以删除的 task,并复制它的 id。
Lab 的 Exercise 2 也提到原始代码中 DELETE 的处理有问题。原始写法没有明确告诉 DynamoDB 要从哪个表删除、根据哪个 key 删除。
Task 3: Implement RESTful API Using API Gateway 本 Task 要用 API Gateway 把 Lambda 暴露成真正可以通过 URL 访问的 REST API。
创建 REST API
API 名称可以用 todo-api
创建 /api resource
创建 /api/tasks resource
在 /api/tasks 下创建 GET 和 POST method
创建 /api/tasks/{taskId} resource
在 /api/tasks/{taskId} 下创建 GET method
部署 API 到 stage,例如 prod
使用浏览器或 curl 测试 API
Step 3.1:创建 REST API 进入 API Gateway 后,在 APIs 区域选择:
你正在创建一个 API Gateway 服务,它会作为外部请求进入后端的入口。
Lambda 本身不是一个公开的 HTTP API。API Gateway 的作用就是把外部 HTTP 请求转换成 Lambda 可以处理的 event。
Step 3.2:创建 /api Resource 在 API Gateway 的 Resources 页面
这是为了让最终 URL 更像一个标准后端 API,例如:
Step 3.3:创建 /api/tasks Resource 并启用 CORS REST API 通常用一个复数资源名表示一类数据。这里 /tasks 表示所有 Todo tasks。
CORS 主要用于浏览器前端跨域请求。如果以后有网页前端调用这个 API,CORS 设置可以避免浏览器拦截请求。
Step 3.4:给 /api/tasks 创建 GET Method 绑定到了 Lambda function todo_handler。
当用户访问 GET /api/tasks 时,API Gateway 会把请求转发给 Lambda。Lambda 收到的 event.httpMethod 会是 GET,于是执行查询所有 tasks 的逻辑。
Step 3.5:给 /api/tasks 创建 POST Method 继续选中 /api/tasks,创建新的 Method:
绑定到了 Lambda function todo_handler。
当外部请求提交 JSON body 创建任务时,API Gateway 会把 body 传给 Lambda。Lambda 看到 event.httpMethod 是 POST,就会创建一条新 task。
可以在 API Gateway 内置测试工具中使用这个 body:
Step 3.6:创建 /api/tasks/{taskId} Resource {taskId} 是 path parameter。它代表这个位置的值是动态的,例如:
API Gateway 会把这个动态值传给 Lambda。
Step 3.7:给 /api/tasks/{taskId} 创建 GET Method 选中 /api/tasks/{taskId},创建:
这个 endpoint 用来查询某一条具体 task,而不是查询全部 tasks。
Step 3.8:可选创建 PUT 和 DELETE Method 如果你想完整测试 Exercise 2,可以继续在 /api/tasks/{taskId} 下创建:
两个 Method 的 Integration 都设置为:
你让 API Gateway 也支持通过 HTTP PUT 更新 task,通过 HTTP DELETE 删除 task。
虽然 Lab 的主要 API Gateway 部分重点是 GET 和 POST,但 Exercise 2 已经让你修复 PUT 和 DELETE 的 Lambda 逻辑。创建这两个 method 可以让你从真实 API URL 测试它们。
Step 3.9:Deploy API 部署完成后,你会得到一个 Invoke URL,格式类似:
你把 API Gateway 的配置正式发布出去。没有 Deploy 的话,外部无法通过 URL 访问你刚刚创建的 methods。
API Gateway 的设计和部署是分开的。创建 resource 和 method 后,需要 Deploy 到某个 stage 才能真正调用。
Step 3.10:用 curl 测试 GET /api/tasks你应该看到 DynamoDB 中所有 tasks 的 JSON 数据。
Step 3.11:用 curl 测试 POST /api/tasks你通过 API Gateway 创建了一条新的 task。
或者回到 DynamoDB 的 todos 表检查新数据。
Step 3.12:用 curl 测试 GET /api/tasks/{taskId}先从 POST 返回结果或 DynamoDB 中复制一个 task id。
你在测试 path parameter 是否能从 API Gateway 正确传到 Lambda。
返回结果应该只包含这一个 task,而不是所有 tasks。
Step 3.13:可选测试 PUT 和 DELETE 如果你创建了 DELETE method,可以执行:
PUT 后回到 DynamoDB 看字段是否更新。
DELETE 后回到 DynamoDB 看对应记录是否消失。
Task 4: Import Todo Tasks from CSV Files 当你上传 CSV 文件到 S3 bucket 后,S3 会触发 Lambda,Lambda 会读取 CSV 内容,并把每一行写入 DynamoDB 的 todos 表。
Step 4.1:创建 S3 Bucket Bucket name 必须全局唯一,可以使用类似:
你创建了一个对象存储 bucket,用来上传 CSV 文件。
S3 可以作为事件源。当有文件上传时,S3 可以自动触发 Lambda。
Step 4.2:创建 todo_import_handler Lambda 你创建了一个专门处理 CSV 导入的 Lambda。
前面的 todo_handler 是处理 HTTP API 请求的。这里的 todo_import_handler 是处理 S3 上传事件的。两个 Lambda 负责不同类型的业务逻辑。
Step 4.3:添加 S3 Trigger 进入 todo_import_handler 的 Function overview / Designer 区域,点击:
你告诉 AWS:只要这个 S3 bucket 中有新文件上传,就触发 todo_import_handler。
这就是典型的事件驱动 Serverless 架构。你不需要手动运行导入程序,S3 文件上传事件会自动启动 Lambda。
Step 4.4:粘贴 CSV 导入 Lambda 代码 进入 todo_import_handler 的代码编辑器,用下面代码替换默认代码。
会跳过 CSV header 行
会把 done 转成 Boolean
每条 task 自动生成 uuid 作为 id
会写入 DynamoDB 表 todos
从 S3 event 中读取 bucket name 和 file key
用 get_object() 下载上传的 CSV 文件
逐行读取 CSV
每行拆成 title, desc, done
生成唯一 id
调用 DynamoDB put_item() 写入 todos 表
Step 4.5:创建 CSV 文件 你准备了一个批量导入 Todo tasks 的 CSV 文件。
后面上传这个文件到 S3,S3 会触发 Lambda 自动读取并写入 DynamoDB。
Step 4.6:上传 CSV 到 S3 上传完成后,S3 trigger 会调用 todo_import_handler。
Lambda 触发可能不是立即显示结果,可能需要等一小会儿。
Step 4.7:检查 Lambda Invocation 进入 todo_import_handler,打开:
你在确认 S3 上传事件是否真的触发了 Lambda。
如果 DynamoDB 中没有数据,第一步不是马上改代码,而是先确认 Lambda 有没有被调用。
Step 4.8:验证 DynamoDB 是否插入数据
Task 5: Setup Development Environment with Cloud9(Optional) 本 Task 是可选内容,主要是用 Cloud9 准备一个在线开发环境。
注意:Cloud9 在 AWS Academy Learner Lab 中可能因为 IAM 限制而不可用。如果无法创建,可以记录原因并跳过,或使用自己的 AWS Free Tier 账号。
Step 5.1:创建 Cloud9 Environment 其余设置可以保持默认, Network settings要选 Secure Shell (SSH)
你在创建一个在线 IDE,后面可以直接在浏览器里运行命令、写代码、部署 AWS Serverless 应用。
Cloud9 和 AWS 服务集成比较方便,尤其适合用 SAM CLI 做 Serverless 项目。
Step 5.2:验证 AWS CLI 进入 Cloud9 IDE 后,在底部 terminal 中运行:
你在确认 Cloud9 里面的终端是否能访问当前 AWS 账号。
后面 SAM 部署应用时,需要 CLI 有权限创建 Lambda、API Gateway、S3、CloudFormation 等资源。
Task 6: Create Hello World Serverless Application with SAM(Optional) 本 Task 是可选内容,目的是用 AWS SAM 快速创建并部署一个 Hello World Serverless 应用。
使用 sam init 创建项目
使用 sam build 构建项目
使用 sam deploy --guided 部署项目
理解 SAM 背后会使用 CloudFormation 创建资源
找到输出中的 HelloWorldApi
用浏览器或 curl 测试 API
实验结束后从 CloudFormation 删除 stack
Step 6.1:初始化 SAM 项目 SAM 会帮你生成一个基本的 Serverless 项目结构。
相比手动在 Console 里点 Lambda 和 API Gateway,SAM 可以用代码和模板描述 Serverless 应用。
Step 6.2:进入项目目录
Step 6.3:构建 SAM 项目 Lambda 代码和依赖需要先被打包成 AWS 可以部署的格式。
Step 6.4:部署 SAM 项目 根据提示填写配置。常见选项可以按 tutor 要求来。
SAM 会把你的 Serverless 应用部署到 AWS。
sam deploy --guided 会生成部署配置,并通过 CloudFormation 创建资源。
Lambda function
API Gateway
S3 bucket,用来存放部署包
IAM role / policy
CloudFormation stack
Step 6.5:测试 HelloWorldApi 应该返回 Hello World 相关的 JSON 或文本响应。
截图占位:这里放 HelloWorldApi 输出和 curl 测试成功截图
Step 6.6:删除 SAM 创建的资源 你在删除 SAM 部署出来的 Lambda、API Gateway、S3 bucket、IAM 等资源。
实验完成后清理资源可以避免产生不必要费用,也能保持 AWS Academy 环境干净。
截图占位:这里放 CloudFormation stack 删除前的截图
Summary 本周 Lab 的主线是用 AWS 的 Serverless 服务重建一个 Todo 应用后端。
本周必须理解的知识点
1. Serverless 是什么 Serverless 不是“没有服务器”,而是你不用自己管理服务器。
创建 EC2
安装 runtime
配置端口
管理扩容
处理服务器维护
2. Lambda 是什么 Lambda 是 AWS 的 Function as a Service。
负责处理 API 请求,例如 GET、POST、PUT、DELETE。
3. API Gateway 是什么
4. DynamoDB 是什么 DynamoDB 是 AWS 托管的 NoSQL 数据库。
本周它保存 Todo tasks,每条数据用 id 作为主键。
5. S3 Trigger 是什么 S3 Trigger 可以在文件上传后自动触发 Lambda。
本周用它实现 CSV 自动导入 Todo tasks。
6. SAM 是什么 SAM 是 AWS Serverless Application Model。
它背后会使用 CloudFormation 创建 Lambda、API Gateway、S3、IAM 等资源。
关键概念对比
Lambda vs EC2
API Gateway vs Lambda
DynamoDB scan vs get
S3 Upload vs S3 Trigger
最后清理资源 AWS Academy 虽然是实验环境,但不需要的资源仍然应该删除,避免产生费用或影响后续 Lab。
https://awsacademy.instructure.com/login/canvasTable name: todos
Partition key: id
Partition key type: Stringid 每个 task 的唯一标识
title task 标题
desc task 描述
done 是否完成
updatedAt 更新时间Function name: todo_handler
Runtime: Node.js 20.xUse an existing role
Existing role: LabRoleTask updated successfullyResource Name: api
Resource Path: /https://xxxxx.execute-api.us-east-1.amazonaws.com/prod/api/tasksResource Name: tasks
Resource Path: /apiIntegration type: Lambda Function
Use Lambda Proxy integration: 勾选
Lambda Region: 选择你的 Region,例如 us-east-1
Lambda Function: todo_handlerIntegration type: Lambda Function
Use Lambda Proxy integration: 勾选
Lambda Region: 选择你的 Region
Lambda Function: todo_handlerResource Name: {taskId}
Resource Path: /api/tasks//api/tasks/abc123
/api/tasks/def456Integration type: Lambda Function
Use Lambda Proxy integration: 勾选
Lambda Region: 选择你的 Region
Lambda Function: todo_handlerIntegration type: Lambda Function
Use Lambda Proxy integration: 勾选
Lambda Region: 选择你的 Region
Lambda Function: todo_handlerDeployment stage: prod
Description: First deployment for todo-apihttps://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prodtodo-import-yourstudentidFunction name: todo_import_handler
Runtime: Python 3.9Use an existing role
Existing role: LabRoletitle,desc,done
Buy milk,Get one bottle of milk,false
Submit lab,Finish AWS serverless lab,false
Review notes,Read Week 09 serverless content,trueDynamoDB
Tables
todos
Explore table itemsBuy milk
Submit lab
Review notesTemplate source: AWS Quick Start Templates
Template: 7
Runtime: nodejs20.x
Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]:N
Would you like to enable monitoring using CloudWatch Application Insights? [y/N]:N
Would you like to set Structured Logging in JSON format on your Lambda functions? [y/N]:N
Project name: 自己输入一个项目名,例如 hello-world-serverlesstemplate.yaml
hello-world/
samconfig.tomlStack Name: 回车
AWS Region: 回车
Confirm changes before deploy:Y
Allow SAM CLI IAM role creation:Y
Disable rollback:N
getAllItemsFunction has no authentication. Is this okay? [y/N]:y
getByIdFunction has no authentication. Is this okay? [y/N]:y
putItemFunction has no authentication. Is this okay?:y
Save arguments to configuration file [Y/n]:Y
SAM configuration file [samconfig.toml]:回车
SAM configuration environment [default]:回车https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/Prod/hello/Client / curl / browser
|
v
API Gateway
|
v
Lambda todo_handler
|
v
DynamoDB todosUpload CSV to S3
|
v
S3 Trigger
|
v
Lambda todo_import_handler
|
v
DynamoDB todosLambda = 被事件触发后运行的一小段后端代码API Gateway = 把外部 HTTP 请求转发给后端服务的前门GET /api/tasks
POST /api/tasks
GET /api/tasks/{taskId}
PUT /api/tasks/{taskId}
DELETE /api/tasks/{taskId}DynamoDB = 不需要自己管理数据库服务器的 key-value / document databaseS3 Trigger = 文件一上传,就自动运行一段 Lambda 代码SAM = 用模板和命令创建、构建、部署 Serverless 应用的工具GET /api/tasks 使用 scan
GET /api/tasks/{id} 使用 get1. API Gateway: 删除 todo-api
2. Lambda: 删除 todo_handler 和 todo_import_handler
3. DynamoDB: 删除 todos 表
4. S3: 先清空 bucket,再删除 bucket
5. Cloud9: 如果创建了 environment,可以删除
6. CloudFormation: 如果用 SAM 部署了应用,删除对应 stackimport { DynamoDB } from ' @aws-sdk/client-dynamodb ' ; import { DynamoDBDocument } from ' @aws-sdk/lib-dynamodb ' ; const dynamo = DynamoDBDocument.from(new DynamoDB()); const tableName = ' todos ' ; export const handler = async (event, context) => { let body; let statusCode = 200; const headers = { ' Content-Type ' : ' application/json ' , ' Access-Control-Allow-Origin ' : ' * ' , ' Access-Control-Allow-Headers ' : ' Content-Type ' , ' Access-Control-Allow-Methods ' : ' GET,POST,PUT,DELETE,OPTIONS ' }; try { const taskId = event.pathParameters?.taskId; const requestBody = event.body ? JSON.parse(event.body.trim()) : {}; switch (event.httpMethod) { case ' GET ' : { if (taskId) { body = await dynamo.get({ TableName: tableName, Key: { id: taskId } }); if (!body.Item) { statusCode = 404; body = { message: ' Task not found ' }; } } else { body = await dynamo.scan({ TableName: tableName }); } break ; } case ' POST ' : { const data = { id: context.awsRequestId, title: requestBody.title || '' , desc: requestBody.desc || '' , done: requestBody.done ?? false, updatedAt: new Date().getTime() }; await dynamo.put({ TableName: tableName, Item: data }); statusCode = 201; body = { message: data }; break ; } case ' PUT ' : { const idToUpdate = taskId || requestBody.id; if (!idToUpdate) { throw new Error( ' Missing task id for PUT request ' ); } const allowedFields = [ ' title ' , ' desc ' , ' done ' ]; const updateExpressions = []; const expressionAttributeNames = {}; const expressionAttributeValues = {}; for ( const field of allowedFields) { if (requestBody[field] !== undefined) { updateExpressions.push( ` # ${ field } = : ${ field }` ); expressionAttributeNames[ ` # ${ field }` ] = field; expressionAttributeValues[ ` : ${ field }` ] = requestBody[field]; } } if (updateExpressions.length === 0) { throw new Error( ' No valid fields provided for update ' ); } updateExpressions.push( ' #updatedAt = :updatedAt ' ); expressionAttributeNames[ ' #updatedAt ' ] = ' updatedAt ' ; expressionAttributeValues[ ' :updatedAt ' ] = new Date().getTime(); body = await dynamo.update({ TableName: tableName, Key: { id: idToUpdate }, UpdateExpression: ` SET ${ updateExpressions . join( ' , ' ) }` , ExpressionAttributeNames: expressionAttributeNames, ExpressionAttributeValues: expressionAttributeValues, ReturnValues: ' ALL_NEW ' }); body = { message: ' Task updated successfully ' , updatedTask: body.Attributes }; break ; } case ' DELETE ' : { const idToDelete = taskId || requestBody.id; if (!idToDelete) { throw new Error( ' Missing task id for DELETE request ' ); } body = await dynamo.delete({ TableName: tableName, Key: { id: idToDelete }, ReturnValues: ' ALL_OLD ' }); if (!body.Attributes) { statusCode = 404; body = { message: ' Task not found ' }; } else { body = { message: ' Task deleted successfully ' , deletedTask: body.Attributes }; } break ; } default : throw new Error( ` Unsupported method " ${ event . httpMethod } " ` ); } } catch (err) { statusCode = 400; body = { error: err.message }; } return { statusCode, body: JSON.stringify(body), headers }; }; { " body " : " {\n\"title\" :\"First Task\",\n\"desc\" :\"sample description\",\n\"done\" : false\n} " , " httpMethod " : " POST " } { " message " : { " id " : " 自动生成的请求 ID " , " title " : " First Task " , " desc " : " sample description " , " done " : false, " updatedAt " : 1710000000000 } } { " Items " : [ { " id " : " 某个 task id " , " title " : " First Task " , " desc " : " sample description " , " done " : false } ], " Count " : 1, " ScannedCount " : 1 } { " httpMethod " : " GET " , " pathParameters " : { " taskId " : " 替换成你自己的 task id " } } { " httpMethod " : " PUT " , " pathParameters " : { " taskId " : " 替换成你自己的 task id " }, " body " : " {\n\"title\":\"Updated Task Title\",\n\"desc\":\"updated description\",\n\"done\": true\n} " } { " httpMethod " : " DELETE " , " pathParameters " : { " taskId " : " 替换成你自己的 task id " } } { TableName: tableName, Key: { id: idToDelete } } { " title " : " newtask-from-gateway " } BASE_URL= " https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod " curl "$ BASE_URL/api/tasks " curl -X POST "$ BASE_URL/api/tasks " \ -H " Content-Type: application/json " \ -d ' { "title": "newtask-from-gateway", "desc": "created from API Gateway", "done": false } ' curl "$ BASE_URL/api/tasks " curl "$ BASE_URL/api/tasks/ $ TASK_ID " curl -X PUT "$ BASE_URL/api/tasks/ $ TASK_ID " \ -H " Content-Type: application/json " \ -d ' { "title": "Updated from curl", "done": true } ' curl -X DELETE "$ BASE_URL/api/tasks/ $ TASK_ID " import json import boto3 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): inserted_count = 0 for record in event[ ' Records ' ]: bucket = record[ ' s3 ' ][ ' bucket ' ][ ' name ' ] key = unquote_plus(record[ ' s3 ' ][ ' object ' ][ ' key ' ]) print( f"File {key} uploaded to {bucket} bucket" ) csvfile = s3_client.get_object(Bucket=bucket, Key=key) csvcontent = csvfile[ ' Body ' ].read().decode( ' utf8 ' ).splitlines() for index, line in enumerate(csvcontent): if not line.strip(): continue tokens = line.strip().split( " , " ) if len(tokens) < 3: print( f"Skipping invalid row: {line} " ) continue title = tokens[0].strip() desc = tokens[1].strip() done_raw = tokens[2].strip().lower() if index == 0 and title.lower() == " title " and desc.lower() == " desc " : print( " Skipping CSV header row " ) continue data = { ' id ' : { ' S ' : str(uuid.uuid4()) }, ' title ' : { ' S ' : title }, ' desc ' : { ' S ' : desc }, ' done ' : { ' BOOL ' : done_raw == ' true ' } } dynamodb.put_item( TableName=TABLE_NAME, Item=data ) inserted_count += 1 return { ' statusCode ' : 200, ' body ' : json.dumps( f' {inserted_count} records successfully inserted into database.' ) } aws sts get-caller-identity { " UserId " : " ... " , " Account " : " ... " , " Arn " : " ... " } cd hello-world-serverless sudo dnf install -y nodejs20 sudo alternatives --set node /usr/bin/node-20 node -v docker pull public.ecr.aws/sam/build-nodejs20.x:latest sam build --use-container --build-image public.ecr.aws/sam/build-nodejs20.x:latest df -h docker system df rm -rf .aws-sam rm -rf ~/.npm/_cacache sudo dnf clean all docker container prune -f docker builder prune -af sam build --use-container --build-image public.ecr.aws/sam/build-nodejs20.x:latest curl " 替换成你的 HelloWorldApi URL "