【Stripe】Webhookの実装をAzure環境に構築する【前編】

こんにちは、サイオステクノロジーの佐藤 陽です。

今日は、Stripe の Webhook のハンドリングを Azure 環境で実際に組み立ててみたいと思います。

  • Azure上でStripe の Webhook を活用したアプリを構築したい
  • Webhook のハンドリングのベストプラクティスを知りたい
  • さくっと動作確認できる環境を作りたい
という方は是非最後までご覧ください!

なお長くなるのでこの記事は前編になります。

後編はこちら ↓

【Stripe】Webhookの実装をAzure環境に構築する【後編】

はじめに

Webhook とは、Stripe 上で何かしらのイベントが発生した場合にイベントが飛ばされる仕組みです。

例えば「Stripe 上に Customer が作成された時」や、「サブスクリプションが作成された時」などのタイミングで、Stripe から指定したエンドポイントに対してイベントが発行されます。

これに基づいてアプリケーション側で処理を行う事で、イベントドリブンな機能の実装が行えます。

要件

今回お試しで作ってみるアプリは以下のようなものとします。
  • Stripe 上に Customer を作成できる API を実装する
    • 受け付けるパラメータとしては、「名前」,「メールアドレス」とする
    • 顧客作成時にサブスクリプションの契約は不要とする
  • 顧客作成が完了したタイミングで、BlobStorage に顧客情報を保存する
    • 保存する情報は「CustomerId」,「名前」,「メールアドレス」,「作成日時」とする
    • {CustomerId}.json」をファイル名とする json 形式で保存する

Azure システムアーキテクチャ

これを満たす Azure のシステムを組み立てていきます。

処理の流れとしては以下のような流れです。
今回の記事では 1.~4.まで扱います。 5. ~ 6.に関しては後編で扱います。
  1. クライアントが WebApps にて実装した API に対して Customer 作成の要求を出します。
  2. API が Stripe の CreateCustomerAPI を実行し、Stripe 上に Customer を作成します。
  3. Stripe 上で Customer が作成されたタイミングで、customer.created の Webhook イベントがされます。
  4. イベントを受けた WebApps は ServiceBus の Queue に対して Enqueue します。
  5. Enqueue からのメッセージをトリガーとして、Functions が実行されます。
  6. Functions が StorageAccount に対してファイルを保存します。
また「4.」の部分で、イベントを受けた WebApps が直接 StorageAccount にファイルを保存すればいいのでは?

と思われる方もいらっしゃるかと思います。鋭いです。

Stripe のベストプラクティスとして「2xx レスポンスを素早く返す」というものがあります。
この理由に関しては、後ほど詳しく解説したいと思います。

では早速進めていきましょう。

リソース準備

色々準備するものがあるので、順を追ってみていきます。

今回使うリソースは以下の通りです。

  • Stripeアカウント(テスト環境)
  • Azure
    • Azure WebApps
    • Azure Functions
    • Azure Service Bus(Queue)
    • Azure Storage Account(Blob)

Stripe

これは新規にアカウント作っていただければ OK ですね。
テスト環境で問題なしです。

Azure WebApps

アプリケーション作成

まずはデプロイするためのアプリを作っていきます。

フレームワークとしては ASP.NET Core 6 を利用します。
なお、フロントエンドを持たない WebAPI のプロジェクトとしてデプロイします。

プロジェクトを作成します。

dotnet new webapi -n StripeWebhookApp

いつものサンプルプロジェクトが作られると思うので、これをベースに改良していきます。

ひとまず作成するだけで OK です。

Azure WebApps 作成

ではデプロイするための WebApps を作成していきます。

とはいいつつ、特に特筆すべき点もないのでさくっと作りましょう。

気を付ける点としては

  • コードデプロイ
  • .NET 6

くらいでしょうか。
プランも無料の Free プランで問題ないです。

Azure ServiceBus

Stripe の Webhook イベントを格納するキューを用意します。
今回は ServiceBus を利用しますが、StorageQueue でも自作のキューでもなんでも OK です。

ServiceBus のプランとしては Basic を選択しました。
トピックを利用する場合は Standard 以上が必要なので、要件に合わせて適宜選択してください。

Namespace の作成が完了したら、キューを作成します。
こちらもデフォルトのパラメータのままで作成します。

Azure Functions

こちらは後編で扱うため、後編で改めて説明します。

Azure Blob Storage

こちらは後編で扱うため、後編で改めて説明します。

RBAC の設定

WebApps が ServiceBus に対してメッセージを Enqueue するためには、権限が必要です。

今回は SystemAssigned ManagedIdentity の設定を行い、RBAC の設定を行います。
コードの実装の方はまだ行っていませんが、先に設定しちゃいましょう。

まずは WebApps のポータル画面へと遷移し、ID ブレードから設定してきます。
「状態」をオンにして「保存」を押すと、一意の ID が割り振られます。

では次に ServiceBus の画面へ行き、権限を割り当てます。

今回は Queue への追加権限が必要であるため、Queue のリソースに対して「Azure Service Bus データ送信者」の Role を追加します。

これでひとまず準備 OK なので、実装に移りましょう。

実装

それでは実際に WebApps へデプロイするアプリケーションを作成していきます。

先ほどサンプルアプリを作成したので、それをベースに改良していきます。

※今回は Controller クラスにドメイン処理を書いたり、API キーをハードコードしたりと
コードの品質に関しては度外視で行っているため、実装の際にはご注意ください。

Create Customer

まずは Stripe 上に Customer を作成する API を実装します。

Stripe のパッケージをインストールします。

dotnet add package Stripe.net --version 42.4.0

Controller と、Model を追加します。

StripeCustomerController.cs

using Microsoft.AspNetCore.Mvc;
using Stripe;

namespace StripeWebhookApp.Controllers;

[ApiController]
public class StripeCustomerController : ControllerBase
{
    private readonly ILogger<StripeCustomerController> _logger;

    public StripeCustomerController(ILogger<StripeCustomerController> logger)
    {
        _logger = logger;
    }

    [HttpPost("api/customers")]
    public async Task<StripeCustomerModel> Create(StripeCustomerModel req)
    {
        //今回は実装の簡略化のため、APIキーをハードコードしていますが
        //セキュアな情報であるため実際に実装するためにはKeyVaultなどに保管し
        //アプリから参照するようにしてください。
        StripeConfiguration.ApiKey = "sk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

        var options = new CustomerCreateOptions
        {
            Name = req.Name,
            Email = req.EMailAddress
        };
        var service = new CustomerService();
        Customer createdCustomer = await service.CreateAsync(options);

        StripeCustomerModel response = new (createdCustomer.Name, createdCustomer.Email);
        return response;
    }
}

StripeCustomerModel.cs

namespace StripeWebhookApp;

public class StripeCustomerModel
{
    public StripeCustomerModel(string name, string eMailAddress){
        Name = name;
        EMailAddress = eMailAddress;
    }
    public string Name { get; set; } = string.Empty;
    public string EMailAddress { get; set; } = string.Empty;
}

これでローカルで実行してみましょう。

dotnet run .\StripeWebhookApp.csproj

実行後、任意のブラウザで

http://localhost:{port}/swagger

のパスに遷移すると、Swagger UI が表示されると思います。
先ほど作成した CreateCustomer の API があると思うので、実行します。

Stripe ダッシュボード上で Customer が新規作成されている事が確認できます。

これで CreateCustomer の API は完成です。

Webhook ハンドリング API

次に、Stripe から飛んでくる Webhook イベントをキャッチして、キューへと追加する API を作成します。

Stripeのドキュメントにもサンプルコードが提供されているので、こちらも参考にしてください。

今回は以下のように実装しました。

StripeIncomingWebhookController.cs

using Azure.Identity;
using Azure.Messaging.ServiceBus;
using Microsoft.AspNetCore.Mvc;
using Stripe;

namespace StripeWebhookApp.Controllers;

[ApiController]
public class StripeIncomingWebhookController : ControllerBase
{
    [HttpPost("api/webhooks/incoming")]
    public async Task<ActionResult> IncomingWebhook()
    {
        using (StreamReader reader = new(HttpContext.Request.Body))
        {
            string readEvent = await reader.ReadToEndAsync();

            //自らが想定するStripeのWebhookイベントであることを検証
            //whsec_から始まるシークレットは、StripeのWebhookの設定画面から取得可能
            EventUtility.ValidateSignature(readEvent, Request.Headers["Stripe-Signature"]
            , "whsec_XXXXXXXXXXXXXXXXXXX");

            //ServiceBusへのEnqueue処理
            ServiceBusClient client = new("sbns-stripe-webhook.servicebus.windows.net", new DefaultAzureCredential());
            ServiceBusSender sender = client.CreateSender("sbq-stripewebhook");

            ServiceBusMessage serviceBusMessage = new(readEvent);
            await sender.SendMessageAsync(serviceBusMessage);

            return Ok();
        }
    }
}

実装のポイントとしては、署名シークレットを利用してイベント内容を検証している部分です。

こちら、Webhook のセキュリティを高めるうえで Stripe も推奨している部分であり、
Webhook のエンドポイントごとに割り当てられる署名シークレットをここで検証することで
自分が必要とするエンドポイントからのイベントのみをキャッチすることができます。

whsec_から始まるキーに関しては、後ほど行う Stripe 上の設定で取得できます。

補足:ServiceBusClientについて

また、今回は記事の対象外としていますが、ServiceBusClient の扱いに関しても本来注意が必要です。
現段階だと API がコールされるたびに Client を生成していますが、これは本来避けるべきです。
そのあたりの内容に関してはこちらにも書いていますので、興味がある方はご覧ください。

Stripe の Webhook エンドポイント設定

Stripe のダッシュボードから Webhook のエンドポイントの追加を行います。

エンドポイントURLは、WebAppsのエンドポイントを指定しましょう。

バージョンに関しては、WebhookをハンドリングするアプリのSDKのバージョンに合わせます。
今回はversion 42.4.0のものを利用しているため、2023-08-16としました。

リッスンするイベントの選択は、必要最低限のイベントのみ受信することが推奨されているため、今回は customer.created のみ設定します。

作成が完了したら、署名シークレットが発行されるので、先ほどの API の実装で利用します。
繰り返しになりますが、こちら実際に実装する際はハードコードを避けましょう。

動かしてみる

ではここまで作れたら実際に動かしてみましょう。

先程作成していたアプリケーションを WebApps にデプロイします。

その後、デプロイした API を利用して Customer を作成します。

postman 等で CreateCustomer の API を実行しましょう。

そうすると、最初に述べた 1.~4.まですべて完了しているはずです。

順を追って確認します

動作確認

StripeDashboard上で、新規に Customer が作成されていることが確認できます。
このCustomerを追っていきたいと思うので、CustomerIDを控えておきます。

cus_OcIKnZS8cVAzNC

では、この作成によって発行されたStripeのEventを確認します。

StripeDashboardの開発者タブから、先ほど作成したWebhookのエンドポイントを作成します。

そうすると、1件「customer.created」のイベントが追加されている事が確認できます。
また、イベント内容を見ると先ほどのCustomerIdが記載されていることも分かります。

ではこのイベントがキューに追加されているか確認してみましょう。
作成したServiceBusのQueueからメッセージを確認します。

すると、Queueに1件メッセージがためられていることが確認できます。
また、内容を見ると先ほどのEventIdCustomerIdが紐づいていることも確認できますね。

これで

  1. クライアントが WebApps にて実装した API に対して Customer 作成の要求を出します。
  2. API が Stripe の CreateCustomerAPI を実行し、Stripe 上に Customer を作成します。
  3. Stripe 上で Customer が作成されたタイミングで、customer.created の Webhook イベントがされます。
  4. イベントを受けた WebApps は ServiceBus の Queue に対して Enqueue します。

の流れが実装できたことが確認できました。

なぜキューを経由するのか?

最初にも少し触れましたが、なぜわざわざキューを経由するのでしょうか。
今回であれば、IncomingWebhookのAPIでイベントを受けたタイミングで、StorageAccountへ追加すればよいのではないでしょうか?

これに関しては色々と理由があります。

時間制限

イベントを受けてから20秒以内に2xxのレスポンスを返さない場合、Stripe側で失敗とみなされます。
そのため重たい処理を実行するのは推奨されず、出来るだけ早くレスポンスを返す必要があります。

Stripeのドキュメントにも以下のような記載があります。

エンドポイントは、タイムアウトを発生させかねない複雑なロジックの前に、素早く成功のステータスコード (2xx) を返す必要があります。たとえば、会計システムで顧客の請求書を支払い済みとして更新する前に、200 のレスポンスを返す必要があります。

リトライ処理

例えば、Webhookのイベント自体は正常に受け取れたが、StorageAccountへの保存に失敗した場合にStripe側に4xxを返すとします。
そうした場合、Stripe側ではリトライ送信が行われます。

ただ、このリトライ送信の間隔などはStripe側の実装に依存し、ドキュメントには

本番環境では、Stripe は指数バックオフを使用して最長 3 日間、お客様の Webhook エンドポイントに対する特定のイベントの配信を試行します。ダッシュボードのイベントセクションで、次回の再試行が発生する時期を確認できます。

と記載があります。

アプリとしてのリトライ処理の要求を、このStripeのリトライ送信の間隔に基づいて実装するのはやや危険ですよね。
それよりは、自分で用意したキューのリトライ設定を正しく設計するべきです。

そのため、IncomingWebhookのAPIに関しては

  1. イベントの受信・検証
  2. キューへの追加
  3. エンドポイントへのレスポンス

のみに集中し、その後の処理に関してはStripeWebhookから独立した場所で処理を行うべきだと考えます。

まとめ

今回はStripeのWebhookハンドリングをAzure環境で実装してみる、と題してキューに追加するところまで実装しました。
Webhookのイベントの扱いに関しては色々と注意する点があります。
ドキュメントには今回触れた部分以外にも、色々と記載があるので是非ご一読ください。

では後編で会いましょう!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です