CIOpsでGrafanaダッシュボードを運用していく
この記事は MicroAd Advent Calendar 2021 及び Ansible Advent Calendar 2021・Ansible Advent Calendar 2021 - Qiita の5日目の記事です。
はじめに
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を用意します。
以下のリポジトリに検証環境をおいているのでそちらを参考にしてください。
やらないこと
Grafanaのユーザーやチーム、プラグインの管理は対象外とします。 あくまでダッシュボード及びダッシュボードに必要なデータソースや通知チャンネルが対象となります。
また、データソースや通知チャンネルをgrafanaから取得する機能が利用するモジュールにないので諦め。
運用方法
今回のダッシュボード運用のために、通常利用する本番用とダッシュボードの開発用の2つのGrafanaを用意します。
また、ダッシュボードやデータソース、通知チャンネルといった情報を管理するためのリポジトリを用意します。
大まかにダッシュボードを更新する際の流れは下図の通りです。
具体的にはダッシュボードを更新する場合は以下の手順で更新します(上図の番号とは連動してません)。
- 本番Grafanaから更新したいダッシュボードをExport → View JSON → Copy to Clipboard
- 開発Grafanaにて、サイドバーの「+」→Create→importを選択し、クリップボードにコピーしたJSONを「Import via panel json」に貼り付けて「Load」を押下
- Optionにて、Folderを本番Grafanaと同じFolderを選択し、警告を無視して「Import(Overwirte)」を押下
- 開発Grafanaにて自由に更新する
- 手順1の様にしてダッシュボードをJSONファイルにExportする ※但し、ファイル名はダッシュボードのuid1とします(重要です)
- 管理用リポジトリにてbranchを作成
- 作成したbranchにて、DLしたJSONファイルを dashboardsに配置(無い場合は新規追加)
- 利用するダッシュボードが新たにデータリソースを変更・追加している場合は、別途 group_vars/XXX/datasources.yml に追加
- PR作成してレビューしマージ
- (マージ時に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 ※末尾が / で終わっているものはディレクトリ
ポイント
- ロールの実行に必要な変数は、対象のGrafana毎にグループ変数を用意して個別に設定を分ける
- Grafanaのトークンや通知チャンネル・データソースのクレデンシャル情報といった平文で保存すると都合の悪いものは、
vault_grafana.yml
に暗号化変数として定義 - GrafanaダッシュボードのJSONファイルは、
dashboards/
以下にダッシュボードのフォルダと同じ名称でディレクトリを作成し、グループ・フォルダ毎に保存 - 毎回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 2021 にCommunity.Grafanaを利用する話を書いていきます。楽しみが増えました(?)ね。
次回のアドカレは、以下のとおりです。
- MicroAd (マイクロアド) Advent Calendar 2021 では @taka_maenishiさんによる「ECDSA鍵暗号の作成と検証」の話(12/6)
- Ansible Advent Calendar 2021 では引き続き私によるCommunity.Grafanaを利用する話(12/6)
- Ansible Advent Calendar 2021 - Qiitaでは、 @comefigo さんによる「No More 変数汚染」の話(12/6)
楽しみですね。それではまた明日。
-
JSONファイルの末尾に “uid”: “l3KqBxCMz” のような形式で記述している。 cf. Dashboard HTTP API | Grafana Labs↩
-
詳しい説明はこちら:Ansible Vault — Ansible Documentation↩
-
作成方法はこちら: Create API Token - Authentication HTTP API | Grafana Labs↩