Grafana provisioningをClaude Codeで守る — ダッシュボードJSONをhookで検査
GrafanaのダッシュボードをJSONごとGit管理するとき、uid・id・version・datasource参照の崩れをClaude CodeのPostToolUse hookで検査し、UI保存との二重管理を避ける手順です。
Grafanaのダッシュボードは、provisioningを使うとJSONファイルをGitで管理し、起動時にGrafanaへ読み込ませられます。ただしJSONは数千行になりやすく、Claude Codeに編集させるとuidの重複、idの混入、存在しないデータソースへの参照といった崩れが差分に紛れ込みます。
ここでは、provisioningの設定をallowUiUpdates: falseで固めたうえで、ClaudeがJSONを書き換えるたびにPostToolUse hookで構造を検査する構成を作ります。
provisioningで何がGitの管理下に入るか
provisioningは、provisioning/dashboardsディレクトリに置いたYAMLのproviders設定から、ローカルファイルシステム上のダッシュボードを読み込む仕組みです。Grafanaは起動時に、設定されたpathにあるダッシュボードを作成または更新します。
最小の設定は次の形です。
apiVersion: 1
providers:
- name: dashboards
type: file
disableDeletion: true
updateIntervalSeconds: 30
allowUiUpdates: false
options:
path: /etc/dashboards
foldersFromFilesStructure: truefoldersFromFilesStructure: trueを使うと、ディレクトリ構成がそのままGrafanaのフォルダになります。たとえばserver/とapplication/を作れば、同名のフォルダがUIに現れます。この機能を使うときはfolderとfolderUidを未設定にする必要があります。フォルダの階層は4段までです。
ダッシュボードのJSONは、dashboardキーの下に本体を置き、folderUidやoverwriteを並べる形で書けます。Kubernetes形式(kind: Dashboard)でも書け、ダッシュボードv2や動的ダッシュボードをprovisioningするときはこの形式が必須です。この記事のスクリプトは前者の形式を前提にします。
UI保存とファイルの二重管理が壊れる場所
Gitで管理するなら、UIで直した内容がファイルへ戻る経路を決めておく必要があります。Grafanaには、provisioningされたダッシュボードをUIから直接ファイルへ書き戻す機能がありません。ここを曖昧にすると、次のような食い違いが起きます。
allowUiUpdatesの設定で変わること
false(Gitを正にする)
保存しようとすると「Cannot save provisioned dashboard」のダイアログが出て、UIからは保存できません。変更はJSONをエクスポートしてリポジトリへ入れる流れに限られます。
true(UI保存を許す)
UIで保存した内容はGrafanaのデータベースに入ります。後からファイル側を更新すると、データベース側が常にファイルの内容で上書きされます。
trueのままにすると、UIで保存した変更は次回のファイル更新で消えます。しかもversionはこの判断に使われません。ファイルのversionがデータベースより小さくても、Grafanaはファイルの内容で上書きします。したがってJSONのversionは、Gitで管理する側にとっては差分のノイズにしかなりません。
もう一つ注意点があります。UIで保存済みのダッシュボードからprovisioningのソースを消すと、disableDeletionをtrueにしていない限り、データベース上のダッシュボードも削除されます。上の設定例でdisableDeletion: trueにしているのはそのためです。
検査するのは4点に絞る
Claudeが編集したJSONで壊れやすい箇所は、Grafanaの挙動から決まります。検査項目は次の4つに絞ります。
JSONで見張る箇所
uid の欠落と重複
uidを持つJSONは、そのuidのダッシュボードを更新対象にします。同じuidを別ファイルに使うと挙動が不安定になるため、空・重複を止めます。同じフォルダ内でtitleを使い回すのも公式が避けるよう求めているので、フォルダ単位で見直すときの目安にしてください。id の混入
エクスポート時にGrafanaが
idを取り除くのは、provisioningで使い回すためです。数値のidが残っていたら、UIからコピーした痕跡として警告します。version の差分ノイズ
ファイルの
versionは上書き判定に使われません。0以外の値が入っていれば、不要な差分として指摘します。datasource 参照
パネルが参照するデータソースを、provisioningしたdatasourceの
uidと突き合わせます。名前の文字列で参照した古い書き方も拾います。
データソース側にもuidを固定する欄があります。datasourceのprovisioning YAMLでuid: my_unique_uidのように決めておけば、ダッシュボードのJSONから同じ値で参照できます。この対応が崩れると、パネルが空になります。
PostToolUse hookで検査スクリプトを走らせる
Claude Codeのhookは、ツールの実行前後にシェルコマンドなどを走らせる仕組みです。ファイルを書き換えた直後に検査したいのでPostToolUseを使い、EditとWriteにマッチさせます。PostToolUseはツールが成功した後に動くため、ブロックはできません。ただし終了コード2で終えると、標準エラー出力がClaudeに渡ります。これで「直して」と指示した形になります。
スクリプトを書く
.claude/hooks/check-grafana-dashboard.shとして保存します。引数にファイルを渡せば単体でも動き、引数がなければhookの標準入力からtool_input.file_pathを読みます。
#!/bin/bash
# 引数があればそのファイル、なければ hook の標準入力から取る
f="${1:-$(jq -r '.tool_input.file_path // empty')}"
root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel)}"
[ -f "$f" ] || exit 0
case "$f" in /*) ;; *) f="$PWD/$f" ;; esac # 相対パスで渡されても、find の結果と比べられるよう先に絶対パスにそろえる
case "$f" in */grafana/dashboards/*.json) ;; *) exit 0 ;; esac
errs=()
jq empty "$f" 2>/dev/null || { echo "JSONとして読めません: $f" >&2; exit 2; }
body='(.dashboard // .)'
uid=$(jq -r "$body | .uid // empty" "$f")
[ -z "$uid" ] && errs+=("uid が空です。固定の文字列を入れてください")
# 別ファイルとの uid 重複
while IFS= read -r g; do
[ "$g" != "$f" ] || continue
other=$(jq -r "$body | .uid // empty" "$g" 2>/dev/null)
[ -n "$uid" ] && [ "$uid" = "$other" ] && errs+=("uid '$uid' が $g と重複しています")
done < <(find "$root/grafana/dashboards" -name '*.json')
# id が数値で残っていないか
jq -e "$body | (.id // null) != null" "$f" >/dev/null &&
errs+=("id が残っています。provisioning 用のファイルでは null か省略にしてください")
# version は Grafana が無視するので 0 以外は差分のノイズ
jq -e "$body | (.version // 0) != 0" "$f" >/dev/null &&
errs+=("version が 0 ではありません。ファイル側の version は上書き判定に使われません")
# datasource 参照を provisioning 済みの uid と突き合わせる
known=$(grep -rhE '^[[:space:]]*-?[[:space:]]*uid:' "$root"/grafana/provisioning/datasources 2>/dev/null \
| sed -E 's/^[[:space:]]*-?[[:space:]]*uid:[[:space:]]*//; s/["'\'']//g')
while IFS= read -r ref; do
[ -z "$ref" ] && continue
case "$ref" in \$*) continue ;; esac # ${DS_X} などの変数は許可
case "$ref" in "-- Grafana --"|"-- Mixed --"|"-- Dashboard --"|grafana) continue ;; esac # 組み込みの参照
grep -qxF "$ref" <<<"$known" || errs+=("datasource '$ref' が provisioning 済みの uid にありません")
done < <(jq -r "$body | [.. | objects | select(has(\"datasource\")) | .datasource
| if type==\"object\" then .uid else . end] | unique | .[] | select(. != null)" "$f")
if [ ${#errs[@]} -gt 0 ]; then
printf '%s\n' "${errs[@]}" >&2
exit 2
fichmod +xを忘れずに実行します。相対パスで呼ばれても自分自身を重複と誤判定しないよう、対象を絞る前に絶対パスにそろえています。findと[[:space:]]を使っているのは、macOS標準のbash 3.2とBSD sedでも動かすためです。組み込みのデータソース参照(-- Grafana --など)は、provisioningしたuidに載らないので許可リストで除いています。パスが/grafana/dashboards/を含むJSONだけを対象にし、それ以外のファイルは何もせず終えます。
settings.jsonに登録する
プロジェクトの.claude/settings.jsonに書けば、リポジトリへ入れてチーム全員に共有できます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-grafana-dashboard.sh"
}
]
}
]
}
}hookにはパス条件を絞るif欄もあり、Edit(**/grafana/dashboards/**)のような権限ルール形式で書けます。ただしifに書けるルールは1つだけで、Writeにも同じ条件を付けるには別のhookを並べる必要があります。スクリプト側でパスを判定する方が、ハンドラが増えずに済みます。
動作を確かめる
hookに登録する前に、単体で呼んで終了コードを見ます。
.claude/hooks/check-grafana-dashboard.sh \
grafana/dashboards/server/common_dashboard.json
echo "exit=$?"違反があれば標準エラー出力に項目が並び、exit=2になります。問題がなければ何も出ずにexit=0です。意図的にuidを消したコピーで試すと、検査が効くことを確認できます。
ClaudeにJSONを直させるときの運用
hookが違反を返すと、Claudeはその標準エラー出力を読んで同じファイルを直します。この反復を助けるために、CLAUDE.mdに最低限のルールを置いておきます。
## Grafana ダッシュボード
- grafana/dashboards 配下の JSON が唯一の正。Grafana の UI で保存しない
- uid は既存の値を変えない。新規ファイルは英小文字とハイフンで固定する
- id と version は触らない。UI からコピーした JSON に含まれていたら取り除く
- datasource は provisioning/datasources の uid で参照するUIで試作したパネルを取り込むときの手順は次のとおりです。
UIで作ったパネルをリポジトリへ取り込む
- 1
UIでJSONを書き出す
保存の際にAdvanced optionsを開き、「Classic」モデルを選んで「Save JSON to file」を使います。V2 Resource形式はKubernetes形式で扱う別の流れです。
- 2
Claudeに差分を渡す
書き出したJSONを既存ファイルの隣に置き、既存の
uidを保ったまま必要なパネルだけ取り込むよう頼みます。 - 3
hookの結果を読ませる
uid・id・datasource参照の違反が出たら、そのまま直させます。差分はgit diffで確認します。 - 4
Grafanaで確認する
updateIntervalSecondsの間隔のあと、変更がGrafanaに反映されます。10秒以下にすると監視方式になり、Dockerのbind mountなどではファイルの変更イベントがGrafanaに届かないことがあります。反映されないときは、10秒を超える値にして定期確認に切り替えます。
落とし穴
Bashで書き換えるとhookが動かない
PostToolUseのEdit|Writeは、sedなどBashコマンドや外部のプロセスが同じファイルを書き換えたときには動きません。Claudeがjqで一括置換する場面は、検査を素通りします。同じスクリプトをCIやpre-commitからも呼び、git diff --name-onlyで挙がったJSONに引数で渡しておくと穴が閉じます。
差分の意味を読ませるレビューの頼み方
構造の検査は決まった規則で通せますが、パネルのクエリが正しいかは機械では判断できません。そこはClaudeに差分を読ませます。たとえば次のように頼みます。
git diff main -- grafana/dashboards の差分をレビューして。
次の観点だけ見て、問題がなければ「なし」と答えて。
- panels の追加・削除と、gridPos の重なり
- targets の expr が変わったパネルの一覧
- templating の変数が、使われなくなっていないか観点を3つほどに絞るのは、全JSONを読ませるよりも、差分の意味に集中させられるからです。大きなダッシュボードはJSON全体を読むだけでコンテキストを大きく消費します。実際のGrafanaに接続して読む方法は、mcp-grafanaでダッシュボードを読む記事が扱っています。ファイルをGitで管理する構成なら、まずはgit diffで必要な部分だけを見せる方が軽く済みます。
hookの設計を広げるとき
hookそのものの仕組みはClaude Codeのhooks入門に整理されています。保存後に自動でlintをかける形はlint自動修正のhookと同じ発想です。ダッシュボードの検査は、lintをJSONに置き換えたものと考えられます。
hookが失敗したとき動作を止めたい場合は、onFailureの設定が使えます。その扱いはonFailureでhookの失敗を止める記事にあります。今回のスクリプトのように検査結果をClaudeに伝えるだけなら、終了コード2で十分です。
まとめ
GrafanaのダッシュボードをGitで管理する要は、allowUiUpdates: falseでUIからの保存を止め、JSONを唯一の正にすることです。そのうえでClaudeが編集するたびにuid・id・version・datasource参照を検査すれば、レビューで見る差分から構造の崩れが消え、クエリやパネルの中身に集中できます。
Bashによる書き換えはhookに掛からないので、同じスクリプトをCIにも置いておくと確実です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。