RustでLambdaを書いてsam local start-apiでLocalStackのDynamoDBを使う
はじめに
Rustの門を叩いてからインプットしているだけでアプリケーションを作ってなかったので、とりあえずなんかやってみようということでAPI Gateway + Lambdaの構成でAPIを作ることに。
サーバレスは運用コストが低かったりと結構好きなんですが、動作確認がデプロイしてからじゃないとできないし面倒だなーなんて思っていました。
そんなときにsam local start-api
でローカル環境でAPI Gateway + Lambdaを実行できるということを知りさっそくやってみようと思います。
技術スタック
- Rust
- Amazon API Gateway
- Amazon DynamoDB
- AWS Lambda
- LocalStack
構成図
かなりアバウトな図ですが、イメージはこんな感じです。
サンプル
1から手順を紹介しますが、急いでいる方のために完成したサンプルプロジェクトを公開します。
必要に応じてこちらをご利用ください。
Rustのプロジェクトを作る
まずはsamでローカル環境を動かせるようにRustのプロジェクトを作成します。
-
プロジェクト作成
cargo new project # or cargo init .
-
template.ymlを作成
サンプルは
/
のエンドポイントでGETとPOSTのAPIを作成します。template.ymlAWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Rust Lambda Function with LocalStack Globals: Function: Runtime: provided.al2023 Architectures: - x86_64 Tracing: Active CodeUri: ./ Handler: bootstrap Timeout: 30 MemorySize: 128 Environment: Variables: RUST_BACKTRACE: 1 Resources: GetFunction: Type: AWS::Serverless::Function Metadata: BuildMethod: rust-cargolambda BuildProperties: Binary: get_function Properties: Events: GetRoot: Type: Api Properties: Path: / Method: GET PostFunction: Type: AWS::Serverless::Function Metadata: BuildMethod: rust-cargolambda BuildProperties: Binary: post_function Properties: Events: PostRoot: Type: Api Properties: Path: / Method: POST
-
cargo.toml修正
最下部に追加する
cargo.toml[[bin]] name = "get_function" path = "src/get_function.rs" [[bin]] name = "post_function" path = "src/post_function.rs"
-
必要なクレートを追加する
cargo add aws_config cargo add serde cargo add tokio --features macros cargo add tracing --features log cargo add tracing-subscriber --no-default-features --features fmt cargo add aws_lambda_events cargo add lambda_runtime
-
Lambdaハンドラーを作成する
src/
配下にcargo.toml
に記載したパス通りのファイルを作る./ ├── Cargo.lock ├── Cargo.toml ├── src │ ├── get_function.rs │ └── post_function.rs └── template.yml
-
実装する
とりあえずリクエストを受け取ってレスポンスを返すだけの処理を書く。
post_function.rs
も同じ実装で良い。get_function.rsuse aws_lambda_events::{apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse}, http::HeaderMap}; use lambda_runtime::{Error, LambdaEvent, run, service_fn}; pub async fn handler( _event: LambdaEvent<ApiGatewayProxyRequest>, ) -> Result<ApiGatewayProxyResponse, Error> { let mut headers = HeaderMap::new(); headers.insert("content-type", "text/html".parse().unwrap()); let resp = ApiGatewayProxyResponse { status_code: 200, multi_value_headers: headers.clone(), is_base64_encoded: false, body: Some("Hello AWS Lambda HTTP request".into()), headers, }; Ok(resp) } #[tokio::main] async fn main() -> Result<(), Error> { run(service_fn(handler)).await }
-
ローカルでAPIを動かす
-
コマンドを実行してAPIサーバを起動する
sam build sam local start-api
-
リクエストを送信する
curl --location 'http://127.0.0.1:3000/'
-
レスポンスが返される
Hello AWS Lambda HTTP request
-
まずはこれでローカル環境でAPI Gateway + LambdaのAPIサーバを動かすことができました。
LocalStackの環境を作る
次はDockerでLocalStack
のDynamoDB
を立ち上げます。
-
docker-compose.yml
を作成するLocalStackはいろんなAWSのサービスをローカルで起動させることができますが、今回は
DynamoDB
のみで良いのでSERVICES=dynamodb
としておきます。docker-compose.ymlservices: localstack: image: localstack/localstack:latest ports: - "4566:4566" environment: - SERVICES=dynamodb - AWS_ACCESS_KEY_ID=dummy - AWS_SECRET_ACCESS_KEY=dummy - AWS_DEFAULT_REGION=ap-northeast-1
-
LocalStack
を起動する-
コンテナを起動する
docker compose up -d
-
LocalStackの起動状況を確認する
curl -s localhost:4566/_localstack/health | jq .
返却内容
{ "services": { "acm": "disabled", "apigateway": "disabled", "cloudformation": "disabled", "cloudwatch": "disabled", "config": "disabled", "dynamodb": "available", "dynamodbstreams": "available", "ec2": "disabled", "es": "disabled", "events": "disabled", "firehose": "disabled", "iam": "disabled", "kinesis": "available", "kms": "disabled", "lambda": "disabled", "logs": "disabled", "opensearch": "disabled", "redshift": "disabled", "resource-groups": "disabled", "resourcegroupstaggingapi": "disabled", "route53": "disabled", "route53resolver": "disabled", "s3": "disabled", "s3control": "disabled", "scheduler": "disabled", "secretsmanager": "disabled", "ses": "disabled", "sns": "disabled", "sqs": "disabled", "ssm": "disabled", "stepfunctions": "disabled", "sts": "disabled", "support": "disabled", "swf": "disabled", "transcribe": "disabled" }, "edition": "community", "version": "4.8.2.dev13" }
-
aws cliを実行してみる
aws dynamodb list-tables
-
実行結果を確認する
{ "TableNames": [] }
このとき
You must specify a region. You can also configure your region by running "aws configure".
と怒られた場合、aws cliのセットアップが完了していないので、
~/.aws/config
と~/.aws/credentials
を作成する。もしくは、
aws configure
コマンドでセットアップを行う。~/.aws/config[default] region = ap-northeast-1 output = json
~/.aws/credentials[default] aws_access_key_id = dummy aws_secret_access_key = dummy
-
DynamoDBにテーブルを作る
LocalStackは無料プランではデータが永続化されないため、ローカル環境ではコンテナを起動するたびに初期化データを投入する必要があります。
毎回手動でテーブルを作成するのは手間なので、初期化用のスクリプトを作成します。
スクリプトはコンテナ側の/etc/localstack/init/ready.d
に配置することで、Dockerコンテナ起動時に自動で実行されます。
-
スクリプトを作る
scripts/init.sh#!/bin/bash key_name="user_id" table_name="users" aws --endpoint-url=http://localstack:4566 dynamodb create-table \ --table-name=$table_name \ --attribute-definitions AttributeName=$key_name,AttributeType=S \ --key-schema AttributeName=$key_name,KeyType=HASH \ --billing-mode PAY_PER_REQUEST
-
docker-compose.yml
を修正するdocker-compose.ymlservices: localstack: image: localstack/localstack:latest ports: - "4566:4566" environment: - SERVICES=dynamodb - AWS_ACCESS_KEY_ID=dummy - AWS_SECRET_ACCESS_KEY=dummy - AWS_DEFAULT_REGION=ap-northeast-1 + volumes: + - "./scripts:/etc/localstack/init/ready.d"
-
コンテナを再起動する(作り直す)
docker compose up -d --force-recreate
DynamoDBが作られているか確認する
aws dynamodb list-tables
作られていることが確認できる
{ "TableNames": [ "users" ] }
Lambdaを改修する
先程作成したGETの実装にDynamoDBからデータを取得する実装をおこないます。
まず、DynamoDB SDKと JSONシリアライズ用のクレートを追加します。
cargo add aws_sdk_dynamodb
cargo add serde_json
use aws_config::BehaviorVersion;
use aws_lambda_events::{apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse}, encodings::Body, http::HeaderMap};
use aws_sdk_dynamodb::Client;
use lambda_runtime::{Error, LambdaEvent, run, service_fn};
use serde_json::json;
pub async fn handler(
_event: LambdaEvent<ApiGatewayProxyRequest>,
) -> Result<ApiGatewayProxyResponse, Error> {
let config = aws_config::defaults(BehaviorVersion::latest())
.endpoint_url("http://localstack:4566")
.load()
.await;
let client = Client::new(&config);
let list_resp = client.list_tables().send().await;
let mut headers = HeaderMap::new();
headers.insert("content-type", "application/json".parse().unwrap());
let resp = match list_resp {
Ok(resp) => {
tracing::info!("Found {} tables", resp.table_names().len());
let tables: Vec<String> = resp.table_names().iter().map(|s| s.to_string()).collect();
let body = json!({
"tables": tables
}).to_string();
ApiGatewayProxyResponse {
status_code: 200,
multi_value_headers: headers.clone(),
is_base64_encoded: false,
body: Some(Body::Text(body)),
headers,
}
}
Err(err) => {
tracing::error!("Failed to list tables: {err:?}");
let error_body = json!({
"error": "Failed to list tables",
"message": err.to_string()
}).to_string();
ApiGatewayProxyResponse {
status_code: 500,
multi_value_headers: headers.clone(),
is_base64_encoded: false,
body: Some(Body::Text(error_body)),
headers,
}
}
};
Ok(resp)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt().json()
.with_max_level(tracing::Level::INFO)
.with_current_span(false)
.with_ansi(false)
.without_time()
.with_target(false)
.init();
run(service_fn(handler)).await
}
ポイントは、DynamoDBクライアントのエンドポイントをhttp://localstack:4566
にすることです。
http://127.0.0.1:4566
やhttp://localhost:4566
の場合LocalStackに接続できません。
修正が完了したら再度sam build
を実行してください。
コンテナ間の通信ができるようにする
実装が完了しましたが、このままではLambdaの処理がLocalStackのコンテナにアクセスできずエラーになってしまいます。
理由は、sam local で実行するLambdaはdockerコンテナで実行されるためです。
コンテナ間の通信が必要になるためlocalhostでは通信が不可能となります。
対応方法
sam local start-api
コマンドに--docker-network
オプションをつけて実行することでコンテナ間の通信を可能にします。
-
LocalStackコンテナが起動しているネットワークを確認する
docker network ls
デフォルトは
ディレクトリ名 + _default
で命名されています。
本プロジェクトは「rust-samlocalapi-localstack_default」という名前で作成しています。NETWORK ID NAME DRIVER SCOPE b86fc161fc86 rust-samlocalapi-localstack_default bridge local
-
表示された
NETWORK ID
をオプションに付けるsam local start-api --docker-network b86fc161fc86
-
APIを実行する
curl --location 'http://127.0.0.1:3000/'
返却されるjson
{"tables":["users"]}
次回
ほとんど完成なんですが、一旦ここまでにして、次の記事でDynamoDBのデータを取得&登録する実装を書こうと思います。
GETでデータ取得、POSTでデータ登録するようなAPIを作るのでお時間があれば見てみてください。
Discussion