Ansible Runner と Ansible Builder で Execution Environment を作って使う

はじめに

Ansible Automation Platform 2.0 がアーリーアクセスで提供されはじめ、次期メジャリリースの情報が出てきました。

目立つところでは、Ansible Tower が Ansible Automation Controller に改名されていますが、アーキテクチャ面でも、制御プレーンと実行プレーンを疎結合にするために Execution Environment(EE)の概念が新たに登場しています。

従来、プレイブックに応じて Python のモジュールや Collection を使い分けたい場合、典型的には Python の仮想環境を用いた環境の分離を行っていました。Execution Environment(EE)は、平たくいえばこれを コンテナに置き換えるもの であり、Ansible のランタイムをコンテナ化したもの と言えそうです。必ずしも Tower(AWX)と組み合わせなくても使えますが、Tower(AWX)目線でも、本体のインスタンスと実行環境が分離されるので、スケールもしやすくなりそうです。

本エントリでは、AWX での Execution Environment(EE)の動きを確かめるための準備として、Ansible RunnerAnsible Builder の動作を確認します。次のエントリ では、本エントリで作成した自前の Execution Environment(EE)を実際に AWX から利用します。

前提とする環境

本エントリでは、次の環境を前提に書いています。

  • CentOS 8.2
  • Python 3.9.2
  • Docker 20.10.7

ざっくりアーキテクチャ

Execution Environment の周辺には、Ansible RunnerAnsible Builder が登場します。荒く整理すると、たぶんこんな感じです。

  • Execution Environment
    • Ansible のランタイムを含んだ、Red Hat UBI(OSS 版は CentOS)ベースの コンテナイメージ
    • 追加したい Collection などの情報をテキストファイル群で定義して Ansible Builder でビルドすることで、独自の環境を作成できる
    • コンテナレジストリで Execution Environment を公開することで、他の環境で容易に再利用でき、環境間での動作の一貫性が得られるほか、スケールもしやすくなる
  • Ansible Builder
    • Execution Environment をビルドするツール
    • 雑に言うと、設定ファイル を基に Dockerfile を自動生成 して podman builddocker build)を実行してくれるツール
    • ベースイメージを指定すると、Ansible のバージョンを(ある程度)選べる
    • 任意の Galaxy の Collection や Role、Python モジュール(pip)、OS パッケージ(RPM)を含められる
    • ビルド中に任意の処理を追加できる(Dockerfile に RUN を追記できるイメージ)
  • Ansible Runner
    • プレイブックを Execution Environment 上で実行するためのツール
    • Execution Environment を使わずに ansible-playbook のラッパとしても利用できる

ビルドした Execution Environment はいわゆるコンテナイメージなので、任意のコンテナレジストリにプッシュできます。Ansible はコンテナの中で動くことになるため、従来のような Python の仮想環境の考慮が不要になるほか、環境の移植性(動作の一貫性)も高められ、またスケールもしやすくなります。

なお、コンテナイメージとしての Execution Environment を管理する目的で、Private Automation Hub がコンテナレジストリとしての役割も担えるようになるようですが、本エントリでは割愛します。

Ansible Builder を使う

だいたいの情報は 公式のドキュメント にまとまっています。

本エントリで使っているファイル群の実物は、GitHub に配置済み です。

インストール

ansible-builder だけあればよく、ansible はなくても動きます。

python3 -m pip install ansible-builder

必要なファイルの用意

ディレクトリをひとつ作って、execution-environment.yml としてメインの構成定義ファイルを作成します。

mkdir builder
cd builder
cat <<EOF > execution-environment.yml
---
version: 1

build_arg_defaults:
  EE_BASE_IMAGE: quay.io/ansible/ansible-runner:stable-2.10-devel

ansible_config: ansible.cfg

dependencies:
  galaxy: requirements.yml
  python: requirements.txt
  system: bindep.txt

additional_build_steps:
  prepend:
    - RUN whoami
    - RUN cat /etc/os-release
  append:
    - RUN echo This is a post-install command!
    - RUN ls -la /etc
EOF

ベースイメージは、無指定の場合はデフォルトで quay.io/ansible/ansible-runner:latest になるようで、現時点では lateststable-2.11-devel と同義です。選択肢は quay.io/ansible/ansible-runner のタグ一覧ページ で確認できますが、2.92.10 も用意されていて、上記例では違いを分かりやすくするため 2.10 を指定しています。利用する Ansible のバージョンを変更したい場合は、ベースイメージを差し替えるのが無難そうですね。

ほか、ansible.cfgrequirements.ymlrequirements.txtbindep.txt は必要に応じて指定します。これらは各ツールにそのまま渡されるため、中の書式はそれぞれの標準に従います。ここでは省略しますが、実際の例を GitHub に配置済み です。

additional_build_steps では、コンテナイメージのビルドプロセス中に任意の処理を追加できます。具体的には、生成される Dockerfile の所定の位置(pip installdnf install の前後)に任意の行を追記できます。追記される位置は実際に生成された Dockerfile(後述)の中身を見るのが確実です。

カスタマイズ済みの設定ファイルの例として、AWX に組み込まれている Execution Environment のソースである ansible/awx-ee も参考にできます。レイヤが重なりすぎるうえに副作用が予想できないのでおすすめはしませんが、自前でビルドするためのベースイメージとしても ビルド済みのイメージ quay.io/ansible/awx-ee も利用できそうでした。

Execution Environment のビルド

必要なファイルが用意できたら、ansible-builder build でビルドできます。引数で、できあがったイメージに付与するタグと、ビルドに利用するコンテナランタイム(デフォルトは podman で、今回は docker)を指定します。--verbosity-v)は任意ですが、無いとビルドの過程が表示されないようです。

$ ansible-builder build --tag registry.example.com/ansible/ee:2.10-custom --container-runtime docker --verbosity 3
Ansible Builder is building your execution environment image, "registry.example.com/ansible/ee:2.10-custom".
File context/_build/requirements.yml will be created.
File context/_build/requirements.txt will be created.
File context/_build/bindep.txt will be created.
File context/_build/ansible.cfg will be created.
Rewriting Containerfile to capture collection requirements
Running command:
  docker build -f context/Dockerfile -t registry.example.com/ansible/ee:2.10-custom context
Sending build context to Docker daemon   7.68kB
Step 1/25 : ARG EE_BASE_IMAGE=quay.io/ansible/ansible-runner:stable-2.10-devel
Step 2/25 : ARG EE_BUILDER_IMAGE=quay.io/ansible/ansible-builder:latest
Step 3/25 : FROM $EE_BASE_IMAGE as galaxy
...
Removing intermediate container a083001a665a
 ---> 050cf7076379
Successfully built 050cf7076379
Successfully tagged registry.example.com/ansible/ee:2.10-custom

Complete! The build context can be found at: /home/********/awx-on-k3s/builder/context

できた Execution Environment は通常のコンテナイメージなので、docker image ls で確認できます。

$ docker image ls
REPOSITORY                              TAG                 IMAGE ID       CREATED         SIZE
registry.example.com/ansible/ee         2.10-custom         6fb343319a80   2 minutes ago   871MB

このあと手作業で Ansible Runner から使うだけであれば、Execution Environment はここまでで完成です。

もしこの Execution Environment を AWX や外部のシステムから利用する要件がある場合は、コンテナレジストリにプッシュが必要です。今回は GitHub に配置済みのファイル を使って K3s 上にプライベートコンテナレジストリを作成しているので、このままプッシュしました。

$ docker push registry.example.com/ansible/ee:2.10-custom
The push refers to repository [registry.example.com/ansible/ee]
...
2.10-custom: digest: sha256:0138445c58253c733f2e255b618469d9f61337901c13e3be6412984fd835ad55 size: 3880

Dockerfile の確認

ビルドの過程で生成された Dockerfile は、カレントディレクトリの context ディレクトリに保存されています。additional_build_steps で指定したコマンドの追加されっぷりなどは、実物を見た方が確実です。

$ cat context/Dockerfile
ARG EE_BASE_IMAGE=quay.io/ansible/ansible-runner:stable-2.10-devel
ARG EE_BUILDER_IMAGE=quay.io/ansible/ansible-builder:latest

FROM $EE_BASE_IMAGE as galaxy
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS=
USER root
...

なお、イメージのビルドは行わずに Dockerfile の生成だけを行いたい場合は、ansible-builder create コマンドが利用できます。

$ ansible-builder create --verbosity 3
Ansible Builder is generating your execution environment build context.
File context/_build/requirements.yml will be created.
File context/_build/requirements.txt will be created.
File context/_build/bindep.txt will be created.
File context/_build/ansible.cfg will be created.
Rewriting Containerfile to capture collection requirements
Complete! The build context can be found at: /home/********/awx-on-k3s/builder/context

Ansible Runner を使う

これもだいたいの情報は 公式のドキュメント にまとまっています。

本エントリで使っているファイル群の実物は、GitHub に配置済み です。

インストール

Execution Environment を利用する(≒ Ansible をコンテナで動かす)だけであれば、ansible-runner だけあればよく、ansible はなくても動きます。ansible-playbook コマンドのラッパとして利用する場合は、当然ながら ansible も必要です。

python3 -m pip install ansible-runner

プレイブックの用意

ディレクトリをひとつ作って、必要なファイルを配置していきます。実物は GitHub に配置済み ですが、ここではサンプルとして Ansible の実行環境の構成が判断しやすいプレイブックにしています。

プレイブックは project ディレクトリに配置します。

mkdir -p runner/project
cat <<EOF > runner/project/demo.yml
---
- hosts: localhost
  connection: local

  tasks:
    - name: Ensure that the host is reachable
      ansible.builtin.ping:

    - name: Print variables for debugging
      ansible.builtin.debug:
        var: data
      vars:
        data:
          ansible_playbook_python: "{{ ansible_playbook_python }}"
          ansible_python_version: "{{ ansible_python_version }}"
          ansible_python.executable: "{{ ansible_python.executable }}"
          ansible_version.full: "{{ ansible_version.full }}"

    - name: Invoke debug commands
      ansible.builtin.command: "{{ item }}"
      changed_when: false
      failed_when: false
      loop:
        - hostname
        - whoami
        - pwd
        - python3 -m pip list
        - ansible-galaxy collection list -p .
      register: command_results
    - ansible.builtin.debug:
        msg: "{{ item.stdout_lines }}"
      loop: "{{ command_results.results }}"
      loop_control:
        label: "{{ item.cmd }}"
EOF

ansible-playbook のラッパとして使う

Ansible Runner は、ローカルの ansible-playbook のラッパとしても利用できます。素の状態(引数で設定を与えず、設定ファイルも用意しなかった場合)ではこの動作です。この場合、Execution Environment の概念はまったく登場しません

$ ansible-runner run runner -p demo.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'

PLAY [localhost] **************************************************************
...
PLAY RECAP *********************************************************************
localhost                  : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

このパタンでは、ansible-playbook コマンドを手打ちする状態とほとんど変わらず、localhostansible-runner を実行したホストそれ自体 を指します。つまり、従来通りの意味の localhost で、何ら特別なことはなく、出力された hostnamewhoamipip listansible-galaxy collection list などの実行結果は、見慣れた自ホストのものになるはずです。サンプルには含めていませんが、localhost に対する ansible.builtin.yumansible.builtin.copy なども、意図通りに自ホストに対して 実行されます。

なお、実行結果のログ実行されたコマンド は、自動で artifacts ディレクトリに保存 されます。何もせずとも相当詳細に残るので、これまで ansible.cfg などでいろいろしないといけなかったログ取得はあまり気にしなくてもよくなりそうです。ただし、何もしないと自動で削除はされずに増え続けるので、適宜 --rotate-artifacts 10 など保持世代数を指定して掃除する必要があります。

プレイブックを Execution Environment で実行する

ansible-runner--process-isolation を引数で与えるか、設定ファイル env/settingsprocess_isolationtrue にして、--process-isolation-executableprocess_isolation_executable)で docker(または podman)を指定すると、Execution Environment を使ってコンテナ上でプレイブックを実行できます。

mkdir runner/env
cat <<EOF > runner/env/settings
---
process_isolation: true
process_isolation_executable: docker
container_image: registry.example.com/ansible/ee:2.10-custom
EOF

なお、--process-isolationprocess_isolation)を有効にして --process-isolation-executableprocess_isolation_executable)を指定しないと、デフォルトでは bwrap でのサンドボックス化が行われますが、個人的に bwrap になじみがないので割愛しています。

Execution Environment にはデフォルトで quay.io/ansible/ansible-runner:devel が利用されますが、引数 --container-image や設定 container_image で任意のものを指定できます。ここでは先の Ansible Builder の手順で自製した registry.example.com/ansible/ee:2.10-custom の利用を指定しています。

設定ファイルを用意したうえで ansible-runner を実行すると、指定した Execution Environment を使ってプレイブックが実行されます。この場合も 実行結果のログ実行されたコマンド は、自動で artifacts ディレクトリに保存 されます。

$ ansible-runner run runner -p demo.yml
...
PLAY [localhost] ***************************************************************
...
ok: [localhost] => {
    "data": {
        ...
        "ansible_version.full": "2.10.12rc1.post0"
    }
}
ok: [localhost] => (item=['python3', '-m', 'pip', 'list']) => {
    "msg": [
        "Package              Version",
        "-------------------- ----------------",
        ...
        "example-pypi-package 0.1.0",
        ...
ok: [localhost] => (item=['ansible-galaxy', 'collection', 'list', '-p', '.']) => {
    "msg": [
        "",
        "# /usr/share/ansible/collections/ansible_collections",
        "Collection        Version",
        "----------------- -------",
        "community.general 3.3.2  "
    ]
}
PLAY RECAP *********************************************************************
localhost                  : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

出力からは、以下のような動作が確認できるはずです。

  • Ansible のバージョンが、ビルド時に指定したベースイメージ(今回は 2.10 系)のものになっていること
  • pip listansible-galaxy collection list の結果に、ビルド時に指定したモノが含まれること

Execution Environment の切り替えは設定ファイルやコマンドライン引数で簡単に行えるため、紹介した例以外でもいろいろ試すと、指定した Execution Environment に応じた Ansible のバージョンやパッケージの有無の変化が確認できます。自製した Execution Environment だけでなく、今回のサンプルのように標準モジュール群だけで動くシンプルなプレイブックであれば、 ビルド用のベースイメージである quay.io/ansible/ansible-runner もそのまま Execution Environment として利用できるので、実際に試してみると楽しいですね。より汎用性の高い既成 Execution Environment としては、AWX に組み込まれている quay.io/ansible/awx-ee(パブリッククラウド関係のコレクションなどが導入済み)も利用できます。

コンテナの作られっぷりと connection: local の扱い

ところで、Execution Environment を使った場合、localhost は、自ホストではなくコンテナ自体を指す ことに注意が必要です。実際、プレイブックの出力結果から、ホスト名(hostname)や実行ユーザ(whoami)が見慣れないものに替わっていることが確認できるはずです。

上記の例では、Ansible は Docker のコンテナとして動作します。確認のため、プレイブックに ansible.builtin.pause を追加し、再度同様に ansible-runner を実行します(余談ですが、ansible.builtin.pause を使った入力は Ansible Runner 経由ではうまく動かないようです……)。

    - name: Simply pause for a while
      ansible.builtin.pause:
        minutes: 10

待ち時間中に、実際のコンテナの存在が確認できます。

$ docker ps -a
CONTAINER ID   IMAGE                                         COMMAND                  CREATED          STATUS          PORTS     NAMES
f0a7a89e5b28   registry.example.com/ansible/ee:2.10-custom   "entrypoint ansible-…"   11 seconds ago   Up 11 seconds             ansible_runner_9bbedba8-520a-4392-9db9-16f76aa597de

このコンテナを inspect すると、ansible-runner の実行時に指定したディレクトリが /runner にマウントされていることがわかります。

$ docker inspect ansible_runner_9bbedba8-520a-4392-9db9-16f76aa597de | jq .[0].Mounts
[
  {
    "Type": "bind",
    "Source": "/home/********/awx-on-k3s/runner",
    "Destination": "/runner",
    "Mode": "Z",
    "RW": true,
    "Propagation": "rprivate"
  },
  {
    "Type": "bind",
    "Source": "/etc/sample",
    "Destination": "/etc/sample",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  }
]

逆にいえば、コンテナとして動作しているので、マウントされた /runner 以外ホスト側のファイルや状態は Ansible からは不可視 です。

このため、connection: locallocalhost に作用するプレイブック は、/runner 下以外は コンテナに閉じた揮発的な作用 にしかならず、意図した結果が得られない 点には注意が必要です。

この点は GitHub の Issue でも課題提起がされていて、今後動作が変わる可能性がありますが、現時点では、従来の意味での localhost に作用させたい場合、コンテナの中から自ホストに SSH させる 必要があります。

なお、次の設定を加えると、コンテナへのバインドマウントを追加でき、/runner 以外のパスも Ansible から作用させられるようになりますが、この設定はドキュメントに記載がない ため、将来的な取り扱いは不明です。

container_volume_mounts:
  - /etc/sample:/etc/sample

逆に、ドキュメントには process_isolation_show_paths の記載がありますが、ソースコードを見た範囲では、bwrap 専用のようで、Docker や Podman では使えませんでした。

おわりに

AWX で EE を使うために、前段として Ansible Builder と Ansible Runner の動作を確認しました。

これまでコンテナでの動作は積極的にはサポートされていませんでしたが、大手を振って使えるようになるわけで、たいへんうれしい進化です。環境の切り替えが用意になっただけでなく、複数の環境での状態の再現が容易になり、可搬性や動作の一貫性も保証しやすくなりました。これからは Playbook は生で叩かずに、できるだけ EE で叩いていきたいですね。

次のエントリ では、実際に AWX で EE を利用します。

@kurokobo

くろいです。ギターアンサンブルやら音響やらがフィールドの IT やさんなアルトギター弾き。たまこう 48 期ぎたさん、SFC '07 おんぞう、新日本ギターアンサンブル、Rubinetto。今は野良気味。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です