AWS CDKを使ってAPI Gateway Lambda(Rust)をデプロイする

1. 背景

AWSコンソール上でポチポチするのは面倒なので、CDKを使ってAWSリソースの作成します。 Rustについては速度に関してLambdaと相性が良いことを期待しているのと、開発体験がどう変わるのか、という実験的部分もありつつ採用しました。 ※Lambdaの中身については、最低限の動作確認が出来るレベルのものなので、この点は悪しからず。

CDKは内部的にCloudFormationが展開されるわけですが、言語の選択権もあり、書き心地や見通しも非常に良いです。
AWSリソースをプロビジョニングするならCDK一択で問題ない、という点から選択しています。
また、所々でURLを掲載していますが完成品は以下となります。

github.com

2. 前提

インストールがされている前提で進めますが、インストール時に参考としたページは次項へ記載。 使用する各種ツール群のVersionは以下のとおり。

# MacOS
Monterey 12.5
Intel chip

# 各種ツール群
aws --version && \
cdk --version && \
rustc --version && \
cargo lambda -V;

aws-cli/2.4.11 Python/3.8.8 Darwin/21.6.0 exe/x86_64 prompt/off
2.49.0 (build 793dd76)
rustc 1.63.0 (4b91a6ea7 2022-08-08)
cargo-lambda 0.11.2 (f9cead5 2022-10-08Z)

2.1. インストール時の参考

docs.aws.amazon.com

docs.aws.amazon.com

www.rust-lang.org

github.com

3. 作業ディレクトリの作成

ディレクトリを作成します。 この中にCDKプロジェクトとLambdaのプロジェクトを作成します。

mkdir rust_backend_cdk
cd rust_backend_cdk

4. CDKプロジェクト作成

initコマンドで作成します。今回はTypeScriptを使用します。

cdk init --language typescript

5. Stackを書いていく

yamljsonで書くのに比べるとかなり楽です。 公式のAPI ReferenceはSampleが豊富なので、こちらを見ながら進めました。

docs.aws.amazon.com

APIGatewayからLambdaを呼び出す前提ですが、パプリックな状態なのでAPIキーを必須にしています。

// https://github.com/Tanabebe/rust_backend_cdk/blob/main/lib/rust_backend_cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import {Construct} from 'constructs';
import {ApiKeySourceType, LambdaIntegration, RestApi} from "aws-cdk-lib/aws-apigateway";
import {Architecture, Code, Function as LambdaFunction, Runtime} from "aws-cdk-lib/aws-lambda";
import {join} from "path";
import {Duration} from "aws-cdk-lib";

export class RustBackendCdkStack extends cdk.Stack {

  // API Gatewayの作成
  private api = new RestApi(this, 'ExampleRustApi', {
    apiKeySourceType: ApiKeySourceType.HEADER
  });

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // APIキーの作成
    const apiKey = this.api.addApiKey('apiKey', {
      apiKeyName: 'ExampleRustApiKey'
    });

    // 使用量プランの作成
    const usagePlan = this.api.addUsagePlan('ExampleRustApiUsagePlan');
    usagePlan.addApiKey(apiKey);
    usagePlan.addApiStage({
      stage: this.api.deploymentStage
    })

    // Lambdaの設定
    const exampleRustLambda = new LambdaFunction(this, 'ExampleRustLambda', {
      runtime: Runtime.PROVIDED_AL2,
      code: Code.fromAsset(join(__dirname, '..', 'functions', 'target', 'lambda', 'functions', 'bootstrap.zip')),
      architecture: Architecture.ARM_64,
      memorySize: 128,
      handler: 'bootstrap',
      timeout: Duration.seconds(20),
    });

    // LambdaをAPIGatewayに統合する
    const exampleLambdaIntegration = new LambdaIntegration(exampleRustLambda);

    // API Gatewayのリソース定義
    const exampleLambdaResource = this.api.root.addResource('example');
    exampleLambdaResource.addMethod('POST', exampleLambdaIntegration, {
      apiKeyRequired: true
    });
  }
}

5.1. Lambdaを作成する

CDKのプロジェクトと同ディレクトリにLambdaのプロジェクトを作成します。

cargo lambda new functions
# 対話時の回答
? Is this function an HTTP function? Yes
? Which service is this function receiving events from? Amazon Api Gateway REST Api

Cargo.tomlに追記する。
※crateのダウンロードに時間がかかるので待ちます。

# https://github.com/Tanabebe/rust_backend_cdk/blob/main/functions/Cargo.toml
serde = "1.0.148"
serde_json = "1.0.89"
openssl = { version = '0.10.42', features = ["vendored"] }
openssl-sys = "0.9.76"
lambda-apigateway-response = "0.1.1"

6. Lambdaを書く

awslabsのサンプルとほぼ同じですが、参考にしながら最低限の動作確認が出来るレベルで書いていきます。 これくらいの内容なら問題ないですが、もう少し混み合ってくると大体1発でコンパイルが通らないので、よく怒られます。

// https://github.com/Tanabebe/rust_backend_cdk/blob/main/functions/src/main.rs
use lambda_runtime::{service_fn, LambdaEvent, Error};
use serde_json::{json, Value};
use lambda_apigateway_response::http::StatusCode;
use lambda_apigateway_response::{Headers, MultiValueHeaders, Response};

type LambdaResult<T> = Result<T, Error>;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let func = service_fn(func);
    lambda_runtime::run(func).await?;
    Ok(())
}

async fn func(event: LambdaEvent<Value>) -> LambdaResult<Response<Value>> {
    let (event, _context) = event.into_parts();
    let first_name = event["body"]["firstName"].as_str().unwrap_or("world");

    let res = Response {
        status_code: StatusCode::OK,
        body: json!({ "message": format!("Hello, {}!", first_name) }),
        headers: Headers::new(),
        multi_value_headers: MultiValueHeaders::new(),
        is_base64_encoded: false,
    };
    Ok(res)
}

7. デプロイ準備

awsのアカウント設定をします。

# aws設定
aws configure
AWS Access Key ID [****************]: your access key
AWS Secret Access Key [****************]: your secret access key
Default region name [ap-northeast-1]: ap-northeast-1
Default output format [json]:json
# cdkをデプロイする時に必要なので実行(1回のみ)
cdk bootstrap

毎度複数のコマンドを叩くのは面倒なので簡易的ではありますが、デプロイ用のMakefileを作成します。

# https://github.com/Tanabebe/rust_backend_cdk/blob/main/Makefile
RUST_DIR = functions
BUILD_ARTIFACT_DIR = target/lambda/functions

.PHONY: build
build:
    cd $(RUST_DIR) && \
    cargo lambda build --release --arm64 && \
    zip -j $(BUILD_ARTIFACT_DIR)/bootstrap.zip $(BUILD_ARTIFACT_DIR)/bootstrap && \
    cd ../ ;

.PHONY: run-dev
run-dev:
    cd $(RUST_DIR) && \
    cargo lambda watch

.PHONY: deploy
deploy:
    make build
    cdk deploy

8. デプロイする

make deloyを実行します。 Do you wish to deploy these changes (y/n)?にはyでOKです。 CloudFormationのStackが展開されるので、完了するまで待ちます。 待機中にAWSコンソール上でCloudFormationを眺めてみるのも良いでしょう。

9. 動作確認

デプロイが完了すると、API Gatewayエンドポイントがコンソールに表示されるのでcURLで確認します。 ※APIキーはAPI Gatewayに移動し、メニューのAPIキーから確認できます ※x-api-keyurlは適宜読み替えてください。

curl -X POST -H "Content-Type: application/json" \
-d '{"firstName": "tanabebe"}' \
--header 'x-api-key:your api key' \
https://your.lambda.endpoint.url/prod/example

削除したい場合はcdk destroyもしくは、CloudFormationのStack削除をしましょう。

10. 最後に

最初はSAMで同じ事を行いましたが、拡張していった場合にjsonyamlだとツラいよね、という事もあり、CDKで作り直しました。 今回の例はサンプル的な内容ではありますが、慣れればプロジェクト作成から動作確認まで30分もかからず行えます。 Rustを利用するためにzipで固める必要があったりerror: failed to run custom build commandのエラーが出たり、Lambdaでデプロイ出来たけど動かないとかそこそこ苦労しましたので、ハマりどころはまた別の機会に。 準備は面倒でしたが、一度作成してしまえば作成、破棄が簡単なので楽ですし、CDKならコードをクリーンにしていけば拡張するときもそこまで苦しくないですね。 プロジェクト初期段階で、ある程度コード化しておくことで、実装するプログラムにも集中出来ます。 とはいえ、この後にRustでひいひい言ってたわけですが。可能なら是非取り組んでみてはいかがでしょうか。