Ansible Builder and Execution Environments
Build a small, testable Ansible execution environment with ansible-builder v3, then prove what is inside before it reaches Automation Controller.
Ansible Automation Platform 2 made a fairly fundamental trade: Python virtual environments on the Controller gave way to container-based execution environments.
The useful part is not “Ansible, but in a container.” It is that the playbook’s dependencies stop belonging to the Controller. Collections, Python packages, and system libraries travel with the job. That decoupling buys us better portability and, more importantly, a cleaner way to scale execution nodes.
This guide builds a small, custom execution environment with ansible-builder and its current v3 definition schema.
What Red Hat Gives You
For AAP 2.7 on RHEL 9, the two images that matter here are:
| Image | What is inside |
|---|---|
ee-minimal-rhel9 |
ansible-core, Ansible Runner, and no collections |
ee-supported-rhel9 |
The minimal contents plus Red Hat-supported collections and their dependencies |
Both images live behind registry.redhat.io, so a registry login (docker login registry.redhat.io or podman login registry.redhat.io, with a Red Hat account or a registry service account) is a prerequisite before any pull below.
The quickest way to feel the difference is still ansible-galaxy collection list. Press play and watch the first command find no usable collection path before the supported image starts unpacking its suitcase.
The minimal failure is authentic: ansible-galaxy exited 5 because there were no usable collection paths to print. The supported transcript is a curated first slice of a much longer real list.
The Size Argument Is the Point
These are the current AAP 2.7 RHEL 9 base images. The real values Docker reported through OrbStack on arm64; image tags, architecture, and container-runtime accounting can all move the exact numbers.
Pull less. Wait less.
Minimal is about 84% smaller here.
One pull is trivia. Multiply it by image revisions, execution nodes, and CI runs and it becomes infrastructure.
That is my bias in one picture: if I know what a job needs, I start from minimal and add those things. Smaller images pull to execution nodes faster, take less registry storage, and shorten the build-test loop. At scale, “we already have it somewhere in supported” is an expensive dependency strategy.
Build the RouterOS Environment
The example is intentionally network-flavored: an EE for MikroTik RouterOS. The source remains in my ansible-execution-environments repository, but the definition below is the v3 shape.
It is one file, execution-environment.yml. Builder v3 accepts the collections inline, so there is nothing else to track. The versions shown were resolved from Ansible Galaxy.
A few details are doing real work:
images.base_image.namesets the base image (here, the AAP 2.7 / RHEL 9 minimal EE).dependenciesnow describes Galaxy content and the Ansible runtime in one structured block.requirements.txtandbindep.txtcan sit besidegalaxywhen the project has extra Python or system requirements.python_interpreterpins the RHEL package and executable to Python 3.12. A real build caught transitive networking RPMs installing the generic RHEL Python 3.9 package; an explicit path keeps Builder on the interpreter shipped by this AAP image.additional_build_steps.append_finalrunspip checkin the finished image, a hook scoped to a specific build stage.options.package_manager_pathtells Builder to use themicrodnfalready present in the RHEL 9 minimal image.
The ansible_core and ansible_runner constraints match the AAP 2.7 generation. The Red Hat base already contains both; keeping compatible constraints in the definition makes the intended runtime visible and prevents an unconstrained PyPI upgrade from quietly changing the engine.
Builder v3 accepts seven top-level sections, but it does not make all seven mandatory. Add knobs because the image needs them, not because a sample file found them first. The v3 definition reference and porting guide are the useful long-form references.
Make Builder Show Its Work
ansible-builder create validates the definition and generates the build context without asking a container runtime to pull or build anything. It is the fastest feedback loop for schema and generated-Containerfile problems.
This is output from ansible-builder 3.1.1, captured from the definition above and redacted only to replace the temporary directory with ..
Then build the image:
ansible-builder build --container-runtime podman -t localhost/routeros-ee:latest
Docker works too; I used OrbStack here. The build produces a 752 MB RouterOS image: the 490 MB minimal base plus the collections’ transitive Python and RPM dependencies.
And this is the collection check from that built image, not a typed approximation:
Notice what did not land: every cloud, virtualization, Windows, and vendor collection that happens to ship in ee-supported. The image says what it is for.
Test with Runner Before AAP
Building successfully proves that the dependency graph can become a container. It does not prove that the container can run your playbook against a router.
Keep an Ansible Runner private data directory beside the EE project:
runner/
├── env/
│ └── settings
├── inventory/
└── project/
└── identity.yml
The playbook asks RouterOS for its system identity using community.routeros.command.
Keep the isolation and image settings in runner/env/settings so they stay out of your shell history:
# runner/env/settings
process_isolation: true
process_isolation_executable: docker
container_image: localhost/routeros-ee:latest
With your lab inventory and credentials under runner/, the run itself is short:
ansible-runner run runner -p identity.yml
That real headless run used Docker through OrbStack against RouterOS 7.21.5 on the lab network. The EE opened the network_cli connection and returned the system identity MikroTik.
Once Runner passes, push the exact tag you tested to Private Automation Hub or your container registry, then attach it to Automation Controller. Do not rebuild between “tested” and “pushed”; that swaps the artifact under your evidence.
All terminal output here is captured from real commands, with temporary paths redacted.