株式会社はてなに入社しました
TiDBをHadoop管理者視点でデータ基盤としての使い所を考えてみる
この記事は MicroAd Advent Calendar 2022 と Distributed computing (Apache Spark, Hadoop, Kafka, ...) Advent Calendar 2022 の25日目の記事です。
今年のアドカレも最終日になりました。
とは言え、Distributed computing Advent Calendar 2022 の方はエントリが結構残ってるので、引き続き空いてる枠への参加をお待ちしてます!
今回は、Hadoopクラスタ管理者の視点で、データ基盤としてTiDBの使い所について考えてみます。 また、データ基盤の規模感としては、ペタバイトレベルのクラスタ、1テーブルあたり最大100TBクラスを想定しています。
- TiDBとは
- データ基盤として見たときにTiDBに求めるもの
- 構築する場合の運用しやすさ
- TiUPコマンドがとにかく便利
- 負荷テストやベンチ、Sandbox環境も簡単。TiUPならね。
- KubernetesでのTiDB構築について
- データの可用性について
- データベースやテーブルサイズに対する上限
- テーブルサイズや行数について
- パーティションについて
- テーブルのデータ型やデータ操作について
- スキーマ進化出来るか(テーブル定義やパーティションの変更しやすさ)
- 構築する場合の運用しやすさ
- 使い所を考える
- さいごに
BigData向けワークロード(Spark/Flink)に適したKubernetesカスタムスケジューラ Apache YuniKorn について
この記事は Distributed computing Advent Calendar 2022 の5日目の記事です1。
今回は、今年の5月にApache Software Foundation (ASF)のTop-Level Project (TLP)2になった Apache YuniKorn について紹介します。
YuniKornの概要
YuniKornは、YARNで要求されていたワークロードに関するリソーススケジューリングをKubernetesで利用できるよう作成したようです。
YARN(Yet Another Resource Negotiator)とは、Hadoopクラスタ内のジョブのスケジュールやリソース管理する基盤です。
YARN自体に馴染みのない方は以下を見るとYuniKornも理解しやすいです。
YARNの概要とFAIRスケジューラについてはこちら。
www.slideshare.net
Capacityスケジューラについてはこちら。
また、Hadoop3エコシステムはだいたいJava(一部C++など)で実装されていますが、YuniKornはgolangで実装されています。
YuniKornの由来と読み方
YuniKornの名前の由来は "Y"をYARN(フロントエンドのyarnじゃないよ)、"K"をKubernetes、"Uni"をUnifiedから取っているそうです。 また、発音は[‘ju:nikɔ:n]となり、ロゴにもなっているUnicornと同じです4。
なぜYuniKornが必要なのか?
BigDataやMLでのワークロードは、大量のデータを処理しなければならないので、レイテンシよりもスループットが非常に大事です。
また、この手のワークロードはマルチテナントで利用されることが多く、リソーススケジューリングにする要求は複雑になりがちです。
例えば、以下のような事が普通に求められ、これまで、YARNにて対応してきました(YARNスゴイヨ)。
- 大事なバッチにはクラスタの半分のリソースを優先して割り当てる
- 部門Aのアドホックなワークロードにはクラスタリソースの10%までを割り当てる
- ただし、バッチなど優先したいワークロードがなくヒマしているなら、10%を超えて使ってもOK
- とはいえ、メインのバッチが始まったらワークロードをプリエンプティブする
- さらに、部門Aのリソースのうち、半分はBさんが優先して使ってOK
そして、これまでHadoopエコシステムのストレージ層はHDFSでしたが、Hadoop基盤自体がパブリッククラウドで利用されるようになった結果、S3、ADSL、GCSといったオブジェクトストレージを中心に利用するようになりました(HDFSの場合は常時稼動が必要になり割高になるので)。
また、データを貯めていたストレージ層とワークロードを実行するコンピュート層が分離しやすい環境が整ってきた結果、コンピュート層を柔軟にスケール出来る環境としてコンテナの活用が期待されています。
そこで、ワークロードの中心的なSparkとFlinkは、YARN以外にもKubernetesでアプリを実行できるようになりました5。
一方、Kubernetesのリソーススケジューラは、上記のような柔軟なリソーススケジューリングには対応できないので、そこを補う形でBigDataやMLワークロードに特化したリソーススケジューラとしてYuniKornが出てきました。
ただ、Kubernetesとしてバッチのワークロードについて何もしていないわけではなく、 Batch Working Group として議論・サポートを進めています6。
どこが使ってるのか?
Hadoopといえば、ClouderaですがClouderaのパブリッククラウドサービスのCDP Public CloudのCloudera Data Engineering(CDE)というサービスにYuniKornが利用されています(AWSとAzureで使えるみたいです)。
また、Cloudera以外にもAlibaba、Apple、Lyft、Visa、Zillowで使っているようです。
そして、10月にあったYuniKorn MeetupではPinterestがスケジューラの性能評価について話していて、YARNからKubernetes(YuniKorn)への移行を検討しているようです7。
リリース状況
YuniKorn自体は、今年の5月(2022/5/6)にGAしています!
現状の最新は 2022/9/8 にリリースされた v1.1.0 となっています。
また、Roadmapによると、年明け1月には、v1.2.0がリリース予定のようです。
YuniKornの特徴
YuniKornの主な機能は以下の通りです。
アプリを考慮したスケジューリング
KubernetesデフォルトのPod単位のスケジューリングとは異なり、ユーザー、アプリ、キューを認識して、リソースの状況や順序などの要素を考慮しながらスケジューリングを行います。
Kubernetesでは、PodをSubmitする際にPodがNamespaceのクォータに収まるように強制し、収まらない場合には、Podは拒否されます。その為、クライアント側で再試行出来るよう複雑な実装が必要になります。
一方、YuniKornは、Kubernetesのクォータとは異なり、SubmitされたたPodは常に受け入れられます。
つまり、YuniKornはキューに入ったPodのリソースを消費したクォータとしてカウントせずに、そのPodをキューにいれます。YuniKornがPodをスケジュールしようとすると、スケジューリング時にPodが割り当てられたキューに設定されたクォータに収まるかどうかチェックします。その時にPodがクォータに収まらない場合、Podはスキップされてリソース消費にカウントされません。これは、Podのスケジュール試行に成功するまでは、YuniKornクォータシステムでPodはリソースを消費していないことを意味しています。
階層型のリソースキュー
クラスタリソースを階層型にキューで管理します。また、キューの最小と最大容量を設定出来ます。階層ごとにリソースを制御出来るので、マルチテナント利用の場合に柔軟な対応が可能になります。
そして、リソースキューの状況はYuniKornのUIでモニタリング出来ます(クラスタリソースの容量、利用率、およびすべてのアプリケーション情報を一元的に表示出来ます)。
出典: Get Started | Apache YuniKorn より
ジョブオーダーとキューイング
アプリは作業キューに格納して、どのアプリが最初にリソースを取得できるのかは、以下のポリシーに応じて決定します。
現状設定可能なポリシーは、以下の3つ。
FIFO
: そのまま。先入れ先出し。Fair
: アプリケーションの使用量に応じて公平にリソースを配分。StateAware
: 実行中または受け入れ状態のアプリは1つに制限される。
StateAware
が分かりにくいので補足します。
Sparkアプリを例にとると想像しやすいです。Sparkアプリは、アプリ全体の調整役となるDriverと、Driverが割り当てた細かいタスクを実行する複数のExecuterに分かれて実行していきます。
例えば、あるキュー(app-queue-1)にapp1→app2の順にSparkアプリをsubmitしたときは以下の様に実行されます。
さらに、キューの最大容量を設定した場合、ジョブやタスクはリソースキューに適切にキューイングされて、残りの容量が十分でない場合は、いくつかのリソースが開放されるまで待機させることが出来ます。
リソースの公平性
マルチテナント環境で、多くのユーザーがクラスタリソースを共有します。テナントがリソースを競合させ、飢餓状態(starving)を避けるために、アプリケーションに割り当てるリソースの重みや優先度を設定出来るようになっています。
リソース予約
YuniKornは未解決のリクエストに対して自動的に予約します。Podが割り当てられなかった場合、YuniKornは適切なNodeにPodを予約しようとして、(他のNodeを試す前に)予約されたNodeにPodを仮で割り当てます。
この仕組みにより、後からSubmitされたより小さくてこだわりのないPodによってこのPodが飢餓状態になる事を回避出来ます。
高いスループット
スループットは、スケジューラの性能を測る重要な基準です。スループットが悪いとアプリケーションはスケジュール待ちで時間を浪費し、サービスのSLAに影響します。クラスタが大きくなるほど高いスループットが要求されます。
Kubemarkを用いて、YuniKornとKubernetesのデフォルトのスケジューラに比べた結果、YuniKornが2倍から4倍のスループットを出したようです。
詳細は以下を参照ください。
YuniKornのアーキテクチャ
YuniKornは、カスタムKubernetesスケジューラとしてデプロイ出来るようになっています。
これは異なるshim層を追加して、Apache Hadoop YARNやその他のシステムを含む異なるリソースマネージャの実装に対応する事が可能になっています。
しかし、独自実装を盛り込んだ結果、Kubernetesのアップデートに追従するのが大変でしたが、YuniKorn v1.0.0からKubernetes v1.19以降に追加されたScheduling Frameworkで導入された拡張ポイント(PreFilter、Filter、PostBindの3つ)を実装したプラグインとして利用できるようになりました8。
下図は、YuniKornのScheduler Pluginを利用しない場合のハイレベルなアーキテクチャです。
出典: Architecture | Apache YuniKorn より
コンポーネント
Scheduler Core
※図中のYuniKorn Coreの事
Scheduler Coreは、すべてのスケジューリングアルゴリズムをカプセル化して、下位のリソース管理プラットフォーム(YARN/Kubernetesなど)からリソースを収集し、コンテナの割り当て要求を担当します。
各リクエストに対して最適な場所はどこかを判断し、リソース管理プラットフォームにレスポンスの割り当てを下位のプラットフォームに依存しないようにすべての通信はScheduler Interfaceを利用します。
詳しくはScheduler Coreの設計を参照ください。
Scheduler Interface
※図中のYuniKorn Scheduler Interfaceの事
リソース管理プラットフォーム(YARN/Kubernetesなど)がgRPCやプログラミング言語バインディングのようなAPIを介して会話する抽象レイヤ
Kubernetes Shim
※図中のYuniKorn K8s-Shimの事
Kubernetes ShimはKubernetesとの対話を担当し、Kubernetesのクラスタリソースや、Scheduler Interface経由のリソースリクエストをScheduler Coreに送ります。 そしてスケジューラが決定すると、Podを特定のNodeに結合する役割を担っています。ShimとScheduler Core間の通信はすべてScheduler Interfaceを介して行われます。
詳しくは、Kubernetes Shimの設計を参照ください。
対応しているワークロードについて
YuniKornが対応しているワークロードは以下の通り
最新の状況は?
10月にあったApache YuniKorn Meetupがあったようでその録画とスライドがありました(ありがたや)。
内容はざっくり以下の通り。
次のバージョン予定されている主な機能
来年1月にリリース予定の v1.2ではで予定しているのは以下だそうです。
- Kubernetes v1.21以降をサポート
- 1キューあたりの最大実行アプリの制限機能の追加
- ユーザーやグループでのクォータの機能について
- 開始ユーザ検索や利用状況の把握するための実装を開始
- 設計が最終段階
- 詳細は デザインドキュメント を参照ください。
今後の方向性について
また、今後の方向性として以下を挙げてました。
- 優先度のサポートについては、最終デザインがペンディング
- Preemptionのサポート
- 一旦、YuniKorn v1.1の古いコードは削除してデザインからやり直し
- フェデレーション
- Kubernetesの制限で1クラスタあたりのノード数に上限あるので、クラスタ跨ぎでクォータ管理を出来るようにするとの事
どうやって使うのか?
ボリュームでかくなったので分けます😇
ひとまず以下が参考になります。
さいごに
いかがだったでしょうか。
KubernetesでBigDataやMLでのワークロードの実行で今後も要チェックなYuniKornの紹介でした。
概要な話が多かったのでより詳細な話は別途、改めてまとめてみます。
以上、 Distributed computing Advent Calendar 2022 の5日目の記事でした。
補足:参考情報
- 公式情報
- ClouderaのBlog
- 過去のイベント(MeetupやApacheCon、KubeConなど)のスライドやセッション動画
- 実は、Distributed computing Advent Calendar 2021 の20日目に書くはずだったネタ…。↩
- The Apache Software Foundation Announces Apache® YuniKorn™ as a Top-Level Project - The Apache Software Foundation Blog↩
-
Hadoopについては以下を参考にしてください。
- Hadoopはどのように動くのか ─並列・分散システム技術から読み解くHadoop処理系の設計と実装:連載|gihyo.jp
- 分散処理技術「Hadoop」とは:NTTデータのHadoopソリューション↩ - 詳しくはこちら。 YuniKorn: a universal resources scheduler - Cloudera Blog↩
-
Running Spark on Kubernetes | Apache Spark
Native Kubernetes | Apache Flink↩ -
ワーキンググループのMTGはYoutubeで公開されています。
https://www.youtube.com/playlist?list=PL69nYSiGNLP05eEikq0j8PcSehEdM4mj7&jct=s4irZjLuvQ2WrUYAGxyRYjs3a2aysg↩ -
スライドはこちら
面白かったのは、500ノードクラスタ、24K Podを同時実行した際に、Spark Operatorが1つではOperatorがボトルネックになったので5つにしたそうです。Operatorが詰まるとかあるのか。規模が違う…。↩ - 詳しくは K8s Scheduler Plugin | Apache YuniKorn を参照。↩
Production-ReadyなK8s(RKE2)クラスタを構築する際のあれこれ
この記事は MicroAd Advent Calendar 2022 の4日目の記事です。
- 前提
- ここで触れないこと
- RKE2って?
- Kubernetesのコンポーネント
- RKE2のアーキテクチャ
- RKE2の制限事項・必要条件
- RKE2 ServerとAgent Nodeの構成をどうするか
- RKE2の設定ポイント
- HAクラスタの構築の流れ
- Tips1:LBの構築ポイント
- Tips2:1台目の設定のserverをコメントアウトしてrke2-serverサービスを再起動する理由
- RKE2(Kubernetes)のアップグレード
- さいごに
- おまけ
- Tips: Rancher CLI
- Tips2: TerraformのRancher2 Provider
本番でUnmanagedなKubernetesクラスタを構築しようとした際にどうやって構築するか悩みますよね。
今回はKubernetesのディストリビューションのRKE2を用いて書いていきます。
AWXサーバの外からansibleでジョブを実行する
この記事は MicroAd Advent Calendar 2021 の20日目 と Ansible Advent Calendar 2021 - Adventarの 7日目の記事です(空いててもったいないので埋めました)。
はじめに
以下の記事の補足になります。
yassan.hatenablog.jp
yassan.hatenablog.jp
PRをベースとしたダッシュボード運用をしていると任意のタイミングでリポジトリの状態とダッシュボードを同期したい場合が出てきます。
例えば以下。
- CIジョブが失敗して再実行したい
- 本番ダッシュボードを削除・壊して復元できない
上記などの状況からどうしても同期取りたい場合が出てきます。
その場合、空コミットを追加しPR作成することで同期できますが、コミットログも汚れてしまいます。
また、ローカルからansible-playbookコマンドを実行する場合、Vault変数を復号するために復号用パスワードが必要になります。
その場合、Grafana運用者以外のユーザにも復号用パスワードを公開することになり具合が悪いです。さらに、ローカルから実行してしまうと「いつ・誰が・何を」と言った大事な実行履歴も残らないので困りものです。
そこで、AWXのジョブテンプレートを用意して、ユーザ及びCIサーバからの実行をAWX経由で実行するようにしてみます。
環境
前提となる環境は以下の通りです。
- ansible-core v2.11(v2.9でも動きます)
- AWX v13.0.0 (多分、v13以降でも大丈夫)
- Jenkins(CircleCIやGitHub ActionsでもOK)
概要
今回は以下のAnsible Collectionの awx.awxを使います。
こちらを利用することで、CIサーバからAWXのジョブを実行出来るようになります。 また、AWXにジョブを作成する事でAWXのWebUIからユーザーが任意のタイミングで実行も可能になります。
前準備
AWXのCollectionを利用できるようにするためのセットアップを行います。
まず、 collections/requirements.yml
に以下のようにして追加。
--- collections: - name: awx.awx version: "13.0.0" # 利用しているAWXに合わせて指定
上記のversionは、 galaxyの awxのページ の Install Version
にあるドロップダウンリストのVersionを指定してください。
また、指定するVersionは、利用しているAWXのVersionに合わせて近いものを使うのが良いです(互換性の問題を回避する目的で)。
collections/requirements.yml
に定義するのは、AWXがデフォルトで参照するCollectionのパスとなっています1。
また、ローカルで実行したい場合は以下のコマンドでinstall出来ます2。
# ansible-core v2.11以降 ansible-galaxy collection install -r collections/requirements.yml
Playbookの作成
awx_launch_job.yml
を以下の様に作成します。
--- - hosts: "{{ grafana_group }}" gather_facts: false connection: local tasks: - name: ジョブの起動 awx.awx.tower_job_launch: # v19の場合は tower_job_launch→job_launch tower_host: "{{ awx_host }}" tower_username: "{{ awx_user }}" tower_oauthtoken: "{{ vault_awx.awx_oauth_token }}" name: "{{ job_name }}" extra_vars: revision: "{{ revision }}" grafana_group: "{{ grafana_group }}" register: job - name: "Wait for job max {{ job_timeout }}s" awx.awx.tower_job_wait: # v19の場合は tower_job_wait→job_wait tower_host: "{{ awx_host }}" tower_username: "{{ awx_user }}" tower_oauthtoken: "{{ vault_awx.awx_oauth_token }}" job_id: "{{ job.id }}" timeout: "{{ job_timeout }}"
モジュール awx.awx.tower_job_launch
を使うことでAWXのジョブを実行出来ます3。
また、 awx.awx.tower_job_wait
を使って待っているのは、CIジョブを以下の順番で実行するので、各ジョブが終わるのを待ち合わせするために設定しています4。
import-datasources
import-notification-channels
import-dashboards
どちらのモジュールでも利用している tower_oauthtoken
に指定する値は、事前にVaultの暗号化変数として{{ vault_awx.awx_oauth_token }}
に格納します5。
また、実行する際は、Makefileに以下を定義して実行を簡略化します。
SHELL=/bin/bash REVISION := $(shell git rev-parse --short HEAD) # Grafanaにダッシュボードを反映する # AWX job: grafana-import-dashboard を実行する # 例: make import-dashboards group=prod .PHONY: import-dashboards import-dashboards: ansible-playbook -i inventories/inventory \ --vault-id .vault_password \ awx_launch_job.yml \ --extra-vars role_name=import_dashboard \ --extra-vars job_name=grafana-import-dashboard \ --extra-vars grafana_group=$(group) \ --extra-vars revision=$(REVISION) \ --extra-vars job_timeout=600;
※他にも role_name
や job_name
を変更して他のロール分(import-datasources
やimport-notification-channels
)も定義します。
AWX側の設定
以下のようにしてAWXからジョブテンプレートを作成します。
また以下は import_dashboard
の分ですが、別途、他のロール分(import-datasources
やimport-notification-channels
)も作成します。
注意点としては、上図の右下隅にある「起動プロンプト」にチェックを入れないと実行出来ないので忘れずにチェックしてください。
また、手動で実行したい場合は、追加変数に grafana_group
と revision
を追加して実行してやればOK。
CI側のフローの設定
特に前回から変更は無いです。Makefileでダッシュボードやデータソース、通知チャンネルのimportを実行するMakeターゲットをAWXジョブからの実行に変更しているので特にここでは変更点がありません。
詳細は前回の記事を参照して下さい。
最後に
これでPRを使わずに実行履歴を残しつつダッシュボードの更新が出来るようになりました。
とは言え、PRベースに更新出来ないのでレビューが出来ず、また、更新意図を残せないのであくまで緊急事態用というところです。
全体を通した今後の改善点
現状、Grafanaのダッシュボード・データソース・通知チャンネルを毎回すべて更新してしまいます。 理想的には更新を検知して、更新ファイルだけ反映するような対応があるとより早く同期できたりします。 ただ、git diffで更新差分を使うアイディアもあるのですが差分をキレイに出すのが難しいです6。
他にも、今回の仕組みではダッシュボードを削除する仕組みが無いので、リポジトリから削除して後、GrafanaのWebUIから手動で削除する必要があるので若干面倒です。
改善ポイントについて良案があれば是非教えて下さい!
-
詳細は 14. Projects — Ansible Tower User Guide v3.8.5 を参照。
また、 GitHubのAWXのdoc にも説明があるので参考になります。↩ -
ansible-core v2.11未満の場合は、
install
オプションに-f
がないと2回目以降でエラーになります。
ただし、 ansible/ansible#65699 にて検討されて、ansible/ansible#73336で、v2.11より-u
オプションが追加されています。↩ -
パラメータの詳細は awx.awx.job_wait を参照下さい。↩
-
パラメータの詳細は awx.awx.job_launch を参照下さい。↩
-
この辺の説明は前回の記事を参照して下さい。
yassan.hatenablog.jp↩ -
mainブランチとの差分を使うと後から作成したPRを先にマージすると差分を取れなくなる問題があります(rebaseしたら治りますけど)。↩
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 を叩いていることがわかります。↩
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↩