Grafana の Git Sync でダッシュボードを GitOps 管理する

Table of Contents

1. はじめに

みなさん、Grafana のダッシュボード管理どうしてますか?

私は自宅の Kubernetes クラスターで Grafana を動かしており、ダッシュボードを Web UI でポチポチ作ったり、mcp-grafana を使って Claude Code や kagent から作ったりしていました。

しかし、それではダッシュボードのデータが Grafana 内部のデータベースにしか残らず、PVC を使っていたとしても Kubernetes クラスター自体を作り直した際などには消えてしまいますし、バージョン管理もできないため履歴を追うことも難しいです。 実際に今まで何回も Kubernetes クラスター自体が壊れて作り直しており、その度に同じようなダッシュボードを都度都度作成していました、、、

そんな中、Grafana v12.4 から Git Sync という機能が使えるようになっていることを知りました。 ダッシュボード設定の JSON を GitHub リポジトリと双方向に同期してくれる機能で、まさに求めていたものです。

今回は、自宅サーバーの Grafana に Git Sync を導入して、ダッシュボードを GitOps で管理できるようにするまでの手順を紹介します。

2. Git Sync とは

Git Sync は、Grafana インスタンスと Git リポジトリの間でダッシュボードを双方向に同期する機能です。 Grafana Cloud だけでなく、OSS 版や Enterprise 版でも利用できます。

主な特徴はこんな感じです。

  • 双方向同期: Web UI での変更は Git にコミットされ、Git への変更は Grafana に反映される
    • PR ワークフロー: Web UI での変更をブランチにコミットして PR を作成できる(branch ワークフロー)
    • 直接コミット: Web UI での変更を直接メインブランチにコミットもできる(write ワークフロー)
  • 定期同期: 設定した間隔(例: 60秒)で自動的に同期される

従来のプロビジョニング(provisioning/dashboards/ にファイルを配置する方法)との大きな違いは、Git → Grafana の一方向ではなく、Grafana → Git の方向も同期される点です。 Web UI で気軽にダッシュボードを編集しつつ、その変更が自動的に Git に記録されるのはとても便利です。

2.1 アーキテクチャ

Git Sync は以下の要素で構成されています。

  • Connection: GitHub App 等の認証情報を管理するリソース
  • Repository: 同期先の Git リポジトリ・ブランチ・パスや同期間隔などを定義するリソース

Grafana は Repository リソースの設定に基づいてリポジトリをポーリングし、指定パス配下のダッシュボード JSON を同期します。 同期されたダッシュボードは Grafana のダッシュボード一覧にフォルダとして表示されます。

3. 前提条件

今回の環境はこんな感じです。

  • Kubernetes クラスター: 自宅サーバー上の kubeadm + OVN-Kubernetes 環境
  • Grafana: grafana/grafana:latest (v13 以降であれば provisioning feature toggle がデフォルト有効)
  • Argo CD: マニフェストの GitOps デプロイに利用

Git Sync を使うには、以下が必要です。

  1. Grafana v13 以降provisioning feature toggle がデフォルト有効。v12.4 でも利用可能だが feature toggle の手動有効化が必要)
  2. GitHub App の作成とインストール
  3. grafana.ini での repository_types の設定

4. 構築手順

4.1 GitHub App の作成

まず、Grafana がリポジトリにアクセスするための GitHub App を作成します。 以下の権限を付与してください。

Permission Access
Contents Read and write
Pull requests Read and write
Webhooks Read and write
Administration Read-only
Metadata Read-only

作成したら、対象のリポジトリにインストールして、App IDInstallation IDPrivate Key(.pem ファイル)を控えておきます。

4.2 Kubernetes Secret の作成

GitHub App の認証情報と Grafana の管理者パスワードを Secret として登録します。

kubectl -n grafana create secret generic grafana-git-sync-secret \
  --from-literal=admin-password=<GRAFANA_ADMIN_PASSWORD> \
  --from-literal=github-app-id=<APP_ID> \
  --from-literal=github-installation-id=<INSTALLATION_ID> \
  --from-file=github-private-key=<PEM_FILE_PATH>

4.3 grafana.ini の設定

Git Sync を有効にするには、grafana.ini に以下を追加します。

[provisioning]
repository_types = github

この設定は 環境変数でオーバーライド することもできます。 Grafana の環境変数は GF_<SECTION>_<KEY> の命名規則で、この場合は GF_PROVISIONING_REPOSITORY_TYPES=github になります。

今回は ConfigMap で grafana.ini を管理しているので、ファイルに直接記述しています。

# kustomization.yaml
configMapGenerator:
  - name: grafana-ini
    files:
      - grafana.ini
    options:
      disableNameSuffixHash: true

4.4 Deployment の設定

Grafana の Deployment では、Secret から管理者パスワードを取得し、grafana.ini を ConfigMap からマウントしています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
spec:
  strategy:
    type: Recreate
  template:
    spec:
      securityContext:
        fsGroup: 472
        supplementalGroups:
          - 0
      containers:
        - name: grafana
          image: grafana/grafana:latest
          env:
            - name: GF_SECURITY_ADMIN_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: grafana-git-sync-secret
                  key: admin-password
          ports:
            - containerPort: 3000
              name: http-grafana
          volumeMounts:
            - mountPath: /var/lib/grafana
              name: grafana-pv
            - mountPath: /etc/grafana/grafana.ini
              name: grafana-ini
              subPath: grafana.ini
              readOnly: true
      volumes:
        - name: grafana-pv
          persistentVolumeClaim:
            claimName: grafana-pvc
        - name: grafana-ini
          configMap:
            name: grafana-ini

特に特別なことはしていなくて、通常の Grafana デプロイに grafana.ini の ConfigMap マウントを追加しただけです。 Git Sync は Grafana 自体の機能なので、サイドカーコンテナなどは不要です。

4.5 Git Sync の設定リソース

Git Sync の設定は、Grafana の Provisioning API で Connection と Repository のリソースを登録して行います。 これらの設定は YAML ファイルとして定義し、grafanactl という CLI ツールで Grafana に投入します。

まず、GitHub App の接続情報を定義する Connection リソースです。

apiVersion: provisioning.grafana.app/v0alpha1
kind: Connection
metadata:
  name: github
  namespace: default
spec:
  title: GitHub
  type: github
  url: https://github.com
  github:
    appID: ""
    installationID: ""
secure:
  privateKey:
    create: ""

appIDinstallationIDprivateKey はこの時点では空にしておき、後述の Job で Secret から注入します。

次に、同期先のリポジトリを定義する Repository リソースです。

apiVersion: provisioning.grafana.app/v0alpha1
kind: Repository
metadata:
  name: kubernetes-bootstrap
spec:
  title: kubernetes-bootstrap
  type: github
  github:
    url: https://github.com/TOMOFUMI-KONDO/kubernetes-bootstrap.git
    branch: main
    path: grafana/dashboards/
  sync:
    enabled: true
    intervalSeconds: 60
    target: folder
  workflows:
    - write
    - branch
  connection:
    name: github

ポイントをいくつか説明します。

  • path: grafana/dashboards/: リポジトリ内のこのディレクトリ配下にあるダッシュボード JSON のみが同期対象になります
  • intervalSeconds: 60: 60秒間隔でポーリングして同期します
  • workflows: [write, branch]: Web UI からの変更を直接コミット(write)と PR 作成(branch)の両方で行えます
  • connection.name: github: 先ほど定義した Connection リソースを参照します

これらの YAML は ConfigMap にまとめて管理しています。

apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-git-sync
data:
  connection.yaml: |-
    # (上記の Connection YAML)
  repository.yaml: |-
    # (上記の Repository YAML)

4.6 セットアップ Job

Git Sync の設定を Grafana に投入するために、Argo CD の PostSync Hook として Job を作成しました。 この Job は Grafana がデプロイされた後に実行され、grafanactl を使って Connection と Repository リソースを登録します。

apiVersion: batch/v1
kind: Job
metadata:
  name: grafana-git-sync-setup
  annotations:
    argocd.argoproj.io/hook: PostSync
spec:
  backoffLimit: 5
  ttlSecondsAfterFinished: 300
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: setup
          image: alpine:3.21
          env:
            - name: GRAFANA_SERVER
              value: http://grafana.grafana.svc:3000
            - name: GRAFANA_ORG_ID
              value: "1"
            - name: GRAFANA_USER
              value: admin
            - name: GRAFANA_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: grafana-git-sync-secret
                  key: admin-password
            - name: GITHUB_APP_ID
              valueFrom:
                secretKeyRef:
                  name: grafana-git-sync-secret
                  key: github-app-id
            - name: GITHUB_INSTALLATION_ID
              valueFrom:
                secretKeyRef:
                  name: grafana-git-sync-secret
                  key: github-installation-id
            - name: GITHUB_PRIVATE_KEY
              valueFrom:
                secretKeyRef:
                  name: grafana-git-sync-secret
                  key: github-private-key
          command:
            - sh
            - -euc
            - |
              echo "Installing dependencies..."
              apk add --no-cache yq

              ARCH=$(uname -m | sed 's/aarch64/arm64/')
              echo "Downloading grafanactl v0.1.9 (${ARCH})..."
              wget -qO- "https://github.com/grafana/grafanactl/releases/download/v0.1.9/grafanactl_Linux_${ARCH}.tar.gz" | tar xz -C /usr/local/bin

              echo "Waiting for Grafana to be ready..."
              until wget -qO /dev/null "${GRAFANA_SERVER}/api/health"; do sleep 5; done
              echo "Grafana is ready."

              echo "Preparing config files..."
              mkdir -p /tmp/config
              cp /config/repository.yaml /tmp/config/

              GITHUB_PRIVATE_KEY_B64=$(echo -n "${GITHUB_PRIVATE_KEY}" | base64 | tr -d '\n')
              export GITHUB_PRIVATE_KEY_B64

              yq ".spec.github.appID = \"${GITHUB_APP_ID}\"
                | .spec.github.installationID = \"${GITHUB_INSTALLATION_ID}\"
                | .secure.privateKey.create = strenv(GITHUB_PRIVATE_KEY_B64)" \
                /config/connection.yaml > /tmp/config/connection.yaml

              echo "Pushing resources to Grafana..."
              grafanactl resources push --stop-on-error -v --path /tmp/config
              echo "Done."
          volumeMounts:
            - name: config
              mountPath: /config
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: grafana-git-sync

この Job がやっていることを整理すると、以下の流れです。

  1. 依存ツールのインストール: yq(YAML 処理)と grafanactl(Grafana CLI)をダウンロード
  2. Grafana の起動待ち: /api/health エンドポイントをポーリングして Grafana が Ready になるまで待機
  3. 設定ファイルの準備: ConfigMap からマウントした connection.yaml に Secret の値を yq で注入
  4. リソースの登録: grafanactl resources push で Connection と Repository を Grafana に投入

ここで一つ工夫したのが、Connection の認証情報の扱いです。 ConfigMap にはテンプレートとして appIDprivateKey を空にした状態で配置し、Job の中で Secret から値を取得して yq で埋め込んでいます。 Private Key は Base64 エンコードして secure.privateKey.create に設定する必要がある点に注意です。

4.7 ダッシュボードの配置

ダッシュボードの JSON ファイルは、Repository リソースの path で指定した grafana/dashboards/ ディレクトリに配置します。

grafana/
└── dashboards/
    ├── argocd.json
    ├── cert-manager.json
    ├── coredns.json
    ├── grafana-mcp.json
    ├── k8s.json
    ├── kagent.json
    ├── kube-state-metrics.json
    └── node-exporter-full.json

既存のダッシュボードがある場合は、Grafana の Web UI からダッシュボードの JSON をエクスポートして、このディレクトリに配置してコミットすれば OK です。

5. ディレクトリ構成

最終的な Kustomize のディレクトリ構成はこのようになりました。

kustomize/grafana/
├── kustomization.yaml
├── namespace.yaml
├── deployment.yaml
├── service.yaml
├── pvc.yaml
├── http-route.yaml
├── grafana.ini
├── git-sync-configmap.yaml      # Connection + Repository の YAML テンプレート
└── git-sync-setup-job.yaml      # PostSync Hook で grafanactl を実行する Job

リポジトリのルートにはダッシュボード JSON を配置する grafana/dashboards/ ディレクトリがあり、Git Sync はここを同期対象として監視します。

6. 動作確認

6.1 Argo CD での同期

Argo CD で Grafana の Application を Sync すると、以下の順序で処理が進みます。

  1. Grafana の Deployment、Service、ConfigMap などがデプロイされる
  2. Grafana Pod が起動し、Ready になる
  3. PostSync Hook の Job が起動する
  4. Job が grafanactl で Connection と Repository を登録する
  5. Grafana が GitHub リポジトリのポーリングを開始する
  6. grafana/dashboards/ 配下のダッシュボード JSON が同期される

6.2 ダッシュボードの確認

同期が成功すると、Grafana のダッシュボード一覧に kubernetes-bootstrap というフォルダが作成され、その中にダッシュボードが表示されます。

ダッシュボード一覧

Administration → Provisioning の画面からも、リポジトリの接続状態と最終同期時刻を確認できます。

Provisioning 画面のリポジトリ一覧

リポジトリ名をクリックすると、Health や Pull status の詳細も確認できます。

Provisioning 画面のリポジトリ詳細

6.3 ダッシュボードの追加

新しいダッシュボードを追加する場合の流れです。

  1. grafana/dashboards/ にダッシュボードの JSON ファイルを配置
  2. Git にコミットしてプッシュ
  3. 最大 60秒後に Grafana に自動反映

逆に、Web UI でダッシュボードを編集した場合は、write ワークフローなら GitHub に直接コミットされ、branch ワークフローなら PR が作成されます。

7. つまずいたポイント

7.1 GitHub App 認証情報の扱い

grafanactl に渡す Connection リソースには GitHub App の appIDinstallationIDprivateKey を含める必要がありますが、ConfigMap に記載して git 管理下におくようなことはセキュリティリスクを考えると避けたいです。

そこで、先ほど説明した通り ConfigMap にはテンプレートとして値を空にした Connection YAML を配置しておき、Job の中で Kubernetes Secret から取得した値を yq で注入するようにしました。

yq ".spec.github.appID = \"${GITHUB_APP_ID}\"
  | .spec.github.installationID = \"${GITHUB_INSTALLATION_ID}\"
  | .secure.privateKey.create = strenv(GITHUB_PRIVATE_KEY_B64)" \
  /config/connection.yaml > /tmp/config/connection.yaml

これにより、Git リポジトリには機密情報を一切含めずに管理できます。

これも再掲にはなりますが、注意点として secure.privateKey.create には Private Key を Base64 エンコードした文字列を渡す必要があります。 PEM ファイルの中身をそのまま渡すと認証に失敗するので気をつけてください。

8. まとめ

Grafana の Git Sync を導入したことで、以下のメリットが得られました。

  • ダッシュボードがコードとして Git 管理される: 変更履歴の追跡、レビュー、ロールバックが可能に
  • 双方向同期: Web UI での編集も自動的に Git に反映されるので、運用の手間が増えない
  • Argo CD との統合: PostSync Hook で初期設定を自動化できる

個人的には、以前は git-sync サイドカー + provisioning で一方向同期する方法も検討していたのですが、Git Sync の双方向同期と PR ワークフローが使えるのは大きなアドバンテージだと感じました。 特に「ちょっとした修正は Web UI でサクッとやりたいけど、Git にも残したい」というユースケースにぴったりです。

最後まで読んでいただき、ありがとうございました。

9. 参考資料