実践的なPrepalertの設定

この記事はMackerel Advent Calendar 2023 の22日目です。
こんにちは、SREチーム所属の@mashiikeです。

先日の、Mackerel Meetup #15 Tokyo ではパネルディスカッションでパネリストの一人として参加させていただきました。とても楽しかったです。ありがとうございます。

さて、この記事ではTechBlogで以前紹介したことのあるOSS『Prepalert』の実践的な設定について話したいと思います。 techblog.kayac.com

Prepalertとは

https://github.com/mashiike/prepaletgithub.com

Prepalertは、『重要性の低いモニタリング アラートの確認』というトイルを削減するためのツールです。
Mackerelを運用している方の中には『SeverityがWarningのアラート』を設定していることがあると思いますが、このWarningのアラートを確認するために各種ログや詳細メトリックを毎回人の手で調査するのは、代表的なトイルになると思います。
そこで、PrepalertではMackerelからWebhookを受け取り、そのWebhookと設定ファイルをもとに各種ログや詳細メトリックの情報を問い合わせして、問い合わせ結果をアラートのメモに貼り付けるということを行います。
これにより、自動的にアラートの1次調査に必要な情報が出揃い、トイルを減らすことができます。

冒頭で登場した以前の記事では、『アラートがオープンされたときに、対応する監視名が [prepalert]のプレフィックスを持っている場合は、CloudWatch Insights を使って applcation-logs のロググループから [error] のログを拾ってくる』という設定例を紹介しました。 今回は、より別の実践的な設定について紹介したいと思います。

S3 Select を使ってログを取得する設定

Prepalertでは自分がよく問い合わせる先をいくつかBuilt-inで実装しています。その一つとしてS3 SelectObjectContent APIがあります。 例えば、S3にあるJSON LinesやCSVを雑にメモに貼り付けたいときは、この機能がとても便利です。

中でも重宝する実践的な例として、ALBのログを取得してくる場合を取り上げます。

以下の設定は、 your-alb-log-bucketに出力された hogehoge-albのログを取得することを想定しています。 そして、その取得されたログが貼り付けられるのは、監視IDが XXXXXXXX でアラートがオープンされたときを想定しています。

./config.hcl

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

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

query "s3_select" "alb_5xx_logs" {
  bucket_name = "your-alb-log-bucket"
  object_key_prefix = join("/", [
    "hogehoge-alb/AWSLogs/123456789012/elasticloadbalancing/ap-northeast-1",
    strftime("%Y/%m/%d/", webhook.alert.opened_at),
  ])
  expression = file("./queries/get_alb_5xx_log.sql")
  params = {
    start_at = strftime_in_zone("%Y-%m-%dT%H:%M:%SZ", "UTC", webhook.alert.opened_at - duration("15m"))
    end_at   = strftime_in_zone("%Y-%m-%dT%H:%M:%SZ", "UTC", coalesce(webhook.alert.closed_at, now()))
  }
  input_serialization {
    compression_type = "GZIP"
    csv {
      field_delimiter  = " "
      record_delimiter = "\n"
    }
  }
}

rule "alb_monitor" {
  when = [
    get_monitor(webhook.alert).id == "XXXXXXXX",
    webhook.alert.is_open == true,
  ]
  update_alert {
    memo = result_to_jsonlines(query.s3_select.alb_5xx_logs),
  }
}

./queries/get_alb_5xx_log.sql

SELECT
    s._1 as "type"
    ,s._2 as "time"
    ,s._3 as "elb"
    ,s._4 as "client_port"
    ,s._5 as "target_port"
    ,s._6 as "request_processing_time"
    ,s._7 as "target_processing_time"
    ,s._8 as "response_processing_time"
    ,s._9 as "elb_status_code"
    ,s._10 as "target_status_code"
    ,s._11 as "received_bytes"
    ,s._12 as "sent_bytes"
    ,s._13 as "request"
    ,s._14 as "user_agent"
    ,s._15 as "ssl_cipher"
    ,s._16 as "ssl_protocol"
    ,s._17 as "target_group_arn"
    ,s._18 as "trace_id"
    ,s._19 as "domain_name"
    ,s._20 as "chosen_cert_arn"
    ,s._21 as "matched_rule_priority"
    ,s._22 as "request_creation_time"
    ,s._23 as "actions_executed"
    ,s._24 as "redirect_url"
    ,s._25 as "error_reason"
    ,s._26 as "target_port_list"
    ,s._27 as "target_status_code_list"
    ,s._28 as "classification"
    ,s._29 as "classification_reason"
FROM s3object s
WHERE TO_TIMESTAMP(s._2) >= TO_TIMESTAMP( :start_at )
AND TO_TIMESTAMP(s._2) <= TO_TIMESTAMP( :end_at )
AND cast(s._9 as INT) >= 500

PrepalertのHCLでは4つのブロックがあります。

  • prepalertブロック: Prepalert全体の設定に関する部分です。アクセス制御や、使用するSQS Queueの指定などがあります。
  • providerブロック: Prepalertが情報を取得する先に関する設定です。
  • queryブロック: providerで指定した設定を用いて、具体的にどのように情報を取得するか?を設定します。
  • ruleブロック: 実際に発生したアラートに関して、どのようなメモを貼るか?の設定です。

この設定例で重要なのはqueryブロックとruleブロックになります。

先に、queryブロックについて説明します。

queryブロック

./config.hclquery ブロックには、 s3_select というproviderを指定しています。 形として

query "provider_name" "query_name" {
// ...
}

となっていて、ブロックの中身は使用するproviderによって変わってきます。

s3_select providerを使う場合は、おもにS3のSelectObjectContent APIにアクセスするための情報を記述します。 docs.aws.amazon.com

API自体は、1つのS3 Objectに対してSQLを実行して、その結果を取得するものです。 Prepalertでは object_key_prefix で指定した文字列に合致するS3 Object全てに対してSelectOjbectContent APIを実行して、その結果を取得するようにしています。

ALBログはPartitionのように日付ごとに別れてるので、効率よく取得するためにアラートの時刻発生時刻ベースでobject_key_prefixを指定したいです。

そこでHCLの関数を使って動的に object_key_prefix、を指定しています。 Prepalertでは時間関係のHCL関数として 以下を用意しています。

  • strftime_in_zone(format, timezone, unixSeconds): unixSeconds(float)を指定したFormatとタイムゾーンで文字列に変換する
  • strftime(format, unixSeconds): unixSeconds(float)を指定したFormatで文字列に変換する
  • now(): 現在時刻をunixSeconds(float)で返す
  • duration(str): 指定した文字列をdurationとしてパースして、seconds(float)で返す

つまり、strftime("%Y/%m/%d/", webhook.alert.opened_at) は、アラートがオープンされたときの時刻を YYYY/MM/DD/の形式で実行ランタイムのローカルタイムゾーンにしたがって文字列に変換します。

join(delimiter,[str, str,...])は 与えられたdelimiterであとの方の文字列の配列を結合します。

ですので、

  object_key_prefix = join("/", [
    "hogehoge-alb/AWSLogs/123456789012/elasticloadbalancing/ap-northeast-1",
    strftime("%Y/%m/%d/", webhook.alert.opened_at),
  ])

の部分は、アラートのオープン時刻が 2021-12-22T12:34:56Z だった場合には、 hogehoge-alb/AWSLogs/123456789012/elasticloadbalancing/ap-northeast-1/2021/12/22/ という文字列になります。

このようにして、実際に受け取ったwebhookのbodyベースで、情報の取得ができるようになっています。

次に expression部分についてです。

  expression = file("./queries/get_alb_5xx_log.sql")
  params = {
    start_at = strftime_in_zone("%Y-%m-%dT%H:%M:%SZ", "UTC", webhook.alert.opened_at - duration("15m"))
    end_at   = strftime_in_zone("%Y-%m-%dT%H:%M:%SZ", "UTC", coalesce(webhook.alert.closed_at, now()))
  }

expression で指定しているところには、S3 SelectObjectContent APIで使用するSQLを記述します。   ここには単純に文字列を書いても良いのですが、それだと管理上困ることがあると思いますので、file(filepath) という関数で別ファイルの内容をテキストで読めるようになっています。これは、設定ファイルのDirからの相対パスを指定する形になっています。 expressionで指定したSQLでは :var_name という形のプレースホルダーを使用できるようにしています。 このプレースホルダーの中身は params で指定するようになっています。
今回の例では、 :start_at:end_at を渡しています。
そして、この2つにはUTCのISO8601形式の文字列で、『アラートの開始時刻から15分前』と『アラートの終了時刻もしくは現在時刻』を渡しています。 input_serialization は S3 SelectObjectContent APIのドキュメントから雰囲気で察せると思いますので、スキップします。

このように、どうやって情報を取得するのか?を指定するのが query ブロックになります。

さて、次に rule ブロックの説明をします。

ruleブロック

ruleブロック中では、queryブロックを参照できるようになっています。
ruleブロック中で参照されたqueryが実際に実行されて、その結果をもとにメモの描画を行います。 上記の例の中では、result_to_jsonlines(query.s3_select.alb_5xx_logs) というのが具体的な参照部分です。 こうすると、queryの結果をJSON Lines形式で取得できます。 他にも次のようなHCL関数を用意しています。

  • result_to_table(query.provider_name.query_name): MySQL Clientっぽい感じのTableで出力します。
  • result_to_vertical(query.provider_name.query_name): MySQL Clientで \Gを指定したっぽい感じの縦長フォーマットで出力します。
  • result_to_markdown(query.provider_name.query_name): MarkdownのTable形式で出力します。
  • result_to_borderless(query.provider_name.query_name): 外枠なしな感じのTableで出力します。

update_alert.memo には 文字列なら何でも指定できるようになっています。 例えば、JSON Linesの情報だけでは味気ないと思ったら、query ブロックの object_key_prefix のときと同様に join(delimiter,[str, str,...]) で説明を付け加えることも可能です。

  update_alert {
    memo = join("\n", [
        "説明だよーーーー!",
        result_to_jsonlines(query.s3_select.alb_5xx_logs),
    ])
  }

他にも、長い説明を付け加えたいときには templatefile(filepath, params) という関数を使うと便利です。

  update_alert {
    memo = tmplfile("./templates/memo.tpl.md", {
        query_result = result_to_jsonlines(query.s3_select.alb_5xx_logs),
    })
  }

と指定して、テンプレートには以下のようにかけます。

説明だよ〜

- 確認項目1
- 確認項目2

${query_result}

ALBのログをアラート発生時に見たいことはよくあると思いますので、試しに使ってみてください。

Redshift Data API を使ってログを取得する設定

Cloudwatch Logs Insights と S3 Select 以外にもう一つ Redshift Data API を使ってログを取得する設定も紹介します。 prepalertブロックと ruleブロックは、先ほどのS3 Selectのときと同様ですので省略しています。

/get_orders.hcl

provider "redshift_data" {
  cluster_identifier = "warehouse"
  database           = must_env("ENV")
  db_user            = "${must_env("ENV")}__prepalert"
}

/* Serverlessをお使いの場合はこんな感じ
provider "redshift_data" {
  workgroup_name = "default"
  database       = "dev"
}
*/

query "redshift_data" "orders" {
  sql = file("./queries/get_orders.sql")
  params = {
    start_at = strftime_in_zone("%Y-%m-%d %H:%M:%S", "UTC", webhook.alert.opened_at - duration("15m"))
    end_at   = strftime_in_zone("%Y-%m-%d %H:%M:%S", "UTC", coalesce(webhook.alert.closed_at, now()))
  }
}

./queries/get_orders.sql

SELECT
  order_id
  ,order_date
  ,order_status
  ,order_total
  ,order_items
FROM orders
WHERE order_date BETWEEN (:start_at)::DATE AND (:end_at)::DATE

こうすることで、Redshiftにあるデータにアクセスして、その結果をメモに貼り付けられます。

localsブロックで設定を使いまわしたい。

Prepalertは、HCLで設定を記述します。 HCLは、Terraformで使われており、Terraformを使ってて便利だなと感じたHCL関数や機能は、Prepalertでも使えるようにしています。 その一つとして、localsブロックがあります。

例えば、文字列が特定のprefixを持っているか?というbool値を返すHCL関数 has_prefix(str, prefix) と組み合わせて、次のように使うことができます。

locals {
  org_name        = "mashiike"
  target_prefix   = "[prepalert]"
  default_message =  <<EOF
How do you respond to alerts?
Describe information about your alert response here.
EOF
}

rule "simple" {
    when = [
        webhook.org_name == local.org_name,
        has_prefix(webhook.alert.monitor_name, local.target_prefix),
    ]
    update_alert {
        memo = local.default_message
    }
}

localsブロックで定義したものは、どこでも使えるようにしているので、設定を使いまわしたいときに便利です。 このように、他のHCLで書かれる製品で便利だなと思ったものは、どんどん取り込んでいます。
この関数使えるかな?と気になった方は、こちらを見ていただければなんとなくわかるかもしれません。 github.com

メモのFullTextをS3に保存して、メモの一部とFullTextURLを貼る設定

ログをそのままアラートのメモに貼っていると、情報が多くなりすぎて困ることがおきます。 そんなときに、次のような設定をすると便利です。

./config.hcl

prepalert {
  required_version = ">=1.0.0"

  sqs_queue_name = "${must_env("ENV")}-prepalert"

  backend "s3" {
    bucket_name                 = "s3://prepalert-your-memo-data-bucket/"
    object_key_prefix           = "alerts/"
    viewer_base_url             = must_env("VIEWER_BASE_URL")
    viewer_google_client_id     = must_env("GOOGLE_CLIENT_ID")
    viewer_google_client_secret = must_env("GOOGLE_CLIENT_SECRET")
    viewer_session_encrypt_key  = "<32byte データのbase64エンコード>"
  }
}

指定したs3 bucketに、メモを保存してなおかつ簡易ビューワーが利用できるようになります。 簡易的にGoogle OAuthを利用して、ユーザーの制限をできるようにはしています。 viewer_session_encrypt_keyは、32byteのランダムなデータをbase64エンコードしたものを指定してください。 次のように作ると良いです。

$ head -c 32 /dev/random | base64

この設定をすると、アラートのメモには次のような情報が貼られるようになります。

簡易ビューワーはこんな感じになります。

まとめ

Prepalertは、アラート対応で必要となる情報をMackerelに集約するために生まれたトイル削減ツールです。
情報を拾ってくる方法に多種多様なバリエーションがあるため、設定が複雑になりがちです。
そこで、PrepalertではHCLを採用しています。
今回は、Prepalertを使う上でよく使うような設定例を紹介しました。 便利そうだなと思った方は、使っていただければ幸いです。

カヤックではSREの仕事を削減するエンジニアを募集しています

hubspot.kayac.com