MackerelとGrafana OnCallを連携しました

SREチームの藤原です。今回は監視サービスのMackerelと、障害発生時に担当者へのオンコールを自動化するGrafana OnCallを連携してみた話です。SRE連載 6月号になります。

3行でまとめ

  • MackerelとGrafana OnCallを連携しました
  • MackerelのアラートWebhookをGrafana OnCallのWebhookに変換するproxyをAWS Lambdaで作りました。OSSで公開しています
  • Grafana OnCallの管理はTerraformでやっています

はじめに

カヤックでは、運用しているサービスの監視のためにMackerelを利用しています。サービスで障害が発生した場合に担当者を呼び出す(オンコール)ためのツールとして、2023年3月までは ryotarai/waker を使用していました。

wakerはもともとクックパッド社で利用されるために開発されたOSSです。wakerはMackerelのアラートWebhookを直接受け付けられるため、カヤックでもMackerelからのアラート通知を受け取ってオンコールを自動化するために利用していました。それに関連して、弊社からもいくつかコントリビューションを行っています。

techlife.cookpad.com

wakerがクックパッドで利用されなくなった後もGMOペパボ社で利用されており、同社のP山さんがメンテナンスをしてくれていました。

ten-snapon.com

しかし2022年6月、次のエントリが公開されました。

tech.pepabo.com

ペパボでは、オンコール通知にインシデント管理サービスであるPagerDutyを利用しています。

おそらくペパボ社はオンコールのシステムをwakerからPagerDutyに切り替えたのでしょう。wakerリポジトリの更新も2年ほどない状態でした。となると、このまま使い続けるためには自分たちでメンテナンスを引き継ぐ必要があります。

検討した結果、自分らでRailsのWebアプリケーション実装であるwakerをメンテナンスし、セルフホスティングをし続けるよりも、他のサービスへの乗り換えを行う方が総合的には低コストであろうと判断しました。

Grafana OnCallとMackerelを連携する

オンコールツールとしては、2022年にリリースされたGrafana OnCallを選択1しました。 Grafana OnCallはGrafana Labs社が提供するOSSで、セルフホスティングも可能ですが、Grafana Cloudでクラウドサービスとして利用することもできます。今回はクラウドサービスを利用することにしました。

Grafana OnCallは、現時点では残念ながらMackerelとの連携をネイティブでサポートしていません。しかしWebhookの仕組みがあるため、これを使うことでMackerelとの連携を実現できます。MackerelのWebhookを受け取って、Grafana OnCallのWebhookの形式に変換して送ってやればよいのです。

ということで、詳細はざっくり省きますが、出来上がったものがこちらになります。OSSとして公開しています。

github.com

Goで実装された、AWS Lambdaで実行できるWeb APIです。Amazon API Gateway, ALB, Lambda Function URLから起動されるLambda関数として動作します。実際には、一番簡単にWeb APIとして実行できる環境である、Function URLで動作させています。

Grafana OnCall側の送信先Webhook URLは、Lambdaの環境変数 GRAFANA_ONCALL_URL で設定します。もしくは、URLパラメーターとして送信先を受け付けることもできますし、JSONで複数のaliasを定義して環境変数 GRAFANA_ONCALL_URL_ALIASES に設定することで、URLパラメーターには短い名前を指定するだけで送信できるようにもなります。詳細はリポジトリのREADMEを参照してください。

{
  "foo": "https://oncall-prod-us-central-0.grafana.net/oncall/integrations/v1/formatted_webhook/xxxxxxx/",
  "bar": "https://oncall-prod-us-central-0.grafana.net/oncall/integrations/v1/formatted_webhook/yyyyyy/"
}

実際の動作

実際にアラートが発生したときの動作がこちらです。

ある日の23:55に、Mackerelで外形監視が失敗したアラートが発生しました。

23:58:59にWebhookからGrafana OnCallにアラートが登録され、エスカレーション定義に従って担当者の各通知手段(email, SMS, mobilepush)で通知が行われています。

Webhookにはアラートに付随するグラフ画像のURLも含まれているため、Grafana側でも画像を確認することができます。

00:02:44には担当者が通知に気が付いてAckしたため、エスカレーションはここで打ち切られます。

翌00:14に障害が解消してMackerel側でアラートがCloseした時にもWebhookが飛ぶため、Grafana OnCallでもアラートがresolveされているのが分かります。

Grafana OnCallとLambdaの管理をTerraformで行う

Grafana OnCallとLambdaの管理は、Terraformで行っています。TerraformのGrafana Providerを利用すると、TerraformでOnCallのインテグレーション(Webhook)、エスカレーション定義などを管理できます。

次の例は、exampleというサービスに対して以下のリソースを作成するものです。

  • エスカレーションチェーンの定義
    1. ユーザーに通知
    2. 5分待つ
    3. (ここまでにだれもackしなかったら)ユーザーに重大(important)通知
  • MackerelからのWebhookを受け付けるインテグレーション
    • デフォルトでSlackのチャンネルに通知する設定を追加しています

各ユーザーは、各自で通常の通知(メールやGrafanaモバイルアプリ)や重大通知(SMSや電話など)を受け取る設定をしています。通知先はTerraformでの管理対象外です。

// エスカレーションチェーンの定義
resource "grafana_oncall_escalation_chain" "example" {
  provider = grafana.oncall
  name     = "example"
}

// チェーンに対してステップを定義
// 1. 全員に通知
resource "grafana_oncall_escalation" "example_notify_step_0" {
  escalation_chain_id = grafana_oncall_escalation_chain.example.id
  type                = "notify_persons"
  persons_to_notify = [
    // グループの全員のuser_id
    for u in local.oncall_groups["example"] : data.grafana_oncall_user.kayac[u].id
  ]
  position = 0
}

// 2. 5分待つ
resource "grafana_oncall_escalation" "example_notify_step_wait" {
  escalation_chain_id = grafana_oncall_escalation_chain.example.id
  type                = "wait"
  duration            = 300 // [60 300 900 1800 3600]のいずれか
  position            = 1
}

// 3. 全員に重大(important)通知
resource "grafana_oncall_escalation" "example_notify_step_important" {
  escalation_chain_id = grafana_oncall_escalation_chain.example.id
  type                = "notify_persons"
  important           = true
  persons_to_notify = [
    // グループの全員のuser_id
    for u in local.oncall_groups["example"] : data.grafana_oncall_user.kayac[u].id
  ]
  position = 2
}

// webhookの定義(プロジェクトごとにやる)
// mackerel-to-grafana-oncallの通知先として
// grafana_oncall_integration.example.link を指定する
resource "grafana_oncall_integration" "example" {
  provider = grafana.oncall
  name     = "example integration"
  type     = "formatted_webhook"
  default_route { // エスカレーション先を指定
    escalation_chain_id = grafana_oncall_escalation_chain.example.id
    slack {
      channel_id = "CXXXXXXX" // SlackのチャンネルID
      enabled    = true
    }
  }
}

なお、エスカレーションの通知先には、Grafana OnCallのユーザーID(ユーザー名ではなく内部のID)を個別に指定する必要があります。Grafana側で定義できるグループはエスカレーションの通知先として指定できません。今回は社内で利用する複数のサービスに対してひとつのTerraformでユーザーと通知先を管理したかったため、Terraform上で簡易的なグループとユーザー管理を行っています。

locals {
  oncall_groups = {
    example = [       // 通知先グループ名を定義
      "someusername", // Grafanaユーザー名を定義
    ]
    foo = [
      // ...
    ]
  }
  // oncall_groupsのユーザー名を重複なく集めた一覧
  oncall_users = distinct(flatten([
    for group, users in local.oncall_groups : users
  ]))
}

// Grafanaユーザー名からユーザーを取得
data "grafana_oncall_user" "kayac" {
  for_each = toset(local.oncall_users)
  username = each.value
}

GrafanaのユーザーIDを得るために、grafana_oncall_userデータソース をユーザー名で定義し、そのプロパティとしてIDを参照しています。

Terraformで作成した grafana_oncall_integrationリソースlink プロパティには、WebhookのURLが格納されています。このURLをLambdaの環境変数に設定してデプロイすることで、MackerelからのWebhookをGrafana OnCallに通知できます。

最後に、MackerelのアラートWebhookの送信先として作成されたLambdaのFunction URLを指定すれば完成です。2

さいごに

今回はMackerelのアラート通知をGrafana OnCallに連携して、オンコール通知する仕組みを整備した様子を紹介しました。Webhookの変換プロキシを実装してOSSとして公開しています。OnCallの設定はTerraformで管理することで、社内で運用している複数のサービスに対して簡単に設定できるようにしています。

なお、今回はWebhookを変換するプロキシをAWS Lambdaで実装してOSSにすることで解決しましたが、よく考えてみるとGrafana OnCallはOSSなのでした。ということは、Grafana OnCall側にMackerelのWebhookを受け付けられるようにコントリビューションする手もありそうですね。以下のリポジトリでは各種監視サービスのWebhookを受け付ける機能が実装されているので、参考にすればMackerel対応も実装できそうです。

https://github.com/grafana/oncall/tree/dev/engine/config_integrations

カヤックでは、業務を通じてOSSにコントリビュートしたいエンジニアも募集しています!


  1. 他のサービスやツールとの比較については割愛します。
  2. OnCallを管理するTerraformは全社共通ですが、Mackerelは各サービス(プロダクト)ごとにOrganizationを用意しているため、今回はMackerelのWebhook定義はTerraform化していません。