フィルターが無くて shell で誤魔化していたものをプラグインにする
目標
Jinja2のフィルター・テスト・ルックアップと、ユーザー定義モジュールを自分で作り、プレイブックから呼び出します。4つがそれぞれどこで実行されるのか、その場所の違いが何をできるようにし、何をできなくするのかを、コードで確認します。
なぜ重要なのか
プレイブックが大きくなると、どのチームにも同じ場所ができます。名前を整え、数字を分類し、どこかの表を調べる作業です。標準のフィルターでできなければ、人々はshellとsedで済ませ、その瞬間、そのタスクは冪等性もチェックモードも失います。プラグインは、その場所のためのものです。ただし、どこにでも入れればよいわけではなく、どこで動くかが設計の半分です。フィルター・テスト・ルックアップはコントローラーで動くので、対象に何もインストールする必要がない代わりに、対象の状態を見られません。モジュールは対象にコピーされてそこで動くので、状態を変えられる代わりに、冪等性とチェックモードを自分で責任を持って扱わなければなりません。このラボは、その境界を4回行き来しながら、手で確認します。
ステップ
/root/ansplug/ansible.cfgを作成し、デフォルトのインベントリを./inventory/hosts.iniに指定してください。/root/ansplug/inventory/hosts.iniには3つのグループを書きます。webにweb1(svc_port 8080、svc_nameWeb Front 01)とweb2(9090、Web Front 02)、dbにdb1(5432、Main DB!)、edgeにcache1(443、Edge Cache)です。4つのホストすべてがansible_host=127.0.0.1、ansible_port=2222で、[all:vars]のansible_userはrootです。そのあと、ansible all -m ansible.builtin.ping -oを実行して、標準出力と標準エラー出力を一緒に/root/ansplug/out/ping.txtに保存してください。/root/ansplug/filter_plugins/labfilters.pyを作成して、slugifyフィルターを1つ登録してください。このフィルターは、文字列を小文字にし、英数字以外の文字をすべて-に置き換え、連続する-を1つにまとめ、両端の-を取り除きます。文字列以外の値が入ってきたら、AnsibleFilterErrorを送出します。そのあと、/root/ansplug/slug.ymlを作成して、'Web Server 01!!'をこのフィルターに入れた結果を/root/ansplug/out/slug.txtに書き、実行してください。- 同じ
/root/ansplug/filter_plugins/labfilters.pyにport_classフィルターを加えてください。port_class(value, privileged_below=1024, dynamic_from=49152)は、ポートがprivileged_belowより小さければsystem、dynamic_from以上ならdynamic、その間ならuserを返し、整数でなければAnsibleFilterErrorを送出します。そのあと、/root/ansplug/ports.ymlを作成して、/root/ansplug/out/ports.txtに、インベントリの4つのホストを名前の昇順で、<호스트> <포트> <분류>(プレースホルダーは順にホスト、ポート、分類です)の形式で1行ずつ書き、実行してください。 /root/ansplug/test_plugins/labtests.pyを作成して、reserved_portテストを登録してください。ポート番号が1024より小さければ真です。そのあと、/root/ansplug/reserved.ymlを作成して、/root/ansplug/out/reserved.txtに、4つのホストを名前の昇順で、<호스트> <포트> <reserved|free>(プレースホルダーは順にホストとポートです)の形式で1行ずつ書き、実行してください。判定は必ずis reserved_portの形で呼び出します。/root/ansplug/registry.jsonに{"web": "seoul-a", "db": "seoul-b", "edge": "seoul-c"}を書いてください。/root/ansplug/lookup_plugins/labregistry.pyを作成して、labregistryルックアップを定義します。キーを受け取ってレジストリの値を返し、registry=キーワード引数で別のファイルを指定でき(デフォルトは/root/ansplug/registry.json)、ファイルがないかキーがなければAnsibleErrorを送出します。そのあと、/root/ansplug/regions.ymlで、/root/ansplug/out/regions.txtに、db・edge・webの3つのグループの<그룹> <지역>(プレースホルダーは順にグループとリージョンです)を、この順序で書き、実行してください。/root/ansplug/collections/ansible_collections/labhub/site/にコレクションを置いてください。galaxy.ymlのnamespaceはlabhub、nameはsiteで、フィルターのファイルはplugins/filter/の下に置きます(ステップ2・3で作ったものをそのままコピーすれば構いません)。/root/ansplug/ansible.cfgにcollections_path = ./collectionsを加え、/root/ansplug/fqcn.ymlで、'Prod DB 02!!'をlabhub.site.slugifyに入れた結果を/root/ansplug/out/fqcn.txtに書き、実行してください。/root/ansplug/library/lab_marker.pyにユーザー定義モジュールlab_markerを作成してください。引数はpathとcontentで、ファイルの内容がすでに同じならchanged=false、違えばファイルを書いてchanged=trueで終わります。supports_check_mode=Trueを宣言し、チェックモードでは書き込みません。/root/ansplug/marker.ymlで、webグループにこのモジュールを実行して、/root/ansplug/out/<호스트이름>.marker(プレースホルダーはホスト名です)にそのホスト名を1行書くようにし、プレイブックを2回実行して、2回目の実行の出力を/root/ansplug/out/marker_run2.txtに保存してください。/root/ansplug/report.ymlで、/root/ansplug/out/report.txtに4つのホストを名前の昇順で1行ずつ書いてください。形式は<호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역>(プレースホルダーは順にホスト、名前のスラッグ、ポート、ポートの分類、リージョンです)で、名前のスラッグはsvc_nameをslugifyした値、ポートの分類はport_class、5つ目の列はreserved_portテスト、リージョンはそのホストの最初のグループ名をlabregistryで引いた値です。プレイブックを2回実行して、2回目の実行の出力を/root/ansplug/out/report_run2.txtに保存してください。
参考
filter_plugins/・test_plugins/・lookup_plugins/・library/は、プレイブックの隣にないと見つかりません。そのため、このラボのプレイブックはすべて/root/ansplugの直下に置きます。- sshdは127.0.0.1の2222で動いています。インベントリの4つのホストは、名前が違うだけで、すべて同じサーバーに接続します。
- 呼び出し方は種類ごとに違います。フィルターは
값 | 이름、テストは값 is 이름、ルックアップはlookup('이름', 인자)、モジュールはタスクのキーです(韓国語の部分はプレースホルダーで、順に値と名前、値と名前、名前と引数です)。 - よくある間違い: クラス名を
FilterModule以外にして、「なぜ見つからないのか」と悩むことです。ファイル名は自由ですが、クラス名は規約です。 - よくある間違い: アドホックコマンドで新しいフィルターを試して、動かないと判断することです。隣のディレクトリの探索は、プレイブックにだけ行われます。
- よくある間違い: コレクションを
ansible_collections/の階層なしで置くことです。エラーは出ず、ただ見つかりません。 - Developing plugins・Adding modules and plugins locally・Lookup plugins・Collection structure・Developing modules
4台を模倣するインベントリを最初に立てる
/root/ansplug/ansible.cfgを作成し、デフォルトのインベントリを./inventory/hosts.iniに指定してください。/root/ansplug/inventory/hosts.iniには3つのグループを書きます。webにweb1(svc_port 8080、svc_name Web Front 01)とweb2(9090、Web Front 02)、dbにdb1(5432、Main DB!)、edgeにcache1(443、Edge Cache)です。4つのホストすべてがansible_host=127.0.0.1、ansible_port=2222で、[all:vars]のansible_userはrootです。そのあと、ansible all -m ansible.builtin.ping -oを実行して、標準出力と標準エラー出力を一緒に/root/ansplug/out/ping.txtに保存してください。
このPodのsshdは127.0.0.1の2222で動いていて、鍵認証ができます。インベントリに名前を複数書いても、すべて同じsshdに接続するので、「複数台」を模倣できます。INIインベントリでは、値に空白が入る場合は、二重引用符で囲みます。マージされた結果はansible-inventory --listで確認します。
最初のフィルター: コントローラーで動く純粋関数
/root/ansplug/filter_plugins/labfilters.pyを作成して、slugifyフィルターを1つ登録してください。このフィルターは、文字列を小文字にし、英数字以外の文字をすべて-に置き換え、連続する-を1つにまとめ、両端の-を取り除きます。文字列以外の値が入ってきたら、AnsibleFilterErrorを送出します。そのあと、/root/ansplug/slug.ymlを作成して、'Web Server 01!!'をこのフィルターに入れた結果を/root/ansplug/out/slug.txtに書き、実行してください。
Ansibleは、filter_plugins/の中のPythonファイルからFilterModuleという名前のクラスを探し、そのクラスのfilters()が返すディクショナリのキーを、フィルター名として使います。このディレクトリはプレイブックの隣にないと見つかりません。アドホックコマンドでは見つけられません。フィルターはコントローラーで動くので、対象に何もインストールする必要がなく、だからこそなおさら、副作用のない純粋関数でなければなりません。
引数を受け取るフィルターと、不正な入力を打ち切る場所
同じ/root/ansplug/filter_plugins/labfilters.pyにport_classフィルターを加えてください。port_class(value, privileged_below=1024, dynamic_from=49152)は、ポートがprivileged_belowより小さければsystem、dynamic_from以上ならdynamic、その間ならuserを返し、整数でなければAnsibleFilterErrorを送出します。そのあと、/root/ansplug/ports.ymlを作成して、/root/ansplug/out/ports.txtに、インベントリの4つのホストを名前の昇順で、<호스트> <포트> <분류>(プレースホルダーは順にホスト、ポート、分類です)の形式で1行ずつ書き、実行してください。
Jinja2のフィルターは、パイプの左の値を最初の引数として受け取り、括弧の中に書いたものが次の引数になります。{{ 3000 | port_class(2000, 4000) }}のようにです。デフォルト値をPython側に置けば、ほとんどの呼び出しは引数なしで済み、特別な場所だけ境界を変えて呼び出せます。PythonではTrueがintのサブタイプなので、isinstance(x, int)を通過します。ブール値を先に除外する必要があります。すべてのホストを走査するのは、groups['all']とhostvars[...]です。
テストプラグイン: 真偽だけを返す場所
/root/ansplug/test_plugins/labtests.pyを作成して、reserved_portテストを登録してください。ポート番号が1024より小さければ真です。そのあと、/root/ansplug/reserved.ymlを作成して、/root/ansplug/out/reserved.txtに、4つのホストを名前の昇順で、<호스트> <포트> <reserved|free>(プレースホルダーは順にホストとポートです)の形式で1行ずつ書き、実行してください。判定は必ずis reserved_portの形で呼び出します。
テストは、フィルターとはクラス名もメソッド名も違います。TestModuleとtests()です。ディレクトリもtest_plugins/です。呼び出し方も違います。フィルターは값 | 이름、テストは값 is 이름(韓国語の部分はプレースホルダーで、順に値と名前、値と名前です)で、テストは必ず真か偽だけを返します。この区別がある理由は、when:とselect・rejectが、テストを前提に使われるからです。真偽でない値を返すと、その場で意味が崩れます。
ルックアップ: コントローラーのファイルを読んでくる場所
/root/ansplug/registry.jsonに{"web": "seoul-a", "db": "seoul-b", "edge": "seoul-c"}を書いてください。/root/ansplug/lookup_plugins/labregistry.pyを作成して、labregistryルックアップを定義します。キーを受け取ってレジストリの値を返し、registry=キーワード引数で別のファイルを指定でき(デフォルトは/root/ansplug/registry.json)、ファイルがないかキーがなければAnsibleErrorを送出します。そのあと、/root/ansplug/regions.ymlで、/root/ansplug/out/regions.txtに、db・edge・webの3つのグループの<그룹> <지역>(プレースホルダーは順にグループとリージョンです)を、この順序で書き、実行してください。
ルックアップはLookupBaseを継承したLookupModuleで、エントリポイントはrun(self, terms, variables=None, **kwargs)です。termsは、lookup('이름', 첫째, 둘째)(プレースホルダーは順に名前、1つ目、2つ目です)の引数のリストで、必ずリストを返す必要があります。kwargsで渡されるのが、registry=のようなキーワード引数です。重要なのは、このコードがコントローラーで動くという事実です。ルックアップが読むファイルは、対象サーバーではなく、プレイブックを実行する場所にある必要があります。
同じフィルターをコレクションの中へ移してFQCNで呼ぶ
/root/ansplug/collections/ansible_collections/labhub/site/にコレクションを置いてください。galaxy.ymlのnamespaceはlabhub、nameはsiteで、フィルターのファイルはplugins/filter/の下に置きます(ステップ2・3で作ったものをそのままコピーすれば構いません)。/root/ansplug/ansible.cfgにcollections_path = ./collectionsを加え、/root/ansplug/fqcn.ymlで、'Prod DB 02!!'をlabhub.site.slugifyに入れた結果を/root/ansplug/out/fqcn.txtに書き、実行してください。
コレクションの場所は、<검색경로>/ansible_collections/<네임스페이스>/<이름>/(プレースホルダーは検索パス、ネームスペース、名前です)と決まっています。この3階層が1つでもずれていると、エラーなしで見つかりません。コレクションの中でフィルターの場所は、filter_plugins/ではなくplugins/filter/です。テストはplugins/test/、ルックアップはplugins/lookup/、モジュールはplugins/modules/です。正しく見つけているかどうかは、ansible-config dump | grep COLLECTIONSで確認します。
モジュールは対象で動く: 冪等性とチェックモードは、そちらの責任
/root/ansplug/library/lab_marker.pyにユーザー定義モジュールlab_markerを作成してください。引数はpathとcontentで、ファイルの内容がすでに同じならchanged=false、違えばファイルを書いてchanged=trueで終わります。supports_check_mode=Trueを宣言し、チェックモードでは書き込みません。/root/ansplug/marker.ymlで、webグループにこのモジュールを実行して、/root/ansplug/out/<호스트이름>.marker(プレースホルダーはホスト名です)にそのホスト名を1行書くようにし、プレイブックを2回実行して、2回目の実行の出力を/root/ansplug/out/marker_run2.txtに保存してください。
フィルター・テスト・ルックアップと違い、モジュールは対象にコピーされて、そこで実行されます。そのため、コントローラーのファイルは読めない代わりに、状態を変えられます。変えられるということは、冪等性とチェックモードを自分で責任を持って扱わなければならないという意味です。モジュールの骨組みは、AnsibleModule(argument_spec=..., supports_check_mode=True)で引数を受け取り、module.exit_json(changed=...)で終えるものです。supports_check_modeを忘れると、--checkでそのタスクは丸ごとスキップされます。失敗ではなく沈黙なので、より危険です。モジュールは、プレイブックの隣のlibrary/で探されます。
4種類を1つのプレイブックでつなげる
/root/ansplug/report.ymlで、/root/ansplug/out/report.txtに4つのホストを名前の昇順で1行ずつ書いてください。形式は<호스트> <이름슬러그> <포트> <포트분류> <reserved|free> <지역>(プレースホルダーは順にホスト、名前のスラッグ、ポート、ポートの分類、リージョンです)で、名前のスラッグはsvc_nameをslugifyした値、ポートの分類はport_class、5つ目の列はreserved_portテスト、リージョンはそのホストの最初のグループ名をlabregistryで引いた値です。プレイブックを2回実行して、2回目の実行の出力を/root/ansplug/out/report_run2.txtに保存してください。
1つのホストが属するグループ名はhostvars[h].group_namesに入っていて、ここではその最初の要素が、そのままレジストリのキーになります。copyモジュールのcontentにJinja2の{% for %}ブロックをそのまま入れれば、複数行を一度に作れます。同じ入力ならファイルの内容が同じなので、2回目の実行はchanged=0である必要があります。これが、「レポートを作るプレイブック」が冪等であることの証拠です。