ITの教科書検索
入門読了 約11分

Webhookとは ― APIとの違いとイベント通知の仕組み

決済の完了やコードのpushを、相手からHTTPで知らせてもらう仕組みがWebhookです。APIとの違い、URLの欄に入れる値、署名の検証と再送への備えが分かります。

目次
  1. Webhookとは
  2. WebhookとAPIの違い
  3. ポーリングとの違い
  4. Webhookの仕組み
  5. 通知の中身
  6. 受け取る側の注意
  7. 届いた通知ごとの動き
  8. よく使われる場面
  9. クラウドでの受け口
  10. 送る側を作るときの注意
  11. 参考にした資料
  12. 練習問題

Webhookとは、出来事が起きたときに自動でHTTPリクエストを送る仕組み

GitHubの設定画面や決済サービスの管理画面には「Webhook URL」の欄があり、通知を受け取るプログラムのURLを入れます。Webhook(ウェブフック)とは、サービスの中で出来事が起きたときに、登録されたURLへ自動でHTTPリクエストを送る仕組みです。

決済が完了したら注文を確定する。コードがpushされたらテストを始める。障害が起きたらチャットに知らせる。こうした連携を、相手に何度も確かめに行かずに作れるので、サービスどうしをつなぐ場面でよく使います。その代わり、受け取るURLをインターネットに公開するため、偽の通知や同じ通知の重複に備える必要があります。

WebhookとAPIの違いは、どちらからリクエストを送るか

Web APIでは、自分のプログラムが知りたいときにリクエストを送り、結果をレスポンスで受け取ります。Webhookでは向きが逆になり、出来事が起きたサービスが、こちらのURLへリクエストを送ってきます。どちらもHTTPを使うので、違うのは通信の方式より、誰がいつ送り始めるかです。取りに行く形をプル型、届けてもらう形をプッシュ型と呼ぶこともあります。

APIを呼ぶ場合とWebhookで受け取る場合の違い
観点APIを呼ぶWebhookで受け取る
送り始める側自分のプログラム出来事が起きたサービス
送るタイミング自分が知りたくなったときサービスの中で出来事が起きたとき
用意するものリクエストを送るプログラムインターネットから届くHTTPSのURL
向く使い方必要なときにデータを取り出す、データを作る出来事をすぐ知って、次の処理を始める

ポーリングとの違いは、確かめに行く回数と気づくまでの遅れ

相手の変化をAPIだけで知るには、一定の間隔でAPIを呼んで確かめます。このやり方がポーリングです。1分おきなら1日に1,440回呼ぶことになり、その多くは変化なしで終わります。間隔を短くすれば早く気づけますが、呼ぶ回数はその分だけ増えます。

決済が完了ポーリング(1分おき)サービスアプリ変化なしここで気づくWebhookサービスアプリPOSTで通知0分1分2分3分4分5分決済が完了ポーリング(1分おき)サービスアプリ変化なしここで気づくWebhookサービスアプリPOSTで通知0分1分2分3分4分5分
決済の完了を1分おきのポーリングで調べると、6回確かめて変化を見つけるのは1回だけで、気づくのは完了の30秒後です。Webhookなら、完了したときにサービスから1回だけ通知が届きます。

Webhookなら、知りたい出来事を最初に1回登録するだけで済みます。GitHubの文書も、APIを呼び続けるより規模が大きくなっても対応しやすく、ほぼリアルタイムに更新を受け取れると説明しています。

ただし、APIが不要になるわけではありません。GitHubの文書は、情報が一度だけ、またはたまにしか要らないときや、調べる対象が少ないときはAPIの方が向くとしています。相手がWebhookに対応していないときや、公開のURLを用意できないときも、ポーリングを選びます。

Webhookの仕組み:URLの登録から2xxの応答まで

通知を送るサービスを送る側、通知を受けるプログラムを受け取る側と呼ぶことにします。1と2の準備は最初の1回だけで、3と4は出来事のたびに繰り返します。

  1. 受け取る側が、通知を受け付けるURLを用意する
  2. 送る側の設定画面に、そのURLと受け取りたいイベントの種類を登録する
  3. 出来事が起きると、送る側がそのURLへPOSTリクエストを送る
  4. 受け取る側が、成功を表す2xxのステータスコードを返す
送る側受け取る側URLとイベントを登録ここから出来事のたびに決済が完了POSTで通知すぐに2xxを返す重い処理はあとで2xxが返らない失敗として再送送る側受け取る側URLとイベントを登録ここから出来事のたびに決済が完了POSTで通知すぐに2xxを返す重い処理はあとで2xxが返らない失敗として再送
URLの登録は最初の1回だけで、そのあとは出来事のたびに送る側から通知が届きます。赤い線は2xxが返らなかったときの動きで、再送するかどうかはサービスによって違います。

2xxの応答は、通知を受け取ったという合図です。200番台以外の応答や、応答が返らないときは、送る側は配信の失敗として扱います。Stripeは301や302などの転送の応答も失敗として扱うので、転送の無い最終のURLを登録します。

Webhook URLの欄に入れるのは、受け取る側のURL

GitHubや決済サービスでは、自分で用意したサーバのURLを入れます。Stripeの場合、登録できるのは公開されたHTTPSのURLです。

Slackのように、サービスの方がURLを発行する場合もあります。SlackのIncoming Webhooksでは、発行されたURLへJSONをPOSTすると、決めたチャンネルにメッセージが出ます。監視ツールなどの設定画面に貼るのはこのURLで、この場合はSlackが受け取る側です。どちらの形でも、URLの持ち主が通知を受け取ります。

Slackの文書は、このURLに秘密の情報が含まれるとして、公開のリポジトリなどに載せないよう注意しています。GitHubの文書も、登録するURLに機密の情報を入れないよう勧めています。

通知の中身はJSONで、イベントの種類と内容が入る

通知の本文には、JSONがよく使われます。次の例は、架空の決済サービスが支払いの完了を知らせるリクエストです。

支払いの完了を知らせるWebhookの例
POST /webhooks/payment HTTP/1.1
Host: shop.example.com
Content-Type: application/json
X-Example-Signature: sha256=3f1c9a7e...

{
  "id": "evt_1001",
  "type": "payment.succeeded",
  "data": {
    "order_id": "A-2048",
    "amount": 3980
  }
}

typeはイベントの種類で、受け取る側はこの値を見て処理を分けます。idはイベントを見分ける値で、同じ通知が2回届いたことに気づくために使います。dataには出来事の内容が入り、この例では注文番号と金額です。

項目の名前や置き場所は、サービスごとに違います。GitHubはイベントの種類をX-GitHub-Eventヘッダ、イベントを見分ける値をX-GitHub-Deliveryヘッダに入れます。Stripeは本文のtypeとidに入れます。GitHubもStripeも、受け取るイベントは必要な種類だけに絞るよう勧めています。

通知に最小限の情報だけを入れるサービスもあります。Stripeのシンイベントには、イベントの種類と対象のIDなどしか入っていません。受け取った側がAPIを呼んで最新の状態を取りに行くので、WebhookとAPIを組み合わせた形になります。

受け取る側の注意:署名の検証、重複への備え、すぐに応答

署名を検証して、本物の通知か確かめる

受け取るURLは公開されているので、誰でもPOSTを送れます。偽の支払い完了の通知を信じると、代金を受け取っていない注文を確定してしまいます。Stripeの文書も、検証しなければ、攻撃者が偽のイベントで注文の処理などを実行できると警告しています。

GitHubやStripeでは、送る側と受け取る側だけが知る文字列を共有しておきます。この文字列をシークレットと呼びます。送る側は本文とシークレットから検査用の値を計算し、ヘッダに入れて送ります。この計算方法がHMACです。受け取る側は同じ計算をして、ヘッダの値と比べます。一致すれば、シークレットを知る相手が送った通知で、途中で書き換えられていないと分かります。

この値は署名と呼ばれていますが、公開鍵を使うデジタル署名ではなく、同じ鍵を両側で持つMACの一種です。GitHubはX-Hub-Signature-256ヘッダ、StripeはStripe-Signatureヘッダに値を入れます。どちらもSHA-256を使うHMACで計算します。検証するときは、次の点を守ります。

  • 受け取った本文をそのまま計算に使う。JSONとして読み直した本文では、値が合わなくなることがある
  • 比べるときは == を使わず、かかる時間が内容で変わらない比較の関数を使う
  • シークレットはコードに書かず、環境変数などに置く

正しい通知を盗み見て、もう一度送りつける攻撃もあります。これをリプレイ攻撃と呼びます。Stripeは署名の対象に時刻を含めるので、攻撃者は時刻を書き換えられません。公式のライブラリは、既定で5分より古い通知を拒否します。

Stripeは署名の検証に加えて、送信元のIPアドレスを許可リストで絞る方法も勧めています。

再送と重複に備えて、同じ通知を2回処理しない

配信に失敗した通知の扱いは、サービスごとに違います。Stripeは本番環境で、間隔を広げながら最長3日間送り直します。GitHubは自動で再送しないので、失敗した配信は画面やAPIから手動で再送します。

再送があると、同じ通知が2回以上届くことがあります。処理は終わったのに、2xxの応答が送る側に届かなかった場合です。同じ処理を2回動かすと、商品を二重に発送したり、確認のメールを2通送ったりします。受け取る側は、同じ通知が何回届いても結果が変わらないように作ります。この性質を冪等性(べきとうせい)と呼びます。

RFC 9110はPOSTを冪等なメソッドに入れていないので、HTTPの決まりには頼れません。よくある作り方は、処理したイベントのIDを記録し、記録にあるIDが届いたら処理せずに2xxだけを返す方法です。GitHubのX-GitHub-Deliveryは、再送でも同じ値のままなので、この用途に使えます。

届く順番にも頼れません。Stripeは、イベントが発生順に届くことを保証しないと明記しています。順番が大事な処理では、通知の内容をそのまま使わず、APIで最新の状態を確かめます。

すぐに応答して、重い処理はあとで行う

送る側は、応答を長くは待ちません。GitHubは10秒以内に2xxを返すよう求めていて、遅いと接続を切って失敗として扱います。Stripeも、時間のかかる処理より先に2xxを返すよう求めています。

そこで、受信の処理では署名とイベントIDを確かめたら、仕事を処理待ちの列に入れて2xxを返します。この列をキューと呼びます。メールの送信や在庫の更新は、別のプログラムがキューから取り出して進めます。通知が一度に増えても、キューがあれば処理できる速さで順に片付けられます。

届いた通知ごとに、受け取る側の動きを比べる

受け取るプログラムの4つの段階を、順に並べました。場面によって、どこで止まり、どの応答を返すかが変わります。

場面を選ぶと、受け取るプログラムの動きが切り替わります。
  1. 通過署名を確かめるヘッダの値と計算した値が一致
  2. 通過イベントIDを確かめる初めて届いたID
  3. 通過キューに入れる注文の確定をキューに登録
  4. 通過応答を返すすぐに返す
200 OK送る側は、配信に成功したと記録します。
注文の確定は、別のプログラムがキューから取り出して進めます。

署名を最初に確かめるのは、偽の通知の中身を使わないためです。処理済みのIDに2xxを返すのは、失敗と判断されて再送が続くのを止めるためです。

Webhookがよく使われる場面:決済、CI、チャット通知

決済の完了を受けて注文を確定する

決済の中には、結果がすぐに決まらないものがあります。Stripeは、顧客の銀行が決済を確認したときや、継続の支払いが成功したときの例を挙げています。Webhookを受けていれば、利用者がブラウザを閉じたあとでも、サーバが結果を受け取って注文を確定できます。

コードのpushをきっかけにテストを始める

GitHubは、コードがpushされると、登録されたURLへ通知を送れます。これをCI/CDのツールが受けると、ビルドとテストのパイプラインが動き出します。開発者が手で実行しなくても、pushのたびに同じ検査が走ります。

監視の異常をチャットに知らせる

監視ツールやCIの結果は、チャットのWebhookへ送ると、担当者の見ているチャンネルに表示できます。前の節で見たとおり、チャットのサービスが発行したURLを、通知を送るツールに登録します。どの異常を通知するかの決め方は、オブザーバビリティの記事が参考になります。

クラウドでの受け口は、APIゲートウェイとサーバーレス関数の組み合わせ

受け口は普通のWebサーバでも作れますが、通知の来ない時間も動かし続けることになります。そこでクラウドでは、公開のURLを持つAPIゲートウェイと、リクエストが来たときだけ動くサーバーレスの関数を組み合わせることがあります。例として、AWSにはAmazon API GatewayとAWS Lambda、AzureにはAzure FunctionsのHTTPトリガーがあります。

サーバーレスの関数は、しばらく呼ばれないと起動に時間がかかることがあります。応答の期限に遅れないよう、関数の中では署名の検証とキューへの登録までに留めます。

Webhookを送る側を作るときの注意:SSRF

自分のサービスにWebhookの機能を付けると、利用者が登録したURLへサーバがリクエストを送ることになります。社内向けのアドレスやクラウドの管理用のアドレスを登録されると、外からは届かない場所へサーバを使って接続されます。この攻撃をSSRFと呼び、OWASPの資料もWebhookを例に挙げています。

送る前に、名前解決した先のアドレスが社内向けでないことを確かめ、確かめたそのアドレスへ接続します。確かめたあとに名前解決をやり直すと、別のアドレスへ向けられるおそれがあるからです。

参考にした資料

練習問題(全6問)

Q1Webhookの説明として、最も適切なものはどれ?
Q25分おきにAPIを呼んで注文の状態を確かめていたアプリを、Webhookで受け取る形に変えた。変化として正しいものはどれ?
Q3コードのpushを自分のサーバで受け取りたい。GitHubのWebhookの設定で、URLの欄に入れるものはどれ?
Q4決済サービスからのWebhookで署名を検証しないと、どのような問題が起きるか。
Q5処理済みのイベントIDを持つ通知が、もう一度届いた。受け取る側の対応として適切なものはどれ?
Q6Webhookの受信処理の作り方として、適切なものはどれ?

選んでから「採点する」を押すと、解説つきで答え合わせができます。

関連記事

クラウドの記事を続けて読む

クラウド 全36本中17本目