本篇文章將逐步引導讀者使用 AWS SAM 和 Rust 建立一個 Serverless 應用程式,並著重於如何利用 S3 預簽名 URL 實作圖片上傳功能。首先,我們會調整專案結構以支援多個 Lambda 函式,並加入必要的依賴項,例如 aws_sdk_dynamodb、serde 和 aws_sdk_s3。接著,我們會更新 template.yaml 檔案,加入 S3 Bucket 的定義以及 Lambda 函式所需的 S3 寫入許可權。後續將詳細說明如何編寫 Rust 程式碼,利用 aws_sdk_s3 產生預簽名 URL,並將其回傳給前端,讓前端可以直接將圖片上傳至 S3,避開 API Gateway 的大小限制。最後,我們會示範如何使用 curl 命令測試已佈署的 API 和預簽名 URL 的功能,確保圖片能正確上傳至 S3。
使用 AWS SAM 和 Rust 開發 Serverless 應用程式:建立上傳 API
在前面的章節中,我們已經成功地使用 AWS SAM 和 Rust 建立了一個基本的 Serverless 應用程式,並且對其進行了測試。現在,我們將繼續擴充套件這個應用程式,建立一個用於上傳貓圖片到 DynamoDB 的 POST API。
更新專案結構
首先,我們需要更新專案結構以支援多個 Lambda 函式。將 src/main.rs 移動到 src/bin/lambda/post-cat.rs,並在 src 目錄下建立一個新的 lib.rs 檔案。
更新後的專案結構應該如下所示:
.
+-- template.yaml
+-- Cargo.toml
+-- src
| |-- bin
| | |-- lambda
| | | +-- post-cat.rs
| +-- lib.rs
更新 Cargo.toml
接下來,我們需要在 Cargo.toml 中新增新的 binary:
[[bin]]
name = "post-cat"
path = "src/bin/lambda/post-cat.rs"
新增依賴項
我們需要新增 aws_sdk_dynamodb 和 serde crate 到我們的專案中。執行以下命令:
cargo add aws_sdk_dynamodb serde
確保版本號與範例程式碼一致。
更新 template.yaml
將 template.yaml 中的 PostCatFunction 的 CodeUri 更新為 target/lambda/post-cat。
編寫 post-cat.rs
現在,我們可以開始編寫 post-cat.rs 的程式碼:
use aws_sdk_dynamodb as dynamodb;
use aws_sdk_dynamodb::model::AttributeValue;
use lambda_http::{http::StatusCode, run, service_fn, Body, Error, Request, RequestExt, Response};
use serde::Deserialize;
#[derive(Deserialize)]
struct RequestBody {
name: String,
}
async fn function_handler(
request: Request,
client: &dynamodb::Client,
table_name: &str,
) -> Result<Response<Body>, Error> {
let body: RequestBody = match request.payload() {
Ok(Some(body)) => body,
_ => {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("Invalid payload".into())
.expect("Failed to render response"))
}
};
let dynamo_request = client
.put_item()
.table_name(table_name)
.item("cat", AttributeValue::S(body.name.clone()));
dynamo_request.send().await?;
let resp = Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/html")
.body(format!("Added cat {}", body.name).into())
.map_err(Box::new)?;
Ok(resp)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_target(false)
.without_time()
.init();
let config = aws_config::load_from_env().await;
let client = dynamodb::Client::new(&config);
let table_name = std::env::var("TABLE_NAME")?.to_string();
run(service_fn(|request| {
function_handler(request, &client, &table_name)
}))
.await
}
程式碼解密:
- 定義 RequestBody 結構:使用
serde::Deserialize來反序列化請求主體。 - 處理請求:從請求中提取主體,並將其反序列化為
RequestBody。 - 建立 DynamoDB 請求:使用
aws_sdk_dynamodb建立一個PutItemRequest,將新的貓專案新增到 DynamoDB 表中。 - 傳送請求:使用
client.put_item()傳送請求到 DynamoDB。 - 處理錯誤:使用
match處理可能發生的錯誤,並傳回適當的 HTTP 回應。 - 傳回回應:建立一個 HTTP 回應,包含成功訊息。
使用 S3 預簽名 URL 上傳圖片
由於 API Gateway 有 10MB 的 payload 大小限制,我們需要使用 S3 預簽名 URL 來上傳圖片。在接下來的章節中,我們將討論如何實作這一點。
使用AWS Rust SDK建立無伺服器架構
在前面的章節中,我們已經成功地佈署了一個簡單的Lambda函式,該函式可以接收POST請求並將貓的名字新增到DynamoDB表中。本章節將繼續擴充套件這個專案,新增一個GET API來檢索目前資料函式庫中的貓名單。
建立新的Lambda函式
首先,我們需要在src/bin/lambda目錄下建立一個新的檔案get-cats.rs。這個檔案將包含用於處理GET請求的Lambda函式程式碼。
接下來,我們需要更新Cargo.toml檔案,以包含新的binary:
[[bin]]
name = "get-cats"
path = "src/bin/lambda/get-cats.rs"
更新template.yaml
然後,我們需要更新template.yaml檔案,以新增新的Lambda函式到我們的架構中(列表6-4)。
列表6-4:Get Cats API的template.yaml更新內容
Resources:
GetCatsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: target/lambda/get-cats/
Handler: bootstrap
Runtime: provided.al2
Architectures: ["arm64"]
Events:
GetCats:
Type: Api
Properties:
Path: /cats
Method: get
Environment:
Variables:
TABLE_NAME: !Ref CatTable
Policies:
- DynamoDBReadPolicy:
TableName: !Ref CatTable
Outputs:
GetCatsFunction:
Description: "Get Cats Lambda Function ARN"
Value: !GetAtt GetCatsFunction.Arn
與之前的Lambda函式相比,這裡我們需要新增DynamoDB的讀取許可權。我們將路徑更改為/cats,並將方法更改為get。
實作Get Cats Lambda函式
完成Cargo.toml和template.yaml的更新後,我們可以開始撰寫src/bin/lambda/get-cats.rs的程式碼(列表6-5)。
列表6-5:Get Cats Lambda函式實作
use aws_sdk_dynamodb as dynamodb;
use lambda_http::{http::StatusCode, run, service_fn, Body, Error, Request, Response};
use serde::Serialize;
#[derive(Serialize)]
struct ResponseBody<'a> {
cats: Vec<&'a String>,
}
async fn function_handler(
_request: Request,
client: &dynamodb::Client,
table_name: &str,
) -> Result<Response<Body>, Error> {
let scan_output = client
.scan()
.table_name(table_name)
.send()
.await;
let scan_output = scan_output?;
let response_body = ResponseBody {
cats: scan_output
.items()
.unwrap_or_default()
.into_iter()
.map(|val| val.get("cat").unwrap().as_s().unwrap())
.collect(),
};
let resp = Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(serde_json::to_string(&response_body).unwrap().into())
.map_err(Box::new)?;
Ok(resp)
}
// .. async fn main ...
內容解密:
ResponseBody結構體:定義了一個用於序列化回應的結構體,包含一個名為cats的向量,用於存放貓的名字。function_handler函式:處理Lambda函式的邏輯。它接收一個Request物件、一個dynamodb::Client例項和一個表名。- 掃描DynamoDB表:使用
dynamodb::Client掃描指定的表,並檢索所有專案。 - 構建回應:將掃描結果對映到
ResponseBody結構體,並序列化為JSON字串。 - 傳回HTTP回應:構建一個HTTP回應,狀態碼為200,內容型別為
application/json,並將JSON字串作為回應體。
重點觀察
- 使用了
aws_sdk_dynamodb來與DynamoDB互動。 lambda_httpcrate用於處理HTTP請求和回應。- 使用了Serde進行序列化和反序列化。
DynamoDB查詢與掃描
DynamoDB支援兩種主要的查詢資料的方式:查詢(query)和掃描(scan)。當你知道要查詢的專案的分割鍵時,使用查詢(query)操作會更加高效。掃描(scan)操作則需要遍歷整個表,因此在不知道分割鍵的情況下很有用,但它的效能通常低於查詢(query)。
DynamoDB 查詢與掃描流程圖
@startuml
skinparam backgroundColor #FEFEFE
skinparam sequenceArrowThickness 2
title Rust Serverless 應用程式圖片上傳 API 建立
actor "客戶端" as client
participant "API Gateway" as gateway
participant "認證服務" as auth
participant "業務服務" as service
database "資料庫" as db
queue "訊息佇列" as mq
client -> gateway : HTTP 請求
gateway -> auth : 驗證 Token
auth --> gateway : 認證結果
alt 認證成功
gateway -> service : 轉發請求
service -> db : 查詢/更新資料
db --> service : 回傳結果
service -> mq : 發送事件
service --> gateway : 回應資料
gateway --> client : HTTP 200 OK
else 認證失敗
gateway --> client : HTTP 401 Unauthorized
end
@enduml此圖示說明瞭根據是否知道分割鍵來選擇使用查詢或掃描操作的流程。
使用S3預簽URL上傳圖片
在前面的章節中,我們已經成功地將貓的名字新增到資料函式庫中。然而,如果我們嘗試直接透過POST請求上傳圖片,會遇到API Gateway的10 MB請求大小限制。為瞭解決這個問題,我們可以使用S3預簽URL。S3預簽URL允許任何人在有限的時間內將檔案上傳到指定的S3位置,而無需提供AWS憑證。
S3預簽URL的工作流程
- 前端呼叫POST /cat端點在DynamoDB中建立貓的記錄。
- POST /cat API生成一個預簽URL並將其傳回給前端。
- 前端使用這個預簽URL直接將貓的圖片上傳到S3。
這種方法有幾個優點:
- S3允許上傳最大5 GB的檔案。
- 避免了透過API Gateway的頻寬消耗,同時也節省了Lambda的處理時間和記憶體使用,從而可能節省成本。
在template.yaml中新增S3 Bucket和許可權
首先,我們需要在template.yaml中新增一個S3 Bucket,並授予POST Lambda函式相應的許可權。
Resources:
CatTable:
# ...
ImageBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: Private
PostCatFunction:
Type: AWS::Serverless::Function
Properties:
# ...
Environment:
Variables:
TABLE_NAME: !Ref CatTable
BUCKET_NAME: !Ref ImageBucket
Policies:
- DynamoDBWritePolicy:
TableName: !Ref CatTable
- S3WritePolicy:
BucketName: !Ref ImageBucket
內容解密:
ImageBucket:定義了一個名為ImageBucket的S3 Bucket,用於儲存上傳的圖片。PostCatFunction:為Lambda函式增加了S3WritePolicy許可權,使其能夠生成預簽URL。
新增AWS S3 Crate
接下來,我們需要新增AWS S3 crate到我們的專案中:
cargo add aws_sdk_s3
生成預簽URL的程式碼
現在,我們可以在src/bin/lambda/post-cat.rs中新增生成預簽URL的程式碼。
use aws_sdk_dynamodb as dynamodb;
use aws_sdk_dynamodb::model::AttributeValue;
use aws_sdk_s3 as s3;
use aws_sdk_s3::presigning::config::PresigningConfig;
use lambda_http::{http::StatusCode, run, service_fn, Body, Error, Request, RequestExt, Response};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Deserialize)]
struct RequestBody {
name: String,
}
#[derive(Serialize)]
struct ResponseBody {
upload_url: String,
}
async fn function_handler(
request: Request,
dynamo_client: &dynamodb::Client,
s3_client: &s3::Client,
table_name: &str,
bucket_name: &str,
) -> Result<Response<Body>, Error> {
let body: RequestBody = match request.payload() {
Ok(Some(body)) => body,
_ => {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("Invalid payload".into())
.expect("Failed to render response"))
}
};
let presigned_request = s3_client
.put_object()
.bucket(bucket_name)
.key(&body.name)
.presigned(
PresigningConfig::expires_in(Duration::from_secs(60))?
)
.await?;
let response_body = ResponseBody {
upload_url: presigned_request.uri().to_string(),
};
let dynamo_request = dynamo_client
.put_item()
.table_name(table_name)
.item("cat", AttributeValue::S(body.name.clone()));
dynamo_request.send().await?;
let resp = Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/html")
.body(serde_json::to_string(&response_body)?.into())
.map_err(Box::new)?;
Ok(resp)
}
內容解密:
presigned_request:使用s3_client生成一個預簽URL,用於上傳圖片到指定的S3 Bucket。response_body:包含了生成的預簽URL,將被傳回給前端。dynamo_request:在DynamoDB中建立一條新的貓的記錄。
測試預簽URL
可以使用curl命令手動測試預簽URL。首先,呼叫POST /cat端點取得預簽URL,然後使用該URL上傳圖片:
curl --header "Content-type: application/json" \
--request POST \
--data '{"name": "Persian"}' \
https://your-api-url.execute-api.us-east-2.amazonaws.com/Prod/cat
curl --request PUT \
--data-binary "@/path/to/image.jpg" \
"https://your-presigned-url.s3.us-east-2.amazonaws.com/Persian"
這樣,我們就實作了使用S3預簽URL上傳圖片的功能,從而避免了API Gateway的請求大小限制,並且提高了上傳大檔案的能力。