Rust Lambda を AWS CDK で簡単管理!Cargo Lambda CDK を試してみた
1. はじめに
この記事は何 ?
Cargo Lambda CDK というライブラリの紹介です.
対象読者
- Rust を書きたい人
- AWS Lambda 関数を使って開発したい人
この記事で解説しないこと
- Rust について
- AWS CDK について
成果物
Cargo Lambda CDK を使ったコードは以下に置いています.
2. 前置き
コードの解説だけ読みたい人はこの章は読まなくても大丈夫です.
Cargo Lambda CDK とは?
Cargo Lambda で作成した Lambda 関数を CDK で定義するためのコンストラクトライブラリです.
Cargo Lambda とは?
Cargo Lambda は、AWS Lambda と連携するための Cargo (Rustパッケージマネージャ) のサブコマンドです.
Cargo Lambda を使うことで Rust で作成した Lambda 関数を手軽にデバッグ, ビルド, デプロイすることができます.
Cargo Lambda CDK を使うモチベーション
AWS CDK を使うと TypeScript で書いた Lambda 関数と他のクラウドリソースもシームレスに管理することができます.
具体的には, cdk synth
などによる Cloud Assembly の作成と同時に Lambda 関数のコードについてもトランスパイル & バンドルしてくれます.
デプロイ時には S3 bucket への Zip ファイルのアップロードまでやってくれます.
以下にサンプルコードを記載しておきます.
Stack 定義
// cdk/lib/stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
export interface CdkStackProps extends cdk.StackProps {}
export class CdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: CdkStackProps) {
super(scope, id, props);
new cdk.aws_lambda_nodejs.NodejsFunction(this, "NodejsFunction", {
runtime: cdk.aws_lambda.Runtime.NODEJS_LATEST,
entry: "lambda/nodejs/index.ts",
handler: "handler",
});
}
}
Lambda 関数定義
// cdk/lambda/handler.ts
import { APIGatewayEvent, ProxyResult } from "aws-lambda";
export const handler = async (event: APIGatewayEvent): Promise<ProxyResult> => {
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Hello World",
}),
};
};
AWS CDK with TypeScript でここまでの体験ができる & Cargo Lambda という便利ツールがある中で
- Lambda 関数は Cargo Lambda, その他のリソースは CDK で管理する
- Cargo Lambda を使わずに CDK 単体で上手いこと頑張る
といった方法を選択することには躊躇いがあります.
できれば Rust で作成した Lambda 関数についてもAWS CDK でシームレスに管理したいですし Cargo Lambda も使いたいです.
それを実現するのが Cargo Lambda CDK です.
3. 環境準備
前提環境
WSL2 の Ubuntu で実行しました.
ツールの準備
事前に以下のツールを利用できるようにしておく必要があります.
ツール | 説明 | インストール方法 |
---|---|---|
Docker Engine | CI 用のイメージ作成に利用 | https://docs.docker.com/engine/install/ |
Rust | 言語本体 | https://www.rust-lang.org/ja/tools/install |
cargo-make | タスクランナー | https://github.com/sagiegurari/cargo-make?tab=readme-ov-file#installation |
Cargo Lambda | Lambda 関数のビルド・デプロイを楽にするツール | https://www.cargo-lambda.info/guide/getting-started.html |
asdf | Node.js, AWS CLI, awscurl などのバージョン管理 | https://asdf-vm.com/guide/getting-started.html |
Node.js | AWS CDK の実行に必要 | asdf install nodejs 22.13.1 |
pnpm | Node.js のパッケージ管理 | asdf install pnpm 9.15.4 |
awscli | AWS にデプロイするため | asdf install awscli 2.12.6 |
awscurl | Lambda の Function URL にリクエストを送るため | https://github.com/okigan/awscurl?tab=readme-ov-file#installation |
4. 設計方針
設計方針について説明します.
Lambda 関数内にコアロジックを書きすぎることは推奨されていません.
そのため, Lambda 関数を定義する部分とアプリケーション独自ロジック部分は別 Layer にします.
今回は簡単な実装に留める予定のため, アプリケーション独自ロジック部分は API レイヤのみ作成します.
より実用的なアプリケーションを作る場合には, Domein Layer や Infrastructure Layer を追加するのが良いと思います.
以下の図のようなイメージです.
このような構成にすることで AWS Lambda 以外のインフラへの移行も楽に実現できるかと思われます.
ここまでを踏まえて以下のようなディレクトリ構成にします.
.
├── aws-lambda-functions
│ └── src
│ └── bin # Lambda 関数のエントリポイントを定義
├── cdk # AWS CDK によるインフラリソースの定義
└── modules # アプリケーションの内部ロジック
└── api # アプリケーションインターフェースの定義
5. 実際に使ってみた
プロジェクトの作成
まずは, プロジェクトのベースを作成します.
cargo new hello-cargo-lambda-cdk
cd hello-cargo-lambda-cdk
Lambda 関数定義用のワークスペースを作成します.
cargo lambda new aws-lambda-functions
以下の質問には y
と答えてください.
/Cargo.toml
の workspace にも忘れずに追加します
[workspace]
resolver = "2"
members = [
"aws-lambda-functions",
]
cdk 用の資材を作ります.
mkdir cdk
cd cdk
pnpm dlx cdk init --language typescript
# cargo-lambda-cdk のインストール
pnpm add cargo-lambda-cdk
また, rust-toolchain.toml, rustfmt.toml, Makefile.toml なども用意しておきます (この辺は主題ではないので細かい解説はしません).
初めての RustFunction のデプロイ
cargo lambda new aws-lambda-functions
を実行すると aws-lambda-functions
というディレクトリが作成されます.
このディレクトリは以下の aws-lambda-functions/src/http_handler.rs
に Lambda 関数の定義がされています.
// aws-lambda-functions/src/http_handler.rs
use lambda_http::{Body, Error, Request, RequestExt, Response};
/// This is the main body for the function.
/// Write your code inside it.
/// There are some code example in the following URLs:
/// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples
pub(crate) async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
// Extract some useful information from the request
let who = event
.query_string_parameters_ref()
.and_then(|params| params.first("name"))
.unwrap_or("world");
let message = format!("Hello {who}, this is an AWS Lambda HTTP request");
// Return something that implements IntoResponse.
// It will be serialized to the right response event automatically by the runtime
let resp = Response::builder()
.status(200)
.header("content-type", "text/html")
.body(message.into())
.map_err(Box::new)?;
Ok(resp)
}
まずはこの関数をデプロイします.
Stack を定義します.
// cdk/lib/stacks/hello-cargo-lambda-cdk-stack
import * as cdk from "aws-cdk-lib";
import { RustFunction } from "cargo-lambda-cdk";
import { Construct } from "constructs";
import * as path from "path";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface HelloCargoLambdaCdkStackProps extends cdk.StackProps {}
export class HelloCargoLambdaCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: HelloCargoLambdaCdkStackProps) {
super(scope, id, props);
const fn = new RustFunction(this, "HelloFunction", {
// デプロイ対象の Lambda 関数をビルドするための Cargo.toml のパスを指定する
manifestPath: path.join(__dirname, "../../../", "aws-lambda-functions", "Cargo.toml"),
});
// 動作確認を行うため関数 URL を追加する
const fnUrl = fn.addFunctionUrl();
new cdk.CfnOutput(this, "HelloFunctionUrl", { value: fnUrl.url });
}
}
pnpm cdk deploy
でこの Stack をデプロイします.
すると, ターミナルに HelloFunctionUrl
の値が出力されます.
XXXXXXXXXXX.HelloFunctionUrl = https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.lambda-url.ap-northeast-1.on.aws/
出力された URL にawscurl でリクエストして Hello world, this is an AWS Lambda HTTP request
と返ってくれば呼び出し成功です.
awscurl --region ap-northeast-1 --service lambda "https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.lambda-url.ap-northeast-1.on.aws/"
Hello world, this is an AWS Lambda HTTP request
API Layer の追加
前述の設計方針に従って, API Layer を追加していきます.
まず, API モジュールを作成します.
mkdir -p modules/api
cd modules/api
cargo init --lib
次に, ルートの Cargo.toml
にワークスペースメンバーとして追加します.
[workspace]
resolver = "2"
members = [
"aws-lambda-functions", "modules/api",
]
[workspace.dependencies]
api = { path = "./modules/api" }
API モジュールに簡単な関数を実装します.今回は2つの数値を足し合わせる関数を実装します.
// modules/api/src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
次に, この API を使用する Lambda 関数を作成します.
cd aws-lambda-functions
cargo lambda add add
Lambda 関数の実装は以下のようになります.
// aws-lambda-functions/src/bin/add.rs
use api::add;
use lambda_http::{run, service_fn, tracing, Body, Error, Request, RequestExt, Response};
pub(crate) async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
// Extract some useful information from the request
let params = event.query_string_parameters_ref().and_then(|params| {
let x = params.first("x")?.parse::<u64>().ok();
let y = params.first("y")?.parse::<u64>().ok();
x.zip(y)
});
let message = params.map_or("Recieved invalid parameters".to_string(), |(x, y)| {
format!("{}", add(x, y))
});
let resp = Response::builder()
.status(200)
.header("content-type", "text/html")
.body(message.into())
.map_err(Box::new)?;
Ok(resp)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing::init_default_subscriber();
run(service_fn(function_handler)).await
}
この関数は, クエリパラメータ x
と y
を受け取り, API モジュールの add
関数を使用して足し算を行います.
最後に, CDK スタックに新しい Lambda 関数を追加します.
// cdk/lib/stacks/hello-cargo-lambda-cdk-stack.ts
const fn = new RustFunction(this, "AddFunction", {
manifestPath: path.join(__dirname, "../../../", "aws-lambda-functions", "Cargo.toml"),
binaryName: "add",
});
const fnUrl = fn.addFunctionUrl();
new cdk.CfnOutput(this, "AddFunctionUrl", { value: fnUrl.url });
binaryName
パラメータを使用して, どのバイナリをデプロイするかを指定しています.これにより, 1つの Cargo.toml から複数の Lambda 関数をデプロイすることができます.
デプロイ後, 出力された AddFunctionUrl
の値を使用して, 以下のようにリクエストを送信できます.
awscurl --region ap-northeast-1 --service lambda "https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.lambda-url.ap-northeast-1.on.aws/?x=5&y=3"
このように, API Layer を分離することで, 責務を分離したモジュール構成にできます.
Lambda Function 以外への移行も容易になります.
CI/CD パイプラインの追加
AWS CDK Pipelines を使用して CI/CD パイプラインを実装します.
まず, パイプラインのスタックを作成します.
// cdk/lib/stacks/cdk-pipeline-stack.ts
import * as cdk from "aws-cdk-lib";
import { aws_codebuild as codebuild, aws_iam as iam, pipelines } from "aws-cdk-lib";
import { StringParameter } from "aws-cdk-lib/aws-ssm";
import { Construct } from "constructs";
import * as path from "path";
import { AppParameterType, Env } from "../parameters";
import { HelloCargoLambdaCdkStage } from "../stages";
export interface HelloCargoLambdaCdkPipelineProps extends cdk.StackProps {
readonly envAlias: Env;
readonly sourceRepository: string;
readonly sourceBranch: string;
readonly appParameter: AppParameterType;
}
export class HelloCargoLambdaCdkPipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: HelloCargoLambdaCdkPipelineProps) {
super(scope, id, props);
const deployRole = new iam.Role(this, "CodeBuildDeployRole", {
assumedBy: new iam.ServicePrincipal("codebuild.amazonaws.com"),
managedPolicies: [
{
managedPolicyArn: "arn:aws:iam::aws:policy/AdministratorAccess",
},
],
});
const connectionArn = StringParameter.valueFromLookup(this, "/github/hello-cargo-lambda-cdk/connectionArn");
const buildImage = codebuild.LinuxBuildImage.fromAsset(this, "LinuxBuildImage", {
directory: path.join(__dirname, "../../", "assets/docker-images/codebuild"),
});
const pipeline = new pipelines.CodePipeline(this, "Pipeline", {
codeBuildDefaults: { buildEnvironment: { buildImage } },
synth: new pipelines.CodeBuildStep("SynthStep", {
input: pipelines.CodePipelineSource.connection(props.sourceRepository, props.sourceBranch, {
connectionArn: connectionArn,
}),
commands: [
// アカウント ID 明示的に定義していないので動的に指定する
"export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)",
"cd cdk",
"pnpm i --frozen-lockfile",
"pnpm fmt",
"pnpm lint",
"pnpm test",
"pnpm cdk synth",
],
role: deployRole,
primaryOutputDirectory: "./cdk/cdk.out",
}),
});
const testStep = new pipelines.ShellStep("Testing", {
commands: ["makers clippy-ci", "makers test-ci"],
});
pipeline.addStage(new HelloCargoLambdaCdkStage(this, props.envAlias, { appParameter: props.appParameter }), {
pre: [testStep],
});
}
}
次に, デプロイするステージを作成します.
// cdk/lib/stages/hello-cargo-lambda-cdk-stage.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { AppParameterType } from "../parameters";
import { HelloCargoLambdaCdkStack } from "../stacks";
export interface HelloCargoLambdaCdkStageProps extends cdk.StageProps {
readonly appParameter: AppParameterType;
}
export class HelloCargoLambdaCdkStage extends cdk.Stage {
readonly helloCargoLambdaCdkStack: HelloCargoLambdaCdkStack;
constructor(scope: Construct, id: string, props: HelloCargoLambdaCdkStageProps) {
super(scope, id, props);
const helloCargoLambdaCdkStack = new HelloCargoLambdaCdkStack(this, "HelloCargoLambdaCdk", {
env: props.appParameter.env,
});
this.helloCargoLambdaCdkStack = helloCargoLambdaCdkStack;
}
}
パイプラインのビルド環境として, カスタムの Docker イメージを使用します.このイメージには, Rust, Cargo Lambda, その他の必要なツールがインストールされています.
# cdk/assets/docker-images/codebuild/Dockerfile
FROM public.ecr.aws/amazonlinux/amazonlinux:2023
RUN dnf update -y && \
dnf install -y \
gcc \
openssl-devel \
git \
make \
zip \
unzip \
tar \
gzip \
curl \
jq \
python3 \
python3-pip \
&& dnf clean all
# Install Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
# Install cargo-lambda
RUN pip3 install cargo-lambda
# Install Node.js
RUN curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
RUN dnf install -y nodejs
# Install pnpm
RUN npm install -g pnpm
# Install cargo-make
RUN cargo install cargo-make
# Install AWS CLI
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
unzip awscliv2.zip && \
./aws/install && \
rm -rf aws awscliv2.zip
# Set working directory
WORKDIR /app
パイプラインを使用するには, GitHub リポジトリとの接続が必要です.
AWS CodeStar Connections を使用して GitHub リポジトリとの接続を設定し, 接続 ARN を SSM パラメータストアに保存します.
aws ssm put-parameter \
--name "/github/hello-cargo-lambda-cdk/connectionArn" \
--value "arn:aws:codestar-connections:ap-northeast-1:XXXXXXXXXXXX:connection/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" \
--type String
最後に, パイプラインをデプロイします.
cd cdk
pnpm cdk deploy HelloCargoLambdaCdkPipeline-dev
これにより, 以下のような CI/CD パイプラインが作成されます:
- GitHub リポジトリからコードを取得
- Rust コードの静的解析とテストを実行
- CDK コードのフォーマット, リント, テストを実行
- CDK スタックを合成
- Lambda 関数を含むスタックをデプロイ
このパイプラインにより, コードの変更が Github にプッシュされるたびに, 自動的にテストとデプロイが行われます.
6. axum の利用
今回は, Cargo Lambda のベースをフル活用しましたが, lambda-rust-runtime では axum を活用することも可能らしいです.
lambda-rust-runtime も axum も tower::Service trait を利用しているため, 容易に axum を利用できるみたいです.
aws-lambda-rust-runtime の example にはたくさんの利用例があるのでとても参考になります.
7. 最後に
Lambda 関数を Rust で実装する世界線最高です.
Discussion