ECS 版との差分で読む Lambda 版 Slack Metrics の設計判断

Lambda 版 Slack Metrics は 1 つのコードを MODE で 3 関数に分け、init に接続と設定読込を寄せる

結論: Lambda はリクエストが来て初めて起動する。ECS 版は常駐して自分でリクエストを待つが、Lambda は起動元が handler を呼ぶ。だから ECS 版のコードは流用せず、コールドスタート時に 1 回だけ走る init に設定読込と DB 接続を寄せる。用途別に API・バッチ・worker の 3 関数 を立て、同じイメージを MODE で切り替える。
Lambda 版 Slack Metrics が 1 つの Docker イメージを MODE で API・バッチ・worker の 3 関数に分け、init で Parameter Store を読み、handler が RDS に書く概要図
図 1 — 同じイメージが MODE の値で 3 つの関数になる。起動時に 1 回読む設定と、毎回走る処理を分けて見る。
  1. init と main はコールドスタート時に 1 回、handler は毎リクエスト走る。
  2. タイムアウト・メモリ・ログの出し先を分けたいので、3 関数に分けてコードは 1 つにする。
  3. 設定は Parameter Store に集め、ENV と MODE だけ関数に直付けする。

Lambda を選べる条件と、一発で外れる条件

Lambda はリクエストが来たときにコンテナが立ち上がる。だから利用者がいない間の費用はほぼゼロで、増えれば最大 1000 台まで自動で並列化する。サーバの OS 管理やパッチ適用も要らない。

代わりに、緩和申請できない上限が多い。実行時間は 15 分、同期呼び出しの入出力は 6 MB、メモリは 10 GB で頭打ちになる。初回リクエストにはコンテナを立ち上げる待ち時間、コールドスタートの数百 ms から数秒が乗る。

そのため、ミリ秒応答が要る広告やゲーム、常時接続の WebSocket、常に高負荷な API には向かない。学習用途や社内管理画面、テレビ CM で瞬間的に跳ねるメディア系サイトには合う。Slack Metrics は利用者が自分だけなので、この条件に収まる。

Lambda に合う条件 一つでも当たれば外す条件 1 2 3 1 2 3 4 5 短い処理が中心 利用者が少ない アクセスの波が激しい ミリ秒の応答が要る 入出力が 6 MB を超える 常時接続 (WebSocket) 15 分を超える処理 常に高負荷 数秒以内で終わる PoC・MVP・社内管理画面 テレビ CM で瞬間的に跳ねる 広告配信・ゲーム 動画は S3 署名 URL で回避 一定時間で切れる SQS や ECS へ逃がす 常駐型の ECS が向く
図 2 — 左が Lambda に合う条件、右が一つでも当たれば外す条件。上限の数字は表 1 で引く。
上限の項目効き方
最大実行時間15 分 (900 秒)超えると途中で強制終了。長時間バッチは SQS や ECS へ逃がす
リクエスト・レスポンス本文6 MB同期呼び出しのみ。動画や画像は S3 の署名 URL で直接送る
メモリ10 GB増やすほど料金が上がる
/tmp の一時領域10 GB呼び出しをまたいで残り、コンテナが破棄されると消える。同じコンテナ内では共有される
表 1 — 緩和申請できない Lambda の上限。商用で引っかかりやすい 4 つ。

init は 1 回、handler は毎回、境目はコールドスタート

Lambda のコンテナは初期化、呼び出し、シャットダウンの 3 段階を辿る。初期化はコールドスタートとも呼ばれ、ランタイムの起動とコードの読込に続いて Go の init() と main() が走る。設定の読込と DB 接続はここで済ませ、呼び出しの段階では handler だけが走るようにする。

処理を終えたコンテナは数分のあいだアイドルで残る。その間に来たリクエストは init を飛ばして即応答し、これをウォームスタートと呼ぶ。アイドルが続くと AWS がコンテナを破棄し、次のリクエストは再びコールドスタートになる。init が重いと、この待ち時間は 10 秒程度に達する。

グローバル変数と /tmp は、同じコンテナを通る全リクエストで共有される。だからユーザー ID をグローバル変数に置くと利用者間で混線し、/tmp のディレクトリ名を固定すると顧客 A のファイルを顧客 B が上書きする。/tmp は UUID で分けて作る。

init と main (コールドスタート時に 1 回) handler (リクエストごと) init 実行 ユーザー A 到着 handler: A handler: B handler: C 同じコンテナで処理 (ウォームスタート) コンテナ破棄 init 実行 ユーザー D 到着 handler: D handler: E アイドル (数分) コールドスタート コールドスタート 10:00 12:00 init 2 回、handler 5 回
図 3 — 10 時に A・B・C が続けて来て、2 時間後に D・E が来る。init と handler をそれぞれ何回通るかを見る。

1 つのコードベースを MODE で 3 関数に分ける

Lambda 版は API、バッチ、SQS worker を別の関数にした。タイムアウトは API なら 30 秒で足りるが、バッチと worker は 15 分をフルに使いたい。メモリも用途で変えたい。ログの入れ物であるロググループも関数単位に作られるので、分けるだけで処理種別ごとにログを追える。

ただしコードは分けない。main() が環境変数 MODE を読み、その値で lambda.Start に渡す handler を選ぶ。batch なら EventBridge 用、sqs なら SQS 用、それ以外なら API Gateway 用になる。関数ごとに別のコードベースを持つと、業務の型定義や DB のスキーマを 3 重に管理することになる。

周辺リソースも ECS 版から流用しない。ECS 用の IAM ロールはタスク実行が前提なので、cp-slack-metrics-lambda-stg を新設する。足す権限は AWS の設定値保管サービス Parameter Store の読込と、VPC 内で通信するネットワークインタフェースの作成に絞る。セキュリティグループも同名で新設し、DB 側のセキュリティグループで受け入れる。

① 起動元 API Gateway (テスト実行) EventBridge Scheduler SQS キュー slack-metrics-stg ② Lambda 関数 (3 つ) slack-metrics-api-stg MODE=api slack-metrics-batch-stg MODE=batch slack-metrics-worker-stg MODE=sqs タイムアウト別 メモリ別 ロググループ別 ③ 同じコード 1 つの Docker イメージ (ECR slack-metrics-lambda-stg) MODE で handler を選択 ④ 共通の周辺リソース IAM ロール cp-slack-metrics-lambda-stg セキュリティグループ cp-slack-metrics-lambda-stg RDS
図 4 — 同じ ECR イメージから 3 関数を立てる。MODE の値で起動元に合わせた handler を選ぶ。

設定は Parameter Store、ENV と MODE だけ関数に直付け

Lambda の環境変数画面は値が丸見えで、関数が増えると重複管理になる。だから DB の接続情報は Parameter Store の slack-metrics-stg に、暗号化して保存する SecureString 型で置く。init がそれを読んで環境変数に流し込む。Secrets Manager は自動ローテーションが要る場面の選択肢で、学習用途には費用が高い。

ECS 版が使っていた S3 は候補から外れる。ECS ではコンテナ起動時の 1 回だけ読むが、Lambda ではコールドスタートのたびに読むので、応答遅延に直結する。

ENV と MODE だけは関数に直付けする。ENV は「どこから設定を読むか」を決める値なので、Parameter Store に置くと読む前に判定できない。MODE は関数ごとに違う値なので、共通の Parameter Store には置けない。Parameter Store の値を変えたら、3 関数とも更新しないと反映されない。

ENV: 読込先の判定 MODE: 関数ごとに別 Lambda 関数に直付けした環境変数 init(): ENV を読む main(): MODE を読む ENV=stg MODE=api ENV が local 以外なら Parameter Store の値を環境変数へ流し込む lambda.Start に渡す handler を選ぶ 起動時 1 回の読込 Parameter Store slack-metrics-stg POSTGRES_MAIN_HOST POSTGRES_MAIN_PASSWORD (暗号化) api batch sqs 値の変更後は 3 関数とも更新
図 5 — 関数に直付けした ENV が読込先を決める。init が Parameter Store の値を環境変数に流し込む。
置き場向く値採否
Lambda の環境変数関数ごとに違う値 (MODE)、起動判定に使う値 (ENV)この 2 つだけ
Parameter Store全関数で共通の値。SecureString で暗号化、無料採用 (slack-metrics-stg)
Secrets Manager自動ローテーションなど厳しい要件の秘密情報費用が高いので見送り
S3ECS 版の設定ファイル置き場コールドスタートごとに読むので外す
表 2 — 設定値の置き場 4 つと、Lambda 版での採否。

SQS worker は待ち受けず、キューに起こされる

EventBridge Scheduler は決めた時刻にバッチ関数を直接起動する。非同期処理は API 関数が SQS の slack-metrics-stg にメッセージを入れ、SQS が worker 関数を起動して RDS に書く。API は投入だけで返るので軽い。

ECS 版の worker は常駐プロセスがメッセージを待つ呼び出しを繰り返し、受け取ったものを処理して自分で削除していた。Lambda 版はキューと関数を紐づける設定を置き、メッセージが来たらキューが関数を起動する。1 件ずつとは限らず、たまった複数件が 1 回の起動にまとめて渡ることがある。

動かないときは、worker 関数に MODE=sqs があるか、IAM ロールに sqs:GetQueueAttributes があるかをまず見る。ワークスペース同期には 30 分に 1 回の呼び出し制限があり、そこで止まっていることもある。ECS 版の API タスクが動いていると、同居する worker がメッセージを横取りする。

ECS 版 worker (常駐) worker コンテナが常時起動 無限ループで ReceiveMessage 20 秒待ち受け メッセージを処理 自分で DeleteMessage 次のメッセージ待ち Lambda 版 worker (起動される) API 関数が SQS に投入 SQS キュー slack-metrics-stg キューが worker 関数を起動 複数件をまとめて渡すことがある メッセージを順に処理し RDS へ エラーなら return err で再試行に回す メッセージが来たら起動
図 6 — 上が常駐する ECS 版 worker、下が起動される Lambda 版 worker。待ち受けの主体が変わる。
設計の要点: ECS 版のコードを流用しない。init に 1 回だけ、handler は軽く、グローバル変数と /tmp はリクエスト間で共有される前提で書く。
デプロイの仕組み (Terraform は image_uri を無視し、関数更新は make で行う) とリソース名一覧5 段落 + 表 6 行

Docker イメージのタグはコミットごとに変わる。そのため aws_lambda_function の lifecycle で image_uri を ignore_changes にする。イメージ更新が Terraform の差分に出なくなる。

関数の更新は cloud-pratica-backend の make update-functions で行う。3 関数の image-uri を一括で更新する。GitHub Actions では build と migrate の後に deploy-lambdas を走らせ、サービスごとに並列で更新する。

Parameter Store の値を変えたときも、この関数更新を通さないと反映されない。GitHub Actions を組んであるなら再実行が確実で、検証中なら画面から更新してもよい。

SQS と worker 関数の紐づけは aws_lambda_event_source_mapping で定義する。関数のソース設定とみなして Lambda 側の module に置く。

このコースは Lambda と RDS を直結する最小構成の段階にあたる。API Gateway での公開、Cognito 認証、RDS Proxy、WAF は後続コースで足す。

リソース名前
ECRslack-metrics-lambda-stg
IAM ロールcp-slack-metrics-lambda-stg
セキュリティグループcp-slack-metrics-lambda-stg
Parameter Storeslack-metrics-stg
SQS キューslack-metrics-stg
Lambda 関数slack-metrics-api-stg / slack-metrics-batch-stg / slack-metrics-worker-stg