BioErrorLog Tech Blog

試行錯誤の記録

RustのAWS LambdaをTerraformで実装する

Rustで書いたAWS LambdaをTerraformでデプロイする方法の備忘録です。

はじめに

こんにちは、@bioerrorlogです。

最近、Rustの採用が増えている印象があります。 MetaでRustがCLIとサーバーサイドでの正式サポート言語に追加されたニュースも記憶に新しいですね。

AWS LambdaではRustランタイムは現在ビルトインでは提供されていませんが、カスタムランタイムを使えばRustで書かれたLambdaを作成できます。

Rustで書いたLambdaをTerraformでデプロイしてみたので、今回はその備忘録を残します。

RustのAWS LambdaをTerraformで実装する

作ったもの

最終的なコードはこちら: github.com

EventBridgeがLambdaをトリガーし、ログをCloudWatch Logsに吐き出すごくシンプルな構成です。

アーキテクチャ: lambda-rust Architecture

ではポイントとなる部分をメモします。

Terraformの実装

Lambda実装の重要な部分を抜粋します。

####################################################
# Lambda Function
####################################################

resource "aws_lambda_function" "this" {
  function_name = "${var.name_prefix}_rust_lambda_sample"
  description   = "Rust lambda sample."

  filename         = data.archive_file.zip.output_path
  source_code_hash = data.archive_file.zip.output_base64sha256

  role    = aws_iam_role.this.arn
  handler = local.lambda_bin_name
  runtime = "provided.al2"
}

resource "null_resource" "rust_build" {
  triggers = {
    code_diff = join("", [
      for file in fileset(local.lambda_local_path, "**.rs")
      : filebase64("${local.lambda_local_path}/${file}")
    ])
  }

  provisioner "local-exec" {
    working_dir = local.lambda_local_path
    command     = "cargo lambda build --release"
  }
}

data "archive_file" "zip" {
  type        = "zip"
  source_file = local.lambda_bin_local_path
  output_path = local.lambda_zip_local_path

  depends_on = [
    null_resource.rust_build
  ]
}

terraform-examples/main.tf at main · bioerrorlog/terraform-examples · GitHub

Lambdaが構築される全体的な流れ:

  1. null_resourceでRustパッケージをビルド
  2. archive_fileでzip化
  3. カスタムランタイム指定したaws_lambda_functionにzipファイルを直接指定

まずはnull_resourceでコマンドcargo lambda build --releaseを実行し、Rustパッケージをビルドしています。 ここで使っているcargo-lambdaは、RustをAWS Lambdaで使うための機能を持ったツールです。 インストール方法などは後の章で説明します。

null_resourceにはtriggersを指定することで、Rustコードに変更があった場合に再ビルドを実行するようにしています。

  triggers = {
    code_diff = join("", [
      for file in fileset(local.lambda_local_path, "**.rs")
      : filebase64("${local.lambda_local_path}/${file}")
    ])
  }

.rsのbase64エンコードに差分が出た場合に、null_resourceが再実行されます。

Rustコードがビルドされたら、そのアーティファクトをarchive_fileを使ってzip化します。 depends_onを使ってRustをビルドするnull_resourceに依存を張ることで、ビルド → zip化の順番で実行されることを保証しています。

そしてaws_lambda_functionでこのzipファイルのパスを直接指定しています。 (S3に配置してそのS3 URIを渡すやり方もありますが、今回はこちらの方式をとりました)

ランタイムには、runtime = "provided.al2"としてカスタムランタイム(Amazon Linux 2)を指定しています。


Terraform側の実装はざっとこんな感じです。
(詳細はコードをご覧ください)

次は、Rustパッケージの実装をみていきます。

Lambda用Rustパッケージの実装

Rustコード部分の最終的なコードはこちら: github.com

Lambda用Rustパッケージの実装は、aws-lambda-rust-runtimeというRustランタイムがAWS公式から提供されているのでこれを利用します。

大きな開発の流れは、

  1. cargo-lambdaのインストール
  2. 新規Lambdaパッケージを生成
  3. Rustコードの実装

です。

1. cargo-lambdaのインストール

まずはcargo-lambdaをインストールします。 先述の通り、cargo-lambdaはRustをAWS Lambdaで使うための機能を持ったツールで、cargoのsubcommandとして呼び出せます。

pip3 install cargo-lambda

cargo installなどのほかの手段でもインストールできます。 詳しくは公式ドキュメントをご覧ください。

cargo install cargo-lambda

2. 新規Lambdaパッケージを生成

インストールしたcargo-lambdaを使って、Rustパッケージを新規作成します。

cargo lambda new YOUR_FUNCTION_NAME

対話的にいろいろ聞かれますが、特に要件が無ければNo->Enterで新規Rustパッケージを生成できます。 ここの質問への答えによって生成されるテンプレートが変化するみたいですね。

$ cargo lambda new lambda
? Is this function an HTTP function? No
? AWS Event type that this function receives  
  activemq::ActiveMqEvent
  autoscaling::AutoScalingEvent
  chime_bot::ChimeBotEvent
  cloudwatch_events::CloudWatchEvent
  cloudwatch_logs::CloudwatchLogsEvent
  cloudwatch_logs::CloudwatchLogsLogEvent
v codebuild::CodeBuildEvent
[↑↓ to move, tab to auto-complete, enter to submit. Leave it blank if you don't want to use any event from the aws_lambda_events crate]

ちなみに生成される新規パッケージのディレクトリ構成は、↓のようにシンプルな形です。

$ tree -L 3
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

3. Rustコードの実装

ここまで来たら、後はRustコードを実装していきます。
(ビルドおよびLambdaへのデプロイは、先述の通りTerraformで自動実行させます)

今回はあくまでRust & Lambdaをデプロイしてみるまでが目的なので、生成されているデフォルトのRustコードを眺めるのみに留めます。

あとは自由に実装していきましょう。

// main.rs
use lambda_runtime::{run, service_fn, Error, LambdaEvent};

use serde::{Deserialize, Serialize};

/// This is a made-up example. Requests come into the runtime as unicode
/// strings in json format, which can map to any structure that implements `serde::Deserialize`
/// The runtime pays no attention to the contents of the request payload.
#[derive(Deserialize)]
struct Request {
    command: String,
}

/// This is a made-up example of what a response structure may look like.
/// There is no restriction on what it can be. The runtime requires responses
/// to be serialized into json. The runtime pays no attention
/// to the contents of the response payload.
#[derive(Serialize)]
struct Response {
    req_id: String,
    msg: String,
}

/// 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
/// - https://github.com/aws-samples/serverless-rust-demo/
async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
    // Extract some useful info from the request
    let command = event.payload.command;

    // Prepare the response
    let resp = Response {
        req_id: event.context.request_id,
        msg: format!("Command {}.", command),
    };

    // Return `Response` (it will be serialized to JSON automatically by the runtime)
    Ok(resp)
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        // disable printing the name of the module in every log line.
        .with_target(false)
        // disabling time is handy because CloudWatch will add the ingestion time.
        .without_time()
        .init();

    run(service_fn(function_handler)).await
}

おわりに

以上、Rust & LambdaをTerraformで実装する方法の備忘録でした。

AWSは近年Rustサポートを強化していく流れもありそうなので、今後はより手厚い機能が提供されていくかもしれませんね。

この記事がどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - awslabs/aws-lambda-rust-runtime: A Rust runtime for AWS Lambda

Rust Runtime for AWS Lambda | AWS Open Source Blog

Installation | Cargo Lambda

https://crates.io/crates/cargo-lambda

Why AWS loves Rust, and how we’d like to help | AWS Open Source Blog

terraform-provider-aws/examples/lambda at main · hashicorp/terraform-provider-aws · GitHub

AWS Lambda and Rust: Building AWS Lambda functions with Terraform pt. 1 | by Jakub Jantošík | Medium