RustアプリのコンテナイメージをLambdaで動かしてみた #reinvent

AWS re:Invent 2020で発表されたLambdaのコンテナイメージ対応をうけて、RustのLambdaアプリケーションをコンテナイメージ化して動かしてみました。
2020.12.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

現在開催中の re:Invent 2020 にてLambdaのコンテナサポートが発表されました。

【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました!! #reinvent

AWS提供のベースイメージに関するドキュメントを確認しましたが、残念ながら現時点でRust専用のイメージは提供されていません。しかし、 Base images for custom runtimes のセクションに providedprovided.al2 というカスタムランタイムで実装するときによく見る名前が出てきました。

Lambda向けのコンテナイメージは Lambda Runtime Interface に対応してね、と説明されていますがこれもカスタムランタイムでよく見かけるものです。Rustの場合は awslabs/aws-lambda-rust-runtime を利用すると裏側でLambda Runtime Interface経由でやり取りしてくれます。

これカスタムランタイムと同じやり方で動くんじゃない?と思い試してみたところばっちり動いてくれました。以下に試した手順を紹介します。

環境

  • Rust: 1.48.0
  • Docker: 20.10.0-rc1

試してみる

provided.al2 ベースのイメージで動作するように、以下の手順で試してみます。

  1. Lambdaアプリケーションを実装する
  2. Lambdaコンテナ用のイメージを作る
  3. AWS環境にデプロイする

Lambdaアプリケーションを実装する

Lambda以外の環境に持っていきやすくするため src/lib.rs にメインの処理を記述し、 src/bin/hello-container.rs にコンテナイメージ用のmain関数を実装していきます。

まずプロジェクトを作成し、 hello-container ディレクトリに移動します。なお以降の作業はすべて同ディレクトリで行います。

$ cargo new hello-container --lib
Created library `hello-container` package
$ cd hello-container

次に依存クレート・実行ファイルの指定を追加します。

Cargo.toml

[package]
name = "hello-container"
version = "0.1.0"
authors = ["yoshihitoh"]
edition = "2018"

# 実行ファイルの配置場所を変更しているため明示的に指定する
[[bin]]
name = "hello-container"
path = "src/bin/hello-container.rs"

[dependencies]
lambda_runtime = "0.2.1"
serde = { version = "1", features = ["derive"] }

イベントと出力データを定義し、ハンドラーの処理を実装します。

src/lib.rs

use serde::{Serialize, Deserialize};

#[derive(Debug, Deserialize)]
pub struct HelloEvent {
    name: String,
}

#[derive(Debug, Serialize)]
pub struct HelloOutput {
    message: String,
}

pub fn hello(event: HelloEvent) -> HelloOutput {
    HelloOutput {
        message: format!("Hello, {}!", event.name),
    }
}

Lambda用の実行ファイルを実装します。

src/bin/hello-container.rs

use std::error::Error as StdError;

use lambda_runtime::{error::HandlerError, lambda, Context};

use hello_container::{hello, HelloEvent, HelloOutput};

fn handler(event: HelloEvent, _context: Context) -> Result<HelloOutput, HandlerError> {
    Ok(hello(event))
}

fn main() -> Result<(), Box<dyn StdError>> {
    lambda!(handler);
    Ok(())
}

アプリケーションの実装は以上で完了です。ちゃんとコンパイルできるか cargo check で確認します。

$ cargo check
   Compiling autocfg v1.0.1
   Compiling libc v0.2.80
    Checking cfg-if v0.1.10
    Checking futures v0.1.30
   Compiling semver-parser v0.7.0
Checking hello-container v0.1.0 (/Users/yoshihitoh/workspace/projects/blog/rust/lambda-container/hello-container)
    Finished dev [unoptimized + debuginfo] target(s) in 40.12s
...

ちゃんとコンパイルできそうですね!

Lambdaコンテナ用のイメージを作る

provided.al2 環境で動作するようにビルドします。ビルドターゲットを x86_64-unknown-linux-musl にすればmacOS環境でもビルドできそうですが、せっかくなので provided.al2 環境でビルドしてみます。なお、Rustアプリケーションはビルド時にRustのツールチェーンが必要ですが、実行時には不要です。そこで、ビルド用・ランタイム用で分けてイメージを作ってみます。

  • Dockerfile.build : ビルド用のDockerfile
  • Dockerfile : ランタイム用のDockerfile

ビルド用のイメージ

provided:al2 をベースにビルド環境を構築します。

Dockerfile.build

FROM public.ecr.aws/lambda/provided:al2

# リンカーとしてgccを利用する
RUN yum install -y gcc

# rustupでRustツールチェーンをインストールする
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
ENV PATH $PATH:/root/.cargo/bin
RUN rustup install stable

# ビルド対象のソースツリーをマウントする
VOLUME /code

# ローカル環境にRustを導入している場合は以下をコメントアウトするとビルドが早くなります
#VOLUME /root/.cargo/registry
#VOLUME /root/.cargo/git

WORKDIR /code
# provided:al2 はランタイム用の設定になっているので、ENTRYPOINTをビルド用に書き換える
ENTRYPOINT ["cargo", "build", "--release"]

イメージをビルドします。

$ docker image build -t "hello-container-build" -f Dockerfile.build .
Sending build context to Docker daemon  478.9MB
Step 1/10 : FROM public.ecr.aws/lambda/provided:al2
 ---> 759805681d04
Step 2/10 : RUN yum install -y gcc
 ---> Running in a9bd79498d48
Loaded plugins: ovl
Resolving Dependencies
--> Running transaction check
---> Package gcc.x86_64 0:7.3.1-9.amzn2 will be installed
--> Processing Dependency: libgomp = 7.3.1-9.amzn2 for package: gcc-7.3.1-9.amzn2.x86_64
--> Processing Dependency: cpp = 7.3.1-9.amzn2 for package: gcc-7.3.1-9.amzn2.x86_64
...
... (中略)
...
Step 10/10 : ENTRYPOINT ["cargo", "build", "--release"]
 ---> Running in 7797bdb36274
Removing intermediate container 7797bdb36274
 ---> b92b1f199ecd
Successfully built b92b1f199ecd
Successfully tagged hello-container-build:latest

ビルドが成功したらコンテナを立ち上げて、アプリケーションを provided:al2 向けにビルドします。

$ docker container run --rm \
    -v $PWD:/code \
    -v $HOME/.cargo/registry:/root/.cargo/registry \
    -v $HOME/.cargo/git:/root/.cargo/git \
    hello-container-build
Compiling autocfg v1.0.1
   Compiling libc v0.2.80
   Compiling cfg-if v0.1.10
   Compiling futures v0.1.30
   Compiling semver-parser v0.7.0
...
... (中略)
...
Compiling lambda_runtime_errors v0.1.1
   Compiling lambda_runtime_client v0.2.2
   Compiling lambda_runtime v0.2.1
   Compiling hello-container v0.1.0 (/code)
    Finished release [optimized] target(s) in 7m 50s

ビルドが成功すると、ローカル環境の hello-container/target/release/hello-container に実行ファイルが出力されます。これを使ってランタイム用のイメージを作成していきます。

ランタイム用のイメージ

ランタイム用のイメージは簡単で、ビルド済みの実行ファイルをコピーしてハンドラを指定するだけです。カスタムランタイム同様ハンドラは利用しないため、適当な文字列を指定します。アプリケーション内で正しいハンドラ名が必要になる場合はその名称を指定してください。

Dockerfile

FROM public.ecr.aws/lambda/provided:al2

# 実行ファイルを起動するようにするため、ファイル名を "bootstrap" に変更する
COPY ./target/release/hello-container ${LAMBDA_RUNTIME_DIR}/bootstrap

# カスタムランタイム同様ハンドラ名は利用しないため、適当な文字列を指定する。
CMD [ "lambda-handler" ]

ランタイム用のイメージをビルドします。

$ docker image build -t "hello-container" .
Sending build context to Docker daemon  261.4MB
Step 1/3 : FROM public.ecr.aws/lambda/provided:al2
 ---> 759805681d04
Step 2/3 : COPY ./target/release/hello-container ${LAMBDA_RUNTIME_DIR}/bootstrap
 ---> 7e36acd891d5
Step 3/3 : CMD [ "dummy-handler" ]
 ---> Running in a129ddbc7c39
Removing intermediate container a129ddbc7c39
 ---> 7657889181e4
Successfully built 7657889181e4
Successfully tagged hello-container:latest

ちゃんとビルドできましたね!以上で準備完了です。

AWS環境にデプロイする

ECRにコンテナイメージをプッシュしてLambdaで動かしてみます。まず、ECRにリポジトリを作成します。

ECRにランタイム用のコンテナイメージをプッシュする

$ aws ecr create-repository --repository-name hello-container
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:xxxxxxxxxxxx:repository/hello-container",
        "registryId": "xxxxxxxxxxxx",
        "repositoryName": "hello-container",
        "repositoryUri": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-container",
        "createdAt": "2020-12-02T22:34:38+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

直前でビルドしたランタイムイメージをリポジトリにプッシュします。マネジメントコンソールからリポジトリにアクセスすると、 View push commands に以降で使うコマンドが表示されるので、これを使うと楽できてオススメです

# 手順1: ログイン
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
Login Succeeded

# 手順2: ランタイムイメージをビルド。ビルド済みの場合は飛ばしてOK
$ docker image build -t "hello-container" .

# 手順3: タグ付け
$ docker image tag hello-container:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-container:latest

# 手順4: ECRにプッシュする
$ docker image push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-container:latest
The push refers to repository [xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-container]
93052e3546e3: Pushed
120614c3628c: Pushed
0289476c93c4: Pushed
0bd47c7653d3: Pushed
af6d16f2417e: Pushed
latest: digest: sha256:db16a0eb085da33488bcce142141b1670fe9a43e40e7700b09332a237a61ff1f size: 1368

マネコンから確認します。ちゃんとプッシュされていますね!

最後にLamdbaを作って動作確認してみます。

Lambda作成&動作確認

手元のAWS CLIのバージョン(2.1.4)だとLambda作成時にコンテナイメージを指定できないようなので、マネコンから作成していきます。作成手順は以下の記事を参照してください。コンテナイメージを選択して作成するだけなのでめっちゃ楽です。

【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました!! #reinvent

テストイベントを作成して動かしてみます。

{
  "name": "Rust container image"
}

ばっちり動作しました!!

まとめ

RustのカスタムランタイムがLambda Runtime APIを利用しているので簡単にコンテナ化することができました!

FFIベースのクレートに依存している場合はライブラリ導入済みのコンテナイメージを構築したり、アプリケーション動作時に参照するアセットをコンテナイメージに組み込んだり、いろんな使い方ができそうです。ぜひ色々試して便利な使い方を見つけて行きましょう!

AWS re:Invent 2020 は 12/19(JST)まで開催中です!

参加がまだの方は、この機会に是非こちらのリンクからレジストレーションして豊富なコンテンツを楽しみましょう!

AWS re:Invent | Amazon Web Services

参考