GitHub webhook の非同期化(自己 Event invoke で 10 秒配信タイムアウトを回避)
起票日: 2026-07-13
関連: PR #625 / content_html 事前生成 / blog-api の VPC + Fargate Proxy 構成
ステータス: 実装済み(dev で E2E 検証完了、本番リリース待ち)
起票理由
article リポジトリへの push 後、GitHub App (shuntaka-blog-api) の Recent Deliveries が 2026-07-11 以降すべて「timed out」で失敗表示になっていた。CloudWatch Logs で webhook 受信〜処理完了の所要時間を遡ると、記事数の増加とともに処理時間が線形に伸び、7/11 にちょうど GitHub の webhook 配信タイムアウト(固定 10 秒、延長不可)を超えたことが分かった。
日時 (JST) |
処理時間 |
GitHub 側の結果 |
|---|---|---|
06-30 / 07-01 |
3 秒 |
成功 |
07-05 |
6 秒 |
成功 |
07-11 10:24〜(5回) |
9〜11 秒 |
ここから全滅 |
07-11 18:58 |
16 秒 |
失敗 |
07-13(4回) |
15〜23 秒 |
失敗 |
処理自体は毎回完走していた(Lambda タイムアウトは 15 分)ため、実害は「GitHub 上の失敗表示」と「GitHub API の一時的な 502 でファイル取得が数件落ちた場合に再配信が必要」の 2 点。ただし全 112 記事のフルスキャン(GitHub Contents API から全ファイル取得 + 記事ごとに DB SELECT 2 回 × Lambda→tidb-proxy→Tailnet→TiDB の往復 30〜40ms ≒ 9 秒)が push のたびに走る構造のため、放置すると悪化し続ける。
調査時に、fetch 失敗のエラーログが slug=unknown でどのファイルが落ちたのか特定できない問題も見つけた(あわせて修正)。
設計方針
論点 |
決定 |
|---|---|
非同期化の方式 |
自分自身への Lambda Event invoke。受付ハンドラーは署名検証 + push/ブランチ判定のみで即 200 を返す。素の Lambda は「レスポンスを返してから処理を続ける」ことができない(返した時点で実行環境がフリーズ)ため |
worker を別関数にするか |
しない。関数は |
|
API Gateway の |
Lambda API への経路 |
blog-api の Lambda は NAT なし VPC 内のため squid( |
ローカル開発 |
|
自己 invoke の IAM |
|
差分ファイルのみの処理 |
今回はやらない。全記事スキャンの意味論(毎 push で全件同期)を維持したまま非同期化のみ行う。Event invoke は失敗時に 2 回自動リトライされ、処理は upsert ベースで冪等 |
変更内容
レイヤー |
変更 |
|---|---|
|
|
|
受付( |
|
|
|
自関数への |
依存追加 |
|
調査メモ(原因特定の手順)
再調査が必要になったときのために、今回使った確認コマンドを残す。
# webhook の受信/完了ログから処理時間を出す(JST 変換は date -r)
aws logs filter-log-events --log-group-name /aws/lambda/p-st-blog-api \
--start-time <epoch_ms> --filter-pattern '"Received GitHub webhook"' \
--query 'events[].[timestamp]' --output text
aws logs filter-log-events --log-group-name /aws/lambda/p-st-blog-api \
--start-time <epoch_ms> --filter-pattern '"Webhook processing complete"' \
--query 'events[].[timestamp]' --output text
aws logs tailは高流量時にイベントを取りこぼすため、件数を数える用途ではfilter-log-eventsを使うwebhook が DB を更新したかは
articles.updated_atと処理完了ログの時刻照合で判別できる(updated_atにON UPDATEは無く、webhook の upsert だけが明示的に書く)GitHub 側の失敗が「タイムアウト表示だけ」か「本当に反映されていない」かは切り分けること。今回は 17:39 の push で poem 記事の取得が GitHub API の 502(4 件failed の中)に含まれ、17:50 の手動再配信で反映されていた
dev での検証
GitHub App の webhook は本番 (api.shuntaka.dev) に向いているため、dev は署名付きリクエストを手動で送って確認する。
# dev の webhook secret を SSM から取得して HMAC を付与
SECRET=$(aws ssm get-parameter --name <GH_WEBHOOK_SECRET_KEY_NAME の dev キー> --with-decryption --query Parameter.Value --output text)
BODY='<push イベントの JSON>'
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"
curl -s -X POST https://<dev の API ドメイン>/webhooks/github \
-H "X-GitHub-Event: push" -H "X-Hub-Signature-256: $SIG" \
-H "Content-Type: application/json" -d "$BODY"
# => {"status":"accepted","message":"Queued for async processing"} が 1〜2 秒で返る
# 実処理の完走は d-st-blog-api のログで "Webhook processing complete" を確認
aws logs tail /aws/lambda/d-st-blog-api --since 10m --format short | grep -E "Received|complete|events"
本番反映後は article リポジトリへの push(または Recent Deliveries の Redeliver)で、配信が 200 になることと記事反映を確認する。
作業ログ
2026-07-13
原因調査(処理時間の推移、GitHub 502 による部分失敗、
slug=unknown問題の発見)実装 + PR #625 作成(CI green)
dev デプロイ(stack=main)を workflow_dispatch で実行 → self-hosted runner の Rancher Desktop が Docker ビルド中に落ちて失敗(
rpc error: Unavailable / EOF。コード起因ではない)
2026-07-14
デプロイ失敗が継続。原因は Mac mini の Rancher Desktop VM のメモリ不足(
cargo chef cook --release中に rustc が OOM Kill)。VM メモリ増強で解決し、dev デプロイ成功dev E2E 検証で 2 つの dev 環境固有の問題を発見・解消
blog_dev.users.github_installation_idが prod の値のままで、dev App(shuntaka-blog-api-dev)の installation id と不一致 → 非同期処理がユーザー解決の 404 で終了していた。dev App の installation id(GitHub の Settings → Applications の Configure リンク先 URL 末尾、または webhook payload のinstallation.idで確認できる)に UPDATE して解消main への push(renovate マージ等)のたびに
deploy.yamlの push トリガーが dev を main のコードで上書きする。ブランチ検証中に旧コードへ巻き戻り、インライン処理に見える現象が発生して切り分けに時間を要した。ブランチの dev 検証をするときはこの上書きに注意
再デプロイ後、E2E 完走を確認: 受付 0.24 秒で
accepted→ 自己 Event invoke →/eventsで署名再検証 →Webhook processing complete: processed=1, succeeded=1, failed=0
残課題・注意点
infrastructure/src/github/client.rsの reqwest がClient::new()でタイムアウト未設定。squid トンネル経由の接続が刺さると処理がハングし得るため、Client::builder().timeout(30s)を別 PR で入れるLWA passthrough では、アプリがエラーレスポンス(4xx/5xx)を返しても invocation 自体は成功扱いになるため、Lambda の Event 自動リトライはクラッシュ/タイムアウト時しか発火しない。アプリレベルの失敗(GitHub 502 等)は再配信または次回 push でのフルスキャンでカバーされる前提