はじめに

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

弊社ではオンコール/インシデント管理に PagerDuty を活用しています。
検知された障害は弊社が一次受けして調査、復旧を実施するご契約が多いのですが、中には、弊社スタッフだけでなくお客様にも直接 PagerDuty から障害通知を飛ばしてほしいというご依頼をいただくこともあります。

その際、「平日の営業時間内は通知してほしくない。祝祭日を含めた営業時間外だけ通知がほしい。」という条件をセットでいただくことが多いのですが、これは PagerDuty単体では実現できず、カレンダーに基づく制御を作り込む必要があります。

弊社ではどうやってこれを実現しているのか、その仕組みをご紹介します。

要件

まずは要件を決めます。

お客様からは以下のようなストーリーでご要望をいただくことが多いです。

営業時間内はみんな監視ダッシュボードを見ていて障害にはすぐに気づけるので、スマホにプッシュ通知が飛んできたり架電されたりするのはウザい。
なので、営業時間内は通知せずに、時間外だけ通知を飛ばしてほしい。
ちなみに営業時間とは「土曜、日曜、祝祭日会社独自の休日」以外の 09:00〜18:00 のことです。

これを実現するためには、

  • 時刻を判定できること
  • 曜日を判定できること
  • 日本の祝祭日を判定できること
  • 独自のカレンダーを定義できること

というあたりができればよさそうです。

時刻、曜日に基づく判定は PagerDuty にも備わっていそうですが、祝祭日の判定はかなり厳しそうな予感…。
独自カレンダー機能があればそちらでカバーできるのですが、果たしてそのような機能はあるのでしょうか。

おさらい: PagerDuty の通知の仕組み

実現可否の調査の前に、PagerDuty の通知の仕組みをおさらいします。

まず、監視ツールから受け取った検知イベントは、関連するイベントごとに 1つのインシデントにまとめられ、それを契機としてオンコール担当者へ通知されます。

通知の宛先はエスカレーション・ポリシーで定義した人となり、この際、インシデントの緊急度 (高 / 低) に応じてチャネル (※) や エスカレーション の有無が変わります。(下表を参照)

チャネルには各メンバーがそれぞれ自分の希望する通知手段 (メール、プッシュ通知、電話など) を自分で登録します。

※ チャネル: 公式ドキュメントにはそのような用語は出てきませんが、概念を理解しやすいように、本記事では緊急度に応じた Notification Rules (通知手段と宛先デバイスのセット) のことをチャネル (Channel) と呼びます。

緊急度 通知先 エスカレーション
Highチャネル する。
一次受けメンバーが応答しない場合は、二次受け、三次受けと宛先を変えていく。
Lowチャネル しない。
一次受けメンバーに通知してお終い。

ちなみに、紛らわしいのですが アラート (Alert)通知 (Notification) は別物です。
アラートは「監視ツールから PagerDuty へ送出されたイベント」、通知は「オンコール担当者にインシデントの発生を知らせること」を指します。

Web UI で設定できるか調べてみた

PagerDuty にログインし、対象の Service » Responseタブ » Assign and Notify » How should responders be notified? で通知ポリシーを指定できます。

選択肢 通知先 (※)
High-urgency notifications, escalations as needed Highチャネル
Low-urgency notifications, do not escalate Lowチャネル
Dynamic notifications based on alert severity アラートの重大度 (Severity) に応じて決定
Based on support hours 任意に定義可能な営業時間帯 (Support hours) の内外で変動

※ 正確には、通知先ではなくインシデント緊急度の決定ポリシーと言えます。
 インシデントの緊急度によってチャネルが決まるので。

Based on support hours

日時に関係しそうな Based on support hours という選択肢があるので見てみます。

曜日 + 時刻で営業時間帯を定義して、時間帯の内外それぞれに別々の通知ポリシーを設定できそうです。

なので、

  • 曜日: 月〜金 / 時刻: 09:00〜18:00
  • 営業時間内: Low-urgency / 時間外: High-urgency
  • オンコール担当者の Lowチャネルを 空に (= 通知手段を登録しない)

とすることで、

時間帯 挙動
月〜金曜日の 09:00〜18:00 通知しない
それ以外 Highチャネルに通知する

という挙動を実現できることがわかりました。

ただし、あくまで曜日による判定であり祝祭日が考慮されていないので、例えば「成人の日」は (1月の 第2月曜日 なので) 通知されない状態となります。
(これでは困ってしまいます。)

例外を設定できるか?

PagerDuty の設定画面を探し回っても、他に例外ルールを重ねたり独自のカレンダーを定義できる箇所は見当たりませんでした。

結論

PagerDuty では祝祭日を考慮した通知ポリシーを設定することは不可能と言えそうです。

我々はお客様の要望を叶えるため、PagerDuty に頼らず自前で通知ポリシーを制御する仕組みを構築することに決めました。

どう実現するか?

「PagerDuty の API を叩いて通知ポリシーの Low <-> High を切り替えるスクリプトを作成し、これを毎日の始業、終業時刻に定期実行する」という素朴な作戦を採用しました。

休業日 or 営業日の判定は、独自カレンダーを参照するようにします。

API を叩く

Terraform pagerduty_service を利用します。

https://www.terraform.io/docs/providers/pagerduty/r/service.html

Low <-> High の切り替えは incident_urgency_ruleブロックでやれそうです。

You may specify one optional incident_urgency_rule block configuring what urgencies to use.

--- Lowチャネル      2020-07-28 16:42:07.000000000 +0900
+++ Highチャネル     2020-07-28 16:42:58.000000000 +0900
@@ -1,4 +1,4 @@
 incident_urgency_rule {
   type    = "constant"
-  urgency = "low"
+  urgency = "high"
 }

休業日を判定する

祝祭日の判定については、すでに jpholidayp などのツールがありますが、今回はこれらをそのまま使うことはできません。

というのも、判定リストには公的な祝祭日しか含まれておらず、年末年始やお盆、会社創立記念日など独自のカレンダーに基づいた判定ができないためです。
また、jpholidayp は定休の曜日が土日で固定されているので、業種によっては都合が悪そうです。

というような背景があって、我々は汎用的に使える自作の判定コマンド wholidisuka (ワタシハ ホリデイ デスカ) を用意することにしました。
(MITライセンスで公開を予定しております!)

休業日の判定ソースは以下のとおりで、

  1. 祝祭日は holiday_jp gem を参照する。
  2. 定休の曜日を引数で指定できる。
  3. 独自カレンダーで休業日/営業日を上書きできる。

判定ソースの優先度は 3 が最も強く、つまり、1 + 2 で定義される祝祭日や定休日に対してさらに休業日を追加したり、逆にキャンセルしたりということを柔軟に行える仕組みです。

Exitステータスで休業日/営業日を判定する (0: 休業日 / 1: 営業日) ようになっているので、以下のように使えます。

#!/bin/bash

# 定休日は土・日。独自カレンダーは `my-calendar.yml` にて定義。
wholidisuka --regular 'sun,sat' --override my-calendar.yml

case $? in
  0)
    # 休業日の場合の処理
    ;;
  1)
    # 営業日の場合の処理
    ;;
  *)
    # エラー時の処理
    ;;
esac

ジョブの内容

毎日の始業時刻 (09:00) と終業時刻 (18:00) に、それぞれのジョブを実行します。

その日が休業日なのか、それとも営業日なのかに応じて、以下の表のとおりに incident_urgency_rule を切り替えます。

09:00 18:00
休業日 High High
営業日 Low High

ジョブを作成する

道具は揃ったので、それらを組み合わせてジョブを作っていきます。

Terraform

まずは Terraform の マニフェスト? (= tfファイル一式。正式な呼び名がわからず…。) を準備します。

ディレクトリ構成
./
├── main.tf
└── module/
    ├── services.tf
    └── variables.tf

※ モジュールについて: この例では対象サービスが 1つだけなのでメリットがないのですが、実際には弊社では複数のサービスをこの仕組みで運用しているのでモジュール化しています。

main.tf
variable "urgency" {}

provider "pagerduty" {}

data "pagerduty_escalation_policy" "default" {
  name = "{{ YOUR_ESCALATION_POLICY_NAME }}"
}

module "myservice" {
  source            = "./module/"
  name              = "{{ SERVICE_NAME }}"
  escalation_policy = "${data.pagerduty_escalation_policy.default.id}"
  urgency           = "${var.urgency}"
}
module/services.tf
resource "pagerduty_service" "default" {
  name              = "${var.name}"
  description       = ""
  escalation_policy = "${var.escalation_policy}"
  alert_creation    = "create_alerts_and_incidents"

  incident_urgency_rule {
    type    = "constant"
    urgency = "${var.urgency}"
  }
}
module/variables.tf
variable "name" {}
variable "escalation_policy" {}
variable "urgency" {}

{{ SERVICE_NAME }} などは自分の環境に合わせて埋めてください。

ジョブ

始業、終業それぞれの時刻に実行するジョブは以下のとおりです。

始業
#!/bin/bash

set -e

cd /path/to/terraform

rm -rf .terraform terraform.tfstate terraform.tfstate.backup

terraform init
terraform import "module.myservice.pagerduty_service.default" "{{ MYSERVICE_ID }}"

wholidisuka --regular "sun,sat" --override my-calendar.yml && :

ret=$?

case "${ret}" in
  0)
    echo "Today is holiday."
    
    terraform plan -var "urgency=high"
    terraform apply -auto-approve -var "urgency=high"
    ;;
  1)
    echo "Today is weekday."
    
    terraform plan -var "urgency=low"
    terraform apply -auto-approve -var "urgency=low"
    ;;
  *)
    echo "Unknown error ${ret}."
    
    exit 2
    ;;
esac
終業
#!/bin/bash

set -e

cd /path/to/terraform

rm -rf .terraform terraform.tfstate terraform.tfstate.backup

terraform init
terraform import "module.myservice.pagerduty_service.default" "{{ MYSERVICE_ID }}"

terraform plan -var "urgency=high"
terraform apply -auto-approve -var "urgency=high"

これらを適当なジョブ・マネージャでスケジュール実行すれば OK です。

  • すでに存在するサービスに対する変更なので、まず状態をインポートする。
  • 意図しない State が残って悪さをしないように、キッチリ削除してから実行する。

というあたりがポイントです。

まとめ

以上、PagerDuty において、自社の独自カレンダーに基づいて通知の有無を切り替える仕組みを紹介しました。

このような切り替えは PagerDuty のみでは実現できないため、弊社では休業日判定のための自作ツールと Terraform を組み合わせて、毎日の始業、終業のタイミングで通知チャネルを切り替えるようにしました。

切り替えジョブの実行成否はよく監視しておく必要がありますが、今のところ大きな負担もなく運用できています。:)

また、休業日判定ツールは近日公開予定です。
公開したらこのブログでも改めてご紹介しますので、ご期待ください。

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


当記事の図表には Font Awesome のアイコンを使用しています。