Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

AWS Lambda の新機能 Custom Runtime を Rust でトライ

f:id:yuk1tyd:20181201203101j:plain

この記事は CyberAgent Developers Advent Calendar 2018 3日目の記事です.

アドテクスタジオ所属の yuki です.社内の方向けに軽く自己紹介をしておくと,2017/11 中途入社です.アドテクスタジオの某プロダクトでテックリードをしています.

Rust が好きなので,基本社内でも社外でも Rust の話しかしていません.Rust のゼミを最近同僚の方と一緒に立ち上げるなど,Scala や Go 言語の採用事例の多いアドテクスタジオ内で,Rust の市民権を得ようと (笑) がんばっています.

先日,ついに念願の待ちに待った, Rust によって記述された関数を AWS Lambda 上で実行可能になったというアナウンスがありました.Custom Runtime という機能です.実はこれまでにも Lambda 上で Rust を実行する方法はあったのです.が,完全に Rust サポートが達成されたので,これからユーザーは Custom Runtime を使っていくことになるでしょう.

ちなみにこれまで Rust では,

このように wasm を介して実行してみたり,あるいは

Go 言語のバイナリになりすましてみたりと,さまざまな方法で実行が試みられてきました.だがしかし!Rustacean はこれで,自身の大好きな Rust で Lambda を実行できるようになったのです!すばらしい世の中になりました.

話がそれてしまいましたが,まず新機能がどういった機能なのか,概要を説明していきます.つぎに,Rust がどのような言語かをご存知ない方も多いかと思いますので,Rust がどういった言語かについて説明します.そのあと,実際に Rust によって記述された関数を Lambda 上で動かしていくことにしましょう.

AWS Lambda Custom Runtime とは?

2018 年の re:Invent で発表された新機能です.要するにさまざまな言語で Lambda を動かすことができますよという環境です.

f:id:yuk1tyd:20181201200419p:plain
従来の言語に加えて,「独自のランタイムを使用する」という項目が追加されている

これを使用するためには,Lambda 上で動かすために必要な処理が書かれた bootstrap というバイナリファイルを用意し,それを zip に固めて従来どおりアップロードするだけです.

Rust とは?

f:id:yuk1tyd:20181201201252p:plain

Rust は近年注目度の高まってきている言語です.StackOverflow の愛され言語ランキングでもここ数年トップを取り続けています.大きな特徴としては次のような点があげられると思います.

所有権,借用,ライフタイム (Ownership, Borrowing, Lifetimes)

Rust の代名詞といってもいいかもしれません.

所有権やライフタイムという仕組みによる強力かつ安全なリソース管理が可能です.メモリだけでなく,ネットワークコネクションの管理も自動的に行います.安全性の満たせないプログラムはコンパイル時に拾い上げられます.

GC がないという特徴を聞いたことがある方もいるかもしれません.それはこの3つの概念によって,安全にメモリの確保・解放を管理しているがゆえに実現されています.メモリ管理に関して,プログラマが余計なコードを追加することはありません.

強力な型システム

型によるリソース管理が行われます.したがって,メモリ安全でない操作はコンパイル時に検出されます.他の言語ではランタイム時に解決されるような問題が,Rust においてはコンパイル時に解決されてしまいます.コンパイラが強い味方です.

また,強いて言うならば型クラス指向の言語で,それゆえに抽象化の力が強いです.抽象化の威力を存分に発揮した柔軟なソフトウェアデザインが可能です.型推論もほとんど完璧に行われるため *1,型を記述する場面はほとんどありません.

あらゆる箇所に型をつけていこうという強い意志が感じられます.またそれを利用した RustBelt のような定理証明系による支援プロジェクトも活発に行われています.強力な型システムに支えられた並行・並列処理への強さも魅力のうちのひとつです.

システムプログラミング言語である

システムプログラミング言語なので OS が作れます.「誰もがシステムプログラマーになれるように」というのが,次の Rust のキャッチフレーズに選ばれたとおり,Rust を使うことで誰もがシステムプログラマーになれます.

最高クラスのパフォーマンスとゼロコスト抽象化

もちろん時と場合によりますが,大抵のケースにおいて,かなり高速なプログラミング言語である Go 言語よりもさらに速くC++ とほぼ互角のパフォーマンスを発揮します.その要因のひとつは,安全性に重きをおきつつゼロコスト抽象化にも妥協しておらず,実行時の余計なオーバーヘッドが発生しないためです.ビビるくらい速い ("blazingly fast") と公式ドキュメントで謳っている通りです.

Cargo

Rust には最初からパッケージマネージャとビルドツールがついています.cargo です.cargo を使用するだけで,ライブラリの依存関係の解決やビルドをすべて cargo xxxコマンドラインで打つだけで実行できます.他言語にはよくあった「どのビルドツールがいいのか?」問題は,Rust では発生しません.cargo 一択だからです.

もちろん,cargo fmt と打つだけでフォーマッタも走ります.したがって,Rust ではフォーマット問題も発生しません.

温かいコミュニティ,ドキュメントの親切さ

Rust の最大の資産になりうるのは,私は温かいコミュニティだと思います.Rust コミュニティには,「Rust は難しい」という自覚が (おそらく) あり,入門者の方が少しでもスムーズに入門できるように,さまざまな工夫の凝らされたドキュメントが豊富に用意されています.OSS でもドキュメント,あるいはコード内のコメントや Spec 用のテストを丁寧に書く文化が醸成されており,初めて使うライブラリの使い方がわからない…という場面に遭遇することが少ないように思います.

Custom Runtime を使ってみる

ドキュメントに沿ってやってみようと思います*2.使用環境は Mac OS X です.エディタは CLion を使用しています.今回参考にしたドキュメントはこちら

toml の準備

サンプルプログラムでは次の crate を使用しますので,Cargo.toml に設定を追加します.

  • lambda_runtime: AWS 提供の Lambda Runtime が記述されたライブラリ.
  • serde, serde_json, serde_derive: Rust ではおなじみの JSON のパースを行うためのライブラリ.
  • log, simple_logger: ロギング機構.

また,Lambda に読み込ませるためにバイナリファイルの設定が若干必要なので,それも行いましょう.完成した Cargo.toml です.

[package]
name = "rust-lambda-testing"
version = "0.1.0"
authors = [{your author name}]

[dependencies]
lambda_runtime = "^0.1"
serde = "^1"
serde_json = "^1"
serde_derive = "^1"
log = "^0.4"
simple_logger = "^1"

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

クロスビルドのための準備

Lambda の環境に合うようにクロスビルドを行う必要があります.Mac の場合の設定方法は公式ドキュメントに載っていました.とても親切ですね.それに従って設定していきましょう.

まず,rustup ツールチェインに x86_64-unknown-linux-musl 向けの設定を追加します.

$ rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
 14.9 MiB /  14.9 MiB (100 %)   3.1 MiB/s ETA:   0 s                
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'

次に,Mac であれば x86_64-unknown-linux-musl 向けのリンカを brew を経由して入れる必要があるので,入れておきます.

$ brew install filosottile/musl-cross/musl-cross

最後に .cargo/config に設定を追加しておきます.

mkdir .cargo

config ファイルを作成して,

[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

と書いておきます.

これで準備は整いました.それでは,コードを軽く書いてビルドし,実際に動かしてみましょう.

コード

とりあえず動かしてみたいので,公式サンプルをそのまま使います.

#[macro_use]
extern crate lambda_runtime as lambda;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;
extern crate simple_logger;

use lambda::error::HandlerError;

use std::error::Error;

#[derive(Deserialize, Clone)]
struct CustomEvent {
    #[serde(rename = "firstName")]
    first_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.first_name == "" {
        error!("Empty first name in request {}", c.aws_request_id);
        return Err(c.new_error("Empty first name"));
    }

    Ok(CustomOutput {
        message: format!("Hello, {}!", e.first_name),
    })
}

ビルド & アップロード用の zip 生成

--release ビルドをしましょう.最適化が走り,Rust 本来の力を発揮できるためです.

cargo build --release --target x86_64-unknown-linux-musl

ところで私の環境でビルドすると,musl-gcc がないと言われました.先ほどインストールした brew の musl が認識されていなかったようです.次のようなエラーが出てしまい,ビルドに失敗しました.

--- stderr
thread 'main' panicked at '

Internal error occurred: Failed to find tool. Is `musl-gcc` installed?

', /[root dir name]/.cargo/registry/src/github.com-1ecc6299db9ec823/cc-1.0.25/src/lib.rs:2260:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

対策は公式ドキュメントにしれっと書いてありますが,

ln -s /usr/local/bin/x86_64-linux-musl-gcc /usr/local/bin/musl-gcc

というリンクを設定しておくことで回避可能です.こうすると,ビルドが通ります.

次に Lambda アップロード用の zip ファイルを作ります.

zip -j rust.zip ./target/x86_64-unknown-linux-musl/release/bootstrap

すると,このような zip ファイルが作成されるかと思います.これを Lambda にアップロードすることで,関数を実行することができます.

f:id:yuk1tyd:20181202165007p:plain
rust.zip が生成されました

実行

上記の zip ファイルを Lambda にアップロードします.その後,テスト関数に次のような JSON を入力し,テストを実行します.

{
  "firstName": "Rustacean"
}

まとめ

  • AWS Lambda Custom Runtime のおかげで,ついに Rust を Lambda で動かす環境が正式にサポートされました.
  • Rust のサンプルプログラムを作って遊びたい際に,Lambda 上で一度遊んでみるという選択肢が増えたという点でとてもすばらしいと思います.
  • もう少し重ためのプログラムを動かして,Rust の威力を存分に味わってみたいなと思いました→と思ったら,Rust の Advent Calendar の方ですでに計測してくださった方がいました.Go 言語と遜色ない性能がでているようですね.(追記あり) AWS Lambda が正式に Rust 対応したので KinesisFirehose にくっつけて性能計測した #rustlang #rust_jp - ソモサン

*1:Hindley-Milner ベースだが,ライフタイムのサポートがあるので完全なそれではありません.

*2:ここからは,Rust をすでに使用したことのある方を対象として書きすすめていきます.Rust のインストールなどは,Rust の公式サイトをご覧ください.また,詳しいプログラムの解説については,今回参考にした公式ドキュメントにかなり突っ込んで書いてありますので,そちらもあわせてご覧ください.