Alertシステムを標準化し、IaCで運用する
(engineering.ab180.co)はじめに
サービスが拡大するにつれて、運用中に確認すべきシグナルも増えていきました。この記事では、Alertをコードで管理し、SlackとPagerDutyにつながる対応フローを標準化した過程を紹介します。
最初の目標はシンプルでした。Alertをより簡単に作成し、より見やすく送信し、誰が見るべきかを明確にすること。 その後、運用を続ける中で grouped Alert、反復定義、対応の自動化、モニタリングシステムの安定性まであわせて磨き込んでいきました。
動機
サービスの可用性を高め、ユーザーへの影響を減らす方法はいくつもあります。
その中で今回の取り組みは、Alertシステムの改善に集中しました。
Alertは、障害予防と障害対応の間をつなぐ運用インターフェースに近いものです。危険なシグナルをより早く見つけられれば、実際の障害につながる前に対処できますし、障害が発生した後でも担当者がより早く気づき、対応を開始できます。
当時、改善したい方向性も明確でした。危険なシグナルをより確実に捉え、担当者がより早く気づけるようにし、調査と対応へすぐにつなげ、繰り返し発生する対応フローを減らしたいと考えていました。
最初からすべてを定量的に測定して始めたわけではありませんが、Alertは単なる通知ではなく、障害を予防し、対応へつなげる運用システムであるべきだという問題意識は明確でした。
Alertの役割
安定したサービスのためには、障害を予防することも重要ですが、運用中のサービスの異常兆候を素早く検知することも重要です。
Alertはこの点で2つの役割を果たします。障害が発生する前には、危険なシグナルを素早く認識し、実際の障害につながる前に事前に対処できるよう支援し、障害が発生した後には、担当者に問題を知らせ、状況把握に必要なコンテキストやRunbook、Dashboard、Log、Silenceといった次のアクションへつなげる役割を担います。
つまりAlertは、単に"問題が起きた"と知らせる通知ではなく、障害予防と対応をつなぐ運用インターフェースに近いものです。
既存Alertの課題
既存のAlertで大きな課題だったのは3つです。作成が難しく、受け取ってもすぐに理解しづらく、誰が対応し管理すべきかが明確ではありませんでした。
Alertを作るのが難しい
当時、Alertの作成と通知にはGrafana、Slack、PagerDuty、CloudWatch、EventBridge、Lambdaなど複数のシステムが絡んでおり、データソースもNewRelic、VictoriaMetrics、Steampipe、OpenSearch、Druid、MySQLなど多岐にわたっていました。
Alertごとに作業方法も異なっていました。あるAlertはGrafanaからSlackへ直接送られ、別のものはCloudWatch Alarmの後ろにLambdaを付け、また別のものはSteampipeでAWSリソースの状態を照会して判断していました。PagerDuty連携が必要な場合には、追加の設定まで考慮する必要がありました。
問題は、組織レベルのコンベンションが不足していたことでした。どのAlertをどこで管理するのか、Slackだけに送るのかPagerDutyまで連携するのか、メッセージにはどのような説明やリンクを入れるのか、担当チームと通知経路をどこで管理するのか、といった整理が十分ではありませんでした。
結果としてAlertは必要になるたびに作られていましたが、時間が経つにつれて作成方法と管理方法は次第に断片化していきました。
Alertが見づらい
Alertを作成したからといって、実際のSlackメッセージが常に見やすい形だったわけではありません。作成者やシステムによってフォーマットや情報の質が異なり、タイトルが長く複雑なAlertもあれば、ValueやLabelsのような内部値がそのまま露出しているものもありました。
リンクがあっても何を最初に見るべきか明確でない場合があり、DashboardやLogボタンがあっても実際には連携されていないケースもありました。また、Alertのcontextが不足しており、担当者がサービス、クラスター、リソース、時間範囲を改めて探さなければならないことも多くありました。
障害対応中は、数分の差も大きく感じられます。そのためAlertを受け取った瞬間に、これは何の問題なのか、どれほど重要なのか、どのサービスとリソースの問題なのか、どこを最初に確認すべきか、次のアクションは何かをすぐに把握できる必要がありました。
Alertの対応責任を管理しづらい
Alertが鳴ったときに、誰が確認して対応すべきかも曖昧な場合がありました。担当チームや担当者が示されていないと、Alertを見た人がまず"これは自分が見るべきなのか?"、"誰に聞けばいいのか?"から判断しなければならず、障害対応中にはこの短い判断も対応の遅れにつながり得ます。
また、Alertが鳴った後の対応責任だけでなく、Alertそのものを誰が所有し管理するのかも重要でした。どのチームのサービスに関するAlertなのか、誰が条件を変更できるのか、メッセージやしきい値をどうレビューするのか、古くなったAlertを誰が整理するのかまで、あわせて見えるようにする必要がありました。
整理すると、改善したかった状況は次のとおりでした。
- Alertの作成・管理方法が断片化している
- Alertを受け取っても意味をひと目で把握しづらい
- Alertが鳴ったときに誰が確認し対応すべきか曖昧
- Alertそのものをどのチームが所有し管理すべきかも不明確
何をどのように改善したか
改善の方向性は3つでした。Alertの作成方法を標準化し、Slackメッセージで必要な情報を一貫した構造で提供し、Alertごとに担当者と通知経路が見えるように構造を整理することです。
Alertの作成・管理方法を標準化する
まずAlertの作成・管理方法を1つにまとめました。Alert ruleの評価と実行はGrafanaに統一し、Grafana・Slack・PagerDuty間の連携はTerraform Moduleで抽象化しました。また、すべてのAlert定義は社内alertsリポジトリのalerts/ディレクトリ配下でIaCとして管理するようにしました。Slackチャンネル接続、PagerDuty連携、メッセージフォーマット、共通ボタンの生成も共通モジュールで処理するよう整理しました。
これによりAlert作成者は、Alertパイプライン全体をすべて理解するよりも、どの条件を検知するか、どれほど重要なAlertなのか、誰が確認すべきか、対応にどのような情報が必要かにより集中できるようになりました。
リポジトリ内では、書き方、ディレクトリ構造、必要なフィールド、推奨コンベンションもあわせて管理しました。Alertをコードで管理することで、レビューや変更履歴もPRとコミット単位で残るようになりました。
Alertディレクトリ構造
すべてのAlertは{main-category}/{sub-category}/{severity}/{alert-name}.ymlという構造に従うよう整理しました。
たとえば次のようになります。
infra/kubernetes/critical/pod-unhealthy.ymldata/airflow/warning/task-failed.ymlfinops/aws/warning/cost-increase.yml
この構造により、ファイルの場所を見るだけで、どの領域のAlertなのか、どの程度の重要度として扱うのかを把握できるようにしました。また、特定領域のAlertを一覧したり、重複Alertや古いAlertを点検したり、Slackチャンネル、PagerDuty Service、CODEOWNERSを紐づける基準としても活用できるようにしました。
Alert定義の方法
各Alertファイルには、datasource、query、threshold、condition、messageといった情報をまとめて入れます。
新たに独自のDSLを作ったわけではありません。Grafana AlertがJSONとしてシリアライズされた内容をYAMLで表現したものに近く、そのおかげでGrafanaで定義できるAlertであれば、ほとんど同じ構造でIaC化できました。
最近ではLLMも活用しています。人が自然言語で"どの条件でどのメッセージのAlertを受け取りたいか"を説明すると、LLMが既存の例やコンベンションを参考にして、YAML形式のAlert定義のたたき台を作る方式です。これによりAlert作成者は、複雑なシリアライズ形式よりも、何を検知し、なぜ必要なのかにより集中できるようになりました。
Alertメッセージをすぐ理解し、対応できるようにする
Alertメッセージも1つのインターフェースだと考えました。障害対応中はメッセージをゆっくり解釈する余裕があまりないため、どのようなAlertが来ても、同じ位置で同じ種類の情報を確認できる必要がありました。
そこで、Slackメッセージの構造を一貫して整理した。タイトルにはAlert名、状態、重要度を含め、本文には人がすぐ理解できる説明と、担当者、チーム、サービス、リージョン、リソース名、主要なラベルを入れた。ボタンも共通ボタンと選択ボタンに分け、基本的には IaC、PagerDuty、Silence を提供し、必要なときだけ Runbook、Dashboard、Log を表示するようにした
共通ボタンはシステム側で自動生成してリンクされるようにし、Alertの状態変化もすべてSlack threadに残すようにした。誰がAcknowledgeしたのか、Slackで処理したのかPagerDutyで処理したのか、いつResolveされたのか、対応中にどんなメモが残されたのかまで、1つの流れとして見られるようにした
結果として、誰がどのAlertを作ってもSlack上での見え方が似たものになり、メンバーがどこを見るべきかをより速く判断できるようになった
Alertの責任構造を明確にする
Alertを見てすぐ理解できることと同じくらい重要だったのは、そのAlertを 誰が責任を持って確認すべきか が分かるようにすることだった
そこで、リソースのタグとラベル情報を対応フローに活用した。Alertごとに担当チームや担当者を直接指定するのではなく、サービス・チーム・リソース・環境といったメタデータを使い、Slackメッセージで適切な担当チームと担当者が自動的にmentionされるようにした
通知経路も同じルールの中で整理した。Alertの分類とseverityを基準にSlackチャンネル、PagerDuty Service、Escalation Policyが自動で紐づくようにし、WarningレベルのAlertはSlackチャンネルにのみ通知し、ユーザー影響や障害の可能性が大きいCritical AlertはPagerDuty Incidentまで作成するようにした
CODEOWNERSも併せて活用した。Alertファイルはカテゴリとサービス領域に応じてディレクトリで分け、各パスごとの担当チームをCODEOWNERSに指定することで、どのチームがどのAlert領域を所有しているのかをリポジトリ内で見えるようにした
結果として、Alertの責任は2つの地点で管理されるようにした。Alertが実際に発火したときはタグとラベルに基づいて担当チームと担当者がmentionされ、Alert定義を変更するときはディレクトリ構造とCODEOWNERSを基準に、どのチームの領域なのかを確認できる
Alert proxyの役割
この構造が実際に動作するには、中間でAlertを解釈して転送するレイヤーが必要だった。そこでGrafanaとSlack、PagerDutyの間に AWS Lambdaベースのproxy を置いた
GrafanaはAlert ruleを評価してwebhookを送る。proxyはこのwebhookを受け取り、category、severity、label、annotation、fingerprintといったAlert contextを解釈し、どのSlackチャンネルに送るか、どのPagerDuty Incidentを作るか、誰をmentionするか、どのボタンを付けるか、既存のSlack threadをどう更新するか、Ack/Resolve lifecycleをどう管理するかを決定する
つまりTerraform moduleとディレクトリ構造が 「Alertをどう定義するか」 を標準化したのだとすれば、proxyはその定義が実際の運用フローで同じように見え、同じように動作するようにつなぐ役割を担ったことになる
proxyのおかげで、Slackメッセージのフォーマット、担当者mention、PagerDuty連携、Slack thread更新、Ack/Resolveのやり取りを一箇所で一貫して管理でき、後からgrouped Alert、custom action button、AIエージェント連携、共通Alert modelといった改善も容易に拡張できた
さらに何が惜しかったのか
最初の改善以降、Alert定義はIaCで管理され、Slackメッセージと通知経路も一貫したルールの下で動作するようになった。しかしAlertシステムは、一度作って終わりのツールではなかった。1年近く運用してみると、Alertが増えるほど、同じAlertの中で発生した複数のinstanceをどう見せるか、繰り返し発生するAlert定義をどう管理するか、人がAlertを見た後に実際に何をできるようにするか、Alertシステム自体の安定性をどう確保して検証するか、といった新しい問題が見え始めた
同じAlertが複数の対象で同時に鳴ると見づらい
Alertを作りやすくなるにつれ、Alertの数も自然に増えた。このとき特に不便だったのは、1つのAlert ruleが複数の対象に対して同時に発火する場合だった
Grafanaでは同じruleでも、region、name、node、pod、app のようなlabel値が異なれば、それぞれを別のAlert instanceとして扱う。たとえばPod unhealthy Alertがあるとき、複数のpodが同時にunhealthy状態になると、podごとにAlert instanceが作られる
GrafanaにはすでにAlert Grouping機能があったが、単にgroupとしてまとめるだけでは不十分だった。重要なのは、まとめられたAlertの状態を運用者が理解しやすく見せることであり、group内にどの対象があるのか、誰がまだfiringなのか、誰がたった今resolveされたのか、新しく追加された対象があるのか、同じgroupの状態変化が1つの流れとしてつながっているのかが重要だった
繰り返されるAlert定義が増える
Alert定義が増えるほど、YAMLをコピーして少しずつ変えるやり方にも限界が見えてきた。SQS lag、CloudWatch error log、Pod OOM、ALB 5xx、Lambda error/throttleのようなAlertは繰り返し作ることになり、最初は既存ファイルをコピーして名前、query、threshold、labelだけを変えればよかった
しかしファイルが増えると共通の動作を直しにくくなり、同じ意図のAlertなのにdashboardリンク、labels構成、threshold表現が少しずつ異なる問題が起きた。そこで 繰り返されるパターンを再利用できる構造 が必要になった
Alertを見た後も次の行動まで距離がある
SlackメッセージにRunbook、Dashboard、IaC、PagerDutyボタンを付けたことは役に立ったが、実際の障害対応ではリンクだけでは十分でない場合が多かった。特にRunbookの効果は明らかだったが、すべてのAlertに良いRunbookを付け、常に最新の状態に保つのは簡単ではなかった
また実際の対応では、Kubernetesログの確認、pod状態の確認、rollout historyの確認、SQS・Lambda指標の確認、error logの確認のように、毎回似た調査作業が繰り返される。こうした作業はほとんどSlackメッセージの外で行われ、担当者はAlertからlabelとvalueを読み取り、別のツールへ移動して値を移したうえで、結果を再びSlack threadに共有しなければならなかった
結局、最初の改善によってAlertをより読みやすくはできたが、調査と対応は依然としてAlertメッセージの外に多く残っていた
モニタリングシステムにSPOFが増える
Alertシステムを整理する中で、SPOFになり得る地点も増えた。Alert ruleの定義とデプロイはalertsリポジトリとTerraformに集約され、Alert ruleの評価はGrafanaが担い、Slackメッセージ・PagerDuty Incident・Ack/Resolve lifecycleはproxyが管理するようになった
役割が明確になったのは良い変化だったが、その地点が失敗したときにAlertフロー全体が影響を受ける可能性も高まった。さらに難しいのは、こうした失敗が外からは分かりにくい場合があることだ。他のシステムの異常を知らせる経路が静かに止まると、実際に障害が発生しても誰も気づかない可能性がある
結局、「モニタリングシステムは誰がモニタリングするのか」 という問いにつながる
2回目の改善
最初の改善でAlertシステムの基本的な骨格は固まったが、Alertを作って送ることが簡単になると、運用しながら見るべき問題も変わった
2回目の改善では4つに集中した。同じAlert ruleで複数の対象が同時に発火したとき、状態変化を1つにまとめて一目で見せること、繰り返されるAlert定義を共通パターンとして再利用できるようにすること、Runbookを補完しながら、繰り返される調査と限定的な緩和作業をSlackボタンにつなげること、SPOFになり得るAlert定義・評価・通知経路の安定性を測定して検証すること
Grouped Alertをきちんと扱う
まず、grouped Alertの表現方法を改善した。同じAlert ruleで複数のinstanceが同時に発火したとき、それぞれを独立したメッセージとして送るとSlackチャンネルが煩雑になり、逆に1つのgroupだけにまとめてしまうと、実際にどのリソースに問題があるのか見落としやすい。
重要なのは、まとめつつ、まとめられた内側の状態を失わないこと。 Slackではgrouped Alertを代表メッセージ1つとして表示しつつ、現在影響を受けている対象も一緒に表示するようにした。新しい対象が追加されたり既存の対象が解決したりした場合、その変化は同じthreadに残すようにした。状態変化が一度に多く発生する場合は、複数の変化をbatchとしてまとめて表示するようにし、PagerDuty側もSlackで見るgrouped Alertと同じ問題を指すように合わせた。
結果として、同じ原因から発生した複数のAlertをSlack上で1つの流れとして見られるようになった。
繰り返されるAlert定義を減らす
コピーして少しずつ変える方法は、Alertの数が増えるほどメンテナンスコストとミスの可能性を高める。これを減らすために、global/templatesとmatrixを追加した。
global/templatesは繰り返されるAlert構造を共通templateとして定義する機能で、matrixは同じAlertを複数のリージョン、queue、datasource、serviceの組み合わせに展開し、複数のAlertとして生成する機能だ。
SQS queue lag、CloudWatch error log、複数クラスターのPod OOM、ALB 5xx、Lambda error/throttle、ECS memory/CPU/max-capacityのような繰り返しパターンをtemplate化し、queue名、region、cluster、threshold、dashboard変数のように変わる値だけをmatrixに入力するようにした。
これにより、共通のメッセージ構造、ボタン、Runbook/dashboardリンクの扱い、datasourceの扱いを1か所で修正できるようになり、Alertが増えるほど生じる不整合とメンテナンスコストも減らせた。
Slackメッセージからすぐに対応を始める
次に、Slackメッセージ内でできることを増やした。Runbookとdashboardリンクは依然として重要だが、毎回同じように繰り返す参照や限定的な緩和作業は、Slackメッセージ内で済ませたいと考えた。
そこで既存のボタンに加えて、custom action buttonを追加した。Alert YAMLのmessage.actionsにコマンドを定義するとSlackメッセージにボタンとして表示され、ボタンを押すとproxyが別のLambda invocationとしてコマンドを実行したうえで、誰がどのボタンを押したのかと実行結果を同じSlack threadにコメントとして残す。
このボタンで、ログ参照、Kubernetes rollout状態の確認、限定された状況でのrollout restart、単一コマンドの実行、複数コマンドの順次実行といった作業を提供できるようにした。
最も気を配ったのは安全性だった。ボタン名が!で終わる場合はSlack confirm dialogを表示し、${labels.namespace}、${labels.pod}のようなlabel値をコマンドに置換しつつshell quotingを適用してcommand injectionを防いだ。追加権限が必要な作業はactionRoleを通じてIAM roleをassumeするようにした。許可されていないroleの使用はfail-closedで処理し、webhookとSlackのインタラクションもそれぞれBearer token、HMAC-SHA256署名、replay protectionで検証した。
AIエージェントと連携する
Alertを受け取ったあとに必要な情報を集める過程も減らしたかった。そこで社内AIエージェントのabotをAlertの流れに接続した。
Slackメッセージのabotボタンを押すと、proxyがAlert名、説明、labels、values、IaCリンク、ユーザーがmodalで追加入力したcontextを集めて分析リクエストを送る。abotはボタンを押した人のOAuthベースのidentityで動作するようにし、Grafana・AWS・Kubernetesなど必要な情報を照会する場合でも、ユーザーが実際に閲覧できる範囲内だけを取得するようにした。
分析結果は同じSlack threadにコメントとして残す。どの指標とログを確認したのか、どの可能性を優先的に疑えるのか、RCAにまとめるべき手がかりは何かまで一緒に残すようにし、そのおかげで複数のシステムを開いて情報を集め直す時間を減らせた。
モニタリングシステムをモニタリングする
2回目の改善では、Alertを定義し、評価し、配信するプロセス自体も観測対象に含めた。
まずproxyの運用指標を集めた。Ackまでにかかった時間、Resolveまでにかかった時間、現在発生中のAlert数、Alertがどれくらい長く発生しているか、同じAlertが再度鳴った回数といった指標を収集し、Lambdaがtimeoutに近づくと検知するwatchdogも追加した。proxyが処理中に失敗した場合は、full stack traceと元のevent payloadを一緒に残すようにした。
しかし、proxyが直接通知する方式には限界があった。proxyがその通知すら送る前に死んでしまうと、本来送られるべきAlertがそのまま欠落し得るからだ。
そこでproxyの外側に、互いに異なるシステムに依存する検知装置を置いた。1つはGrafanaで、proxyが送ったmetricをmonitoringドメインのAlertとして評価し、異常を表面化させるようにした。ただしGrafanaとVictoriaMetricsが同じEKS上にあるため、EKSやGrafanaが丸ごと落ちると検知できない。
もう1つはdeadman switchだった。Grafanaは正常時に/api/healthをheartbeatとして送出し、そのheartbeatはEKSとは独立したCloudWatchが受け取る。CloudWatch alarmはheartbeatが途絶えたりmissingになったりすると障害と判断し、この場合はGrafanaやproxyを経由せずPagerDutyとSlackへ直接通知するようにした。
検知する側と検知される側を別々のインフラに置いた形であり、そのおかげでEKSとCloudWatchが同時に落ちない限り、モニタリングシステムのダウンを知ることができるようになった。
今後の改善課題
2回目の改善は終えたが、まださらに改善すべき部分が残っている。
収集した運用指標をきちんと活用する
proxyがAlert運用指標を収集することで、どのチャンネルでどのAlertがどれくらい頻繁に鳴っているのか、特定の担当者やチームにAlertが過度に集中していないか、何のインタラクションもなくfiringとauto resolveだけを繰り返すAlertはないか、といった質問にデータで答えられるようになった。
しかし、データを見られることと、それを根拠に実際のAlertを磨き込むことは別の話だ。まだthreshold調整、似たAlertのグルーピング、不要なAlertの削除、noisy alertの削減、認知時間と解決時間を実際に短縮する方向での改善には本格的に取り組めていない。
Alert IaCの改善
現在Alert定義はalertsリポジトリからCI/CDでデプロイされているが、まだGrafana Terraform providerのgrafana_rule_groupリソースに依存している。問題は、ruleを1つ変えただけでもPR上ではrule group全体が変わったように見えてdiffが読みづらいこと、そしてinterval_secondsがrule group単位なので、Alertごとに異なる評価周期を持たせるにはgroupを細かく分割しなければならないことだ。
最近Grafanaには、Alert ruleをKubernetesリソースのように扱う新しいalerting APIができ、Terraform providerにもgrafana_apps_rules_alertrule_v0alpha1リソースが追加された。まだalphaなので今すぐの導入は見送っているが、stableになれば既存のgrafana_rule_groupから移行することを検討したい。
期待している点は明確だ。rule groupとruleを分離して定義し、ruleを1つ変えただけならその変更だけがきれいに現れるdiffを得て、ruleごとに評価周期を細かく調整し、モニタリングリソースをより効率的に使うこと。
おわりに
最初の目標は単純だった。Alertをより簡単に作れるようにし、受け取ったときにひと目で理解できるようにし、誰が責任を持つのかを明確にすること。
第1次改善では、Alert定義をIaCに集約し、Slackメッセージと配信経路を標準化し、担当者mentionとCODEOWNERSを連携し、proxyを通じたSlack/PagerDuty lifecycle管理を整理した。
運用を続ける中で明らかになった問題は、2次改善で手を入れた。同じルールから大量に発生するアラートを1つにまとめて表示し、繰り返しの定義をtemplateとmatrixで減らし、Slackメッセージ内から調査と緩和を始められるようにし、モニタリングシステム自体が停止したときに気づける仕組みも用意した
そのおかげで、アラートを作成し、送信し、対応する作業は以前より楽になった
ただし、データを見られるようになったことと、そのデータを根拠に実際の運用を改善することは別の話だ。これまでがアラートシステムを標準化し、測定できるようにする作業だったとすれば、これからはその数値を見ながら実際に減らし、磨き込む作業が残っている
この記事には、分量の都合で含められなかった内容があります。さらに詳しい内容が気になる方は、元記事もあわせて読んでみることをおすすめします。
まだコメントはありません。