CIOpsでGrafanaダッシュボードを運用していく~ansible実装編~
この記事は Ansible Advent Calendar 2021 の6日目の記事です。
はじめに
以下の続きとなります。
以下で紹介するロールをどう使うか、メリットなどは前回の記事を参照ください。
Grafanaダッシュボードを更新する際に利用するAnsibleモジュールの紹介
Grafana向けにCommunity.Grafanaという Ansible Collectionがあるのでこちらを利用します1。
今回作成するロールについて
今回は以下の名前でロールを4つ作成します。
- import_datasource (データソースをGrafanaに反映する)
- import_notification_channel (通知チャンネルをGrafanaに反映する)
- export_resources (Grafanaからダッシュボードを取得する)
- import_dashboard (ダッシュボードをGrafanaに反映する)
また、ダッシュボードを更新する際は処理の順番に注意が必要です。
先にダッシュボードで必要となるデータソースや通知チャンネルを更新する必要があるので実行順序に注意が必要です。
また、ロールの全てについては紹介しません。今回はtaskの実行部分の紹介のみとし、moleculeのシナリオなどは省略します。
import_datasource(データソースをGrafanaに反映する)
データソースは community.grafana.grafana_datasource を利用して追加・更新します。
グループ変数の設定
Grafana毎に必要なデータソース用の変数を以下のように設定します。
--- # group_vars/prod/datasources.yml datasources: - name: "Prometheus Sample" is_default: true type: prometheus access: proxy orgId: 1 url: http://prom.example.com:9090/ - name: "Elasticsearch Sample" type: elasticsearch orgId: 1 url: http://es.example.com:9200 database: "[monitor-]YYYY.MM.DD" interval: "Daily" time_field: "@timestamp" es_version: 70 max_concurrent_shard_requests: 5 additional_json_data: logLevelField: "log.status" logMessageField: "log.message" - name: "PostgreSQL Sample" type: postgres orgId: 1 host: pg.example.com:5432 database: digdag1xx user: digdag password: "{{ vault_grafana.pg_pw }}" - name: "MySQL Sample" type: mysql orgId: 1 host: mysql.example.com:5432 database: grafana03_devel user: grafana password: "{{ vault_grafana.mysql_pw }}"
ロールの処理
main.yml
は以下の通り、グループ変数 {{datasources}}
を実行処理の import_datasource.yml
に渡してデータソース単位に更新します。
--- # roles/import_datasource/tasks/main.yml - name: Grafanaホストにデータソースをインポート include_tasks: import_datasource.yml with_items: "{{ datasources }}"
import_datasource.yml
ではデータソースのimportするための記述となりますが、 ds_type 単位に設定を書くと記述量が大量になってしまいます。
そこで、1タスクで community.grafana.grafana_datasource
で用しているパラメータをすべて列挙しています。
また、 {{datasources}}
で定義していない項目は default(omit)
で利用しないようにしています。
--- # roles/import_datasource/tasks/import_datasource.yml - name: "Import datasource :{{ item.name }}" community.grafana.grafana_datasource: grafana_url: "{{ grafana_url }}" grafana_api_key: "{{ vault_grafana.grafana_api_key | default(omit) }}" grafana_password: "{{ vault_grafana.url_password | default(omit) }}" grafana_user: "{{ vault_grafana.url_username | default(omit) }}" access: "{{ item.access | default(omit) }}" additional_json_data: "{{ item.additional_json_data | default(omit) }}" additional_secure_json_data: "{{ item.additional_secure_json_data | default(omit) }}" basic_auth_password: "{{ item.basic_auth_password | default(omit) }}" basic_auth_user: "{{ item.basic_auth_user | default(omit) }}" client_cert: "{{ item.client_cert | default(omit) }}" database: "{{ item.database | default(omit) }}" ds_type: "{{ item.type }}" ds_url: "{{ item.url }}" enforce_secure_data: "{{ item.enforce_secure_data | default(omit) }}" es_version: "{{ item.es_version | default(omit) }}" interval: "{{ item.interval | default(omit) }}" is_default: "{{ item.is_default | default(omit) }}" max_concurrent_shard_requests: "{{ item.max_concurrent_shard_requests | default(omit) }}" name: "{{ item.name }}" org_id: "{{ item.orgId }}" sslmode: "{{ item.sslmode | default(omit) }}" state: "{{ item.state | default(omit) }}" time_field: "{{ item.time_field | default(omit) }}" time_interval: "{{ item.time_interval | default(omit) }}" tls_skip_verify: "{{ item.tls_skip_verify | default(omit) }}" tsdb_resolution: "{{ item.tsdb_resolution | default(omit) }}" tsdb_version: "{{ item.tsdb_version | default(omit) }}" use_proxy: "{{ item.use_proxy | default(omit) }}" user: "{{ item.user | default(omit) }}" validate_certs: "{{ item.validate_certs | default(omit) }}" with_credentials: "{{ item.validate_certs | default(omit) }}"
import_notification_channel(通知チャンネルをGrafanaに反映する)
通知チャンネルは、community.grafana.grafana_notification_channel を利用して追加・更新します。
グループ変数の設定
Grafana毎に必要な通知チャンネル用の変数を以下のように設定します。
--- # group_vars/prod/notification_channels.yml notification_channels: - name: "info notice slack" type: slack uid: "c80pnXDvYq" recipient: "#info" orgId: 1 is_default: true include_image: true disable_resolve_message: false url: https://hooks.slack.com/services/XXXX/YYYY/ZZZZZ token: "{{ vault_grafana.slack_token_info }}" - name: "grafana Info mail" type: email orgId: 1 uid: "EQ8Pn7paaX" is_default: false include_image: true disable_resolve_message: true reminder_frequency: "" email_addresses: - "issue@example.com" email_single: false
ロールの処理
データソースの場合と同様です。
main.yml
では、グループ変数 {{notification_channels}}
を実行処理の import_notification_channel.yml
に渡しています。
--- # roles/import_notification_channel/tasks/main.yml - name: Grafanaに通知チャンネルをインポート include_tasks: import_notification_channel.yml with_items: "{{ notification_channels }}"
import_notification_channel.yml
も同様です。
community.grafana.grafana_notification_channelのパラメータ にある項目を1つにまとめて記述します。
--- # roles/import_notification_channel/tasks/import_notification_channel.yml - name: "Create slack notification channel :{{ item.name }}" community.grafana.grafana_notification_channel: grafana_url: "{{ grafana_url }}" grafana_api_key: "{{ vault_grafana.grafana_api_key | default(omit) }}" url_password: "{{ vault_grafana.url_password | default(omit) }}" url_username: "{{ vault_grafana.url_username | default(omit) }}" uid: "{{ item.uid }}" name: "{{ item.name }}" org_id: "{{ item.orgId }}" is_default: "{{ item.is_default }}" include_image: "{{ item.include_image }}" disable_resolve_message: "{{ item.disable_resolve_message }}" reminder_frequency: "{{ item.reminder_frequency | default(omit) }}" type: "{{item.type}}" slack_url: "{{ item.url | default(omit) }}" slack_token: "{{ item.token | default(omit) }}" slack_recipient: "{{ item.recipient | default(omit) }}" slack_mention_users: "{{ item.mention_users | default(omit) }}" slack_mention_groups: "{{ item.mention_groups | default(omit) }}" email_addresses: "{{ item.email_addresses | default(omit) }}" email_single: "{{ item.email_single | default(omit) }}"
import_dashboard(ダッシュボードをGrafanaに反映する)
ダッシュボードを追加・更新するには community.grafana.grafana_dashboard のImportを利用します。
また、フォルダに分けてダッシュボードを登録することになるので、合わせて
community.grafana.grafana_folder も利用します。
さらに、既存のダッシュボードの一覧を変数として取得する grafana_dashboardのLookup Plugin も利用します。
グループ変数の設定
Grafana毎に必要なダッシュボード用の変数を以下のように設定します。
こちらにはImport/exportの対象となるダッシュボードのフォルダ名を列挙します。
また、 Dashboard HTTP API の "Create / Update dashboard" にあるようにデフォルトフォルダがGeneralなのでこれは必ず入れておきます。
--- # group_vars/prod/dashboards.yml gf_folder: - General - XXXXX
ロールの処理
ダッシュボードフォルダ以下のダッシュボードは以下のファイルパス上に保存しています。
dashboards/(グループ名)/(フォルダ)/(ダッシュボードのUID).json
そこでフォルダ以下の全てのJSONファイルパスを取得して、ダッシュボード1つずつGrafanaにimportします。
mail.yml
では、対象のダッシュボードのフォルダのリストとなるグループ変数 gf_folder
をループして、ファルダを作成します(import_folder.yml
)。
次にダッシュボードも同様にgf_folder
を使ってimportしていきます(import_dashboard.yml
)。
--- # roles/import_dashboard/tasks/main.yml - name: フォルダ include_tasks: import_folder.yml with_items: "{{ gf_folder }}" loop_control: loop_var: folder - name: ダッシュボード include_tasks: import_dashboard.yml with_items: "{{ gf_folder }}" loop_control: loop_var: folder
フォルダのimport(import_folder.yml
)について、グループ変数 gf_folder
から対象のフォルダ名がループで渡ってくるので、それをもとにフォルダをimport(作成)します。ただし、デフォルトで入っている General
については不要なので実施内容にしている(無視して実行するとエラーになります)。
--- # roles/import_dashboard/tasks/import_folder.yml - name: "{{ folder }} をGrafanaに作成" community.grafana.grafana_folder: url: "{{ grafana_url }}" grafana_api_key: "{{ vault_grafana.grafana_api_key | default(omit) }}" url_password: "{{ vault_grafana.url_password | default(omit) }}" url_username: "{{ vault_grafana.url_username | default(omit) }}" name: "{{ folder }}" state: present when: "folder != 'General'"
最後にダッシュボードは、フォルダ単位にダッシュボードをimportします(import_dashboard.yml
)。
まず、ダッシュボードのJSONファイルを dashboards/prod_grafana/(フォルダ)
以下から fileglob を使って取得します。そして取得したリストでloopしてダッシュボードを1つずつimportしています。
--- # roles/import_dashboard/tasks/import_dashboard.yml - name: "{{ folder }} のダッシュボードをGrafanaに反映" community.grafana.grafana_dashboard: grafana_url: "{{ grafana_url }}" grafana_api_key: "{{ vault_grafana.grafana_api_key | default(omit) }}" url_password: "{{ vault_grafana.url_password | default(omit) }}" url_username: "{{ vault_grafana.url_username | default(omit) }}" state: present overwrite: yes commit_message: "Push hash: {{ revision }}" path: "{{ _json_file_path }}" folder: "{{ folder }}" loop: "{{ lookup('fileglob', playbook_dir + '/dashboards/' + grafana_group + '/' + folder + '/*.json', wantlist=True) }}" loop_control: loop_var: _json_file_path
export_resources(Grafanaからダッシュボードを取得する)
既存のダッシュボードを取得するには community.grafana.grafana_dashboard のExportを利用します。 さらに、既存のダッシュボードの一覧を変数として取得する grafana_dashboardのLookup Plugin も利用します2。
グループ変数の設定
import_dashboard で紹介したグループ変数 gf_folder
を使うので割愛します。
ロールの処理
大まかな処理の流れとしては、Grafanaから根こそぎダッシュボード情報を取得してfactsに保存する。
その後、保存したfactsを使ってダッシュボード1つずつloop変数に入れて回しながらダッシュボードのフォルダ単位にディレクトリを作成しつつ整形してJSONファイルに変換しています。
--- # roles/export_resources/tasks/main.yml - name: ダッシュボード情報を取得 include_tasks: get_all_dashbords.yml - name: JSONファイルに変換 include_tasks: export_dashboard_to_json.yml with_items: "{{ __dashboards }}" loop_control: loop_var: dashboards
では、掘り下げていきます。
まず、get_all_dashbords.yml
ではGrafanaサイトからダッシュボード情報をfacts __dashboards
に保存します。
--- # roles/export_resources/tasks/get_all_dashbords.yml - name: すべてのダッシュボードをfacts __dashboards にセット set_fact: __dashboards: "{{ lookup('grafana_dashboard', 'grafana_url=' ~ grafana_url + ' ' + 'grafana_api_key=' ~ vault_grafana.grafana_api_key | replace('==', '') ) }}"
次に、実行処理となる export_dashboard_to_json.yml
についてです。
大まかには、1つのダッシュボード情報が 変数 dashboards
に入ってるのでGrafanaにフォルダ→ダッシュボードの順でimportします。
まず、ダッシュボードJSONファイルを出力するパスを作成します。
「変数 dashboards
にあるフォルダ名」と「確定しているGrafanaグループまでのパス({{ playbook_dir }}/dashboards/{{ grafana_group }}
)」を組み合わせて、facts export_dir
にフォルダまでのパスを取得します。
次に、作成したexport_dir
を使ってJSONファイルの出力先のフォルダを作成します。
JSONファイルの出力先のフォルダパスが決まったので一時的に tmp.json
として、GrafanaからExportします。
書き出したJSONファイルは1行になっているのでPRする際に差分が取りにくくレビューしづらいので整形して出力します。
また、JSONファイル名は、他のダッシュボードのファイル名と衝突しないようにダッシュボードのUIDを使うことで回避しています。
--- # roles/export_resources/tasks/export_dashboard_to_json.yml - name: JSONファイルの出力先ディレクトリ変数 set_fact: export_dir: "{{ playbook_dir }}/dashboards/{{ grafana_group }}/{{ dashboards.folderTitle | default('General') }}" - name: "{{ export_dir }}を作成" ansible.builtin.file: path: "{{ export_dir }}" state: directory recurse: true - name: "Export dashboard :{{ dashboards.title }}" community.grafana.grafana_dashboard: grafana_url: "{{ grafana_url }}" grafana_api_key: "{{ vault_grafana.grafana_api_key | default(omit) }}" url_password: "{{ vault_grafana.url_password | default(omit) }}" url_username: "{{ vault_grafana.url_username | default(omit) }}" org_id: 1 state: export uid: "{{ dashboards.uid }}" path: "{{ export_dir }}/tmp.json" - name: "JSONファイルを読み込み" command: "cat {{ export_dir }}/tmp.json" register: raw_json_contents - name: "仮出力したJSONファイル raw_json_contents を facts __dashboard_json_contents に格納" set_fact: __dashboard_json_contents: "{{ raw_json_contents.stdout | from_json }}" - name: "__dashboard_json_contentsを整形してJSONファイル出力する" copy: content: "{{ __dashboard_json_contents | to_nice_json(indent=2) }}" dest: "{{ export_dir }}/{{ dashboards.uid }}.json" - name: "rm tmp.json" ansible.builtin.file: path: "{{ export_dir }}/tmp.json" state: absent
Grafanaとの認証に関する注意事項
各ロールでは、Grafanaにアクセスする際は認証が必要になります。
Grafana v7.4以降を利用している場合は、認証にパラーメタ grafana_api_key
を利用します。
しかし、Grafana v7.0辺りの古いバージョンを利用している場合は、認証にパラーメタ grafana_user
と grafana_password
を利用するので注意してください。
# 例 - name: "Import datasource :{{ item.name }}" community.grafana.grafana_datasource: grafana_url: "{{ grafana_url }}" grafana_api_key: "{{ vault_grafana.grafana_api_key | default(omit) }}" # v7.4以降はこれだけ grafana_password: "{{ vault_grafana.url_password | default(omit) }}" # v7.0以前はこっちを使う grafana_user: "{{ vault_grafana.url_username | default(omit) }}" # v7.0以前はこっちを使う
特に、 roles/export_resources/tasks/get_all_dashbords.yml
で利用している grafana_dashboardのlookupプラグインの記述方法が異なるのでご注意ください。
最後に
次回は、 @Endyさんの「 Ansible初心者向けの学習ロードマップ」(12/8)とのことです。楽しみですね!
-
各モジュールの利用方法は、Collectionの各モジュールのExamplesも参考になりますが、GrafanaのCollectionのあるリポジトリのtest環境が非常に参考になります。
github.com
他にも、各処理は最終的にはHTTP APIを使って処理しているので、以下のドキュメントの調べたいロールのAPIを参考にすると良いです。
HTTP API | Grafana Labs↩ -
Tips: Lookup Plugins: grafana_dashboard について
set_factsでダッシュボードの情報を根こそぎディクショナリ変数に変換出来る。
community.grafana.grafana_dashboard の情報よりも以下の実装を直接見た方が分かりやすいです。
https://github.com/ansible-collections/community.grafana/blob/main/plugins/lookup/grafana_dashboard.py
また、上記の実装を見てわかるように Folder/Dashboard Search HTTP API | Grafana Labs を叩いていることがわかります。↩