Running playbooks locally in the execution environment production uses
Seventh entry in the Ansible development environment series. Part 6 pointed the editor at the team's execution environment (EE), the container image that AAP (Red Hat Ansible Automation Platform) and its automation controller run every job in. This part does the same for the terminal. The aim is Alex Dworjan's rule: run the playbook locally, in that same image, before pushing, rather than pushing, syncing the project in controller, and finding out there.
The tool is ansible-navigator, part of ADT. By default it runs ansible-playbook inside an EE instead of on the host. Everything below was run with ansible-navigator 26.9.0 and Docker against the public ghcr.io/ansible-community/community-ee-base image. That image carries ansible-core 2.21.3 and three collections: ansible.posix, ansible.utils and ansible.windows.
Commit an ansible-navigator.yml
Navigator reads ansible-navigator.yml from the project directory, so the choice of image travels with the repository:
---
ansible-navigator:
execution-environment:
enabled: true
container-engine: auto
image: ghcr.io/ansible-community/community-ee-base@sha256:9f2836592ab92794e8b1982311d504ba3c28a2c09b3f8de221ca3564842c0902
pull:
policy: missing
environment-variables:
pass:
- DEMO_TOKEN
mode: stdout
playbook-artifact:
enable: false
imageis the EE, by digest, as in part 3. It's the same value as the editor'sansible.executionEnvironment.imagein part 6.container-engine: autotries Podman first, then Docker. On this machine, with no Podman,ansible-navigator settings --effectiveshowed it resolved todocker.pull.policy: missingpulls only when the image isn't there yet. With a digest, a new version is a new reference, so nothing goes stale.environment-variables.passlists the host variables the container may see (below).mode: stdoutprints plainansible-playbookoutput instead of navigator's interactive text UI.playbook-artifact.enable: falseturns off the JSON record navigator otherwise saves after every run. The default istrue, which writeswhere-artifact-<timestamp>.jsoninto the project directory. Keep it on, and ignore the files in.gitignore, if you want to replay runs.
What runs where
A playbook that prints where it is:
- hosts: localhost
gather_facts: false
tasks:
- ansible.builtin.debug:
msg:
- "ansible-core {{ ansible_version.full }}"
- "playbook_dir {{ playbook_dir }}"
- "config {{ ansible_config_file }}"
- "DEMO_TOKEN={{ lookup('ansible.builtin.env', 'DEMO_TOKEN') | default('unset', true) }}"
- "OTHER_VAR={{ lookup('ansible.builtin.env', 'OTHER_VAR') | default('unset', true) }}"
Run with DEMO_TOKEN=abc123 OTHER_VAR=xyz ansible-navigator run where.yml:
"ansible-core 2.21.3",
"playbook_dir <project>",
"config <project>/ansible.cfg",
"DEMO_TOKEN=abc123",
"OTHER_VAR=unset"
- The version is the EE's. The host venv had ansible-core 2.21.4; the playbook reported 2.21.3, the image's.
- The project is mounted at the same path.
playbook_dirwas the host path, and the project'sansible.cfgapplied, with its inventory. - The host environment isn't. Only
DEMO_TOKEN, listed underpass, reached the playbook. Cloud credentials, proxy settings and tokens have to be listed there, or given per run with--penv NAME. Controller injects its own credentials instead, so a variable that works locally only because your shell has it would be missing there.
The EE decides which collections exist
The same image has no community.general. A playbook using community.general.json_query failed in the EE with:
No filter named 'community.general.json_query'.
That's the useful failure: it's the one controller would give. There's one way to hide it locally. Ansible also loads collections from a collections/ directory next to the playbook. After ansible-galaxy collection install community.general -p ./collections, the same run found the filter, and ansible-navigator collections --mode stdout listed community.general 13.4.0 from the project beside the EE's three. But it failed one step later:
You need to install "jmespath" prior to running json_query filter
A collection copied into the project brings its code, not its Python dependencies. The EE has no jmespath, and the host venv's copy doesn't count. AWX's docs describe the controller-side equivalent: "If you specify a collections requirements file in SCM at collections/requirements.yml of a project, then AWX will install collections from that file" when the project updates. That installs collections only, into the job, so the same missing-library error would follow. A collection that needs Python libraries belongs in the EE, where ansible-builder installs its requirements too (part 3).
Before pushing
ansible-navigator collections --mode stdoutshows what the EE, plus anything in./collections, actually provides.ansible-navigator run site.yml --checkruns in the production image against a test inventory.--checkis Ansible's dry run.- Push when it passes. What's left to differ in controller is what the EE can't reproduce: controller's inventory, credentials and survey answers.
In the editor, Dworjan notes that the first time the Ansible extension switches to an EE, it pulls the image and copies its plugin docs. Highlighting may need a window reload to catch up.
Sources
- Alex Dworjan: Ansible Development Environment Options (2024), Dev Containers (2024) and Dev Spaces with Execution Environments (2026), and his ansible-navigator.yml template.
ansible-navigator run --help(26.9.0):--penv, and--pae"(default: true)".- AWX
docs/collections.md, "Project Collections Requirements", in ansible/awx. - Every output above came from ansible-navigator 26.9.0 with Docker 29.3.1 and
ghcr.io/ansible-community/community-ee-base(digestsha256:9f283659…, built 2026-08-21).
Created 2026-09-30T00:05:51+02:00 · Edit