GrizzlyとGrafonnetで始めるGrafana Dashboards as Code

先日、というかもう3ヶ月も経ってしまいましたが Grafana Meetup Japan #1 - connpass にてLTをさせて頂きました。

趣味のダッシュボード開発というテーマで、あわせてGrafanaのダッシュボードをコードで管理するソリューションの1つとして Grizzly GitHub - grafana/grizzly: A utility for managing Jsonnet dashboards against the Grafana API を紹介するお話をさせて頂いたのですが、結構な人数が参加されていたものの知ってる人が一人もいないぞ・・という結果になり、それもなんだかもったいない話だと思いました。

ということで今回はとりあえずGrizzly、Grafonnetがどういったもので、どうGetStartedしていくとよいか、といった話を改めてブログで書いていきます。

なお本記事で利用するツールのバージョンはこんな感じです、jsonnet-bundlerはdevってなんやねんという感じですが、Homebrewで入る最新のものを使ってます(※0.5.1でした)。

$grr --version
grr version v0.4.3

$jb --version
dev

$jsonnet --version
Jsonnet commandline interpreter (Go implementation) v0.20.0

リモートのGrafanaバージョン
Grafana v10.4.2 (22809dea50)

いちおうサンプルコード一式はここにおいてます

GitHub - egmc/egmc-dashboard-blog: sample code for my blog post

Grafanaのダッシュボード表現とGrafonnet

GrafanaのダッシュボードはJSONで構造化されて表されます。 JSONなので、このままでもバージョン管理できるのですが、これをエディタで編集して変更管理する、というのはなかなかに大変です。 結局ダッシュボードの変更は画面上から行い、結果としてexporterされたJSONをバージョン管理するというのは変更とexport、コミットを繰り返すことになるので作業としてもやや冗長になります。

最終的にJSON表現となればよいのでJSONを生成するテンプレート言語を使ってより簡潔な記述を可能とするのがJsonnet(JSONテンプレート言語)とGrafonnet(JsonnetでGrafanaのダッシュボード表現を扱うためのライブラリ)です。

Jsonnet、Grafonnetのセットアップ

Jsonnet、Grafonnetのセットアップについては以下を参照

起点として以下のページからたどれますが

Home - Grafonnet

Jsonnetはc++とgoの実装がありますが公式の推奨どおりgo-jsonnetを使いましょう

github.com

Grafonnetを使うためにJsonnetバンドラー(Jsonnetのパッケージマネージャ)を入れます

github.com

最後に実際にダッシュボード管理を行うプロジェクトレポジトリでgrafonnetをインストールします

grafana.github.io

参考までにmacでHomebrewを使う場合は以下のような流れになります(細かい部分はそれぞれのドキュメント参照)

$ brew install go-jsonnet
$ brew install jsonnet-bundler

#以下プロジェクトフォルダにて
$ jb init
$ jb install github.com/grafana/grafonnet/gen/grafonnet-latest@main

Grizzlyを使う

GrizzlyはGrafanaダッシュボードなどを管理するためのcliツールです。

野良ではなく、GrafanaのOrg配下で開発されています・・が情報は正直少ないです。

ダッシュボード管理機能からスタートしたものの、現在はGrafanaの周辺ツール(Synthetic MonitoringやPrometheusのアラートルールなど)にも対応を広げているため、observability resourcesをコードで管理するためのcliツールという位置づけになっています。

Grafana Grizzly is a command line tool that allows you to manage your observability resources with code.

本記事ではGrafanaダッシュボードのみを取り扱います。

Grizzlyのセットアップ

プラットフォームごとのバイナリリリースがありますのでこれをそのまま落としてきて使います

Releases · grafana/grizzly · GitHub

grrコマンドが実行可能になったら、まずはリモートのGrafanaを扱うためのクレデンシャルをセットします(環境変数でも設定できますが、本記事では最初から利便性のためにconfig setを利用します)

Setup and Configuration | Grafana Grizzly Docs

を参考にまずはリモート環境に合わせたコンテキスト(環境名)を設定します

grr config create-context xxx

そしてクレデンシャル(GrafanaのURLとトークン)をセットします

grr config set grafana.url {リモートのGrafana URL}
grr config set grafana.token {Grafanaのサービスアカウントに紐づいたトークン}

以降特に指定せずに現在のコンテキストに紐づいた設定が利用されます。

複数のGrafana環境を切り替える際は別名でコンテキストを作成して切り替えると良いでしょう。

Grizzlyを使ってJSON/形式でダッシュボードを作成する

サンプルダッシュボードを作っていきます。

本記事ではお題として拙作の GitHub - egmc/systemd_resolved_exporter を使ってsystemd_resolvedのキャッシュヒット率を出すグラフを作ってみます。

PromQL的にはこんな感じです

rate(systemd_resolved_cache_hits_total[$__rate_interval]) / (rate(systemd_resolved_cache_hits_total[$__rate_interval]) + rate(systemd_resolved_cache_misses_total[$__rate_interval]))

(単にこちらはサンプルなので、実際に動かす場合はすでに取得しているexporterの適当なメトリクスで同様のステップで試してみると良いでしょう)

単にGrafanaのUIを使ってダッシュボードを作成し、JSON表現(Grizzlyのデフォルトはyamlですが)を管理するだけであれば grr serve が簡単です。

この機能は比較的最近追加されたものですが、なかなか動作的におもしろい機能になっています。

例としてこんなyamlファイルを用意し(metadataのnameとspecのuidは合わせる必要があります)

apiVersion: grizzly.grafana.com/v1alpha1
kind: Dashboard
metadata:
    folder: general
    name: systemd_resolved_sample
spec:
    title: systemd_resolved_sample
    uid: systemd_resolved_sample

systemd_resolved_sample.yamlとしてプロジェクト直下に保存します。

この時点では単にタイトルとuidだけが指定されているだけの空のダッシュボードです。

$ grr serve ./*.yaml -t dashboard

するとlocalhost:8080でおもむろにサーバが立ち上がり、アクセスするとこんな表示になり

systemd_resolved_sampleにアクセスすると空のダッシュボードが立ち上がっているので、Add visualizationで先ほどのクエリをもとにUI上で作業してグラフを足していきます

単にクエリを追加しただけだと0.xxといった値が折れ線グラフで表示されるだけなので、いくつかグラフをいじって見た目を整えます

  • unitをpercent(0.0-1.0)に
  • maxを1に(常に100%まで表示されるように)
  • グラフタイトルを設定してLegendをsampleに
  • 諸事情によりこのサンプルは60s間隔でscrapeしてるのでinteval=60に設定してます

グラフが出来上がったらそのままSave dashboardから保存します。 するとおもむろに手元のyamlが書き換わり、以下のようなものになります。

(データソースのIDなどは環境固有なので、各々環境で適宜読み替えてください)

apiVersion: grizzly.grafana.com/v1alpha1
kind: Dashboard
metadata:
    folder: general
    name: systemd_resolved_sample
spec:
    annotations:
        list:
            - builtIn: 1
              datasource:
                type: grafana
                uid: -- Grafana --
              enable: true
              hide: true
              iconColor: rgba(0, 211, 255, 1)
              name: Annotations & Alerts
              type: dashboard
    editable: true
    fiscalYearStartMonth: 0
    graphTooltip: 0
    links: []
    panels:
        - datasource:
            type: prometheus
            uid: adk2jn9tqqiv4e
          fieldConfig:
            defaults:
                color:
                    mode: palette-classic
                custom:
                    axisBorderShow: false
                    axisCenteredZero: false
                    axisColorMode: text
                    axisLabel: ""
                    axisPlacement: auto
                    barAlignment: 0
                    drawStyle: line
                    fillOpacity: 0
                    gradientMode: none
                    hideFrom:
                        legend: false
                        tooltip: false
                        viz: false
                    insertNulls: false
                    lineInterpolation: linear
                    lineWidth: 1
                    pointSize: 5
                    scaleDistribution:
                        type: linear
                    showPoints: auto
                    spanNulls: false
                    stacking:
                        group: A
                        mode: none
                    thresholdsStyle:
                        mode: "off"
                mappings: []
                max: 1
                thresholds:
                    mode: absolute
                    steps:
                        - color: green
                          value: null
                        - color: red
                          value: 80
                unit: percentunit
            overrides: []
          gridPos:
            h: 17
            w: 24
            x: 0
            "y": 0
          id: 1
          options:
            legend:
                calcs: []
                displayMode: list
                placement: bottom
                showLegend: true
            tooltip:
                mode: single
                sort: none
          targets:
            - datasource:
                type: prometheus
                uid: adk2jn9tqqiv4e
              editorMode: code
              expr: rate(systemd_resolved_cache_hits_total[$__rate_interval]) / (rate(systemd_resolved_cache_hits_total[$__rate_interval]) + rate(systemd_resolved_cache_misses_total[$__rate_interval]))
              instant: false
              interval: "60"
              legendFormat: sample
              range: true
              refId: A
          title: systemd resolved chache hit rate
          type: timeseries
    schemaVersion: 39
    tags: []
    templating:
        list: []
    time:
        from: now-6h
        to: now
    timepicker: {}
    timezone: ""
    title: systemd_resolved_sample
    uid: systemd_resolved_sample
    version: 1
    weekStart: ""

これでJSON Model(を内包したYAMLファイル)が残りましたので、このままバージョン管理を行うことができますし、再び grr serve で編集を再開することができます。

grr serve の動作としては、ローカルでGrafanaを立ち上げつつ、データソースとしては指定したクレデンシャルを利用してリモートのGrafanaのデータソースへリクエストをプロキシして利用するような形になっています。

リモートのデータソースを利用しつつ手元でトライアンドエラーを行ってダッシュボードをファイルで管理できる、といったあたりが利点になるかと思います。

一方、GrafanaのJSON Modelを生で扱うため抽象度は低く、明示的に指定したいプロパティ以外もすべて特定のスキーマバージョンに基づいて保存されてしまうため、記述内容そのものは冗長になりますし、バージョンをまたいだ環境間の行き来などは難しそうです(実際Grizzlyのpull/pushでGrafanaバージョンまたいでダッシュボード定義を移動しましたが割と動かないものがあったりしました)。

Grizzlyを使ってGrafonnetでダッシュボード管理を作成する

同様のダッシュボードを今度はGrafonnetで作成してみます。

今度は最初からコード管理を行いますので、新規に systemd_resolved_sample.jsonnet を作成します。

local g = import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet';
 
# dashboardを生成してpanelを追加する、必要な属性はGrafonnetが提供する関数を利用して生成し+結合で追加していく
local dashboard = g.dashboard.new("systemd_resolved_sample_jsonnet") + g.dashboard.withUid('systemd_resolved_sample_jsonnet')
  + g.dashboard.withPanels([
    g.panel.timeSeries.new('systemd-resolved cache hit rate')
    + g.panel.timeSeries.queryOptions.withTargets([
        g.query.prometheus.new(
            'prometheus',
            'rate(systemd_resolved_cache_hits_total[$__rate_interval]) / (rate(systemd_resolved_cache_hits_total[$__rate_interval]) + rate(systemd_resolved_cache_misses_total[$__rate_interval]))',
        ) + g.query.prometheus.withInterval("60")
          + g.query.prometheus.withDatasource('prometheus')
          + g.query.prometheus.withLegendFormat("sample")
    ]) + g.panel.timeSeries.queryOptions.withDatasource('prometheus', 'adk2jn9tqqiv4e')
    + g.panel.timeSeries.standardOptions.withUnit('percentunit')
    + g.panel.timeSeries.standardOptions.withMax("1")
    + g.panel.timeSeries.panelOptions.withGridPos(17,
     24, 0, 0)
]);
# Grizzlyの作法に合わせてmetadataを付与し、specにdashboardのJSONを入れ込む
{ 
  apiVersion: 'grizzly.grafana.com/v1alpha1',
  kind: 'Dashboard',
  metadata: {
    name: "systemd_resolved_sample_jsonnet",
    folder: "general" 
  },
  spec: dashboard
} 
$ grr apply ./systemd_resolved_sample.jsonnet -t  dashboard

で反映します。

また、 grr watch を使うことでファイルの変更を検知して(エラーがない限り)リモートのGrafanaへ即座に反映することもできます。 リモートに対してトライアンドエラーができる場合はこのやり方がおすすめです。

$ grr watch . dashboard -t  dashboard

grr serve ポチポチつくたものと同様のダッシュボードができました。

Jsonnetは作法を抑える必要はありますが、記述そのものは生のJSON Model表現より「実際の関心事」フォーカスにした内容になっています。

(本記事では簡単のためにダッシュボードのuidを環境に合わせて指定していますが、このあたりは実際は動的に設定することで環境固有の情報をなくせるでしょう)

スキーマバージョンについてはGrafonnetライブラリに依存しますが、コードそのものにそういった情報は含まれないため、ライブラリが追随してくれる限り将来のバージョンアップも比較的容易になるはずです。

まとめ

  • Grizzlyを使うことでお手軽にGrafanaのDashboards as Codeをはじめることができます
  • grr serve は手元でGrafanaそのものを使ってDashboardを作成するのに便利です
  • Grafonnetは作法を覚える必要はありますが、本格的にダッシュボードのコード管理を長期的に行う場合は有用な選択肢になります

・・・と一旦まとめた後の雑多な補足

  • Grizzlyは実際便利だと思うんですが、まだまだバギーでコマンド体系は統一されておらず、ツールとしてははまりどころも多いです
    • エラーも不親切でまだまだ発展途上な感は否めません
    • ということで本記事の一つの目的としてはユーザー増やしてツールの環境がもっとよくなるいいなあ、などという意図もあったりします、バグレポでもPRでも
  • Jsonnetでの記述でDashboardをspecに入れてGrizzlyのapiVersion、kind、metadataをいれるやり方ですが実際これ公式自体は公式ドキュメントにないので、これが意図したやり方なのか実は確認してません、しかしかつてのHidden Elementを利用した記述方法ももはやサポートされてなさそうなので、こうするしかないんじゃないかな・・と思ってます
  • Grafonnetライブラリはかつて https://github.com/grafana/grafonnet-lib が使われていたのですがGrafana本体のアップデート、API変更に追随が難しくなり自動生成するアプローチになったのが現在の https://github.com/grafana/grafonnet レポジトリです。最新のAPIに追随してくれるのは良いのですが、特定のGrafanaバージョン、スキーマバージョンの指定みたいなのはできなさそうなので古いGrafanaバージョンを使っている場合はやや注意がいりそうです。タグで分けてくれたらいいんじゃないかと思うんですが・・。
  • クレデンシャルを含むconfigのパスは grr config path で取得できます。手元のMacだと /Users/{ユーザー}/Library/Application Support/grizzly/settings.yaml に設定されてました。

参考

公式系

その他

  • Jsonnetの薦め #JSON - Qiita 元記事の作成時期は結構古いですがJsonnetについての概要を日本語で掴むのにおすすめです