OSS 『Prepalert』 の紹介

SREチームの池田です。 この記事が出ている頃には私は SRE Next 2023 に参加しているでしょう。
SRE Next 2023での私のセッションは『Warningアラートを放置しない!アラート駆動でログやメトリックを自動収集する仕組みによる恩恵』です。
このセッション中で話す仕組みはOSS『Prepalert』というもので実現しているのですが、今回の記事ではセッションの裏番組的にOSS『Prepalert』の紹介をします。

github.com

Prepalertについては以前にTechBlog上で記事を書いているので、そことの差分を中心に紹介します。

techblog.kayac.com

3行でまとめ

  • OSS『Prepalert』はMackerel Webhookを受け取って、各所に情報を問い合わせてMackerelのアラートのメモに貼り付ける仕組み
  • そろそろ運用歴2年でv1リリースが近い。現時点ではv1-betaの立ち位置のv0.12.0が最新
  • v0時代とv1時代の違いは主にアーキテクチャとConfigのフォーマット

はじめに

こちらの記事にありますように、SREの世界では「トイル」という概念があります。

cloud.google.com

「トイルとは、手作業、繰り返される、自動化が可能、戦術的、長期的な価値がない、サービスの成長に比例して増加する、といった特徴を持つ作業です。」 トイルの例としては次のようなものがあります。

・割り当てリクエストの処理
・データベース スキーマ変更の適用
・重要性の低いモニタリング アラートの確認
・プレイブックからのコマンドのコピーと貼り付け

ここに挙げたすべての例に共通する特徴は、エンジニアが頭で判断する必要がないということです。

そして、『重要性の低いモニタリング アラートの確認』はトイルの代表例の一つです。 セッション中では『重要度の低いモニタリングアラート』=『SeverityがWarningのアラート』として話してます。

これらのアラートの確認において、一番トイルのもとになるのが情報の収集です。 例えば、必要な情報がマニュアル化されていたとしても『マニュアルからのコマンドのコピーと貼り付け』というトイルになってしまいます。 マニュアル化されていない場合は『担当者が手作業で、ケースバイケースに様々なところから情報を集積する』ということになります。

『ApplicationLoadBalancer (ALB)で5xxエラーが1件以上ありました。』というWarningのアラートに対して、 それがなにか対応をする必要があるのか、ということを判断するためには、どれくらいの情報が必要でしょうか?

  • WebAppliatioFirewall (WAF) がConnectTimeoutで5xxになったのか?
  • Nginxまで到達しているが、アプリケーションに接続できなくて5xxになったのか?
  • アプリケーション内部でエラーが発生して5xxになったのか?
  • etc..

大体の場合、これらの疑問に対する回答を一度に得ることはできません。 1次情報を収集することは、思ったよりも重たい作業になりがちで、大きなトイルの一つになります。

そこで、OSS『Prepalert』は、Mackerelのアラートが発生したことをWebhookとして受け取り、 色々なところに情報を問い合せ、テキストとして構成します。その後、テキスト情報をMackerelのアラートのメモに貼り付けます。 これによって、自動的にアラートの1次調査に必要な情報が出揃い、トイルを減らすことができます。

今のPrepalertの状況

現時点でのPrepalertは v0.12.0がPre-release状態にあります。これは、v1のbetaバージョンとしての立ち位置になります。 少し本番運用してみて、特に問題がないようであれば10月第1週くらいを目処にv1をリリースしようと思います。

v0とv1でのアーキテクチャの違い

現時点でのアーキテクチャ図としては次のようになります。

v1アーキテクチャ

比較のために、2022当時のv0のアーキテクチャも載せておきます。

v0アーキテクチャ

大きく変わった点は、Lambda関数の数です。これは、AWS Dev Day 2023で聞いたセッションの影響です。 同様の内容のSlideがspeakerdeckにありますので、リンクを掲載しておきます。

運用上、v0時代のようにLambda関数が2つに別れていると、Configを更新したら2回デプロイしたくなってきます。
そこで、v1ではデフォルトの駆動に関しては、Lambda関数を1つにまとめて、デプロイが1回で済むようにしてみました。
もちろん、環境変数によってWebhookHandlerだけ、Workerだけと個別に動かしてLambda関数を2つに分けられるようにもしてあります。

具体的な v1でのシンプルな構成は こちら に terrafromのコードがありますのでご参考にしてください。

v0とv1でのConfigの違い

v0と同様、以下のコマンドを実行すると設定ファイルであるHCLを生成します。

$ prepalert init --config config_dir/

ただし、v1ではConfigの構造に変化があります。

// after v1
prepalert {
    required_version = ">=v0.12.0"
    sqs_queue_name   = "prepalert"
}

locals {
    default_message =  <<EOF
How do you respond to alerts?
Describe information about your alert response here.
EOF
}

rule "simple" {
    when = true
    update_alert {
        memo = local.default_message
    }
}

比較のために、rule ブロックに関してのみv0時代の同様の内容のConfigを載せておきます。

// v0
rule "simple" {
  alert {
      any = true
  }
  information = <<EOF
How do you respond to alerts?
Describe information about your alert response here.
EOF

  post_graph_annotation = false
  update_alert_memo     = true
}

v0時代では、メモを貼り付ける条件の指定は alert というHCLのblokで設定するようになっていましたが、 v1からは、when というHCLのattributeにBool値もしくはBoolのリストを指定することになります。 Boolのリストを指定した場合は、それぞれのAnd条件とみなします。

ruleのblockの中では以下のような構造の変数が使えるようになっています。 これは、MackerelのWebhookで送信されてくるpayload のキーをキャメルケースからスネークケースに変換したものです。

{
  "webhook": {
    "alert": {
      "closed_at": 1473130092,
      "created_at": 1473129912693,
      "critical_threshold": 1.9588528112516932,
      "duration": 5,
      "id": "2bj...",
      "is_open": true,
      "metric_label": "MetricName",
      "metric_value": 2.255356387321597,
      "monitor_name": "MonitorName",
      "monitor_operator": "\\u003e",
      "opened_at": 1473129912,
      "status": "critical",
      "trigger": "monitor",
      "url": "https://mackerel.io/orgs/.../alerts/2bj...",
      "warning_threshold": 1.4665636369580741
    },
    "event": "alert",
    "host": {
      "id": "22D4...",
      "is_retired": false,
      "memo": "",
      "name": "app01",
      "roles": [
        {
          "fullname": "Service: Role",
          "role_name": "Role",
          "role_url": "https://mackerel.io/orgs/.../services/...",
          "service_name": "Service",
          "service_url": "https://mackerel.io/orgs/.../services/..."
        }
      ],
      "status": "working",
      "type": "unknown",
      "url": "https://mackerel.io/orgs/.../hosts/..."
    },
    "image_url": "https://mackerel.io/embed/public/.../....png",
    "memo": "memo....",
    "org_name": "Macker..."
  }
}

例えば、アラートのIDを参照したい場合は webhook.alert.id というように指定します。 ruleブロックの中ならどこでも参照できるのでwebhook.host.url というようにホストのURLをメモに書くのも便利です。

補助用のHCL関数も用意しており、例えば『指定したMonitorIDのアラートが開かれたときにメモを貼り付ける』というルールは以下のように書けます。

// after v1
rule "id_12345_is_open" {
    when = [
        get_monitor(webhook.alert).id == "12345",
        webhook.alert.is_open == true,
    ]
    //...
}

この変更は、HCLの柔軟性にによりルールの適用条件を高めることを意図しています。 この他にも、細かな差異としてルールの実施順序をコントロールする priority というattributeが追加されていたりもします。 細かく設定できるようにしてあるので、どこかのタイミングで設定事例集みたいなものは書いてみようかなと思います。

より実用的な例

より実用的な例として、CloudWatch Logs Insightsからログを取得して、アラートのメモに貼り付けるという設定を紹介します。

config.hcl

// after v1
prepalert {
  required_version = ">=v0.12.0"
  sqs_queue_name   = "prepalert"
  
  // ここを設定しておくと、WebhookのエンドポイントにBasic認証がかかります。
  auth {
    client_id     = "prepalert"
    client_secret = must_env("PREPALERT_WEBHOOK_CLIENT_SECRET")
  }
}

provider "cloudwatch_logs_insights" {
  region = "ap-northeast-1"
}

query "cloudwatch_logs_insights" "app_logs" {
  log_group_names = [
    "applcation-logs",
  ]
  query      =  <<EOT
fields @timestamp, @message
    | parse @message "* [*] *" as time, loggingType, loggingMessage
    | filter loggingType = "error"
    | display @timestamp, loggingMessage
    | sort @timestamp desc
    | limit 2000
EOT
  start_time = webhook.alert.opened_at - duration("15m")
  end_time   = coalesce(webhook.alert.closed_at, now()) // アラートが閉じられていない場合は現在時刻を使う
}

rule "simple" {
  when = [
    has_prefix(get_monitor(webhook.alert).name,"[prepalert]"),
    webhook.alert.is_open == true,
  ]
  update_alert {
    memo = templatefile("./memo.tpl.md", {
        app_logs = result_to_jsonlines(query.cloudwatch_logs_insights.app_logs)
    })
  }
  post_graph_annotation {
    service = "prepalert"
  }
}

memo.tpl.md

アラートが発生した時間の15分前からのアプリケーションエラーログです。

${app_logs}

このように設定すると、アラートが発生したときに、CloudWatch Logs Insightsからログを取得して、アラートのメモに貼り付けることができます。

発行されるクエリは

fields @timestamp, @message
    | parse @message "* [*] *" as time, loggingType, loggingMessage
    | filter loggingType = "error"
    | display @timestamp, loggingMessage
    | sort @timestamp desc
    | limit 2000

というもので、application-logs というlog_groupに対して、[error] という文字列を含むログを取得しています。 取ってきた結果は、result_to_jsonlines()という関数やresult_to_table() という関数を用意してあるので、それらを使って文字列に変換します。 メモやクエリの内容は複雑化しやすいので、templatefile という関数を用意しています。この関数を使って、外部ファイルにメモやクエリのテンプレートを書いておくことができます。

provider というのデータの供給もとを表すものです。 ここでは、Cloudwatch Logs Insightsからデータを取得するために、cloudwatch_logs_insights というproviderを使っています。 ビルドインでは他に redshift_datas3_select があります。provideごとの具体的な使い方はまたの機会に紹介します。

まとめ

2021年頃から開発し始めて、ゆっくりと成長したOSS『Prepalert』ですが、そろそろv1のメジャーバージョンとしてリリースしようと思っています。 使ってみるととても便利ですので、MackerelとAWSをお使いの方は試しに使用してみていただければ幸いです。

カヤックではトイル削減が好きな人も募集してます。

hubspot.kayac.com