Open22

AWS LambdaをRustで動かしてみる

zamaxzamax

なんか知らんけど、rustupがバージョンアップできなかった。(m1チップの問題っぽい?)

error: failed to install component: 'rust-std-wasm32-unknown-unknown', detected conflict: 'lib/rustlib/manifest-rust-std-wasm32-unknown-unknown'
info: checking for self-updates

  stable-aarch64-apple-darwin update failed - rustc 1.56.1 (59eed8a2a 2021-11-01)

info: cleaning up downloads & tmp directories

ので仕方ないから一回アンインストールして

https://yuzu441.hateblo.jp/entry/2017/03/22/201027

公式から入れ直した

https://www.rust-lang.org/tools/install

今のバージョン

rustc 1.57.0 (f1edd0429 2021-11-29)
rustup 1.24.3 (ce5817a94 2021-05-31)
cargo 1.57.0 (b2e52d7ca 2021-10-21)
zamaxzamax

ちなみに本当はVSCodeのRemoteContainerで環境作りたいんだけど、以前にやはりm1チップがらみで断念した気がする。今はできるようになってるのかな。。。

zamaxzamax

とりあえずcargo newする

cargo new rust-lambda-cdk
zamaxzamax

とりあえず、

[package]
name = "rust-lambda-cdk"
version = "0.1.0"
edition = "2021"
readme = "README.md"
license = "MIT"

[dependencies]
lambda_runtime = "0.4.1"
zamaxzamax

とりあえずRust部分の写経をしてみる。
DevelopersIOとaws-lambda-rust-runtimeのREADMEのあいのこ写経になってきた。

Cargo.toml

[package]
name = "rust-lambda-cdk"
version = "0.1.0"
edition = "2021"
readme = "README.md"
license = "MIT"

[lib]
name = "lib"
path = "src/lib.rs"

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

[dependencies]
lambda_runtime = "0.4.1"
serde_json = "1.0.74"
tokio = "1.15.0"

src/lib.rs

use lambda_runtime::{Context, Error};
use serde_json::{json, Value};

pub async fn hello_world(event: Value, _: Context) -> Result<Value, Error> {
    let first_name = event["firstName"].as_str().unwrap_or("world");
    
    Ok(json!({"message": format!("Hello, {}!", first_name)}))
}

src/bin/bootstrap.rs

use lambda_runtime::{handler_fn, Error};
use ::lib::hello_world;

#[tokio::main]
async fn main() -> Result<(), Error>{
    println!("execute bootstrap#main");
    let runtime_handler = handler_fn(hello_world);
    lambda_runtime::run(runtime_handler).await?;
    Ok(())
}
zamaxzamax

改めてLambdaのカスタムランタイムの仕様を調べてみる。
なるほど、bootstrapという名前の実行ファイルを叩いたときに初期化処理とイベントループが回ればいいのか。
おそらくlambda_runtime::runは後者のイベントループを回してくれていると。

https://www.m3tech.blog/entry/aws-lambda-custom-runtime

zamaxzamax

とりあえずmacからamazon linuxにクロスコンパイルできるように設定。これCIするときはどうするんだろ。。まあ、後で考えるか。

クロスコンパイラ?のインストール

brew install filosottile/musl-cross/musl-cross

意外と時間かかるので待機。

zamaxzamax

これ、クロスコンパイルしなくていいっていう意味ではAWS Cloud9とかGitHub CodeSpacesに開発環境作った方が捗るのかも知れない。

zamaxzamax

とりあえず素でビルドしてみる。

targetの追加

rustup target add x86_64-unknown-linux-musl 

ビルド

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

./target/x86_64-unknown-linux-musl/release以下にbootstrapを含めたいろいろなファイルができた。これをlambdaのカスタムランタイムに突っ込めば良いわけか。

zamaxzamax
projen new awscdk-app-ts

このままだと、元々cargo newで作られていた.gitignoreが上書きされてtarget/以下がgitの管理対象になってしまうので、

./.projenrc.js

const { awscdk } = require('projen');
const project = new awscdk.AwsCdkTypeScriptApp({
  cdkVersion: '2.1.0',
  defaultReleaseBranch: 'main',
  name: 'rust-lambda-cdk',

  gitignore: [
    'target/'
  ]

  // deps: [],                /* Runtime dependencies of this module. */
  // description: undefined,  /* The description is just a string that helps people understand the purpose of the package. */
  // devDeps: [],             /* Build dependencies for this module. */
  // packageName: undefined,  /* The "name" in package.json. */
});
project.synth();

.projenrc.jsを更新して、

npx projen
zamaxzamax

DevelopersIOの記事を参考に、先ほど手でやったRustのビルドをnpm scriptでやるように編集。

./.projenrc.js

const { awscdk } = require('projen');
const project = new awscdk.AwsCdkTypeScriptApp({
  cdkVersion: '2.1.0',
  defaultReleaseBranch: 'main',
  name: 'rust-lambda-cdk',

  gitignore: [
    'target/'
  ],

  scripts: {
      "build:rust": "npm run build:rust:clean && rustup target add x86_64-unknown-linux-musl && cargo build --release --target x86_64-unknown-linux-musl && mkdir -p ./target/cdk/release && cp ./target/x86_64-unknown-linux-musl/release/bootstrap ./target/cdk/release/bootstrap",
      "build:rust:clean": "rm -r ./target/cdk/release || echo '[build:clean] No existing release found.'" 
  }


  // deps: [],                /* Runtime dependencies of this module. */
  // description: undefined,  /* The description is just a string that helps people understand the purpose of the package. */
  // devDeps: [],             /* Build dependencies for this module. */
  // packageName: undefined,  /* The "name" in package.json. */
});
project.synth();
npx projen
npm run build:rust

target/cdk/releasebootstrapがコピーされたのでOKっぽい。

zamaxzamax

CDKの空定義をする。projen newしたときに既にsrcディレクトリがあったからなのか、projenmain.tsを作ってくれないので自作する。

./src/main.ts

import { App, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class RustLambdaCdkStack extends Stack {
  constructor(scope: Construct, id:string, props: StackProps = {}) {
    super(scope, id, props);
  }
}

const devEnv = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
};

const app = new App();

new RustLambdaCdkStack(app, 'rust-lamnda-cdk', { env: devEnv });

app.synth();

./test/main.test.ts

test('No test yet', () => {
  expect(true).toBe(true);
});

ついでに、npm run buildしたときに、先にRustをコンパイルするよう.projenrc.jsを設定する。

./.projenrc.js

const { awscdk } = require('projen');
const project = new awscdk.AwsCdkTypeScriptApp({
  cdkVersion: '2.1.0',
  defaultReleaseBranch: 'main',
  name: 'rust-lambda-cdk',

  gitignore: [
    'target/',
  ],

  scripts: {
    'build:rust': 'npm run build:rust:clean && rustup target add x86_64-unknown-linux-musl && cargo build --release --target x86_64-unknown-linux-musl && mkdir -p ./target/cdk/release && cp ./target/x86_64-unknown-linux-musl/release/bootstrap ./target/cdk/release/bootstrap',
    'build:rust:clean': "rm -r ./target/cdk/release || echo '[build:clean] No existing release found.'",
  },


  // deps: [],                /* Runtime dependencies of this module. */
  // description: undefined,  /* The description is just a string that helps people understand the purpose of the package. */
  // devDeps: [],             /* Build dependencies for this module. */
  // packageName: undefined,  /* The "name" in package.json. */
});
project.preCompileTask.exec('npx projen build:rust');
project.synth();
zamaxzamax

カスタムランタイムのLambdaをCDKで定義。

import { App, Stack, StackProps } from 'aws-cdk-lib';
import { Code, Function, Runtime, Tracing } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class RustLambdaCdkStack extends Stack {
  constructor(scope: Construct, id:string, props: StackProps = {}) {
    super(scope, id, props);

    new Function(this, 'rust-helloworld', {
      runtime: Runtime.PROVIDED_AL2,
      handler: 'rust-lambda-cdk',
      code: Code.fromAsset(`${__dirname}/../target/cdk/release`),
      tracing: Tracing.ACTIVE,
    });
  }
}

const devEnv = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
};

const app = new App();

new RustLambdaCdkStack(app, 'rust-lamnda-cdk', { env: devEnv });

app.synth();
zamaxzamax

ちなみに上を書くときに、handlerに何を入れるだろうと思ったが。。。以下を読む限りでは今回に関しては何入れても良いっぽい。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-custom.html

本来は、bootstrapの中で環境変数_HANDLERの中身を確認して、それに応じて実行する(Rustの)関数を切り分けたりするべきなのかな。

zamaxzamax

ということで、deployする。

npm run deploy

特に問題なくというか、普通にdeployできた。

zamaxzamax

マネージメントコンソールからテスト実行。速い・・・スクショは数回目の実行結果なのでホットスタートだが、それにしても速い。。記憶ベースだが、コールドスタート時もトータル10msくらいだったと思う。こんなHello Worldコードでもはっきり分かるくらいインタプリタ系の言語とは格が違う。