Architecture: additive EN/HN join for containerized AAP¶
Scope: containerized AAP 2.6+ only. RPM is not supported. OpenShift/operator-based node setup should use the native AAP UI workflow: AAP 2.6, AAP 2.7. Historical installer research (including RPM) lives in FINDINGS.md.
Goals¶
- Add execution or hop nodes on containerized AAP without re-running the full installer.
- Avoid restarting gateway / controller / hub / EDA / redis.
- Reuse the containerized installer join pattern (mesh cert tree +
ansible.containerized_installer.receptorfromaap_setup_dir). - Discover / register / verify via
awx-manageonautomationcontroller(SSH +podman exec; no gateway token). - Reuse the installer inventory as topology source of truth; only act on net-new / incomplete hosts.
Non-goals¶
- OpenShift / operator clusters
- RPM installer trees
- AAP 2.5 and earlier (including pre-gateway / 2.4)
- Greenfield full installs (
setup.sh/ fullinstall.yml) - Automated deprovision / rollback playbook (manual steps in TROUBLESHOOTING.md)
- Gateway OAuth /
ansible.controller.instanceregistration - Putting mesh CA private key paths into the installer inventory
Inventory model¶
Source of truth = the same containerized installer inventory (controllers, hops, execution nodes — including hosts you are about to add).
- Add the new host(s) to
[execution_nodes](receptor_type='hop'|'execution',receptor_peersas a real list — INIreceptor_peers='["peer.example.com"]'or YAML list, etc.). - Playbook diffs inventory against
awx-manage list_instances. - Dynamic group
aap_add_node_targets= hosts without a heartbeat yet. - Mint / register / receptor install run only against that group.
- Existing nodes are context (peers, mesh CA) and are not reconfigured unless
enable_controller_listenerswaps AIOlocal-only→tcp-listener.
┌───────────────────┐ ┌─────────────────────────┐
│ Installer │ │ awx-manage │
│ inventory │────▶│ list_instances │
└───────────────────┘ └───────────┬─────────────┘
│ │
│ ▼
│ ┌─────────────────────┐
│ │ Diff │
│ └─────────┬───────────┘
│ │
▼ ▼
┌───────────────────┐ ┌─────────────────────────┐
│ Peer / CA context │ │ aap_add_node_targets │
└───────────────────┘ └─────────┬───────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌─────────────┐ ┌────────────┐
│Mint certs│ │awx-manage │ │Install │
│ │ │register │ │receptor │
└──────────┘ └─────────────┘ └────────────┘
Out of the installer inventory¶
| Material | Where |
|---|---|
| Mesh CA private key | Fetched to control host bundles/_mesh_material/ or override paths |
aap_add_node_* toggles |
Extra-vars / vault (examples/add_node.secrets.yml) |
ansible-playbook playbooks/add_node.yml \
-i /path/to/containerized-setup/inventory \
-e aap_setup_dir=/path/to/containerized-setup \
-e @examples/add_node.secrets.yml
Installer directory (aap_setup_dir)¶
Required: extracted containerized setup tree with ansible.containerized_installer.
| Mode | What it supplies |
|---|---|
| Online | Registry/image defaults from installer common role |
| Bundle | bundle/images/*.tar.gz via bundle_dir / bundle_install |
Never invoke setup.sh / full install.yml from that tree for an additive join.
Controller HA¶
Tasks use groups['automationcontroller'][0]. Any controller is fine because awx-manage updates the shared Controller DB; [0] is only a stable SSH/podman exec target. Mesh CA is expected under each controller install-user home (~/aap/tls/…).
Control host¶
The control host is wherever you run ansible-playbook — not necessarily a controller. Supported patterns:
| Control host | Typical inventory | Notes |
|---|---|---|
| Laptop / bastion | Full cluster inventory; SSH to all targets | Default mental model |
Gateway (automationgateway) |
Often ansible_connection=local on the gateway host |
Plays use connection: local; delegated tasks must SSH to controllers and new nodes |
| Controller (AIO or HA) | Controller may have ansible_connection=local |
Delegation to self uses local connection from inventory |
| Execution / hop node | Same as laptop if SSH keys reach peers | No requirement to run from a controller |
Early plays use hosts: localhost + connection: local on the control machine. Remote work (awx-manage, mesh CA fetch, EN/HN preflight) uses delegate_to with connection: "{{ hostvars[target].ansible_connection | default('ssh') }}". See CONVENTIONS.md.
Zero / near-zero downtime¶
Prefer outbound peer dial: new node → existing hop/controller listener (receptor_peers). Existing receptor.conf files stay untouched.
Inbound dial (controllers peer to the new node) requires controller receptor edits/reloads — supported only for the AIO local-only → tcp-listener helper.
Failed joins¶
- No auto-deprovision on failure.
- Re-run skips hosts that already show a heartbeat.
- Provisioned-but-incomplete hosts (no heartbeat) are re-targeted;
provision_instanceis idempotent. - Manual undo:
awx-manage deprovision_instance --hostname=…— see TROUBLESHOOTING.md.
Role ownership¶
| Concern | Owner |
|---|---|
| Setup tree validation | validate_setup_dir |
| SSH + task container preflight | assert_controller_container |
| Discovery / verify | list_instances + discover_new_nodes / verify_mesh |
| Mesh CA + work pubkey | fetch_mesh_material |
| Node TLS mint | fetch_or_mint_certs |
| Host prep / images | host_prep → ansible.containerized_installer.common |
| DB registration | register_instance → awx-manage |
| Receptor container | install_receptor_node → ansible.containerized_installer.receptor |
See COLLECTION_MAP.md for mermaid diagrams of the playbook flow.
Known gaps¶
- Cluster (multi-controller) topologies lab-validated on containerized AAP 2.7 (RHEL 10 HA, 2026-08-12). 2.6 cluster still open.
- No automated integration tests against a live AAP cluster (CI covers lint/build/changelog).