プラグイン — どこで動くかが設計の半分
一言でいうと
Ansibleを拡張する場所は複数ありますが、分ける基準は1つです。フィルター・テスト・ルックアップは、プレイブックを実行するコントローラーで動き、モジュールは対象サーバーにコピーされて、そこで動きます。何をどこに置くかは、この1行からほとんど決まります。
なぜ必要なのか
プレイブックが成長すると、どのチームにも同じ形の場所ができます。サービス名をファイル名として使える形に整える作業、ポート番号を等級に分ける作業、社内のどこかにある表から値を探してくる作業です。標準のフィルターでできなければ、人々は2つのうちどちらかをします。
1つ目は、Jinja2を無理に伸ばすことです。{{ name | lower | replace(' ', '-') | replace('.', '-') | replace('_', '-') | trim('-') }}のような行ができ、半年後には、誰もその行を直そうとしなくなります。テストもできません。そのロジックはプレイブックの中にしかなく、単独で呼び出してみる方法がないからです。
2つ目は、シェルに降りることです。shell: echo {{ name }} | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9]/-/g'と書いて、結果をregisterで受け取ります。この1行で、Ansibleが与えてくれたものがすべて失われます。冪等性がなく(常にchanged)、チェックモードではスキップされ、対象にtrとsedがあるという前提が生まれ、往復が1回増えます。値を計算する作業に、SSHの往復を使うことになるのです。
プラグインは、この場所のためのものです。値を計算する作業は、コントローラーでPythonを使って行い、対象には触れません。そうすればそのロジックは1つのファイルに集まり、名前が付き、単独でテストできるようになります。
どう動くのか
Ansibleのプラグインは10種類以上ありますが、プレイブックを書く人が自分で作ることになるのは、たいてい4つです。
| 種類 | 呼び出し方 | 動く場所 | 返すもの |
|---|---|---|---|
| フィルター(filter) | 값 | 이름 |
コントローラー | 任意の値 |
| テスト(test) | 값 is 이름 |
コントローラー | 真または偽 |
| ルックアップ(lookup) | lookup('이름', 인자) |
コントローラー | リスト |
| モジュール(module) | タスクのキー | 対象 | JSON(changedを含む) |
表の「呼び出し方」の列にある韓国語はプレースホルダーで、順に値と名前、値と名前、名前と引数です。
前の3つは、テンプレートエンジンが値を作るときに呼ばれます。そのため、対象に何もインストールする必要がなく、SSHの往復も増えません。その代わり、対象の状態は見られません。対象のディスクがどれだけ空いているかを、フィルターは知ることができません。それはファクトやモジュールの仕事です。
モジュールは反対です。ファイルが対象にコピーされて、対象のPythonで実行されます。そのため状態を変えられ、変えられるということは、冪等性とチェックモードを自分で責任を持って扱わなければならないということです。
ディレクトリとクラスの規約
Ansibleは、設定ファイルに登録するよう求めません。その代わり、場所と名前で探します。
플레이북 옆:
filter_plugins/ → class FilterModule 의 filters() 가 돌려주는 사전
test_plugins/ → class TestModule 의 tests() 가 돌려주는 사전
lookup_plugins/ → class LookupModule(LookupBase) 의 run()
library/ → 모듈. 파일 이름이 곧 모듈 이름
컬렉션 안:
plugins/filter/ plugins/test/ plugins/lookup/ plugins/modules/
ここで、2つのことがよく人を引っかけます。1つは、ファイル名は自由でも、クラス名は規約だということです。FilterModuleでなければ、エラーもなく、ただ見つかりません。もう1つは、「プレイブックの隣」という言葉が文字どおりだということです。アドホックコマンド(ansible -m debug -a ...)では、この探索が行われないので、新しいフィルターをアドホックで試して「動かない」と結論を出してしまうことがよくあります。
コレクションの中に置くと、場所が変わります。filter_plugins/ではなくplugins/filter/で、呼び出すときは、네임스페이스.이름.필터(プレースホルダーはネームスペース、名前、フィルターです)のようなFQCNを使います。コレクションは、<검색경로>/ansible_collections/<네임스페이스>/<컬렉션이름>/(プレースホルダーは検索パス、ネームスペース、コレクション名です)という3階層の下になければならず、1階層でもずれていれば、やはり黙って見つかりません。
フィルターが守るべきこと
フィルターは、Jinja2が値を作るときに呼ぶ関数です。そのため、純粋関数でなければなりません。同じ入力に対して常に同じ値を返し、外の世界に触れてはいけません。理由は性能ではなく、予測可能性です。テンプレートは、いつ何回評価されるかが決まっていません。条件が付いたタスクでは評価されないこともあり、複数のホストでそれぞれ評価されることもあります。フィルターがファイルを書いたり時刻を読んだりすると、結果が実行のたびに変わり、そのタスクは永遠に冪等になれません。
不正な入力に出会ったときは、AnsibleFilterErrorで打ち切ります。黙って空文字列を返すフィルターは、もっと悪いです。誤った値がそのまま設定ファイルに入り、誰も気づかないからです。
ルックアップが起こす事故
ルックアップはコントローラーで動きます。そのためlookup('file', '/etc/app/secret')は、対象サーバーのそのファイルではなく、コントローラーのそのファイルを読みます。ドキュメントがこの点を太字で書いているのに、最もよく引っかかる場所です。開発者のノートPCでは動いていたのに、CIランナーでファイルがなくて失敗したり、逆にCIランナーのファイルが対象にデプロイされたりすることが、ここから起こります。
ルックアップは必ずリストを返します。with_による繰り返しが、もともとルックアップの上に作られた機能だからです。値が1つだけ必要なら、| firstを付けるか、query()ではなくlookup()を使います。
フィルターの代わりにモジュールを使うのはいつか
判断の基準は簡単です。
- 対象の状態を読む必要があるか → モジュール(またはファクト)。フィルターは見られません。
- 対象の状態を変える必要があるか → モジュール。フィルターがファイルを書けば、その時点で純粋関数ではなくなります。
- 値を変形するだけか → フィルター。
- 真偽1つで答えが出るか → テスト。
when:とselect/rejectでそのまま使われます。 - コントローラー側のデータを探してくる必要があるか → ルックアップ。
現場での姿
1つ目は、フィルター1つがチームの標準になることです。サービス名をスラッグに変えるフィルターを作ると、その後はKubernetesのラベルも、ファイル名も、ログのタグも、同じルールで作られます。ルールがコードの1か所に集まることが、この作業の本当の価値です。ルールを変えたいときに、直す場所が1つで済みます。
2つ目は、テストできるようになることです。プラグインはただのPythonファイルなので、Pythonのテストツールから直接呼び出せます。プレイブックの中のJinja2の1行は、そうはできません。ロジックが複雑になるほど、この差が大きくなります。
3つ目は、コレクションに移すと名前が変わることです。最初はfilter_plugins/に置いていても、複数のリポジトリで使うようになればコレクションに移すことになり、そのとき呼び出し側がすべてFQCNに変わります。最初から名前にチームのプレフィックスを付けておくと、移すときの衝突が減ります。
4つ目は、ユーザー定義モジュールは、思ったほど頻繁には必要にならないことです。社内のAPIを呼ぶ作業なら、uriモジュールでたいてい足ります。モジュールを自作する価値がある場面は、「何回呼んでも1回だけ変えなければならない複雑な状態」を扱うときです。そのときは、supports_check_modeを必ず宣言します。宣言しないと、--checkでそのタスクは失敗ではなく、黙ってスキップされます。計画を見るためにチェックモードを実行した人は、そのタスクが何をするのかを最後までわかりません。
参考ドキュメント
- プラグイン開発ガイド: https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html
- モジュールとプラグインをローカルに追加する: https://docs.ansible.com/ansible/latest/dev_guide/developing_locally.html
- ルックアッププラグイン: https://docs.ansible.com/ansible/latest/plugins/lookup.html
- コレクションのディレクトリ構造: https://docs.ansible.com/ansible/latest/dev_guide/developing_collections_structure.html
- モジュール開発ガイド: https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html
次のラボですること
4種類を自分で作り、1つのプレイブックでつなげます。filter_plugins/にスラッグのフィルターを作り、境界を2つ引数に受け取る2つ目のフィルターを加え、test_plugins/に真偽だけを返すテストを作り、lookup_plugins/にコントローラーのJSONレジストリを読むルックアップを作ります。同じフィルターをコレクションの中(plugins/filter/)に移してFQCNで呼び、最後にlibrary/にユーザー定義モジュールを作って対象で動かし、2回目の実行がchanged=0になるか、チェックモードでファイルが作られないかまで確認します。