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に吐き出すごくシンプルな構成です。
ではポイントとなる部分をメモします。
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が構築される全体的な流れ:
null_resource
でRustパッケージをビルドarchive_file
でzip化- カスタムランタイム指定した
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公式から提供されているのでこれを利用します。
大きな開発の流れは、
- cargo-lambdaのインストール
- 新規Lambdaパッケージを生成
- 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サポートを強化していく流れもありそうなので、今後はより手厚い機能が提供されていくかもしれませんね。
この記事がどなたかの参考になれば幸いです。
[関連記事]
参考
GitHub - awslabs/aws-lambda-rust-runtime: A Rust runtime for AWS Lambda
Rust Runtime for AWS Lambda | AWS Open Source Blog
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