AWS Lambda と SAM と Rustの私的メモ
しばらく前、RustでLambda関数を作ることに成功して思ったのですが、意外とRustとLambdaの相性良いのでは?と思い、AWS SAMを使いAPI Gatewayやらも含めて設定していきたいななんて思ったりもしました。
やりたいことはこんな感じです。
- RustでLambda関数を作る
- API Gateway -> Lambda 関数のコンビ
- Lambda関数からは Lightsail上に作ったDBサーバへアクセス
- Lightsailはピアリング接続する
なんでLightsailにデータベースサーバ立てるかというと、お金の問題です(´・ω・`)
貧乏人にはRDSを使う予算がございません。さらにいうとEC2にDBサーバ立てるのも高くて・・・・。
今回は個人用のメモ残しが半分ぐらいなのでわかりづらい内容になっているかと思います。ご了承くださいませ・・・・
Rust側の話
DB接続先は環境変数からとるようにします。またプログラムとしては意味のない感じのものになります。
Cargo.toml
不要なクレートが入っているかもしれません。DBアクセスにはsqlxを使います。
[package]
name = "test-lambda"
version = "0.1.0"
authors = ["XXXXXXXX <xxxxx@example.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aws_lambda_events = "0.4.0"
config = "0.11.0"
dotenv = "0.15.0"
http = "0.2.3"
lambda_http = "0.3.0"
lambda_runtime = "0.3.0"
lazy_static = "1.4.0"
log = "0.4.14"
serde = "1.0.124"
serde_derive = "1.0.124"
serde_json = "1.0.64"
simple_logger = "1.11.0"
sqlx = { version="0.5.1", features = ["runtime-tokio-rustls", "any", "postgres", "sqlite", "macros", "migrate", "chrono"] }
tokio = { version = "1.4.0", features = ["full"] }
[[bin]]
name = "bootstrap"
path = "src/main.rs"
config.rs
設定情報(データベースの接続情報)を環境変数から取得するために作成
からまるまるコピペといっても過言ではないです。というかものすごくありがたい記事です。use serde::Deserialize;
use config::ConfigError;
use dotenv::dotenv;
use lazy_static::lazy_static;
#[derive(Deserialize, Debug)]
pub struct Config {
pub database_url: String,
}
impl Config {
/// 環境変数からデータを読み込む
pub fn from_env() -> Result<Self, ConfigError> {
let mut cfg = config::Config::new();
cfg.merge(config::Environment::new())?;
cfg.try_into()
}
}
lazy_static! {
pub static ref CONFIG: Config = {
dotenv().ok();
Config::from_env().unwrap()
};
}
main.rs
前回と違うのは「aws_lambda_events」を使っていることです。
APIGatewayからのデータのリクエスト、レスポンスに関してのデータ型が定義されています。便利!
実はlambda_httpというのもあったのですが、こちらうまく動かずこの形になりました。
プログラムの内容はApiGatewayのアクセスしたパスとDB上のテーブル一覧を返すという内容です。
mod config;
use http::HeaderMap;
use lambda_runtime::{handler_fn, Context, Error};
use log::LevelFilter;
use serde_derive::Serialize;
use simple_logger::SimpleLogger;
use sqlx::{Any, AnyPool, Pool};
use crate::config::CONFIG;
use aws_lambda_events::event::apigw::{ApiGatewayProxyResponse, ApiGatewayProxyRequest };
#[derive(Serialize, Clone)]
struct CustomOutput {
path: String,
result: Vec<String>
}
#[tokio::main]
async fn main() -> Result<(), Error> {
SimpleLogger::new()
.with_level(LevelFilter::Info)
.init()
.unwrap();
let db_pool = AnyPool::connect(&CONFIG.database_url).await.unwrap();
let func = handler_fn(move |e, c| {
log::info!("lambda start---!!!");
my_handler(e, c, db_pool.clone())
});
lambda_runtime::run(func).await?;
Ok(())
}
async fn my_handler(req: ApiGatewayProxyRequest, _: Context, db_pool: Pool<Any>) -> Result<ApiGatewayProxyResponse, Error> {
let tables = db_access(&db_pool).await;
let x = CustomOutput {path: req.path.unwrap_or("".to_string()), result: tables};
let x = serde_json::to_string(&x).unwrap();
Ok(
ApiGatewayProxyResponse {
status_code: 200,
body: Some(x.into()),
is_base64_encoded: None,
headers: HeaderMap::new(),
multi_value_headers: HeaderMap::new()
}
)
}
async fn db_access(db_pool: &Pool<Any>) -> Vec<String> {
let sql = if CONFIG.database_url.starts_with("sqlite:") {
"select tbl_name from sqlite_master;"
} else {
"select table_name from information_schema.tables;"
};
sqlx::query_as::<_, (String,)>(sql)
.fetch_all(db_pool)
.await
.unwrap().into_iter().map(|i| i.0 ).collect()
}
ビルド用のスクリプト
下記のようなものを作っておきました。
cross build --release --target x86_64-unknown-linux-musl
zip -j bootstrap.zip target/x86_64-unknown-linux-musl/release/bootstrap
AWS SAMがらみの話
今回ビルドはビルドスクリプトを使うので、sam build コマンドは使用しません。sam buildでビルドできる方法もあったのですが処理が妙に時間がかかったり、私の環境では妙に安定しなかったので使用を諦めました。
template.yml
- Lambda関数作ってそこにAPI Gatewayを割り当てる
- Lambda関数にはVPCを割り当てる
- Lambda関数用のロールを作成する
- CloudWathに出力されるロググループの期限を30日にする
- VPCの設定とデータベースの接続情報は環境変数から取得する
といったことをしています。
なお、RustをLambdaとしてデプロイするうえで重要なのは
CodeUri: ./bootstrap.zip
Handler: bootstrap.is.real.handler
Runtime: provided.al2
の部分です。CodeUriにビルドで作ったzipファイルへのパスを書きRuntimeにprovided.al2を指定します。Handlerは多分なんでもよかったような気がします。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-app
Sample SAM Template for sam-app
Parameters:
DataBaseURL:
Type: String
SecurityGroupIds:
Type: CommaDelimitedList
SubnetIds:
Type: CommaDelimitedList
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3
Environment:
Variables:
DATABASE_URL: !Ref DataBaseURL
VpcConfig:
SecurityGroupIds: !Ref SecurityGroupIds
SubnetIds: !Ref SubnetIds
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
FunctionName: HelloRust
CodeUri: ./bootstrap.zip
Handler: bootstrap.is.real.handler
Runtime: provided.al2
Role: !GetAtt LambdaRole.Arn
Events:
ProxyApi:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /test/{proxy+}
Method: ANY
HelloWorldFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${HelloWorldFunction}
RetentionInDays: 30
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
samconfig.toml
最初 sam deploy --guided コマンドで適当にデプロイした内容にちょいちょい修正しています。
parameter_overridesで実際の設定を記載します。(今回はVPCの設定とデータベースの接続情報)
version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "sam-app"
s3_bucket = "[みせられないよ!!]"
s3_prefix = "sam-app"
region = "ap-northeast-1"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "DataBaseURL=postgresql://xxxxx:xxxxx@xxx.xxx.xxx.xxx/postgres SecurityGroupIds=sg-xxxxxxxx SubnetIds=subnet-xxxxxa,subnet-xxxxxb,subnet-xxxxxc"
デプロイについて
普通にRustのビルドをして、sam deployでデプロイします。それだけでスパーンと作られるので初めての時は感動しました。
その他
Serverless Frameworkというのにも興味があって使ってみようとしたのですが、Serverless Rustを使って行うとしたのですがどうにもビルドで失敗して先進めませんでした。。。
Discussion