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