Receptor (4): AWX と Automation Mesh での Receptor の使われ方

はじめに

これまでの Receptor 関連のエントリ([1][2][3])では、Receptor 単体に注目して、その動作を紹介してきました。

今回は、Receptor のユースケースひとつとして、AWX での Receptor の使われ方 を、共に使われている Ansible Runner の動作と共に紹介します。併せて、Automation Mesh にも触れます。

AWX は、ジョブの実行、プロジェクトやインベントリの更新など、いくつもの処理を内部では Receptor のワークとして実行 しています。

ともすれば単なるオーバヘッドにも思えてしまうこの実装ですが、見方を変えれば、あえて Receptor を介して処理する実装 にすることで、メッシュネットワークを拡張しさえすればどのノードにもそれをオフロードできる ようになっているとも言えます。

そして、AWX における Execution Node が、まさにその例のひとつです。詳細は本エントリで紹介しますが、技術的には Execution Node は Receptor の Executor そのものです。すなわち、AWX が参加しているメッシュネットワーク に Executor を追加してワーク(≒ AWX のジョブ)の送信先を変更可能に する機能と解釈できます。

さらにこれが Ansible Automation Platform(AAP)では Automation Mesh として拡張され、Execution Node に加えて Hop Node も利用できるようになります。そしてこれも、技術的には AAP が参加する Receptor のメッシュネットワーク を ユーザが任意のノードで任意の構成にできる ようにして、ワークを任意の Executor に送れる ようにしたものです。

前提: Ansible Runner の機能

AWX の実装を紐解くうえで、Ansible Runner の機能の理解が不可欠です。昔のエントリ でも簡単に紹介はしましたが、ここでは、後続の説明の前提として、特に次の三つの機能を紹介します。

  • プレイブックの実行
  • リモートホストでのジョブの実行
  • Execution Environment(EE)の利用

以下、例で使っているファイル群は GitHub のリポジトリ に配置しています。

プレイブックの実行

Ansible Runner を単なる Ansible のラッパとして扱う、もっとも簡単な使い方です。

確認用に、以下の簡単なプレイブックを用意しました。

---
- hosts: localhost
  gather_facts: true

  tasks:
    - ansible.builtin.debug:
        var: dump
      vars:
        dump:
          hostname: "{{ ansible_facts.hostname }}"
          user_dir: "{{ ansible_facts.user_dir }}"
          user_id: "{{ ansible_facts.user_id }}"
          virtualization_type: "{{ ansible_facts.virtualization_type }}"
    - ansible.builtin.pause:
        seconds: "{{ wait_seconds | default(30) }}"

このプレイブックを demo.yml として次のように配置して ansible-runner コマンドを実行すると、プレイブックが実行されます。localhost をターゲットとしたプレイブックは、ansible-runner を実行したホストに対して実行されます。

$ cd 07_awx/runner
$ tree .
.
├── inventory
│   └── hosts
└── project
    └── demo.yml

2 directories, 2 files
$ ansible-runner run . -p demo.yml
...
TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
    "dump": {
        "hostname": "kuro-awx01",
        "user_dir": "/home/kuro",
        "user_id": "kuro",
        "virtualization_type": "VMware"
    }
}
...

リモートホストでのジョブの実行

Ansible Runner は、プレイブックの実処理をリモートホストに任せられる ようにする、次のようなリモートジョブ実行機能を持っています。

  • Transmit 機能
    • プレイブックの実行に必要な すべてのファイルと情報標準出力に吐き出す 機能
  • Worker 機能
    • Transmit 機能の出力を 標準入力から受け取り、中身に従って実際に プレイブックを実行 して 結果を標準出力に吐き出す 機能
  • Process 機能
    • Worker 機能の出力を 標準入力から受け取り、中身に従って 実行結果を整形して表示ログを保存 する機能

Transmit、Worker、Process の 機能間の連携は標準入出力 で行われます。以下の実行例はすべて同一のノードで実行したものですが、実際には SSH 越しでも Receptor 越しでもファイル経由でも 何らかの形で標準入出力の中身さえ連携 できれば、それぞれを別のホストで処理できる ことになります。

Transmit フェイズ

プレイブックの実行に必要な一切合切まとめて標準出力に吐く フェイズです。

実際に Transmit フェイズを実行し、標準出力を transmit.log として保存して中身を確認します。コマンドは先ほどの runtransmit に置き換えるだけです。

Transmit フェイズはいわば 準備 であり、この段階では プレイブックは実行されない ため、処理はすぐに完了します。

$ cd 07_awx/runner_remote
$ ansible-runner transmit . -p demo.yml | tee transmit.log
{"kwargs": {"ident": "81e67a815f6d4acc95446101ab2d0e3e", ...
{"zipfile": 1246}
UEsDBBQAAAAIAFEplFX/yIRIMQAAAEYAAAAKAAAALmdpdGlnbm9yZdPiU...{"eof": true}

1 行目には ansible-runner に渡した引数の情報が入ります。

$ cat transmit.log | head -n 1 | jq
{
  "kwargs": {
    ...
    "playbook": "demo.yml",
    ...

3 行目の先頭から {"eof": true} までが Base64 でエンコードされた ZIP ファイルです。2 行目には ZIP ファイルのサイズが入ります。切り出してデコードし、解凍します。

$ grep -A 1 zipfile transmit.log | tail -n 1 | sed 's/{"eof": true}//g' | base64 -d > transmit.zip
$ unzip -q transmit.zip -d transmit
$ ls -l transmit
total 4
drwxrwxr-x. 2 kuro kuro  19 Dec 14 04:52 inventory
drwxrwxr-x. 2 kuro kuro  22 Dec 14 04:52 project
-rw-rw-r--. 1 kuro kuro 819 Dec 14 05:16 transmit.log

ansible-runner に渡したディレクトリ(.)が丸ごと圧縮されています。今回の project 下にはプレイブックがひとつしかありませんが、コレクションやロールがあればそれも含まれることになります。

ここまでで、Ansible Runner の Transmit フェイズでは、ansible-runner に渡した 引数ディレクトリ丸ごと が標準出力に吐かれることが確認できました。この標準出力にはプレイブックの実行に必要な情報がすべて揃っているため、これさえ連携できれば、任意のリモートホストでプレイブックを実行できることになります。

Worker フェイズ

Transmit フェイズの出力を受け取って、実際にプレイブックを実行する フェイズです。今回は Transmit フェイズの出力を transmit.log として保存していたため、ここでは実際にこれを cat で渡して Worker フェイズを実行し、さらにその出力を worker.log として保存して中身を確認します。

Worker フェイズは実際にプレイブックを実行するため、少し時間がかかります。

$ cat transmit.log | ansible-runner worker | tee worker.log
...
{"uuid": "9ae64665-6fd2-95e0-564f-000000000006", "counter": 2, "stdout": "\r\nPLAY [localhost] ***************************************************************", ...
...
{"uuid": "c764303b-8118-42fc-9731-d8d60ad94ec0", "counter": 14, "stdout": "\r\nPLAY RECAP *********************************************************************...
...
{"zipfile": 11202}
UEsDBBQAAAAIALUwlVXNDA0KawYAAMARAAAHAAAAY29tbWFuZL...{"eof": true}

出力の各行には、Ansible の実行ログが JSON 形式で確認できます。また、末尾には Transmit フェイズのように ZIP ファイルが含まれます。解凍して中身を確認します。

$ grep -A 1 zipfile worker.log | tail -n 1 | sed 's/{"eof": true}//g' | base64 -d > worker.zip
$ unzip -q worker.zip -d worker
$ ls -l worker
total 20
-rw-------. 1 kuro kuro 4544 Dec 14 20:49 command
drwxr-xr-x. 2 kuro kuro   23 Dec 14 20:49 fact_cache
drwx------. 2 kuro kuro    6 Dec 14 20:50 job_events
-rw-------. 1 kuro kuro    1 Dec 14 20:50 rc
-rw-------. 1 kuro kuro   10 Dec 14 20:50 status
-rw-rw-r--. 1 kuro kuro    0 Dec 14 20:49 stderr
-rw-------. 1 kuro kuro  966 Dec 14 20:50 stdout

実行された Ansible のパラメータや、実行ログが確認できます。

$ cat worker/command | jq
{
  "command": [
    "ansible-playbook",
    "-i",
    "/tmp/tmppub6q_87/inventory",
    "demo.yml"
  ],
  "cwd": "/tmp/tmppub6q_87/project",
  ...

$ cat worker/stdout 
...
TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
    "dump": {
        "hostname": "kuro-awx01",
        "user_dir": "/home/kuro",
        "user_id": "kuro",
        "virtualization_type": "VMware"
    }
}
...

Process フェイズ

Worker フェイズの出力を受け取って、実行結果を整理する フェイズです。Worker フェイズの出力を worker.log として保存していたため、これを cat で渡して Process フェイズを実行します。引数には、ログ一式(Artifacts)の保存先を指定します。

$ cat worker.log | ansible-runner process ./process
...
TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
    "dump": {
        "hostname": "kuro-awx01",
        "user_dir": "/home/kuro",
        "user_id": "kuro",
        "virtualization_type": "VMware"
    }
}
...

見慣れた結果が返ってきましたが、あくまで Worker フェイズのログを整理して出力 しただけで、プレイブックがこの瞬間に実行されているわけではありません。

引数で指定したディレクトリには、ansible-runner run の実行時と同じログ一式が保存されます。

$ ls -l process/artifacts
total 32
-rw-------. 1 kuro kuro 4544 Dec 14 20:49 command
drwxr-xr-x. 2 kuro kuro   23 Dec 14 21:18 fact_cache
drwx------. 2 kuro kuro 4096 Dec 14 21:18 job_events
-rw-------. 1 kuro kuro    1 Dec 14 20:50 rc
-rw-------. 1 kuro kuro   10 Dec 14 20:50 status
-rw-rw-r--. 1 kuro kuro    0 Dec 14 20:49 stderr
-rw-------. 1 kuro kuro  966 Dec 14 20:50 stdout

標準入出力を介して、プレイブックの実行に必要なファイルと情報をまとめる Transmit、実際にプレイブックを実行する Worker、結果を整理する Process の各フェイズを別々に実行できることが確認できました。

フェイズ間の連携は標準入出力で行われるため、前述の通り、SSH 越しでも Receptor 越しでもファイル経由でも 何らかの形で標準入出力の中身さえ連携 できれば、それぞれを別のホストで処理できる ことになります。

Execution Environment(EE)の利用

Ansible Runner の持つ プロセス分離 機能を使うと、プレイブックを実行するプロセスを分離、平たく言えば プレイブックをコンテナ内で実行 できます。

そして、コンテナイメージには、AWX で利用できる任意の Execution Environment(EE)のイメージを指定 できます。すなわち、EE を使ってプレイブックを実行 できます。

基本の動き

ansible-runner run の引数に --process-isolation を与えるとプロセス分離が有効になり、デフォルトでは Podman を使ってコンテナ内でプレイブックが実行されるようになります。

さらに、--container-imageコンテナイメージを指定 できます。ここでは AWX のデフォルトの EE である quay.io/ansible/awx-ee:latest を指定しています(このイメージは AWX 用にしっかりカスタマイズされているので Ansible Runner で使うには --user=0 を Podman に渡す必要があります、自前で Ansible Builder でビルドしたイメージであれば不要です)。

$ cd 07_awx/runner_ee
$ ansible-runner run . -p demo.yml \
  --process-isolation \
  --container-image quay.io/ansible/awx-ee:latest \
  --container-option="--user=root"
...
TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
    "dump": {
        "hostname": "a02f803a5615",
        "user_dir": "/root",
        "user_id": "root",
        "virtualization_type": "container"
    }
}
...

実行したものは先の例と同じ localhost をターゲットとしたプレイブックですが、実行結果から、今回は コンテナ内で実行 された様子がうかがえます。

podman コマンドで確認すると、ansible-runner コマンドの実行中にコンテナが起動し、ansible-playbook が実行されていることが観察できます。

$ podman ps
CONTAINER ID  IMAGE                          COMMAND          ...
a02f803a5615  quay.io/ansible/awx-ee:latest  ansible-playbook ...

$ podman inspect a02f803a5615
...
     "Config": {
          ...
          "Cmd": [
               "ansible-playbook",
               "-i",
               "/runner/inventory/hosts",
               "demo.yml"
          ],
          "Image": "quay.io/ansible/awx-ee:latest",
          ...

プライベートコンテナレジストリの利用

引数や設定ファイルを工夫することで、認証が必要なプライベートコンテナレジストリ上の EE のイメージも利用できます。次のような設定ファイルを env/settings として配置します。

---
process_isolation: true
container_image: registry.example.com/ansible/ee:2.12-custom
container_auth_data:
  host: registry.example.com
  username: reguser
  password: Registry123!
  verify_ssl: false

ここでは、利用するコンテナイメージを registry.example.com/ansible/ee:2.12-custom に変更しています。このコンテナレジストリ registry.example.com認証が必要 で、かつエンドポイントが 自己署名証明書を利用した HTTPS のため、container_auth_data認証情報と SSL の検証の無効化を指定 しています。

この状態で ansible-runner コマンドを実行すると、指定した認証情報を使ってイメージがプルされ、プレイブックが実行されます。

$ ansible-runner run . -p demo.yml
...
TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
    "dump": {
        "hostname": "50e720f8ae7b",
        "user_dir": "/root",
        "user_id": "root",
        "virtualization_type": "container"
    }
}
...

$ podman events --filter event=pull --since 1h
...
2022-12-14 22:00:21.372323245 +0000 UTC image pull  registry.example.com/ansible/ee:2.12-custom
...

$ podman ps
CONTAINER ID  IMAGE                                        COMMAND          ...
50e720f8ae7b  registry.example.com/ansible/ee:2.12-custom  ansible-playbook ...

Ansible Runner 経由ではイメージのプルが行えましたが、Podman 全体の設定は特にいじっていないので、このレジストリに改めて手動でアクセスしようとしてもハネられてしまいます。

$ podman pull registry.example.com/ansible/ee:2.12-custom
Trying to pull registry.example.com/ansible/ee:2.12-custom...
Error: initializing source docker://registry.example.com/ansible/ee:2.12-custom: pinging container registry registry.example.com: Get "https://registry.example.com/v2/": x509: certificate signed by unknown authority

このことから、Ansible Runner が container_auth_data の内容に基づいて Podman をハンドリングしていることが伺えます。

実装として、Ansible Runner は、認証情報(container_auth_data)が指定されていれば、認証用の一時ファイルを作成 し Podman でコンテナを起動(podman run)する際に 引数 --authfile として指定 しています(以下の出力例には改行を加えています)。

$ ps -ef | grep "podman run"
kuro       10964   10963  5 22:54 pts/1    00:00:08
/usr/bin/podman run
  ...
  --authfile=/tmp/ansible_runner_registry_668a4418-dafa-447b-9e76-e5adced33e6f_6x83947w/auth.json
  ...
  registry.example.com/ansible/ee:2.12-custom
  ansible-playbook -i /runner/inventory/hosts demo.yml

このファイルには、Ansible Runner に container_auth_data で指定した認証情報が含まれています。

$ cat /tmp/ansible_runner_registry_668a4418-dafa-447b-9e76-e5adced33e6f_6x83947w/auth.json
{
    "auths": {
        "registry.example.com": {
            "auth": "cmVndXNlcjpSZWdpc3RyeTEyMyE="
        }
    }
}

$ cat /tmp/ansible_runner_registry_668a4418-dafa-447b-9e76-e5adced33e6f_6x83947w/auth.json \
  | jq -r '.auths."registry.example.com".auth' | base64 -d
reguser:Registry123!

さらに、container_auth_dataverify_sslfalse に指定されたレジストリがある場合、Ansible Runner は、設定用の一時ファイル を作成し、podman run のプロセスに 環境変数 CONTAINERS_REGISTRIES_CONFREGISTRIES_CONFIG_PATH として渡し ます(二つあるのは Podman の 3.1.0 未満と以降のバリエーションに対応するためです)。

$ ps -ef | grep "podman run"
kuro       10964   10963  5 22:54 pts/1    00:00:08 ...

$ cat /proc/10964/environ --show-nonprinting | sed 's/\^@/\n/g' | grep REGISTRIES
CONTAINERS_REGISTRIES_CONF=/tmp/ansible_runner_registry_668a4418-dafa-447b-9e76-e5adced33e6f_6x83947w/registries.conf
REGISTRIES_CONFIG_PATH=/tmp/ansible_runner_registry_668a4418-dafa-447b-9e76-e5adced33e6f_6x83947w/registries.conf

このファイルには、container_auth_dataverify_sslfalse にしたレジストリが insecure = true として含まれています。

$ cat /tmp/ansible_runner_registry_668a4418-dafa-447b-9e76-e5adced33e6f_6x83947w/registries.conf
[[registry]]
location = "registry.example.com"
insecure = true

Ansible Runner が container_auth_data の内容に基づいて Podman に一時的に設定ファイル群を与える ことで、Podman がプライベートレジストリからのイメージをプルできるようにうまくハンドリングしている様子が確認できました。ユーザはあくまで Ansible Runner に設定を与えたのみ であり、 Podman の設定には直接は全く触れていない 点がポイントです。

リモートジョブ実行機能との併用

前述したリモートジョブ実行機能とももちろん併用できます。この場合、実際にコンテナが起動するのは Worker フェイズのみです。

Worker フェイズの出力からファイルを抽出すると、コンテナや EE 関連の情報(Ansible のバージョンや利用可能なコレクション、Podman を起動するパラメータなど)も確認できます。

$ ansible-runner transmit . -p demo.yml | ansible-runner worker | tee worker.log
...

$ grep -A 1 zipfile worker.log | tail -n 1 | sed 's/{"eof": true}//g' | base64 -d > worker.zip
$ unzip -q worker.zip -d worker

$ cat worker/ansible_version.txt 
ansible [core 2.12.5.post0]

$ cat worker/collections.json | jq
{
  "/usr/share/ansible/collections/ansible_collections": {
    "community.general": {
      "version": "6.0.0"
    ...

$ cat worker/command | jq
{
  "command": [
    "podman",
    "run",
    ...
    "--authfile=/tmp/ansible_runner_registry_30044782759d4ba9a5f8d787ce571cb0_2ju3l7gd/auth.json",
    ...
    "registry.example.com/ansible/ee:2.12-custom",
    "ansible-playbook",
    "-i",
    "/runner/inventory/hosts",
    "demo.yml"
  ],
  ...
  "env": {
    ...
    "CONTAINERS_REGISTRIES_CONF": "/tmp/ansible_runner_registry_30044782759d4ba9a5f8d787ce571cb0_2ju3l7gd/registries.conf",
    "REGISTRIES_CONFIG_PATH": "/tmp/ansible_runner_registry_30044782759d4ba9a5f8d787ce571cb0_2ju3l7gd/registries.conf"
  }
}

command ファイルからは、前述の --authfile オプションや環境変数(CONTAINERS_REGISTRIES_CONFREGISTRIES_CONFIG_PATH)が実際に追加されている様子も容易に確認できます。

AWX と Receptor

本題の、AWX における Receptor の使われ方を確認します。なお、ここで紹介するものは AWX の 21.10.1 のものです。

全体像

ひとことでいえば、AWX はさまざまなシーンで Receptor のワーク として Ansible Runner の Worker フェイズを実行 しています。以下、AWX での ジョブの実行 を例に、まずは全体像を確認します。

Container Group でのジョブの実行

AWX から Container Group でジョブを実行すると、Kubernetes クラスタ上に EE の Pod が作成されて処理されます。

ここでは、Receptor の Kubernetes ワーク として Ansible Runner の Worker フェイズ が実行されています。

AWX は Ansible Runner の Transmit フェイズを実行し、その出力を Kubernetes ワークのペイロード として渡しています。Kubernetes ワークは Executor としての自分自身 に送信されており、実行された Worker フェイズの出力を受け取ってジョブの実行結果として処理しています(ただし、結果の処理には Ansible Runner の Process フェイズは使われていません)。

このとき、Pod の作成は Receptor の責任範囲 であり、Ansible Runner は単にプレイブックを実行するだけ です。したがって、AWX が実行する Ansible Runner の Transmit フェイズでは、プロセス分離は無効 な設定で実行されます。

Instance Group でのジョブの実行

AWX から Instance Group でジョブを実行すると、Execution Node 上の Podman で EE のコンテナが作成されて処理されます。

ここでは、Receptor のコマンドワーク として Ansible Runner の Worker フェイズ が実行されています。

AWX が実行した Ansible Runner の Transmit フェイズの出力は、Executor としての Execution Nodeコマンドワークのペイロード として渡されます。

この場合は、コンテナの作成は Ansible Runner の責任範囲 です。したがって、AWX が実行する Ansible Runner の Transmit フェイズでは、プロセス分離が有効 な設定で実行されます。

実行結果の処理は Container Group の場合と同じです。

Receptor の設定内容

実際の Receptor の設定を確認します。

AWX の Receptor の設定

AWX では、Receptor のプロセスは AWX の Pod の awx-ee コンテナで動作 しています。AWX 側の Receptor は、Controller 兼 Executor として構成されています。

初期状態では、次の設定ファイルです(改行のみ整形しています)。これまでの Receptor 関連のエントリ([1][2][3])で紹介した記述ばかりです。

- local-only: null

- log-level: debug

- node:
    firewallrules:
    - action: reject
      tonode: awx-bdfc84dd7-wp2b6
      toservice: control

- control-service:
    filename: /var/run/receptor/receptor.sock
    permissions: '0660'
    service: control

- work-command:
    allowruntimeparams: true
    command: ansible-runner
    params: worker
    worktype: local

- work-signing:
    privatekey: /etc/receptor/signing/work-private-key.pem
    tokenexpiration: 1m

- work-kubernetes:
    allowruntimeauth: true
    allowruntimeparams: true
    allowruntimepod: true
    authmethod: runtime
    worktype: kubernetes-runtime-auth

- work-kubernetes:
    allowruntimeauth: true
    allowruntimeparams: true
    allowruntimepod: true
    authmethod: incluster
    worktype: kubernetes-incluster-auth

- tls-client:
    cert: /etc/receptor/tls/receptor.crt
    key: /etc/receptor/tls/receptor.key
    name: tlsclient
    rootcas: /etc/receptor/tls/ca/receptor-ca.crt

ノードの id が指定されていないため、Receptor としてのノード ID には自身のホスト名が設定されます。

Controller として control-service が定義されており、ソケットファイルのパスが指定されています。

また、Executor のワークタイプとして コマンドワークの localKubernetes ワークの kubernetes-runtime-authkubernetes-incluster-auth が定義されています。

コマンドワーク localansible-runner自身で直接実行 するものです。params のとおり、Worker フェイズ の実行を前提としています。

Kubernetes ワークは authmethod の違いで二種類あり、kubernetes-runtime-auth は主に 他の Kubernetes クラスタ に対して Pod を作成する用途、kubernetes-incluster-auth自身が動作する Kubernetes クラスタ で Pod を作成する用途のワークタイプです。

Kubernetes ワークはいずれも allowruntimepodtrue に設定されており、具体的な Pod の仕様は実行時に渡されることがわかります。また、ワークの電子署名用の work-signing と、TLS クライアント(tls-client)も構成されています。

さらに、AWX に Execution Node を追加すると、追加したノードごとに次の 3 行が設定ファイルに追加されます(以下は exec01.ansible.internal をポート番号 27199 で追加した例です)。

- tcp-peer:
    address: exec01.ansible.internal:27199
    tls: tlsclient

なお、細かくなりすぎるので詳しい紹介は省きますが、Control Service に利用されているソケットファイルは awx-task コンテナとも共有されており、実際に receptorctl(CLI ツールではなく Python モジュール版)での操作を行っているのは awx-task コンテナです。前述の Ansible Runner の Transmit フェイズも、awx-task 側で行われています。

Execution Node の Receptor の設定

AWX に Execution Node を追加すると、これが Executor として Receptor のメッシュネットワークに参加します(Controller としての機能も持たせられているようです)。

Execution Node には、初期状態で次の設定ファイルが配置されます。以下は exec01.ansible.internal の例です(改行のみ整形しています)。

---
- node:
    id: exec01.ansible.internal

- work-verification:
    publickey: /etc/receptor/work_public_key.pem

- log-level: info

- control-service:
    service: control
    filename: /var/run/receptor/receptor.sock
    permissions: 0660
    tls: tls_server

- tls-server:
    name: tls_server
    cert: /etc/receptor/tls/exec01.ansible.internal.crt
    key: /etc/receptor/tls/exec01.ansible.internal.key
    clientcas: /etc/receptor/tls/ca/mesh-CA.crt
    requireclientcert: true
    mintls13: False

- tls-client:
    name: tls_client
    cert: /etc/receptor/tls/exec01.ansible.internal.crt
    key: /etc/receptor/tls/exec01.ansible.internal.key
    rootcas: /etc/receptor/tls/ca/mesh-CA.crt
    insecureskipverify: false
    mintls13: False

- tcp-listener:
    port: 27199
    tls: tls_server

- work-command:
    worktype: ansible-runner
    command: ansible-runner
    params: worker
    allowruntimeparams: True
    verifysignature: True

ワークタイプとして Execution Node で ansible-runner を実行する コマンドワーク ansible-runner が定義されています。params のとおり、Worker フェイズ の実行を前提としています。

このほか、Controller である AWX 側と接続するための TLS 関連や、電子署名を検証するための鍵などが構成されています。

実際の動き

実際に AWX 側から操作を行い、Receptor や Ansible Runner の使われ方を確認します。あらかじめ、AWX 側ではジョブテンプレートとしてこれまでと同じプレイブックを実行できるように構成しています。

Container Group でのジョブの実行とインベントリの同期(In-Cluster 認証)

AWX では、ジョブの実行やインベントリの同期は、それを実行するインスタンスが Container Group の場合、Kubernetes クラスタに EE の Pod が作成されて処理されます。すなわち、Kubernetes ワーク です。

まずは In-Cluster 認証 を用いた Kubernetes ワークの動きを追いかけるために、デフォルトの Container Group を指定してジョブを実行します。コンテナレジストリへの認証も確認するため、ジョブテンプレートで使う EE をプライベートコンテナレジストリ上のものに構成しています。

初回のエントリ で紹介したように、Receptor では、ワークで取り扱う標準入出力はすべて一時ファイルで保存されます。ジョブの実行中awx-ee コンテナの一時ファイルを見れば、ワークの動きを観察できます。ジョブの実行中に次のコマンドで一時ファイル群をホスト側にコピーします。

$ cd 07_awx/awx
$ kubectl -n awx exec deployment/awx -c awx-ee -- bash -c "tar zcf - /tmp/receptor/*/*" \
  | tar zxvf - --strip-components 4 -C cg_incluster
tmp/receptor/awx-bdfc84dd7-wp2b6/eq4pkWHs/status.lock
tmp/receptor/awx-bdfc84dd7-wp2b6/eq4pkWHs/status
tmp/receptor/awx-bdfc84dd7-wp2b6/eq4pkWHs/stdin
tmp/receptor/awx-bdfc84dd7-wp2b6/eq4pkWHs/stdout

Receptor のワークに指定されたパラメータ を確認するため、status ファイル を確認します。

$ cat cg_incluster/status | jq
{
  ...
  "WorkType": "kubernetes-incluster-auth",
  "ExtraData": {
    ...
    "KubePod": "---\napiVersion: v1\n...",
    ...
  }
}

$ cat cg_incluster/status | jq -r '.ExtraData.KubePod'
---
apiVersion: v1
kind: Pod
...
spec:
  ...
  containers:
  - args:
    - ansible-runner
    - worker
    - --private-data-dir=/runner
    image: registry.example.com/ansible/ee:2.12-custom
    imagePullPolicy: IfNotPresent
    name: worker
    ...
  imagePullSecrets:
  - name: automation-1568d-image-pull-secret-3
  serviceAccountName: default

status ファイルから、Kubernetes ワークタイプの kubernetes-incluster-auth が実行されている様子が確認できます。また、ExtraData.KubePod で Pod の仕様が指定されており、ansible-runner の Worker フェイズのコマンドが指定されていること、プライベートコンテナレジストリ上のイメージが指定されていること、そのために imagePullSecrets が指定されていることが確認できます。ansible-runner worker--private-data-dir オプションは Transmit フェイズから渡された ZIP ファイルを展開するパスの指定です。

imagePullSecrets は、指定された EE に認証情報が設定されている場合に AWX により自動で作成されます。

$ kubectl -n awx get secret automation-1568d-image-pull-secret-3 -o yaml
apiVersion: v1
data:
  .dockerconfigjson: ewogICAgImF1dGhzIjogewogICAgICAgICJyZWdpc3RyeS5leGF...
kind: Secret
...
type: kubernetes.io/dockerconfigjson

$ kubectl -n awx get secret automation-1568d-image-pull-secret-3 -o jsonpath='{$.data.\.dockerconfigjson}' | base64 -d
{
    "auths": {
        "registry.example.com": {
            "auth": "cmVndXNlcjpSZWdpc3RyeTEyMyE="
        }
    }
}

$ kubectl -n awx get secret automation-1568d-image-pull-secret-3 -o jsonpath='{$.data.\.dockerconfigjson}' | base64 -d \
  | jq -r '.auths."registry.example.com".auth' | base64 -d
reguser:Registry123!

続けて、stdin ファイル の中身を確認します。これはつまり、AWX 側の Ansible Runner の Transmit フェイズで生成されたデータ です。Receptor を通じて、Pod で動作する Ansible Runner の Worker プロセスに渡されます。

$ cat cg_incluster/stdin
{"kwargs": {"ident": 19, "playbook": "demo.yml", ...
{"zipfile": 1839}
UEsDBBQAAAAAABCnjlUAAAAAAAAAAAAAAAAKAAAAaW52ZW50b3J5...{"eof": true}

$ cat cg_incluster/stdin | head -n 1 | jq
{
  "kwargs": {
    ...
    "playbook": "demo.yml",
    "inventory": "inventory/hosts",
    "passwords": {
      ...
      "sudo password.*:\\s*?$": "Root123!",
      ...
      "SSH password:\\s*?$": "User123!",
      ...
    },
    "suppress_env_files": true,
    "envvars": {
      ...
      "ANSIBLE_SSH_CONTROL_PATH_DIR": "/runner/cp",
      "ANSIBLE_COLLECTIONS_PATHS": "/runner/requirements_collections:~/.ansible/collections:/usr/share/ansible/collections",
      "ANSIBLE_ROLES_PATH": "/runner/requirements_roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles"
    },
    ...

stdin ファイルの中身は、冒頭で紹介した Ansible Runner の Transmit フェイズの出力そのままです。

AWX は Ansible Runner をその API を通じて実行しているため、CLI(ansible-runner コマンド)よりも細かな制御が可能です。Transmit フェイズの出力の 1 行目では、Ansible Runner の実行時のパラメータが確認でき、インベントリやプレイブックのほか、ジョブテンプレートに設定したパスワード類 も復号化されて渡されていることが確認できます。このほか、Worker フェイズで実行される Ansible の動作を制御する環境変数類 も多数指定されています。例えば、これが vCenter Server をソースとするインベントリの同期であれば、環境変数に認証情報も含まれることになります。

Transmit フェイズの出力には、前述の通り、Base64 でエンコードされた ZIP ファイルも含まれます。

$ grep -A 1 zipfile cg_incluster/stdin | tail -n 1 | sed 's/{"eof": true}//g' | base64 -d > cg_incluster/stdin.zip
$ unzip -q cg_incluster/stdin.zip -d cg_incluster/stdin_
$ ls -l cg_incluster/stdin_
total 0
drwx------. 2 kuro kuro  6 Dec 14 20:56 cp
drwxr-xr-x. 2 kuro kuro 54 Dec 14 20:56 env
drwxr-xr-x. 2 kuro kuro 19 Dec 14 20:56 inventory
drwxrwxr-x. 2 kuro kuro 22 Dec 14 20:13 project

inventory ディレクトリには hosts ファイルが含まれています。これは、AWX 側で指定したインベントリを JSON 形式で出力する Python スクリプト です。

$ python3 cg_incluster/stdin_/inventory/hosts | jq
{
  "all": {
    "hosts": [
      "localhost"
    ],
    ...
  },
  ...
  "_meta": {
    "hostvars": {
      ...
      "localhost": {
        "ansible_connection": "local",
        "ansible_python_interpreter": "{{ ansible_playbook_python }}",
        ...
      }
    }
  }
}

冒頭で確認した Ansible Runner のリモートジョブ実行機能が、Receptor を通じて実行されている様子が確認できました。

Container Group でのジョブの実行とインベントリの同期(Runtime 認証)

つづけて、Runtime 認証の動きを確認します。インスタンスとして 認証情報を指定した Container Group を指定してジョブを実行し、先ほどと同様に Receptor の一時ファイルを取得します。

$ kubectl -n awx exec deployment/awx -c awx-ee -- bash -c "tar zcf - /tmp/receptor/*/*" \
  | tar zxvf - --strip-components 4 -C cg_runtime
tmp/receptor/awx-bdfc84dd7-wp2b6/aIefUYrp/status.lock
tmp/receptor/awx-bdfc84dd7-wp2b6/aIefUYrp/status
tmp/receptor/awx-bdfc84dd7-wp2b6/aIefUYrp/stdin
tmp/receptor/awx-bdfc84dd7-wp2b6/aIefUYrp/stdout

status ファイルを確認します。

$ cat cg_runtime/status | jq
{
  ...
  "WorkType": "kubernetes-runtime-auth",
  "ExtraData": {
    ...
    "KubeConfig": "---\napiVersion: v1\n...",
    ...
    "KubePod": "---\napiVersion: v1\n...",
    ...
  }
}

たしかにワークタイプ kubernetes-runtime-auth が指定されています。また、先ほどと異なり、KubeConfig が指定されています。

$ cat cg_runtime/status | jq -r '.ExtraData.KubeConfig'
---
apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://192.168.0.219:6443
  ...
contexts:
- context:
    cluster: https://192.168.0.219:6443
    namespace: awx
    user: https://192.168.0.219:6443
  ...
current-context: https://192.168.0.219:6443
kind: Config
preferences: {}
users:
- name: https://192.168.0.219:6443
  user:
    token: eyJhbGciOiJSUzI1NiIsImtpZCI6IkI1LXotczRZN1RU...

これは、AWX 側で指定した Kubernetes クラスタの認証情報に基づいて AWX が生成した KubeConfig データです。外部の Kubernetes クラスタへの認証 の動きが確認できました。

stdin ファイルの中身は先ほどの例と変わらないため省略します。

$ cat cg_runtime/stdin | head -n 1 | jq
{
  "kwargs": {
    ...
    "playbook": "demo.yml",
    "inventory": "inventory/hosts",
    "passwords": {
      ...

$ grep -A 1 zipfile cg_runtime/stdin | tail -n 1 | sed 's/{"eof": true}//g' | base64 -d > cg_runtime/stdin.zip
$ unzip -q cg_runtime/stdin.zip -d cg_runtime/stdin_
$ ls -l cg_runtime/stdin_
total 0
drwx------. 2 kuro kuro  6 Dec 14 20:54 cp
drwxr-xr-x. 2 kuro kuro 54 Dec 14 20:54 env
drwxr-xr-x. 2 kuro kuro 19 Dec 14 20:54 inventory
drwxrwxr-x. 2 kuro kuro 22 Dec 14 20:13 project

Instance Group でのジョブの実行とインベントリの同期

続けて、Execution Node を利用する場合、つまり Instance Group を指定してジョブを実行した場合 の動きを確認します。

先ほどと同様に一時ファイルを取得します。

$ kubectl -n awx exec deployment/awx -c awx-ee -- bash -c "tar zcf - /tmp/receptor/*/*" \
  | tar zxvf - --strip-components 4 -C ig_podman
tmp/receptor/awx-bdfc84dd7-wp2b6/tPqM2J8t/status.lock
tmp/receptor/awx-bdfc84dd7-wp2b6/tPqM2J8t/status
tmp/receptor/awx-bdfc84dd7-wp2b6/tPqM2J8t/stdin
tmp/receptor/awx-bdfc84dd7-wp2b6/tPqM2J8t/stdout

status ファイルを確認します。

$ cat ig_podman/status | jq
{
  ...
  "WorkType": "remote",
  "ExtraData": {
    "RemoteNode": "exec01.ansible.internal",
    "RemoteWorkType": "ansible-runner",
    "RemoteParams": {
      "params": "--private-data-dir=/tmp/awx_17_3jjx2zyd --delete"
    },
    ...
    "SignWork": true,
    "TLSClient": "tlsclient",
    ...
  }
}

Executor としての Execution Nodeコマンドワーク ansible-runner が送信されていることがわかります。このコマンドワークは、先の設定ファイルで定義されている通り、ansible-runner worker を実行するものです。また、このパラメータとして、ZIP ファイルの展開先を示す --private-data-dir と、そのパスの展開時の初期化と終了後の削除を指示する --delete が渡されています。

続けて、stdin ファイルの 1 行目を確認します。

$ cat ig_podman/stdin | head -n 1 | jq
{
  "kwargs": {
    ...
    "playbook": "demo.yml",
    "inventory": "inventory/hosts",
    "passwords": {
      ...
      "sudo password.*:\\s*?$": "Root123!",
      ...
      "SSH password:\\s*?$": "User123!",
      ...
    },
    ...
    "envvars": {
      ...
    },
    ...
    "container_image": "registry.example.com/ansible/ee:2.12-custom",
    "process_isolation": true,
    "process_isolation_executable": "podman",
    "container_options": [
      "--user=root",
      ...
      "--pull=missing"
    ],
    "container_auth_data": {
      "host": "registry.example.com",
      "username": "reguser",
      "password": "Registry123!",
      "verify_ssl": false
    },
    ...
  }
}

Executor Node でジョブを実行する場合には Podman のコンテナとして EE が起動しますが、このコンテナの起動は Ansible Runner の責任範囲です。したがって、Podman の動作を制御するためのパラメータも Transmit フェイズから渡されます。コンテナイメージ(container_image)のほか、コンテナレジストリの認証情報(container_auth_data)も渡されています。

stdin ファイルに含まれる ZIP ファイルの中身はこれまでと同様です。

$ grep -A 1 zipfile ig_podman/stdin | tail -n 1 | sed 's/{"eof": true}//g' | base64 -d > ig_podman/stdin.zip
$ unzip -q ig_podman/stdin.zip -d ig_podman/stdin_
$ ls -l ig_podman/stdin_
total 0
drwx------. 2 kuro kuro  6 Dec 14 20:42 cp
drwxr-xr-x. 2 kuro kuro 54 Dec 14 20:42 env
drwxr-xr-x. 2 kuro kuro 19 Dec 14 20:42 inventory
drwxrwxr-x. 2 kuro kuro 22 Dec 14 20:13 project

プロジェクトの同期

最後に、プロジェクトの同期 の動作です。AWX では、プロジェクトの同期は AWX の Pod 内で閉じて処理されます。ここでは、自分自身に対するコマンドワーク が送信されています。

$ kubectl -n awx exec deployment/awx -c awx-ee -- bash -c "tar zcf - /tmp/receptor/*/*" \
  | tar zxvf - --strip-components 4 -C sync_pj
tmp/receptor/awx-bdfc84dd7-wp2b6/YsoGXshO/status.lock
tmp/receptor/awx-bdfc84dd7-wp2b6/YsoGXshO/status
tmp/receptor/awx-bdfc84dd7-wp2b6/YsoGXshO/stdin
tmp/receptor/awx-bdfc84dd7-wp2b6/YsoGXshO/stdout

$ cat sync_pj/status | jq
{
  ...
  "WorkType": "local",
  "ExtraData": {
    ...
    "Params": "worker --private-data-dir=/tmp/awx_24_80ydyxhs"
  }
}

$ cat sync_pj/stdin | head -n 1 | jq
{
  "kwargs": {
    ...
    "playbook": "project_update.yml",
    "inventory": "inventory/hosts",
    "passwords": {
      ...
      "Password:\\s*?$": "Git123!",
      ...
    },
    ...
  }
}

プロジェクトの同期を実行するプレイブック project_update.yml が実行されることがわかります。これは AWX に組み込まれたプレイブックであり、同期する対象は Ansible の変数で指定されます。

$ grep -A 1 zipfile sync_pj/stdin | tail -n 1 | sed 's/{"eof": true}//g' | base64 -d > sync_pj/stdin.zip
$ unzip -q sync_pj/stdin.zip -d sync_pj/stdin_
$ ls -l sync_pj/stdin_
total 0
drwxr-xr-x. 2 kuro kuro 54 Dec 14 21:26 env
drwxr-xr-x. 2 kuro kuro 19 Dec 14 21:26 inventory
drwxr-xr-x. 4 kuro kuro 69 Dec 12 19:05 project

$ cat sync_pj/stdin_/env/extravars 
...
!unsafe 'project_path': !unsafe '/var/lib/awx/projects/_6__demo_project'
...
!unsafe 'scm_url': !unsafe 'https://git:Git123%21@github.com/ansible/ansible-tower-samples'
...

Automation Mesh と Receptor

AAP の Automation Mesh における Receptor も、役割は AWX のそれと大きく変わりません。Automation Mesh とは、つまりは Receptor のメッシュネットワーク を下地に、Controller としての AAPExecutor としての Execution Node任意の配置で構成できる機能 と言えます。

In this configuration, resilient controller nodes are peered with resilient local execution nodes. Resilient local hop nodes are peered with the controller nodes. A remote execution node and a remote hop node are peered with the local hop nodes.

Chapter 3. Automation mesh design patterns – 3.5. Multi-hopped execution node

Automation Mesh では上図のような 複雑なトポロジ も構成できますが、これまでのエントリで紹介したように、Receptor では メッシュネットワークが組めさえすればノード間の経路はもはや気にする必要がない わけで、あえて Receptor を介する実装 にすることで、Receptor の持つ柔軟性と拡張性をそのまま AAP の強みにできている とも言えます。

AWX にはない Hop Node(図の *_h_*)も、つまり、ワークタイプが定義されていない Receptor のノード(Relayer) なだけです。

インストール先が OpenShift でなければ Kubernetes ワークの出番が少なくなるくらいで、親玉たる 制御プレーン実行先のインスタンスに応じて Ansible Runner と Receptor に渡す情報をコントロール している動きは、結局は AWX と同じです。

まとめ

AWX のさまざまな動きが、Ansible Runner と Receptor のそれぞれの仕組みをうまく使うことで実現されていることを紹介しました。AWX は、実行先のインスタンスタイプに応じて Ansible Runner や Receptor に渡す情報を制御しています。

全体としては非常に複雑そうに見えるものの、それぞれのツールの責任範囲がはっきりしているため、踏み込んでみればそれぞれの動きは比較的シンプルであることがわかります。

もちろんこれは偶然ではなく、こうした利用方法を見越してそれぞれのツールの開発がすすめられたものとは思いますが、それにしてもよく嚙み合っていておもしろい動きです。

Receptor 関連エントリ

@kurokobo

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

コメントを残す

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