この記事は every Tech Blog Advent Calendar 2024(夏) 11 日目の記事です。
エブリーで小売業界向き合いの開発を行っている @kosukeohmura といいます。
エブリーでは retail HUB という小売業界向けのサービスを展開しており、その開発を行う中でイベントログを収集する API を作る機会がありました。この記事ではその中でも表題の点にフォーカスして詳細をお伝えできればと思います。
イベントログを収集する API の概観
クライアントからのイベントログを API Gateway で作成した API で受け、Amazon Data Firehose ストリーム経由で S3 に保存します。
API では一度のリクエストで複数のイベントを受け取り、その後 Amazon Data Firehose の PutRecordBatch API を使用し、受け取ったイベントをまとめて Firehose ストリーム へと送信します。Amazon Data Firehose では受け取ったデータをバッファしながら、動的にパーティショニングを行いつつ、S3 に保存します。
ここで API Gateway から Amazon Data Firehose ストリームへデータを送信する際に、データ構造を少し変換する必要があります。次節で詳しく説明します。
API Gateway から Amazon Data Firehose ストリームへデータを送信する
Amazon Data Firehose 配信ストリームへ送信する際 PutRecordBatch API を使用しますが、その API へのリクエストでは次のシンタックスを要求されます。ここで blob
はレコード 1 つを Base64 エンコードしたものです。
{ "DeliveryStreamName": "string", "Records": [ { "Data": blob } ] }
具体的に、API で受け取る JSON が下記のような値であったとすると、
[ {"field1": "value11", "field2": "value12"}, {"field1": "value21", "field2": "value22"} ]
このような PutRecordBatch API へのリクエストに適合する JSON に変換する必要があります。
{ "DeliveryStreamName": "string", "Records": [ {"Data": "eyJmaWVsZDEiOiAidmFsdWUxMSIsICJmaWVsZDIiOiAidmFsdWUxMiJ9Cg=="}, {"Data": "eyJmaWVsZDEiOiAidmFsdWUyMSIsICJmaWVsZDIiOiAidmFsdWUyMiJ9Cg=="} ] }
Lambda を挟んで変換処理しようかと考えましたが、その場合、管理・運用する対象が
- Lambda 関数のソースコード
- 使用メモリ量等の設定
- 実行ログを流す CloudWatch Logs ストリーム
- 関数実行時の IAM ロール
- その他、実行失敗時の通知機構など
と増えます。加えて、Lambda ランタイムのサポート切れや実行時の料金も考慮を要します。
なので Lambda の利用をできれば避けたいと思っていたところ、API Gateway の マッピングテンプレート を利用することでリクエストボディの変換ができることを知り、今回はその方法を取りました。
マッピングテンプレートを使用してリクエストを書き換える
マッピングテンプレートは Velocity Template Language (VTL) で記述します。実際に API Gateway の統合リクエストで使用しているテンプレートほぼそのままを下記に記します。
## 1. メタデータ付加を行うパート #set($records = []) #foreach($inputRecord in $input.path('$')) #set($record = '') #foreach($key in $inputRecord.keySet()) ## 1-a. 値の型に応じて、クォートでくくったりします #set($value = $inputRecord.get($key)) #if($value == $null) #set($value = 'null') #elseif($value.getClass().getName().equals('java.lang.String')) #set($value = '"' + $value + '"') #end #set($record = $record + '"' + $key + '"' + ':' + $value + ',') #end ## 1-b. 必要なメタデータをログに付加します #set($record = $record + '"server_time":' + $context.requestTimeEpoch / 1000 + ",") #set($record = $record + '"source_ip":' + '"' + $context.identity.sourceIp + '"' + ",") #set($record = $record + '"user_agent":' + '"' + $context.identity.userAgent + '"' + ",") #set($record = $record + '"request_id":' + '"' + $context.requestId + '"') #set($record = '{' + $record + '}') ## 1-c. エラー回避のため空代入しています (もっとスマートな方法をご存じの方、教えて下さい!) #set($dummy = $records.add($record)) #end ## 2. PutRecordBatch API へのシンタックスへと変換し出力するパート { "DeliveryStreamName": <your_firehose_stream_name>, "Records": [ #foreach($record in $records) {"Data": "$util.base64Encode($record)"}#if($foreach.hasNext),#end #end ] }
なおこのマッピングテンプレートでは、API のリクエストボディの JSON の形式として次を想定しています:
- 最上位は配列
- その配下にフラットなオブジェクトが並ぶ
また、テンプレート内の $
から始まる変数リストはこのリファレンスに載っています。
以下、テンプレートの内容を 2 つに分けて簡単に解説します。
1. メタデータ付加を行うパート
サーバー側で取得するのが望ましいリクエスト日時や IP アドレス、User-Agent など、イベントログと合わせて保存したいメタデータを付加しています。そのため一旦 JSON をパースしますが、そこから元の JSON 文字列に戻すために value をクォートでくくりなおしたり、value が null の場合に欠落してしまう ("key":,
となる) のを避けるなど、ひと手間かかっています。
2. PutRecordBatch API へのシンタックスへと変換し出力するパート
PutRecordBatch API の仕様通り、各イベントログを Base64 エンコードしつつリクエストボディを組み立てます。
Lambda を挟んでのデータ変換との良し悪し
今回 API Gateway 内で簡単なデータ変換を行いつつ、Amazon Data Firehose ストリームへとデータを送信することができました。
マッピングテンプレートでは VTL を使う必要があり、これに不慣れな方も多いと思います。ただ VTL はテンプレート言語であり、動的な処理を行うには限界があるため、VTL 自体の習得はさほど難しくはないと感じています。
今回マッピングテンプレートを完成させる中で問題だと感じたのは、API Gateway のテスト実行時に VTL の実行結果のエラーの詳細を見る方法がない(と思われる)点です。エラー原因の見当がつかない場合、マネジメントコンソール上の VTL を少しずつ修正しながら、トライアンドエラーを繰り返すような泥臭い作業が必要でした。
このことから、実装したい変換がある程度複雑であれば、管理・運用する対象を増やすことを受け入れ、Lambda 関数を呼び出し加工したデータを Firehose 配信ストリームに送信する形を検討したほうが良いかもしれません。一方 VTL で書いても十分にシンプルであれば、マッピングテンプレートを使用する方針を取りたいと思います。
さいごに
ここまで読んでいただきありがとうございました。every Tech Blog Advent Calendar 2024(夏) はまだまだ続きます!