PagerDuty Statusページの移行に対応する
はじめに
平素は大変お世話になっております。
クイックガードのパー子です。
弊社ではインシデント管理に PagerDuty と Opsgenie を併用 しています。
PagerDuty を主系、Opsgenie を副系として運用しており、PagerDuty の Statusページ をサブスクライブして異常が発生した場合は Opsgenie に通知を飛ばすようにしています。
ところが最近、この Statusページが Atlassian の Statuspage から PagerDuty自身の同等サービス に移行されました。
(公式のお知らせ)
この移行に伴い、以前に使用していた Opsgenie の Statuspageインテグレーション が使えなくなったため、挙動はそのままに新たな仕組みに切り替えました。
この記事では、その新しい仕組みを紹介します。
仕組みの概要
Atlassian版と同じく、PagerDuty の Statusページも Webhook を提供しています。
新たな仕組みでも、引き続き Webhook を利用して通知を受け取ります。
Opsgenie の Alert の操作は、愚直に Alert API を叩いて行うことにします。
実行環境として AWS Lambda を採用し、Function URL で Webhook を受けます。
Webhook のペイロードに応じて、適切な API を叩き分けて Alert を作成、更新、またはクローズします。
実装
具体的な実装手順を解説します。
Opsgenie
まず、Opsgenie のインテグレーションを作成します。
作成するインテグレーションは APIインテグレーション です。
インテグレーションを作成すると APIキーが発行されるので、控えておきましょう。
後ほど AWS Lambda の設定で使用します。
インテグレーションを作成したら、ペイロードの内容に応じて実行される アクション のルールを定義します。
なお、今回の実装では Lambda側でイベントの中身を見て API を叩き分けるので、Filter の設定は不要です。
Create Alert
“Alert Fields” を次のとおり定義します。
項目 | 値 |
---|---|
Message | [PagerDuty] {{extraProperties.status}} - {{message}} |
Alias | {{extraProperties.incident_id}} |
Priority | {{priority}} |
Entity | {{entity}} |
Source | {{extraProperties.status_page}} |
Tags | incident_id: {{extraProperties.incident_id}} |
Actions | {{actions}} |
Description | {{description}} |
Extra Properties | {{extraProperties}} |
User | {{extraProperties.status_page}} |
Note | {{description}} |
なお、AWS Lambda で Create Alert API を実行する際、リクエスト・フィールドと Webhookペイロード を次のようにマッピングするものとします。
リクエスト・フィールド | Webhookペイロード |
---|---|
message | title |
description | message |
details.ends_at | ends_at |
details.href | href |
details.incident_id | href の末尾の ID |
details.next_update_ms | next_update_ms |
details.post_type | post_type |
details.reported_at | reported_at |
details.service | services[].service_name のリスト |
details.severity | severity |
details.starts_at | starts_at |
details.status | status |
details.status_page | status_page |
details.*
は任意に追加できるカスタム属性であり、上記 “Alert Fields” の中で extraProperties
として参照できます。
例として、以下のイベントが発生したとき、
event.json
{
"ends_at": null,
"href": "https://subdomain.trust.pagerduty.com/incident_details/PXXXXXX",
"message": "We are currently working on the issue.",
"next_update_ms": 900000,
"post_type": "incident",
"reported_at": "2023-05-15T20:47:12Z",
"services": [
{
"service_name": "Checkout",
"severity": "minor"
}
],
"severity": "minor",
"starts_at": null,
"status": "investigating",
"status_page": "Acme Corp",
"title": "Checkout is temporarily unavailable"
}
各フィールドは次のように Alert にマッピングされます。
Close Alert
“Alert Fields” は次のとおりです。
項目 | 値 |
---|---|
Alias | {{alias}} |
User | {{user}} |
Note | {{note}} |
Close Alert API のリクエスト・フィールドと Webhookペイロードは次のようにマッピングします。
リクエスト・フィールド | Webhookペイロード |
---|---|
user | status_page |
source | status_page |
note | message |
AWS Lambda
次に、AWS Lambda で Function を作成します。
Function URL
PagerDuty からの Webhook を受け取るために、Function URL を有効化します。
この Function に固有の URL が払い出されるので、控えておきます。
コード
ランタイムは Ruby 3.2 で、ハンドラは source.Handler.process
です。
source.rb
require 'json'
require 'net/http'
require 'uri'
class Handler
class << self
def process(event:, context:)
puts(event)
payload = JSON.parse(event['body'])
case payload['post_type'].to_sym
when :maintenance, :incident
handler = \
case payload['status'].to_sym
when :resolved, :completed
ApiClient::Alert::Closer
else
ApiClient::Alert::Creater
end
handler.new(payload).call
end
:ok
end
end
end
module ApiClient
module Alert
class Base
BASE_URL = 'https://api.opsgenie.com/'; private_constant :BASE_URL
API_KEY = ENV.fetch('OPSGENIE_API_KEY'); private_constant :API_KEY
def initialize(payload)
@payload = payload
@incident_id = payload['href'].split('/').last
@service_names = payload['services'].map{|x| x['service_name']}.join(', ')
end
def call
res = Net::HTTP.post(url, data.to_json, {
'Authorization': "GenieKey #{API_KEY}",
'Content-Type': 'application/json'
})
unless res.code == '202'
puts(
fail_log(res)
)
end
end
private
def url
@url ||= URI.join(BASE_URL, path)
end
def path
raise NotImplementedError
end
def data
raise NotImplementedError
end
def lookup(key)
v = @payload[key.to_s]
v.nil? ? '<null>' : v.to_s
end
def fail_log(res)
JSON.generate({
url: url.to_s,
status: res.code,
message: res.body
})
end
end
class Creater < Base
private
def path
'/v2/alerts'
end
def data
{
message: lookup(:title),
description: lookup(:message),
details: {
ends_at: lookup(:ends_at),
href: lookup(:href),
incident_id: @incident_id,
next_update_ms: lookup(:next_update_ms),
post_type: lookup(:post_type),
reported_at: lookup(:reported_at),
service: @service_names,
severity: lookup(:severity),
starts_at: lookup(:starts_at),
status: lookup(:status),
status_page: lookup(:status_page)
}
}
end
end
class Closer < Base
private
def path
"/v2/alerts/#{@incident_id}/close?identifierType=alias"
end
def data
{
user: lookup(:status_page),
source: lookup(:status_page),
note: lookup(:message)
}
end
end
end
end
Alert の作成とクローズでは APIエンドポイントもリクエスト・ボディも異なるため、“status” の値に基づいて処理を分岐します。
また、公式ドキュメント には記載がありませんが、ポストモーテムの場合は “post_type” に postmortem
という値がセットされます。
ポストモーテムは無視したいので、そのようなイベントは処理をスキップします。
Alert の作成において、“Alias"フィールドは重複排除のための識別子として機能します。
Create Alert API を複数回呼び出した場合、Alias が同じならば新しい Alert は作成されず、既存の Alert が更新されるだけとなります。
(この際、メッセージは “Note"欄に追記されていきます。)
APIキー
前段階で控えておいた Opsgenie の APIキーを、環境変数 OPSGENIE_API_KEY
にセットします。
PagerDuty Statusページ
最後に、AWS Lambda の Function URL を PagerDuty Statusページ の Webhook に登録します。
注意点として、現状では登録した URL を解除する機能が提供されていません。
そのため、解除したい場合は PagerDuty社に問い合わせる (?) か、あるいは解除せずに当該URL を破棄することになるため、恒久的に使い続けたい URL を登録するのは避けたほうがよいでしょう。
問題点
以上の実装で以前と同じ挙動を実現できたのですが、しかし、PagerDuty側の信頼性や運用に起因する問題がいくつか発生するようになりました。
いずれも PagerDuty側に原因があり、こちら側ですぐに何か対策するのも難しいのでひとまず様子見していますが、改善しないようなら全面的な仕組みの見直しが必要となるかもしれません。
不安定な通知
イベントの通知基盤があまり安定していないようで、通知が飛んでこないことがありました。
Webhook と併せて Slack とメールもサブスクライブしているのですが、そのときはいずれのチャネルにも通知されませんでした。
Webhook に強く依存する仕組みなので、ここが危ういと仕組みそのものが破綻してしまいます。
検知できない障害
Detected や Investigating を飛ばして、いきなり Resolved で公開されるインシデントも存在します。
(例: #P7O0UTE, #P87XMD5)
Detected の公開をサボったのでしょうか?
それとも PagerDuty側でも検知できなかった (= いつの間にか障害が発生していて、気づいたら復旧していた) のでしょうか?
理由はわかりませんが、検知・公開体制の強化を期待したいところです。
まとめ
本記事は「Pagerduty の障害に Opsgenie で備える」の続編です。
先日の PagerDuty Statusページの 移行 に伴い、Statusページに基づく障害検知の仕組みを刷新する必要が生じました。
AWS Lambda + 素の Opsgenie API を叩くことで元々の挙動を再現しましたが、新しい Statusページの信頼性に起因して、いくつかの問題が発生するようになりました。
状況が悪化するようなら根本的な対策を検討せねばなりませんが、しばらくは様子を見ながら運用してみたいと思います。
今後ともよろしくお願い申し上げます。
当記事の図表には AWS Architecture Icons、PagerDuty のロゴ、Opsgenie のロゴ を使用しています。