TiDBをHadoop管理者視点でデータ基盤としての使い所を考えてみる

この記事は MicroAd Advent Calendar 2022Distributed computing (Apache Spark, Hadoop, Kafka, ...) Advent Calendar 2022 の25日目の記事です。

今年のアドカレも最終日になりました。
とは言え、Distributed computing Advent Calendar 2022 の方はエントリが結構残ってるので、引き続き空いてる枠への参加をお待ちしてます!

qiita.com

qiita.com

今回は、Hadoopクラスタ管理者の視点で、データ基盤としてTiDBの使い所について考えてみます。 また、データ基盤の規模感としては、ペタバイトレベルのクラスタ、1テーブルあたり最大100TBクラスを想定しています。

TiDBとは

TiDBをすーぱーざっくりに説明すると、NewSQLに分類されるペタバイトクラスまで水平スケール出来るらしいMySQL互換の分散SQLデータベース。SQLを解釈するTiDBとデータの永続化を担うTiKVを使って実現しています。また分析用途にも対応出来るように、TiDBからデータを列指向で同期して利用するTiFlushもあります。

今回は、TiDB自体については割愛します。
詳しくは、以下のPingCAPのドキュメントを参照ください。

もしくは、Cloud Native Database Meetup #51 での発表がひとまず一通り知るにはとても参考になります。

speakerdeck.com

youtu.be

他にもPingCAP社による以下の無料トレーニングのアーカイブを読むとさらに深掘りできます。

公式のドキュメントはこっち。

また、その他の動画やユーザ事例については、PingCAP社のYouTubeチャンネルが参考になります。

www.youtube.com

他にもDeNA社とDMM社のTiDBの検証記事も深掘りしていて、とても参考になりました。

engineering.dena.com

inside.dmm.com

前振りはこの辺にして、本題に入っていきます。

データ基盤として見たときにTiDBに求めるもの

TiDB(TiFlush)の売りとして、ペタバイトクラスまで扱え列指向で分析にも使えるという事で分析系クラスタとして扱えたりしないかなと考えました。
正直なところ、役割が違うので全ての要件を満たすとは考えてないのですが、考えられる要件は以下の通り。

  1. 構築する場合の運用しやすさ
  2. データの可用性について
  3. データベースやテーブルサイズに対する上限
  4. テーブルで利用できるデータ型について
  5. スキーマ進化出来るか(テーブル定義やパーティションの変更しやすさ)

以降で、1つずつ掘り下げていきます。

構築する場合の運用しやすさ

TiUPコマンドがとにかく便利

TiDBはTiUPという管理用のCLIが用意されています。

docs.pingcap.com

これが非常によく出来ています。

雰囲気は、 SUSE RancherのKubernetesクラスタを構築するCLI RKE と同じ感じで、クラスタの構成情報や設定を記載したYAML(topology.yaml)をクラスタ外の管理用ホストからtiupコマンドを実行するとクラスタが構築出来ます(ちゃんとコマンド補完も出来る2)。

利用の際にはSSH鍵でのパスワードなしログイン出来るようにしておくと楽です3。 また、sshpass が必要っぽく書いてたり、ドキュメントにinstanceがどうの書いていますが、ベアメタルサーバーでも構築出来ます。

また、構築前にクラスタとして必要な設定が実行済みかをチェックし、適用までやってくれます。

ざっくり、topology.yamlを作成後、以下の様にするとTiUPクラスタが利用できるようになります。

# サーバの設定を確認(ubuntuユーザでSSH鍵ログイン出来るようにしています)
$ tiup cluster check ./topology.yaml --user ubuntu

# 推奨設定を反映
$ tiup cluster check ./topology.yaml --apply --user ubuntu

# TiDB v6.4.0 でデプロイ
$ tiup cluster deploy tidb-test v6.4.0 ./topology.yaml --user ubuntu

# データベース tidb-test を初期化
$ tiup cluster start tidb-test --init

# TiDBの構成情報を確認
$ tiup cluster display tidb-test

# TiDBのIP(198.51.100.4)を指定してDBに接続
$ mysql -u root -h 198.51.100.4 -P 4000 -p

詳しくは以下を参照ください。

docs.pingcap.com

topology.yamlの設定については以下が参考になります。

docs.pingcap.com

以下のTiUPコマンドを見てもらうと分かりますが、クラスタの運用で必要になる様々な事がTiUPで完結します。

TiUP Cluster | PingCAP Docs

負荷テストやベンチ、Sandbox環境も簡単。TiUPならね。

さらにすごいのは、負荷テストにも使えるベンチマーク機能もあります。以下のようにするだけでTPC-Cベンチマーク取れます4TPC-Hも出来るよ)。

$ tiup install bench
$ tiup bench tpcc -H 198.51.100.4 -P 4000 -D tpcc --warehouses 1000 --threads 20 prepare

他にも、新しいバージョンでの動作確認や開発時のローカル端末での利用の際にシュッとお試し環境を構築できるtiup playgroundが便利。

docs.pingcap.com

利用者の事を分かってるなぁと思います。

KubernetesでのTiDB構築について

TiDBはKubernetesOperatorもあるのでKubernetesでの構築も出来ます。

ただ、個人的には頻繁に構成を変えたいとかないのなら、ある程度の規模のKubernetes運用経験や環境が無いならTiUPでの運用が安全で楽だと考えます。特にストレージ層となるTiKVのPodに割り当てるVolumeは、レプリカ分もあるので最低でも論理サイズ5の3倍必要になり大量に必要ですので運用がメンドイです(Kubernetesで利用するストレージは高価になりがちなので費用も辛くなりそう)。

また、データベースとして要求されがちなパフォーマンス面もベアメタルサーバーやVMインスタンスのDisk直読みの方が速いです。

これだけ運用しやすいTiUPコマンドあるので、個人的にはKubernetesでのTiDB運用は規模がでかくなればなるほどメリットを感じません。

データの可用性について

TiKV は、Google Spanner の設計に基づいたマルチラフト グループ レプリカ メカニズムを取っているそうです。

データ自体は、リージョンという単位で格納し、複数のレプリカでRaftグループを形成しています。また、レプリカはピアと呼ばれ、通常3つで構成します。そして、ノード内のデータは、RocksDBを使って格納しています。

詳細は以下のドキュメントまたは無料トレーニングのアーカイブを参照ください。

docs.pingcap.com

pingcap.co.jp

また、RocksDB自体はRocksDBのWikiを参照ください。

github.com

データベースやテーブルサイズに対する上限

確認したい以下の項目をTiDB Limitations から確認していきます。

  1. 1テーブルあたり論理サイズで100TBクラスを扱えるか
  2. 1テーブルあたる数十億レコードクラスを扱えるのか
  3. 1万パーティションくらいは作成出来るか

テーブルサイズや行数について

無制限となっていました。
ただし、以下の通り、TiKVの1ノードあたりのDiskの上限が2TB未満ということでした。

docs.pingcap.com

It is recommended to keep the size of TiKV hard disk within 2 TB if you are using PCIe SSDs or within 1.5 TB if you are using regular SSDs.

これは結構問題です。 TiKVの1ノードあたりDiskが2TB未満ということは、レプリカを考慮するとデフォルトでピアは3つになるので、1ノードあたりの論理サイズは2TBの3分の1(0.67TB)になってしまいます。

つまり、TiDBクラスタとしてペタバイト並の論理サイズのクラスタを構築するには、TiKVだけで1,000台以上必要になってしまいます。

この1ノードあたりの上限にいては、TiKVのアーキテクチャからくるもののようです。つまり、TiKVノードの管理するリージョン数が多くなりすぎるとリージョンごとのステータス管理のためのPDとのハートビートなどに問題が発生する可能性があるので2TB未満としていると、TiDBコミュニティのSlackで教えていただきました。

また、将来的にはこの制限を緩和出来るように、Dynamic size regionといった機能をTiKVに実装して、リージョンサイズを大きくすることで同じデータ量に対するTiKVノードあたりのリージョン数を減らすことで1ノードあたりのDiskサイズの制限を緩やかにする事が可能になっていくそうです。

Dynamic size regionについては、以下のTiKVのRFCを参照ください。

github.com

パーティションについて

1テーブルあたり8192までだそうです。
hourlyでパーティションを考えた場合は、341日で1年弱なので許容出来そうですが、ただ、特定キー×hourylyでパーティションした場合では不足しそう。Hiveテーブルだと1万パーティションとか普通にあるので、もう少し増えて欲しいところです。

テーブルのデータ型やデータ操作について

TiDBで扱えるデータ型は以下のドキュメントの通り。

docs.pingcap.com

MySQL互換というだけあって、この互換性はすごいですね。

ただ、HiveテーブルにあるComplex(arrray/map/struct)型6はもちろん無いのでそこがネックです。 TiDBにはJSON型があるのでそれで代用出来そうですが、変換コストが大きいです。

以下のMySQLの互換性のドキュメントを確認して分かったのは、UDFが未対応という点はつらいです。

docs.pingcap.com

Complex型をJSON型に構造を変更するとしても、UDFが無いとETLの処理や参照時に困ります。 MySQLで出来る分としてJSON Functionsがあるのである程度の操作はやりやすいですが、UDFが無いのは辛いところ。

スキーマ進化出来るか(テーブル定義やパーティションの変更しやすさ)

MySQL互換ということで、Hiveテーブルの様なスキーマ進化は出来ない。これが一番つらい。

HiveなどHadoopエコシステムで扱うSQLエンジンは、Schema-On-Read を前提としています。つまり、データの読み取り時にスキーマを適用することを意味します。それによりスキーマの変更時にテーブルデータを再投入する必要がありません。

例えば、Hiveテーブルの場合、以下のドキュメントを見ると分かりますが、テーブル名の変更、カラムの追加・リネーム、パーティション操作などのSQL構文が用意されています。

cwiki.apache.org

さらに今となっては、Data WarehouseやDaka Lakeのいいとこ取りをしたData Lakehouseなんてものも登場して昔のHiveしか知らない人がみたら驚く進化をしているので運用負荷がさらに下がっています7

ここまでスキーマ進化にこだわるのは、データ基盤で扱うデータサイズが1テーブルで数TBはあたりまえで、大きいものだと100TB以上になるのも珍しくありません。また、利用する過程でカラムの追加やカラム名の変更やカラム位置の移動8が割りと発生します。そのため、データの洗替えが毎回発生し利用するまで数時間・数日掛かってしまってはビジネスインパクトが無視できません。

使い所を考える

Data LakeやData Warehouseとしては考えるのは無理がありました(そもそも欲しがり過ぎ)。

ただ、MySQLを水平スケールする点に着目した場合、使いどころがありました。Hiveを運用する際、テーブルのメタデータをHive Metasore(以降、HMS)を利用するのですが、HMSのデータはMySQLに格納します。

ここで問題になるのは、HDFSファイル数が多くなり、パーション数がデカくなると、パーティションをガツッと削除するとメタデータの更新でMySQLが詰まる問題があります。そこで、このMySQLをTiDBで置き換えられないか?と言う点です。

探してみたらHiveのWikiに記載がありました。みんな考えることは同じですね。

cwiki.apache.org

PingCAPのユースケースの紹介としても記事がありました。

www.pingcap.com

どうやらMySQLの代わりとし使えそうです。Hive2系までなら本番環境でも確認していて、3系でもテストはしているそうです。

もう1点、BIとしてRedash使ってる場合、QueryResultは便利なのですが、RedashのデータベースにJSON型で格納するのでQueryResultを多用するとデータベース側のDiskを圧迫するので考えものです。

ただ、RedashのデータベースはPostgreSQLしかサポートしないようなので諦め。残念。

discuss.redash.io

一方でPingCAPのBigData向けの取り組みは、TiFlash以外にもありました。

1つ目がTiSparkです。

docs.pingcap.com

SparkSQLの実行をTiKV・PDと連携して実行するものらしいです。TiFlashも合わせて利用することも出来るらしい。
トランザクションのACID特性が保証され、DataFrameも使えて複数テーブルへの書き込みも出来る。また、TiSparkはTPC-HのLINEITEMデータ6000万行を8分以内に書き込むことができるそうです。

詳しくは以下のPingCAPのブログを参照ください。

www.pingcap.com

2つ目はFlink/Hive/Trino・Prestoとの連携するTiBigDataです。

github.com

想定しているユースケースや事例などは以下のPingCAPのブログ記事がとてもおもしろいです。 例えば、Xiaohongshu社のユースケースが面白いです。TiCDCクラスタでTiDBのリアルタイムな変更データを抽出し、変更ログをKafkaに送信します。 それをFlinkはKafkaから変更ログを読み込み、集計・結合などを実行します。そして、Flinkはその結果をTiDBのワイドテーブルに書き込んで分析に利用するそうです。

www.pingcap.com

さいごに

現状のところはデータ基盤としてのTiDBの利用は限定的になりそうですが、TiSparkやTiBigdataなど、データ基盤の一部で利用できそうなものが出てきていて、今後が非常に楽しみです。また、本来の用途であるデータベースとして見た場合、MySQLとの互換性(やTiDB特有の制限事項)を受け入れ可能なら水平スケール可能なMySQLとしてとても素晴らしいプロダクトだと考えます。

また、TiDBはプロダクトだけでなくドキュメントの充実ぶりも素晴らしいのですが、動画コンテンツもしっかり用意していて導入しやすそうです。

更に、PingCAP社はコミュニティへも積極的に関わっていて、Slack でも積極的に情報発信をしています。そして、日本語チャンネル #tidb-japan もあるのでとてもありがたいです。

引き続き、TiDBについてはウォッチしていくので、なにかの機会にまた記事を書こうかと思います。

以上、MicroAd Advent Calendar 2022Distributed computing (Apache Spark, Hadoop, Kafka, ...) Advent Calendar 2022 の25日目の記事でした。


  1. Cloud Native Database Meetup #5 情報
    イベントページ / アーカイブ動画
  2. tiup completion | PingCAP Docs
  3. パスワードなしで SSH 相互信頼と sudo を手動で構成する
  4. 詳しくはこちら。Stress Test TiDB Using TiUP Bench Component | PingCAP Docs
  5. 論理サイズとはレプリカ数を考慮して実施にシステムとして利用できるサイズとします。
    反対に物理サイズは実際に必要になるDiskのサイズとします。
  6. Hiveで利用できる型。具体的には以下の3つ。
    arrays: ARRAY<data_type>
    maps: MAP<primitive_type, data_type>
    structs: STRUCT<col_name : data_type [COMMENT col_comment], ...>
    union: UNIONTYPE<data_type, data_type, ...>
    cf. LanguageManual Types - Apache Hive - Apache Software Foundation
  7. Data Lakehouseとして代表的なのは、DeltaLakeIceberg があります。また、Data Lakehouseの登場の背景については、Databricksの記事が分かりやすいです。
    データウェアハウスでよくある問題をデータレイクスで解決する方法 - The Databricks Blog
  8. データ基盤のテーブルのカラム数は数百となるので、分かりやすい位置にカラムを移動したいと言う要望は割りとあります。

BigData向けワークロード(Spark/Flink)に適したKubernetesカスタムスケジューラ Apache YuniKorn について

この記事は Distributed computing Advent Calendar 2022 の5日目の記事です1

qiita.com

今回は、今年の5月にApache Software Foundation (ASF)のTop-Level Project (TLP)2になった Apache YuniKorn について紹介します。

yunikorn.apache.org

YuniKornの概要

YuniKornは、YARNで要求されていたワークロードに関するリソーススケジューリングをKubernetesで利用できるよう作成したようです。

YARN(Yet Another Resource Negotiator)とは、Hadoopクラスタ内のジョブのスケジュールやリソース管理する基盤です。

YARN自体に馴染みのない方は以下を見るとYuniKornも理解しやすいです。

YARNの概要とFAIRスケジューラについてはこちら。

www.slideshare.net

Capacityスケジューラについてはこちら。

blog.cloudera.com

また、Hadoop3エコシステムはだいたいJava(一部C++など)で実装されていますが、YuniKornはgolangで実装されています。

YuniKornの由来と読み方

YuniKornの名前の由来は "Y"をYARN(フロントエンドのyarnじゃないよ)、"K"をKubernetes、"Uni"をUnifiedから取っているそうです。 また、発音は[‘ju:nikɔ:n]となり、ロゴにもなっているUnicornと同じです4

なぜYuniKornが必要なのか?

BigDataやMLでのワークロードは、大量のデータを処理しなければならないので、レイテンシよりもスループットが非常に大事です。

また、この手のワークロードはマルチテナントで利用されることが多く、リソーススケジューリングにする要求は複雑になりがちです。

例えば、以下のような事が普通に求められ、これまで、YARNにて対応してきました(YARNスゴイヨ)。

  1. 大事なバッチにはクラスタの半分のリソースを優先して割り当てる
  2. 部門Aのアドホックなワークロードにはクラスタリソースの10%までを割り当てる
    1. ただし、バッチなど優先したいワークロードがなくヒマしているなら、10%を超えて使ってもOK
    2. とはいえ、メインのバッチが始まったらワークロードをプリエンプティブする
    3. さらに、部門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、AppleLyft、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でモニタリング出来ます(クラスタリソースの容量、利用率、およびすべてのアプリケーション情報を一元的に表示出来ます)。

YuniKorn UI
出典: Get Started | Apache YuniKorn より

ジョブオーダーとキューイング

アプリは作業キューに格納して、どのアプリが最初にリソースを取得できるのかは、以下のポリシーに応じて決定します。

現状設定可能なポリシーは、以下の3つ。

  • FIFO : そのまま。先入れ先出し
  • Fair : アプリケーションの使用量に応じて公平にリソースを配分。
  • StateAware : 実行中または受け入れ状態のアプリは1つに制限される。

StateAware が分かりにくいので補足します。
Sparkアプリを例にとると想像しやすいです。Sparkアプリは、アプリ全体の調整役となるDriverと、Driverが割り当てた細かいタスクを実行する複数のExecuterに分かれて実行していきます。

例えば、あるキュー(app-queue-1)にapp1→app2の順にSparkアプリをsubmitしたときは以下の様に実行されます。

StateAwareの説明

さらに、キューの最大容量を設定した場合、ジョブやタスクはリソースキューに適切にキューイングされて、残りの容量が十分でない場合は、いくつかのリソースが開放されるまで待機させることが出来ます。

リソースの公平性

マルチテナント環境で、多くのユーザーがクラスタリソースを共有します。テナントがリソースを競合させ、飢餓状態(starving)を避けるために、アプリケーションに割り当てるリソースの重みや優先度を設定出来るようになっています。

リソース予約

YuniKornは未解決のリクエストに対して自動的に予約します。Podが割り当てられなかった場合、YuniKornは適切なNodeにPodを予約しようとして、(他のNodeを試す前に)予約されたNodeにPodを仮で割り当てます。

この仕組みにより、後からSubmitされたより小さくてこだわりのないPodによってこのPodが飢餓状態になる事を回避出来ます。

高いスループット

スループットは、スケジューラの性能を測る重要な基準です。スループットが悪いとアプリケーションはスケジュール待ちで時間を浪費し、サービスのSLAに影響します。クラスタが大きくなるほど高いスループットが要求されます。

Kubemarkを用いて、YuniKornとKubernetesのデフォルトのスケジューラに比べた結果、YuniKornが2倍から4倍のスループットを出したようです。

詳細は以下を参照ください。

yunikorn.apache.org

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を利用しない場合のハイレベルなアーキテクチャです。

YuniKornのアーキテクチャ
出典: 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があったようでその録画とスライドがありました(ありがたや)。

youtu.be

内容はざっくり以下の通り。

次のバージョン予定されている主な機能

来年1月にリリース予定の v1.2ではで予定しているのは以下だそうです。

  • Kubernetes v1.21以降をサポート
  • 1キューあたりの最大実行アプリの制限機能の追加
  • ユーザーやグループでのクォータの機能について
    • 開始ユーザ検索や利用状況の把握するための実装を開始
    • 設計が最終段階

今後の方向性について

また、今後の方向性として以下を挙げてました。

  • 優先度のサポートについては、最終デザインがペンディング
  • Preemptionのサポート
    • 一旦、YuniKorn v1.1の古いコードは削除してデザインからやり直し
  • フェデレーション

どうやって使うのか?

ボリュームでかくなったので分けます😇

ひとまず以下が参考になります。

さいごに

いかがだったでしょうか。
KubernetesでBigDataやMLでのワークロードの実行で今後も要チェックなYuniKornの紹介でした。
概要な話が多かったのでより詳細な話は別途、改めてまとめてみます。

以上、 Distributed computing Advent Calendar 2022 の5日目の記事でした。

補足:参考情報

Production-ReadyなK8s(RKE2)クラスタを構築する際のあれこれ

この記事は MicroAd Advent Calendar 2022 の4日目の記事です。

qiita.com

本番でUnmanagedなKubernetesクラスタを構築しようとした際にどうやって構築するか悩みますよね。
今回はKubernetesディストリビューションのRKE2を用いて書いていきます。

前提

  • OS:Ubuntu 20.04 (RH系も多分大丈夫)
  • Arch:amd64
  • Kubernetes distribution: RKE2
  • Ranhcer: v2.6
  • CNI: Cilium(L2・VXLAN)
  • 用途: Rancher Serverのinstall先、または、Rancherで管理しないKubernetesクラスタ

ここで触れないこと

CNIやPV、Secretなどは、ボリュームがデカいのでまたの機会に。

RKE2って?

docs.rke2.io

RKE2は、米国連邦政府のセキュリティとコンプライアンスに重点を置き、完全準拠しているKubernetesディストリビューションだそうです(なんか強そう)。

そのため、RKE2では以下を行っているそうです。

また、Rancher Labsの血を引いているのでK3sの使いやすさやシンプルさを兼ね備えています(ただし機能はK3sとはことなり何かを引いているとかはないです)。

アーキテクチャについては以下を参照ください。

docs.rke2.io

RancherのWeb UIからRKE2 Nodeをinstall可能ですが、K3sの様に簡単にinstall出来ます。
Server Node(≒control plane)をセットアップする場合、 /etc/rancher/rke2/config.yaml に設定ファイルを置いた上で、以下を実行したら立ち上がってきます。

# curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION="v1.XX.X+rke2rX" INSTALL_RKE2_TYPE="server" sh -
# systemctl enable --now rke2-server.service

IaCし易い構成になっていて、以下の様にansibleのリポジトリもあります。

github.com

また、Kubernetesクラスタ構築の際に、インターネット経由でコンテナイメージを各サーバから取得出来ない場合は、Air-Gap向けのinstallが用意されています。

docs.rke2.io

Kubernetesコンポーネント

Kubernetesクラスタを構成する場合、大きく「Control Plane」と「Node」の役割があります。
アプリはNode上でPodを管理し、Control Planeは、クラスタ内のNodeとPodを管理します。

kubernetes.io

RKE2のアーキテクチャ

RKE2のアーキテクチャは以下が参考になります。
基本的には一般的なKubernetesと変わりないです。

docs.rke2.io

RKE2特有の用語として、「Server Node」と「Agent Node」があります。
Kubernetesコンポーネントと照らして考えた場合、
ざっくりと「Server Node」=「Control Plane」、「Agent Node」=「Node」として考えたらOKです。 (正確には「Server Node」にはcontrol planeとetcdが含まれます)

ただ、RKE2をinstallする際に INSTALL_RKE2_TYPE="server" として構築した「Server Node」は、Kubernetesコンポーネントでいうところの「Control Plane」だけでなく「Node」としての役割としても機能出来ます。 (利用者のワークロードを実行したくない場合は、install設定でnode-taintに設定が必要です1

つまり、1台だけでもKubernetesとして機能します。

RKE2の制限事項・必要条件

ハマりそうなのは以下の2つ。

  • firewalldを無効するRKE2と言うよりはCNIの制限)
  • NetworkManagerを使っている場合は、NetworkManager が calico/flannel 関連のネットワークインターフェイスを無視するよう設定が必要
  • ホスト名はユニークにしなければいけない

他にもあるので詳細は以下を参照ください。

docs.rke2.io

docs.rke2.io

次から本番クラスタについて考えていきます。

RKE2 ServerとAgent Nodeの構成をどうするか

ざっくり考えられそうなパターンは以下。

No Server Node Agent Node 備考
1 1 0 可用性0のお試し環境
2 3 0 最小のHA構成。HAなRancher Management Serverの最小構成
Downstream User Clusterとしてユーザ利用する場合はワークロードがI/OヘビーだとKubernetes API(etcd)に影響するのでおすすめ出来ない。
3 3 1以上 安パイなHA構成。
★:No.2と違いServer Nodeにユーザのワークロードを実行しないようにNode Taintを付ける。
4 5 1以上 一番安全な構成。Server Nodeをcontrol plane×2、etcd×3として構成する。
★:No.3と同様にServer NodeにNode Taintを付ける。

RKE2にてcontrol planeとetcdを分けて構築するには

以下のようにして、etcd・control planeロールのみになる設定するとOK。

www.suse.com

RancherからCreateしたクラスタと構築済みのクラスタを登録する場合の違い

「(Web UIやCLI使って)Rancherからで作成する場合」と「構築済みのクラスタをRancherに登録(Import)する場合」では出来る操作が変わります。基本的には登録したクラスタについてはRancherを使わずに管理することになります。詳細は以下のドキュメントあたりが参考になります。

出来ることが増えるので、Rancherを使ってクラスタを管理するなら特別な理由が無い限り、Downstream User ClusterとしてRKE2を使う場合には、基本的にはRancherからクラスタを作成した方が良いです。

RKE2の設定ポイント

RKE2の設定は以下にまとまってます

docs.rke2.io

設定ポイント:RKE2で利用するデータ領域を別Diskに指定する

オンプレ利用時の特有な考慮事項。
OSと同じDiskをRKE2でも使った場合、RKE2関連の利用によるH/W障害がOSに影響しないようにするためにDisk分けるという選択もありです。

--data-dir (default: "/var/lib/rancher/rke2") を使って、別Diskを指定することも出来ます。

設定ポイント: tokenは指定する方が楽 ※超重要※

tokenは設定しないと /var/lib/rancher/rke2/server/node-token に自動生成してしまいます。
事前に任意のランダム文字列を決めておいてそれを利用する方がIaCするときには非常に楽です(文字列なら何でもOK)。

設定ポイント:RKE2 Server Node

ドキュメントが古いのでCLIのヘルプを確認する

RKE2 Serverの設定は以下の通り。

Server Configuration Reference | RKE 2

ただし、このドキュメントは古くて必要なパラメータがすべて入っていません。
RKE2 CLIのバージョンによっても変わるので、以下のようにして実際にコマンドのhelpを確認した方が良いです。

# rke2 help server
NAME:
   rke2 server - Run management server

USAGE:
   rke2 server [OPTIONS]

OPTIONS:
   --config FILE, -c FILE                        (config) Load configuration from FILE (default: "/etc/rancher/rke2/config.yaml") [$RKE2_CONFIG_FILE]
   --debug                                       (logging) Turn on debug logs [$RKE2_DEBUG]
   --bind-address value                          (listener) rke2 bind address (default: 0.0.0.0)
   --advertise-address value                     (listener) IPv4 address that apiserver uses to advertise to members of the cluster (default: node-external-ip/node-ip)
   --tls-san value                               (listener) Add additional hostnames or IPv4/IPv6 addresses as Subject Alternative Names on the server TLS cert
   --data-dir value, -d value                    (data) Folder to hold state (default: "/var/lib/rancher/rke2")
   --cluster-cidr value                          (networking) IPv4/IPv6 network CIDRs to use for pod IPs (default: 10.42.0.0/16)
   --service-cidr value                          (networking) IPv4/IPv6 network CIDRs to use for service IPs (default: 10.43.0.0/16)
   --service-node-port-range value               (networking) Port range to reserve for services with NodePort visibility (default: "30000-32767")
   --cluster-dns value                           (networking) IPv4 Cluster IP for coredns service. Should be in your service-cidr range (default: 10.43.0.10)
   --cluster-domain value                        (networking) Cluster Domain (default: "cluster.local")
   --egress-selector-mode value                  (networking) One of 'agent', cluster', 'pod', 'disabled' (default: "agent")
   --servicelb-namespace value                   (networking) Namespace of the pods for the servicelb component (default: "kube-system")
   --token value, -t value                       (cluster) Shared secret used to join a server or agent to a cluster [$RKE2_TOKEN]
   --token-file value                            (cluster) File containing the cluster-secret/token [$RKE2_TOKEN_FILE]
   --write-kubeconfig value, -o value            (client) Write kubeconfig for admin client to this file [$RKE2_KUBECONFIG_OUTPUT]
   --write-kubeconfig-mode value                 (client) Write kubeconfig with this mode [$RKE2_KUBECONFIG_MODE]
   --enable-pprof                                (experimental) Enable pprof endpoint on supervisor port
   --kube-apiserver-arg value                    (flags) Customized flag for kube-apiserver process
   --etcd-arg value                              (flags) Customized flag for etcd process
   --kube-controller-manager-arg value           (flags) Customized flag for kube-controller-manager process
   --kube-scheduler-arg value                    (flags) Customized flag for kube-scheduler process
   --etcd-expose-metrics                         (db) Expose etcd metrics to client interface. (Default false)
   --etcd-disable-snapshots                      (db) Disable automatic etcd snapshots
   --etcd-snapshot-name value                    (db) Set the base name of etcd snapshots. Default: etcd-snapshot-<unix-timestamp> (default: "etcd-snapshot")
   --etcd-snapshot-schedule-cron value           (db) Snapshot interval time in cron spec. eg. every 5 hours '* */5 * * *' (default: "0 */12 * * *")
   --etcd-snapshot-retention value               (db) Number of snapshots to retain (default: 5)
   --etcd-snapshot-dir value                     (db) Directory to save db snapshots. (Default location: ${data-dir}/db/snapshots)
   --etcd-snapshot-compress                      (db) Compress etcd snapshot
   --etcd-s3                                     (db) Enable backup to S3
   --etcd-s3-endpoint value                      (db) S3 endpoint url (default: "s3.amazonaws.com")
   --etcd-s3-endpoint-ca value                   (db) S3 custom CA cert to connect to S3 endpoint
   --etcd-s3-skip-ssl-verify                     (db) Disables S3 SSL certificate validation
   --etcd-s3-access-key value                    (db) S3 access key [$AWS_ACCESS_KEY_ID]
   --etcd-s3-secret-key value                    (db) S3 secret key [$AWS_SECRET_ACCESS_KEY]
   --etcd-s3-bucket value                        (db) S3 bucket name
   --etcd-s3-region value                        (db) S3 region / bucket location (optional) (default: "us-east-1")
   --etcd-s3-folder value                        (db) S3 folder
   --etcd-s3-insecure                            (db) Disables S3 over HTTPS
   --etcd-s3-timeout value                       (db) S3 timeout (default: 5m0s)
   --disable value                               (components) Do not deploy packaged components and delete any deployed components (valid items: rke2-coredns, rke2-ingress-nginx, rke2-metrics-server)
   --disable-scheduler                           (components) Disable Kubernetes default scheduler
   --disable-cloud-controller                    (components) Disable rke2 default cloud controller manager
   --disable-kube-proxy                          (components) Disable running kube-proxy
   --node-name value                             (agent/node) Node name [$RKE2_NODE_NAME]
   --node-label value                            (agent/node) Registering and starting kubelet with set of labels
   --node-taint value                            (agent/node) Registering kubelet with set of taints
   --image-credential-provider-bin-dir value     (agent/node) The path to the directory where credential provider plugin binaries are located (default: "/var/lib/rancher/credentialprovider/bin")
   --image-credential-provider-config value      (agent/node) The path to the credential provider plugin config file (default: "/var/lib/rancher/credentialprovider/config.yaml")
   --container-runtime-endpoint value            (agent/runtime) Disable embedded containerd and use alternative CRI implementation
   --snapshotter value                           (agent/runtime) Override default containerd snapshotter (default: "overlayfs")
   --private-registry value                      (agent/runtime) Private registry configuration file (default: "/etc/rancher/rke2/registries.yaml")
   --node-ip value, -i value                     (agent/networking) IPv4/IPv6 addresses to advertise for node
   --node-external-ip value                      (agent/networking) IPv4/IPv6 external IP addresses to advertise for node
   --resolv-conf value                           (agent/networking) Kubelet resolv.conf file [$RKE2_RESOLV_CONF]
   --kubelet-arg value                           (agent/flags) Customized flag for kubelet process
   --kube-proxy-arg value                        (agent/flags) Customized flag for kube-proxy process
   --protect-kernel-defaults                     (agent/node) Kernel tuning behavior. If set, error if kernel tunables are different than kubelet defaults.
   --agent-token value                           (cluster) Shared secret used to join agents to the cluster, but not servers [$RKE2_AGENT_TOKEN]
   --agent-token-file value                      (cluster) File containing the agent secret [$RKE2_AGENT_TOKEN_FILE]
   --server value, -s value                      (cluster) Server to connect to, used to join a cluster [$RKE2_URL]
   --cluster-reset                               (cluster) Forget all peers and become sole member of a new cluster [$RKE2_CLUSTER_RESET]
   --cluster-reset-restore-path value            (db) Path to snapshot file to be restored
   --system-default-registry value               (image) Private registry to be used for all system images [$RKE2_SYSTEM_DEFAULT_REGISTRY]
   --selinux                                     (agent/node) Enable SELinux in containerd [$RKE2_SELINUX]
   --lb-server-port value                        (agent/node) Local port for supervisor client load-balancer. If the supervisor and apiserver are not colocated an additional port 1 less than this port will also be used for the apiserver client load-balancer. (default: 6444) [$RKE2_LB_SERVER_PORT]
   --cni value                                   (networking) CNI Plugins to deploy, one of none, calico, canal, cilium; optionally with multus as the first value to enable the multus meta-plugin (default: canal) [$RKE2_CNI]
   --enable-servicelb                            (components) Enable rke2 default cloud controller manager's service controller [$RKE2_ENABLE_SERVICELB]
   --kube-apiserver-image value                  (image) Override image to use for kube-apiserver [$RKE2_KUBE_APISERVER_IMAGE]
   --kube-controller-manager-image value         (image) Override image to use for kube-controller-manager [$RKE2_KUBE_CONTROLLER_MANAGER_IMAGE]
   --cloud-controller-manager-image value        (image) Override image to use for cloud-controller-manager [$RKE2_CLOUD_CONTROLLER_MANAGER_IMAGE]
   --kube-proxy-image value                      (image) Override image to use for kube-proxy [$RKE2_KUBE_PROXY_IMAGE]
   --kube-scheduler-image value                  (image) Override image to use for kube-scheduler [$RKE2_KUBE_SCHEDULER_IMAGE]
   --pause-image value                           (image) Override image to use for pause [$RKE2_PAUSE_IMAGE]
   --runtime-image value                         (image) Override image to use for runtime binaries (containerd, kubectl, crictl, etc) [$RKE2_RUNTIME_IMAGE]
   --etcd-image value                            (image) Override image to use for etcd [$RKE2_ETCD_IMAGE]
   --kubelet-path value                          (experimental/agent) Override kubelet binary path [$RKE2_KUBELET_PATH]
   --cloud-provider-name value                   (cloud provider) Cloud provider name [$RKE2_CLOUD_PROVIDER_NAME]
   --cloud-provider-config value                 (cloud provider) Cloud provider configuration file path [$RKE2_CLOUD_PROVIDER_CONFIG]
   --profile value                               (security) Validate system configuration against the selected benchmark (valid items: cis-1.5, cis-1.6 ) [$RKE2_CIS_PROFILE]
   --audit-policy-file value                     (security) Path to the file that defines the audit policy configuration [$RKE2_AUDIT_POLICY_FILE]
   --control-plane-resource-requests value       (components) Control Plane resource requests [$RKE2_CONTROL_PLANE_RESOURCE_REQUESTS]
   --control-plane-resource-limits value         (components) Control Plane resource limits [$RKE2_CONTROL_PLANE_RESOURCE_LIMITS]
   --kube-apiserver-extra-mount value            (components) kube-apiserver extra volume mounts [$RKE2_KUBE_APISERVER_EXTRA_MOUNT]
   --kube-scheduler-extra-mount value            (components) kube-scheduler extra volume mounts [$RKE2_KUBE_SCHEDULER_EXTRA_MOUNT]
   --kube-controller-manager-extra-mount value   (components) kube-controller-manager extra volume mounts [$RKE2_KUBE_CONTROLLER_MANAGER_EXTRA_MOUNT]
   --kube-proxy-extra-mount value                (components) kube-proxy extra volume mounts [$RKE2_KUBE_PROXY_EXTRA_MOUNT]
   --etcd-extra-mount value                      (components) etcd extra volume mounts [$RKE2_ETCD_EXTRA_MOUNT]
   --cloud-controller-manager-extra-mount value  (components) cloud-controller-manager extra volume mounts [$RKE2_CLOUD_CONTROLLER_MANAGER_EXTRA_MOUNT]
   --kube-apiserver-extra-env value              (components) kube-apiserver extra environment variables [$RKE2_KUBE_APISERVER_EXTRA_ENV]
   --kube-scheduler-extra-env value              (components) kube-scheduler extra environment variables [$RKE2_KUBE_SCHEDULER_EXTRA_ENV]
   --kube-controller-manager-extra-env value     (components) kube-controller-manager extra environment variables [$RKE2_KUBE_CONTROLLER_MANAGER_EXTRA_ENV]
   --kube-proxy-extra-env value                  (components) kube-proxy extra environment variables [$RKE2_KUBE_PROXY_EXTRA_ENV]
   --etcd-extra-env value                        (components) etcd extra environment variables [$RKE2_ETCD_EXTRA_ENV]
   --cloud-controller-manager-extra-env value    (components) cloud-controller-manager extra environment variables [$RKE2_CLOUD_CONTROLLER_MANAGER_EXTRA_ENV]

クラスタ内部から外へ通信が発生する場合は--tls-sanへの追加を忘れない

クラスタ内部で外部と通信が必要な場合(クラスタ外のHashicorp VaultサーバからSecretをもらいたいとか)、tls-sanFQDNまたはIPアドレスを追加する必要がります。

例えば、tls-sanに外部Vaultのエンドポイントを追加しておかないとPodとVault間の認証に使う VaultのKubernetes Auth Methodにて、403 permission denied になってしまうので要注意。
また、後から追加したい場合は、Serever Nodeに1台ずつ追加してrke2-serverを再起動し全台適用が済んだら利用可能になる

ネットワーク設定について

ネットワーク関連の設定は以下が参考になります。

docs.rke2.io

また、CNIについては、Network Options | RKE 2 にある通り、RKE2から利用できるCNIは限定されています(とは言えメジャーどころは抑えてます)。

標準で用意されているオプションは以下の通り。

docs.rke2.io

CNIが、Rancher Servcerとして利用可能か または Rancher Servcerの管理下のクラスタとして利用できるか については、 Rancherのサポートマトリクス を確認ください。

サポート外でも良いので、RKE2が用意していないCNI使う場合は、以前作成した以下の記事を参考にしてください。

qiita.com

※ただし、クラスタのアップデートなどは慎重に確認が必要です。

設定ポイント:RKE2 Agent Node

ドキュメントが古いのでCLIのヘルプを確認する

RKE2 Serverの設定は以下の通り。

Agent Configuration Reference | RKE 2

Serverと同じ様に古いのでCLIのヘルプを確認した方が良いです。

# rke2 help agent
NAME:
   rke2 agent - Run node agent

USAGE:
   rke2 agent [OPTIONS]

OPTIONS:
   --config FILE, -c FILE                        (config) Load configuration from FILE (default: "/etc/rancher/rke2/config.yaml") [$RKE2_CONFIG_FILE]
   --debug                                       (logging) Turn on debug logs [$RKE2_DEBUG]
   --token value, -t value                       (cluster) Token to use for authentication [$RKE2_TOKEN]
   --token-file value                            (cluster) Token file to use for authentication [$RKE2_TOKEN_FILE]
   --server value, -s value                      (cluster) Server to connect to [$RKE2_URL]
   --data-dir value, -d value                    (data) Folder to hold state (default: "/var/lib/rancher/rke2")
   --node-name value                             (agent/node) Node name [$RKE2_NODE_NAME]
   --node-label value                            (agent/node) Registering and starting kubelet with set of labels
   --node-taint value                            (agent/node) Registering kubelet with set of taints
   --image-credential-provider-bin-dir value     (agent/node) The path to the directory where credential provider plugin binaries are located (default: "/var/lib/rancher/credentialprovider/bin")
   --image-credential-provider-config value      (agent/node) The path to the credential provider plugin config file (default: "/var/lib/rancher/credentialprovider/config.yaml")
   --container-runtime-endpoint value            (agent/runtime) Disable embedded containerd and use alternative CRI implementation
   --snapshotter value                           (agent/runtime) Override default containerd snapshotter (default: "overlayfs")
   --private-registry value                      (agent/runtime) Private registry configuration file (default: "/etc/rancher/rke2/registries.yaml")
   --node-ip value, -i value                     (agent/networking) IPv4/IPv6 addresses to advertise for node
   --node-external-ip value                      (agent/networking) IPv4/IPv6 external IP addresses to advertise for node
   --resolv-conf value                           (agent/networking) Kubelet resolv.conf file [$RKE2_RESOLV_CONF]
   --kubelet-arg value                           (agent/flags) Customized flag for kubelet process
   --kube-proxy-arg value                        (agent/flags) Customized flag for kube-proxy process
   --protect-kernel-defaults                     (agent/node) Kernel tuning behavior. If set, error if kernel tunables are different than kubelet defaults.
   --selinux                                     (agent/node) Enable SELinux in containerd [$RKE2_SELINUX]
   --lb-server-port value                        (agent/node) Local port for supervisor client load-balancer. If the supervisor and apiserver are not colocated an additional port 1 less than this port will also be used for the apiserver client load-balancer. (default: 6444) [$RKE2_LB_SERVER_PORT]
   --kube-apiserver-image value                  (image) Override image to use for kube-apiserver [$RKE2_KUBE_APISERVER_IMAGE]
   --kube-controller-manager-image value         (image) Override image to use for kube-controller-manager [$RKE2_KUBE_CONTROLLER_MANAGER_IMAGE]
   --cloud-controller-manager-image value        (image) Override image to use for cloud-controller-manager [$RKE2_CLOUD_CONTROLLER_MANAGER_IMAGE]
   --kube-proxy-image value                      (image) Override image to use for kube-proxy [$RKE2_KUBE_PROXY_IMAGE]
   --kube-scheduler-image value                  (image) Override image to use for kube-scheduler [$RKE2_KUBE_SCHEDULER_IMAGE]
   --pause-image value                           (image) Override image to use for pause [$RKE2_PAUSE_IMAGE]
   --runtime-image value                         (image) Override image to use for runtime binaries (containerd, kubectl, crictl, etc) [$RKE2_RUNTIME_IMAGE]
   --etcd-image value                            (image) Override image to use for etcd [$RKE2_ETCD_IMAGE]
   --kubelet-path value                          (experimental/agent) Override kubelet binary path [$RKE2_KUBELET_PATH]
   --cloud-provider-name value                   (cloud provider) Cloud provider name [$RKE2_CLOUD_PROVIDER_NAME]
   --cloud-provider-config value                 (cloud provider) Cloud provider configuration file path [$RKE2_CLOUD_PROVIDER_CONFIG]
   --profile value                               (security) Validate system configuration against the selected benchmark (valid items: cis-1.5, cis-1.6 ) [$RKE2_CIS_PROFILE]
   --audit-policy-file value                     (security) Path to the file that defines the audit policy configuration [$RKE2_AUDIT_POLICY_FILE]
   --control-plane-resource-requests value       (components) Control Plane resource requests [$RKE2_CONTROL_PLANE_RESOURCE_REQUESTS]
   --control-plane-resource-limits value         (components) Control Plane resource limits [$RKE2_CONTROL_PLANE_RESOURCE_LIMITS]
   --kube-apiserver-extra-mount value            (components) kube-apiserver extra volume mounts [$RKE2_KUBE_APISERVER_EXTRA_MOUNT]
   --kube-scheduler-extra-mount value            (components) kube-scheduler extra volume mounts [$RKE2_KUBE_SCHEDULER_EXTRA_MOUNT]
   --kube-controller-manager-extra-mount value   (components) kube-controller-manager extra volume mounts [$RKE2_KUBE_CONTROLLER_MANAGER_EXTRA_MOUNT]
   --kube-proxy-extra-mount value                (components) kube-proxy extra volume mounts [$RKE2_KUBE_PROXY_EXTRA_MOUNT]
   --etcd-extra-mount value                      (components) etcd extra volume mounts [$RKE2_ETCD_EXTRA_MOUNT]
   --cloud-controller-manager-extra-mount value  (components) cloud-controller-manager extra volume mounts [$RKE2_CLOUD_CONTROLLER_MANAGER_EXTRA_MOUNT]
   --kube-apiserver-extra-env value              (components) kube-apiserver extra environment variables [$RKE2_KUBE_APISERVER_EXTRA_ENV]
   --kube-scheduler-extra-env value              (components) kube-scheduler extra environment variables [$RKE2_KUBE_SCHEDULER_EXTRA_ENV]
   --kube-controller-manager-extra-env value     (components) kube-controller-manager extra environment variables [$RKE2_KUBE_CONTROLLER_MANAGER_EXTRA_ENV]
   --kube-proxy-extra-env value                  (components) kube-proxy extra environment variables [$RKE2_KUBE_PROXY_EXTRA_ENV]
   --etcd-extra-env value                        (components) etcd extra environment variables [$RKE2_ETCD_EXTRA_ENV]
   --cloud-controller-manager-extra-env value    (components) cloud-controller-manager extra environment variables [$RKE2_CLOUD_CONTROLLER_MANAGER_EXTRA_ENV]

CNIの設定は不要

Agentの設定にcniの指定は不要です。
RKE2 Server NodeがAgent Nodeをクラスタに追加する際に、Server Node側からCNIの指定をするのでAgent Nodeで指定する必要は無いです。

なので、特別な設定が何も必要ないなら設定は以下だけ。

server: https://${LBのFQDN}:9345
token: ${SERVER_TOKEN}

HAクラスタの構築の流れ

Rancherを使わずにRKE2にてHAクラスタを構築する場合の流れは、基本的には以下のドキュメントの通りです。

docs.rke2.io

大まかな流れは以下の通り。

  1. LBを構築して起動
    • ただし、ProxyするのはRKE2 Serverの1台目だけ
  2. RKE2 Server Nodeの1台目を起動
  3. RKE2 Server Nodeの2台目以降を起動
  4. RKE2 Server Nodeが正常に起動後、1台目の設定のserverコメントアウトしてrke2-serverサービスを再起動
  5. LBでProxyする対象を2台目以降のRKE2 Server Nodeも含めるようにする
  6. RKE2 Agent Nodeを追加
  7. kubectl get nodekubectl get po -A -o wide してPodが正常に稼働しているか最終確認

また、RKE2のロールのetcd、control plane、workerを分けて構築する場合は以下を参考にしてください。

www.suse.com

この場合、大まかな流れは以下の通り。

  1. LBを構築して起動
    • ただし、Proxyするのはcontrol planeロールのNodeは1台目だけ
  2. etcdロールの設定をして起動(etcdとして起動したいNode分だけ実行)
  3. control planeロールの設定をして起動
    • ただし、1台目の設定に LBを指定したserverは必要だが、もう1台には不要
  4. control planeロールのNodeが2台とも正常に起動後、1台目の設定のserverコメントアウトしてrke2-serverサービスを再起動
  5. LBでProxyする対象を2台目のcontrol planeロールのNodeも含めるようにする
  6. RKE2 Agent Nodeを追加
  7. kubectl get nodekubectl get po -A -o wide してPodが正常に稼働しているか最終確認

rke2-serverサービスの状況は sudo journalctl -u rke2-server -f でログを確認すると分かります。
どちらの方法についても、kubectl get nodeでNodeのSTATUSはReadyとなっていても、以下の様なエラーが出て正常にクラスタ参加が出来ていない状態ですが、1台目のcontrol planeロールのNodeが正常に起動するとエラーログは消えます。

Nov 30 05:27:17 rke2-server01 rke2[1190]: time="2022-11-30T05:27:17+09:00" level=info msg="Connecting to proxy" url="wss://172.30.40.26:9345/v1-rke2/connect"
Nov 30 05:27:19 rke2-server01 rke2[1190]: time="2022-11-30T05:27:19+09:00" level=error msg="Remotedialer proxy error" error="connect not allowed"

Tips1:LBの構築ポイント

クラスタ管理用のエンドポイントとして利用するLBの構築ポイントについて。

以下のポートをcontrol planeのいるNodeにProxyしたらOK。

  • 6443 → RKE2 Server Nodeの6443(Kubernetes API Server用ポート)
  • 9345 → RKE2 Server Nodeの9345 (RKE2向けの新しいNodeの登録用ポート)

例えば、HAProxyの場合ならこんな感じ。

frontend apiservers
    bind *:6443
    mode tcp
    option  tcplog
    default_backend k8s_apiservers

backend k8s_apiservers
    mode tcp
    balance roundrobin
    option log-health-checks
    default-server inter 10s fall 2
    server rke2-server01 198.51.100.2:6443 check
    server rke2-server02 198.51.100.3:6443 check
    server rke2-server03 198.51.100.4:6443 check

frontend new_node_regist
    bind 198.51.100.1:9345 # LBのIP
    option tcplog
    mode tcp
    default_backend rke2_new_node_regist

backend rke2_new_node_regist
    mode tcp
    option ssl-hello-chk
    option log-health-checks
    default-server inter 10s fall 2
    server rke2-server01 198.51.100.2:9345 check
    server rke2-server02 198.51.100.3:9345 check
    server rke2-server03 198.51.100.4:9345 check

HAProxyで無くても以下の条件を満たせばOK。例えばLVSとか。

  • A layer 4 (TCP) load balancer
  • Round-robin DNS
  • Virtual or Elastic IP addresses

docs.rke2.io

また、LB自体の可用性も必要になるので、Keepalivedなどの利用も合わせて検討ください。

そのあたりについては、以下のKubeadmのドキュメントが参考になります。

github.com

(´-`).。oO(kube-vipがGAしたら良いなぁ)

Tips2:1台目の設定のserverコメントアウトしてrke2-serverサービスを再起動する理由

これを実施しないと1台目のNodeのrke2-serverサービスを再起動した際に、稼働中の2台目のServer Node(control plane)に参加出来ない為です。

RKE2(Kubernetes)のアップグレード

Rancher UIの ☰ > Cluster Management から対象のクラスタを選択して> Edit Configにアップグレード可能なバージョンがあるのでそれを選んでアップグレードする方法もあります。詳細は、 Upgrading and Rolling Back Kubernetes | Rancher Manager を参照ください。

他にもRKE2の場合は、 手動でアップグレードする方法 もあります。
ただ、system-upgrade-controller を使ったKubernetesネイティブな自動アップグレード方法があり、柔軟にアップグレード設定出来るのでとても便利。オススメ。

docs.rke2.io

自動といってもCRDを使って設定する際に、versionv1.23.1+rke2r2のようにして設定したらバージョンを固定に出来るので勝手にアップグレードされることはないです。
ただ、CRDのversionの代わりに、channelを使ってhttps://update.rke2.io/v1-release/channels/stableなどのように指定するとリリースされた時点で自動でアップデート可能なようです。設定可能なチャンネルは https://update.rke2.io/v1-release/channels を見ると分かります。

さいごに

いかがだったでしょうか。
RKE2を使ったKubernetesクラスタを構築する際の考慮事項についてまとめてみました。

これをきっかけに挑戦してみた方がいたら是非以下のRancherJPのMeetupでの登壇お待ちしています!

rancherjp.connpass.com

また、Slackもあるのでよかったら参加ください!

以上、MicroAd Advent Calendar 2022 の4日目の記事でした。

おまけ

Tips: Rancher CLI

RancherというとWeb UIからポチポチのイメージあるかもしれませんが、CLIもちゃんとあります。

クラスタの作成のコマンドも以下の通りあるので必要に応じて利用してください。

docs.ranchermanager.rancher.io

例えば、クラスタの作成は以下の通り。

❯ ./rancher cluster create --help
Usage: 
  rancher clusters create [OPTIONS] [NEWCLUSTERNAME...]

Options:
   --description value         Description to apply to the cluster
   --disable-docker-version    Allow unsupported versions of docker on the nodes, [default=true]
   --import                    Mark the cluster for import, this is required if the cluster is going to be used to import an existing k8s cluster
   --k8s-version value         Kubernetes version to use for the cluster, pass in 'list' to see available versions
   --network-provider value    Network provider for the cluster (flannel, canal, calico) (default: "canal")
   --psp-default-policy value  Default pod security policy to apply
   --rke-config value          Location of an rke config file to import. Can be JSON or YAML format

他にも以下のようなコマンドがあります。

❯ ./rancher --help
Rancher CLI, managing containers one UTF-8 character at a time

Usage: rancher [OPTIONS] COMMAND [arg...]

Version: v2.6.0

Options:
  --debug                   Debug logging
  --config value, -c value  Path to rancher config (default: "/home/nagatomi/.rancher") [$RANCHER_CONFIG_DIR]
  --help, -h                show help
  --version, -v             print the version
  
Commands:
  apps, [app]                                       Operations with apps. Uses helm. Flags prepended with "helm" can also be accurately described by helm documentation.
  catalog                                           Operations with catalogs
  clusters, [cluster]                               Operations on clusters
  context                                           Operations for the context
  globaldns                                         Operations on global DNS providers and entries
  inspect                                           View details of resources
  kubectl                                           Run kubectl commands
  login, [l]                                        Login to a Rancher server
  machines, [machine]                               Operations on machines
  multiclusterapps, [multiclusterapp mcapps mcapp]  Operations with multi-cluster apps
  namespaces, [namespace]                           Operations on namespaces
  nodes, [node]                                     Operations on nodes
  projects, [project]                               Operations on projects
  ps                                                Show workloads in a project
  server                                            Operations for the server
  settings, [setting]                               Show settings for the current server
  ssh                                               SSH into a node
  up                                                apply compose config
  wait                                              Wait for resources cluster, app, project, multiClusterApp
  token                                             Authenticate and generate new kubeconfig token
  help, [h]                                         Shows a list of commands or help for one command

Tips2: TerraformのRancher2 Provider

使ったことないのですが、Terraformのプロバイダの用意もあるのでこちらもどうぞ。

registry.terraform.io

AWXサーバの外からansibleでジョブを実行する

この記事は MicroAd Advent Calendar 202120日目 と Ansible Advent Calendar 2021 - Adventarの 7日目の記事です(空いててもったいないので埋めました)。

qiita.com

adventar.org

はじめに

以下の記事の補足になります。

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を使います。

galaxy.ansible.com

こちらを利用することで、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

  1. import-datasources
  2. import-notification-channels
  3. 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_namejob_name を変更して他のロール分(import-datasourcesimport-notification-channels)も定義します。

AWX側の設定

以下のようにしてAWXからジョブテンプレートを作成します。
また以下は import_dashboard の分ですが、別途、他のロール分(import-datasourcesimport-notification-channels)も作成します。

f:id:yassan0627:20211220161327p:plain
ジョブテンプレートの設定例

注意点としては、上図の右下隅にある「起動プロンプト」にチェックを入れないと実行出来ないので忘れずにチェックしてください。 また、手動で実行したい場合は、追加変数に grafana_grouprevision を追加して実行してやればOK。

CI側のフローの設定

特に前回から変更は無いです。Makefileダッシュボードやデータソース、通知チャンネルのimportを実行するMakeターゲットをAWXジョブからの実行に変更しているので特にここでは変更点がありません。

詳細は前回の記事を参照して下さい。

yassan.hatenablog.jp

最後に

これでPRを使わずに実行履歴を残しつつダッシュボードの更新が出来るようになりました。
とは言え、PRベースに更新出来ないのでレビューが出来ず、また、更新意図を残せないのであくまで緊急事態用というところです。

全体を通した今後の改善点

現状、Grafanaのダッシュボード・データソース・通知チャンネルを毎回すべて更新してしまいます。 理想的には更新を検知して、更新ファイルだけ反映するような対応があるとより早く同期できたりします。 ただ、git diffで更新差分を使うアイディアもあるのですが差分をキレイに出すのが難しいです6

他にも、今回の仕組みではダッシュボードを削除する仕組みが無いので、リポジトリから削除して後、GrafanaのWebUIから手動で削除する必要があるので若干面倒です。

改善ポイントについて良案があれば是非教えて下さい!


  1. 詳細は 14. Projects — Ansible Tower User Guide v3.8.5 を参照。
    また、 GitHubのAWXのdoc にも説明があるので参考になります。

  2. ansible-core v2.11未満の場合は、 install オプションに -f がないと2回目以降でエラーになります。
    ただし、 ansible/ansible#65699 にて検討されて、ansible/ansible#73336で、v2.11より -u オプションが追加されています。

  3. パラメータの詳細は awx.awx.job_wait を参照下さい。

  4. パラメータの詳細は awx.awx.job_launch を参照下さい。

  5. この辺の説明は前回の記事を参照して下さい。
    yassan.hatenablog.jp

  6. mainブランチとの差分を使うと後から作成したPRを先にマージすると差分を取れなくなる問題があります(rebaseしたら治りますけど)。

CIOpsでGrafanaダッシュボードを運用していく~ansible実装編~

この記事は Ansible Advent Calendar 2021 の6日目の記事です。

adventar.org

はじめに

以下の続きとなります。

yassan.hatenablog.jp

以下で紹介するロールをどう使うか、メリットなどは前回の記事を参照ください。

Grafanaダッシュボードを更新する際に利用するAnsibleモジュールの紹介

Grafana向けにCommunity.Grafanaという Ansible Collectionがあるのでこちらを利用します1

docs.ansible.com

今回作成するロールについて

今回は以下の名前でロールを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_usergrafana_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)とのことです。楽しみですね!


  1. 各モジュールの利用方法は、Collectionの各モジュールのExamplesも参考になりますが、GrafanaのCollectionのあるリポジトリのtest環境が非常に参考になります。
    github.com
    他にも、各処理は最終的にはHTTP APIを使って処理しているので、以下のドキュメントの調べたいロールのAPIを参考にすると良いです。
    HTTP API | Grafana Labs

  2. 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 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

Win10でネイティブにAnsibleを実行したかった件

f:id:yassan0627:20210508153841p:plain

結論

Windows Frequently Asked Questions · ansible/ansible にある通り、現状、Windowsから直接Ansibleは実行出来ない(但し、WSL経由ならいける)。

Can Ansible run on Windows? No, Ansible can only manage Windows hosts. Ansible cannot run on a Windows host natively, though it can run under the Windows Subsystem for Linux (WSL).

環境

-- --
エディション Windows 10 Pro
バージョン 21H1
インストール日 2020/08/23
OS ビルド 19043.964
エクスペリエンス Windows Feature Experience Pack 120.2212.3740.0

モチベ

Win10 Homeで、VagrantHyper-V使ってVM起動する際に、 初回の作業(特定のユーザ作ってSSH鍵追加したり、dnf updateとか)がダルいので自動化したい、、、

んで、Provisionerにちょうどよいのないかなぁとドキュメント読んでたら、 VagrantのProvisionerにAnsibleが使えるって事で試そうとした。

www.vagrantup.com

やった事

pythonのインストール

以下にある通り、パッケージをDLってインストール。 www.python.jp

仮想環境の作成&ansibleのインストール

ansibleがwinネイティブに使えるなら他にも使いたいので、venvを使って環境を分ける。

❯ py -m venv --without-pip .venv
❯ .venv\Scripts\activate.ps1
❯ curl https://bootstrap.pypa.io/get-pip.py
❯ py get-pip.py
❯ py -m pip install -r requirements.txt

requirements.txt

flake8
pytest
cryptography
jmespath == 0.9.5
PyYAML >= 5.4.1

ansible-base <2.11,>=2.10.7
yamllint >=1.25.0
ansible-lint >=5.0.0
python-vagrant >= 0.5.15

上手くいくかと思ったけど、以下のエラー

❯ py -m pip install ansible-base==2.10.9
Collecting ansible-base==2.10.9
  Using cached ansible-base-2.10.9.tar.gz (6.0 MB)
Requirement already satisfied: jinja2 in c:\users\yassan\vargant\hoge\.venv\lib\site-packages (from ansible-base==2.10.9) (2.11.3)
Requirement already satisfied: PyYAML in c:\users\yassan\vargant\hoge\.venv\lib\site-packages (from ansible-base==2.10.9) (5.4.1)
Requirement already satisfied: cryptography in c:\users\yassan\vargant\hoge\.venv\lib\site-packages (from ansible-base==2.10.9) (3.4.7)
Requirement already satisfied: packaging in c:\users\yassan\vargant\hoge\.venv\lib\site-packages (from ansible-base==2.10.9) (20.9)
Requirement already satisfied: cffi>=1.12 in c:\users\yassan\vargant\hoge\.venv\lib\site-packages (from cryptography->ansible-base==2.10.9) (1.14.5)
Requirement already satisfied: pycparser in c:\users\yassan\vargant\hoge\.venv\lib\site-packages (from cffi>=1.12->cryptography->ansible-base==2.10.9) (2.20)
Requirement already satisfied: MarkupSafe>=0.23 in c:\users\yassan\vargant\hoge\.venv\lib\site-packages (from jinja2->ansible-base==2.10.9) (1.1.1)
Requirement already satisfied: pyparsing>=2.0.2 in c:\users\yassan\vargant\hoge\.venv\lib\site-packages (from packaging->ansible-base==2.10.9) (2.4.7)
Building wheels for collected packages: ansible-base
  Building wheel for ansible-base (setup.py) ... error
  ERROR: Command errored out with exit status 1:
   command: 'C:\Users\yassan\vargant\hoge\.venv\Scripts\python.exe' -u -c 'import io, os, sys, setuptools, tokenize; sys.argv[0] = '"'"'C:\\Users\\yassan\\AppData\\Local\\Temp\\pip-install-4p46nnmv\\ansible-base_68e8945aa34749959f5671f4bb8c4177\\setup.py'"'"'; __file__='"'"'C:\\Users\\yassan\\AppData\\Local\\Temp\\pip-install-4p46nnmv\\ansible-base_68e8945aa34749959f5671f4bb8c4177\\setup.py'"'"';f = getattr(tokenize, '"'"'open'"'"', open)(__file__) if os.path.exists(__file__) else io.StringIO('"'"'from setuptools import setup; setup()'"'"');code = f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' bdist_wheel -d 'C:\Users\yassan\AppData\Local\Temp\pip-wheel-tymwpqt_'
       cwd: C:\Users\yassan\AppData\Local\Temp\pip-install-4p46nnmv\ansible-base_68e8945aa34749959f5671f4bb8c4177\
  Complete output (951 lines):
  running bdist_wheel
   :
  creating build\lib\ansible_test\_data\sanity\yamllint
  copying test\lib\ansible_test\_data\sanity\yamllint\yamllinter.py -> build\lib\ansible_test\_data\sanity\yamllint
  creating build\lib\ansible_test\_data\sanity\yamllint\config
  copying test\lib\ansible_test\_data\sanity\yamllint\config\default.yml -> build\lib\ansible_test\_data\sanity\yamllint\config
  copying test\lib\ansible_test\_data\sanity\yamllint\config\modules.yml -> build\lib\ansible_test\_data\sanity\yamllint\config
  copying test\lib\ansible_test\_data\sanity\yamllint\config\plugins.yml -> build\lib\ansible_test\_data\sanity\yamllint\config
  creating build\lib\ansible_test\_data\setup
  copying test\lib\ansible_test\_data\setup\ConfigureRemotingForAnsible.ps1 -> build\lib\ansible_test\_data\setup
  copying test\lib\ansible_test\_data\setup\docker.sh -> build\lib\ansible_test\_data\setup
  copying test\lib\ansible_test\_data\setup\remote.sh -> build\lib\ansible_test\_data\setup
  copying test\lib\ansible_test\_data\setup\windows-httptester.ps1 -> build\lib\ansible_test\_data\setup
  creating build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-aws.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-azure.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-cloudscale.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-cs.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-gcp.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-hcloud.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-opennebula.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-openshift.kubeconfig.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-scaleway.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-tower.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-vcenter.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\cloud-config-vultr.ini.template -> build\lib\ansible_test\config
  copying test\lib\ansible_test\config\inventory.networking.template -> build\lib\ansible_test\config
   :
  copying test\lib\ansible_test\config\inventory.winrm.template -> build\lib\ansible_test\config
  error: symbolic link privilege not held
  ----------------------------------------
  ERROR: Failed building wheel for ansible-base
  Running setup.py clean for ansible-base
Failed to build ansible-base
Installing collected packages: ansible-base
    Running setup.py install for ansible-base ... error
    ERROR: Command errored out with exit status 1:
     command: 'C:\Users\yassan\vargant\hoge\.venv\Scripts\python.exe' -u -c 'import io, os, sys, setuptools, tokenize; sys.argv[0] = '"'"'C:\\Users\\yassan\\AppData\\Local\\Temp\\pip-install-4p46nnmv\\ansible-base_68e8945aa34749959f5671f4bb8c4177\\setup.py'"'"'; __file__='"'"'C:\\Users\\yassan\\AppData\\Local\\Temp\\pip-install-4p46nnmv\\ansible-base_68e8945aa34749959f5671f4bb8c4177\\setup.py'"'"';f = getattr(tokenize, '"'"'open'"'"', open)(__file__) if os.path.exists(__file__) else io.StringIO('"'"'from setuptools import setup; setup()'"'"');code = f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record 'C:\Users\yassan\AppData\Local\Temp\pip-record-9tp3_0q8\install-record.txt' --single-version-externally-managed --compile --install-headers 'C:\Users\yassan\vargant\hoge\.venv\include\site\python3.6\ansible-base'
         cwd: C:\Users\yassan\AppData\Local\Temp\pip-install-4p46nnmv\ansible-base_68e8945aa34749959f5671f4bb8c4177\
    Complete output (949 lines):
    running install
    running build
    running build_py
    creating build
    creating build\lib
      :
    copying test\lib\ansible_test\config\inventory.winrm.template -> build\lib\ansible_test\config
    error: symbolic link privilege not held
    ----------------------------------------
ERROR: Command errored out with exit status 1: 'C:\Users\yassan\vargant\hoge\.venv\Scripts\python.exe' -u -c 'import io, os, sys, setuptools, tokenize; sys.argv[0] = '"'"'C:\\Users\\yassan\\AppData\\Local\\Temp\\pip-install-4p46nnmv\\ansible-base_68e8945aa34749959f5671f4bb8c4177\\setup.py'"'"'; __file__='"'"'C:\\Users\\yassan\\AppData\\Local\\Temp\\pip-install-4p46nnmv\\ansible-base_68e8945aa34749959f5671f4bb8c4177\\setup.py'"'"';f = getattr(tokenize, '"'"'open'"'"', open)(__file__) if os.path.exists(__file__) else io.StringIO('"'"'from setuptools import setup; setup()'"'"');code = f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record 'C:\Users\yassan\AppData\Local\Temp\pip-record-9tp3_0q8\install-record.txt' --single-version-externally-managed --compile --install-headers 'C:\Users\yassan\vargant\hoge\.venv\include\site\python3.6\ansible-base' Check the logs for full command output.

結論にある通り、FAQ見たら動かないとの事。。残念。

Can Ansible run on Windows? No, Ansible can only manage Windows hosts. Ansible cannot run on a Windows host natively, though it can run under the Windows Subsystem for Linux (WSL).

参考