LoginSignup
7
2

More than 1 year has passed since last update.

AWS LambdaでRustをDockerで使う

Last updated at Posted at 2022-06-15

目的

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通信を行う処理を中心に書いていきます。
リクエストパラメーターやリクエストボディの解析を行って、最終的なレスポンスを構築します。

Cargo.toml
[package]
name = "api"
version = "0.1.0"
edition = "2021"

[dependencies]
common = { path = "../common" }
lambda_http = "0.5.2"
tokio = { version = "1", features = ["full"] }
main.rs
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に失敗します。

Cargo.toml
[package]
name = "common"
version = "0.1.0"
edition = "2021"

[dependencies]
main.rs
pub fn hello(name: &str) -> String {
    format!("Hello {}!", name)
}

Workspace

Cargo.toml
[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」と出力されていまいます。

Dockerfile
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を使っています。

serverless.yml
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も取得します。

Cargo.toml
[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"
main.rs
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の中身は暗号化して使います。

Cargo.toml
[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"] }
main.rs
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()))?)
}
7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2