CIOpsでGrafanaダッシュボードを運用していく

この記事は MicroAd Advent Calendar 2021 及び Ansible Advent Calendar 2021Ansible Advent Calendar 2021 - Qiita の5日目の記事です。

qiita.com
adventar.org

はじめに

Grafana(だけじゃないはず)ダッシュボードを運用して、沢山グラフが増えてきた際に、こんな困りごとは無いですか?

  • 「このダッシュボードのグラフ壊れてるんですけど」
  • 「このグラフってなんでXXXがYYYYに変更されるんだ?」
  • 「そもそもこのダッシュボード or グラフってなんで追加したんだっけ?」
  • 「新しいバージョンで動くか検証したい」

以上のようにダッシュボードが増えるほど運用が大変になります。
Grafanaのダッシュボード自身はGrafanaの機能としてバージョン管理しているので簡単に戻すことは可能ですが、作成者が意図した状態を常に保っていて欲しいものです。

そこで、CIOpsでPRベースにGrafanaダッシュボードを管理する方法をご紹介します。

環境

今回の記事は以下を前提としています。

項目 Ver.
OS CentOS7以降、Ubuntu20.04以降
S/W Docker v20.10、Docker Compose v2.2.1
Grafana v8.2.5
ansible-core v2.11.6(v2.9でも動きます)
Jenkins(CircleCIでもGitHub Actionsでも何でも)

検証環境の用意

弄り倒すGrafanaを用意します。
以下のリポジトリに検証環境をおいているのでそちらを参考にしてください。

github.com

やらないこと

Grafanaのユーザーやチーム、プラグインの管理は対象外とします。 あくまでダッシュボード及びダッシュボードに必要なデータソースや通知チャンネルが対象となります。

また、データソースや通知チャンネルをgrafanaから取得する機能が利用するモジュールにないので諦め。

運用方法

今回のダッシュボード運用のために、通常利用する本番用とダッシュボードの開発用の2つのGrafanaを用意します。
また、ダッシュボードやデータソース、通知チャンネルといった情報を管理するためのリポジトリを用意します。

大まかにダッシュボードを更新する際の流れは下図の通りです。

f:id:yassan0627:20211205065905p:plain
ダッシュボード更新の流れ

具体的にはダッシュボードを更新する場合は以下の手順で更新します(上図の番号とは連動してません)。

  1. 本番Grafanaから更新したいダッシュボードをExport → View JSON → Copy to Clipboard
  2. 開発Grafanaにて、サイドバーの「+」→Create→importを選択し、クリップボードにコピーしたJSONを「Import via panel json」に貼り付けて「Load」を押下
  3. Optionにて、Folderを本番Grafanaと同じFolderを選択し、警告を無視して「Import(Overwirte)」を押下
  4. 開発Grafanaにて自由に更新する
  5. 手順1の様にしてダッシュボードをJSONファイルにExportする ※但し、ファイル名はダッシュボードのuid1とします(重要です)
  6. 管理用リポジトリにてbranchを作成
  7. 作成したbranchにて、DLしたJSONファイルを dashboardsに配置(無い場合は新規追加)
  8. 利用するダッシュボードが新たにデータリソースを変更・追加している場合は、別途 group_vars/XXX/datasources.yml に追加
  9. PR作成してレビューしマージ
  10. (マージ時にJenkinsにてAnsible使って本番Grafanaにダッシュボードを反映)

また、本番Grafanaがリポジトリと乖離すると運用が崩れます。
そこで、夜中にリポジトリダッシュボード、データソース、通知チャンネルを全て本番Grafanaと同期して防止します。

この運用の良い面と課題

良い面

  • 動作保証しているリポジトリの状態を常に保つことが出来るので「なんか知らんけど壊れてた」を防止
    • 補足:Grafanaにはダッシュボードのバージョンがあるので戻すのも簡単ですがどこが壊れてない場所かは探さないといけない
  • PRレビューをするのでダッシュボードの更新経緯を残しておけるし、誰かの目が入るので属人性を緩和できる
  • ダッシュボードの更新をIaCしたことで、別のホストで検証用に立ち上げたGrafanaや手元の環境にシュッとダッシュボード一式を用意出来る
  • PRレビューでダッシュボードの差分がコードで出るのでGrafana上でのパッと見で分からない点に気づくことが出来る

課題

  • 更新する際に本番GrafanaからダッシュボードをExportして開発GrafanaにImportするなど、更新が若干手間

では、具体的な話に入っていきます。

リポジトリの構成

リポジトリの構成は以下の通り。

.
├── collections
│   └── requirements.yml
├── dashboards/  ・・・・ダッシュボードの更新に使うJSONファイルの置き場所
│   ├── devel/  ・・・・グループ毎にダッシュボードのフォルダ名と同じように構成する
│   │   ├── General/
│   │   └── XXXXXX/
│        :
├── group_vars/  ・・・・グループ毎に必要な変数
│   └── devel/
│       ├── general.yml ・・・・・GrafanaのURLなど各ロールで共通して利用する変数
│       ├── dashboards.yml  ・・・ダッシュボードの取得・更新に必要な変数
│       ├── datasources.yml ・・・データソースの更新に必要な変数
│       ├── notification_channels.yml ・・・通知チャンネルの更新に必要な変数
│       └── vault_grafana.yml ・・平分で保存したくないものをすべてここに変数として保存
│        :
├── inventories/
│   ├── inventory
│   └── local
├── roles/
│   ├── XXXXXX/  ・・・必要になるロール
│            :
├── .vault_password ・・復号用PWファイル(リポジトリには含まない)
├── playbook.yml  ・・・実行用Playbook
├── view_secret.yml ・・暗号化変数を復号して中身を確認するPlaybook(管理用)
├── requirements.txt
├── Jenkinsfile ・・・・CI用の設定ファイル(今回はJenkins Pipelineをつかうので)
├── Makefile  ・・・・・各種コマンドはmakeで実行
└── ansible.cfg

※末尾が / で終わっているものはディレクトリ

ポイント

  1. ロールの実行に必要な変数は、対象のGrafana毎にグループ変数を用意して個別に設定を分ける
  2. Grafanaのトークンや通知チャンネル・データソースのクレデンシャル情報といった平文で保存すると都合の悪いものは、 vault_grafana.yml に暗号化変数として定義
  3. GrafanaダッシュボードのJSONファイルは、dashboards/ 以下にダッシュボードのフォルダと同じ名称でディレクトリを作成し、グループ・フォルダ毎に保存
  4. 毎回ansibleコマンドを打つのがめんどいのでmakeを使って簡略化(これはCIを定義する際にもCI側定義を楽にできます)

具体的な実装方法について

ロールを適用するには

以下のPlaybookを使って、ロールを適用します。単純に変数 role_name で指定したロールのみを変数 grafana_group にあるグループのみに適用します。

---
# playbook.yml
- hosts: "{{ grafana_group }}"
  connection: local
  gather_facts: false
  become: false
  vars:
    ansible_python_interpreter: "{{ python_interpreter | default('/usr/bin/python3')}}"
  roles:
    - "{{ role_name }}"

また、インベントリ(inventories/inventory)は以下のようにしています。

# inventories/inventory
# 本番用Grafana
[prod]
prod-grafana.example.com

# 開発用Grafana
[devel]
devel-grafana.example.com

上記をもとに ansible-playbook コマンドを使って実行します。
また、 playbook.yml で利用する各変数は追加変数として --extra-vars を用いて指定しています。

例えば、ロール名 export_dashboard を グループ prod に適用する場合は以下の通り。

$ ansible-playbook -i inventories/inventory --connection=local \
        --vault-id .vault_password \
        playbook.yml \
            --extra-vars role_name=export_dashboard \
            --extra-vars grafana_group=prod ;

ただ、毎回上記のコマンドを打つのがめんどくさいのでMakefileに以下ように追加します。 すると、 make export_dashboard group=prod だけで済むようになります。

.PHONY: export_dashboard
export_dashboard:
  ansible-playbook -i inventories/inventory --connection=local \
      --vault-id .vault_password \
      playbook.yml \
          --extra-vars role_name=export_dashboard \
          --extra-vars grafana_group=$(group) ;

シークレット情報の扱いについて

データソースや通知チャンネルには、一部、平文で保存してはいけないパスワードやトークンなどが含まれます。 Gitリポジトリで管理するので、そのまま記述出来ません。

そこで、Vault変数を用いて暗号化し、実行に内部で復号して利用します。
今回はencrypt_stringを用いて、YAMLファイルに埋め込む暗号化変数2をもちいます。

以下がその例です。

---
# group_vars/devel/vault_grafana.yml
vault_grafana:
  grafana_api_key: !vault |
    $ANSIBLE_VAULT;1.1;AES256
    36353963333633386231333964343035346539633430646434333931656634663064306164336663
    3632393562643635343062336531653162383835653633620a366561383465393930306130623434
    37656362393038376533323533613530313938306262366463343433363666323662306562653264
    6130356262316233300a616135616535643536346138626133343231333232323730663539376237
    34383430316265303062613233326438303932626330356339663962353634333263373633666538
    63323335653832356636326565656230303661626335623132356230613434616164363836666266
    65646232353137643732303535613066653739363639303263393937613465373364393738623264
    32613662346639393964

上記のように定義することで、必要な箇所で "{{ vault_grafana.slack_token_test }}" として呼び出し可能になります。

暗号化変数の登録方法について

登録は以下のようにコマンドを実行して、暗号化変数を生成します。 また、事前に .vault_password を作成して中に暗号化キーを入れておいてください。

$ ansible-vault encrypt_string \
        --vault-id .vault_password \
        --stdin-name "hoge_token" ;
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)
himitsu
hoge_token: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          35613234613332316261383133343830346230343938343765326237623037393435396166633830
          3839373538376361616538666135316134343935636363370a643839666537323539316133346238
          63636137363165343035303032613461356363626134656465323237626435353138386663656135
          3239343634323937610a393835333438653738363133323764376161323665643736323664633466
          6266
Encryption successful

必要なのは以下の部分だけで、これをコピーして group_vars/devel/vault_grafana.yml に追加していけばOK。

hoge_token: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          35613234613332316261383133343830346230343938343765326237623037393435396166633830
          3839373538376361616538666135316134343935636363370a643839666537323539316133346238
          63636137363165343035303032613461356363626134656465323237626435353138386663656135
          3239343634323937610a393835333438653738363133323764376161323665643736323664633466
          6266

以下のように追加します。

---
# group_vars/devel/vault_grafana.yml
vault_grafana:
  grafana_api_key: !vault |
    $ANSIBLE_VAULT;1.1;AES256
    :
  hoge_token: !vault |
    $ANSIBLE_VAULT;1.1;AES256
    35613234613332316261383133343830346230343938343765326237623037393435396166633830
    3839373538376361616538666135316134343935636363370a643839666537323539316133346238
    63636137363165343035303032613461356363626134656465323237626435353138386663656135
    3239343634323937610a393835333438653738363133323764376161323665643736323664633466
    6266

また、これも以下のようにMakefileに追加すます。

.PHONY: generate-vault-string
generate-vault-string:
  ansible-vault encrypt_string \
      --vault-id .vault_password \
      --stdin-name "$(name)" ;

登録したVault変数を確認するには

登録後に正しく値が入っているか確認するためにPlaybookを作成します。

---
# view_secret.yml
- name: Execute a role
  hosts: localhost
  gather_facts: no
  pre_tasks:
    - name: Include variables
      include_vars: 
        dir: "{{ target_dir }}"
        ignore_unknown_extensions: true
        
  tasks:
    - name: view vault string
      debug:
        var: "{{ lookup('vars', item) }}"
      loop:
        - target_vault

上記のPlaybookを以下のように実行します。登録時と同じようにmakeから実行できるようにします。 また、利用方法は make view-vault-string dir=group_vars/devel name=vault_grafana のようにて実行します。

.PHONY: view-vault-string
view-vault-string:
   @ansible-playbook -i inventories/inventory \
      --vault-id .vault_password \
      view_secret.yml \
          -e target_dir=$(dir) -e target_vault=$(name)

CIについて

今回はJenkins Pipelineを使うので、以下のように定義します。

pipeline {
    agent {
        label 'worker'
    }
    triggers {
        // 毎日0am(JST)のどっかで実施
        cron '''TZ=Asia/Tokyo
        0 0 * * *'''
    }
    environment {
        ANSIBLE_VAULT_PASSWORD = credentials('vault_password_ansible_ci')
    }
    stages {
        stage('定期更新: prod-grafana') {
            when {
                // 定期実行のみの処理(masterのみ)
                branch 'master'
                triggeredBy 'TimerTrigger'
            }
            steps {
                println "Execute Test Node [${NODE_NAME}]"
                // Pythonの仮想環境のSetup
                sh '''
                    export HTTP_PROXY=http://XXX:8080
                    export HTTPS_PROXY=http://XXX:8080
                    make init
                    make init-test
                '''
                // 本番環境の更新
                sh '''
                    echo $ANSIBLE_VAULT_PASSWORD > .vault_password
                    source .venv/bin/activate
                    make import_notification_channel group=prod
                    make import_datasource group=prod
                    make import_dashboard group=prod
                '''
            }
        }
        stage('Update: Grafanaリソース') {
            // 課題:変更差分からグループと対応するGrafanaリソースをパース出来てないので
            //    changesetでcheckしてprod決め打ちでmasterマージ時に更新する
            // → masterへのmergeリクエストの際に、変更差分がpatternにマッチした場合に実行
            when {
                branch 'master'
                changeset pattern: '^group_vars/prod/.*|^dashboards/prod/.*', comparator: 'REGEXP'
            }
            steps {
                println "Execute Test Node [${NODE_NAME}]"
                // Pythonの仮想環境のSetup
                sh '''
                    export HTTP_PROXY=http://XXX:8080
                    export HTTPS_PROXY=http://XXX:8080
                    make init
                    make init-test
                '''
                // 本番環境の更新
                sh '''
                    echo $ANSIBLE_VAULT_PASSWORD > .vault_password
                    source .venv/bin/activate
                    make import_notification_channel group=prod
                    make import_datasource group=prod
                    make import_dashboard group=prod
                '''
            }
        }
    }
}

ポイントは以下の通り。

  • Vault変数を復号するためにJenkinsのクレデンシャルに vault_password_ansible_ci として、本番GrafanaのAdminロールのAPIキー3を登録している
  • 定期的に同期を取るように TimerTrigger を使って実行
  • マスターへのマージ時に本番Grafanaのグループ変数やダッシュボードに更新が入った場合に同期を行う
    • ただし、個別に更新が出来てないので全て同期をとってます

次に、各ロールで実行するansibleのtaskの記述方法を説明するのですが、長くなってきたので今回はこの辺で。

続きは、 Ansible Advent Calendar 2021Community.Grafanaを利用する話を書いていきます。楽しみが増えました(?)ね。

次回のアドカレは、以下のとおりです。

楽しみですね。それではまた明日。


  1. JSONファイルの末尾に “uid”: “l3KqBxCMz” のような形式で記述している。 cf. Dashboard HTTP API | Grafana Labs

  2. 詳しい説明はこちら:Ansible Vault — Ansible Documentation

  3. 作成方法はこちら: Create API Token - Authentication HTTP API | Grafana Labs