せや、AWS LambdaのハンドラをSQLにしたろ


はじめに

イマドキのAWS LambdaはハンドラをSQLで記述できます。

AWS LambdaのコンソールでこんなSQLを記述したとします。

実行するとこうなります。

SQLの結果が出力されていますね。

説明

冒頭のクエリを確認してみます。

index.sql
SELECT 'Hello World' body, 200 statusCode;

このクエリをAWS Lambdaのハンドラーに設定して実行すると、AWS Lambdaは次のJSON文字列をアウトプットします。

output
{
  "body": "Hello World",
  "statusCode": 200
}

SELECTしたキーとバリューを構造に持つJSON文字列がアウトプットされました。
JSON形式で返せるということは、イベントソースにAPI Gatewayを設定すれば簡単なWEB APIを作れるといっても過言ではありません。

これにより『SQLは書けるがプログラミング言語は利用できない』といった方でも簡単なLambda関数を作成できるようになり、求められるハードルがまた一つ低くなったのではないでしょうか。

なお、この記事が投稿されたのは4月1日であり、実際にはAWS LambdaにハンドラーをSQLで記述するといった機能は存在しません..........................

といったエイプリルフールネタを投稿したかった

昨年、2021年のエイプリルフールネタで『時代に即したMySQレの新機能:PLEASE句』というブログを拝見しました。

https://sakaik.hateblo.jp/entry/20210401/mysql_please_clause_in_aprilfool
当時の私は上記ブログにとても関心を示し、似たようなネタを個人ブログにでも投稿したかったのですが、諸事情あって個人ブログは未開放にしていて実現できませんでした。

そこで情報共有コミュニティZennへの投稿を検討しましたが、利用規約にフェイクは許されないと記載されていました。(当たり前

第4条(禁止事項)

利用者は、本サービスの利用にあたり、以下の行為をしてはなりません。
10. 他の利用者および第三者を欺く虚偽の内容を記載する行為

では、この記事をどう投稿するか。
答えは簡単です。フェイクでなければ良いのです。嘘から出た誠にしてしまいましょう。
ということで、全てが誠となるように実際にSQLでハンドラを記述できる仕組みを作りました。

実際に作る

検討事項

実現させるにあたり次の項目を検討しました。

  • AWS Lambdaで任意の環境を使う
  • 外部RDBMSサーバーを必要としないSQLの実装
  • AWS LambdaのEventをSQLから取得する仕組み

AWS Lambdaで任意の環境を使う

今回の目的はAWS LambdaのハンドラをSQLで記述することですが、
そもそもAWS Lambdaとは、コードを用意するだけでサーバーを意識せずにバックエンドを作成できるAWSのサービスです。基本的な使い方をするならAWSが提供するランタイム(言語やバージョン)を選択し、それに沿ってコードを記述する必要があります。
では、AWS LambdaではAWSが用意した言語やバージョンしか利用できないのでしょうか🤔

そんなことはありません。
AWS Lambdaには任意の環境を利用する仕組みが2種類あります。

  • カスタムランタイムを利用するパターン
  • Dockerイメージを利用するパターン

後者のDockerイメージを利用するパターンは、AWSで提供されていない言語を使いたい時に優先的に検討するべき一般的な選択肢です。

しかし、「Dockerイメージを使ってAWS LambdaでSQLを動かしました!!」と記事を投稿したとして閲覧者は何が面白いのでしょうか。「それってLambdaからSQL実行しているだけだよね?」とツッコミを入れられて終わるでしょう。
もちろん、実用的な記事としては十分価値がありそうですが、今回は実用性ではなくユニーク性を求めていたのでレギュレーションを設けることにしました。

まあ、そうですよね。
Dockerイメージで動くAWS Lambdaはマネジメントコンソールからコードが見えないのでハンドラをSQLで書いているようには見えません。
あたかもSQLだけで動いているかのように見せかけるため、今回はカスタムランタイムを使用することにしました。

カスタムランタイムを使用する

さて、カスタムランタイムを使用することにしましたが、カスタムランタイムを使用するにはまずランタイムAPIを自身で実装する必要があります。
ランタイムAPIとはAWS Lambdaがハンドラを呼び出す仕組みそのもので、以下のタスクで構成されています。

  • 初期化タスク
    • 設定の取得
    • 関数の初期化
    • 初期化エラーハンドリング
  • 処理タスク
    • イベントの取得
    • トレースヘッダーの伝播
    • コンテキストオブジェクトの作成
    • 関数ハンドラの呼び出し
    • レスポンスの処理
    • 呼び出しエラーハンドリング
    • クリーンアップ

カスタムランタイムを使用するにはこれらを任意の方法で実装した実行可能ファイルを用意する必要があります。
具体的にはこのような実装になります。

sequenceDiagram
    autonumber
    
    rect rgb(191, 223, 255)
        Note over AWS Lambda,自分が実装する部分: 初期化タスク
        AWS Lambda->>+自分が実装する部分: Init
        自分が実装する部分->>自分が実装する部分: 初期化処理
        自分が実装する部分->>自分が実装する部分: 関数の初期化
        opt エラー発生
            自分が実装する部分->>AWS Lambda: 初期化エラーハンドリング
        end
    end
    rect rgb(224, 247, 250)
        Note over AWS Lambda,自分が実装する部分: 処理タスク
        loop
            自分が実装する部分->>+AWS Lambda: イベントの取得
            AWS Lambda-->>-自分が実装する部分: event
            自分が実装する部分->>自分が実装する部分: トレースヘッダーの伝播
            自分が実装する部分->>自分が実装する部分: コンテキストオブジェクトの作成
            自分が実装する部分->>自分が実装する部分: 関数ハンドラの呼び出し
            opt エラー発生
                自分が実装する部分->>AWS Lambda: 呼び出しエラーハンドリング
            end
            自分が実装する部分->>+AWS Lambda: レスポンスの処理
        end
        自分が実装する部分->>自分が実装する部分: クリーンアップ
        自分が実装する部分-->>-AWS Lambda: Shutdown
    end

これらを実装していくと分かりますが少々手間がかかります。
この手間を解消するため、ランタイムAPIを実装しやすくするフレームワークaws-lambda-custom-runtime-kitを作成しました。aws-lambda-custom-runtime-kitを使うとGo言語で簡単にカスタムランタイムが実装できるようになります。

使い方は簡単で、次のインターフェースを満たす構造体を作成してaws-lambda-custom-runtime-kitの提供するエントリーポイントへ渡すだけです。

type AWSLambdaRuntime interface {
	Setup(env *AWSLambdaRuntimeEnvironemnt) error
	Invoke(event []byte, context *Context) (interface{}, error)
	Cleanup(env *AWSLambdaRuntimeEnvironemnt)
}

aws-lambda-custom-runtime-kitはこのようなアーキテクチャになっています。
Custom runtime kitより右側が実装者の担当する領域になります。

sequenceDiagram
    autonumber
    participant AWS Lambda
    participant Custom runtime kit
    actor Your runtime
    AWS Lambda->>+Custom runtime kit: Init
    Custom runtime kit->>+Your runtime: Setup()
    Your runtime-->>-Custom runtime kit: error
    opt return error
        Custom runtime kit->>AWS Lambda: Initilize error
    end
    loop
        Custom runtime kit->>+AWS Lambda: Get an event
        AWS Lambda-->>-Custom runtime kit: event
        Custom runtime kit->>+Your runtime: Invoke()
        Your runtime->>+Any: any logic
        Any-->>-Your runtime: result
        Your runtime-->>-Custom runtime kit: result, error
        opt return error
            Custom runtime kit->>AWS Lambda: Invoke error
        end
        Custom runtime kit->>+AWS Lambda: send result
    end
    Custom runtime kit->>Your runtime: Cleanup()
    Custom runtime kit-->>-AWS Lambda: Shutdown

ランタイムAPIを実装するのに必要な処理がグっと減ったのが分かりますね。

なお、aws-lambda-custom-runtime-kitはGitHubにて公開していますので、ご覧になっている方もこちらを使ってクレイジーなカスタムランタイムを是非実装してみてください。

https://github.com/WinterYukky/aws-lambda-custom-runtime-kit

外部RDBMSサーバーを必要としないSQLの実装

次にSQLを解釈する方法です。
はっきり言ってジョーク記事のためにオレオレパーサーは作りたくなかったのでSQLiteを採用し、素直にmattnさんのgo-sqlite3を利用することにしました。

https://github.com/mattn/go-sqlite3

先ほど紹介したaws-lambda-custom-runtime-kit上で構築すると次のようなアーキテクチャになります。

sequenceDiagram
    autonumber
    participant AWS Lambda
    participant Custom runtime kit
    actor SQL runtime
    AWS Lambda->>+Custom runtime kit: Init
    Custom runtime kit->>+SQL runtime: Setup()
    SQL runtime->>SQL runtime: SQLファイルを読み取る
    SQL runtime-->>-Custom runtime kit: error
    opt return error
        Custom runtime kit->>AWS Lambda: Initilize error
    end
    loop
        Custom runtime kit->>+AWS Lambda: Get an event
        AWS Lambda-->>-Custom runtime kit: event
        Custom runtime kit->>+SQL runtime: Invoke()
        SQL runtime->>+sqlite3: query
        sqlite3-->>-SQL runtime: result
        SQL runtime-->>-Custom runtime kit: result, error
        opt return error
            Custom runtime kit->>AWS Lambda: Invoke error
        end
        Custom runtime kit->>+AWS Lambda: send result
    end
    Custom runtime kit->>SQL runtime: Cleanup()
    Custom runtime kit-->>-AWS Lambda: Shutdown

AWS LambdaのEventをSQLから取得する仕組み

最後にEventを取得する仕組みです。
これがなければ実行時における動的なインプットがなく、常に同じ結果を返すことになってしまい実用性がありません。
実用性は求めないと記述した気がしますが気のせいです。

では、どうするか。
他にも案はありましたが、今回はSQLiteのUDFで実装することにしました。

UDFはUser-Defined Functionの略称で、SQLiteに限らず様々なRDBMSで実装されている『ユーザーがプログラミング言語で定義した任意の関数をSQLで使用する』機能です。go-sqlite3では次のように記述できます。

// 関数を定義
eventUDF := func(path string) string {
  if path == "" {
    return string(event)
  }
  value := gjson.Get(string(event), path)
  return value.String()
}

// 関数を登録
sql.Register(driverName, &sqlite3.SQLiteDriver{
  ConnectHook: func(conn *sqlite3.SQLiteConn) error {
    if err := conn.RegisterFunc("event", eventUDF, false); err != nil {
      return err
    }
    return nil
  },
})

今回はgjsonを利用してevent関数に渡されたJSONキーの値を取得するようにしてみました。

https://github.com/tidwall/gjson

では、SQLからEventを利用する方法をみてみましょう。
例として、遠足に持っていけるおやつをチェックするAPIをSQL Lambdaで実装するとします。

Lambdaのハンドラーに登録するSQL
SELECT
  CASE
    WHEN
      event('おやつ') = 'バナナ' THEN 400
    ELSE 200
  END statusCode,
  CASE
    WHEN
      event('おやつ') = 'バナナ' THEN 'バナナを持ってきた人は遠足に連れていきません'
    ELSE '300円までならOKです'
  END body;

上記ハンドラに次のEventを渡して実行すると・・・

遠足に持っていくおやつ.json
{ 
  "おやつ": "バナナ"
}

このようにEvent情報を利用した分岐処理が確認できます。

レスポンス
{
  "statusCode": 400,
  "body": "バナナを持ってきた人は遠足に連れていきません"
}

完成

最後に、ここまで紹介したコードをビルドしてLambdaレイヤーとして登録することで・・・
完成です🎉

ソースはこちらに公開しています。最新のリリースからビルドされたバイナリを取得できるのでSQLでAWS Lambdaを動かしてみたい方はそちらを利用してみてください。セキュリティは一切考慮されていないので絶対に遊び以外では使用しないでください

https://github.com/WinterYukky/aws-lambda-sql-runtime

さいごに

今回ランタイムAPIを実装したことによってAWS Lambdaについて勉強になりました。

例えばNode.jsランタイムを使用する際に初期化処理はハンドラーの外で実行することが推奨されていましたが、これは初期化タスクでハンドラソースをrequire()でインポートし、その後ハンドラを呼び出す関数がイベントのキューを常にポーリングし続けているためなんだろうと具体的に想像できました。

もう一度紹介しますが、是非皆さんもクレイジーなカスタムランタイムを実装してみてください。CloudFormationの記述で動いたりしたら面白そうです・・・。

https://github.com/WinterYukky/aws-lambda-custom-runtime-kit

参考

https://sakaik.hateblo.jp/entry/20210401/mysql_please_clause_in_aprilfool
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-custom.html