Webhookとは何か?仕組み、メリット・デメリット、実践的なユースケース

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

ポーリング vs. Webhook:通知を受け取る2つの方法

サービス間の連携を初めて構築したとき、デフォルトのアプローチはポーリングでした。30秒ごとにAPIエンドポイントを叩いて、何か変わったか確認し、対応する。確かに動きます。でも数ヶ月本番で運用していると、問題が見えてきます。無駄なAPIコール、レート制限の悩み、そして常にポーリング間隔の半分以上あるレイテンシ。

Webhookはこのモデルを逆転させます。アプリが定期的に「何か起きた?」と尋ねる代わりに、外部サービスが何か起きた瞬間に教えてくれます。1時間ごとにポストを確認しに行くか、郵便配達員がドアベルを鳴らしてくれるか、その違いと同じです。

WebhookはHTTPコールバックです。特定のイベントをトリガーに、あるシステムが指定したURLにPOSTリクエストを送る仕組みです。支払い完了?StripeがあなたのエンドポイントにPOSTします。新しいコミットがプッシュされた?GitHubがCIサーバーにPOSTします。コードがデプロイされた?監視ツールがSlackのWebhook URLにPOSTします。

具体的に比較してみましょう:

# ポーリング方式(30秒ごとに実行)
while True:
    response = requests.get('https://api.example.com/orders?status=new')
    for order in response.json():
        process_order(order)
    time.sleep(30)

# Webhook方式(外部サービスから呼び出されるエンドポイント)
@app.route('/webhook/new-order', methods=['POST'])
def handle_new_order():
    order = request.json
    process_order(order)
    return '', 200

ポーリング版は何も起きていなくても実行され続けます。Webhook版は注文が届いたときだけ動きます。トラフィックが少なければ差は小さいですが、スケールすると、1日5万回の無駄なAPIコールとゼロの差になります。

6ヶ月の本番運用で見えたメリット・デメリット

うまくいくこと

  • リアルタイム配信 — イベントは発生してからミリ秒以内に届きます。Stripeの決済が成功してから1秒以内に確認メールが送れるようになりました。
  • サーバー負荷の軽減 — 無駄なポーリングループがなくなります。実際に処理が必要なときだけサーバーが動きます。
  • シンプルなコード — Webhookハンドラーは小さなHTTPエンドポイントだけです。「最終確認時刻」を追跡するためのステートマシンも不要です。
  • サードパーティエコシステム — GitHub、Stripe、Shopify、Twilio、Slack——これらはすべてWebhookをネイティブにサポートしています。連携する価値のあるサービスなら、まずWebhookをサポートしています。

辛くなるところ

  • エンドポイントが公開されている必要がある — ローカル開発中はこれが面倒です。ローカルホストを公開するためにngroklocaltunnelのようなトンネルツールが必要になります。
  • 受信側になる — イベントが発生したときにサーバーがダウンしていると、簡単に再試行できません。多くのサービスは数回リトライしてくれますが、長時間ダウンしているとイベントを失います。
  • 重複配信は現実に起きる — ネットワークのタイムアウトがリトライを引き起こします。本番環境で同じ支払いイベントを2回処理したことがあります。べき等性チェックは省略できません。
  • セキュリティはあなたの責任 — エンドポイントのURLを知っている人は誰でもPOSTできます。リクエストが期待したソースから来たことを必ず検証する必要があります。

正直なところ、イベント駆動のユースケースではWebhookはポーリングより明らかに優れています。ただし、運用上の責任があなたに移ります。ポーリングは単純で信頼性が高く、Webhookは賢いが注意が必要です。

本番環境での推奨セットアップ

1. Webhookシグネチャを検証する

主要なWebhookプロバイダーはすべてシグネチャヘッダーを提供しています。GitHubはX-Hub-Signature-256、StripeはStripe-Signature(リプレイ攻撃を防ぐタイムスタンプも含む)です。ペイロードを処理する前に必ず検証してください。

import hmac
import hashlib

def verify_github_signature(payload_body: bytes, signature_header: str, secret: str) -> bool:
    """GitHub Webhookのシグネチャを検証する。"""
    if not signature_header:
        return False
    
    hash_object = hmac.new(
        secret.encode('utf-8'),
        msg=payload_body,
        digestmod=hashlib.sha256
    )
    expected_signature = 'sha256=' + hash_object.hexdigest()
    
    # タイミング攻撃を防ぐためhmac.compare_digestを使用
    return hmac.compare_digest(expected_signature, signature_header)

6ヶ月間の本番トラフィックで、なりすましリクエストはゼロ。エンドポイントに10件ほどの不正なPOSTが来ましたが、ほとんど自動スキャナーでした。HMACチェックがすべて弾いてくれます。

2. 素早く200を返し、非同期で処理する

Webhookエンドポイントは2〜5秒以内に応答する必要があります。それ以上かかると送信側が失敗と判断してリトライします。重い処理をインラインで行わず、ジョブをキューに積んですぐ返してください。

import redis
import json

r = redis.Redis()

@app.route('/webhook/stripe', methods=['POST'])
def stripe_webhook():
    payload = request.get_data()
    sig_header = request.headers.get('Stripe-Signature')
    
    # まずシグネチャを検証
    if not verify_stripe_signature(payload, sig_header):
        return '無効なシグネチャ', 400
    
    # キューに積んですぐ返す
    event = json.loads(payload)
    r.lpush('webhook_queue', json.dumps(event))
    
    return '', 200  # 素早く応答

# 別ワーカーがこのキューを処理
def worker():
    while True:
        _, job = r.brpop('webhook_queue')
        process_stripe_event(json.loads(job))

3. べき等キーで重複を処理する

処理済みのイベントIDを保存してください。イベントを処理する前に、すでに見たことがあるか確認します。

def process_stripe_event(event: dict) -> None:
    event_id = event['id']  # 例: 'evt_1ABC123...'
    
    # 処理済みかどうか確認
    if r.sismember('processed_events', event_id):
        print(f'重複イベントをスキップ: {event_id}')
        return
    
    # イベントを処理
    if event['type'] == 'payment_intent.succeeded':
        handle_payment(event['data']['object'])
    
    # 処理済みとしてマーク(7日後に期限切れ)
    r.sadd('processed_events', event_id)
    r.expire('processed_events', 604800)

4. ローカル開発にはngrokを使う

# ngrokをインストール
brew install ngrok  # macOS
# またはngrok.comからLinux/Windows向けをダウンロード

# ポート5000でローカルサーバーを起動してから外部に公開
ngrok http 5000

# ngrokが以下のような公開URLを発行します:
# https://a1b2c3d4.ngrok.io
# テスト時はこれをWebhook URLとして使用

実装ガイド:自動デプロイのためのGitHub Webhook

実際に使っているパターンです:mainブランチにプッシュ → Webhookが発火 → サーバーがプルして再起動。

ステップ1:Webhookレシーバーをセットアップする

from flask import Flask, request, abort
import hmac, hashlib, subprocess, os

app = Flask(__name__)
SECRET = os.environ['GITHUB_WEBHOOK_SECRET']

@app.route('/deploy', methods=['POST'])
def deploy():
    # 1. シグネチャを検証
    sig = request.headers.get('X-Hub-Signature-256', '')
    body = request.get_data()
    expected = 'sha256=' + hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(403)
    
    # 2. mainへのプッシュのみ処理
    data = request.json
    if data.get('ref') != 'refs/heads/main':
        return '無視しました', 200
    
    # 3. デプロイをトリガー(ノンブロッキング)
    subprocess.Popen(['/opt/scripts/deploy.sh'])
    return 'デプロイ中', 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

ステップ2:GitHubにWebhookを登録する

# GitHub CLIを使う場合
gh api repos/{owner}/{repo}/hooks \
  --method POST \
  --field 'name=web' \
  --field 'active=true' \
  --field 'events[]=push' \
  --field 'config[url]=https://yourdomain.com/deploy' \
  --field 'config[content_type]=json' \
  --field 'config[secret]=your_secret_here'

ステップ3:リバースプロキシの背後で動かす

# Nginx設定の一部
location /deploy {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    
    # 不正利用を防ぐためレート制限
    limit_req zone=webhook burst=10 nodelay;
}

Webhookが最も効果を発揮する場面

自動デプロイは一例です。ポーリングよりWebhookが一貫して優れていると感じた場面を紹介します:

  • 決済処理 — StripeやPayPalは決済成功、返金、紛争のイベントを発火します。確認待ちでユーザーをブロックできない非同期決済フローには不可欠です。
  • CI/CDパイプライン — GitHubやGitLabのWebhookはプッシュやPR更新のたびにビルドをトリガーします。どんなポーリング間隔よりも速いフィードバックループを実現できます。
  • チャット連携 — SlackやDiscordのインカミングWebhookを使えば、HTTPを1回POSTするだけでどのサービスからも通知を送れます。OAuthもSDKも不要、URLだけでOKです。
  • EC(Eコマース) — ShopifyのWebhookは注文が入ったとき、在庫が閾値を下回ったとき、顧客が登録したときに通知します。大規模ストアでは1時間に数千件を処理することもあります。
  • 監視アラート — PagerDuty、Datadog、Grafanaはすべてアラート発生時にカスタムエンドポイントへアウトバウンドWebhookを送れます。アラートを社内ツールにルーティングするのに便利です。

個人的なルール:外部APIを1分に1回以上ポーリングしているなら、まずWebhookの代替がないか探します。Webhookエンドポイントを立ち上げるオーバーヘッドは確かにありますが、ほぼ毎回その価値があります。

Share: