
Summary
この文書の要点
- プロダクト機能・運用アラートは共通パッケージに寄せ、CIパイプラインのようにライフサイクルが違う箇所はあえて独立実装のままにします。
- 同じ障害で通知が連投されないよう、threadKeyベースの重複排除・抑制ウィンドウを設けます。
- Webhook URLはトリガーごとに別々のSecret Manager項目として管理し、1つの漏えいが他へ波及しない範囲に閉じます。
- 通知の送信失敗は、呼び出し元の本処理を止めずに握りつぶし、別の場所に失敗の痕跡を残します。
性質の異なる通知源を、1つのチャネルに集約する必要
システムが育つと、チャットツールへ通知を送りたい場面が自然に増えていきます。クラウド監視サービスからの障害アラート、コスト予算の異常検知、CIパイプラインの日次サマリー、問い合わせフォームからの着信、利用者が自分で設定するWebhook通知。それぞれ別のチームや別のタイミングで作られることが多く、気づけば似たような「チャットに投稿する」処理があちこちに重複して存在する状態になりがちです。
重複に気づいた時点で「全部を1つの共通ライブラリにまとめよう」と考えるのは自然な発想です。しかし、実際に手を動かしてみると、通知の送信という表面的な処理は似ていても、それぞれのトリガーが置かれている実行環境、失敗時に許容される挙動、更新頻度がまったく異なることに気づきます。この記事では、どこまで共通化し、どこをあえて共通化しなかったかという線引きの実例を整理します。
トリガーごとに、失敗時に許される挙動が違います
障害監視のような運用アラートは、通知の送信自体は多少遅れても致命的ではありませんが、確実に届くことが重要です。一方、CIパイプラインの中で走るサマリー通知は、通知が失敗したからといってパイプライン全体を失敗扱いにしてはいけません。せっかく完了した作業が、通知処理の一時的な障害によって「失敗」のレッテルを貼られるのは本末転倒です。
同じ障害が続けて何度も発生した場合の扱いも異なります。運用アラートは同じ障害で通知が連投されると受け手が疲弊するため、抑制が必要です。一方、日次サマリーのような定期通知はそもそも頻度が低いため、抑制の仕組み自体が過剰投資になります。「通知を送る」という共通点だけを見て一枚岩の設計にすると、こうしたトリガーごとの違いを吸収しきれません。
技術スタック
プロダクト機能や運用アラートの通知は、TypeScriptの共通パッケージとして実装し、Webhook経由の投稿とAPI経由の投稿の両方をサポートしています。API経由の投稿では、サービスアカウント認証を使い、あらかじめ登録したbotとしてメッセージを投稿することで、単純なWebhookでは扱いにくいスレッド指定や高度な書式を扱えるようにしています。CIパイプライン側は、独立したスクリプトとして別言語(Python)で実装されており、共通パッケージには依存していません。
あえて独立実装のままにするという判断
一方、CIパイプラインのサマリー通知だけは、意図的に共通パッケージへ寄せず、独立した実装のままにしています。理由は単純で、CIパイプラインの実行環境がアプリケーションサーバーとはまったく別のライフサイクル(都度起動して終わる一回限りのスクリプト実行)を持っており、そのために別の言語ランタイムを使っているためです。
ここで無理に共通パッケージを使おうとすると、言語をまたいだパッケージ配布の仕組みを新たに整備するか、あるいはCIスクリプトの中だけ別言語のサブプロセスを呼ぶような迂遠な構成になります。「重複コードを1つにまとめる」という一般論だけを優先すると、こうした無理のある共通化に踏み込みがちです。
実際には、CI側の通知処理はプレーンテキストの組み立てとHTTP POSTだけで完結する、数十行程度の単純な処理です。この程度の重複であれば、共通化によって得られる保守性の向上より、言語をまたいだ依存を増やすことの複雑さのほうが上回ると判断し、意図的に共通化しませんでした。「同じに見える処理」でも、置かれている環境が違えば、別々に持つほうが結果的にシンプルになることがあります。
- CIパイプラインの通知は、実行環境(言語ランタイム)が違うため独立実装のままにする。
- 重複が小さく、共通化のための越境コストのほうが大きい場合は、無理に一本化しない。
重複排除と抑制ウィンドウの設計
運用アラートの通知では、同じ障害が短時間に何度も検知されて、そのたびに新しいメッセージが投稿されると、受け手のチャットが同じ内容で埋め尽くされてしまいます。これを防ぐため、アラートの種類と対象リソースから重複排除キーを計算し、同じキーを持つアラートが一定時間内に再度発生した場合は、新しいメッセージを作らずに抑制する仕組みを入れています。
抑制の判定には、直近に送信済みのキーとその送信時刻を保持する短命なメモリ上のマップを使っています。永続化されたキューやデータベースを使わず、プロセス内のメモリで十分としたのは、抑制の目的が「同じ通知を人間に何度も見せない」ことであり、多少の取りこぼし(プロセス再起動直後は抑制情報がリセットされる、など)が許容できる性質のものだからです。
抑制と同時に、関連するメッセージはスレッドキーでグルーピングしています。同じ障害に関する複数の情報(検知、状況更新、解消)を1つのスレッドにまとめることで、通知は増えても会話としては1本にまとまり、後から経緯を追いやすくなります。
- アラート種別+対象リソースから重複排除キーを計算し、一定時間内の再通知を抑制する。
- 抑制状態はプロセス内メモリで管理し、永続化の複雑さを持ち込まない。
- 同じ障害に関する複数の通知はthreadKeyでまとめ、スレッドとして経緯を追えるようにする。
Webhook URLはトリガーごとに別々のシークレットとして管理する
通知先のWebhook URLやAPIの認証情報は、コードにもログにもハードコードせず、すべてSecret Manager経由で注入しています。ここで重要なのは、トリガーの種類ごとに別々のシークレット項目として管理していることです。問い合わせ通知用、運用監視用、ステータスページのインシデント通知用、CIパイプライン用は、それぞれ独立したシークレットIDを持っています。
1つのシークレットにまとめてしまうと、あるトリガーの実装に関わるメンバーが、本来アクセスする必要のない別のトリガーの通知先URLにもアクセスできてしまいます。トリガーごとにシークレットを分けておけば、あるチャネルの権限を見直したい、あるいはWebhook URLを再発行したいときに、影響範囲をそのトリガーだけに閉じ込められます。
通知の失敗が、本処理を止めてはいけません
CIパイプラインのように、通知が処理全体の主目的ではない箇所では、通知の送信に失敗しても、それによって完了済みの作業自体を失敗扱いにしないという原則を徹底しています。実装上は、通知送信の呼び出しをすべて例外処理で囲み、失敗しても例外を上位に伝播させず、ログに記録するだけに留めています。
ただし、失敗を握りつぶすだけでは「本当は届いていなかった」ことに誰も気づけません。CIパイプラインでは、通知の送信に失敗した場合、その旨を該当作業の追跡Issueへコメントとして残す、という代替の記録先を用意しています。チャットへの通知という即時性の高い経路が失われても、後から追跡できる経路は必ず残す、という二段構えです。
この「失敗を握りつぶしてでも本処理を守る」という判断は、運用アラートには当てはめていません。運用アラートは通知が届くこと自体が目的であり、失敗を握りつぶすと誰にも気づかれないまま障害が放置されるリスクがあるためです。同じ「通知が失敗した」という事象でも、その通知が処理の主目的か副産物かによって、扱いを変える必要があります。
- 副産物としての通知(CIサマリーなど)は、失敗しても本処理を止めない。ただし失敗の痕跡は別経路に残す。
- 目的そのものである通知(障害アラートなど)は、失敗を握りつぶさず、届いたことの確認を設計に含める。
カード形式とプレーンテキストの使い分け
運用アラートやプロダクト通知には、見出し・詳細・アクションリンクのようなフィールドを構造化して表示できるカード形式のメッセージを使っています。人間が一目で状況を把握できることを優先し、構造化のための実装コストを許容する判断です。
一方でCIパイプラインの通知は、あえてプレーンテキストのみにし、文字数を一定の上限で打ち切るだけの単純な実装にしています。カード形式を組み立てるロジックを追加で持ち込むより、依存を増やさず壊れにくいことを優先した結果です。通知の見た目の豊かさより、それを実装・保守するコストが実行環境に見合っているかを基準にしています。
検証観点: シークレットの棚卸しと抑制ウィンドウの妥当性
通知経路が増えるたびに、どのシークレットがどのトリガーに紐づき、誰がアクセスできるかが分かりにくくなっていきます。定期的にシークレットの一覧と用途を棚卸しし、使われなくなった経路のシークレットを放置しないことを運用のチェック項目にしています。
抑制ウィンドウの長さも固定して終わりではなく、実際の障害対応の中で「抑制が効きすぎて状況の変化に気づけなかった」「逆に抑制が短すぎて同じ内容が連投された」という声を継続的に拾い、調整しています。抑制は一度決めたら終わりの設定ではなく、運用しながら育てるパラメータです。
1つのチャネルに集約することの弊害
通知チャネルを1つに集約すると、それぞれの通知源の頻度が積み重なり、重要な障害アラートが日常的なサマリー通知に埋もれてしまうリスクがあります。重複排除やスレッド化はノイズを減らす効果はありますが、根本的な解決にはなりません。重要度の高い通知は別のスペースやチャネルに分ける、優先度でメンションの有無を変えるといった、通知チャネルの設計自体の見直しも必要になります。
また、あるチームにとっては便利な通知が、別のチームにとってはノイズでしかない、ということも起こります。通知を集約する設計は、技術的な共通化だけでなく、誰に何を届けるべきかという運用上の合意とセットで進める必要があります。
まとめ: 共通化は目的ではなく手段です
複数の通知源を1つのチャットツールに集約するとき、すべてを1つの実装にまとめることが正解とは限りません。同じ実行環境で頻繁に使われる処理は共通化して重複を減らす一方、実行環境やライフサイクルが根本的に異なる処理は、無理に一本化せず独立させたほうが、結果として壊れにくいシステムになります。
この記事で扱った重複排除、シークレットの分離、失敗時の扱い、書式の選び方は、どれも「通知を送る」という機能そのものより、それぞれのトリガーが置かれた文脈に合わせて挙動を変えるための工夫です。AIの性能ではなく止め方が保守の品質を決めるのと同じように、通知基盤の質を決めるのも、共通化の広さではなく、共通化しない場所を見極める判断力だと考えています。


