SendGridのWebhookを受け取ってログを吐くだけのサービスを作ったときにやったことまとめ
はじめに
みてねプロダクト開発部 プラットフォームグループ CREチームのこすげです。
この記事ではSendGridのWebhook機能を使ってログをBigQueryに保存して計測、可視化までできるようにしたときに実施したことをまとめた記事になります。
当初は、HTTPを受け取れるCloud Runを使って簡単に作れると思っていました。
しかし、CREチーム内で検討を進めるうちに、事前に考慮すべき点や準備すべき内容が多数出てきて、実際にコードを書き始めるまでに様々な準備が必要でした。
今まで1つの小さいサービスを作るときにここまで考えて作ったことがなかったため、自分自身の甘さと経験不足を痛感しました。振り返って反省し、次に活かせるようにこの記事を書きました。
この記事読んでわかること・わからないこと
多分以下のことがわかると思います。
- 各サービスのWebhookを受け取って様々な処理をしたい場合に調べた方がいいこと
- webhookを受け取るサービスを作る際に考えた方がいいこと
以下については深く触れません。
- SendGridのWebhookの仕様について
- 前提として書きはしますが、必要最低限になります
- この記事でSendGridのWebhookの仕様について詳しくなれるわけではありません
- 実際に書いたソースコード
- Cloud Loggingの構造化ログ
- この点については参考にした記事を紹介します
上記のことを踏まえた上で読んでいただければ嬉しいです。
以下では、作り始めたきっかけや技術的な背景から、実際に行ったことを時系列順に説明していきます。
背景
社内で採用されている技術の前提について紹介します。
SendGridのWebhookを使うことになったわけ
みてねが実施しているメルマガ関連はSendGridを使って管理しています。
メルマガを送った後の効果計測をする際に以下のことを行っていました。
- SendGridの管理画面にアクセス
- 該当のメルマガレポート画面を開く
- CSVをダウンロード
- CSVをスプレッドシートに貼り付けて不要な箇所を削る
- 数字をまとめる
画面共有で作業を確認したところ、同じような作業を繰り返すのが大変そうで、改善の必要性を感じたのがスタートでした。
そこからある程度調査するとWebhookを受け取ってレポートを集計できることがわかりました。
データの集計
社内で集計データがBigQueryに集約していること、Lookerを使って集計、ビジュアライズまで行っていることも知ったため、そこに合わせた設計で進めていく方針で進めました。
インフラ周り
みてねはKubernetesを利用しているため、最初はその上で動かす形で進めようとしていました。
ただチーム内で
- みてねに関わるサービスではないこと
- 作るサービスの不具合でみてね全体に影響を及ぼすことがないようにしたい
という観点で別サービスとして切り出すことにしました。
組織内でも「小さいサービスを作って運用する」際のテンプレートが特になかったため、設計から自由に決められることがわかり、チーム内(必要に応じて関係各所)で相談しながら進めることにしました。
進めていくにあたって
ここの段階で私としては
- 最終的にBigQueryにデータを格納したいのでGoogle Cloudを使用しよう
- 使用経験のあるCloud RunでWebhookを受け取るようにしよう
- BigQueryにinsertすればよい
と考えていました。
先にWebhookを受け取るサービスを本稼働まで持っていって、完了した時のチケットの中身を紹介します。
チケットのタイトルはちょっと具体的すぎたので黒塗りしました。
チケットの数と分類くらいの感覚が伝わってくれたら嬉しいです。
前提が長くなってしまいましたが、この画像にあるおおよそ全てをカバーした内容をこれから書いていきます。
目次を確認いただければ流れがよくわかるように書いているので、必要な箷所を選んで読んでいただければと思います。
開発に使う言語と開発環境を決める
開発言語や開発環境の選定について説明します。
みてねではサーバーサイドにRuby on Railsが使われていて、Rubyの知見もありました。
また数年後にいきなりメンテナンスすることも踏まえて、生成AIが幅を利かせられるといいよねと話し合って、結果Rubyに落ち着きました。
フレームワークはRuby on Railsだと大きすぎるだろうから、Sinatraを使うことにしました。
開発環境に関しては、Docker使えば環境構築も楽になるからDocker使うことにしました。
またみてねでは開発環境でKubernetes上にSandbox環境を個別に用意してもらっていて、そこである程度は自由に開発できる場所になっているため、その中で作っていく方針にしました。
サービスレベルと設計を決める
実際にWebhookを受け取るサービスがどれくらい稼働していればいいのかを決めました。
SLAを決めることで設計ができるとチームメンバーから教えてもらい、非常に勉強になりました。
今までSLA を考慮した経験がなかったためです。
AWSやGoogle Cloudが提示してるものをざっくり知ってるくらいで、自ら定義してそれを守るような設計を考えたことがありませんでした。
SLAについて深く語れませんが、要はサービスがどういう品質を担保するかを宣言することだと思いました。
絶対に守らないといけないことは何かを考え、調査しました。
今回のサービスで大事にしないといけないことは
SendGridのレポートの欠損をできる限りなくすこと
でした。
SendGridの管理画面からレポートをCSVダウンロードして~の作業をなくすために作るので管理画面で見る数字と今回つくったサービスの数字の乖離が著しい場合、使ってもらえないですもんね。
レポートの数字との乖離をなくすためには、SendGridから送られてくるWebhookをどれくらい処理できる必要があるのかを明確にする必要がありました。
この情報は調査で簡単に見つかりました。
ここから引用すると大事なのは以下の2点です
イベントは現状30秒毎またはバッチサイズが768KBに達するか、いずれか早いタイミングでPOSTされます。これはサーバごとのため、Webhook URLは毎秒数十回~数百回のPOSTを受信する場合があります。
SendGridは、HTTPステータスコード 2xx が返されるのを待ちます。指定されたURLから 2xx 以外のコードが返された場合は、24時間再送を試みます。再送期間内に送信されなかったイベントはそのまま失われます。
このことから、24時間以内にサービスを復旧できれば問題ないことが判明しました。
これまで緩い要件であるため、適切な構成を組めば十分対応可能と判断し、以下の点を考慮することにしました。
- 開発環境と実行環境(本番環境)が出来る限り同じだと嬉しい
- テストなども含めて
- 1リクエストあたりの金額
- 今回でいうとBigQueryへinsertまでがサービスのゴール
上記をたくさん考慮して(時には生成AIに質問した)、今回はGoogle CloudのCloud Runを使うことにしました。
またCloud Runからログをstdoutで吐き出すだけで Cloud Logging -> BigQueryへ流れるような仕組みもすでにあったので、それを使うようにしました(なんと無料)。
他にどんな考慮をしたの?って気になる方はメモがたくさん残っているので、知りたい方がいらっしゃったらコメントでもください。
記事書きます。
ここまで決まれば開発まで一気に行けるかなと思ったのですが、まだまだ調べました。
セキュリティ関連の調査
要はSendGridのWebhook以外からのrequestをどう捌くかという話です。
Webhookはサービスによって様々な仕様があります。ただ今回守らないといけないのはレポートの数字なのでどこかの誰かのイタズラで計測に不備が生じることは許されません。
そこでSendGridのWebhookのセキュリティ周りを調査すると以下のドキュメントが見つかりました。
深くは説明しないですが、SendGridは2パターンの検証方法を提示してくれています。
- Signed Event Webhook Requests
- 公開鍵を使って、Requestを受け取る側が検証するパターン
- OAuth2
- OAuth2の仕組みを使って、認可するパターン
今回はSigned Event Webhook Requestsの仕組みを使って、SendGridからのリクエストかどうかを検証するように実装しました。
ここら辺はSendGridが用意してくれているライブラリを使って楽できました。
セキュアな値の保存場所と参照方法
Google Cloudを使うので、SecretManagerを使ってCloud Runへ値を渡す方法はすでに確立しており、そこは問題ありませんでした。
気になったのは開発環境におけるセキュアな値の取り扱いです。
今回でいうと以下の2点が対象でした。
- Signed Event Webhook Requestsで発行される公開鍵
- SendGrid APIに必要なAPI Token
API Tokenが厄介です。これがあるとSendGridに保存されているデータがすべて参照できてしまいます。
SendGridはメールアドレス等個人情報の宝庫なので、より注意しないといけません。
ここに関してはSendGridのサブユーザー的な取り扱いがあったので、開発環境ではそちらで発行したAPI Token等を使うことにしました。
他のサービスでもそのような仕組みがあるかどうかを調査する必要があると思いました。
実装開始
上記の点が決まり、ようやく実装を開始しました。
Google CloudはTerraformで管理されていたため、学習しながらチームメンバーにレビューを受けつつ進めました。
作ったのは以下のリソースです。
- Artifact Registry
- Docker Imageを管理するサービス
- Cloud Run
- Service
- Service Account
- Cloud Runで使うアカウント
- GitHub Actionsで使うアカウント
- Logging Bucket
- Cloud Loggingで吐き出したログをfilterして格納するbucket
- ここに溜め込んだログがBigQueryから参照できるようになる
- Monitoring
- alert policyを作って、エラーが上がってないかどうかを監視
- 閾値を決めて、それ以上になった場合slackに通知するようにした
実装に関しては難しいことはなく、Sinatraを使って様々な処理を実装しました。
エンドポイントも1つで済みました。
Cloud Loggingに合わせた構造化ログに関しては以下の記事をとても参考にしました。
今回特別なことをしてるとしたら、SendGridのカスタムフィールドにみてね特有の値が設定してあり、それを参照してログに入れ込みたいためにSendGridのAPIを叩く処理を追加で書いています。
そこに関してもSendGrid公式のgemを使えば苦労なくできました。
全体の実装と構成に関してチームメンバーにたくさんレビューしてもらい、テストも書きやすくできたと思います。
とても感謝してます。
ここまで終わり、Cloud Runで稼働させて、SendGridのWebhookの設定も終わり稼働させてみました。
そうするとしっかりエラーが出るようになりました。
エラーの調査と対応
エラーの原因が不明な状態から始まりました。
そこでチームメンバーに相談したところ、まずはエラーログをしっかり吐き出しましょうという初歩的なところから始めました。
このサービスで言うと以下のポイントがあります。
- requestを受け取った
- SendGridのAPIを叩いた
- SendGridからレスポンスが返ってきた
あとはRubyのソースコードの話でそこはテストでカバーできているので、一旦抜きにして上記のポイントにログを仕込んでみました。
余談ですが、メルマガの送信時にSendGridのWebhookで送信時のイベントが大量に届きます。
この時にエラーがたくさんでる(それ以外のイベントはユーザーの行動ベースなのでタイミングがバラバラ)ため、検証にとても時間がかかりました...。
ログを見たところ、SendGridのWebhookを受け取ってから処理が開始するまでの時間にとても差があることがわかりました。
様々な仮説を立てては検証してを繰り返した結果、Cloud Runの同時リクエスト数とSinatraの前にいるPumaサーバーのスレッド周りに差があって、捌ききれていないことがわかりました。
この点は使用する言語によって問題が異なるため、詳細は記載しません。
ただし、Sinatra + Puma構成のサービスをCloud Run上で運用する場合は特に注意が必要です。
この点に関する詳細は別の記事で取り上げる予定です。
上記の対応も入れておかしいエラーもほぼなくなりました。
管理画面のレポートの数字の突き合わせ
一番大事なところです。ここがズレてると稼働させる意味がなくなってしまいます。
結果は全イベントで99.9%の一致率を出すことができました。
ここも数字を出してチームメンバーに共有してという流れで実施しました。
どうしてもメルマガ送信時のタイミングでしかエラーが出ないため、検証に時間がかかったのもあり、その間にCI / CD周りの整備だったりドキュメントを書いたりLookerで可視化したりと順番が前後した部分はありましたが、無事にサービスを稼働させ現状は満足に使えています。
まとめ
ざっくりとですが要点を絞ってWebhookを受け取るサービスを作る上で考慮した方がいい点を私の経験談に沿って書きました。
一番大事なのは サービスを作る上で何を大事にするか だと思います。
そこを崩さないように進めていくのがいいと思いました。
実際にこのサービスを作って手離れしたのが大体2ヶ月くらいです。
当初の私のとても甘い見積もりだと1週間くらいです。
実施した感想としては もっと早くできた、できなくて悔しい でした。
ただ甘い見積もりをしていただけなことに気がつけて本当によかったです。
多分ですが、いろいろ犠牲にしないといけない部分もありますし、考慮しないでおく問題もあると思います。
ただ大事なのは何も知らないよりも、知った上で判断できることだと思います。
いい感じに判断して、要件を定義して開発できるようになっていきたいです。
余談
今回、もし私1人で当初の想定の1週間で作ったと仮定した時に実際に質問されて答えられなかったなという質問をいくつか残しておきます。
- SendGrid Webhookのrequestを捌ききれなかった場合にどういう挙動を取るのか?
- SendGridからのrequestかどうかどうやって検証しているのか?
- Cloud Runでエラーレポートが上がっているけど気づいているのか?
以上になります。
ありがとうございました。
Discussion