本篇文章將逐步引導讀者使用 AWS SAM 和 Rust 建立一個 Serverless 應用程式,並著重於如何利用 S3 預簽名 URL 實作圖片上傳功能。首先,我們會調整專案結構以支援多個 Lambda 函式,並加入必要的依賴項,例如 aws_sdk_dynamodbserdeaws_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_dynamodbserde crate 到我們的專案中。執行以下命令:

cargo add aws_sdk_dynamodb serde

確保版本號與範例程式碼一致。

更新 template.yaml

template.yaml 中的 PostCatFunctionCodeUri 更新為 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
}

程式碼解密:

  1. 定義 RequestBody 結構:使用 serde::Deserialize 來反序列化請求主體。
  2. 處理請求:從請求中提取主體,並將其反序列化為 RequestBody
  3. 建立 DynamoDB 請求:使用 aws_sdk_dynamodb 建立一個 PutItemRequest,將新的貓專案新增到 DynamoDB 表中。
  4. 傳送請求:使用 client.put_item() 傳送請求到 DynamoDB。
  5. 處理錯誤:使用 match 處理可能發生的錯誤,並傳回適當的 HTTP 回應。
  6. 傳回回應:建立一個 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.tomltemplate.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 ...

內容解密:

  1. ResponseBody結構體:定義了一個用於序列化回應的結構體,包含一個名為cats的向量,用於存放貓的名字。
  2. function_handler函式:處理Lambda函式的邏輯。它接收一個Request物件、一個dynamodb::Client例項和一個表名。
  3. 掃描DynamoDB表:使用dynamodb::Client掃描指定的表,並檢索所有專案。
  4. 構建回應:將掃描結果對映到ResponseBody結構體,並序列化為JSON字串。
  5. 傳回HTTP回應:構建一個HTTP回應,狀態碼為200,內容型別為application/json,並將JSON字串作為回應體。

重點觀察

  • 使用了aws_sdk_dynamodb來與DynamoDB互動。
  • lambda_http crate用於處理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的工作流程

  1. 前端呼叫POST /cat端點在DynamoDB中建立貓的記錄。
  2. POST /cat API生成一個預簽URL並將其傳回給前端。
  3. 前端使用這個預簽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的請求大小限制,並且提高了上傳大檔案的能力。