はじめに

平素は大変お世話になっております。
クイックガードのパー子です。

弊社ではオンコールとインシデントの管理に PagerDuty を利用しています。

弊社の監視システムにおいて、アラーティングについてはメールや Slackなどの多チャネルに同報しているものの、エスカレーション体制は PagerDuty に強く依存しています。

PagerDuty のダウンタイムや遅延は弊社の業務品質を直接的に低下させるため、そのリスクに備えて我々は PagerDuty をバックアップする仕組みを構築してきました。

今回の記事では、その仕組みをご紹介します。

バックアップの仕組み

バックアップには、PagerDuty の類似サービスである Atlassian社の Opsgenie を採用しました。

以下の 2つの観点でバックアップします。

  1. PagerDuty自体の死活を監視し、異常時に Opsgenie へ通知する。
  2. PagerDutyダウン中に発生したインシデントのアラートを Opsgenie が代わりに受け取る。

PagerDuty が復旧するまでの一時凌ぎとしての役割を果たせればよいので、Opsgenie側のエスカレーション・ポリシーは簡易的なものとしています。

PagerDuty自体の死活

PagerDuty は Atlassian Statuspage による ステータス画面 を公開しています。

Opsgenie に Statuspageインテグレーション が用意されているので、これを使ってステータスを追跡します。

ドキュメントのとおりに、

  1. Opsgenie側でインテグレーションを追加する。
  2. Opsgenie側の Webhook受け口となる URL (※) をコピーする。
  3. PagerDuty のステータス画面で、2 の URL を登録する。

とすれば OK です。

※ インテグレーション追加画面の上部のガイドに表示されている URL のこと。

これで PagerDuty で発生した障害が Opsgenie上に Alert として記録されるようになります。
また、障害ステータスが更新されると、その詳細が Note欄 に追記されていきます。

(Statuspage上の該当の障害: ID tbppngyg2qzl)

なお、Statuspage には Postmortem機能 があり、解決した障害に対してポストモーテムを公表することができます。

ところが、このポストモーテムは該当の Alert の Note には追記されず、まったく別の Alert として作成されてしまいます。

今回の用途ではポストモーテムの通知を受け取る必要はないので、Notification policy で遮断することにしました。
インテグレーションの設定がデフォルトのままであれば、以下の条件でポストモーテムを絞り込めます。

  • Message + -- + Starts With + [Statuspage]postmortem -
  • Source + -- + Equals + Atlassian StatusPage

PagerDutyダウン中のインシデント

弊社ではサービス間連携のためのワークフロー自動化ツールとして n8n を運用しています。
これを使って PagerDuty がダウンしている間に発生したインシデントを Opsgenie へアラーティングすることにしました。

大枠の仕組みは以下のとおりで、

まず、監視システムで検知したイベントは、各種インテグレーション を用いて PagerDuty に自動で Incident が作成されるようにしています。

それに加えて、さらに n8n に対してもイベントを送信するようにしました。

n8n は PagerDuty に Incident が作成されているかをチェックし、作成されていればワークフローは終了。
一方、PagerDuty がダウンしているなどの理由で Incident の作成に失敗している場合は、Opsgenie にアラートを飛ばすように設定します。

ワークフローとしての収まりで言えば、監視システムからのイベントの送り先を n8n に一本化して、PagerDuty の Incident作成も n8n上で実行したほうがスッキリするところです。
しかし、あくまで Opsgenie はバックアップに過ぎないので、アラーティングの本流に対して本質ではない障害点を持ち込まないようにあえて別経路に分けました。

例: Amazon CloudWatch Alarm

Amazon CloudWatch Alarm に対する実際の設定例を見てみましょう。

連携方法

CloudWatch との連携には、Amazon SNS を経由してそれぞれ以下の連携機構を使います。

連携用の Webhook URL がそれぞれ発行されるので、CloudWatch Alarm の通知先SNS のサブスクリプション (HTTPSエンドポイント) に 2つとも追加されている状態にします。

n8nワークフロー

AWS SNS trigger を起点として、以下のワークフローを組みます。

n8n ではワークフローのステップの 1つ1つを Node と呼びます。

今回のワークフローは以下の Node で構成されます。

Node名種類処理内容
1AWS SNSAWS SNS triggerSNS からのイベント受信を契機にワークフローを開始する。
2Parse JSON messageSet#1 で受信したペイロードの Message属性をパースする。
3If state is ALARMIfCloudWatch Alarm の状態が ALARM かどうかを判定する。
4a (#3 の判定結果が true の場合)Wait 15sWait15秒ほど待つ。
4b (〃 false の場合)NOP1No Operation, do nothing何もせずワークフローを終了する。
5Get PD incidentsPagerDutyPagerDuty に作成されているはずの Incident を取得する。
6Count incidentsCode#5 で取得した Incident の数をカウントする。
7If incident existsIf#6 のカウント結果が 1以上か (= Incident が存在したか) 判定する。
8a (#7 の判定結果が true の場合)NOP2No Operation, do nothing何もせずワークフローを終了する。
8b (〃 false の場合)Alert to OpsgenieHTTP RequestAPI を叩いて Opsgenie にアラートを飛ばす。

[1. AWS SNS]

ドキュメント にしたがって Node を設定すると、

入力データとして SNS から以下の 形式 のイベントが飛んでくるようになります。

[
  {
    "Type": "Notification",
    "Subject": "ALARM: \"CPUUtilization\" in Asia Pacific (Tokyo)",
    "Message": "{\"AlarmName\":\"CPUUtilization\",\"AlarmConfigurationUpdatedTimestamp\":\"2023-02-20T02:46:45.014+0000\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 1 out of the last 1 datapoints [26.47127222953716 (20/02/23 07:32:00)] was greater than or equal to the threshold (5.0) (minimum 1 datapoint for OK -> ALARM transition).\",\"StateChangeTime\":\"2023-02-20T07:37:38.650+0000\",\"Region\":\"Asia Pacific (Tokyo)\",\"OldStateValue\":\"OK\",\"Trigger\":{\"MetricName\":\"CPUUtilization\",\"Namespace\":\"AWS/EC2\",\"StatisticType\":\"Statistic\",\"Statistic\":\"AVERAGE\",\"Unit\":null,\"Dimensions\":[{\"value\":\"i-0123456789abcdef\",\"name\":\"InstanceId\"}],\"Period\":300,\"EvaluationPeriods\":1,\"DatapointsToAlarm\":1,\"ComparisonOperator\":\"GreaterThanOrEqualToThreshold\",\"Threshold\":5.0,\"TreatMissingData\":\"\",\"EvaluateLowSampleCountPercentile\":\"\"},...}",
    "Timestamp": "2023-02-20T07:37:38.703Z",
    ...
  }
]

これがそのまま次の Node へ出力されます。

[2. Parse JSON message]

CloudWatch Alarm の詳細情報は Message属性に JSON形式で格納されているので、これを後続のステップで扱いやすいようにパースします。

具体的には、以下の Value をセットします。

データ型NameValue
StringMessage{{ JSON.parse($json.Message) }}

また、Message 以外の属性は不要なので、“Keep Only Set” を ON にします。

結果として、この Node の出力は以下のようになります。

[
  {
    "Message": {
      "AlarmName": "CPUUtilization",
      "AlarmConfigurationUpdatedTimestamp": "2023-02-20T02:46:45.014+0000",
      "NewStateValue": "ALARM",
      "NewStateReason": "Threshold Crossed: 1 out of the last 1 datapoints [26.47127222953716 (20/02/23 07:32:00)] was greater than or equal to the threshold (5.0) (minimum 1 datapoint for OK -> ALARM transition).",
      "StateChangeTime": "2023-02-20T07:37:38.650+0000",
      "Region": "Asia Pacific (Tokyo)",
      "OldStateValue": "OK",
      "Trigger": {
        "MetricName": "CPUUtilization",
        "Namespace": "AWS/EC2",
        "StatisticType": "Statistic",
        "Statistic": "AVERAGE",
        "Unit": null,
        "Dimensions": [
          {
            "value": "i-0123456789abcdef",
            "name": "InstanceId"
          }
        ],
        "Period": 300,
        "EvaluationPeriods": 1,
        "DatapointsToAlarm": 1,
        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
        "Threshold": 5,
        "TreatMissingData": "",
        "EvaluateLowSampleCountPercentile": "",
        ...
      }
    }
  }
]

[3. If state is ALARM]

CloudWatch Alarm の状態が ALARM かどうか判定します。

データ型Value 1OperationValue 2
String{{ $json.Message.NewStateValue }}EqualALARM

条件にマッチすれば Node #4a “Wait 15s” に、マッチしなければ #4b “NOP1” に遷移します。

[4a. Wait 15s]

PagerDuty の遅延に備えて、15秒ほど待機します。

PagerDuty側で処理に遅延が生じ Incident の作成に時間がかかった場合、当該Incident が作成される前にその存在の確認を始めてしまうことを防ぐため、待ち時間を設けています。

なお、15 という数字に特に意味はなく、誤検知をほどほど抑制しつつ真陽性の場合の発報を遅らせない程度の適度な秒数として、この値としました。

[5. Get PD incidents]

PagerDuty の API を叩いて、トリガーとなったイベントにより作成された Incident を取得します。

リスト取得の対象期間はイベントの発生日時以降です。

また、PagerDuty の Alertalert_key という重複排除のための属性を持っており、PagerDuty - CloudWatchインテグレーションによって生成される Alert にはこの属性に CloudWatch Alarm の名前がセットされます。

この属性を使って目的の Incident を 絞り込む ことにします。

incident_key string
Incident de-duplication key. Incidents with child alerts do not have an incident key; querying by incident key will return incidents whose alerts have alert_key matching the given incident key.

Incident取得の具体的なパラメータは以下です。

パラメータ
Incident Key{{ $json.Message.AlarmName }}
Since{{ $json.Message.StateChangeTime }}

後続のステップで必要な情報は目的の Incident が存在するかどうかだけなので、“Operation” を Get Many、“Limit” を 1 とし、条件にマッチした Incident を 1件だけ出力するようにします。

なお、何らかの理由で Incident の取得に失敗した場合でも次のステップに進みたい (そして、Opsgenie にアラートを飛ばしたい) ので、“Always Output Data” と “Continue On Fail” を ON にしておきます。

そのようなケースでは空のアイテムが出力されます。

[
  {}
]

[6. Count incidents]

#5 で取得した Incident の数をカウントします。

“Mode”Run Once for All Items とし、コードは 公式のスニペット を参考に、以下とします。
(n8n の データ構造のルール にしたがって、"json": {...} というキーで本来の出力内容を包む必要があります。)

return [
  {
    json: {
      results: Object.keys(items[0].json).length === 0 ? 0 : items.length,
    }
  }
];

#5 で取得する Incident を 1件に絞っているため、results は 0 か 1 になります。

[
  {
    "results": <0|1>
  }
]

[7. If incident exists]

#6 の結果が 1以上かどうかを判定します。

データ型Value 1OperationValue 2
Number{{ $json.results }}Larger or Equal1

条件にマッチすれば Incident が正常に作成されていたということになるので、Node #8a “NOP2” に遷移してワークフロー終了。
マッチしなければ #8b “Alert to Opsgenie” に遷移します。

[8b. Alert to Opsgenie]

Opsgenie にアラートを飛ばします。

n8n には Opsgenie とのインテグレーションは用意されていないので、汎用の HTTP Request Node を用いて API を直に叩きます。

リクエスト・ヘッダには Opsgenie の APIキーをセットします。

インシデントを識別しやすくするために、リクエスト・ボディには以下のパラメータを指定しました。

パラメータ
message{{ $node["Parse JSON message"].json.Message.NewStateReason }}
description{{ JSON.stringify($node["Parse JSON message"].json.Message.Trigger) }}
alias{{ $node["Parse JSON message"].json.Message.AlarmName }}

$node[Node名].json. というコードで、指定の Node の出力を参照できます。
ここでは CloudWatch Alarm の詳細情報を利用したいので Node #2 “Parse JSON message” の出力を参照しています。

特別なオプションは不要です。

まとめ

PagerDuty の障害に備えて、Opsgenie をバックアップとして運用する仕組みをご紹介しました。

この仕組みにより、PagerDuty に何かあった場合でも安心してオンコール体制を持続させることができるようになりました。

また、Amazon CloudWatch Alarm に対する具体的な設定例をお見せしました。

CloudWatch Alarm以外の他の監視システムではインテグレーションや PagerDuty Incident の探索条件が個々に違ってきますが、大枠のスキーム自体は同じものを適用できます。

以上、今後ともよろしくお願い申し上げます。


当記事の図表には PagerDuty のロゴOpsgenie のロゴMaterial Design Icons を使用しています。