Running Ansible Playbooks in Execution Environments with ansible-navigator
Use Ansible Execution Environments to containerize control-node dependencies and run playbooks reproducibly with ansible-navigator.
01 Dec 2025, 18:38 UTC

The problem and the useful takeaway
Installing Ansible collections, Python modules, and system packages directly on a Linux control node works until two playbooks need different versions of the same collection, or a CI job fails because a teammate upgraded a dependency on the shared jump host. An Execution Environment (EE) is a container image built with ansible-builder that packages the Ansible runtime, collections, Python packages, and system dependencies together. ansible-navigator runs your playbook inside that image on a control node that only needs a container runtime. The takeaway: pin the EE image, mount only the project directory, forward the SSH agent socket, and treat the EE as an immutable dependency artifact rather than a mutable control-node environment.
Requirements
- Linux control node with Podman or Docker installed, and the current user able to access the container socket.
- ansible-builder to build the EE image.
- ansible-navigator to run playbooks inside the EE.
- Ansible core 2.12 or newer on the control node; EE support was introduced in that release.
- Target Linux/Unix hosts reachable over SSH, with an inventory file inside the project directory.
Smallest suitable design
Create an execution-environment.yml in the project root:
version: 1
dependencies:
python: []
system: []
collections:
- name: ansible.posix
- name: community.general
additional_files: []
Build the image on the control node:
ansible-builder build -t registry.example.com/ops/ansible-ee:1.0.0 -c ee-build-context
Run as a user with permission to use the container runtime, typically membership in the podman or docker group. The build pulls base images from a registry, so network access to the registry is required.
Run a playbook headless, suitable for CI:
ansible-navigator run site.yml -i inventory --ee registry.example.com/ops/ansible-ee:1.0.0 --eei registry.example.com/ops/ansible-ee@sha256:0123...abcdef
Expected check: the playbook executes without collection-not-found errors and reaches the first task. Risk: if the image digest does not match the tagged image, ansible-navigator may pull a newer image silently unless digest pinning is enforced.
For local interactive work, omit --eei and use the text UI:
ansible-navigator run site.yml -i inventory --ee registry.example.com/ops/ansible-ee:1.0.0
Trust and data boundaries
The container registry is the first trust boundary. Verify image authenticity and use pull credentials in CI; do not rely on :latest because the image can change underneath running jobs. The container runtime socket is a second boundary: anyone who can speak to the Podman or Docker socket can run containers with the host's privileges, so restrict socket access to trusted users.
Bind mounts and forwarded sockets are the data boundary. Mount only the project directory, not the home directory. Forward the SSH agent socket so the EE can authenticate to target hosts without copying private keys into the image:
ansible-navigator run site.yml -i inventory --ee registry.example.com/ops/ansible-ee:1.0.0 --container-options "--volume=$SSH_AUTH_SOCK:/ssh-agent --env=SSH_AUTH_SOCK=/ssh-agent"
Vault password files must be readable inside the container. Keep them inside the mounted project tree or pass them as a bind-mounted file; placing them outside the mount means decryption fails inside the EE.
Operational checks
- Run ansible-navigator --version and ansible-navigator images on the control node to confirm the CLI and EE images are installed.
- Build a test EE and run a non-destructive playbook, such as one that only runs ansible.builtin.ping:
ansible-navigator run smoke.yml -i inventory --ee registry.example.com/ops/ansible-ee:1.0.0
- Start a container from the EE and list installed collections to verify required content is present:
podman run --rm registry.example.com/ops/ansible-ee:1.0.0 ansible-galaxy collection list
- In CI, pin by digest and run headless:
ansible-navigator run site.yml --eei registry.example.com/ops/ansible-ee@sha256:0123...abcdef
Check that ansible-playbook --version reports Ansible core 2.12+ on the control node; the ansible meta-package alone does not guarantee EE support.
Failure modes and conditions that change the design
- Permission failures on mounted artifacts. Default EE images run as a non-root user, commonly UID 1000. Rootless Podman may map that UID to a different host UID, so retry files, callback output, or log files written to the mounted project directory can fail with permission denied. Adjust directory ownership or run the container with a matching user ID.
- SELinux blocks the bind mount. On SELinux enforcing systems, the container may be denied access to the mounted project directory. Add the Z or z mount option to relabel the volume, or adjust SELinux policy.
- Missing cloud SDKs. EEs do not automatically include boto3, azure-sdk, or similar. Dynamic inventory plugins that require them fail unless those Python packages are added to execution-environment.yml and the image is rebuilt.
- Immutability means rebuilds for every change. Adding a collection or Python package requires rebuilding and retagging the image. This changes patching compared with a virtualenv on the control host: you cannot pip install into a running EE and expect the change to persist.
- Secret exposure. Mounting the entire home directory or copying SSH private keys into the EE expands the blast radius if the image is compromised. Forward only the agent socket and mount only the project tree.
Conditions that would change the design
Switch away from a shared EE if different teams need conflicting collection versions that cannot be reconciled in one image; in that case, build per-project EEs. Avoid EEs for one-off commands on a workstation where installing ansible-core directly is faster than building an image. If target hosts are Windows or require credential plugins not present in the base EE, add those dependencies to the build file or use a custom control-node virtualenv instead.
Limitations and how to verify the result
EE content is fixed at build time. You cannot troubleshoot a missing module by installing it inside a running container and expecting the fix to apply to the next job. Verify by listing collections and Python modules inside the image before promoting it to production. Confirm the control-node user can access the container runtime socket without sudo, since rootless and rootful Podman behave differently with UID mapping. The practical check is a successful headless run of a non-destructive playbook against a test host using the pinned image digest.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.