【AWS】RustでServerlessFrameworkを使ってみる


この記事は Rustその2 Advent Calendar 2019 12/3 の記事です。

以前ブログでも書いたのですが、今回はRustを使ってServerlessFramework用のプロジェクトを作るための最小限のことだけ書いていきます。

## 環境構築
### Rust
||バージョン|
|---|---|
|rustc|1.41.0-nightly|
|rustup|1.20.2|
|cargo|1.41.0-nightly|

### ServerlessFramework
||バージョン|
|---|---|
|yarn|1.19.2|
|serverless|1.58.0|
|serverless-rust|0.3.7|


### Rustインストール

```bash:console
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

`.profile`にPATHが更新されるので確認して、読み込みます。

```bash:console
$ cat ~/.profile 

export PATH="$HOME/.cargo/bin:$PATH"

$ source ~/.profile
```

`cargo`コマンドが使えれば完了です。

```bash:console
$ cargo version
cargo 1.41.0-nightly (8280633db 2019-11-11)
```

### ServerlessFrameworkのインストール

今回は`yarn`を使って準備していきます。
まずは`yarn init`で`package.json`を作ります。
ひとまずすべてデフォルトで作ります。

```bash:console
$ yarn init
yarn init v1.19.2
question name (rust_serverless): 
question version (1.0.0): 
question description: 
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
question private: 
success Saved package.json
```

出来上がるとこんな感じになります。

```json:package.json
{
  "name": "rust_serverless",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

```

ここから、`ServerlessFramework`と`serverless-rust`を追加

```
$ yarn add serverless@1.58.0
```

```
$ yarn add serverless-rust@0.3.7
```

追加後のpackage.jsonはこうなります。

```
{
  "name": "rust_serverless",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "serverless": "^1.58.0",
    "serverless-rust": "^0.3.7"
  }
}

```

### プロジェクト作成

rustのtemplateは現在ないみたいなので、`sls create`コマンドは使いません。
まずは、`serverless.yml`を手書きします。

```yml:serverless.yml
service: rust-sample
provider:
  name: aws
  runtime: rust
  memorySize: 128
  region: ap-northeast-1
plugins:
  - serverless-rust
package:
  individually: true
functions:
  example-function:
    handler: rust-sample
    events:
      - http:
          path: /
          method: GET

```

次にrustの初期化をします。
`serverless.yml`、`package.json `と同じ位置で以下のコマンドを叩きます。


```
$ cargo init
```

ここまでやると、以下のディレクトリ構成になってると思います。

![スクリーンショット 2019-12-02 23.40.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33165/96b2da9b-dada-c3a0-e1bd-6e3d604f8d4b.png "スクリーンショット 2019-12-02 23.40.24.png")

## コーディング

まず、main.rs用のサンプルのコードになります。
もとのコード消して、こちらに差し替えます。


```rust:main.rs
use lambda_http::{lambda, IntoResponse, Request};
use lambda_runtime::{error::HandlerError, Context};
use serde_json::json;

fn main() {
    lambda!(handler)
}

fn handler(
    _: Request,
    _: Context,
) -> Result<impl IntoResponse, HandlerError> {
    Ok(json!({
        "message": "AWS Lambda on Rust"
    }))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn handler_handles() {
        let request: Request<> = Request::default();
        let expected = json!({
            "message": "AWS Lambda on Rust"
        })
        .into_response();
        let response = handler(request, Context::default())
            .expect("expected Ok(_) value")
            .into_response();
        assert_eq!(response.body(), expected.body())
    }
}
```

次に、上記のソースに必要なPackageを`Cargo.toml`に追加します。

```toml:Cargo.toml
[package]
name = "rust-sample"
version = "0.1.0"
authors = ["hisayuki <*****@***>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
lambda_runtime = "0.2.1"
lambda_http = "0.1.1"
log = "0.4"
serde_json = "^1"
serde_derive = "^1"
```

このときに、`[package]`のnameを`serverless.yml`の`handler`名と一緒にします。

```yml:serverless.yml
functions:
  example-function:
    handler: rust-sample
    events:
      - http:
          path: /
          method: GET
```

この段階で、`main.rs`の単体テストは出来るので実際にやってみます。
コマンドは`cargo test`になります。

```bash:console
$ cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.20s
     Running target/debug/deps/rust_sample-eb5e576424cf1ee7

running 1 test
test tests::handler_handles ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

`Cargo.toml`にpackageを設定してからcargoコマンドを実行していない場合は、ここでpackageのインストールが始まります。

## テスト
### Lambdaのローカル単体テスト

Lambdaとしてテストスル場合、ローカルであってもリクエストを渡す必要があります。
Requestを渡さないとエラーが返ってきます。

```json:console
{"errorType":"JsonError","errorMessage":"JsonError: invalid type: string \"\", expected struct LambdaRequest at line 1 column 2"}
```

そのため、本来API Gatewayから送られてくるRequestの簡易版を用意します。


```json:example_request.json
{
  "path": "/",
  "httpMethod": "GET",
  "headers": {
    "Host": "amazonaws.com"
  },
  "requestContext": {
    "accountId": "",
    "resourceId": "",
    "stage": "dev",
    "requestId": "",
    "identity": {
      "sourceIp": ""
    },
    "resourcePath": "",
    "httpMethod": "",
    "apiId": ""
  },
  "queryStringParameters": {}
}
```

ディレクトリ構成はこんな感じ。


![スクリーンショット 2019-12-03 0.48.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33165/e5d3a852-e753-e367-605a-ff16e1363671.png "スクリーンショット 2019-12-03 0.48.17.png")

プロジェクトのrootで以下のコマンドを打つと実行結果が帰ってきます。

```bash:console
$ yarn sls invoke local -f example-function --path test/resources/example_request.json 
yarn run v1.19.2
$ /Users/hisayuki/docker_dev/rust_serverless/node_modules/.bin/sls invoke local -f rust-sample --path test/resources/example_request.json
Serverless: Building native Rust rust-sample func...
    Finished release [optimized] target(s) in 3.04s
  adding: bootstrap (deflated 61%)
Serverless: Packaging service...
Serverless: Building Docker image...
START RequestId: 7cdd45ff-65e6-1b2f-f341-832c8239935c Version: $LATEST

END RequestId: 7cdd45ff-65e6-1b2f-f341-832c8239935c
REPORT RequestId: 7cdd45ff-65e6-1b2f-f341-832c8239935c  Init Duration: 161.04 ms        Duration: 5.20 ms       Billed Duration: 100 ms Memory Size: 1536 MB    Max Memory Used: 10 MB


{"statusCode":200,"headers":{"content-type":"application/json"},"multiValueHeaders":{"content-type":["application/json"]},"body":"{\"message\":\"AWS Lambda on Rust\"}","isBase64Encoded":false}

✨  Done in 54.26s.

```

### API Gateway経由のローカル結合テスト

他言語で使われている`serverless-offline`のPackageを追加しようとおもったのですが・・・
Rustは`serverless-offline`対応していないので、ローカルでは出来ないです。
API GatewayからのテストはAWS上にデプロイして行います。

## デプロイ

AWS上に`sls deploy`でデプロイします。

```bash:console
$ yarn sls deploy --aws-profile [プロファイル名]
yarn run v1.19.2
$ /Users/hisayuki/docker_dev/rust_serverless/node_modules/.bin/sls deploy --aws-profile [プロファイル名]
Serverless: Building native Rust rust-sample func...
    Finished release [optimized] target(s) in 2.05s
objcopy: stlfi43N: debuglink section already exists
  adding: bootstrap (deflated 60%)
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service rust-sample.zip file to S3 (1.06 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...........................
Serverless: Stack update finished...
Service Information
service: rust-sample
stage: dev
region: ap-northeast-1
stack: rust-sample-dev
resources: 10
api keys:
  None
endpoints:
  GET - https://a5uhcphfsj.execute-api.ap-northeast-1.amazonaws.com/dev/
functions:
  example-function: rust-sample-dev-example-function
layers:
  None
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
✨  Done in 59.47s.
```

deploy完了すると、マネジメントコンソールでも確認できます。

![スクリーンショット 2019-12-03 15.37.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33165/1c93e7d5-8149-2d46-d07f-f83526f51cd8.png "スクリーンショット 2019-12-03 15.37.17.png")


### AWS上でのLambda単体テスト

deploy後に`local`をつけずに`invoke`で実行します。

```bash:console
$ yarn sls invoke -f example-function --aws-profile [プロファイル名]
yarn run v1.19.2
$ /Users/hisayuki/docker_dev/rust_serverless/node_modules/.bin/sls invoke -f example-function --path test/resources/example_request.json --aws-profile [プロファイル名]
{
    "statusCode": 200,
    "headers": {
        "content-type": "application/json"
    },
    "multiValueHeaders": {
        "content-type": [
            "application/json"
        ]
    },
    "body": "{\"message\":\"AWS Lambda on Rust\"}",
    "isBase64Encoded": false
}
✨  Done in 2.79s.
```

このように、Responseが返ってきます。


### AWS上でのAPI Gateway経由結合テスト

こちらは、先程のLambdaのdeploy時にAPI Gatewayの作成とEndpointが作成されています。

```bash:deploy時のResponse
endpoints:
  GET - https://a5uhcphfsj.execute-api.ap-northeast-1.amazonaws.com/dev/
```

マネジメントコンソールでも確認できます。

![スクリーンショット 2019-12-03 15.39.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33165/9b5049da-0b3b-9942-a94b-458283c2b4f7.png "スクリーンショット 2019-12-03 15.39.12.png")

テストの方法は`curl`で実行をします。

```
$ curl -X GET  https://a5uhcphfsj.execute-api.ap-northeast-1.amazonaws.com/dev/
{"message":"AWS Lambda on Rust"}
```

これでAPI Gatewayからの実行テストも完了


## まとめ
serverless-offlineが使えないのは残念ですが、RustでのLambda作成もだいぶ整ってきてます。
今回は書きませんでしたが、DynamoDBへの問い合わせ、複数Fanctionの作成方法もあります。
デフォルトで型とテストを備えているRustで、ぜひAPIの作成をしてみてください。

## 参照Github
[http Function](https://github.com/softprops/serverless-aws-rust-http)
[Multi Function](https://github.com/softprops/serverless-aws-rust-multi)