The universe, as it turns out, is big. Really big. And somewhere in that vast expanse of chaos sits your friendly neighborhood automation engineer, clutching a coffee-stained YAML file and muttering something about "dependency hell."
For a year now, I've lived happily on Red Hat Ansible Automation Platform 2.5 RPM running on Red Hat Enterprise Linux 9 (RHEL). A somewhat volatile yet exciting planet in an otherwise noisy galaxy. My jobs ran on schedule, my logs were mostly legible, and my PostgreSQL database hummed like a contented Vogon. Life was adequate.
Then came Red Hat Ansible Automation Platform 2.6, with a promise of Lightspeed intelligent assistant, a self-service Portal, and an automation dashboard that made me question why I ever tolerated the old one.
Naturally, I ignored it for as long as possible because like most engineers, I operate on a simple principle: If it ain't on fire, don't touch it. But then one day, I glanced at the release notes and saw the words "RHEL 10 support" and "container deployment", which reminded me that my current environment would eventually become obsolete.
And so began my journey, leaving behind the RPM comforts of RHEL 9 for the gleaming, containerized wilds of RHEL 10. Yes, I could have launched this adventure on RHEL 9, but I figured if I was already venturing into the unknown, I might as well go one operating system release further.
This is a story of YAML, Podman, and the occasional existential crisis. Grab your towel and make a fresh backup, because we're about to migrate.
Why upgrade now?
In short, when Red Hat Ansible Automation Platform 2.7 rolls out, the RPM install becomes officially deprecated. Translation: it's time to stop inviting it to new projects.
Also, there are lots of great new features that easily convinced me to upgrade, and I think you might agree. For more information, read the release notes.
There are several official moving routes. If you're still on RHEL 8 and plan to upgrade to Red Hat Ansible Automation Platform 2.6, then you must be on install RHEL 9 or greater.
My lab setup: The "before" times
I started with six virtual machines (VM) on RHEL 9 running Ansible Automation Platform 2.5 (RPM bundle):
- Controller
- Gateway
- Private automation hub
- Event-Driven Ansible controller
- Execution node
- PostgreSQL database (installer-managed)
I like symmetry, so my target environment consists of six new RHEL 10 VMs ready to host Ansible Automation Platform 2.6 (container bundle). By using containers for each component, you can create a tiny ecosystem of cooperative services (my private automation hub alone hosts Redis, web, and content containers, all pretending to get along).
The best part? You can patch or reboot those VMs and Ansible doesn't even flinch. It just keeps automating, completely unbothered by your maintenance window.
Patch away, my friends. Patch away. Just keep one eye on Podman—it's the one actually keeping your containers from staging a coup.
Out-of-scope components
You must recreate some components manually after migration.
- Event-Driven Ansible configuration
- Instance groups
- Hub content
- Custom CA for receptor mesh
- Non-default execution environments
Prerequisites
Before you start, make sure you have satisfied these requirements:
- You have enough storage for database dumps and backups.
- You have network connectivity between source and target.
- Backup! Yes, seriously, make a backup before beginning.
- On the source: Update your Ansible Automation Platform 2.5 RPM deployment to the latest release.
- On the target: RHEL 10 VMs are prepped for the container bundle.
- Download the latest Ansible Automation Platform 2.5 container installer bundle.
- Copy your binaries to the target. I went ahead and put mine on the soon-to-be controller node, letting it get familiar with its responsibilities early.
Migration sequence
- Prepare and assess source environment
- Export source environment
- Create and verify migration artifact
- Prepare target environment
- Import migration content
- Reconcile target post-import
- Validate target environment
On source
First things first, let's take a backup of the source environment because confidence is great, but recoverability pays the bills.
On the source environment (where Ansible Automation Platform is already living its best life), change into the directory containing setup.sh and your original inventory file from the Ansible Automation Platform 2.5 RPM install. Then run the following command and pretend you're not holding your breath:
./setup.sh -e 'backup_dest=/path/to/backups/' -e 'use_compression=True' -bDatabase verification
Let's start on the source environment. First, verify the version of PostgreSQL is at version 15 from the PostgreSQL node as the postgres ID:
psql -c 'SELECT version();'Collect your secrets and settings
Gather connection info from each component. There are three separate tasks here.
From the controller:
awx-manage print_settings | grep '^DATABASES'From the automation hub:
grep '^DATABASES' /etc/pulp/settings.pyFrom the gateway:
aap-gateway-manage print_settings | grep '^DATABASES'Create the artifact
On your gateway node, stage the files:
mkdir -p /tmp/backups/artifact/{controller,gateway,hub}
mkdir -p /tmp/backups/artifact/controller/custom_configs
touch /tmp/backups/artifact/secrets.yml
cd /tmp/backups/artifactThen craft your sacred secrets.yml file:
awx_pg_database: awx # name of my controller database
automationhub_pg_database: automationhub # name of my hub
automationgateway_pg_database: automationgateway # name of my gateway database
controller_secret_key: "<controller_key>"
hub_secret_key: "<hub_key>"
hub_db_fields_encryption_key: "<hub_db_key>"
gateway_secret_key: "<gateway_key>"Store it safely. You must not lose this, or your migration will fail and become an archaeological dig.
Exporting secrets
It's time to export secrets from your RPM environment. One node from each component group will do. Think of this as an interplanetary scavenger hunt, except instead of prizes, you get YAML entries.
For each of the following, log in as root because this is one of those rare moments where "root or bust" actually applies.
Automation controller secret key
Log in to your automation controller node using SSH and retrieve the secrets key:
cat /etc/tower/SECRET_KEYCopy that value into your secrets.yml file under controller_secret_key. You may feel powerful, but remember… with great SECRET_KEYs comes great responsibility.
Automation hub secret key
Next, head over to your automation hub node and extract its secret key:
grep '^SECRET_KEY' /etc/pulp/settings.py | awk -F'=' '{ print $2 }'Paste that value into your secrets.yml under hub_secret_key. Yes, this command looks like something you'd find scribbled on the back of a napkin at a DevOps meetup, but it works.
Automation hub field encryption key
Still on the automation hub node, fetch your database field encryption key:
cat /etc/pulp/certs/database_fields.symmetric.keyAdd this value to hub_db_fields_encryption_key in your secrets.yml.
Platform gateway secret key
Finally, visit your platform gateway node and get its secret key:
cat /etc/ansible-automation-platform/gateway/SECRET_KEYAdd this to your secrets.yml as gateway_secret_key.
Keep your secrets.yml export safe. You can encrypt it for safety. Keep it close. And for the love of YAML, don't email it to yourself.
Congratulations! You've now completed a full cross-galactic extraction of your platform's hidden knowledge.
Database export
On your gateway node, dump your databases as the postgres user:
psql -h <pg_hostname> -U <component_pg_user> -d <database_name> -t -c 'SHOW server_version;' # ensure connectivity to the databasepg_dump -h pg.example.com -U awx -d awx --clean --create -Fc -f awx.pgc
pg_dump -h pg.example.com -U automationhub -d automationhub --clean \
--create -Fc -f pah.pgc
pg_dump -h pg.example.com -U automationgateway -d automationgateway \
--clean --create -Fc -f gw.pgcls -ld <component>/<component>.pgc
echo "<component>_pg_database: <database_name>" >> secrets.yml ## Add the database name for the component to the secrets fileOnce your databases are exported, place them under /tmp/backups/artifact/{controller,gateway,hub} on the Gateway Node (the directory tree you created earlier). Speaking from experience: If you don't put them there, you definitely won't remember where you put them.
Export automation controller custom configurations
If you've got any custom configurations in /etc/tower/conf.d, now's the time to preserve those. Copy your custom settings into /tmp/backups/artifact/controller/custom_configs. Not everything in that folder counts as a custom configuration. The installer manages a few of those files automatically, so preserve only what you've created yourself. Files you can skip:
/etc/tower/conf.d/postgres.py/etc/tower/conf.d/channels.py/etc/tower/conf.d/caching.py/etc/tower/conf.d/cluster_host_id.py
I had no custom configs, so I skipped this step.
Checksum everything, because trust must be earned. Run these commands on the gateway node where you stage the archive directory and subdirectory:
cd /tmp/backups/artifact/
[ -f sha256sum.txt ] && rm -f sha256sum.txt
find . -type f -name "*.pgc" -exec sha256sum {} \; >> sha256sum.txt
cat sha256sum.txt Here is an example of what the artifact directory tree on your gateway node should look like with all the files:
├── controller │ ├── awx.pgc │ └── custom_configs ├── gateway │ └── gw.pgc ├── hub │ └── pah.pgc ├── secrets.yml └── sha256sum.txt
Compress all artifacts, and then generate a record of its checksum:
tar cf artifact.tar artifact
sha256sum artifact.tar > artifact.tar.sha256Check your hash:
sha256sum --check artifact.tar.sha256Transfer the archive to the target (I sent mine to my controller):
scp artifact.tar <target>:/tmpCongratulations! You've completed everything on the source environment. Now onward to the target environment.
Target environment
On your target machine, extract the archive and verify the checksums—because nothing says "I trust this software" like double-checking it didn't get scrambled in transit. For my setup, I did all of this (and kicked off the install) right from my controller node.
sha256sum --check artifact.tar.sha256
tar xf artifact.tar
sha256sum --check sha256sum.txtCompare inventories using diff, and then install Ansible Automation Platform 2.5 container for RHEL 10. Yes, you must first install version 2.5, but don't panic, we'll upgrade to version 2.6 later.
Run the installer:
ansible-playbook -i inventory ansible.containerized_installer.install \
-e @~/artifact/secrets.yml \
-e "__hub_database_fields='{{ hub_db_fields_encryption_key }}'"Take a backup immediately after:
ansible-playbook -i inventory ansible.containerized_installer.backupImport databases using Podman
First, you must stop containerized services (except databases) in your target environment.
On the controller node:
systemctl --user stop automation-controller-task \
automation-controller-web automation-controller-rsyslog
systemctl --user stop receptorOn your private automation hub node:
systemctl --user stop automation-hub-api \
automation-hub-content automation-hub-web \
automation-hub-worker-1 automation-hub-worker-2On the Event-Driven Ansible controller node:
systemctl --user stop automation-eda-scheduler \
automation-eda-daphne automation-eda-web \
automation-eda-api automation-eda-worker-1 automation-eda-worker-2 \
automation-eda-activation-worker-1 automation-eda-activation-worker-2On the gateway node:
systemctl --user stop automation-gateway automation-gateway-proxyAt this point, your target Ansible Automation Platform 2.5 is in a Zen-like state of nothingness. A perfect time to begin database necromancy.
Start a temporary PostgreSQL container
We'll use a disposable PostgreSQL container to restore our dumps. On the PostgreSQL node, place the artifact.tar.sha256 tarball into your home directory and extract it. Think of it as a time machine built out of Podman and caffeine.
Then run Podman:
podman run -it --rm --name postgresql_restore_temp --network host \
--volume ~/aap/tls/extracted:/etc/pki/ca-trust/extracted:z \
--volume ~/aap/postgresql/server.crt:/var/lib/pgsql/server.crt:ro,z \
--volume ~/aap/postgresql/server.key:/var/lib/pgsql/server.key:ro,z \
--volume ~/artifact:/var/lib/pgsql/backups:ro,z \
registry.redhat.io/rhel8/postgresql-15:latest bashInside the container
Grant each role the CREATEDB power, for a brief time:
psql -h pg.example.com -U postgresRun these SQL commands:
ALTER ROLE awx WITH CREATEDB;
ALTER ROLE automationhub WITH CREATEDB;
ALTER ROLE automationgateway WITH CREATEDB;
ALTER ROLE edacontroller WITH CREATEDB;
\qWith the CREATEDB powers bestowed upon your roles, it's time to venture into /var/lib/pgsql.
cd /var/lib/pgsql/Now perform the sacred restores:
pg_restore --clean --create --no-owner -h pg26.example.com -U awx -d template1 controller/awx.pgc
pg_restore --clean --create --no-owner -h pg26.example.com -U automationhub -d template1 hub/pah.pgc
pg_restore --clean --create --no-owner -h pg26.example.com -U automationgateway \
-d template1 gateway/gw.pgcRevoke the temporary permissions:
psql -h pg26.example.com -U postgresRun these SQL commands:
ALTER ROLE awx WITH NOCREATEDB;
ALTER ROLE automationhub WITH NOCREATEDB;
ALTER ROLE automationgateway WITH NOCREATEDB;
ALTER ROLE edacontroller WITH NOCREATEDB;
\qRestart services
You can now bring your services back. On the controller node:
systemctl --user start automation-controller-task \
automation-controller-web automation-controller-rsyslog
systemctl --user start receptorOn the automation hub node:
systemctl --user start automation-hub-api \
automation-hub-content automation-hub-web \
automation-hub-worker-1 automation-hub-worker-2On the Event-Driven Ansible node:
systemctl --user start automation-eda-scheduler \
automation-eda-daphne automation-eda-web \
automation-eda-api automation-eda-worker-1 automation-eda-worker-2 \
automation-eda-activation-worker-1 automation-eda-activation-worker-2On the gateway node:
systemctl --user start automation-gateway automation-gateway-proxyIf everything starts without smoke or strange noises, congratulations! Your databases have successfully made it safely to their new containerized reality.
The universe hates loose ends. You've restored your databases and restarted services, so now it's time for cleaning up.
De-provision the old gateway configuration
During a migration, the gateway's internal database can accumulate stale or inconsistent objects. Some of them are still clinging to memories of your old cluster topology like a robot refusing to accept that the planet has moved.
From the Gateway node, use Podman to access the gateway container and de-provision the old Gateway configuration, gently reminding it that the past is no longer relevant.
podman exec -it automation-gateway bashaap-gateway-manage migrate
aap-gateway-manage shell_plus>> HTTPPort.objects.all().delete(); ServiceNode.objects.all().delete();
ServiceCluster.objects.all().delete()These objects are automatically recreated when the upgraded/new controller nodes re-register with the gateway.
Transfer custom settings
I didn't have any custom settings, so I skipped this step. However, if you do have custom configs, then you need to open your inventory file on your target environment and apply any relevant extra settings to each component using the component_extra_settings variables (for example, controller_extra_settings, hub_extra_settings, eda_extra_settings, postgresql_extra_settings, and so on).
These are the variables you’ll use in the inventory file of your target environment. The new place your Ansible Automation Platform 2.5 container installation will call home (again we upgrade to Ansible Automation Platform version 2.6 container) at the end. See the following example before your YAML starts judging you.
postgresql_extra_settings: ssl_ciphers:'HIGH:!aNULL:!MD5'Update the resource server secret key
Time to refresh those resource secrets. Without them, your components can't talk to each other and that's how automation friendships die. Gather the current resource secret values
Launch a terminal and get the secrets from the gateway node:
podman exec -it automation-gateway bash -c 'aap-gateway-manage shell_plus --quiet -c "[print(cl.name, key.secret) for cl in ServiceCluster.objects.all() for key in cl.service_keys.all()]"'Validate the current secret values
Ensure that everything still matches as they should:
for secret_name in eda_resource_server hub_resource_server controller_resource_server
do
echo $secret_name
podman secret inspect $secret_name --showsecret | grep SecretData
doneIf something's not right, just delete the secret:
podman secret rm <SECRET_NAME>Repeat for your hub and controller secrets. Think of it as therapy through command-line repetition. Then re-create the secret:
echo "secret_value" | podman secret create <SECRET_NAME> - In all cases, you must substitute <SECRET_NAME> with the right one for each component:
eda_resource_server: Event-Driven Ansiblehub_resource_server: Automation Hubcontroller_resource_server: Automation Controller
Once done, your components can talk to one another (fully encrypted) again. Harmony restored!
Re-run the installer
You've already done this before, but it's time to do it once more, using the same inventory. A quick word of advice: Pull your execution environments from your source environment and bring them to your target.
If you've got any local and remotely pulled images hanging around from earlier runs, delete them! Because the migration doesn't copy over Pulp, those old images are useless artifacts that take up too much space.
ansible-playbook -i inventory ansible.containerized_installer.installWhen this finishes without error messages, you have successfully realigned the gateway. You've restored order and brought balance back to your automation universe.
Validation and making sure that it works
If you've made it this far, then your Ansible Automation Platform now exists in a new reality: Containerized, modular, and theoretically functional. But before you declare victory and post about it on LinkedIn, let's validate that your new Ansible Automation Platform 2.5 container deployment actually works.
Controller node: Check the cluster's health
Log in to your controller node using SSH. This is the heart of the operation.
ssh controller26.example.compodman exec -it automation-controller-task bash
awx-manage list_instancesIf all is well, you see something like this:
[default capacity=272]
controller26.example.com capacity=136 node_type=hybrid version=4.7.4 heartbeat="2025-11-10 21:10:53"
en26.example.com capacity=136 node_type=execution version=ansible-runner-2.4.1 heartbeat="2025-11-10 21:10:44"
[controlplane capacity=136]
controller26.example.com capacity=136 node_type=hybrid version=4.7.4 heartbeat="2025-11-10 21:10:53"
[executionplane capacity=136]
en26.example.com capacity=136 node_type=execution version=ansible-runner-2.4.1 heartbeat="2025-11-10 21:10:44"
This output is the automation equivalent of a heartbeat. If you see those timestamps, the system's alive.
Remove old nodes
Sometimes old nodes hang around after migration. A good indicator of an old node is that it has 0 capacity, which means the node failed its health check and is "haunting" your cluster. Remove them:
awx-manage deprovision_instance --host=node1.example.org
awx-manage deprovision_instance --host=node2.example.orgRepair Pulp
Next, you must repair orphaned automation hub content links in Pulp, because content integrity is important.
curl -d '{"verify_checksums": true }' \
-X POST -k https://<gateway-url>/api/galaxy/pulp/api/v3/repair/ \
-u <gateway_admin_user>:<gateway_admin_password>If /repair/ returns some JSON, there's no need to panic. You generally don't have to do anything unless there's a specific error listed. It's not a list of chores. It's just the background task object politely reporting in from Pulp.
If you see a message that state = failed, then you need to spring into action. Take a look at:
error: Tells you what went wrongtraceback: Tells you where and when it went wrongreserved_resources_record: Tells you what is responsible for the problem
Reconcile instance groups
Instance groups aren't included in the migration package. You need to recreate their associations afterwards and de-associate any stray instances that are hanging around.
- Log in to the web UI for Ansible Automation Platform.
- Go to Automation Execution > Infrastructure > Instance Groups.
- Select the group, click the Instances tab, and associate or disassociate as needed.
This step is where you decide which nodes belong.
Reconcile decision environments
Next, go to Automation Decisions > Decision Environments in your web UI.
- Edit each decision environment that references a registry URL that no longer exists (or worse, points back to your old cluster).
- Update them to match your new hub URLs.
If you had automation hub decision environments referencing the old hub, fix those too. Otherwise they'll be trying to reach Ansible Automation Platform 2.5 forever.
Reconcile execution environments and credentials
Go to Automation Execution > Infrastructure > Execution Environments in your web UI.
- Check that each image path points to your new registry.
- Then visit Automation Execution > Infrastructure > Credentials and make sure your registry credentials align with the new environment.
Validate user access and permissions
It's time to make sure everyone can still get in, but only to the places they're supposed to.
- User authentication: Test logins with a few accounts to confirm authentication works as expected.
- Role-based access controls: Double-check that users still have the right permissions across organizations, projects, inventories, and job templates. You don't want any surprise promotions to "Super Admin"!
- Team memberships: Confirm that teams still exist, and that they have the correct members.
- API access: Test API tokens to make sure automation can still talk to the platform, and in the right place.
- SSO integration (if applicable): Verify that single sign-on (SSO) is working.
More validations
It's important to validate everything. Here's what you need to look at.
Platform gateway
- Visit your new Ansible Automation Platform (specifically, the URL of your gateway)
- Dashboard loads? Good.
- No HTTP 500s? Even better.
- Gateway service connected to controller? Perfect!
Automation Controller
- Under Automation Execution, confirm projects, inventories, and job templates exist.
- Run a few jobs and confirm that they finish successfully.
Automation Hub
- Under Automation Content, confirm all collections and namespaces appear correctly.
- Sync and publish if needed.
Event-Driven Ansible
- Check Automation Execution > Decisions.
- Confirm that all rulebooks, activations, and rule audits migrated successfully.
Monitoring the logs
If you sense that something's not right with a container, then you can follow along with its logs using Podman:
podman logs -f <container_name>If it's quiet, then you're fine. If there's a lot of output, then you need to investigate further.
Smoke test everything
Here are yet more considerations to keep in mind.
- Credentials: Verify them. If they don't work, it's probably because they've expired.
- Job templates: Run several, especially ones involving different credentials or projects.
- Workflow templates: Ensure nodes trigger in the correct order, and jobs complete without interference.
- Execution environments: Verify that jobs land in the correct environments and that dependencies are available.
- Job artifacts: Confirm that they're being saved and are viewable.
- Schedules: Test scheduled jobs to make sure they still happen when they're supposed to.
Validate user access and permissions
- Login: Try a few user accounts.
- RBAC: Verify roles for organizations, projects, and inventories.
- Teams: Confirm team memberships.
- API tokens: Test them. You'll thank yourself later.
- SSO: Make sure your identity provider isn't suddenly lost.
Confirm content sync and availability
An automation platform isn't very useful if it can't find its content.
- Collection sync: Make sure syncing from remotes still works.
- Collection upload: Upload one as a test.
- Collection repos: Ensure that the hub serves collections properly.
- Project sync: Validate that your Git projects still clone successfully.
- External sources: Test connectivity to Ansible Galaxy or any remote hubs.
- Execution environment access: Confirm that all required environments are present and reachable.
- Dependencies: Run jobs that require external content.
Victory lap
If everything checks out:
- Your dashboards glow green
- Jobs run clean
- Logs are quiet
Upgrade to Ansible Automation Platform 2.6
At long last, the final step! Download the Ansible Automation Platform 2.6 container bundle for RHEL 10. This is the final stop in your migration journey. Once it's downloaded, copy it to your target environment (I put mine on the controller node).
Before you touch a single YAML file, take another backup of your freshly migrated 2.5 container environment on your new RHEL 10 VMs. Yes, again.
ansible-playbook -i inventory ansible.containerized_installer.backupRed Hat Ansible Lightspeed for AI enthusiasts
Now, for my fellow AI adventurers, we'll look at new Ansible Lightspeed inventory variables. Nothing says "modern automation" like teaching your platform to talk back.
If you're ready to try it out, all you need is a good large language model (LLM), and an extra VM for Ansible Lightspeed to run.
# AAP Lightspeed
# https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.6/html/containerized_installation/appendix-inventory-files-vars#ref-lightspeed-variables
# -----------------------------------------------------
# lightspeed_admin_password=<set your own>
# lightspeed_pg_host=externaldb.example.org
# lightspeed_pg_password=<set your own>
# lightspeed_chatbot_model_url=<set your own>
# lightspeed_chatbot_model_api_key=<set your own>
# lightspeed_chatbot_model_id=<set your own>
# lightspeed_mcp_controller_enabled=true
# lightspeed_mcp_lightspeed_enabled=true
# lightspeed_wca_model_api_key=<set your own>
# lightspeed_wca_model_id=<set your own>Model context protocol (MCP) is still in Tech Preview, so it's available for you to try.
If you'd rather keep your automation human-powered for now, simply omit those Lightspeed variables and use your 2.5 inventory layout. You can always come back later, add the AI variables, and rerun the installer.
Time to experience the future
After updating your inventory and transposing your 2.5 variables to 2.6, it's showtime. Run the installer for Ansible Automation Platform 2.6:
ansible-playbook -i inventory ansible.containerized_installer.installOnce the upgrade is complete, validate your environment the same way you did after your 2.5 migration.
You've done it! A full migration from Ansible Automation Platform 2.5 RPM on RHEL 9 to Ansible Automation Platform 2.6 Container on RHEL 10.
"Just remember, automation doesn't make you lazy. It just gives you more time to complain about YAML."