AWX に Kubernetes クラスタ外のスタンドアロン実行ノードを追加する

はじめに

AWX の 21.7.0 から、Execution Node実行ノード)がサポートされました。これにより、AWX が動作する Kubernetes クラスタの 独立したホスト に、ジョブの実処理を任せられる ようになります。

構築方法のドキュメント も提供されており、これが親切なので愚直に従えば動くところまで比較的簡単に持っていけますが、本エントリでは、概要や構成方法、使い方を、実装面を少し紐解きつつ紹介します。

Execution Node とは

概要

Kubernetes クラスタ上の AWX でジョブを実行すると、デフォルトの構成では、ジョブごとに Execution Environment(実行環境、EE)の Pod が作成されて処理されます。この場合、ジョブのターゲットノードには EE の Pod から接続できる必要があります。

今回サポートされた Execution Node は、Kubernetes クラスタの にある RHEL ファミリの 独立したホスト に、ジョブの実処理を任せられる機能です。

Execution Node には、それを構成する過程で ReceptorPodman が導入され、Receptor により Kubernetes 上の AWX インスタンスとの間にメッシュネットワークが構成されます。AWX でジョブが実行されると、Receptor を介して Ansible Runner の Worker プロセスが起動し、Podman 上に EE のコンテナが作られて処理されます。

メリット

Execution Node を用意すると、ジョブのターゲットノードにはこの Execution Node から接続できればよくなります。つまり、Kubernetes クラスタからターゲットノードへの直接の接続性を確保する必要がなくなる ため、いわゆる 踏み台サーバに近い イメージでも使えることになります(ansible_ssh_common_args を使ってがんばらなくてもよくなる環境も多そうです)。ただし、ジョブを実行する EE のイメージのプルは Execution Node 上の Podman が直接行うため、コンテナレジストリへの到達性は必要です。

また、大規模な環境やジョブの同時実行数が多い環境では、ジョブ実行用のリソースを(Kubernetes クラスタへのノード追加ではない形で)簡単に増強できるようになります。リソースをジョブの実行で占有できるのもメリットです。

AWX のこれまでの実装でも、技術的には Kubernetes のワーカノードを別に用意して EE の起動を Container Group でそのノードに限定すれば似たようなことは(やや強引に)実現できましたが、Kubernetes クラスタがマネージドサービスだったり、ノード間のネットワークの論理的・物理的制約が強かったりすると、そうした構成も難しいことがありました。

この点でも、今回の Execution Node のサポートで、複雑なネットワーク環境でもシンプルで柔軟な構成が取りやすくなったといえそうです。例えば、クラウド環境でリージョンをまたいだジョブの実行なども構成しやすくなるでしょう。

なお、AWX のインスタンスと Execution Node の間の通信は TLS の証明書による認証と暗号化が行われます。この証明書の発行には、デフォルトでは自動生成された自己署名 CA 証明書が使われますが、Kubernetes 上に Secret リソースとして作成することで任意の CA 証明書に置き換えられる ようです。

構成上の注意点(21.7.0 のみ)

本機能が実装された最初のリリースである 21.7.0 でのみ、Execution Node 用に生成される 証明書の有効期限が 10 日間 で固定されている問題があります。

AWX にも Receptor にも証明書の自動更新の機構は(少なくとも現時点では)存在しないため、証明書の有効期限が切れると Receptor としての通信ができなくなります。したがって、21.7.0 で本機能を本格的に使う場合、高い頻度で証明書を手動で更新し続ける運用が必要です。証明書の更新手順は 後述の補足 で紹介しています。

本件はすでに Issue で報告済みで、PR もマージ済みのため、次のリリースでは修正されます。急ぎでなければ、本格的な活用は次のリリースを待つほうがよいでしょう(追記: この問題は 21.8.0 で修正されました)。

参考: Ansible Automation Platform での実装

商用版の Ansible Automation Platform(AAP)では、2.1 から Automation Mesh として近い機能がリリースされています。

Automation Mesh でも Execution Node は実行プレーンを構成する要素のひとつ として次のように定義されており、今回の AWX での Execution Node と同等のものといえそうです。

The execution plane consists of execution nodes that execute automation on behalf of the control plane and have no control functions. Hop nodes serve to communicate. Nodes in the execution plane only run user-space jobs, and may be geographically separated, with high latency, from the control plane.

Red Hat Ansible Automation Platform automation mesh guide – 1.2.2. Execution plane

Execution nodes – Execution nodes run jobs under ansible-runner with podman isolation. This node type is similar to isolated nodes.

Red Hat Ansible Automation Platform automation mesh guide – 1.2.2. Execution plane

なお、AAP の Automation Mesh では、Execution Node のほかに Hop Node も定義されています。これはジョブを他の Execution Node に中継する目的のホストですが、現時点の AWX では Execution Node ほど簡単には構成できないようです(余談ですが、語義としてはこちらのほうが『踏み台』には近いですね)。

Hop nodes – similar to a jump host, hop nodes will route traffic to other execution nodes. Hop nodes cannot execute automation.

Red Hat Ansible Automation Platform automation mesh guide – 1.2.2. Execution plane

Execution Node の構成

実際に Execution Node を構成して、AWX から利用します。基本的には AWX のドキュメント に従うのみですが、各所、もう少し詳しい説明を加えつつ紹介します。

前提: 今回の構成と作業の流れ

Execution Node を構成するには、次のモノが必要です。

  • AWX(21.7.0 以降)
  • Execution Node として構成する RHEL ファミリ(RHEL、CentOS、Fedora など)のホスト
  • Execution Node として構成するホストに対して Ansible が実行できる環境

三点目は、Execution Node 用のホストを Execution Node として構成するためのインストール用プレイブックの実行に利用します。Execution Node 用のホストに Ansible として接続できさえすればよいので、構成にまったく無関係の適当な環境を使ってもよいですし、あるいは Execution Node 上で自分自身に対して実行する作戦もアリです。

さて、今回は、シングルノードの K3s 上に構成した AWX(awx.example.com に、Execution Node として CentOS Stream 8 のホスト(exec01.ansible.internal)を追加することを考えます。踏み台サーバのように使えることを確認するため、Execution Node をマルチホームで構成し、K3s ホストから直接の到達性がないネットワークにいるターゲット(kuro-dev01.ansible.private)をジョブで操作できることも確認します。

また、ジョブの実行に利用する EE は、デフォルトの公開イメージ(quay.io 上の awx-ee)のほか、AWX と同じ K3s クラスタ上に構成した プライベートコンテナレジストリregistry.example.com)上のものも利用します。このコンテナレジストリは、自己署名証明書の HTTPS を使っており、認証も必須です。

構成は次の流れで作業します。AWX 側の操作は API を直接叩いても行えますが、今回は Web GUI で作業しています。

  1. Execution Node 用ホストの準備
  2. AWX での新規インスタンスの定義
  3. Execution Node 用のインストールバンドルの取得
  4. Execution Node 用のインストールバンドルの実行
  5. AWX での新規インスタンスグループの定義

Execution Node 用ホストの準備

Execution Node として構成するホストを用意します。要件は次の通りです。

  • RHEL ファミリの OS(RHEL、CentOS、Fedora など)であること
  • 固定 IP アドレス、または AWX から DNS で名前解決できるホスト名を持つこと
  • AWX から Receptor のポート(デフォルトで 27199/tcp)にアクセスできること

対応 OS が RHEL ファミリに限定されていますが、後述するインストールバンドルが利用している Ansible のロール ansible.receptor.setup が RHEL ファミリ向けに書かれていることも影響していそうです。

なお、このホストの具体的な リソース要件 は AWX のドキュメントでは定められていませんが、AAP のドキュメント では Execution Node は 4 コアの CPU16 GB のメモリ が必要とされています。厳密にいえば別製品ですが、Execution Node の実装はほとんど同じと考えられるため、参考にはできそうです。このホストの CPU コア数やメモリ容量は、AWX 側でジョブの同時実行数などの計算に利用 されるため、利用する環境の規模に応じて Ansible Automation Controller(AAC) のドキュメント も併せて参考して調整するとよいでしょう。

今回は、CentOS Stream 8 を用意しました。AAP のドキュメントを尊重して 4 vCPU、16 GB RAM で、最小インストール構成です。あらかじめ、Receptor で利用するポートをファイアウォールで許可 しておきます。ポート番号はデフォルトでは 27199/tcp です。

sudo firewall-cmd --add-port=27199/tcp --permanent
sudo firewall-cmd --reload

また、後述の手順でインストール用のプレイブックをこのホストに対して実行することになるため、Ansible での接続に利用するユーザやそのための SSH の認証を構成しておきます。

AWX での新規インスタンスの定義

Execution Node は、AWX 上では インスタンス の一種です。まずは、AWX の Administration > Instances > Add で、追加する Execution Node の情報を定義します。

以降、AWX からはここで入力した Host Name に宛てて通信が発生し、証明書もこのホスト名に対して発行されます。したがって、ここでは AWX から名前解決ができて到達可能なホスト名 または 固定 IP アドレス を入力する必要があります。その他の修正は任意です。今回はすべてデフォルトにしています。

入力後、Save を押下するとインスタンスが追加されます。

この段階で、AWX は新しいインスタンスのインストールの完了を待つ状態になり、awx-ee コンテナから Receptor での接続を試行してノードの状態を定期的に確認するようになります。現時点では当然ながら接続できません。

$ kubectl -n awx logs -f deployment/awx -c awx-ee
...
WARNING 2022/10/05 22:08:40 Backend connection failed (will retry): dial tcp 192.168.0.218:27199: connect: connection refused
WARNING 2022/10/05 22:09:00 Backend connection failed (will retry): dial tcp 192.168.0.218:27199: connect: connection refused
WARNING 2022/10/05 22:09:20 Backend connection failed (will retry): dial tcp 192.168.0.218:27199: connect: connection refused
...

Execution Node 用のインストールバンドルの取得

前述の画面で、Install Bundle のダウンロードボタンをクリックして、そのインスタンス専用のインストールバンドル<ホスト名>_install_bundle.tar.gz)をダウンロードします。

この中には、Receptor での通信に必要な CA 証明書やそのノード用の証明書のほか、それらを使ってホストを Execution Node として構成するために必要なプレイブックなどが含まれています。

Execution Node 用のインストールバンドルの実行

インストールバンドルの実体は、Execution Node 用のホストをターゲットとする Ansible のプレイブックであり、手動で実行が必要 です。

Ansible を実行できる適当な環境にインストールバンドルを配置して展開し、まずは同梱の requirements.yml を使ってプレイブックに必要なコレクションをインストールします。

ansible-galaxy collection install -r requirements.yml

続けて、インストールバンドルに含まれるインベントリファイル(inventory.yml)を編集します。ターゲットノードのホスト名には AWX 側で入力したホスト名があらかじめ含まれていますが、これ以外の箇所は修正が必要 です。プレイブックは become アリで実行される ため、権限昇格も踏まえて適宜修正します。

---
all:
  hosts:
    remote-execution:
      ansible_host: exec01.ansible.internal
      ansible_user: <username> # user provided
      ansible_ssh_private_key_file: ~/.ssh/id_rsa

鍵認証向けの記述も含まれていますが、つながって実行できさえすればよいので、パスワード認証向けの記述に修正しても構いませんし、あるいはターゲットノード上で直接実行する場合は ansible_connection: local にするのもアリです。become のパスワードはインベントリファイルに書いておいてもよいですし、プレイブックを実行する段階で --ask-become-pass で与えてもよいでしょう。

準備ができたら、プレイブックを実行して完了を待ちます。

$ ansible-playbook -i inventory.yml install_receptor.yml
...
PLAY RECAP *******************************************************************************************************
remote-execution           : ok=32   changed=18   unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

プレイブックが完了して AWX と正常に接続できるようになれば、数分で AWX 側のインスタンスの StatusReady に変化します。これで、Execution Node がインスタンスとして追加されました。

awx-ee コンテナのログからも、Receptor を介したネゴシエーションの様子が確認できますz

$ kubectl -n awx logs -f deployment/awx -c awx-ee
...
DEBUG 2022/10/06 20:12:16 Sending initial connection message
INFO 2022/10/06 20:12:16 Connection established with exec01.ansible.internal
DEBUG 2022/10/06 20:12:16 Stopping initial updates
DEBUG 2022/10/06 20:12:16 Re-calculating routing table
INFO 2022/10/06 20:12:16 Known Connections:
DEBUG 2022/10/06 20:12:16 Sending routing update tNidLlEw. Connections: exec01.ansible.internal(1.00)
INFO 2022/10/06 20:12:16    awx-667b7b7b54-bwhmv: exec01.ansible.internal(1.00) 
INFO 2022/10/06 20:12:16    exec01.ansible.internal: awx-667b7b7b54-bwhmv(1.00) 
INFO 2022/10/06 20:12:16 Routing Table:
INFO 2022/10/06 20:12:16    exec01.ansible.internal via exec01.ansible.internal
DEBUG 2022/10/06 20:12:16 Received routing update UMiwQwzW from exec01.ansible.internal via exec01.ansible.internal
...
DEBUG 2022/10/06 20:13:11 Reloading
...
DEBUG 2022/10/06 20:13:14 Stdout complete - closing channel for: quF9TI7P 
...
DEBUG 2022/10/06 20:13:14 closing connection from service gMPLKKYm to exec01.ansible.internal:control
...

Ready にならず、Installed のまま変化しなかったり、Unavailable になってしまった場合、何らかの問題が発生していると考えられます。後述の トラブルシュート を参考に原因を特定し修正します。

AWX での新規 Instance Group の定義

AWX でのジョブの実行は、インスタンス単位ではなく、Instance Group(または Container Group)に対して割り当てられます。したがって、特定のインスタンスでジョブを実行させる には、そのインスタンスを含んだ Instance Group が必要 です。

まずは、AWX の Administration > Instance Groups > Add > Add instance group で任意の名称の Instance Group を定義します。

その後、追加した Instance Group の Instances タブで Associate ボタンをクリックし、今回新しく登録した Execution Node のインスタンスを選択して Save ボタンをクリックします。

これで Instance Group に Execution Node がインスタンスとして追加され、ジョブの実行に利用できるようになりました。

動作の確認

環境ができたので、ここからは Execution Node を使ったジョブの実行を実際にテストします。

なお、Execution Node でジョブを実行するには、前述のとおりそのジョブに Instance Group を指定する必要があり、この指定は Organization 単位Inventory 単位、もしくは Job Template 単位 で可能です。今回はいずれも次の図のように Job Template 単位で指定しています。

基本的な動作の確認

まずは基本的な動作の確認として、デフォルトの EE(quay.io/ansible/awx-ee:latest)を使い、EE 内で完結するよう localhost をターゲットにしたジョブを実行させます。挙動が追いやすいよう、ここでは ansible.builtin.pause を含むプレイブック を Job Template として使っています。

ジョブを実行すると、実行中は Execution Node では Podman 上で EE のコンテナが動作していることが観察できます。このコンテナはユーザ awx(前述の手順で実行したインストール用プレイブックにより作成されています)の名前空間内で動作しているため、事前に su して観察します。

$ sudo su - awx
$ podman ps
CONTAINER ID  IMAGE                          COMMAND               CREATED         STATUS             PORTS       NAMES
3e02ac3e9685  quay.io/ansible/awx-ee:latest  ansible-playbook ...  39 seconds ago  Up 40 seconds ago              ansible_runner_8

ところで、Kubernetes クラスタ上の EE では ansible-runner コマンドが実行されていました が、上記出力からは、Execution Node 上の EE では ansible-playbook コマンドが実行されていることがわかります。

これは、Execution Node を用いたジョブの実行では、Ansible Runner の Worker プロセスは Executio Node 上で直接(非コンテナ環境で)動作 するためで、Podman はあくまでその Ansible Runner が Process Isolation の手段として利用 しているだけだからです。したがって、ansible-runner 用のファイル群も、Execution Node 上の /tmp に直接展開されています。

# 関連プロセス群。
# Receptor が直接 Ansible Runner の Worker プロセスを起動していることがわかる。
# Ansible Runner は /tmp/awx_8_2g2vrzz7 を作業ディレクトリとして利用している。
$ ps -ef | grep -E '(receptor|ansible-runner)' | grep -v grep
awx 16373     1 0 05:11 ? 00:00:08 /usr/bin/receptor -c /etc/receptor/receptor.conf
awx 53942 16373 0 06:37 ? 00:00:00 /usr/bin/receptor --node id=worker --log-level info --command-runner command=ansible-runner params=worker --private-data-dir=/tmp/awx_8_2g2vrzz7 --delete unitdir=/tmp/receptor/exec01.ansible.internal/8CbcJ0G6
awx 53951 53942 1 06:37 ? 00:00:00 /usr/bin/python3.9 /usr/local/bin/ansible-runner worker --private-data-dir=/tmp/awx_8_2g2vrzz7 --delete

# 関連ファイル群。
# Ansible Runner の処理に必要な実際のファイル群は、
# Execution Node 上の /tmp 下に直に展開されている。
$ ls -l /tmp/awx_8_2g2vrzz7
total 0
drwxr-xr-x.  3 awx awx  15 Oct  7 06:37 artifacts
drwx------.  2 awx awx   6 Oct  6 21:37 cp
drwxr-xr-x.  2 awx awx  54 Oct  7 06:37 env
drwxr-xr-x.  2 awx awx  19 Oct  7 06:37 inventory
drwxr-xr-x. 14 awx awx 228 Oct  7 06:37 project

また、EE のイメージが Podman によりプルされていることから、コンテナレジストリへの接続性は Execution Node からも必要であることが確認できます。

$ podman events --filter event=pull --since 1h
...
2022-10-07 06:37:10.577200393 +0900 JST image pull  quay.io/ansible/awx-ee:latest
...

$ podman images
REPOSITORY              TAG         IMAGE ID      CREATED      SIZE
quay.io/ansible/awx-ee  latest      8982fdafc038  9 hours ago  1.96 GB

隔離ネットワーク上のターゲットへのジョブの実行

続けて、AWX が動作している K3s ホストからは到達性がないネットワークにあるターゲットノード(図中右上の kuro-dev01.ansible.private)に対するジョブの実行をテストします。図の通り、このターゲットには Execution Node からのみアクセス可能です。

テストのため、AWX 上で Inventories と Credentials にターゲットノードへの接続に必要な情報を登録して、Job Template として facts を収集して ping 後に表示するだけの次の適当なプレイブックを追加しました。

---
- hosts: all
  gather_facts: true
  tasks:
    - ansible.builtin.ping:
    - ansible.builtin.debug:
        var: ansible_facts

まずは、Instance Group を 指定しないでジョブを実行 し、ターゲットノードに到達できずに失敗することを確認します。

改めて Instance Group を 指定して再度ジョブを実行 すると、ジョブが成功するようになりました。正しく Execution Node 経由で接続できているようです。

従来、Kubernetes クラスタの Pod から直接は接続できないネットワークにいるターゲットには、SSH を踏み台経由で到達させるために ansible_ssh_common_args を使ってがんばるなどするのが常套手段でしたが、Execution Node を使うと非常にシンプルに構成できるようになりそうです。

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

最後に、プライベートコンテナレジストリ上の EE が使えることをテストします。準備として、Kubernetes クラスタ上に次のようなプライベートコンテナレジストリを構築し、EE のイメージをプッシュしました。

  • 自己署名証明書による HTTPS でホストされる
  • 認証が必要である

さらに AWX では、Verify SSL をオフにした Container Registry タイプの Credential を作成し、プライベートコンテナレジストリ上のイメージをその Credential と共に Execution Environment として追加しています。

プライベートレジストリ registry.example.com は、前述のとおり 自己署名証明書の HTTPS で、認証も必要 です。つまり、Execution Node 上で手動でプルしようとしても、当然ながら特別な設定なしには次のように 失敗 します。

# 自己署名証明書なので、明示的に insecure を指定しないと接続できない。
# insecure を指定したとしても、認証が必要なためログインしなければ接続できない。
$ 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.builtin.pause を含む Job Template に、Instance Group とプライベートレジストリ上の Execution Environment を指定してジョブを実行します。

実行すると、イメージがプルできずに失敗することもなく、ジョブは何の問題もなく開始し、完了します。

Execution Node 上では、プライベートレジストリ上のイメージが正しくプルされています。

$ podman events --filter event=pull --since 1h
...
2022-10-07 08:59:17.874383265 +0900 JST image pull  registry.example.com/ansible/ee:2.12-custom
...

$ podman images
REPOSITORY                       TAG          IMAGE ID      CREATED       SIZE
quay.io/ansible/awx-ee           latest       8982fdafc038  12 hours ago  1.96 GB
registry.example.com/ansible/ee  2.12-custom  196d9b9302d1  21 hours ago  1.18 GB

というわけで、AWX 側の設定に応じて、証明書検証も認証もなにやらよしなにやってくれていそうです。もう少し踏み込んで確認します。

実装上、AWX は、指定された Execution Environment に Credentials が設定されている場合、Ansible Runner にその情報を渡し、さらに Ansible Runner は、受け取った認証情報や設定を /tmp 下に auth.jsonregistries.conf として保存 して、それを Podman に渡し ています。

実行されている podman run コマンドの引数を見ると、--authfile が指定 されていることがわかります。この引数は、AWX から認証情報が渡された場合のみ指定されるものです(以下の出力例は見やすいように改行を加えています)。

$ ps -ef | grep "podman run"
awx 58712 58704 0 08:59 pts/0 00:00:00
  /usr/bin/podman run
  --rm
  --tty
  --interactive
  --workdir /runner/project
  -v /tmp/awx_17_3za59d3w/:/runner/:Z
  --authfile=/tmp/ansible_runner_registry_17_dya5ku7p/auth.json
  -v /etc/pki/ca-trust/:/etc/pki/ca-trust/:O
  -v /usr/share/pki/:/usr/share/pki/:O
  --env-file /tmp/awx_17_3za59d3w/artifacts/17/env.list
  --quiet
  --name ansible_runner_17
  --user=root
  --network slirp4netns:enable_ipv6=true
  --pull=missing
  registry.example.com/ansible/ee:2.12-custom
  ansible-playbook -u root -i /runner/inventory/hosts -e @/runner/env/extravars runner/project/demo.yml

--authfile で指定されているファイルの中身には、レジストリの認証情報 がいつもの JSON 形式で保存されています。Base64 でエンコードされていますが、デコードすれば平文が得られます。これが、プライベートレジストリに対して認証を行える仕組みです。

$ cat /tmp/ansible_runner_registry_17_dya5ku7p/auth.json
{
    "auths": {
        "registry.example.com": {
            "auth": "cmVndXNlcjpSZWdpc3RyeTEyMyE="
        }
    }
}

$ echo "cmVndXNlcjpSZWdpc3RyeTEyMyE=" | base64 -d
reguser:Registry123!

また、AWX 側で Verify SSL をオフ にした場合、この podman のプロセスには Podman が読み込む設定ファイルのパスを示す環境変数 CONTAINERS_REGISTRIES_CONFREGISTRIES_CONFIG_PATH が設定 されます(二つあるのは Podman の 3.1.0 以降とそれ未満の両方のバージョンに対応するためのようです)。この環境変数によって、そのジョブ限りの一時的な registries.conf が渡され、そこに記述された insecure = true が機能 します。

$ cat /proc/58712/environ --show-nonprinting | sed 's/\^@/\n/g' | grep REGISTRIES
CONTAINERS_REGISTRIES_CONF=/tmp/ansible_runner_registry_17_dya5ku7p/registries.conf
REGISTRIES_CONFIG_PATH=/tmp/ansible_runner_registry_17_dya5ku7p/registries.conf

$ cat /tmp/ansible_runner_registry_17_dya5ku7p/registries.conf
[[registry]]
location = "registry.example.com"
insecure = true

こうして、特に Podman 側の構成を手動で変更することなく、AWX 側での指定のみでプライベートコンテナレジストリ上の EE イメージをプルできるようになっています。

補足

Execution Node 用の証明書の中身

インストールバンドルには、Execution Node で Receptor が使う証明書と秘密鍵が含まれています。これは、Receptor の CA で署名されています。これの有効期限が固定で 10 日間であることが、前述の注意事項の根拠です。AWX の次のリリースではこの有効期限は 10 年に延長されます。

$ openssl x509 -text -in receptor/tls/receptor.crt -noout
Certificate:
    Data:
        ...
        Issuer: CN = awx Receptor Root CA
        Validity
            Not Before: Oct 17 23:18:26 2022 GMT
            Not After : Oct 27 23:18:26 2022 GMT
        ...
        Subject: CN = exec01.ansible.internal
        ...
        X509v3 extensions:
            X509v3 Subject Alternative Name: 
                DNS:exec01.ansible.internal, othername:<unsupported>
    ...

Receptor の CA 自体の証明書も含まれています。デフォルトでは自己署名証明書で、こちらは 10 年間の有効期限があります。

$ openssl x509 -text -in receptor/tls/ca/receptor-ca.crt -noout
Certificate:
    Data:
        ...
        Issuer: CN = awx Receptor Root CA
        ...
        Subject: CN = awx Receptor Root CA
        ...

前述のように、CA の証明書は Kubernetes 上に Secret リソースとして作成することで任意のものに置き換えられる ようです。なお、Execution Node の構成後に後から CA の証明書を置き換える際は、全 Execution Node でインストールバンドルの再生成と再実行が必要になるようです。

期限が近付いた証明書の更新

Execution Node の証明書を正攻法で更新する場合は、次の手順で対応できます。Receptor の CA 証明書を自前のものに置き換えた場合の手順も同じです。

  1. AWX 上で当該 Execution Node 用のインストールバンドルを再ダウンロードする
  2. 初期インストール時と同様に、インストールバンドル中のプレイブックを実行する
  3. Execution Node で Receptor サービスを再起動する(sudo systemctl restart receptor

手順 3 の手動でのサービス再起動が必要なのが面倒ですが、手順 2 のプレイブックで対応させる(正確にはプレイブックが利用しているコレクションで対応させる)ための IssuePR が既に作成されているので、近い将来には不要になると考えられます。

Execution Node の削除

AWX 側からは、インスタンスを登録した画面で Remove ボタンから削除できます。

ただし、Execution Node 上でプレイブックによって構成された諸々のアンインストールは行われないので、Receptor サービスの停止やアンインストールなど、原状回復は手動での作業が必要です。

receptorctl による確認と調査

デフォルトではインストールされていませんが、Receptor の CLI である receptorctl を導入すると、Receptor の状況や動作確認をもう一歩踏み込んで行えます。

AWX 側では、awx-ee コンテナに導入するとよいでしょう(永続化されないので、Pod の再作成できれいな状態に戻せます)。Execution Node 側では、素直に pip を使いますが、awx ユーザに su してから作業すると後続の作業で困りません。

# AWX 側への receptorctl の導入
kubectl -n awx exec deploy/awx -c awx-ee -- pip install receptorctl

# Execution Node 側への receptorctl の導入(仮想環境作成は省略)
sudo su - awx
pip install receptorctl

導入できたら、次のコマンドで、メッシュネットワークの状況を確認できます。

# AWX 側での実行例
$ kubectl -n awx exec deploy/awx -c awx-ee -- /home/runner/.local/bin/receptorctl --socket /var/run/receptor/receptor.sock status
Warning: receptorctl and receptor are different versions, they may not be compatible
Node ID: awx-667b7b7b54-bwhmv
Version: 1.2.0+g3213360
System CPU Count: 4
System Memory MiB: 7762

Connection              Cost
exec01.ansible.internal 1

Known Node              Known Connections
awx-667b7b7b54-bwhmv    exec01.ansible.internal: 1 
exec01.ansible.internal awx-667b7b7b54-bwhmv: 1 

Route                   Via
exec01.ansible.internal exec01.ansible.internal

Node                    Service   Type       Last Seen             Tags
awx-667b7b7b54-bwhmv    control   Stream     2022-10-07 01:32:20   {'type': 'Control Service'}
exec01.ansible.internal control   StreamTLS  2022-10-07 01:31:33   {'type': 'Control Service'}

Node                    Work Types
awx-667b7b7b54-bwhmv    local, kubernetes-runtime-auth, kubernetes-incluster-auth

Node                    Secure Work Types
exec01.ansible.internal ansible-runner

# Execution Node 側で実行する場合のコマンド(出力例は省略)
$ receptorctl --socket /var/run/receptor/receptor.sock status

細かい説明は割愛しますが、次のコマンドで AWX 側から Execution Node 側への疎通確認も可能です。AWX での Run health check もほぼ同じことをしています。

# Execution Node 側で ansible-runner worker --worker-info を実行して結果を取得する
$ kubectl -n awx exec deploy/awx -c awx-ee -- \
  /home/runner/.local/bin/receptorctl \
  --socket /var/run/receptor/receptor.sock \
  work submit \
  -f \
  --rm \
  --node exec01.ansible.internal \
  --tls-client tlsclient \
  --signwork \
  ansible-runner \
  -n \
  -a params="--worker-info"

{cpu_count: 4, errors: [], mem_in_bytes: 8139796480, runner_version: 2.2.1, uuid: 527e22aa-286b-4e07-bc1c-e7e0ec5ac7e3}

(BMq8X00P, released)

さらに掘り下げたい場合は、Receptor のドキュメント を参照するとよいでしょう。

トラブルシュート

Execution Node が正常に追加できない場合の確認手段をいくつか紹介します。

Receptor サービスの起動状態

前提として、双方の Receptor サービスが起動している必要があります。

AWX 側

AWX 側では、Receptor のプロセスは AWX の Pod の awx-ee コンテナで動作しています。

Receptor のプロセスが動作していないとそもそも AWX の Pod が Running にならないため、AWX 自体が動作していれば Receptor も動作していると判断できます。

Execution Node 側

Execution Node 側では、Receptor は systemd で管理されます。起動状態は systemctl で確認できます。

sudo systemctl status receptor

設定ファイルの編集後や証明書の更新後などは、再起動が必要です。

sudo systemctl restart receptor

Receptor のログ

Receptor のメッシュネットワークを構成するノード間のやりとりは、Receptor のログから確認できます。

AWX 側

AWX 側の Receptor のログは、AWX の Pod の awx-ee コンテナのログとして確認できます。デフォルトで debug レベルのログが出力されています。

kubectl -n awx logs -f deploy/awx -c awx-ee

名前解決ができなかったりポートが閉じていたりして何らかの理由で Execution Node に到達できない場合や、証明書に含まれるホスト名が不正な場合、証明書の有効期限が切れている場合などは、このログから確認できます。

Execution Node 側

Execution Node 側では、Receptor のログは /var/log/receptor/receptor.log に出力されます。

tail -f /var/log/receptor/receptor.log

ただし、デフォルトで info レベルの出力のため、やや粒度が荒い状態です。より詳細なログを確認したい場合は、/etc/receptor/receptor.conflog-leveldebug にし、Receptor サービスを再起動します。

$ sudo cat /etc/receptor/receptor.conf
...
- log-level: debug
...

$ sudo systemctl status receptor

なお、systemd 管理下にあるため、ジャーナルログも確認できますが、有益な情報はほとんど含まれないようです。

sudo journalctl -u receptor

Receptor 経由で起動されたプロセスの標準入出力

AWX で使われる Receptor では、Receptor 経由で ansible-runner コマンドが起動できるように構成されていますが、ansible-runner コマンド自体の出力は、前述の Receptor のログには含まれません。Receptor としてのメッシュネットワークの構成が正しくても、それ経由で起動された ansible-runner でエラーが発生している場合、Receptor のログだけでは判別は困難です。

Receptor は、ノード間でやりとりするデータ、すなわち起動されるプロセスの標準入出力のダンプを、一時ディレクトリに保存します。Receptor 経由で起動されたプロセスのエラーが疑われる場合、これを確認すると原因を探索する助けになることがあります。典型的には、次のようなシーンです。

  • AWX 側で Execution Node が Unavailable
  • AWX 側で Run health check をしても、exit status 1 としか表示されず詳細がわからない
  • Receptor のログにも助けになる情報がない

一時ディレクトリは、AWX 側(awx-ee コンテナ)、Execution Node 側とも共通で、既定で /tmp/receptor/<ノード名>/<ユニット ID> です。ファイル stdinstdout に標準入力と標準出力がダンプされます。以下は Execution Node 側の出力例です。

$ sudo ls -lR /tmp/receptor/exec01.ansible.internal
/tmp/receptor/exec01.ansible.internal:
total 0
drwx------. 2 awx awx 66 Oct  7 08:59 ZKLrOH9m

/tmp/receptor/exec01.ansible.internal/ZKLrOH9m:
total 248
-rw-------. 1 awx awx    182 Oct  7 08:59 status
-rw-------. 1 awx awx      0 Oct  7 08:59 status.lock
-rw-------. 1 awx awx 118050 Oct  7 08:59 stdin
-rw-------. 1 awx awx  78388 Oct  7 08:59 stdout

ただし、ユニット ID はやりとりが発生するごとに変わる上、AWX 経由で発生した処理では終了後にユニット ID のディレクトリも即座に削除されてしまうため、このファイルが確認できる時間は非常に短期間です。

このため、ユニット ID の都度確認はせずに、タイミングを見計らって次のようなコマンドで再帰で全ファイルを舐めるとラクです。例えば、AWX 側で Run health check をしてすぐに Execution Node 側でこれを実行すれば、ansible-runner の出力も確認できるでしょう。

grep -R "" /tmp/receptor/<ノード名>

以下は無理やり ansible-runner をコケさせたときの例です。具体的なコケ方を Python の Traceback まで追いかけられることがわかります(この例では、/home/awx のパーミッションがおかしい状態です)。

$ sudo grep -R "" /tmp/receptor/exec01.ansible.internal
/tmp/receptor/exec01.ansible.internal/FW69cPcD/status:{"State":3,"Detail":"exit status 1","StdoutSize":630,"WorkType":"ansible-runner","ExtraData":null}
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:Traceback (most recent call last):
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:  File "/usr/local/bin/ansible-runner", line 8, in <module>
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:    sys.exit(main())
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:  File "/usr/local/lib/python3.9/site-packages/ansible_runner/__main__.py", line 745, in main
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:    uuid = ensure_uuid()
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:  File "/usr/local/lib/python3.9/site-packages/ansible_runner/utils/capacity.py", line 31, in ensure_uuid
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:    if uuid_file_path.exists():
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:  File "/usr/lib64/python3.9/pathlib.py", line 1424, in exists
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:    self.stat()
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:  File "/usr/lib64/python3.9/pathlib.py", line 1232, in stat
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:    return self._accessor.stat(self)
/tmp/receptor/exec01.ansible.internal/FW69cPcD/stdout:PermissionError: [Errno 13] Permission denied: '/home/awx/.ansible_runner_uuid'

まとめ

AWX 21.7.0 でアナウンスされた Execution Node の概要と構成方法、使い方を簡単に紹介しました。構成の幅が広がりそうなおもしろい機能です。

大規模な環境で拡張性を向上させるだけでなく、小規模な環境であっても隔離ネットワークへの踏み台としての使い道はありそうです。

前述のとおり、21.7.0 の段階では、証明書の期限の問題があるため本格的な活用は少し難しいかもしれませんが、次のリリース以降では安心して使えるようになりそう(追記: この問題は 21.8.0 で修正されました)なので、要件に応じて便利に使っていけるとよいですね。

@kurokobo

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

「AWX に Kubernetes クラスタ外のスタンドアロン実行ノードを追加する」への3件のフィードバック

  1. Hello,
    Thanks for the post, it was really helpful!
    I am trying to get an execution node working and it is in ready state, but failing to launch the job:
    SSH password:
    [WARNING]: * Failed to parse /runner/inventory/hosts with script plugin:
    problem running /runner/inventory/hosts –list ([Errno 13] Permission denied:
    ‘/runner/inventory/hosts’)
    [WARNING]: * Failed to parse /runner/inventory/hosts with yaml plugin: We were
    unable to read either as JSON nor YAML, these are the errors we got from each:
    JSON: Expecting value: line 1 column 1 (char 0) Syntax Error while loading
    YAML. did not find expected key The error appears to be in
    ‘/runner/inventory/hosts’: line 3, column 51, but may be elsewhere in the file
    depending on the exact syntax problem. The offending line appears to be: #
    -*- coding: utf-8 -*- print(‘{“all”: {“hosts”: [“lab01”]},
    “_meta”: {“hostvars”: {“lab01”: {“remote_tower_enabled”: “true”,
    “remote_host_enabled”: “true”, “remote_host_id”: 8343}}}}’)
    ^ here We could be wrong, but this one looks like it might be an issue with
    unbalanced quotes. If starting a value with a quote, make sure the line ends
    with the same set of quotes. For instance this arbitrary example: foo:
    “bad” “wolf” Could be written as: foo: ‘”bad” “wolf”‘
    [WARNING]: * Failed to parse /runner/inventory/hosts with ini plugin: host
    range must be begin:end or begin:end:step
    [WARNING]: Unable to parse /runner/inventory/hosts as an inventory source
    ERROR! No inventory was parsed, please check your configuration and options.

    I do have inventory added but still says no inventory was parsed. Anything I can do to fix this?

    Thanks

  2. @mumoham
    Hi, thanks for referring to my post.
    I see you’ve commented on the issue on AWX repo (ansible/awx#13195), so this my comment is summary of the solution for that issue.

    Your error usually related to combination of AWX_ISOLATION_BASE_PATH and noexec mount option.
    You should review your AWX_ISOLATION_BASE_PATH (“Settings” > “Job Settings” > “Job execution path”) and that path is mounted with noexec option on your execution node (by mount | grep "Your AWX_ISOLATION_BASE_PATH").

    If your AWX_ISOLATION_BASE_PATH (/tmp by default) is mounted with noexec, this is the cause of your issue.

    $ sudo mount | grep /tmp
    /dev/vdb on /tmp type ext4 (rw,noexec,relatime,seclabel)
    

    To solve this,

    • Modify your AWX_ISOLATION_BASE_PATH to the path where mounted without noexec option, e.g. /var/tmp

    or,

    • Remount your AWX_ISOLATION_BASE_PATH without noexec option

    Hope this helps.

  3. Oh I didn’t check the solution was provided on AWX repo. Yes it worked when i changed the job execution path to /var/tmp . Thanks for the solution.

コメントを残す

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