AWX の Automation Mesh で Hop Node と Mesh Ingress を使う

はじめに

AWX の Automation Mesh を構成する機能として、過去に紹介した Execution Node に加えて、Hop NodeMesh Ingress が最近のリリースで追加されました。

関連する情報は AWX のドキュメントAWX Operator のドキュメント にもまとまっていますが、本エントリでは Mesh Ingress を中心にこれらの新機能を簡単に紹介します。

Execution Node と Hop Node

Automation Mesh を構成する基本のノードは、Execution Node と Hop Node の二種類です。Execution Node は AWX の 21.7.0 で、Hop Node は 23.0.0 でそれぞれ追加されました。

Execution Node については 過去のエントリ でも触れましたが、まずは基本となるこの二種類を改めて簡単に紹介します。関連する公式のドキュメントは以下です。

概要

Execution Node

Execution Node は、Kubernetes クラスタの外に構成されたスタンドアロンのインスタンスで、AWX で起動されたジョブをクラスタ外のリモートホスト上で実行するために使用します。

ネットワークの都合で AWX からは直接到達できないターゲットノードに対しても、Execution Node を介してプレイブックを実行できます。

Execution Node には、インストールバンドルにより、Receptor と Ansible Runner、Podman が導入されます。AWX でジョブを起動すると、Podman のコンテナとして Execution Environment(EE)が起動し、その中でプレイブックが実行されます。

Hop Node

Hop Node は、AWX と Execution Node の間の通信を中継するためのインスタンスです。Execution Node と同様、Kubernetes クラスタの外にスタンドアロンで構成します。

Execution Node が実装された当初は、Execution Node は AWX から直接到達できる範囲にしか配置できませんでした。Hop Node により、任意の数の Hop Node を AWX と Execution Node の間に配置して、ジョブをさらに遠くのネットワークまで届けられるようになりました。

Hop Node には、インストールバンドルにより Receptor のみが導入されます。Execution Node と異なり Ansible Runner と Podman は導入されず、ジョブの実行機能は持ちません。

構成方法

例として、Execution Node と Hop Node を含んだ Automation Mesh を実際に構成します。

トポロジの設計

まずは Automation Mesh のトポロジを設計します。今回は下図の構成で考えます。Execution Node をふたつ用意し、ひとつは AWX から直接、もうひとつは Hop Node を経由してつながるものとします。

トポロジを設計する際は、ノード間の接続方向図中の矢印)も設計が必要です。これは、Automation Mesh のバックエンド接続の方向で、ノードの間に ファイアウォールが配置されている場合の穴あけの方向 に相当します。

ただし、AWX からその隣接ノードへ必ずアウトバウンド方向 で設計します。AWX 以外のノード間の接続方向は任意 です。これを踏まえて、上図にホスト名とポート番号を加えて少しだけ細かくした図が以下です。

ホストの準備

RHEL ファミリのホストを用意して、ネットワークを設定します。経路上やホスト上でファイアウォールを利用している場合は、前述の接続方向にあわせて穴をあけておきます。

インスタンスの追加

設計したすべてのノードを、AWX の Web UI からインスタンスとして追加します。

  1. Administration > Instances > Add から、Create new Instance 画面を開きます。
  2. 次の通りに構成します。
    • Host Name として、そのノードに接続しにくる側(矢印の根元側)で名前解決可能なホスト名(または IP アドレス)を指定します。
    • Listener Port は、そのノードが 接続を受ける側(矢印の先端側)の場合 に入力します。標準は 27199 です。
    • Instance Type は、そのノードにあわせて ExecutionHop から選択します。
    • そのノードが AWX の隣接ノードの場合 は、Peers from control nodes にチェック を入れます。
  3. Save をクリックします。

ノードの数だけこれを繰り返して、全ノードがインスタンスとして登録された状態にします。

ピアの構成

インスタンスを全て追加し終わったら、それぞれのインスタンスの ピア を指定します。つまり、インスタンス同士を設計通りに矢印でつなぐ作業 です。

  1. Administration > Instances で、接続しにいく側(矢印の根元側)のインスタンスを選択し、Peers タブを開きます。
  2. Associate をクリックし、接続を受ける側(矢印の先端側)のインスタンスにチェックを入れて Save をクリックします。

AWX から直接つながるノード(今回の例では exec01hop01)へのピア設定は、インスタンスの追加時に Peers from control nodes にチェックをいれたことで完了しています。したがって、今回の構成では hop01.ansible.internalPeers タブで exec02.ansible.internal を追加すれば完了です。

矢印の数だけこの作業を繰り返せば、Automation Mesh のトポロジが完成します。トポロジは、Administration > Topology View で確認できます。この画面でも、接続の方向は矢印で表現 されているため、正しく構成できていることを線のつながりと矢印の方向で確認します。

インストールバンドルの実行

トポロジが設計通りに完成したら、それぞれのインスタンスの詳細ページからインストールバンドルをダウンロードし、inventory.ymlansible_user を修正したうえでプレイブックを実行します。

$ ansible-galaxy collection install -f -r requirements.yml
...

$ ansible-playbook -i inventory.yml install_receptor.yml
...

なお、インストールバンドルにはそのインスタンス専用の証明書やピアの情報が含まれているため、使いまわしはできません。全インスタンスについて個別にインストールバンドルをダウンロードして実行します。

インストール後、しばらくして各インスタンスの StatusReady になれば正常です。

インスタンスグループの追加

インスタンスグループを作成して、Execution Node を追加します。

  1. Administration > Instance Groups > Add > Add instance group をクリックします。
  2. Name を入力して Save をクリックします。
  3. Instances タブを開きます。
  4. Associate をクリックし、任意の Execution Node にチェックを入れて Save をクリックします。

グループに複数の Execution Node が含まれる場合は、ジョブの実行にはいずれかのインスタンスが利用されます。厳密に特定の Execution Node のみでジョブを実行させたい場合は、インスタンスがひとつだけのグループを作成します。

ジョブの実行

作成したインスタンスグループをジョブテンプレートの Instance Groups で指定することで、ジョブが Execution Node で実行されるようになります。

ジョブを実行して、Execution Node 側の awx ユーザで Podman の状態を確認すると、Execution Environment が動作している様子が観察できます。

[awx@exec01 ~]$ podman ps
CONTAINER ID  IMAGE                          COMMAND               CREATED       STATUS           PORTS       NAMES
a02702fb0b21  quay.io/ansible/awx-ee:latest  ansible-playbook ...  1 second ago  Up 1 second ago              ansible_runner_1
[awx@exec02 ~]$ podman ps
CONTAINER ID  IMAGE                          COMMAND               CREATED       STATUS           PORTS       NAMES
40ef61ed9c60  quay.io/ansible/awx-ee:latest  ansible-playbook ...  1 second ago  Up 1 second ago              ansible_runner_2

Mesh Ingress

AWX の 23.8.0 で、Automation Mesh を構成する要素として新しく Mesh Ingress が追加されました。関連ドキュメントは以下です。

概要

これまでの Execution Node と Hop Node を使った Automation Mesh では、AWX とそれに隣接するノードの接続は、前述の通り、AWX からみてアウトバウンド方向、つまり、Kubernetes クラスタから出ていく方向でしか構成できません でした。

これを解決するのが Mesh Ingress で、これは Kubernetes クラスタ外からのインバウンド方向の接続を受け入れる Hop Node の一種 です。Hop Node の一種ですが、Hop Node とは異なり Kubernetes クラスタ内の Pod として動作します。

これにより、ネットワークポリシの都合で Kubernetes クラスタからのアウトバウンド方向のピアリングが許容されない場合でも、Automation Mesh を構成できるようになりました。

構成方法

MeshIngress を構成するには、AWX Operator を利用します(がんばれば手動でもできますがまったくおすすめしません)。2.11.0 以降で、新しい CR である AWXMeshIngress が定義できるようになっています。

なお、CR の詳細や例、デプロイ方法などは AWX Operator のドキュメント にも記載があります。

トポロジの設計

今回は、前述の図の通りの構成を目指します。Mesh Ingress が クラスタ外のノードからインバウンド方向で接続されている 点が重要です。

技術的な実装は後述しますが、Mesh Ingress はバックエンドに(Executiton Node や Hop Node のような TCP ではなく)WebSocket を利用しています。上図をもう少し詳しくしたものが以下です。

以降、Mesh Ingress の構成に絞って紹介します。Mesh Ingress 以外の Execution Node と Hop Node の構成方法は、前述した従来の手順と同じです。

CR の作成

今回の構成に合わせた CR は以下の通りです。これは Traefik 用ですが、Nginx など他の Ingress Controller でも適切に CR をカスタマイズすれば動作します(ドキュメント にいくつか CR の例を 増やす予定 です)。

---
apiVersion: awx.ansible.com/v1alpha1
kind: AWXMeshIngress
metadata:
  namespace: awx
  name: inbound-hop01
spec:
  deployment_name: awx

  ingress_type: IngressRouteTCP
  ingress_controller: traefik
  ingress_class_name: traefik
  ingress_api_version: traefik.io/v1alpha1

  external_hostname: inbound-hop01.ansible.internal

deployment_name で Mesh Ingress を追加する AWX のインスタンス名を指定し、external_hostname でクラスタ外からの接続を受け入れるための FQDN を指定します。併せて、接続点となる Ingress(OpenShift の場合は Route)の構成のため、今回使う Traefik にあわせて ingress_typeingress_api_version を指定しています。

これを作成すると、AWX Operator がいろいろして、デプロイが完了します。

$ kubectl -n awx logs deployments/awx-operator-controller-manager
...
----- Ansible Task Status Event StdOut (awx.ansible.com/v1alpha1, Kind=AWXMeshIngress, inbound-hop01/awx) -----
PLAY RECAP *********************************************************************
localhost                  : ok=19   changed=3    unreachable=0    failed=0    skipped=5    rescued=0    ignored=0
$ kubectl -n awx get deployment,pod,service,ingressroutetcp.traefik.io
NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
...
deployment.apps/inbound-hop01   1/1     1            1           35s

NAME                                READY   STATUS    RESTARTS   AGE
...
pod/inbound-hop01-b858d78cb-89ctn   1/1     Running   0          35s

NAME                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)     AGE
...
service/inbound-hop01   ClusterIP   10.43.165.50   <none>        27199/TCP   36s

NAME                                       AGE
ingressroutetcp.traefik.io/inbound-hop01   37s

AWX Operator は、AWX へのインスタンスの追加と AWX からのピアの設定も実施するため、AWX の Web UI ではこの段階で Mesh Ingress のインスタンスが確認できます。名前は CR の name で指定したもので、Node Typehop です。

Mesh Ingress は CR のデプロイだけで構成が完了 します。Web UI からの操作は不要で、インストールバンドルなども存在しません。したがって、以上で Mesh Ingress の構成は完了です。

他のインスタンスの構成

Mesh Ingress 以外のノードを、前述した従来の手順に従って構成します。Mesh Ingress をピアに持つインスタンスも構成することになりますが、手順は他のインスタンスの場合と同じです。

  • ホストの準備
  • インスタンスの追加
  • ピアの構成
  • インストールバンドルの実行
  • インスタンスグループの追加

適切に構成できれば、Topology View でも Mesh Ingress(inbound-hop01)に矢印が集まっている(インバウンド方向で接続されている)ことが確認できます。

ジョブの実行

インスタンスグループを指定してジョブを起動すると、ジョブが Mesh Ingress を経由して Execution Node まで届けられて実行されたことが、Execution Node の Podman の状態から確認できます。

[awx@exec03 ~]$ podman ps
CONTAINER ID  IMAGE                          COMMAND               CREATED       STATUS            PORTS       NAMES
6b149d385677  quay.io/ansible/awx-ee:latest  ansible-playbook ...  1 second ago  Up 2 seconds ago              ansible_runner_3
[awx@exec04 ~]$ podman ps
CONTAINER ID  IMAGE                          COMMAND               CREATED        STATUS        PORTS       NAMES
77c74eec136d  quay.io/ansible/awx-ee:latest  ansible-playbook ...  2 seconds ago  Up 2 seconds              ansible_runner_4

参考: 技術的な補足

Mesh Ingress 周辺の技術的ないろいろです。

Mesh Ingress の実装のモチベーション

Mesh Ingress のない Automation Mesh では、AWX とそれに隣接するノードの接続は、AWX からみてアウトバウンド方向 でしか構成できません。これは、コントロールプレーンのレプリカ数が 2 以上のとき に、インバウンド方向のバックエンド接続を維持できなくなる ことが理由です。

Receptor は、複数のノードを仮想的にひとつのノードとして扱うような冗長化の機能を持ちません。すべてのノードは、それぞれが独立した別のノードとしてメッシュに参加します。

ここで、コントロールプレーンのレプリカ数が 2 以上の場合、すなわち、Task の Deployment の replicas2 以上の場合を考えます。Deployment を外部に公開する場合、一般的には図のように Service を構成し、外部からの通信がいずれかの Pod へロードバランスされる状態 にします。

ここで、仕様上、Receptor としては二つの Pod 内の Receptor がそれぞれ別のノードとして扱われる 点に注意します。Receptor としては上図は 3 ノードのメッシュであり、インバウンド方向でピア接続を確立する には、クラスタ外のノードはこの二つの Pod と明示的かつ別々に通信 できなければなりません。つまり、ロードバランスされると通信の到達先が意図せずに切り替わって困る ことになります。

したがって、クラスタ外からのピア接続を受け入れるには、Receptor としては Pod と Service が一対一で対応していることが必須 で、通常の Web アプリケーションなどではたいへん便利な Kubernetes のレプリカ機能も、Receptor とはすこぶる相性が悪いということになります。

こうした背景から、Pod と Service が必ず一対一になるようレプリカ数を 1 に固定した Deployment を作って、それにクラスタ外からの接続を引き受けさせようとして実装されたのが Mesh Ingress です。

Mesh Ingress の仕組み

Mesh Ingress の内部実装を少し掘り下げて紹介します。

Kubernetes 上に作成されるリソース

Mesh Ingress を Kubernetes 上にデプロイすると、AWX Operator によって次のリソースが作成されます。

  • Receptor が動作する DeploymentPod
  • Pod のポートをクラスタ内に公開する Servicetype: ClusterIP
  • Service を外部に公開する Ingress(または Route
  • Deployment を起動する Service Account
  • Receptor の設定ファイルを含む ConfigMap
$ kubectl -n awx get deployment,pod,service,ingressroutetcp.traefik.io,serviceaccount,configmap
NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
...
deployment.apps/inbound-hop01   1/1     1            1           35s

NAME                                READY   STATUS    RESTARTS   AGE
...
pod/inbound-hop01-b858d78cb-89ctn   1/1     Running   0          35s

NAME                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)     AGE
...
service/inbound-hop01   ClusterIP   10.43.165.50   <none>        27199/TCP   36s

NAME                                       AGE
ingressroutetcp.traefik.io/inbound-hop01   37s

NAME                           SECRETS   AGE
...
serviceaccount/inbound-hop01   0         37s

NAME                                      DATA   AGE
...
configmap/inbound-hop01-receptor-config   1      36s

Pod で利用されるイメージはデフォルトで quay.io/ansible/awx-ee:latest です。AWX 本体の CR に control_plane_ee_image の指定があればそれが利用 されます。

Receptor の設定

Mesh Ingress 用の Receptor の設定ファイルは次の通りです。ConfigMap か Pod 内の /etc/receptor/receptor.conf で確認できます。

---
- node:
    id: inbound-hop01
- log-level: debug
- control-service:
    service: control
- ws-listener:
    port: 27199
    tls: tlsserver
- tls-server:
    cert: /etc/receptor/tls/receptor.crt
    key: /etc/receptor/tls/receptor.key
    name: tlsserver
    clientcas: /etc/receptor/tls/ca/mesh-CA.crt
    requireclientcert: true
    mintls13: false

Listener として ws-listener が定義されており、WebSocket をポート 27199 で待ち受けている ことがわかります。ここには tls が指定されているため、実際には WebSocket over TLS(WSS)です。

Receptor 用の証明書

前述の tls-server の定義の通り、隣接ノードとの接続時には相互に証明書の検証が行われるように設定されています。

Mesh Ingress は、AWX 本体が利用している Receptor の CA 証明書と秘密鍵(Kubernetes 上の Secret リソース)をそのままマウントしています。また、それを使って 自分自身の起動時に自分自身用のサーバ証明書を生成 しています。

このサーバ証明書の有効期限は一年間と比較的短いですが、Pod が再作成されるたびに証明書も再生成されるため、同一の Pod が一年以上動作し続けない限りは失効しません(念のため 延長する是非を伺い中 です)。

証明書のマウントっぷりや証明書を生成するコマンドは、Deployment の定義から確認できます。

$ kubectl -n awx get deployment/inbound-hop01 -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  ...
  name: inbound-hop01
  ...
spec:
  ...
  template:
    ...
    spec:
      containers:
      - args:
        - /bin/sh
        - -c
        - |
          internal_hostname=inbound-hop01
          external_hostname=inbound-hop01.ansible.internal
          receptor --cert-makereq bits=2048 \
            commonname=$internal_hostname \
            dnsname=$internal_hostname \
            nodeid=$internal_hostname \
            dnsname=$external_hostname \
            outreq=/etc/receptor/tls/receptor.req \
            outkey=/etc/receptor/tls/receptor.key
          receptor --cert-signreq \
            req=/etc/receptor/tls/receptor.req \
            cacert=/etc/receptor/tls/ca/mesh-CA.crt \
            cakey=/etc/receptor/tls/ca/mesh-CA.key \
            outcert=/etc/receptor/tls/receptor.crt \
            verify=yes
          exec receptor --config /etc/receptor/receptor.conf
        image: quay.io/ansible/awx-ee:latest
        ...
        name: inbound-hop01-mesh-ingress
        ...
        volumeMounts:
        - mountPath: /etc/receptor/receptor.conf
          name: inbound-hop01-receptor-config
          subPath: receptor.conf
        - mountPath: /etc/receptor/tls/ca/mesh-CA.crt
          name: inbound-hop01-receptor-ca
          readOnly: true
          subPath: tls.crt
        - mountPath: /etc/receptor/tls/ca/mesh-CA.key
          name: inbound-hop01-receptor-ca
          readOnly: true
          subPath: tls.key
        - mountPath: /etc/receptor/tls/
          name: inbound-hop01-receptor-tls
      ...
      volumes:
      - emptyDir: {}
        name: inbound-hop01-receptor-tls
      - name: inbound-hop01-receptor-ca
        secret:
          defaultMode: 420
          secretName: awx-receptor-ca
      - configMap:
          defaultMode: 420
          items:
          - key: receptor_conf
            path: receptor.conf
          name: inbound-hop01-receptor-config
        name: inbound-hop01-receptor-config
...

args で指定されているコマンドの通り、Mesh Ingress が利用するサーバ証明書は、CN だけでなく SAN も使って 内部用と外部用の二つのホスト名の正当性を保証 するように作られます。今回の例では、内部用が inbound-hop01 で、外部用が FQDN の inbound-hop01.ansible.internal です。

実際、証明書の内容を確認するとそのようになっています。

$ kubectl -n awx exec -it deployment/inbound-hop01 -- openssl x509 -text -in /etc/receptor/tls/receptor.crt -noout
Certificate:
    Data:
        ...
        Issuer: CN = awx Receptor Root CA
        Validity
            Not Before: Feb 16 13:00:59 2024 GMT
            Not After : Feb 16 13:00:59 2025 GMT
        Subject: CN = inbound-hop01
        ...
        X509v3 extensions:
            ...
            X509v3 Subject Alternative Name: 
                DNS:inbound-hop01, DNS:inbound-hop01.ansible.internal, othername: 1.3.6.1.4.1.2312.19.1::inbound-hop01
    ...

ホスト名が二種類用意されているのは、Mesh Ingress に対して隣接ノードが接続する際に 二種類のホスト名が使われる からです。

隣接ノードとの接続

Mesh Ingress は、クラスタ内で AWX からの接続を受け付けるだけでなく、クラスタ外の他のノードからの接続も受け付けます。このとき、クラスタ外の他のノードは Ingress(または Route)を宛先に接続するしかありませんが、AWX は同じクラスタ内にいる ため Service を宛先にして接続 しています。

このため、AWX が Mesh Ingress に接続するときは Service の名称 が、クラスタ外のノードが接続するときは FQDN が利用されます。したがって、Mesh Ingress はこの二種類のホスト名の正当性をどちらも証明できる必要があり、これが証明書に二種類のホスト名が含まれていた背景です。

この二つのホスト名やポート番号の実際の値は、Mesh Ingress のインスタンスの Listener Addresses タブで確認できます。

Mesh Ingress 用の Ingress リソースを自製するには

Ingress Controller の都合や環境の制約など、何らかの理由で、AWX Operator で作成できる Ingress では不十分な場合は、Ingress のみ自製すれば対応できます。CR で ingress_typenone(デフォルト)にすると、AWX Operator は Ingress をデプロイしません。なお、external_hostname は、Receptor の証明書の生成にも使用されるため、依然として指定は必要です。

---
apiVersion: awx.ansible.com/v1alpha1
kind: AWXMeshIngress
metadata:
  name: inbound-hop01
spec:
  deployment_name: <awx instance name>

  ingress_type: none
  external_hostname: inbound-hop01.ansible.internal

自製する Ingress の要件は大まかには次の通りです。type: ClusterIP の Service が Operator により作成されるので、これをバックエンドに指定します。

  • WebSocket に対応していること
  • 443 番ポートでアクセスできること
  • TLS パススルーを有効にすること(TLS の終端は Receptor が行うため)
  • Mesh Ingress 用の Service の 27199 番ポートにルーティングすること
  • AWXMeshIngressexternal_hostname と同じホスト名を使っていること

ドキュメント に例を載せる 予定 です。

Mesh Ingress を削除するには

CR AWXMeshIngresskubectl delete で削除するだけです。

CR の Finalizer が AWX Operator により起動し、Kubernetes 上のリソースの削除だけでなく、AWX からのインスタンスの削除も実行されます。

各ノードを冗長化するには

前述の理由から、Mesh Ingress のレプリカ数は 1 で固定で増やせません。

このため、Mesh Ingress 自体を冗長化したい場合は、別の Mesh Ingress を追加でデプロイして Automation Mesh に参加させることで対応します。

Execution Node や Hop Node を冗長化させたい場合も、隣にもう一台同じ役割のものを追加で用意して Automation Mesh に参加させるだけです。Execution Node は、冗長化させたい複数台を同じインスタンスグループに含めることで、一方で障害が発生しても他方で実行される状態を保てます。

おわりに

AWX で Automation Mesh を構成するための Execution Node、Hop Node、Mesh Ingress のそれぞれを紹介しました。これまで以上に柔軟な構成が取れるようになり、AWX が使える範囲も広がりそうです。

趣味の一環で Mesh Ingress のアーキテクチャの検討 には初期のころから参加していましたが、出した案がわりと素直に採用されておもしろかったです。AWX Operator 側の実装も、Ingress 周辺を少しだけ担当しています。Nginx と Traefik 以外の Ingress Controller はまったくテストできていないので、不具合があったら Issue を作って教えてください。

@kurokobo

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

コメントを残す

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