みなさんメリークリスマス。Red Hatのさいとうです。例年になく静かなクリスマスを過しながら、このAnsible Advent Calendar最終日12月25日の記事を書いています。 2022年もAnsible界隈では大きな動きがいくつかありました。個人的にはコンテナ実行環境とPrivate AutomationHubに やられた 注力した1年でした。 毎年、Advent Calendarの最終日には、来年に向けて僕が「来そう」だと予想している技術をご紹介しているのですが、来年、2023年に来そうなのは「Ansible SDK」です。 Ansible SDK Ansible SDKは、2022年に本格的な活動が始まった最新のプロジェクトの中の1つです。ソースコードの最初のコミットは2022年7月26日ですね。 commit 318bde98585223f00842a8a1605f0f1d6089dd21 Author: Abhijeet Kasurde <akasurde@redhat.com> Date: Tue Jul 26 19:14:22 2022 +0530 Initial commit CI/CDなどのシステムからAnsibleを利用しようとすると、これまではansible-playbookコマンドを実行し、その実行結果をハンドリングするというのが一般的な利用方法でした。Ansibleはシンプルなコマンドラインツールですから、すでに稼動しているJenkinsなどのシステムに組み込むには、このansible-playbookコマンドを実行する仕組み開発し、プラグインとして組み込むというのが一般的な利用方法ではないでしょうか。 Ansible SDKは、このようなコマンド実行という仕組みではなく、よりプログラマブルでキメ細かい制御を実現したいというパワーユーザ向けのSDKです。Ansibleを外部システムから便利に利用するため機能、たとえば実行環境(ネイティブ環境、コンテナ環境)の切り替えといった機能をまとめてSDKとして提供しています(していく予定だと思います)。 Ansible SDKの ソースコードリポジトリ Ansible SDKの デモ Ansible SDKの ドキュメント これまでのAnsible Python API Ansibleは、ansible-playbookやansible-galaxyなどのシンプルなコマンドラインツール群で構成されていますが、それらのコマンドラインツールが内部で利用しているPython APIを、外部プログラムから利用できるようPython APIも提供しています。誰も使ってないけれど。 例えば、pingモジュールを2回(うち1回はdata='crash'で失敗させる)をlocalhostとserver00(こっちは未到達)に実行するようなPlaybookを、Python APIを利用して書くと以下のようになります。この例では、既存のPlaybookのロードなどの処理は書いておらず、必要最低限の処理としてcallback程度しか書いていませんが、これは...わざわざPython APIを使用するよりも、普通にansible-playbookコマンドで叩いたほうが確かに良さそうです。 #!/usr/bin/env python import json import shutil import ansible.constants as C from ansible.executor.task_queue_manager import TaskQueueManager from ansible.module_utils.common.collections import ImmutableDict from ansible.inventory.manager import InventoryManager from ansible.parsing.dataloader import DataLoader from ansible.playbook.play import Play from ansible.plugins.callback import CallbackBase from ansible.vars.manager import VariableManager from ansible import context class ResultsCollectorJSONCallback (CallbackBase): def __init__ (self, *args, **kwargs): super (ResultsCollectorJSONCallback, self).__init__(*args, **kwargs) self.host_ok = {} self.host_unreachable = {} self.host_failed = {} def v2_runner_on_unreachable (self, result): host = result._host self.host_unreachable[host.get_name()] = result def v2_runner_on_ok (self, result, *args, **kwargs): host = result._host self.host_ok[host.get_name()] = result print (json.dumps({host.name: result._result}, indent= 4 )) def v2_runner_on_failed (self, result, *args, **kwargs): host = result._host self.host_failed[host.get_name()] = result def main (): host_list = [ 'localhost' , 'server00' ] context.CLIARGS = ImmutableDict(connection= 'smart' ) sources = ',' .join(host_list) if len (host_list) == 1 : sources += ',' loader = DataLoader() passwords = dict () results_callback = ResultsCollectorJSONCallback() inventory = InventoryManager(loader=loader, sources=sources) variable_manager = VariableManager(loader=loader, inventory=inventory) tqm = TaskQueueManager( inventory=inventory, variable_manager=variable_manager, loader=loader, passwords=passwords, stdout_callback=results_callback, ) play_source = dict ( name= "Ansible Play" , hosts=host_list, gather_facts= 'no' , tasks=[ dict (action= dict (module= 'ping' )), dict (action= dict (module= 'ping' , args= dict (data= 'crash' ))), ] ) play = Play().load(play_source, variable_manager=variable_manager, loader=loader) try : result = tqm.run(play) finally : tqm.cleanup() if loader: loader.cleanup_all_tmp_files() shutil.rmtree(C.DEFAULT_LOCAL_TMP, True ) print ( "UP ***********" ) for host, result in results_callback.host_ok.items(): print ( '{0} >>> {1}' .format(host, result._result[ 'ping' ])) print ( "FAILED *******" ) for host, result in results_callback.host_failed.items(): print ( '{0} >>> {1}' .format(host, result._result[ 'msg' ])) print ( '{0} >>> STDOUT:{1}' .format(host, result._result[ 'module_stdout' ])) print ( '{0} >>> STDERR:{1}' .format(host, result._result[ 'module_stderr' ])) print ( "DOWN *********" ) for host, result in results_callback.host_unreachable.items(): print ( '{0} >>> {1}' .format(host, result._result[ 'msg' ])) if __name__ == '__main__' : main() 実行してみると、成功、失敗、未到達といった状況もハンドリングでき、実用としては十分ではありますが、やはりちょっと手間がかかりますね。 $ python example_api.py { "localhost": { "ping": "pong", "invocation": { "module_args": { "data": "pong" } }, "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "warnings": [ "Platform darwin on host localhost is using the discovered Python interpreter at /usr/bin/python3, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.14/reference_appendices/interpreter_discovery.html for more information." ], "_ansible_no_log": null, "changed": false } } UP *********** localhost >>> pong FAILED ******* localhost >>> MODULE FAILURE See stdout/stderr for the exact error localhost >>> STDOUT:Traceback (most recent call last): File "/Users/hsaito/.ansible/tmp/ansible-tmp-1671962136.749241-66036-54422040219731/AnsiballZ_ping.py", line 107, in <module> _ansiballz_main() File "/Users/hsaito/.ansible/tmp/ansible-tmp-1671962136.749241-66036-54422040219731/AnsiballZ_ping.py", line 99, in _ansiballz_main invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS) File "/Users/hsaito/.ansible/tmp/ansible-tmp-1671962136.749241-66036-54422040219731/AnsiballZ_ping.py", line 47, in invoke_module runpy.run_module(mod_name='ansible.modules.ping', init_globals=dict(_module_fqn='ansible.modules.ping', _modlib_path=modlib_path), File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 210, in run_module return _run_module_code(code, init_globals, run_name, mod_spec) File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 97, in _run_module_code _run_code(code, mod_globals, init_globals, File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code exec(code, run_globals) File "/var/folders/kb/tk3jg2mn4m1535zd5zr4lnkc0000gn/T/ansible_ping_payload_4qlm0wra/ansible_ping_payload.zip/ansible/modules/ping.py", line 89, in <module> File "/var/folders/kb/tk3jg2mn4m1535zd5zr4lnkc0000gn/T/ansible_ping_payload_4qlm0wra/ansible_ping_payload.zip/ansible/modules/ping.py", line 79, in main Exception: boom localhost >>> STDERR:Shared connection to localhost closed. DOWN ********* server00 >>> Failed to connect to the host via ssh: ssh: connect to host server00 port 22: Network is unreachable Ansible Python APIの ドキュメント Ansible SDKの良いところ 従来のPython APIは、Ansible Coreが提供するAPIを利用しています。つまり、Ansible Runnerが提供するような、Playbook実行環境の切り替え(ネイティブ実行環境とコンテナ実行環境の切り替え)や、Receptorを経由したPlaybook実行環境の延伸といった、エンタープライズ環境で求められるような魅力的な機能を利用することができませんでした。 Ansible SDKでは、自動化フレームワークの中でAnsible Runnerを利用しています。また、実行環境の延伸手段としてReceptorを利用することができます。これは、Red Hatが提供しているAnsible automation controllerと同じ仕組みであるため、「コンテナ環境でPlaybookを実行したい」、「遠隔地にある実行環境を利用してPlaybookを実行したい」といったリクエスにも対応することができます(仮に今はできなくても近い将来可能となるはずです)。 Ansible SDKによるPlaybookの実行 Ansible SDKを利用するには、以下のコンポーネントが必要です。あらかじめインストールしておきましょう。SDKは、ソースコードリポジトリをcloneして、最新版を利用します。 $ python -m venv venv/ansible-sdk $ source venv/ansible-sdk/bin/activate (ansible-sdk)$ pip install -U pip (ansible-sdk)$ pip install ansible-core (ansible-sdk)$ pip install ansible-runner (ansible-sdk)$ pip install receptorctl (ansible-sdk)$ git clone git://github.com/ansible/ansible-sdk (ansible-sdk)$ cd ansible-sdk (ansible-sdk)$ pip install -e . Ansible SDKでは、Ansible Runnerを利用してPlaybookを実行するためのフレームワークを提供します。では、pingモジュールを実行してみましょう。以下のコードを example00.py として保存します。 #!/usr/bin/env python import asyncio from tempfile import mkdtemp from ansible_sdk import AnsibleJobDef from ansible_sdk.executors import AnsibleSubprocessJobExecutor, AnsibleSubprocessJobOptions async def main (): executor = AnsibleSubprocessJobExecutor() executor_options = AnsibleSubprocessJobOptions() tmp_data_dir = mkdtemp() jobdef = AnsibleJobDef( data_dir=tmp_data_dir, playbook= None , module= 'ping' , host_pattern= 'localhost' ) job_status = await executor.submit_job(jobdef, executor_options) async for line in job_status.stdout_lines: print (line) await job_status if __name__ == '__main__' : asyncio.run(main()) 実行してみると、たしかにad-hocコマンド( ansible localhost -m ping )と同様の結果が得られました。前述の通りAnsible SDKは、自動化のフレームワークとしてAnsible Runnerを使用しているので、Runnerの流儀にしたがって /tmp/ に、data_dir(今回は空)とartifactsが保存されるワーキングディレクトリが作成されているはずです。 (ansible-sdk)$ python example00.py [WARNING]: No inventory was parsed, only implicit localhost is available localhost | SUCCESS => { "changed": false, "ping": "pong" } (ansible-sdk)$ ansible localhost -m ping [WARNING]: No inventory was parsed, only implicit localhost is available localhost | SUCCESS => { "changed": false, "ping": "pong" } (ansible-sdk)$ find /tmp/tmp* /tmp/tmp64v1c63m /tmp/tmpl3r13t33 /tmp/tmpl3r13t33/artifacts /tmp/tmpl3r13t33/artifacts/110fb364-db2b-480e-8225-ff32838abd86 /tmp/tmpl3r13t33/artifacts/110fb364-db2b-480e-8225-ff32838abd86/rc /tmp/tmpl3r13t33/artifacts/110fb364-db2b-480e-8225-ff32838abd86/status /tmp/tmpl3r13t33/artifacts/110fb364-db2b-480e-8225-ff32838abd86/fact_cache /tmp/tmpl3r13t33/artifacts/110fb364-db2b-480e-8225-ff32838abd86/stderr /tmp/tmpl3r13t33/artifacts/110fb364-db2b-480e-8225-ff32838abd86/stdout /tmp/tmpl3r13t33/artifacts/110fb364-db2b-480e-8225-ff32838abd86/command /tmp/tmpl3r13t33/artifacts/110fb364-db2b-480e-8225-ff32838abd86/job_events Ansible SDKからコンテナ実行環境を利用する Ansible Runnerを利用しているということは、前述のad-hocコマンドやPlaybookの実行時にDocker/podmanを利用したコンテナベースの実行環境が利用できるということです。そこで、アップストリームにある サンプルコード を参考に、コンテナ実行環境でad-hocコマンドを実行するコードを書いて example01.py として保存します。 #!/usr/bin/env python import asyncio from tempfile import mkdtemp from ansible_sdk import AnsibleJobDef from ansible_sdk.executors import AnsiblePodmanJobExecutor, AnsiblePodmanJobOptions async def main (): executor = AnsiblePodmanJobExecutor() executor_options = AnsiblePodmanJobOptions(container_image_ref= 'quay.io/ansible/ansible-runner:devel' ) tmp_data_dir = mkdtemp() jobdef = AnsibleJobDef( data_dir=tmp_data_dir, playbook= None , module= 'pause' , module_args= 'seconds=180' , host_pattern= 'localhost' ) job_status = await executor.submit_job(jobdef, executor_options) async for line in job_status.stdout_lines: print (line) await job_status if __name__ == '__main__' : asyncio.run(main()) example01.py では、ansible-runnerコンテナ(quay.io/ansible/ansible-runner:devel)を利用して、ad-hocコマンドを実行しています。awx-eeイメージが存在しないことを確認してから、 example01.py を実行してみましょう。 (ansible-sdk)$ podman images REPOSITORY TAG IMAGE ID CREATED SIZE (ansible-sdk)$ python example01.py [WARNING]: You are running the development version of Ansible. You should only run Ansible from "devel" if you are modifying the Ansible engine, or trying out features under development. This is a rapidly changing source of code and can become unstable at any point. [WARNING]: Unable to parse /runner/inventory/hosts as an inventory source [WARNING]: No inventory was parsed, only implicit localhost is available Pausing for 180 seconds (ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort) localhost | SUCCESS => { "changed": false, "delta": 180, "echo": true, "rc": 0, "start": "2022-12-25 09:04:33.396725", "stderr": "", "stdout": "Paused for 180.0 seconds", "stop": "2022-12-25 09:07:33.397015", "user_input": "" } pauseモジュールの実行中に、別ターミナルをオープンしてコンテナの実行状況を確認してると、コンテナ実行環境を利用してad-hocコマンドとしてpauseモジュールが実行されているのがわかります。 $ podman ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 076655c80b00 quay.io/ansible/ansible-runner:devel ansible -i /runne... About a minute ago Up About a minute ago ansible_runner_3cff6702-dab0-47ce-9879-748d950e1442 $ podman inspect 076655c80b00 [ { "Id": "076655c80b00fdb9348be9bfb61d6193bfd8504e5e006bd0f935ad7dddc11944", "Created": "2022-12-25T09:04:31.419783404Z", "Path": "entrypoint", "Args": [ "ansible", "-i", "/runner/inventory/hosts", "-m", "pause", "-a", "seconds=180", "localhost" ], ...省略... "Cmd": [ "ansible", "-i", "/runner/inventory/hosts", "-m", "pause", "-a", "seconds=180", "localhost" ], "Image": "quay.io/ansible/ansible-runner:devel", ...省略... "CreateCommand": [ "/usr/bin/podman", "run", "--rm", "--tty", "--interactive", "--workdir", "/runner/project", "-v", "/tmp/tmpu4bv0901/:/runner/:Z", "--env-file", "/tmp/tmpu4bv0901/artifacts/3cff6702-dab0-47ce-9879-748d950e1442/env.list", "--quiet", "--name", "ansible_runner_3cff6702-dab0-47ce-9879-748d950e1442", "quay.io/ansible/ansible-runner:devel", "ansible", "-i", "/runner/inventory/hosts", "-m", "pause", "-a", "seconds=180", "localhost" ], ...以下略... 2023年のAnsibleとわたし 2023年、オペレーションの自動化を提供するエコシステムの一部としてAnsibleを利用しており、かつRed Hat Ansible automation controllerやAWXのような本格的な自動化フレームワークを必要としないCI/CDなどのシステムは、徐々にこのSDKを利用したものに移行しはじめるのではないかと予想しています。 2022年のFestやContributor Meetupで紹介されて、2023年に大きく飛躍しそうなプロジェクトは他にもありますが、このような新しいプロジェクトには、我々のようなコントリビューターが開発に参加するチャンスが多くあります。Ansible SDKをみなさんのエコシステムを支える自動化基盤技術の1つとして検討してみてください。 それでは、みなさんメリークリスマス & ハッピーオートメーション! :)