😽

AWS Lambda と SAM と Rustの私的メモ

2021/04/14に公開

しばらく前、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

設定情報(データベースの接続情報)を環境変数から取得するために作成
https://qiita.com/tiohsa/items/d694dfbfce52da09ea53
からまるまるコピペといっても過言ではないです。というかものすごくありがたい記事です。

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