目的
AWS LambdaでRustを使います。
ライブラリとしてlambda_httpを使用します。
いくつかデプロイの方法があります。本家のドキュメントには以下のようなデプロイの仕方がありました。
- バイナリを作成してzipにしてアップロード
- SAM
- Serverless Framework
- Docker
Serverless Framewrokを使いたいんですが、pluginのバージョンが古くて微妙でした。
ここでは本家には無いServeless FrameworkとDockerを組み合わせた方法でデプロイします。
フォルダー構成
workspaceを使った構成にします。apiがメインの処理でcommonがライブラリとなります。
service
- api
- Cargo.toml
- main.rs
- common
- Cargo.toml
- lib.rs
- Cargo.toml
- Cargo.lock
- Dockerfile
- serverless.yml
各種ファイルの説明
api
http通信を行う処理を中心に書いていきます。
リクエストパラメーターやリクエストボディの解析を行って、最終的なレスポンスを構築します。
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
common = { path = "../common" }
lambda_http = "0.5.2"
tokio = { version = "1", features = ["full"] }
use common::hello;
use lambda_http::{service_fn, Error, IntoResponse, Request, RequestExt};
#[tokio::main]
async fn main() -> Result<(), Error> {
lambda_http::run(service_fn(func)).await?;
Ok(())
}
async fn func(request: Request) -> Result<impl IntoResponse, Error> {
let params = request.query_string_parameters();
let name = params.first("name").unwrap_or("World");
Ok(hello(&name))
}
common
共通ロジックなどを書いていきます。
ちなみにTLSを使うライブラリを使用する場合RustTLSを使うようにfeaturesを調整してください。OpenSSLを使うとbuildに失敗します。
[package]
name = "common"
version = "0.1.0"
edition = "2021"
[dependencies]
pub fn hello(name: &str) -> String {
format!("Hello {}!", name)
}
Workspace
[workspace]
members = [
"common",
"api"
]
Dockerfileで注意する点はlambdaの動作環境に合わせてbuildする必要があります。
そこで「x86_64-unknown-linux-musl」をつかいます。
Cargo.tomlだけ抽出してbuildしているのはライブラリのキャッシュをしているためです。ライブライの変更が無い場合コンパイル時間が短くなります。
srcをコピーした後にtouchしているのは、cargoにファイルが変更したことを伝えるためです。
これが無いとリリースした後のログに「if you see this, the build broke」と出力されていまいます。
FROM rust:buster as build
RUN apt -y update && apt -y install musl-tools libssl-dev pkg-config build-essential
RUN rustup update && \
rustup target add x86_64-unknown-linux-musl
WORKDIR /app
ENV CARGO_TARGET_DIR=/tmp/target
ENV PKG_CONFIG_ALLOW_CROSS=1
COPY Cargo.toml Cargo.toml
COPY Cargo.lock Cargo.lock
RUN mkdir -p common/src/
RUN mkdir -p api/src/
COPY common/Cargo.toml common/Cargo.toml
COPY api/Cargo.toml api/Cargo.toml
RUN echo "fn main() {println!(\"if you see this, the build broke\")}" >api/src/main.rs
RUN echo "//lib" > common/src/lib.rs
RUN cargo build --release --target x86_64-unknown-linux-musl
RUN rm -f /tmp/target/release/deps/app*
COPY common/src common/src
COPY api/src api/src
RUN touch common/src/lib.rs api/src/main.rs && cargo build --release --target x86_64-unknown-linux-musl
FROM public.ecr.aws/lambda/provided:al2
COPY --from=build /tmp/target/x86_64-unknown-linux-musl/release/api ${LAMBDA_RUNTIME_DIR}/bootstrap
CMD [ "lambda-handler" ]
serverless.ymlではAWS Lambda Function URLsを使っています。
service: lambda-test
frameworkVersion: '3'
provider:
name: aws
runtime: provided
region: ap-northeast-1
stage: staging
ecr:
images:
lambda-test:
path: .
file: Dockerfile
functions:
index:
image:
name: lambda-test
url: true
デプロイ
sls --verbose deploy
実行結果
curl https://xxxxx.lambda-url.ap-northeast-1.on.aws/
Hello World!
curl https://xxxxx.lambda-url.ap-northeast-1.on.aws/?name=aaa
Hello aaa!
おまけ
よくあるパターンをやってみました。
GET/POSTやパスによる分岐
リクエストBodyも取得します。
[package]
name = "api"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
common = { path = "../common" }
lambda_http = "0.5.2"
tokio = { version = "1", features = ["full"] }
serde_json = "1.0"
use common::hello;
use lambda_http::{
http::{response::Builder, Method, StatusCode},
service_fn, Body, Error, Request, RequestExt, Response,
};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Error> {
lambda_http::run(service_fn(func)).await?;
Ok(())
}
async fn func(request: Request) -> Result<Response<Body>, Error> {
let unmatch = not_found();
match request.uri().path() {
"/name" => match request.method() {
&Method::GET => get(&request).await,
&Method::POST => post(&request).await,
_ => unmatch,
},
_ => unmatch,
}
}
fn not_found() -> Result<Response<Body>, Error> {
let builder = Builder::new().status(StatusCode::NOT_FOUND);
let json = json!({});
Ok(builder.body(Body::Text(json.to_string()))?)
}
async fn get(request: &Request) -> Result<Response<Body>, Error> {
let params = request.query_string_parameters();
let name = params.first("name").unwrap_or("World");
let builder = Builder::new().status(StatusCode::OK);
let json = json!({
"type": "get",
"message": hello(name)
});
Ok(builder.body(Body::Text(json.to_string()))?)
}
async fn post(request: &Request) -> Result<Response<Body>, Error> {
let json: serde_json::Value = match request.body() {
Body::Text(text) => serde_json::from_str(text).unwrap_or(serde_json::Value::Null),
_ => serde_json::Value::Null,
};
let builder = Builder::new().status(StatusCode::OK);
let json = json!({
"type": "post",
"message": hello(json["name"].as_str().unwrap_or("World"))
});
Ok(builder.body(Body::Text(json.to_string()))?)
}
Cookie
Cookieの中身は暗号化して使います。
[package]
name = "api"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lambda_http = "0.5.2"
once_cell = "1.12.0"
tokio = { version = "1", features = ["full"] }
tower-cookies = { version = "0.7.0", default_features=false, features = ["private"] }
use lambda_http::{
tower::ServiceBuilder,
http::{response::Builder, StatusCode},
service_fn, Body, Error, Request, Response,
};
use once_cell::sync::OnceCell;
use tower_cookies::{CookieManagerLayer, Cookies, Cookie, Key};
const COOKIE_NAME: &str = "visited_private";
static KEY: OnceCell<Key> = OnceCell::new();
#[tokio::main]
async fn main() -> Result<(), Error> {
let my_key: &[u8] = &[0; 64]; // Your real key must be cryptographically random
KEY.set(Key::from(my_key)).ok();
let handler = ServiceBuilder::new()
.layer(CookieManagerLayer::new())
.service(service_fn(func));
lambda_http::run(handler).await?;
Ok(())
}
async fn func(request: Request) -> Result<Response<Body>, Error> {
let cookies: &Cookies = request.extensions().get().unwrap();
let private = cookies.private(&KEY.get().unwrap());
let value: i64 = match private.get(COOKIE_NAME) {
Some(cookie) => cookie.value().parse().unwrap_or(0),
None => 0
};
private.add(Cookie::new(COOKIE_NAME, value.to_string()));
let builder = Builder::new().status(StatusCode::OK);
Ok(builder.body(Body::Text("value".to_string()))?)
}