概要 #
障害の連絡を受けて Cloud Monitoring を開くと、ダッシュボードが何十も並んでいます。Google Cloud が自動で用意したもの、テンプレートから入れたもの、誰かが作ったもの。どれを開けばよいのか、足りないときに自分で作るべきなのかが分かりません。
作ると決めても迷います。コンソールで作ったダッシュボードも JSON で取り出せます。ただ、作っただけでは Git などのソース管理に載りません。Terraform で管理しようとすると、今度はコンソールで触ってよいのかが分からなくなります。
この記事では、次の順で進めます。
- Google Cloud が用意したダッシュボードとテンプレートで足りるかを見る
- 足りない分を Custom Dashboard にまとめる
- フィルタと変数で、対象を切り替えられるようにする
- 共有・権限・履歴など、運用に要るものを押さえる
- JSON を取り出し、gcloud と Terraform で管理する
ステップ2では、メトリクスに加えてログ・Incident・変更イベントも同じ画面に置きます。
Metrics Explorer を開いたことがある人向けです。Terraform の基本操作(init / plan / apply)は前提とします。
扱わないものもあります。
- メトリクスの意味と集計の選び方。メトリクスの解説編に任せます
- アラートポリシー・通知チャネル・SLO の設計
- Cloud Logging のクエリの書き方。クエリ集に任せます
- サードパーティ製ダッシュボード、Grafana からの取り込み、Managed Service for Prometheus の構築
- すべてのウィジェットとサービスを網羅すること
検証環境 #
- Terraform v1.14.3、
hashicorp/googlev7.46.1 - Google Cloud SDK 562.0.0、kubectl v1.35.2(GKE v1.35.8-gke.1036000)
- リージョン
asia-northeast1
サンプルコードは次にあります。
29-cloud-monitoring-dashboard/
├── README.md
├── docs/
│ ├── PARAMETER.md
│ └── RESOURCE-PARAMETERS.md
├── DEPENDENCY-GRAPH.svg
├── dashboards/
│ └── web-service-overview.json # gcloud と Terraform が共通で読む
├── k8s/
│ └── workloads.yaml # namespace shop / blog と、落ち続ける Pod
├── scripts/
│ └── load.sh # Cloud Run に 200 / 500 / 遅延を混ぜて送る
├── versions.tf
├── provider.tf
├── variables.tf
├── network.tf
├── main.tf # GCE / GKE / Cloud Run
├── alert.tf # Cloud Run の 5xx で開くアラート
├── dashboard.tf
├── outputs.tf
└── terraform.tfvars.example
ダッシュボードに載せるデータを作るため、3種類の発生源を用意しました。
flowchart LR
subgraph src[データの発生源]
VM[GCE VM
CPU を周期的に回す]
GKE[GKE
namespace shop / blog
crasher が再起動を繰り返す]
RUN[Cloud Run
httpbin で 200 / 500 / 遅延]
end
AP[アラートポリシー
Cloud Run の 5xx > 0]
subgraph obs[Cloud Monitoring / Cloud Logging]
MET[(メトリクス)]
LOG[(ログ・監査ログ)]
INC[Incident]
end
DASH[Web Service Overview
Custom Dashboard]
VM --> MET
GKE --> MET
GKE --> LOG
RUN --> MET
RUN --> LOG
MET --> AP --> INC
MET --> DASH
LOG --> DASH
INC --> DASH
JSON[dashboards/*.json] -->|gcloud / Terraform| DASH
VM・GKE・Cloud Run には、どれも environment=test のラベルを付けています。ダッシュボードの絞り込みに使えるかを、あとで確かめます。
実行する前に、gcloud config get-value project と terraform.tfvars の project_id が同じ検証用プロジェクトを指していることを確かめます。Terraform は Application Default Credentials(gcloud auth application-default login)で認証します。
Cloud Run は allUsers に roles/run.invoker を付けて、認証なしで公開します。 手元から負荷をかけるための設定で、URL を知っていれば誰でも呼べます。検証が終わったら destroy します。
cp terraform.tfvars.example terraform.tfvars # project_id と authorized_ipv4_cidr を書く
terraform init
terraform apply
gcloud container clusters get-credentials tf-adv-dash --zone asia-northeast1-a
kubectl apply -f k8s/workloads.yaml
bash scripts/load.sh "$(terraform output -raw run_url)" 30
Apply complete! Resources: 18 added, 0 changed, 0 destroyed.
GKE ノードのサービスアカウントには、GKE が推奨する最小権限ロール roles/container.defaultNodeServiceAccount だけを付けています。この記事の測定は、同じサービスアカウントに個別のロール(ログ書き込み・メトリクス書き込みなど)を付けていた版で行いました。今のサンプルでも、コンテナのログと、コンテナ・ノードのメトリクスが届くことを確かめています。
apply のあとの確認 #
apply が通ったあと、状態を別に確かめます。plan の終了コードが 0 なら、Terraform が検出できる範囲では JSON と実物に差がありません。後述のとおり、provider が抑制する差分は plan に出ません。
terraform plan -detailed-exitcode; echo "exit=$?"
gcloud monitoring dashboards list --filter='displayName="Web Service Overview"' --format='table(displayName,labels.example)'
kubectl get pods -n blog
exit=0
DISPLAY_NAME EXAMPLE
Web Service Overview 29-cloud-monitoring-dashboard
NAME READY STATUS RESTARTS AGE
crasher-545dcbfc6c-kkks4 0/1 CrashLoopBackOff 11 (119s ago) 39m
web-659d4f7c7b-j8xvh 1/1 Running 0 39m
crasher は、30秒ごとに終了するように作っています。CrashLoopBackOff になるのは想定どおりです。
GUI の手順は公式ドキュメントに沿って書いています。 作成と更新は gcloud と Terraform で行い、コンソールでは表示だけを確かめました。
ダッシュボードの種類を整理する #
公式の概要は、ダッシュボードを3種類に分けています。
| 種類 | 誰が作るか | 編集 |
|---|---|---|
| Google Cloud dashboard | リソースを作ると Cloud Monitoring が入れる | できない |
| third-party dashboard | 対応アプリのメトリクスが届くと入る | コピーすれば編集できる |
| Custom Dashboard | 自分で作る、コピーする、テンプレートから入れる | できる |
Dashboard Template は4つ目の種類ではありません。 テンプレートの資料によれば、入れたテンプレートはプロジェクトの Custom Dashboard になります。Custom Dashboard を作る手段の1つです。
コンソールの一覧(Monitoring > ダッシュボード)では、「タイプ」の区分がこうなっていました。
すべて 56
カスタム 3
ハンドブック 11
Google サービス 42
Google サービスの中には、種類の列が「コンテキスト内」のものも混ざっていました。ハンドブックとコンテキスト内は、上に挙げた概要ページの3分類には出てきません。この記事では扱いません。
まず Google Cloud のダッシュボードを見る #
Google Cloud dashboard の資料の手順では、次のように探します。
- Google Cloud コンソールで Monitoring > ダッシュボード を開く
- タイプ で Google サービス を選ぶ
- 一覧からサービス名のダッシュボードを開く
GCE・GKE・Cloud Run のリソースを作ったあとで開くと、VM Instances、GKE、Cloud Run Monitoring などが並んでいました。42件のうち多くは、Google がサービスごとに用意したグラフの組み合わせです。
コピーできるものと、できないもの #
概要ページと詳細ページで、コピーについての記述に差があります。
| 資料 | 記述 |
|---|---|
| 概要 | 変更もコピーもできない。グラフは Custom Dashboard へコピーできる |
| Google Cloud dashboard | すべてがコピーできるわけではない。コピーのアイコンが無ければコピーできない |
実際の一覧は、詳細ページの記述どおりでした。右端の「操作」列に、コピーのアイコンがあるものと無いものが混ざっています。
| ダッシュボード | コピーのアイコン |
|---|---|
VM Instances、GKE、Firewalls、Google Cloud Load Balancers |
無い |
Cloud Run Monitoring、GKE Cluster Monitoring、GCE VM Instance Monitoring など |
ある |
アイコンが無かった4つのうち、GKE・Firewalls・Google Cloud Load Balancers は、説明文が「リソースの一覧を表示する」でした。VM Instances はグラフの説明で、一覧型だけが対象というわけでもありません。どれがコピーできるかの基準は、公式には書かれていません。
コピーしたダッシュボードは Custom の区分に入り、編集できます。近いものをコピーして不要なウィジェットを削除するのも、Custom Dashboard を作る手軽な方法の1つです。
グラフ単位で持ち出す #
ダッシュボード全体が要らないときは、グラフだけを持ち出せます。同じ資料によれば、グラフのメニューから次のどちらかを選びます。
- Clone widget — そのまま別の Custom Dashboard へ複製する。表示されないグラフもある
- View in Metrics Explorer — Metrics Explorer で開き、集計や絞り込みを変えてから Save as > Dashboard chart で保存する
Metrics Explorer では集計や絞り込みを変えられるので、障害調査でさらに掘り下げるときに使えます。使える形になったら、そのまま Custom Dashboard へ置けます。
Custom Dashboard のグラフのメニューには、日本語のコンソールで次の項目が並んでいました。
全画面で表示 / グラフの凡例を開く / モード / ダウンロード
Metrics Explorer で表示する / 関連するログを検査
ウィジェットを複製 / ウィジェットを削除
「関連するログを検査」を使えば、グラフの山からログエクスプローラへ移れます。
API からは見えない #
Google Cloud dashboard は、Dashboards API の対象外です。API の資料に「取得・編集・削除できない」とあります。
実際、コンソールで56件見えていたときに gcloud で一覧を取りました。返ったのは、自分で作成した Custom Dashboard の3件だけです。内訳は、gcloud と Terraform で作った2件と、テンプレートから入れた1件です。
gcloud monitoring dashboards list --format='table(name.basename(),displayName)'
NAME DISPLAY_NAME
66bacfff-5973-4025-9325-ed591a327503 Web Service Overview
81141e17-3b4f-4780-97a1-450c3b922efa Web Service Overview (gcloud v4)
9711cc42-897c-494a-b512-21df368b1693 Cloud Run Monitoring
Google Cloud dashboard を Terraform で管理したり、JSON で取り出したりはできません。 コードで管理したいなら、コピーするか自分で作った Custom Dashboard が対象になります。
公式テンプレートを入れる #
自分で作る前に、テンプレートに近いものが無いかを見ます。
テンプレートの実体は JSON です。GitHub の monitoring-dashboard-samples に置かれています。コンソールでは次の手順で入れます。
- Monitoring > ダッシュボード > ダッシュボード テンプレートを表示
- テンプレートを探してプレビューする
- 追加し、ダイアログで名前とラベルを決める
同じ JSON は gcloud でも入れられます。Cloud Run 用のテンプレートをリポジトリのコミット 02e04b0 から取得して入れました。
curl -sLo cloudrun-monitoring.json \
https://raw.githubusercontent.com/GoogleCloudPlatform/monitoring-dashboard-samples/02e04b0/dashboards/google-cloud-run/cloudrun-monitoring.json
gcloud monitoring dashboards create --config-from-file=cloudrun-monitoring.json
Created [9711cc42-897c-494a-b512-21df368b1693].
テンプレートの dashboardFilters は、project_id・location・service_name の3つの pinned filter でした。自分で作るときの手本になります。
テンプレートの labels は空でした。一覧をラベルで絞る運用なら、入れるときにラベルを付けます。
コピーできた Google Cloud dashboard の一部は、テンプレートと同じ中身でした。
Google サービスの Cloud Run Monitoring はグラフが7つで、Cloud Run 用テンプレートと同じ構成です。開くと右上のボタンが「ダッシュボードをコピー」になっていました。テンプレートを入れる代わりに、これをコピーして始める手もあります。
Custom Dashboard が必要になるとき #
Google Cloud dashboard もテンプレートも、サービスごとにまとまっています。次のような見方をしたくなったら、Custom Dashboard を作ります。
| したいこと | Google Cloud dashboard で困る点 |
|---|---|
| Cloud Run の 5xx と GKE の再起動を並べて見る | サービスごとに画面が分かれる |
| 自分の namespace や環境だけに絞る | 用意されたフィルタしか無い |
| メトリクスの山と ERROR ログを同じ時間軸で見る | ログが無い画面がある |
| 障害対応の手順やリンクをダッシュボード上に表示しておく | 編集できない |
Web Service Overview を作る #
1つの Web サービスを想定して、次の3段に分けました。
- Availability — 利用者に見えるもの(リクエスト数・5xx・レイテンシ)
- Resources — 原因になりやすいもの(CPU・メモリ・再起動)
- Troubleshooting — 原因を追うもの(レイテンシ分布・ERROR ログ・Incident)
GUI で作る手順 #
Custom Dashboard の資料の手順です。
- Monitoring > ダッシュボード > カスタム ダッシュボードを作成します
- ウィジェットを追加 から種類を選び、設定ペインでメトリクスを指定する
- ツールバーの 保存 を押す
グラフは Metrics Explorer から Save as > Dashboard chart でも追加できます。
置いたメトリクス #
メトリクスは解説編で扱ったものを再利用しました。
| 段 | ウィジェット | メトリクス | 集計 |
|---|---|---|---|
| Availability | 積み上げ棒 | run.googleapis.com/request_count |
ALIGN_RATE、response_code_class ごとに合計 |
| スコアカード | 同上(5xx だけ) |
ALIGN_DELTA 5分、合計。0 を超えたら赤 |
|
| 折れ線 | run.googleapis.com/request_latencies |
ALIGN_DELTA、REDUCE_PERCENTILE_95 |
|
| Resources | 折れ線 | compute.googleapis.com/instance/cpu/utilization |
ALIGN_MEAN |
| 折れ線 | kubernetes.io/container/memory/used_bytes(non-evictable) |
ALIGN_MEAN、namespace ごとに合計 |
|
| 表 | kubernetes.io/container/restart_count |
ALIGN_DELTA 10分、Pod ごと |
|
| Troubleshooting | ヒートマップ | run.googleapis.com/request_latencies |
ALIGN_DELTA、合計 |
| Logs パネル | severity>=ERROR(システムの namespace を除外、後述) |
— | |
| Incident 一覧 | — | — |
ウィジェットと同じメトリクス・フィルタを API で引き、データが届いているかを確かめました。集計はデータの有無を見るために変えています(リクエスト数は5分の合計、再起動は10分の差分)。
API は Monitoring API の timeSeries.list です。何度も呼ぶので、次の関数にまとめました。YOUR_PROJECT_ID は自分のプロジェクト ID に置き換えます。
# Monitoring API の timeSeries.list を直近20分で引く
# 引数: フィルタ / 時系列ごとの集計 / 集計の間隔 / 時系列をまとめる方法 / まとめる単位(カンマ区切り)
ts() {
local args=(
--data-urlencode "filter=$1"
--data-urlencode "interval.startTime=$(date -u -d '-20 min' +%Y-%m-%dT%H:%M:%SZ)"
--data-urlencode "interval.endTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
--data-urlencode "aggregation.alignmentPeriod=$3"
--data-urlencode "aggregation.perSeriesAligner=$2"
)
[ -n "${4:-}" ] && args+=(--data-urlencode "aggregation.crossSeriesReducer=$4")
[ -n "${5:-}" ] && for g in ${5//,/ }; do args+=(--data-urlencode "aggregation.groupByFields=$g"); done
curl -sG -H "Authorization: Bearer $(gcloud auth print-access-token)" \
"https://monitoring.googleapis.com/v3/projects/YOUR_PROJECT_ID/timeSeries" "${args[@]}"
}
3つのウィジェットに当たるメトリクスを引きます。jq では、時系列ごとのラベルと最新の値だけを取り出しています。
echo '== run request_count by class (ALIGN_DELTA 300s sum)'
ts 'metric.type="run.googleapis.com/request_count" resource.type="cloud_run_revision"' \
ALIGN_DELTA 300s REDUCE_SUM metric.label.response_code_class \
| jq -r '.timeSeries[]? | "\(.metric.labels.response_code_class) \(.points[0].interval.endTime) \(.points[0].value.int64Value) pts=\(.points|length)"'
echo '== gke memory by ns'
ts 'metric.type="kubernetes.io/container/memory/used_bytes" resource.type="k8s_container" metric.label.memory_type="non-evictable"' \
ALIGN_MEAN 60s REDUCE_SUM resource.label.namespace_name \
| jq -r '.timeSeries[]? | "\(.resource.labels.namespace_name) \(.points[0].value.doubleValue // .points[0].value.int64Value)"'
echo '== gke restarts (600s delta)'
ts 'metric.type="kubernetes.io/container/restart_count" resource.type="k8s_container" resource.label.namespace_name=monitoring.regex.full_match("shop|blog")' \
ALIGN_DELTA 600s REDUCE_SUM resource.label.namespace_name,resource.label.pod_name \
| jq -r '.timeSeries[]? | "\(.resource.labels.namespace_name) \(.resource.labels.pod_name) \(.points[0].value.int64Value)"'
== run request_count by class (ALIGN_DELTA 300s sum)
2xx 2026-09-24T10:55:54Z 777 pts=3
5xx 2026-09-24T10:55:54Z 37 pts=3
== gke memory by ns
blog 3530752
gmp-system 41623552
kube-system 302497792
shop 6320128
== gke restarts (600s delta)
blog crasher-545dcbfc6c-kkks4 1
blog web-659d4f7c7b-j8xvh 0
shop web-659d4f7c7b-247pp 0
shop web-659d4f7c7b-wrvz8 0
リクエスト数は 2xx と 5xx に分かれ、メモリは namespace ごとに返りました。直近10分で再起動したのは crasher だけです。
GCE の CPU には、VM に加えて GKE のノードも出ました。GKE のノードも gce_instance として数えられるからです。VM だけを見たいなら、ラベルか名前で絞ります。
ウィジェットの選び方 #
ウィジェットの種類は多いので(API 定義)、全部を覚えるより、何を見たいかで選びます。
| 見たいもの | ウィジェット | この画面での例 |
|---|---|---|
| 時間の流れ | 折れ線(XY chart) | CPU、メモリ、p95 |
| 内訳の比率 | 積み上げ棒 | 2xx と 5xx の割合 |
| いまの値と、しきい値を超えたか | スコアカード | 直近5分の 5xx 件数 |
| 複数の対象を並べて比べる | 時系列の表 | Pod ごとの再起動回数 |
| 分布(一部だけ遅いのか) | ヒートマップ | レイテンシの分布 |
| 原因の手がかり | Logs パネル | ERROR ログ |
| 障害が起きているか | Incident 一覧 | 開いている Incident |
| 画面の区切り | セクション見出し | Availability / Resources / Troubleshooting |
| 手順やリンク | テキスト(Markdown) | この画面では使っていない |
作成したダッシュボードには、レイテンシのウィジェットが2つあります。「Cloud Run: latency p95」(折れ線)と「Cloud Run: latency distribution」(ヒートマップ)で、どちらも同じ request_latencies から作っています。
p95 の折れ線は1本の線に要約するので、1分に数回しか来ない遅いリクエストで大きく跳ねました。ヒートマップなら、速いリクエストの帯と遅いリクエストの点が分かれて見えます。
フィルタと変数で絞り込む #
ダッシュボードを絞る仕組みは3つあります。
| 保存されるか | 効く範囲 | 作るのに要るロール | |
|---|---|---|---|
| temporary filter | されない。ページを再読み込みすると消える | 同じラベルを持つウィジェット | Monitoring 閲覧者 |
| pinned filter | される | 同じラベルを持つウィジェット | Monitoring 編集者 |
変数($ で始まる) |
される | クエリで参照したウィジェットだけ | Monitoring 編集者 |
出典は、temporary filter が専用の資料、pinned filter と変数が変数と pinned filter の資料です。ロールは、どちらもコンソールで操作する場合の記述です。
障害調査で一時的に絞るなら temporary filter、画面の仕様として残すなら pinned filter か変数です。 「permanent filter」という呼び方を見かけることがあります。現在の資料では pinned filter です。
作ったダッシュボードには、pinned filter を1つと変数を2つ置きました。
{
"dashboardFilters": [
{
"filterType": "USER_METADATA_LABEL",
"labelKey": "environment",
"stringValue": "test",
"valueType": "STRING"
},
{
"filterType": "RESOURCE_LABEL",
"labelKey": "cluster_name",
"templateVariable": "cluster",
"valueType": "STRING"
},
{
"filterType": "RESOURCE_LABEL",
"labelKey": "namespace_name",
"templateVariable": "namespace",
"valueType": "STRING"
}
]
}
変数は、GKE のウィジェットのフィルタで参照しています。
metric.type="kubernetes.io/container/memory/used_bytes" resource.type="k8s_container" metric.label."memory_type"="non-evictable" ${cluster} ${namespace}
environment ラベルで絞れたのは GCE だけだった #
VM・GKE クラスタ・ノードプール・Cloud Run に、同じ environment=test を付けました。metadata.user_labels.environment で絞ったとき、何本の時系列が返るかをリソースの種類ごとに数えました。
数えるのには、前の節の ts 関数を使いました。リソースの種類ごとに、ラベルのフィルタを付けない場合(all)と付けた場合(with_env_test)の2回引き、返った時系列の本数を比べます。userLabelKeys は、レスポンスに入っていたユーザーラベルのキーの一覧です。
for m in 'compute.googleapis.com/instance/cpu/utilization|gce_instance|ALIGN_MEAN' \
'kubernetes.io/container/memory/used_bytes|k8s_container|ALIGN_MEAN' \
'kubernetes.io/node/cpu/allocatable_utilization|k8s_node|ALIGN_MEAN' \
'run.googleapis.com/request_count|cloud_run_revision|ALIGN_DELTA'; do
IFS='|' read -r mt rt al <<<"$m"
all=$(ts "metric.type=\"$mt\" resource.type=\"$rt\"" "$al" 300s | jq '[.timeSeries[]?]|length')
lab=$(ts "metric.type=\"$mt\" resource.type=\"$rt\" metadata.user_labels.environment=\"test\"" "$al" 300s | jq '[.timeSeries[]?]|length')
keys=$(ts "metric.type=\"$mt\" resource.type=\"$rt\"" "$al" 300s | jq -c '[.timeSeries[]?.metadata.userLabels // {} | keys[]]|unique')
echo "$rt all=$all with_env_test=$lab userLabelKeys=$keys"
done
gce_instance all=2 with_env_test=2 userLabelKeys=[]
k8s_container all=62 with_env_test=0 userLabelKeys=[]
k8s_node all=0 with_env_test=0 userLabelKeys=[]
cloud_run_revision all=2 with_env_test=0 userLabelKeys=[]
userLabelKeys が4種類とも空なのは、このレスポンスに metadata が入っていなかったためです。ラベルで絞れたかどうかは with_env_test の本数で見ます。k8s_node はフィルタなしでも0本で、比べられませんでした。
一致したのは GCE だけで、VM と GKE のノードの2台です。今回の環境では、クラスタと Cloud Run に付けた environment ラベルでは、コンテナとリビジョンを絞れませんでした。
k8s_container については、Pod に付けたラベルでは一致しました。metadata.user_labels.app="crasher" で引くと crasher の Pod が返っています。一方、クラスタとノードプールに付けた environment では一致しませんでした。GKE のワークロードを環境で絞りたいなら、Pod にラベルを付けて試すのが近道です。
メトリクスモデルの資料は、ユーザーが作れるラベルの例に VM を挙げています。どのリソースが対象かの一覧はありません。この記事で確かめたのは、上の3種類だけです。
同じ資料は、metadata label は非同期に付くため、アラートなどの選択に頼らないよう注意しています。ダッシュボードで眺める用途に限るのが無難です。
pinned filter はラベルを持たないウィジェットには適用されない #
environment: test がかかった状態でも、Cloud Run と GKE のグラフにはデータが出ていました。資料のとおりです。
A pinned filter is ignored by a widget when the widget includes a filter for the same label key, or when the data displayed by the widget doesn’t contain the label key specified in the pinned filter.
pinned filter は、そのラベルを持たないウィジェットには適用されません。 空になるのではなく、絞られないまま表示されます。画面にデータが出ていても、全部が絞られているとは限りません。
変数は参照したウィジェットだけを切り替える #
$namespace が * のあいだ、再起動の表には kube-system や gmp-system の Pod も並びました。blog に切り替えると、次のように変わりました。
| ウィジェット | $namespace: blog にしたとき |
|---|---|
| GKE のメモリ | blog の1本だけになった |
| GKE の再起動 | blog の2つの Pod だけになった |
| Cloud Run の3つ、GCE の CPU | 変わらない |
| ERROR logs、Incidents | 変わらない |
変わったのは、フィルタに ${namespace} を書いた2つだけです。Logs パネルでも絞りたいなら、そのクエリにも変数を書きます。
1つの操作で画面全体を切り替えたいなら、切り替えたいウィジェットすべてに変数を書きます。 pinned filter でも一括で効きますが、対象はそのラベルを持つウィジェットだけです。Cloud Run のように namespace_name を持たないウィジェットは切り替わりません。
ログ・Incident・イベントを同じ画面に置く #
メトリクスで「いつ・どこで」が分かっても、「なぜ」は別の場所にあります。同じダッシュボードに置いておくと、画面を行き来せずに済みます。
severity>=ERROR だけでは GKE 基盤の Pod のログが大半になった #
最初は severity>=ERROR だけを指定しました。この環境の30分ぶんを数えると、中身の大半は GKE 基盤の Pod のログでした。kube-system の fluentbit や node-cache、gmp-system の operator です。
1070 kube-system fluentbit
272 kube-system node-cache
84 kube-system core-metrics-exporter
69 kube-system gke-metrics-agent
19 kube-system autoscaler
16 shop nginx
11 gmp-system operator
8 blog nginx
3 blog app
GKE の資料「About GKE logs」の「Best practices」の節に、次の記述があります。
By default, logs written to the standard output are on the
INFOlevel and logs written to the standard error are on theERRORlevel.
GKE では、重大度を明示していない stderr のログは、既定で ERROR として Cloud Logging に取り込まれます。そのため、アプリケーションが単に stderr へ出力したメッセージでも、severity>=ERROR の検索結果に含まれることがあります。
同じ節によれば、構造化ログで severity を明示した場合は、その値が使われます。
nginx の起動メッセージも ERROR として並びました。実際に見たい crasher のログ(blog の app)は3件だけでした。
システムの namespace を外すと、Cloud Run の 500 と shop / blog のログだけが残ります。パネルには、Cloud Run の 500 と crasher の fatal: config file not found が並びました。
severity>=ERROR
NOT resource.labels.namespace_name=("kube-system" OR "gmp-system")
資料によれば、ダッシュボードのフィルタは Logs パネルのクエリにも適用されます。変数は、前の節で見たとおり、クエリに ${変数名} を書いたときだけ効きます。
Incident 一覧 #
アラートポリシーを1つ作り、Cloud Run の 5xx が1件でもあれば開くようにしました。Incident 一覧のウィジェットには、ポリシー名とラベル service_name: tf-adv-dash-api 付きで Incident が表示されました。
5xx のスコアカードが赤くなっている時間帯と、Incident が開いた時刻を同じ画面で突き合わせられます。
イベント(アノテーション) #
ツールバーの アノテーション から、グラフ上にイベントの印を出せます(資料)。JSON では annotations.eventAnnotations に種類を並べます。
{
"annotations": {
"eventAnnotations": [
{ "eventType": "CLOUD_ALERTING_ALERT", "enabled": true },
{ "eventType": "CLOUD_RUN_DEPLOYMENT", "enabled": true },
{ "eventType": "GKE_WORKLOAD_DEPLOYMENT", "enabled": true },
{ "eventType": "GKE_POD_CRASH", "enabled": true }
]
}
}
グラフには、Alert が開いた印のほか、数分おきに警告の印が並びました。crasher が再起動する間隔と重なりますが、警告の印は開いて中身を確かめていません。
イベントがどう識別されるかは、資料の「How events are identified」にあります。Alert は Cloud Monitoring が識別します。Personalized Service Health は、Service Health API への問い合わせで識別します。
それ以外のイベントは、Cloud Logging のログエントリを解析して識別されます。 Cloud Run のデプロイについては、イベントの種類の資料に、google.cloud.run.v1.Services.ReplaceService の監査ログを使った検索例があります。
ところが、更新の方法によって記録される API が違いました。
2026-09-24T10:57:54.708292Z google.cloud.run.v2.Services.UpdateService # terraform apply
2026-09-24T10:57:37.069401Z google.cloud.run.v1.Services.ReplaceService # gcloud run services update
2026-09-24T10:43:30.993976Z google.cloud.run.v2.Services.CreateService # terraform apply(作成)
Terraform の google_cloud_run_v2_service は v2 API を呼びます。資料のクエリに当てはまるのは、gcloud での更新だけです。
それでも Cloud Run Monitoring には、19:57 に「2」と書かれた印が出ました。開くと、2件とも「Cloud Run のデプロイ」でした。
2 件のログベースのイベント
Cloud Run のデプロイ asia-northeast1 のサービス tf-adv-dash-api | 9月24日 19:57
Cloud Run のデプロイ asia-northeast1 のサービス tf-adv-dash-api | 9月24日 19:57
同じ分に2件なので、これだけではどちらが v2 由来かを切り分けられません。そこで、gcloud を使わず Terraform だけで2分あけて2回更新しました。監査ログには v2 の UpdateService だけが残り、グラフにも2件の印が出ました。
2026-09-24T11:54:55.566359Z google.cloud.run.v2.Services.UpdateService # 20:54 JST
2026-09-24T11:52:50.443825Z google.cloud.run.v2.Services.UpdateService # 20:52 JST
2 件のログベースのイベント
Cloud Run のデプロイ asia-northeast1 のサービス tf-adv-dash-api | 9月24日 20:54
Cloud Run のデプロイ asia-northeast1 のサービス tf-adv-dash-api | 9月24日 20:52
今回の環境では、Terraform(v2 API の UpdateService)での更新もデプロイのイベントとして表示されました。 資料に書かれているクエリは v1 の ReplaceService です。Terraform で運用するなら、自分の環境でもデプロイの印が出るかを一度確かめておくと安心です。作成(CreateService)の時刻には印が出ていませんでした。
運用する #
共有と IAM #
ダッシュボードは、その URL を相手に伝えて共有します。ツールバーの 共有 からも送れます(資料)。受け取った人に権限が無ければ、URL を開いても表示できません。
必要なロールは、やりたいことで分かれます。
| したいこと | ロール | 資料 |
|---|---|---|
| ダッシュボードとグラフのデータを見る | Monitoring 閲覧者(roles/monitoring.viewer) |
アクセス制御 |
| temporary filter で一時的に絞る | 同上 | temporary filter |
| ダッシュボードを作る・直す | Monitoring ダッシュボード構成編集者(roles/monitoring.dashboardEditor) |
アクセス制御 |
| pinned filter と変数をコンソールで作る・直す | Monitoring 編集者(roles/monitoring.editor) |
変数と pinned filter |
| 既存の Logs パネルを見る | 対象ログの閲覧権限。標準的にはログ閲覧者(roles/logging.viewer)。ログビューならログビューアクセサー(roles/logging.viewAccessor) |
Logs パネル |
| Logs パネルを追加する | Monitoring 編集者、ログ閲覧者(roles/logging.viewer)。ログビューを使うならログビューアクセサー(roles/logging.viewAccessor) |
Logs パネル |
roles/monitoring.dashboardViewer だけではグラフが空になります。 このロールが持つのはダッシュボードの設定を読む権限だけです。時系列を読む monitoring.timeSeries.list を持っていません。
既存の Logs パネルを見る場合も、対象のログを閲覧できる権限が必要です。 Logs パネルの資料によれば、パネルには、見る人が閲覧権限を持つログエントリだけが表示されます。標準的にはログ閲覧者(roles/logging.viewer)を使い、ログビューを参照する場合はログビューアクセサー(roles/logging.viewAccessor)も要ります。この記事では、IAM の組み合わせを実際には試していません。
ロールは、ログを見るプロジェクトごとに付けます。ログ閲覧者は「ログを見たい各プロジェクト」、ログビューアクセサーは「ログバケットを持つ各プロジェクト」です。
ラベル・コピー・バージョン履歴 #
Custom Dashboard の資料にある管理機能です。
| 機能 | 使いどころ |
|---|---|
| ラベル | 一覧を持ち主や用途で絞る |
| コピー | 手本から派生させる。別プロジェクトへ持っていく |
| バージョン履歴 | 誤って壊したときに戻す。過去の版は90日保持、最新版は無期限 |
JSON の labels に書いたラベルは、コンソールの一覧の左側「ラベル」に example として出ました。選ぶと、ラベルを持つ2件だけに絞られました。
バージョン履歴は、設定(歯車)から開きます。Terraform で作ったダッシュボードでは「2件のリビジョン」が1つのグループにまとまっていました。作成と、Logs パネルのフィルタを直した apply の2回です(後述の削除と作り直しより前の時点)。
API から加えた変更も履歴に載り、コンソールで差分を比べられます。 履歴には、JSON に書いていない既定値("filter": "" など)も展開されて表示されていました。
コード管理する #
Custom Dashboard の JSON を取り出す #
作ったダッシュボードは JSON で取り出せます。
gcloud monitoring dashboards describe DASHBOARD_ID --format=json > current.json
jq -c 'keys' current.json
["annotations","dashboardFilters","displayName","etag","labels","mosaicLayout","name"]
mosaicLayout.tiles の1つ1つが、位置(xPos / yPos / width / height)とウィジェットの組です。
送った JSON と、返ってくる JSON は同じではありません。 項目のパスで比べると、2種類の違いがありました。
| 違い | 例 |
|---|---|
| 送った値が消える | "xPos": 0、"yPos": 0 |
| 送っていない値が増える | xyChart の "targetAxis": "Y1" |
この違いが、あとの Terraform で効いてきます。
gcloud で作り直す #
JSON があれば、別のプロジェクトにも同じ構成のダッシュボードを作れます。API の資料にあるコマンドです。
gcloud monitoring dashboards create --config-from-file=web-service-overview.json
gcloud monitoring dashboards list
gcloud monitoring dashboards describe DASHBOARD_ID --format=json
gcloud monitoring dashboards update DASHBOARD_ID --config-from-file=updated.json
gcloud monitoring dashboards delete DASHBOARD_ID
Dashboards API のメソッドは create / delete / get / list / patch の5つで、削除を取り消すメソッドはありません。消す前に describe で JSON を保存しておきます。
describe で取り出した JSON を create に使うときは、name と etag を消してから渡します。どちらも取り出し元のダッシュボードを指す値で、新しく作るダッシュボードには当てはまりません。
jq 'del(.name, .etag)' current.json > new.json
gcloud monitoring dashboards create --config-from-file=new.json
Terraform で管理しているダッシュボードを gcloud で消すと、次の plan は作り直しを提案しました。
Note: Objects have changed outside of Terraform
# google_monitoring_dashboard.web has been deleted
# google_monitoring_dashboard.web will be created
Plan: 1 to add, 0 to change, 0 to destroy.
apply で戻りますが、ダッシュボードの ID は新しくなりました(66bacfff-… から 0217702c-…)。同じ JSON から作り直しても、同じダッシュボード ID には戻りません。
更新には最新の etag が要る #
update では etag に注意します。 更新する JSON には、直前に取得した etag を入れておく必要があります。3通り試しました。
| 送った JSON | 結果 | 終了コード |
|---|---|---|
etag なし |
拒否 | 1 |
古い etag(あとから更新されている) |
拒否 | 1 |
最新の etag |
更新された | 0 |
ERROR: (gcloud.monitoring.dashboards.update) INVALID_ARGUMENT: Update Dashboard should specify a non empty etag.
ERROR: (gcloud.monitoring.dashboards.update) ABORTED: The supplied etag is not up to date. Please get the updated version of projects/PROJECT_NUMBER/dashboards/81141e17-3b4f-4780-97a1-450c3b922efa. 5b9735a594907f742b894f53e2b97186
API リファレンスでは、etag は同時の更新が互いを上書きしないための楽観的な排他制御だと説明されています。describe で取り直し、差分を当ててから update します。
削除したあとの describe は NOT_FOUND(終了コード1)でした。
Terraform で管理する #
Terraform では google_monitoring_dashboard を使い、gcloud と同じ JSON ファイルを渡します。
resource "google_monitoring_dashboard" "web" {
dashboard_json = file("${path.module}/dashboards/web-service-overview.json")
}
座標としきい値の 0 で、毎回差分が出た #
apply の直後に plan を流すと、何も変えていないのに差分が出ました。
~ resource "google_monitoring_dashboard" "web" {
~ dashboard_json = jsonencode(
~ {
- etag = "0670e935cf5678338b2a84bcd875b9b7"
~ mosaicLayout = {
~ tiles = [
~ {
+ xPos = 0
+ yPos = 0
...
- targetAxis = "Y1"
原因を分けるため、JSON から xPos: 0 と yPos: 0 だけを消して plan し直しました。targetAxis は書き足していません。
jq '.mosaicLayout.tiles |= map(with_entries(select(.value != 0 or (.key|IN("xPos","yPos")|not))))' \
web-service-overview.json > tmp.json && mv tmp.json web-service-overview.json
terraform plan -detailed-exitcode
終了コードは 0 で、差分は消えました。原因は座標の 0 で、API が補う targetAxis のほうは差分になりませんでした。
スコアカードのしきい値でも同じことが起きました。"value": 0 を書くと、apply のあとも + value = 0 が出続けました。value のキーごと省くと、差分は消えました(しきい値は 0 のまま)。
provider の資料には、この挙動に関わる説明があります。
To prevent permanent diffs from default values, Terraform will attempt to suppress diffs where the value is returned in the JSON string but doesn’t exist in the configuration.
「API が返すが設定に無い」値は無視されます。targetAxis はこれに当たります。
逆向きの「設定にあるが API が返さない」値は無視されず、今回は xPos / yPos / しきい値の value の 0 がそれでした。確かめたのはこの3か所で、hashicorp/google v7.46.1 での結果です。ほかの項目でも起きうるので、apply のあとに plan で差分が消えたことを確かめます。
同じ仕組みの裏返しで、キーを消しただけの変更は plan に出ません。 資料は、キーの削除を反映させたいときは、ほかの変更と一緒に行うよう求めています。
Terraform 管理のダッシュボードをコンソールで編集しない #
API の資料には、Terraform で作ったダッシュボードはコンソールで編集しないよう書かれています。編集すると、ダッシュボードが再スケールされるためです。
コンソールで開くと、右上の Autosave が ON になっていました。開いて時間範囲や $namespace を切り替えたあと、UpdateDashboard の監査ログは見つからず、plan も差分なしでした。
ウィジェットを動かすなど、構成を変える操作は試していません。自動で保存される可能性があるので、Terraform 管理のダッシュボードを試しに触るなら、コピーしてから触ります。
既存のダッシュボードを Terraform の管理に移すときは、import ブロックを使えます。
import {
to = google_monitoring_dashboard.web
id = "projects/YOUR_PROJECT_ID/dashboards/DASHBOARD_ID"
}
補足:ダッシュボード操作を監査ログで確認する #
ダッシュボードの作成・更新は、管理アクティビティの監査ログに残りました。誰がいつ変えたかを追うときに使えます。
次のコマンドで探しました。
logName:"cloudaudit.googleapis.com%2Factivity"— 管理アクティビティの監査ログだけに絞るprotoPayload.serviceName="monitoring.googleapis.com"— Cloud Monitoring の API への操作だけに絞るprotoPayload.methodName:"Dashboard"— メソッド名にDashboardを含むもの(CreateDashboardやUpdateDashboard)だけに絞る--freshness=2h— 直近2時間のログを対象にする
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.serviceName="monitoring.googleapis.com" AND protoPayload.methodName:"Dashboard"' \
--freshness=2h \
--format='table(timestamp,protoPayload.methodName,protoPayload.status.code,protoPayload.request.dashboard.displayName)'
TIMESTAMP METHOD_NAME CODE DISPLAY_NAME
2026-09-24T10:45:13.922835Z google.monitoring.dashboard.v1.DashboardsService.CreateDashboard Cloud Run Monitoring
2026-09-24T10:44:40.524625Z google.monitoring.dashboard.v1.DashboardsService.UpdateDashboard 10 Web Service Overview (gcloud v5)
2026-09-24T10:44:25.897686Z google.monitoring.dashboard.v1.DashboardsService.UpdateDashboard Web Service Overview (gcloud v4)
2026-09-24T10:44:20.653822Z google.monitoring.dashboard.v1.DashboardsService.UpdateDashboard Web Service Overview (gcloud v3)
2026-09-24T10:44:11.124386Z google.monitoring.dashboard.v1.DashboardsService.CreateDashboard Web Service Overview (gcloud)
2026-09-24T10:43:30.999139Z google.monitoring.dashboard.v1.DashboardsService.CreateDashboard Web Service Overview
古い etag で拒否された更新は、CODE 10(ABORTED)として残りました。
etag なしで拒否された2回は、見つかりませんでした。 時間帯を 10:43〜10:50(UTC)に絞り、activity・data_access・system_event の監査ログを探した結果です。拒否された操作まで監査ログで追いたい場合は、どの操作がどのログに残るかを、事前に自分の環境で試しておくと確実です。
どれを使うか #
| 状況 | 選ぶもの |
|---|---|
| まず全体を見たい、サービス単体の障害を見たい | Google Cloud dashboard |
| 近いものがテンプレートにある | Dashboard Template を導入し、不要なウィジェットを削除する |
| 自分の画面を手早く作りたい、試しながら決めたい | コンソールで Custom Dashboard |
| 複数のプロジェクト・環境で同じ画面を持ちたい | JSON を gcloud か Terraform で |
| レビューを通して変更したい | Terraform(コンソールでは編集しない) |
コンソールで作り始めて、形が決まったら JSON を取り出して Terraform に移すのが、手戻りの少ない順番です。そのときは name と etag を消します。今回差分の原因になった 0 の値(座標・しきい値)も外し、plan で差分が無いことを確かめます。
後片付け #
pkill -f 'scripts/load.sh' # 負荷をかけ続けている場合
gcloud monitoring dashboards delete DASHBOARD_ID # テンプレートなど gcloud で作ったもの
terraform destroy
検証用プロジェクトで実行してください。 terraform destroy は VM・GKE クラスタ・Cloud Run・ダッシュボード・アラートポリシーを消し、元に戻せません。ダッシュボードは、事前に describe で JSON を保存しておけば作り直せます。ただし ID は変わります。
terraform destroy が消すのは、state で管理しているリソースだけです。gcloud やテンプレートで入れたもの、コンソールでコピーしたものは、import していなければ残ります。gcloud monitoring dashboards list で残りを確かめます。
このサンプルは disable_on_destroy = false なので、有効にした API は destroy 後も有効のままです。
主なインフラの課金対象は、GKE ゾーンクラスタ・Spot ノード・VM・Cloud Run・Cloud NAT です。ディスクや通信量を含め、実行前に各サービスの料金を確認してください。
加えて、Cloud Logging と Cloud Monitoring にも使用量に応じた料金があります。
- Cloud Logging:ログバケットに保存されるログデータ(Logging storage)。プロジェクトごとに毎月50GiBまで無料
- Cloud Monitoring:API で読み取った時系列の件数など。請求先アカウントごとに毎月100万件まで無料で、コンソールからの読み取りは無料(Cloud Shell から実行したものを除く)
この記事の timeSeries.list の実行も、API での読み取りに当たります。
まとめ #
- ダッシュボードは Google Cloud / third-party / Custom の3種類。 テンプレートは Custom を作る手段
- Google Cloud dashboard は API から見えない。 コンソールで56件見えていても、
gcloud monitoring dashboards listに出たのは、自分で作成した(テンプレートから入れたものを含む)Custom Dashboard の3件だけ - Google Cloud dashboard にはコピーできるものとできないものがある。
VM InstancesやGKEにはアイコンが無く、Cloud Run Monitoringなどにはあった - 絞り込みは temporary filter・pinned filter・変数の3つ。 保存されるかと、効く範囲が違う
- pinned filter は、そのラベルを持たないウィジェットには適用されない。
environment=testで絞れたのは GCE だけで、Cloud Run と GKE のグラフは絞られないまま表示された - GKE のコンテナは、Pod のラベルでは絞れたが、クラスタのラベルでは絞れなかった
- 変数は、クエリで参照したウィジェットだけを切り替える。
$namespaceを変えても、Cloud Run・GCE・Logs パネル・Incident は変わらなかった severity>=ERRORだけでは、GKE 基盤の Pod(kube-system・gmp-system)のログが大半になった。 今回の環境では30分で約1,500件のうち、見たいアプリのログは3件だった- 今回の環境では、Terraform(v2 API)での Cloud Run の更新もデプロイのイベントとして表示された。 資料のクエリは v1 の
ReplaceServiceを挙げている - API で加えた変更もバージョン履歴に載る。 Terraform で作って1回更新したダッシュボードに、2件のリビジョンが並んだ
- gcloud の update には最新の etag が要る。 無ければ
INVALID_ARGUMENT、古ければABORTED - Terraform では
xPos/yPos/ しきい値のvalueに 0 を書くと、毎回差分が出た。 API が補うtargetAxisは差分にならなかった - Terraform で管理するダッシュボードはコンソールで編集しない。 触るならコピーを使う
参考資料 #
- Cloud Monitoring dashboards overview
- View Google Cloud dashboards
- Create and manage custom dashboards
- Create and manage dashboards by API
- Add temporary filters to a custom dashboard
- Create and manage variables and pinned filters
- Install dashboard templates
- Display logs on a dashboard
- Show events on dashboards
- Event types
- Share dashboards
- Access control with IAM
- Metric model
- REST Resource: projects.dashboards
- About GKE logs
- google_monitoring_dashboard
- GoogleCloudPlatform/monitoring-dashboard-samples
- Cloud Monitoring メトリクスの解説編
- Cloud Logging のクエリ集