🧩

TerraformでPagerDuty設定応用 - Alertmanagerのconfigを自動生成する

2024/12/21に公開

はじめに

PagerDuty Advent Calendar 2024の21日目の記事です。

この記事は、PagerDuty on Tour TOKYO 2024のLT枠で登壇したときに紹介した内容について、より詳しく説明するものです。

前提: TerraformでPagerDutyの設定を実施する

TerraformでPagerDuty設定入門 その1などで述べられているように、TerraformでPagerDutyの各種リソースを管理することができます。

この記事の想定読者は、既にServiceやService Integrationの設定をTerraformで実施している方々です。

具体的には、

  • pagerduty_service
  • pagerduty_service_integration

の2種類のリソースをTerraformで管理していて、pagerduty_service_integrationのtypeをeventapi_v2_inbound_integrationに設定している、もしくはそのように設定することができることを想定しています。

Alertmanager x PagerDuty

Alertmanagerは、Prometheusなどからのアラートを受け取り、それらを任意の宛先に通知するためのアプリケーションです。宛先にはSlackやEmail, PagerDutyなどを指定することができます。

Alertmanagerでは宛先のことをreceiver、どのアラートをどのreceiverに通知するかのルールをrouteという設定で定義します。

Receiverの設定

一旦receiverの話に絞って、PagerDutyへの通知設定をどのように記述するのかの例を見てみましょう。

receivers:
- name: 'pd_service_hoge'
  pagerduty_configs:
  - routing_key: '<service intergrationで発行されたkey>'
    description: '<インシデントのタイトル>'
    details: <map[string]string>

receiversの下に任意の名前のreceiverを定義し、pagerduty_configsの下にPagerDutyへの通知設定を記述します。ここで重要なのはrouting_keyで、これは基本的にはservice integrationの設定時にしか確認できない情報です。

そのため、宛先のPagerDuty Serviceを追加する度にService Integrationを作成->routing_keyを確認->alertmanager.ymlを編集->Alertmanagerを再起動という手順を踏む必要があります。

Routeの設定

無事任意のPagerDuty Serviceへのreceiverを設定したら、どのアラートをそのreceiverに送るかをrouteとして設定する必要があります。

以下は、serviceというラベルがalertに定義されていて、それがalertのgroupingに使われているときに、serviceラベルの値がhoge-で始まるアラートを先程の例で定義したreceiverに送る設定の例です。

routes:
- receiver: 'pd_service_hoge'
  match_re:
    service: 'hoge-.+'

これらを手動で管理するのは大変です。特に、多くのPagerDuty Serviceに対して様々な種類のアラートをルーティングしなくてはいけない場合は、手動管理では記述ミスのリスクが大きくなります。routingされないアラートには気付きづらいため、そもそも記述ミスが発覚しないまま重大インシデントを迎えてしまうことも考えられます。

Terraform stateとAnsibleを使ったAlertmanager config fileの自動生成

そこで、Terraform stateとAnsibleを使ってalertmanager config fileを自動生成する方法を紹介します。

基本的な生成手順は以下です。

  1. Terraform stateのdump
  2. Ansibleのfrom_json filterでのデータ取り込み
  3. Ansibleのtemplate moduleでのconfig fileの生成

また、適切にreceiverrouteをマッピングするために、TerraformでのService, Service Integration resourceの定義にも仕込みをします。まずはその仕込みから説明し、その後に1~3について例を示します。

TerraformのService, Service Integrationのリソース定義

「仕込み」の目的は、Alertmanagerのreceiverrouteのマッピングを適切に行うことです。

receiverに必要な情報はpagerduty_service_integrationintegration_keyから取れるのですが、pagerduty_service_integrationの定義内でrouteに必要な情報を記述できるArgumentは存在しないので、頭を悩ませることになります。

そこで、pagerduty_service_integrationの前提リソースであるpagerduty_serviceに定義されているdescription argumentを利用します。

具体的な仕込みの内容は以下です。

pagerduty_servicedescriptionに「routeに必要な情報」をjsonで記述する

  • 必要な情報はroutematch_reに指定するkey-valueと、receiverの名前です。
    • receiverにはpagerduty_service_integrationnameを指定します。
  • 後でパースできるようにjsonで書き込みます。
    • descriptionが改行をサポートしてる可能性があるので、YAMLの方が可読性が高くていいかもしれないです(未検証)

以下はhogeという名前のServiceを作成する例です。

resource "pagerduty_service" "hoge" {
    name = "Hoge"
    description = jsonencode({
        "receiver": "pd_hoge",
        "match_re": {
            "service": "hoge-.*",
            "severity": "critical|error"
        }
    })
    escalation_policy = pagerduty_escalation_policy.hoge.id
    alert_creation    = "create_alerts_and_incidents"

    incident_urgency_rule {
        type    = "constant"
        urgency = "severity_based"
    }
}

resource "pagerduty_service_integration" "hoge" {
    name    = "pd_hoge"
    type    = "events_api_v2_inbound_integration"
    service = pagerduty_service.hoge.id
}

1. Terraform stateのdump

Terraformのstateは、terraform state pullコマンドでjson形式で取得することができます。

jsonの構造を説明すると長くなってしまうので、いくつかデータ参照のための例を示します。

先ほどの仕込みをしたpagerduty_servicedescriptionを抜き出すjqコマンド

terraform state pull | jq -r '[.resources[] | select(.type == "pagerduty_service") | .instances[] | .attributes.description | fromjson]'
[
  {
    "match_re": {
      "service": "hoge-.*"
    },
    "receiver": "pd_hoge"
  },
  {
    "match_re": {
      "service": "fuga-.*"
    },
    "receiver": "pd_fuga"
  }
]

先ほどの仕込みをしたpagerduty_service_integrationnameintegration_keyを抜き出すjqコマンド

terraform state pull | jq -r '[.resources[] | select(.type == "pagerduty_service_integration") | .instances[] | {"name": .attributes.name, "integration_key": .attributes.integration_key}]'
[
  {
    "name": "pd_hoge",
    "integration_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  {
    "name": "pd_fuga",
    "integration_key": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
  }
]

2. Ansibleのfrom_json filterを使ってのデータの取り込み

先ほど示したjqコマンドをスクリプト化するなどして、Ansibleのlookup pluginのpipeを通して実行 → from_json filterでデータを取り込むことができます。

例えば、alertmanager roleのdefaults/main.ymlなどに以下のように定義しておくと、role内で取り込んだデータを使うことができます。

alertmanager_routes: "{{ lookup('pipe', 'scripts/get_alertmanager_routes.sh') | from_json}}"
alertmanager_receivers: "{{ lookup('pipe', 'scripts/get_alertmanager_receivers.sh') | from_json}}"

※ 実際にはスクリプトを参照できるように絶対パスを変数などで補正してあげる必要があります。

3. Ansibleのtemplate moduleでのconfig fileの生成

後はtemplatemoduleを使ってconfigを生成するだけです。

必要な部分だけ例を示します。

route:
  group_by: ['alertname', 'env']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 1h
  receiver: default
  routes:
{% for route in alertmanager_routes %}
  - match_re:
{% for key, value in route.match_re.items() %}
      {{ key }}: {{ value }}
{% endfor %}
    receiver: {{ route.receiver }}
{% endfor %}

receivers:
{% for receiver in alertmanager_receivers %}
- name: {{ receiver.name }}
  pagerduty_configs:
  - routing_key: {{ receiver.integration_key }}
    description: '[{{ .GroupLabels.env }}] {{ .GroupLabels.alertname }}'
    details: {...}
{% endfor %}

Key points

  • terraform stateはjsonで取得できるので、Ansibleのfrom_json filterで取り込んで使うことができる
    • jqで前処理しておくと楽
  • pagerduty_servicedescriptionにjsonを埋め込むことで、pagerduty_service_integrationとの関連を表現することができる
    • 1000文字くらいは入るので、複雑な条件でも問題無し
    • そもそもこの2つのリソースは同じ.tfファイルで管理した方が見通しが良いこともあり、格納場所として都合がよい

さいごに

通知周りはミスると大変なので、PagerDutyへの通知ルートをAlertmanagerなどで自前実装するのであれば、設定は極力自動化しましょう!

Discussion