RustをつかってAWS Lambdaを実装&AWS CDKでデプロイする

2021.04.26

Introduction

最近Rustが各所で盛り上がっています。

Rustは5年連続で最も愛されているプログラミング言語になっている
開発者に人気のプログラミング言語です。
また、LinuxカーネルにRustを採用しようという動きがあったり、
AndroidのOS開発でRustをサポート 、といった具合に、 さまざまなところでRustの話題がでています。

AWS・Google・MicrosoftなどがRust Foundationを立ち上げたことも後押しとなり、
使えるようになりたいなーということで最近私もさわりはじめました。

本記事ではカスタムランタイムをつかってRustでAWS Lambdaを作成し、AWS CDKでデプロイしたり
Localstackを使ってローカルでLambdaを実行したりしてみます。

本記事は、ここにあるソースほぼそのまま参考にして作成しました。
実際は(私の環境では)このままだと動かなかったので、
これを参考にしてさらに内容を絞ったサンプルを作成しました。

作成したサンプルはここです。

Environment

  • OS : MacOS 10.15.7
  • Node : v14.4.0
  • Docker : 20.10.5
  • aws-cli : 2.1.38
  • Rust : 1.51.0

※AWSアカウント設定などは終わっている前提

Create Rsut + Lambda example

ではRustで動くAWS Lambdaをつくっていきましょう。

Create & Setup Project

まずはcargoでプロジェクトを新規作成。

% cargo new rust-lambda-cdk
Created binary (application) `rust-lambda-cdk` package

cargo.tomlに必要な情報を記述します。
エントリーポイントとなるプログラムはbootstrap.rsです。

[package]
name = "rust-lambda-cdk"
version = "0.1.0"
authors = ["your name <your mail address>"]
edition = "2018"
readme = "README.md"
license = "MIT"

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

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

[dependencies]
lambda = { package = "netlify_lambda", version = "0.2.0" }
tokio = "1.5.0"
serde = "1.0.125"
serde_derive = "1.0.125"
serde_json = "1.0.64"

[dev-dependencies]
pretty_assertions = "0.7.2"

次にnpm設定。

% npm init
・・・

必要になるnpmモジュールをインストールします。

  • @aws-cdk/aws-lambda
  • @aws-cdk/core
  • @aws-cdk/aws-s3
  • @types/node
  • aws-cdk
  • ts-node
  • typescript
  • tsconfig-paths
% npm install --save  @aws-cdk/aws-lambda ・・・・・・・

lambdaのデプロイはcdkを使用します。
今回cdkはTypeScriptで記述するので、tsconfigを作成しましょう。

% tsc --init

tsconfig.json は下のような感じで記述します。

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "lib": ["es2016", "es2017.object", "es2017.string"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "baseUrl": "."
  }
}

次に、ローカルでLinuxを対象にしてビルドしなければいけないのでクロスコンパイラをインストールします。

% brew install filosottile/musl-cross/musl-cross

.cargo/configファイルを作成し、linkerの指定。

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

package.jsonのscriptsにビルドやデプロイ用スクリプトを記述します。
ビルドやデプロイなど、必要なコマンドを定義します。

・・・

  "scripts": {
    "build": "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:clean": "rm -r ./target/cdk/release || echo '[build:clean] No existing release found.'",
    "deploy": "npm run build:clean && npm run build && npm run cdk:deploy",
    "cdk:deploy": "[[ $CI == 'true' ]] && export CDK_APPROVAL='never' || export CDK_APPROVAL='broadening'; cdk deploy --require-approval $CDK_APPROVAL '*'",
    "cdk:bootstrap": "cdk bootstrap aws://$(aws sts get-caller-identity | jq -r .Account)/$AWS_REGION",
    "cdklocal:start": "docker-compose up",
    "cdklocal:clear-cache": "(rm ~/.cdk/cache/accounts.json || true) && (rm ~/.cdk/cache/accounts_partitions.json || true)",
    "cdklocal:deploy": "npm run --silent cdklocal:clear-cache && CDK_LOCAL=true  cdklocal deploy --require-approval never '*'",
    "cdklocal:bootstrap": "npm run --silent cdklocal:clear-cache && CDK_LOCAL=true  cdklocal bootstrap aws://000000000000/us-west-1"
  },

・・・

cdklocal:〜のコマンドは、LocalStackを使ってローカルでLambdaの動作確認をするためのコマンドです。
LocalStackはでDockerコンテナで起動するので、使用する場合には事前にDockerをインストールしておきます。

build Rust sources

環境ができたので、Rustのソースを記述します。
Lambdaのエントリーポイントとなるsrc/bin/bootstrap.rsを作成しましょう。

use lambda::handler_fn;
use ::lib::handler;
use ::lib::LambdaError;

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

src/lib.rsではbootstrapから利用するハンドラを定義します。
nameという名前のパラメータをうけとったら、その値で文字列を生成してJsonで返します。

use lambda::Context;
use serde_json::{json, Value};

pub type LambdaError = Box<dyn std::error::Error + Send + Sync + 'static>;

pub async fn handler(event: Value, _: Context) -> Result<Value, LambdaError> {
    println!("execute lib#handler");
    let name = event["name"].as_str().unwrap_or("world");
    Ok(json!({ "message": format!("Hello, {}!", name) }))
}

npm runでビルドしてみます。

% npm run build

> rust-lambda-cdk@1.0.0 build /rust-lambda-cdk
> 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

info: component 'rust-std' for target 'x86_64-unknown-linux-musl' is up to date
    Finished release [optimized] target(s) in 0.52s

target/cdk/release以下にファイルが生成されます。

Deploy via CDK

ビルドが完了したので、次はCDKをつかってAWS Lambdaをデプロイします。
まずはcdk.jsonを作成。

{
  "app": "ts-node -r tsconfig-paths/register deploy/stack.ts"
}

deployディレクトリを作成し、そこにstack.tsを下記内容で記述します。

import * as cdk from "@aws-cdk/core";
import { LambdaStack } from "./lib/lambda-stack";
import * as pkg from "../package.json";

const { BENCHMARK_SUFFIX } = process.env;
const STACK_NAME = BENCHMARK_SUFFIX ? `${pkg.name}-${BENCHMARK_SUFFIX}` : pkg.name;

export default class Stack {
  public lambdaStack: LambdaStack;

  constructor(app: cdk.App) {
    this.lambdaStack = new LambdaStack(app, `${STACK_NAME}`, {});
  }
}

const app = new cdk.App();
new Stack(app);

deploy/lib/lambda-stack.tsを作成します。
ここではLambdaの設定を行います。

idはcargoで指定しているpackage.name(rust-lambda-cdk)、
functionNameは${id} + "-main"となります。
(rust-lambda-cdk-main)

import * as core from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as s3 from "@aws-cdk/aws-s3";
import * as cdk from "@aws-cdk/core";

const { CDK_LOCAL } = process.env;

interface Props {}

export class LambdaStack extends core.Stack {
  constructor(scope: cdk.App, id: string, props: Props) {
    super(scope, id);

    const bootstrapLocation = `${__dirname}/../../target/cdk/release`;

    const entryId = "main";
    const entryFnName = `${id}-${entryId}`;
    const entry = new lambda.Function(this, entryId, {
      functionName: entryFnName,
      description: "Rust + Lambda + CDK",
      runtime: lambda.Runtime.PROVIDED_AL2,
      handler: `${id}`, 
      code:
        CDK_LOCAL !== "true"
          ? lambda.Code.fromAsset(bootstrapLocation)
          : lambda.Code.fromBucket(s3.Bucket.fromBucketName(this, `LocalBucket`, "__local__"), bootstrapLocation),
      memorySize: 256,
      timeout: cdk.Duration.seconds(10),
      tracing: lambda.Tracing.ACTIVE,
    });

    entry.addEnvironment("AWS_NODEJS_CONNECTION_REUSE_ENABLED", "1");

    core.Aspects.of(entry).add(new cdk.Tag("service-type", "API"));
    core.Aspects.of(entry).add(new cdk.Tag("billing", `lambda-${entryFnName}`));
  }
}

CDK用のtsを記述したら、AWS_REGION環境変数をセットし、boostrapコマンドを実行します。
※ 事前にaws configureで適切なLambdaへのアクセス設定をしておく

% export AWS_REGION=<your target region>
% npm run bootstrap
> rust-lambda-cdk@1.0.0 cdk:bootstrap /Users/nakamurashuuta/dev/rust/rust-lambda-cdk
> cdk bootstrap aws://$(aws sts get-caller-identity | jq -r .Account)/$AWS_REGION

 ⏳  Bootstrapping environment aws://xxxxxxxx/us-west-1...
 ✅  Environment aws://xxxxxxxx/us-west-1 bootstrapped (no changes).

成功したら次はビルドしたLambdaプログラムをデプロイ。

% npm run deploy

> rust-lambda-cdk@1.0.0 deploy /dev/rust/rust-lambda-cdk
> npm run build:clean && npm run build && npm run cdk:deploy


> rust-lambda-cdk@1.0.0 build:clean /dev/rust/rust-lambda-cdk
> rm -r ./target/cdk/release || echo '[build:clean] No existing release found.'


> rust-lambda-cdk@1.0.0 build /dev/rust/rust-lambda-cdk
> 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

info: component 'rust-std' for target 'x86_64-unknown-linux-musl' is up to date
    Finished release [optimized] target(s) in 0.38s

> rust-lambda-cdk@1.0.0 cdk:deploy /dev/rust/rust-lambda-cdk
> [[ $CI == 'true' ]] && export CDK_APPROVAL='never' || export CDK_APPROVAL='broadening'; cdk deploy --require-approval $CDK_APPROVAL '*'

rust-lambda-cdk: deploying...

 ✅  rust-lambda-cdk (no changes)

Stack ARN:
arn:aws:cloudformation:us-west-1:xxxxxxxxx:stack/rust-lambda-cdk/xxxx-xxxxx-xxxxx

invokeコマンドでデプロイしたLambdaを実行してみましょう。

% aws lambda invoke \
--function-name rust-lambda-cdk-main \
--cli-binary-format raw-in-base64-out \
--region $AWS_REGION \
--payload '{}' \
tmp-output.json > /dev/null && cat tmp-output.json && rm tmp-output.json

{"message":"Hello, world!"}

Rustで記述したAWS LamdaがCDKでデプロイされ、実行できるのを確認しました。

LocalStackを使う

次は LocalStackを使って、ローカルでLamdaを動かしてみます。
dockerが使用できる状態になっていれば、docker-compose.ymlを用意するだけ。
内容はほぼここにあるようなものです。

version: "2.1"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack:latest
    network_mode: bridge
    ports:
      - "4566:4566"
      - "4571:4571"
      - "${PORT_WEB_UI-9888}:${PORT_WEB_UI-9888}"
    environment:
      - SERVICES=${SERVICES-serverless,cloudformation,iam,sts,sqs,ssm,s3,acm,cloudwatch,cloudwatch-logs,lambda,apigateway}
      - DEFAULT_REGION=${DEFAULT_REGION-us-west-1}
      - DEBUG=${DEBUG- }
      - DATA_DIR=${DATA_DIR- }
      - PORT_WEB_UI=${PORT_WEB_UI-9888}
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
      - LAMBDA_REMOTE_DOCKER=${LAMBDA_REMOTE_DOCKER-false}
      - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
      - DOCKER_HOST=unix:///var/run/docker.sock
      - HOST_TMP_FOLDER=${TMPDIR:-/tmp/localstack}
    volumes:
      - "${TMPDIR:-/tmp/localstack}:/tmp/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

cdklocal:startコマンド(docker-compose upしてるだけ)でLocalStackコンテナの起動をします。

% npm run cdklocal:start

 > rust-lambda-cdk@1.0.0 cdklocal:start /dev/rust/rust-lambda-cdk
 > docker-compose up

 Docker Compose is now in the Docker CLI, try `docker compose up`

 Creating localstack_main ... done
 Attaching to localstack_main

LocalStackに対してbootstrapとdeployを実行しましょう。

% npm run cdklocal:bootstrap
> rust-lambda-cdk@1.0.0 cdklocal:bootstrap /dev/rust/rust-lambda-cdk
> npm run --silent cdklocal:clear-cache && CDK_LOCAL=true  cdklocal bootstrap aws://000000000000/us-west-1

 ⏳  Bootstrapping environment aws://000000000000/us-west-1...
CDKToolkit: creating CloudFormation changeset...
10:28:10 | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack | UsePublicAccessBlockConfiguration
 ✅  Environment aws://000000000000/us-west-1 bootstrapped.

% npm run cdklocal:deploy
> rust-lambda-cdk@1.0.0 cdklocal:deploy /dev/rust/rust-lambda-cdk
> npm run --silent cdklocal:clear-cache && CDK_LOCAL=true  cdklocal deploy --require-approval never '*'

rust-lambda-cdk: deploying...
rust-lambda-cdk: creating CloudFormation changeset...
10:29:05 | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack | CDKMetadata/Condition

 ✅  rust-lambda-cdk

Stack ARN:
arn:aws:cloudformation:us-west-1:000000000000:stack/rust-lambda-cdk/xxxxx

LocalStackに対してinvokeしてみます。(今度はパラメータつき)

% aws --endpoint-url=http://localhost:4566 lambda invoke \
--function-name rust-lambda-cdk-main \
--cli-binary-format raw-in-base64-out \
--payload '{"name": "taro"}' \
tmp-output.json > /dev/null && cat tmp-output.json && rm tmp-output.json

{"message":"Hello, taro!"}

Summary

今回はRustでAWS Lambdaを実装してCDKでデプロイしてみました。
RustとLambdaは相性がいいといわれてますが、
開発環境も使いやすくなればさらに開発しやすくなりますね。

参考