Skip to main content
RunBook Academy

AnsibleL · Automation Disaster RecoveryMaking the rebuild possible

Execution environments as a reproducibility answer

Advanced⏱ ~30 minbashpodman

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

Not yet marked complete on this device.

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

LayerPinned virtualenvExecution environment
ansible-coreYes, by constraintsYes, baked into the image
CollectionsYes, by requirements.ymlYes, baked into the image
Python librariesYes, by constraintsYes, baked into the image
Python interpreterPartly — the venv uses whatever python3 existsYes, the image carries it
OS libraries, SSH client, localeNoYes
Reproducible by digestNo — a rebuild re-resolvesYes — 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

Read-only / Safeexecution-environment.yml - a complete version 3 definition
---
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-date

The 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

Configuration changebuild the image
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 list

Those 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.

Service impact possiblerun a playbook through 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

  1. 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.
  2. 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".
  3. 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.
  4. 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.
  5. 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

  1. Q1. What does an execution environment pin that a pinned virtualenv plus requirements.yml cannot?

  2. Q2. Which statements about a version 3 execution-environment.yml are correct? Select all that apply.

  3. Q3. When a playbook runs inside an execution environment, modules still execute on the managed nodes using those nodes own Python interpreters.

  4. 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?

  5. 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.