Rustでサーバレスやりたい人が一番最初に見る場所

# Rustでサーバレス

re:Invent2018のアップデートにて、Rustを使用したAWS Lambdaの開発が可能になりました。サーバレスでRustを扱ってみたいという方のために、Custom Runtimeの動かし方からDB操作までを整理していきます。

# [初級編] RustをLambdaで動かす

Custom Runtimeを利用して、実際にLambdaを動かしてみます。手順を一つ一つ記載しようと思いましたが、すでに素晴らしい記事が上がっていたためリンクを貼ることにします。

まずは環境を構築します。Rustを初めて使う方は、以下リンクを参考にインストールを済ませてください。`cargo run`でRustのプロジェクトを起動できるようになることが目標です。

:pencil: [Rustの開発環境セットアップ](https://dev.classmethod.jp/server-side/language/rust-setup-with-rustup/)

環境構築が完了したら、Lambdaを動かしてみましょう。

ポイントは

1. Lambdaで起動するためのコンパイル環境を用意する
2. Lambdaで実行されるエントリポイントと設定ファイルの記述する
3. ビルドモジュールをZipに固めてAWSコンソールからアップロードする

です。こちらもやり方を丁寧に記載してある記事が既にありましたので、手順に従って進めてください。

:pencil: [AWS LambdaのCustom RuntimeでRustを実行してみた #reinvent](https://dev.classmethod.jp/cloud/aws/custom-runtime-rust/)

# [中級編] DynamoDBでCRUD

DynamoDBを使用して、Rustから簡単なCRUD操作をしていきます。

Lambdaを使用しない場合では、Rustのデータ管理にRDBを使用するケースが多かったのではないかと思います。本記事の最後に記載している通り、Lambdaによる開発がメインになると、今後はRustでもDynamoDBを使いたくなるユースケースが徐々に増えてくるのではないでしょうか。

CRUDを確認していくため、まずはDynamoDBのテーブルをマネジメントコンソールから作成しましょう。DynamoDBのページから、テーブル名とパーティションキーのみを入力します。

![FireShot Capture 108 - DynamoDB · AWS Console_ - https___console.aws.amazon.com_dynamodb_home.png](https://qiita-image-store.s3.amazonaws.com/0/206255/744f0291-bb5a-cb85-5ebc-032db1191ae5.png)

次に、DynamoDBの操作方法を見ていきます。

DynamoDBにアクセスするために、ライブラリとして`rusoto `を使用していきます。このライブラリはDynamoDBだけではなく、Rustから他のAWSサービスを呼び出すときにも使用できる優れものです。

前述の`cargo new`で作成したRustのプロジェクトに含まれている`Cargo.toml`を開き、`rust_core`と`rust_dynamodb`をdependenciesに追加していきます。

```text:Cargo.toml
[package]
name = "my_lambda_function"
version = "0.1.0"
authors = ["小池駿平 <xxx@gmail.com>"]
edition = "2018"

[dependencies]
lambda_runtime = "^0.1"
serde = "^1"
serde_json = "^1"
serde_derive = "^1"
log = "^0.4"
simple_logger = "^1"
rusoto_core = {version = "0.35.0", default_features = false, features=["rustls"]}
rusoto_dynamodb = {version = "0.35.0", default_features = false, features=["rustls"]}

[[bin]]
name = "bootstrap"
path = "src/main.rs"
```

続いて`main.rs`を開き、`rusoto`を使用できるよう宣言していきます。
※...は記載省略。

```rust:main.rs
...
extern crate rusoto_core;
extern crate rusoto_dynamodb;
...
use rusoto_core::Region;
// 今回紹介する①アイテム登録(PutItemInput) ②アイテム取得(GetItemInput) ③アイテム削除(DeleteItemInput)で使用するstructのみを宣言しています
use rusoto_dynamodb::{DynamoDb, DynamoDbClient, GetItemInput, PutItemInput, DeleteItemInput, AttributeValue};
...
```

`my_handler()`でDynamoDBへのアクセスを実行することにします。まずはアイテムの登録からです。HashMapを宣言して、登録するkey-valueを定義します。

```rust:main.rs
...
fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    ...
    // key-valueでDynamoDBのデータを扱います
    let mut create_key: HashMap<String, AttributeValue> = HashMap::new();
    // HashMapのkeyにはパーティションキーで指定した文字列を
    // valueにはLambdaコール時に受け渡されるイベント引数を指定します
    create_key.insert(String::from("name"), AttributeValue {
        s: Some(e.first_name),
        ..Default::default()
    });
    ...
}
...
```

Itemの登録にはPutItemInputを使用します。テーブル名とHashMapを指定して、PutItemInputを作成します。

```rust:main.rs
fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    ...
    let create_serials = PutItemInput {
        item: create_key,
        table_name: String::from("rust_serverless_sample"),
        ..Default::default()
    };
    ...
}
```

`DynamoDbClient`を利用して、clientオブジェクトを作成します。現在使用しているAWSリージョンを引数で渡しましょう。本記事ではバージニアリージョン`us-east-1`を使用しています。

```rust:main.rs
fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    ...
    let client = DynamoDbClient::new(Region::UsEast1);
    ...
}
```

実際にDynamoDBへの書き込みを行います。`put_item`の引数に上記で作成した`query_serials`を渡して`sync`すると、書き込まれた結果の成否が判定できます。

```rust:main.rs
fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> { {
    ...
    match client.put_item(create_serials).sync() {
        Ok(result) => {
            match result.attributes {
                Some(_) => println!("some"),
                None => println!("none"),
            }
        },
        Err(error) => {
            panic!("Error: {:?}", error);
        },
    };
    ...
}
```

最後に、レスポンスとしてメッセージを何か返してあげましょう。`Success`という文字列を単に返すだけのシンプルな構成にしてみます。

```rust:main.rs
fn my_handler() {
    ...
    Ok(CustomOutput {
        message: format!("Success"),
    })
    ...
}
```

ここまでの内容を全てまとめると、以下のようなmain.rsが完成します。

```rust:main.rs
#[macro_use]
extern crate lambda_runtime as lambda;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;
extern crate simple_logger;

extern crate rusoto_core;
extern crate rusoto_dynamodb;

use lambda::error::HandlerError;

use std::error::Error;

use rusoto_core::Region;
use rusoto_dynamodb::{DynamoDb, DynamoDbClient, ListTablesInput, GetItemInput, PutItemInput, DeleteItemInput, AttributeValue};
use std::collections::HashMap;

#[derive(Deserialize, Clone)]
struct CustomEvent {
    name: String,
}

#[derive(Serialize, Clone)]
struct CustomOutput {
    message: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    simple_logger::init_with_level(log::Level::Info) ?;
    lambda!(my_handler);

    Ok(())
}

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    if e.name == "" {
        error!("Empty name in request {}", c.aws_request_id);
        return Err(c.new_error("Empty name"));
    }

    let mut create_key: HashMap<String, AttributeValue> = HashMap::new();
    create_key.insert(String::from("name"), AttributeValue {
        s: Some(e.name),
        ..Default::default()
    });

    let create_serials = PutItemInput {
        item: create_key,
        table_name: String::from("rust_serverless_sample"),
        ..Default::default()
    };

    let client = DynamoDbClient::new(Region::UsEast1);

    match client.put_item(create_serials).sync() {
        Ok(result) => {
            match result.attributes {
                Some(_) => println!("some"),
                None => println!("none"),
            }
        },
        Err(error) => {
            panic!("Error: {:?}", error);
        },
    };

    Ok(CustomOutput {
        message: format!("Success"),
    })
}
```

ここで、作成したファイルをZipに固めて動作確認してみましょう。


![FireShot Capture 112 - Lambda Management Console_ - https___console.aws.amazon.com_lam.png](https://qiita-image-store.s3.amazonaws.com/0/206255/249112b6-737f-c9f0-a74b-d6bc33793da6.png)

DynamoDBでデータの登録を確認してみます。

![FireShot Capture 113 - DynamoDB · AWS Console_ - https___console.aws.amazon.com_dynamodb_home.png](https://qiita-image-store.s3.amazonaws.com/0/206255/fe4d1c13-5138-fa14-d94f-21ad405324b0.png)

パーティションキー以外のデータも登録したい場合には、以下のように`AttributeValue`を追加してあげればOKです。

```rust:main.rs
fn my_handler() {
    ...
    let mut create_key: HashMap<String, AttributeValue> = HashMap::new();
    create_key.insert(String::from("name"), AttributeValue {
        s: Some(e.name),
        ..Default::default()
    });
    // 追加でkey-valueを指定
    create_key.insert(String::from("address"), AttributeValue {
        s: Some(e.address), // "meguro".to_string()
        ..Default::default()
    });
    ...
}
```

DymamoDBを確認すると、属性が追加されていることがわかります。

![FireShot Capture 114 - DynamoDB · AWS Console_ - https___console.aws.amazon.com_dynamodb_home.png](https://qiita-image-store.s3.amazonaws.com/0/206255/1e3d65cc-e177-c2aa-8755-73211d5d2727.png)



続いて、登録したデータを取得してみましょう。

```rust:main.rs
fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    if e.name == "" {
        error!("Empty name in request {}", c.aws_request_id);
        return Err(c.new_error("Empty name"));
    }

    let mut query_key: HashMap<String, AttributeValue> = HashMap::new();
    query_key.insert(String::from("name"), AttributeValue {
        s: Some(e.name),
        ..Default::default()
    });

    let query_serials = GetItemInput {
        key: query_key,
        table_name: String::from("rust_serverless_sample"),
        ..Default::default()
    };

    let client = DynamoDbClient::new(Region::UsEast1);

    match client.get_item(query_serials).sync() {
        Ok(result) => {
            match result.item {
                Some(_) => print!("Match!"),
                None => print!("UnMatch!")
            }
            
        },
        Err(error) => {
            panic!("Error: {:?}", error);
        },
    };

    Ok(CustomOutput {
        message: format!("Success"),
    })
}
```

テストイベントに適当なキーを設定して実行してみましょう。DynamoDBに保存されているものなら「Match」、保存されていないものなら「Unmatch」がログに残り、データ取得が確認できると思います。

![FireShot Capture 115 - Lambda Management Console_ - https___console.aws.amazon.com_lam.png](https://qiita-image-store.s3.amazonaws.com/0/206255/3b1adf69-f21e-e74e-7b55-a30df7b6e184.png)


最後に、delete処理を実行します。下記をZipに固めたLambdaを実行したあとDynamoDBを覗くと、指定したキーのアイテムが削除されていることを確認できます。

```rust:main.rs
...
fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    if e.name == "" {
        error!("Empty name in request {}", c.aws_request_id);
        return Err(c.new_error("Empty name"));
    }

    let mut delete_key: HashMap<String, AttributeValue> = HashMap::new();
    delete_key.insert(String::from("name"), AttributeValue {
        s: Some(e.name),
        ..Default::default()
    });

    let delete_serials = DeleteItemInput {
        key: delete_key,
        table_name: String::from("rust_serverless_sample"),
        ..Default::default()
    };

    let client = DynamoDbClient::new(Region::UsEast1);

    match client.delete_item(delete_serials).sync() {
        Ok(result) => {
            match result.attributes {
                Some(_) => print!("some"),
                None => print!("none"),
            }
            
        },
        Err(error) => {
            panic!("Error: {:?}", error);
        },
    };

    Ok(CustomOutput {
        message: format!("Success"),
    })
}
```

このように、DynamoDBへの操作が可能なライブラリは既に用意されています。
API全量は以下のリンク先に記載されていますので、同じ要領で試してみてください。

https://rusoto.github.io/rusoto/rusoto_dynamodb/index.html

フィルタされた一覧情報の取得、トランザクション処理などの上級編は別記事でご紹介していこうと思います。

# 今後の期待
RustでLambda Layersを取り上げようと思いましたが、2018/12/13時点ではレイヤ部分のCustom Runtimeは対応しておりません。Lambda Layersを使用すると同じ記述を複数のLambdaで記載する必要がなくなるため便利ですが、Rust対応を待つことにします。

また、AuroraServerlessのアップデートにも期待です。現時点ではVPCを作成してその中にAuroraServerlessを構築していきますが、LambdaをVPC内に配置すると起動に時間がかかる場合があり、あまりお勧めできない構成となってしまいます。VPC外でも利用できるRDSベースのサービスができると、今後サーバレスRustも普及していくかも知れません。

AWSからのアップデートがありましたら記事を追加していきますので、引き続きよろしくお願いいたします！