
はじめに
この記事はBASEアドベントカレンダーの2日目の記事です。
こんにちは、 BASE Feature Dev1 Group で PHPer をしている @meihei です。今日は Gopher です。
この記事では、外部サービスの Webhook を AWS Lambda (Function URLs) で受け取り、SQS にいれる設計と実装、そして、それら全体が正常に稼働しているかを監視するやり方について書きます。
1. 前提とアーキテクチャ
前提として BASE が連携する外部サービスでイベントが発生した際、その通知を Webhook 経由で受け取り、必要なユースケースを後続ワーカーが実行できるようにつなぐ仕組みを設計します。

技術要件
外部サービスの中には EventBridge の Partner Event Source を利用して直接イベントを受信できるものもあります。しかし、今回の対象サービスは EventBridge 連携に対応していないため Webhook を利用しています。Webhook の正当性については、署名ベースの検証によって確認しています。
また、Webhook を受信するエンドポイントは BASE の PHP アプリケーションサーバーでは処理せず、サーバーレス環境(Lambda)で受信する方針としています。後続のワーカーは、既存のコンテナ基盤上で稼働する PHP プロセスによって実行されます。
ビジネス要件
今回は、BASE から外部サービスへ API 経由で商品連携を行った後に、商品審査ステータスの変更が発生し、その審査ステータスを受け取って BASE 側の連携ステータスを更新するユースケースについて解説します。
ここで求められるビジネス要件は以下のとおりです。
- 許容遅延:リアルタイム性は必須ではありませんが、できる限り早く反映されることが望ましい。
- 処理漏れへの対応:処理漏れは設計上許容せず、万が一発生した場合は必ず SQS Dead Letter Queue (以下、DLQ)に退避させ、後続の運用フローで確実に回収できるように。
- 二重通知への対応:外部サービスから同一イベントが複数回送られたとしても、後続ワーカー側で冪等性を担保し、重複処理を許容。
- 通知順序のズレ:順序入れ替わりは発生するものとして扱い、最終的に正しい状態へ同期されていれば問題なし。
これらの要件から最終的に以下のような構成になりました。

SQS は Standard Queue を使用し、外部サービスのシークレットキーなどは AWS Systems Manager のパラメータストアを利用しています。
2. インフラ(Terraform+lambroll)
本構成で必要となるAWSリソースは以下の通りです。*1
- IAM: Lambda 実行用の Role と Policy
- Lambda: Webhook を受信する Lambda Function(Function URLs)
- SQS: イベントを後続ワーカーへ受け渡すための Main Queue と Dead Letter Queue
- Systems Manager & KMS: 外部サービスのシークレットキーを保管するためのParameter Store と KMS Key
BASE ではインフラ構築に Terraform を、Lambda のデプロイには lambroll を利用しています。これらは別々のリポジトリで管理されていて、アプリケーションエンジニアでも安全かつ容易に AWS リソースを管理でき、Lambda のコードは継続的デリバリーが可能な体制になっています。

Terraform 編
Terraform は一般的な AWS 構築を行いますが、Lambda だけは異なります。
aws_lambda_function のリソースは Lambda を作成する時と、削除する時だけに使用し、最初にダミーファイルをデプロイします。その後は source_code_hash や runtime, environment などの情報に差分を検出しても変更を適用しないように設定しています。これらは lambroll 側で行います。
また、 BASE では Terraform をモジュール化して管理しており、インフラエンジニアや SRE でなくても、必要なパラメータを入力すれば標準的な AWS リソースを安全に作成できるようになっています。
例えばこんな感じです。
// lambda.tf module "lambda_hogehoge_integration_webhooks" { source = "../modules/lambda" function_name = "hogehoge-integration-webhooks" ... } // sqs.tf module "hogehoge_integration_webhooks" { source = "../modules/sqs_dead_letter" name_sqs = "hogehoge-integration-webhooks" dead_letter_queue_arn = module.hogehoge_integration_webhooks_dead.sqs_arn ... } module "hogehoge_integration_webhooks_dead" { source = "../modules/sqs" name_sqs = "hogehoge-integration-webhooks-dead" ... }
module からアタッチするポリシーも設定出来るので、送信先 SQS への sqs:SendMessage、シークレットを格納している SSM Parameter Store への ssm:GetParameter、そして Parameter Store 経由で Secrets Manager のシークレットを参照するための kms:Decrypt だけをアタッチして、最小権限になるようにします。
また、Lambda Function URL の設定は lambroll 側ではなく Terraform 側で定義しました。
lambroll 編
lambroll は、AWS Lambda に特化したシンプルなデプロイツールです。Terraform でインフラ(関数そのものや IAM ロール、Function URL など)を作成しつつ、アプリケーションコードのビルドとデプロイは lambroll に任せることで、役割を分離しています。
まず、Terraform で作成した既存の Lambda 関数を、Lambda 専用リポジトリ側から管理できるように初期化します。
lambroll init --function-name hogehoge-integration-webhooks --download
このコマンドにより、対象の関数設定を取得し、lambroll 用の設定ファイル(function.json
)が手元に生成されます。
{ "Architectures": [ "arm64" ], "Environment": { "Variables": { "APP_KEY": "{{ must_env `APP_KEY` }}", "APP_SECRET_NAME": "{{ must_env `APP_SECRET_NAME` }}" } }, "Handler": "bootstrap", "Runtime": "provided.al2023", ... }
function.json では Terraform で ignore の設定をしている環境変数やランタイムを指定します。
コードを書き換えた後の Go のビルドとデプロイは次のように実行します。
go build -v -o ../build/bootstrap lambroll deploy --src="build"
この一連のフローは GitHub Actions 上から自動でビルドとデプロイが走るようになっています。
3. 実装(Go)
Lambda のコードは Go 言語で実装しています。後続ワーカーはドメインロジックを担うため BASE のアプリケーションと同じ PHP を採用していますが、Lambda 側ではビジネスロジックを持たないため採用言語に強い制約はありません。
今回は外部サービスの公式ドキュメントが Go のサンプルを提供していたこと、社内で Go の利用実績があることから、Lambda は Go で実装する方針としました。
次のようなディレクトリ構成で進めています。
hogehoge-integration-webhooks/
build/
bootstrap
src/
go.mod
go.sum
main.go
Makefile
function.json
.env
まず、init 関数では Lambda 起動時に一度だけ実行される初期化処理として、SQS クライアントの生成とシークレットの取得を行います。ここで作成したクライアントやシークレットはグローバル変数として保持し、各リクエスト処理で再生成しないようにしています。
func init() { ... // SQSクライアントを作成 sqsClient = sqs.NewFromConfig(cfg) // SSM Parameter Store から APP_SECRET を取得 ssmClient := ssm.NewFromConfig(cfg) withDecryptionc := true param, err := ssmClient.GetParameter(context.TODO(), &ssm.GetParameterInput{ Name: &appSecretName, WithDecryption: &withDecryptionc, }) appSecret = *param.Parameter.Value ... }
main 関数では lambda.Start(handler) を呼び出し、実際のリクエスト処理は handler 関数に集約しています。handler では署名の検証と SQS へのメッセージ送信だけを担当させ、ビジネスロジックは持たないようにしています。署名検証に失敗した場合は 404 を返すことで、認証まわりの情報を外部に漏らさないようにしています。
func handler(ctx context.Context, request events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) { // 事前検証 ... // 署名を検証 if !verifyWebhookSignature(request.Body, authHeader, appKey, appSecret) { return events.LambdaFunctionURLResponse{ StatusCode: 404, }, nil } // リクエストBodyをそのままSQSに送信 _, err := sqsClient.SendMessage(ctx, &sqs.SendMessageInput{ QueueUrl: &sqsQueueURL, MessageBody: &request.Body, }) if err != nil { log.Printf("Failed to send message to SQS: %v", err) return events.LambdaFunctionURLResponse{ StatusCode: 500, }, err } // 成功の場合空で返す return events.LambdaFunctionURLResponse{ StatusCode: 200, }, nil }
今回の Lambda は「受け取った Webhook を検証し、そのまま SQS に流す」ことだけに専念しており、後続のワーカーがビジネスロジックを実行する前提のため、リクエスト Body をそのまま SQS に送信するシンプルな実装としています。
もしビジネス要件の異なる複数種類の Webhook を単一のエンドポイントで受け取る必要がある場合は、この handler 内でイベント種別(リクエスト Body 内の値)などに応じて送信先のキューを振り分ける構成にすると、FIFO Queue が使用可能後になったり、後続のワーカーを用途ごとに分離しやすくなります。
4. 監視・運用
前提として BASE では New Relic でも AWS サービスのインフラの観測を行っています。
New Relic では以下の様なダッシュボードを用意し、ひと目でサービスの状態が把握出来るようになっており、万が一異常が起きてもアラートを設定していて slack へ通知されるようにしています。
ダッシュボード編
この連携の状態を把握するための「入り口」として New Relic のダッシュボードを用意しています。ダッシュボードを開けば、構成・ステータス・関連リソースへの導線がひと目で分かるようにすることを意識しています。

まず、ダッシュボードの一番上には全体構成が分かる図を配置します。New Relic では Mermaid 記法でダッシュボード内に図を記述できるため、Lambda Function URL → Lambda → SQS → ワーカー というイベントの流れを示した簡単な構成図を載せています。
参考: Mermaid記法でダッシュボードにアーキテクチャ図など視覚的な表現が可能に! #AWS - Qiita

次に、この連携に関わる周辺情報へのリンクをまとめた Markdown テキストを置きます。具体的には、対象 Lambda 関数や SQS キューの AWS コンソールへのリンク、関連する GitHub リポジトリ、ドキュメントなどを並べておき、運用時にここからすぐに辿れるようにしています。
その下には、主要なメトリクスをサービスの流れに沿って並べます。入口となる Lambda については Invocations、Duration、Errors、Throttles を New Relic 経由で可視化し、エラーやスロットルの有無をすぐ確認できるようにしています。SQS については、SentMessages/ReceivedMessages/DeletedMessages といったトラフィックの傾向に加えて、ApproximateAgeOfOldestMessage、ApproximateNumberOfMessagesNotVisible、ApproximateNumberOfMessagesVisible を配置し、キュー滞留や詰まり具合を一目で判断できるようにしています。
(LatencyのSLOはうまく計測出来ておらず、0となってしまっている…)
これらを「入口 → キュー → 下流ワーカー」の順番で並べることで、どのレイヤーで異常が起きているかを直感的に追えるダッシュボード構成としています。
アラート設定と運用
New Relic アラートでは、各レイヤーで重要なメトリクスを監視対象として、異常を最速で検知し、slack へ送るようにしています。

Lambda では Errors と Throttles を監視し、失敗が発生した場合は Slack へ即時通知されるようにしています。Lambda の失敗時は、外部サービス側の Webhook リトライが発生しているかの確認を行います。
SQS 側では ApproximateAgeOfOldestMessage と ApproximateNumberOfMessagesVisible をアラート条件に設定し、キューの滞留や処理遅延が発生した場合に通知します。これにより、下流ワーカーの負荷増大を早期に把握できます。
さらに、DLQ にメッセージが入った場合は、件数が1件以上であることをトリガーとして Slack に通知しています。
5. まとめ
今回の構成は、Webhook を受け取り、後続ワーカーへ処理を引き渡すことを目的としたシンプルなアーキテクチャでした。入口となる Lambda Function URL は軽量で扱いやすく、ビジネスロジックを後続ワーカー側へ集約したことで責務が明確になり、機能追加や修正にも柔軟に対応できる構造になっています。
今回の事例が、今後の設計や運用を進める際の参考になれば幸いです!
BASE ではアプリケーションエンジニアが設計からインフラ・監視まで幅広く活躍することが出来ます。興味があれば採用情報もぜひご覧ください!
明日は、BASEアドベントカレンダーは @zan_sakurai さんの記事です。お楽しみに!
*1:Cloudwatch Logsもありますが、ここでは省略して書いています。