AnsibleL · Automation Disaster RecoveryMaking the rebuild possible
Execution environments as a reproducibility answer
What you'll learn
- Explain what an execution environment pins that a virtualenv plus requirements.yml does not
- Read a version 3 execution-environment.yml and name what each top-level key controls
- Choose between a pinned virtualenv and a container image on operational grounds
- Identify what an execution environment does not solve, including the controller-side state it cannot carry
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
Lesson 3 pinned five layers and left one unpinned, because a virtualenv cannot pin it: the operating system underneath.
The system Python’s patch level, the OpenSSH client version, the
system’s libssl, the presence of sshpass, the locale — none of these
are in requirements.yml or constraints.txt, and all of them can
change behaviour. Most of the time they do not matter. The times they do
are memorable: an SSH client that changed its default key-exchange
algorithms and stopped talking to older network devices, a locale
difference that changed how a module parsed command output, a
libcrypto that dropped an algorithm a legacy host still required.
An execution environment is a container image containing the whole
toolchain — ansible-core, collections, Python dependencies and the OS
libraries they need. Rebuilding the controller becomes pulling an image
by digest.
What an EE pins that a virtualenv does not
| Layer | Pinned virtualenv | Execution environment |
|---|---|---|
ansible-core | Yes, by constraints | Yes, baked into the image |
| Collections | Yes, by requirements.yml | Yes, baked into the image |
| Python libraries | Yes, by constraints | Yes, baked into the image |
| Python interpreter | Partly — the venv uses whatever python3 exists | Yes, the image carries it |
| OS libraries, SSH client, locale | No | Yes |
| Reproducible by digest | No — a rebuild re-resolves | Yes — the digest is the identity |
The last row is the substantive difference. pip install -c constraints.txt
re-resolves the dependency graph every time it runs, and while pins make
that resolution deterministic in the versions it picks, it still depends
on those artefacts being available and on the build host’s OS. Pulling
registry.example.com/ansible-ee@sha256:REDACTED is not a resolution.
It is a byte-identical retrieval or a failure.
The definition file, version 3
---
version: 3
images:
base_image:
name: quay.io/centos/centos:stream9
dependencies:
ansible_core:
package_pip: ansible-core==2.21.3
ansible_runner:
package_pip: ansible-runner
galaxy: requirements.yml
python: constraints.txt
system: bindep.txt
options:
package_manager_path: /usr/bin/dnf
additional_build_steps:
append_final:
- RUN echo "built $(date -u +%Y-%m-%dT%H:%M:%SZ)" > /etc/ee-build-dateThe seven top-level keys of a version: 3 definition are version,
images, dependencies, options, additional_build_files,
additional_build_steps and build_arg_defaults. Four of them carry
the weight:
images.base_image sets what the toolchain runs on — the layer a
virtualenv cannot pin. Pin it by digest rather than by tag if you want
the reproducibility this whole lesson is arguing for; :stream9 moves,
@sha256:… does not.
dependencies is where the files from lesson 3 are consumed
directly. galaxy: takes your requirements.yml, python: takes your
constraints or requirements file, and system: takes a bindep.txt of
OS packages. This is the key property: the EE definition does not
replace your pinned files, it consumes them. A team that adopts EEs
does not throw away lesson 3’s work; it wraps it.
ansible_core and ansible_runner are declared separately because they
are the two components the builder needs to guarantee are present.
ansible-runner is the execution API the image exposes, which is how
ansible-navigator drives a playbook inside the container.
options carries the build-behaviour switches:
package_manager_path, skip_ansible_check, skip_pip_install,
container_init, relax_passwd_permissions, workdir, user and
tags. package_manager_path is the one you will actually set,
because it is how you tell the builder that your base image uses apt
rather than dnf.
additional_build_steps injects raw container-build instructions at
eight named points — prepend_base, append_base, prepend_galaxy,
append_galaxy, prepend_builder, append_builder, prepend_final
and append_final. The prefixes correspond to the builder’s multi-stage
build: base is the foundation, galaxy is where collections are
installed, builder is where Python wheels are compiled, and final is
the image you ship.
The remaining two keys are additional_build_files, which copies files
from the build context into the image so additional_build_steps can
use them, and build_arg_defaults, which sets default build arguments
such as proxy settings.
Building and running
cd /srv/ansible
# Generate the build context and build the image.
ansible-builder build --file execution-environment.yml --tag registry.example.com/ansible-ee:2.21.3-1 --container-runtime podman
# Inspect what actually landed in it.
podman run --rm registry.example.com/ansible-ee:2.21.3-1 ansible --version
podman run --rm registry.example.com/ansible-ee:2.21.3-1 ansible-galaxy collection listThose two inspection commands are the EE equivalent of lesson 3’s verification checks, and they matter for the same reason: a build that printed no errors is not evidence about what is inside the image.
ansible-navigator run site.yml --execution-environment-image registry.example.com/ansible-ee:2.21.3-1 --mode stdout --limit canary --check--mode stdout is worth knowing on day one. ansible-navigator
defaults to an interactive text UI, which is genuinely useful for
exploring a run afterwards and completely wrong for CI, for a wrapper
script, or for anyone expecting playbook output on a terminal.
Choosing
- Does your fleet include hosts sensitive to SSH client or crypto library versions - old network devices, legacy appliances, hosts with restricted algorithms? If yes, the OS layer matters and an EE earns its cost.
- Do multiple people or systems run this automation from different machines? If yes, an image is a much better answer than "everyone install these versions".
- Do you already run containers in production, with a registry, a build pipeline and people who understand image lifecycle? If no, an EE adds a new operational surface to your DR plan rather than simplifying it.
- Is your controller a single well-managed host with a pinned venv, verified quarterly? If yes, you may already have enough, and lesson 6 will tell you.
- Whichever you choose, the answer must be in the repository as a definition, and the drill in lesson 6 must exercise the path you actually intend to use.
Knowledge check
Knowledge check · 5 questions
Q1. What does an execution environment pin that a pinned virtualenv plus requirements.yml cannot?
Q2. Which statements about a version 3 execution-environment.yml are correct? Select all that apply.
Q3. When a playbook runs inside an execution environment, modules still execute on the managed nodes using those nodes own Python interpreters.
Q4. A team adopts execution environments and stores the image only in their internal registry, which runs on the cluster their Ansible manages. What is wrong with this as a DR plan?
Q5. A playbook that worked from the pinned virtualenv reports every host unreachable when run through ansible-navigator with an execution environment. What is the most likely cause?
Passing score: 75%. Answers are checked in this browser.