AWS LambdaでRustをDockerで使う

# 目的
AWS LambdaでRustを使います。
ライブラリとしてlambda_httpを使用します。
いくつかデプロイの方法があります。[本家のドキュメント](https://crates.io/crates/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通信を行う処理を中心に書いていきます。
リクエストパラメーターやリクエストボディの解析を行って、最終的なレスポンスを構築します。

```rust: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"] }
```

```rust: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に失敗します。

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

[dependencies]
```

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

## Workspace
```rust: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: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を使っています。

```yaml: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も取得します。

```rust: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"
```

```rust: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の中身は暗号化して使います。

```rust: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"] }
```

```rust: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()))?)
}
```