はじめに

この記事では全体の構成と、
実際にハマった4つのポイントを紹介します。
Google Cloudで監視を作るのが初めての人でも、
この記事の順番どおりに進めれば
同じ構成を再現できるように書いています。

全体像:監視は4つの経路でできている

[ログ]       Google Cloud Logging → Google Cloud Pub/Sub → New Relic
[メトリクス] New Relic が Google Cloud のAPIを定期的に読み取る
[アラート]   New Relic のNRQL条件で、しきい値を超えたら検知
[通知]       New Relic のワークフロー → PagerDuty
経路 向き Terraformの主なリソース
ログ Google Cloud → New Relicへ送る google_logging_project_sink / google_pubsub_topic
メトリクス New Relicが取りに来る google_project_iam_member / newrelic_cloud_gcp_link_account
アラート New Relic内で判定 newrelic_nrql_alert_condition
通知 New Relic → 外部 newrelic_workflow ほか

ログは「こちらから送る」、
メトリクスは「向こうが取りに来る」という、
向きの違いを最初に押さえておくと混乱しません。

① ログ:シンクに権限を付けないと届かない

郵便ポスト(シンク)を置いても、
配送センター(Google Cloud Pub/Sub)に
受け取りの許可を出さないと荷物は届かない、というイメージです。

Google Cloud Loggingのシンクで対象のログを絞り込み、
Google Cloud Pub/Subのトピックへ流します。
ここで必要なのが、シンク専用の書き込み用アカウント
(writer identity)に、トピックへの発行権限を付けることです。

resource "google_logging_project_sink" "to_newrelic" {
  name                   = "${var.prefix}-sink-newrelic-01"
  destination            = "pubsub.googleapis.com/${google_pubsub_topic.newrelic_logs.id}"
  filter                 = var.log_filter
  unique_writer_identity = true
}

# シンクの writer identity に発行権限を付ける(これを忘れるとログが届かない)
resource "google_pubsub_topic_iam_member" "sink_publisher" {
  topic  = google_pubsub_topic.newrelic_logs.id
  role   = "roles/pubsub.publisher"
  member = google_logging_project_sink.to_newrelic.writer_identity
}

トピックからNew Relicへ届ける方法は2通りあります。

方式 向いている場面 注意点
プッシュ配信のサブスクリプション ログ量が少ない・まず動かしたい 設定が数行で済む
Google Cloud Dataflowのジョブ ログ量が多い ジョブの起動にライセンスキーが要る

今回はまずプッシュ配信で動かし、
ログ量が増えたらGoogle Cloud Dataflowへ切り替える方針にしました。
なお、Google Cloud DataflowのジョブをTerraformで管理すると、
ライセンスキーがtfstateに平文で残ります。
そのためジョブの起動だけはTerraformの管理外とし、
手順書に残す運用にしました。

# 取り込み用URLが設定されたときだけ、プッシュ用サブスクリプションを作る
resource "google_pubsub_subscription" "newrelic_logs_push" {
  count = var.newrelic_logs_ingest_url != "" ? 1 : 0
  name  = "${var.prefix}-sub-newrelic-logs-push-01"
  topic = google_pubsub_topic.newrelic_logs.id

  push_config {
    push_endpoint = var.newrelic_logs_ingest_url
  }
}

ただし、取り込み用URLにライセンスキーを含める形式の場合は、
プッシュ配信のサブスクリプションでもキーがtfstateに残ります。
tfstateの保存先は閲覧できる人を絞り、
キーを含むtfvarsはGitに含めないようにしておきましょう。

② メトリクス:New Relicに「見学パス」を渡す

工場見学の人に、見るだけのパスと、
入館手続き用のパスの2枚を渡すイメージです。

メトリクスはNew Relic側がGoogle CloudのAPIを定期的に読み取ります。
そのため、New Relicのサービスアカウントに閲覧の権限を付け、
監視対象のプロジェクトとしてNew Relicに登録します。

resource "google_project_iam_member" "newrelic_viewer" {
  project = var.project_id
  role    = "roles/viewer"
  member  = "serviceAccount:${var.newrelic_gcp_service_account}"
}

resource "google_project_iam_member" "newrelic_service_usage_consumer" {
  project = var.project_id
  role    = "roles/serviceusage.serviceUsageConsumer"
  member  = "serviceAccount:${var.newrelic_gcp_service_account}"
}

# New Relic側の「Add a GCP project」に相当
resource "newrelic_cloud_gcp_link_account" "this" {
  name       = "${var.prefix}-gcp"
  project_id = var.project_id
}

Google Cloud側の権限付与と、New Relic側の登録を
同じTerraformで管理できるので、
環境を増やすときも同じコードで済みます。

③ アラート:AWSのメトリクス名はGoogle Cloudには存在しない

日本の家電のリモコンで、
海外製のテレビを操作しようとしていたような状態です。

監視項目の一覧を作る際、別案件の一覧を流用した結果、
AWS Lambdaや生成AI系サービス向けのメトリクス名が残っていました。
当然、Google Cloud Runにはそのメトリクスはありません。

New Relicの公式ドキュメントを確認すると、
Google Cloud Runで取れるのは、レイテンシや
課金対象のインスタンス時間など、限られた項目でした。
また、連携方式(サービスアカウント方式/Workload Identity連携方式)によって、
取れるメトリクスの種類も変わります。

当初4項目あった監視条件のうち、2項目は対応するメトリクスが存在しませんでした。
最終的には、実在する2項目(レイテンシ・課金対象のインスタンス時間)に絞って
監視条件を作り直しました。

もう1つのハマりどころは、NRQLのWHERE句に使う属性名です。
ドキュメントの名前と実データの名前が一致しないことがあるため、
条件を書く前に実データで確認しました。

-- まず実データにどんな属性があるかを確認する
SELECT keyset() FROM GcpRunRevisionSample SINCE 1 hour ago

条件はTerraformの for_each でまとめて管理し、
項目の追加や削除はtfvarsの編集だけで済むようにしました。

resource "newrelic_nrql_alert_condition" "cloudrun" {
  for_each           = var.cloudrun_map_data
  policy_id          = newrelic_alert_policy.alert_policy.id
  type               = "static"
  name               = "${each.value.name} [CloudRun] ${each.value.category}"
  aggregation_window = 60 # 1分間隔で評価

  nrql {
    query = each.value.nrql_query
  }

  critical {
    operator              = each.value.operator
    threshold             = each.value.threshold
    threshold_duration    = 300 # 5分継続で発報
    threshold_occurrences = each.value.threshold_occurrences
  }
}

④ 通知:条件を作っただけでは誰にも届かない

火災報知器を付けても、
消防署への回線をつながないと誰も駆けつけない、というイメージです。

アラート条件を作ると、New Relic上ではissueが発生します。
ただし通知先を別途設定しないと、どこにも連絡は飛びません。
現在のNew Relicでは、次の3つのリソースを組み合わせて
通知経路を作ります。

リソース 役割
newrelic_notification_destination 送り先(PagerDutyなど)と認証情報
newrelic_notification_channel 送るメッセージの中身
newrelic_workflow どのポリシーのissueを、どのチャネルへ流すか

以前よく使われていた newrelic_alert_channel は非推奨のため、
新しく作るならこちらの構成を選びます。

また、PagerDutyの統合キーがまだ手元にない段階でも、
アラート条件だけ先に反映したい場面がありました。
そこで count を使い、
キーが空のうちは通知系のリソースを作らないようにしています。

locals {
  notification_enabled = var.pagerduty_service_key != "" ? 1 : 0
}

resource "newrelic_workflow" "pagerduty" {
  count      = local.notification_enabled
  account_id = var.nr_account_id
  name       = "${var.project_name}-wf-prd"

  issues_filter {
    name = "policy-filter"
    type = "FILTER"
    predicate {
      attribute = "labels.policyIds"
      operator  = "EXACTLY_MATCHES"
      values    = [newrelic_alert_policy.alert_policy.id]
    }
  }

  destination {
    channel_id = newrelic_notification_channel.pagerduty[0].id
  }
}

これから作る人向け:再現チェックリスト

同じ構成を作るときは、次の順番で進めると手戻りがありません。

順番 作業 できたかの確認方法
1 監視したい項目を、公式ドキュメントのメトリクス一覧と突き合わせる 全項目に対応するメトリクス名がある
2 シンク・トピック・writer identityの権限・プッシュ配信を作る New Relicで SELECT count(*) FROM Log が0件でない
3 New Relicのサービスアカウントに権限を付け、プロジェクトを登録する New RelicのGoogle Cloud連携画面にプロジェクトが表示される
4 SELECT keyset() で属性名を確かめてから、NRQL条件を書く 条件のクエリで値が返ってくる
5 destination・channel・workflowで通知経路を作る テスト通知が送り先に届く
6 キーを含むtfvarsとtfstateの扱いを確認する tfvarsがGitの管理外、tfstateの閲覧者が限定されている

各手順の右端の列が埋まれば、
監視は「作った」ではなく「動いている」と言える状態です。

まとめ

# ハマりどころ 対策
① シンクを作ってもログが届かない writer identityにトピックへの発行権限を付ける
② ライセンスキーがtfstateに残る ジョブ起動はTerraformの管理外にし、それでも残る分はtfstateの閲覧権限を絞って守る
③ 存在しないメトリクスで条件を書いていた 公式ドキュメントと実データ(keyset())で先に確認する
④ 条件だけ作って通知が飛ばない destination・channel・workflowの3点セットで経路を作る

監視は「作って終わり」ではなく、
「本当に届くか」「本当にそのデータがあるか」を
確かめるところまでが仕事でした。
まずは自分の環境で SELECT keyset() を1回実行して、
どんな属性が取れているかを見てみてください。

参考資料