Grafonnet + TerraformでGrafanaのダッシュボードを作ってみる
はじめに
こんにちは、kazz です。
この記事は Grafana Advent Calendar 2025 の 21 日目の記事です。
Grafanaのダッシュボードや権限(組織・フォルダなど)をTerraformで管理していると、ダッシュボードが扱いづらいと感じることがあります。
Terraformでダッシュボードを管理する場合、基本的にはエクスポートしたJSONをそのまま config_json に渡す形になります。
ただ、ダッシュボードのJSONは大きくなりがちで、差分も追いづらく、同じようなダッシュボードをテンプレート的に使いまわしたい時などに少し不便です。
そこでこの記事では、Grafonnetを使ってダッシュボードの定義をコードとして書く方法を試していきます。
Grafonnet / Jsonnet とは
Grafonnetは、JsonnetでGrafanaのダッシュボードを記述しやすくするためのライブラリです。
Jsonnetは、JSONをベースにしつつ、関数や変数、条件分岐などを扱える言語です。プログラムのように記述できるJSONといったイメージです。
Grafonnetについては、Grafana OSSのOpenAPIから生成されていること、旧grafonnet-libの後継であること、現時点ではexperimentalであることが説明されています。利用する場合はこれらの点を理解した上で検討するとよいでしょう。
Grafonnetでダッシュボードを書いてみる
ここでは、まずJsonnetでダッシュボードを定義し、Terraformが扱えるJSONを生成するところまでやってみます。
準備
Jsonnetの実行環境と、依存関係を扱うjsonnet-bundlerを用意して、プロジェクトを初期化します。
brew install go-jsonnet jsonnet-bundler
mkdir my-dashboards && cd my-dashboards
jb init
jb install github.com/grafana/grafonnet/gen/grafonnet-latest@main
ダッシュボード定義
例として、複数のパネルを配置したダッシュボードを作ります。JSONを直接編集する代わりに、共通部分を関数にしたり、同じ設定を使い回せるのがポイントです。
今回は、Random Walk を使った適当なパネルを並べたダッシュボードを作ります。
local g = import 'vendor/github.com/grafana/grafonnet/gen/grafonnet-v11.4.0/main.libsonnet';
local ts = g.panel.timeSeries;
local bar = g.panel.barChart;
local stat = g.panel.stat;
local timeline = g.panel.stateTimeline;
local grafanaQuery(refId) = {
datasource: { type: 'datasource', uid: 'grafana' },
queryType: 'randomWalk',
refId: refId,
};
local thresholds = [
{ color: 'green', value: null },
{ color: 'red', value: 80 },
];
local legend = { displayMode: 'list', placement: 'bottom', showLegend: true };
local tooltip = { mode: 'single', sort: 'none' };
local grid(panel, x, y, w, h) = panel + ts.panelOptions.withGridPos(x=x, y=y, w=w, h=h);
local panels = {
timeSeries:
ts.new('Sample Panel 1')
+ ts.standardOptions.color.withMode('palette-classic')
+ ts.standardOptions.thresholds.withSteps(thresholds)
+ ts.options.withLegend(legend)
+ ts.options.withTooltip(tooltip)
+ ts.queryOptions.withDatasource('datasource', 'grafana')
+ ts.queryOptions.withTargets([grafanaQuery('A')]),
barChart:
bar.new('Sample Panel 2')
+ bar.standardOptions.color.withMode('palette-classic')
+ bar.standardOptions.thresholds.withSteps(thresholds)
+ bar.fieldConfig.defaults.custom.withFillOpacity(80)
+ bar.options.withLegend(legend)
+ bar.options.withTooltip(tooltip)
+ bar.queryOptions.withDatasource('datasource', 'grafana')
+ bar.queryOptions.withTargets([grafanaQuery('A')]),
stat:
stat.new('Sample Panel 3')
+ stat.standardOptions.color.withMode('thresholds')
+ stat.standardOptions.thresholds.withSteps(thresholds)
+ stat.options.withGraphMode('area')
+ stat.options.withReduceOptions({ calcs: ['lastNotNull'], values: false })
+ stat.queryOptions.withDatasource('datasource', 'grafana')
+ stat.queryOptions.withTargets([grafanaQuery('A')]),
stateTimeline:
timeline.new('Sample Panel 4')
+ timeline.standardOptions.color.withMode('continuous-GrYlRd')
+ timeline.standardOptions.thresholds.withSteps(thresholds)
+ timeline.fieldConfig.defaults.custom.withFillOpacity(70)
+ timeline.options.withMergeValues(true)
+ timeline.options.withRowHeight(0.9)
+ timeline.options.withLegend(legend)
+ timeline.options.withTooltip(tooltip)
+ timeline.queryOptions.withDatasource('datasource', 'grafana')
+ timeline.queryOptions.withTargets([grafanaQuery('A')]),
};
g.dashboard.new('Sample Dashboard')
+ g.dashboard.withUid('sample')
+ g.dashboard.withEditable(true)
+ g.dashboard.graphTooltip.withSharedCrosshair()
+ g.dashboard.time.withFrom('now-6h')
+ g.dashboard.time.withTo('now')
+ g.dashboard.withTimezone('browser')
+ g.dashboard.withPanels([
grid(panels.timeSeries, 0, 0, 12, 11),
grid(panels.barChart, 12, 0, 12, 11),
grid(panels.stat, 0, 11, 12, 10),
grid(panels.stateTimeline, 12, 11, 12, 10),
])
JSON生成
Terraformに渡すためのJSONを生成します。
jsonnet -J vendor sample.jsonnet > dashboard.json
実際に上記のJsonnetファイルから生成したJSONファイルは以下です。70行ほどのコードから、250行のJSONファイルが生成されました。
生成されたJSON
{
"editable": true,
"graphTooltip": 1,
"panels": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
}
},
"gridPos": {
"h": 11,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "v11.4.0",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Sample Panel 1",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
}
},
"gridPos": {
"h": 11,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "v11.4.0",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Sample Panel 2",
"type": "barchart"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
}
},
"gridPos": {
"h": 10,
"w": 12,
"x": 0,
"y": 11
},
"id": 3,
"options": {
"graphMode": "area",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"values": false
}
},
"pluginVersion": "v11.4.0",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Sample Panel 3",
"type": "stat"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"fillOpacity": 70
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
}
},
"gridPos": {
"h": 10,
"w": 12,
"x": 12,
"y": 11
},
"id": 4,
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"mergeValues": true,
"rowHeight": 0.90000000000000002,
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "v11.4.0",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Sample Panel 4",
"type": "state-timeline"
}
],
"schemaVersion": 39,
"time": {
"from": "now-6h",
"to": "now"
},
"timezone": "browser",
"title": "Sample Dashboard",
"uid": "sample"
}
生成されたJSONは、通常のダッシュボードJSONと同じように扱えます。UIからのインポートにも使えますし、TerraformやGit Syncの入力としても利用できます。
実際にGrafanaでインポートしたところ、以下のようなダッシュボードが出来上がりました。

Terraformと組み合わせたCI/CD
今回は、GrafonnetとTerraformを組み合わせたダッシュボードの管理をやってみます。
Terraformの適用前にGrafonnetをJSONに変換する工程をCI/CDに組み込みます。ここではGitHub Actionsを例にします。
Terraform
まずはTerraformで組織などのリソースを定義し、ダッシュボードはJSONを読み込んで渡します。
resource "grafana_organization" "main" {
name = "Sample Organization"
admin_user = "admin"
create_users = false
}
resource "grafana_dashboard" "main" {
depends_on = [grafana_organization.main]
org_id = grafana_organization.main.id
config_json = templatefile(
"./dashboards/sample.json",{}
)
}
GitHub Actions
CI/CDの中で、Terraformのplan/applyの前に、Grafonnet からJSONを生成する処理を挟みます。
jobs:
deploy-grafana:
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v3
- name: Setup Jsonnet
run: |
go install github.com/google/go-jsonnet/cmd/jsonnet@latest
go install github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb@latest
- name: Build Dashboards
run: |
jb install
mkdir -p generated
jsonnet -J vendor main.jsonnet > dashboards/sample.json
- name: Terraform Init & Plan
run: |
terraform init
terraform plan
# 省略のためApplyも一つのWorkflow にまとめています
- name: Terraform Init & Apply
run: |
terraform init
terraform apply -auto-approve
これで、ダッシュボードのJSONはあくまで中間の成果物として扱えます。
やってみた感想
Grafonnetを使ってみた感想として、現状の自分としてはメリットよりも新たにJsonnetをキャッチアップするコストの方が大きいなと思いました。UIで編集してエクスポートしたほうが、色々捗りそうです。
一方で、慣れたら楽そう、レビューしやすそう、UIからの変更ができない場合は活躍しそう、といったメリットはあるなと感じました。
既存ダッシュボードの変換も、AIを併用すると現実的な手間で進められそうなので、うまく使えばObservability as Codeを実践できると思います。
Discussion