🦀

Rust Lambda を AWS CDK で簡単管理!Cargo Lambda CDK を試してみた

2025/02/16に公開

1. はじめに

この記事は何 ?

Cargo Lambda CDK というライブラリの紹介です.

対象読者

  • Rust を書きたい人
  • AWS Lambda 関数を使って開発したい人

この記事で解説しないこと

  • Rust について
  • AWS CDK について

成果物

Cargo Lambda CDK を使ったコードは以下に置いています.

https://github.com/virtual-hippo/hello-cargo-lambda-cdk?tab=readme-ov-file#hello-cargo-lambda-cdk

2. 前置き

コードの解説だけ読みたい人はこの章は読まなくても大丈夫です.

Cargo Lambda CDK とは?

Cargo Lambda で作成した Lambda 関数を CDK で定義するためのコンストラクトライブラリです.

https://github.com/cargo-lambda/cargo-lambda-cdk?tab=readme-ov-file#cargo-lambda-cdk-construct

Cargo Lambda とは?

Cargo Lambda は、AWS Lambda と連携するための Cargo (Rustパッケージマネージャ) のサブコマンドです.

https://www.cargo-lambda.info/guide/what-is-cargo-lambda.html

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
}

この関数は, クエリパラメータ xy を受け取り, 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 パイプラインが作成されます:

  1. GitHub リポジトリからコードを取得
  2. Rust コードの静的解析とテストを実行
  3. CDK コードのフォーマット, リント, テストを実行
  4. CDK スタックを合成
  5. Lambda 関数を含むスタックをデプロイ

このパイプラインにより, コードの変更が Github にプッシュされるたびに, 自動的にテストとデプロイが行われます.

6. axum の利用

今回は, Cargo Lambda のベースをフル活用しましたが, lambda-rust-runtime では axum を活用することも可能らしいです.
lambda-rust-runtime も axum も tower::Service trait を利用しているため, 容易に axum を利用できるみたいです.

aws-lambda-rust-runtime の example にはたくさんの利用例があるのでとても参考になります.

https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples

7. 最後に

Lambda 関数を Rust で実装する世界線最高です.

Discussion