Home DevOps Ansible Execution Environments: Stop Fighting Dependencies, Ship Faster
Advanced 3 min · July 11, 2026

Ansible Execution Environments: Stop Fighting Dependencies, Ship Faster

Ansible Execution Environments solve dependency hell.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 11, 2026
last updated
1,727
articles · all by Naren
Before you start⏱ 30 min
  • Docker or Podman installed. Basic familiarity with container concepts. Ansible 2.12+ installed.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Ansible Execution Environments are container images that package Ansible with all its dependencies. Build them with ansible-builder, run them with ansible-navigator. They solve dependency conflicts and ensure consistent automation execution across teams and CI/CD pipelines.

✦ Definition~90s read
What is Ansible Execution Environments?

Ansible Execution Environments (EEs) are containerized, self-contained environments that bundle Ansible Core, collections, dependencies, and tools into a single image. They eliminate the 'works on my machine' problem by ensuring every automation run uses the exact same runtime, regardless of the control node.

Imagine you're a chef who needs to cook the same dish in different kitchens.
Plain-English First

Imagine you're a chef who needs to cook the same dish in different kitchens. Without EEs, you'd have to check each kitchen for the right pans, spices, and stove settings. With EEs, you bring your own portable kitchen — a container with everything you need. You just plug it in and cook. No surprises.

Every team I've worked with has a horror story about Ansible breaking because someone's laptop had a different Python version or a collection conflicted with a system package. I've seen a pip install inside a playbook silently upgrade a library and take down a production deployment at 2 AM. The root cause? No isolation. Ansible Execution Environments are the fix — they containerize your automation stack so you never have to debug a 'but it works on my machine' again. By the end of this, you'll be able to build, test, and debug EEs in production, and you'll know exactly when they're overkill.

Why Your Ansible Setup Is Broken (And EEs Fix It)

Before EEs, every Ansible control node was a snowflake. You'd have a Jenkins server with Python 3.6, a developer's Mac with Python 3.9, and a production bastion with Python 3.8. Collections installed via ansible-galaxy would conflict with system packages. Someone would run pip install --upgrade and break everything. EEs solve this by bundling Ansible, Python, collections, and system dependencies into a single container image. Now every run — local, CI, production — uses the exact same environment. No more 'works on my machine'.

execution-environment.ymlDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — DevOps tutorial

---
version: 1

build_arg_defaults:
  EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner:stable-2.12-devel'

ansible_config: 'ansible.cfg'

dependencies:
  galaxy: requirements.yml
  python: requirements.txt
  system: bindep.txt

additional_build_steps:
  prepend: |
    RUN pip3 install --upgrade pip setuptools
  append: |
    RUN echo "EE built on $(date)" > /image_build_info.txt
Output
No direct output. Build with: ansible-builder build -f execution-environment.yml -t my-ee:latest
Production Trap:
Never use latest as your EE tag in CI. Pin to a specific version like my-ee:v1.2.3. I've seen latest get rebuilt mid-deployment and break a playbook because a collection API changed.

Building Your First EE: The Right Way

You don't build EEs by hand — you use ansible-builder. It reads a definition file (execution-environment.yml) and generates a Containerfile (or Dockerfile) for you. The key is to keep it minimal. Start with a base image from quay.io/ansible/ansible-runner. Then add only the collections and Python packages your playbook needs. Use bindep.txt for system-level dependencies (like libffi-dev for cryptography). The build process is: ansible-builder build -f execution-environment.yml -t my-ee:1.0.0. This creates a container image you can push to any registry.

requirements.ymlDEVOPS
1
2
3
4
5
6
7
8
9
10
// io.thecodeforge — DevOps tutorial

---
collections:
  - name: ansible.posix
    version: '>=1.5.0,<2.0.0'
  - name: community.general
    version: '>=5.0.0,<6.0.0'
  - name: amazon.aws
    version: '>=3.0.0,<4.0.0'
Output
No direct output. Used by ansible-builder during build.
Senior Shortcut:
Use ansible-builder introspect to scan your playbook directory and auto-generate requirements.yml and requirements.txt. It's not perfect, but it's a great starting point.

Running Playbooks with ansible-navigator

ansible-navigator is the CLI tool that runs your playbook inside the EE. It handles mounting inventory, playbooks, and SSH keys into the container. The basic command: ansible-navigator run playbook.yml --eei my-ee:1.0.0 --mode stdout. The --mode stdout flag makes it behave like traditional ansible-playbook — otherwise you get an interactive TUI. For CI, always use --mode stdout and --pull-policy missing to avoid pulling the image every time if it's already cached.

run_playbook.shDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — DevOps tutorial

#!/bin/bash
# Run a playbook inside an EE with production settings
ansible-navigator run site.yml \
  --eei registry.example.com/ansible-ee:1.0.0 \
  --mode stdout \
  --pull-policy missing \
  --timeout 300 \
  --container-options '--memory=2g --cpus=2' \
  --pass-environment-variable AWS_ACCESS_KEY_ID \
  --pass-environment-variable AWS_SECRET_ACCESS_KEY
Output
Standard Ansible playbook output (task results, changed/failed counts).
Never Do This:
Don't pass secrets via --env in the command line. Use --pass-environment-variable to forward existing env vars, or mount a secrets file. Your shell history is not a vault.

Debugging EE Failures: The TUI and Logs

When a playbook fails inside an EE, you can't just SSH into the container — it's ephemeral. Use ansible-navigator's interactive mode (--mode interactive) to explore the EE after a failure. Or, for CI, capture the container logs by running the EE manually: podman run --rm -it my-ee:1.0.0 /bin/bash. Inside, you can check Python versions, collection paths, and test imports. Common failure: a collection's Python dependency is missing. Fix it by adding to requirements.txt and rebuilding.

debug_ee.shDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — DevOps tutorial

#!/bin/bash
# Debug an EE by running it interactively
podman run --rm -it \
  -v $(pwd):/runner/project:z \
  -v ~/.ssh:/home/runner/.ssh:ro,z \
  my-ee:1.0.0 /bin/bash

# Inside the container, check:
# ansible --version
# python3 -c "import boto3"  # test import
# ansible-galaxy collection list
Output
Interactive shell inside the EE container.
Interview Gold:
Question: 'How do you debug a missing Python dependency in an EE?' Answer: Run the EE interactively, try importing the module, then add it to requirements.txt and rebuild. Always version-pin your dependencies.

CI/CD Integration: The Pipeline That Doesn't Lie

EEs shine in CI. Your pipeline builds the EE once, pushes it to a registry, then every job pulls the same image. No more 'but it passed on Jenkins'. Use a multi-stage build: first stage installs dependencies, second stage copies only what's needed. Tag with the commit SHA for traceability. In your pipeline, run ansible-navigator run with --pull-policy always to ensure you're using the exact image from the registry. Example GitLab CI job: image: $CI_REGISTRY/ansible-ee:$CI_COMMIT_SHA.

.gitlab-ci.ymlDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — DevOps tutorial

stages:
  - build
  - test
  - deploy

build-ee:
  stage: build
  script:
    - ansible-builder build -f execution-environment.yml -t $CI_REGISTRY/ansible-ee:$CI_COMMIT_SHA
    - podman push $CI_REGISTRY/ansible-ee:$CI_COMMIT_SHA

deploy:
  stage: deploy
  image: $CI_REGISTRY/ansible-ee:$CI_COMMIT_SHA
  script:
    - ansible-navigator run deploy.yml --mode stdout --pull-policy never
Output
Pipeline stages: build-ee (builds and pushes), deploy (runs playbook using the EE).
Production Trap:
Don't use --pull-policy always in deploy jobs — it adds latency and can fail if the registry is down. Use --pull-policy missing or never after the image is verified in the test stage.

When EEs Are Overkill (And What to Use Instead)

EEs add complexity. You need a container runtime, a registry, and the build pipeline. For a single playbook run weekly on a laptop, a virtual environment (python3 -m venv) with pip install ansible is simpler. For teams with fewer than 5 people and no CI, EEs are overkill. But if you have multiple projects with conflicting dependencies, or you're running automation in production, EEs are the only sane choice. The rule: if you've ever said 'but it works on my machine', you need EEs.

Senior Shortcut:
For small teams, use ansible-runner with a virtual environment instead of full EEs. It's lighter and doesn't require container skills. Migrate to EEs when you hit dependency conflicts or need to scale.
● Production incidentPOST-MORTEMseverity: high

The 4GB Container That Kept Dying

Symptom
Our CI pipeline for a multi-cloud provisioning playbook would randomly fail with Error: OOMKilled after 10 minutes of execution.
Assumption
We assumed the playbook was leaking memory — maybe a loop over 5000 VMs was holding references.
Root cause
The EE image included every Ansible collection we'd ever used (50+ collections), plus full Python packages like boto3, google-cloud-sdk, and azure-cli. The image was 4GB. Podman's default memory limit for containers is 2GB. The container hit the limit during a large inventory parse.
Fix
We split the EE into two images: one for AWS/GCP (light, 800MB) and one for Azure (1.2GB). We also set --memory=4g in the CI runner config. Reduced collection set to only what each pipeline needed.
Key lesson
  • An EE is not a dump truck.
  • Only include what your playbook actually needs.
  • A lean EE is a fast, stable EE.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Error: Unable to pull image or manifest not found
Fix
1. Check image tag exists in registry. 2. Verify registry credentials in CI. 3. Run podman pull <image> manually to test. 4. If using --pull-policy always, ensure network access.
Symptom · 02
Playbook runs but tasks fail with ModuleNotFoundError
Fix
1. Run EE interactively: podman run --rm -it <ee-image> /bin/bash. 2. Try python3 -c "import <module>". 3. If missing, add to requirements.txt and rebuild. 4. Check if collection is installed: ansible-galaxy collection list.
Symptom · 03
Container exits with code 137 (OOMKilled)
Fix
1. Check container memory limit: podman inspect <container> | jq '.[0].HostConfig.Memory'. 2. Increase memory via --container-options '--memory=4g'. 3. Reduce EE image size by trimming collections. 4. Add swap or use a larger instance.
★ Ansible Execution Environments Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
`Error: Failed to find execution environment`
Immediate action
Check if the image exists locally or in the registry.
Commands
podman images | grep <ee-name>
podman pull <registry>/<ee-name>:<tag>
Fix now
Build and push the EE: ansible-builder build -f execution-environment.yml -t <tag> && podman push <tag>
`Error: Missing Python module 'boto3'`+
Immediate action
Check if boto3 is in the EE's requirements.txt.
Commands
podman run --rm <ee-image> python3 -c "import boto3"
podman run --rm <ee-image> pip3 list | grep boto3
Fix now
Add boto3>=1.26.0 to requirements.txt, rebuild, and redeploy.
Playbook runs but SSH fails with `Permission denied`+
Immediate action
Check if SSH keys are mounted into the container.
Commands
podman run --rm -v $SSH_AUTH_SOCK:/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent <ee-image> ssh -T git@github.com
ansible-navigator run playbook.yml --mode stdout --container-options '-v $SSH_AUTH_SOCK:/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent'
Fix now
Use --ssh-forward flag in ansible-navigator or mount the SSH agent socket.
`Error: Container exited with code 137`+
Immediate action
Check OOM killer logs.
Commands
podman inspect <container-id> | jq '.[0].State.OOMKilled'
dmesg | grep -i oom
Fix now
Increase memory limit: --container-options '--memory=4g' or reduce EE image size.
Feature / AspectVirtual EnvironmentExecution Environment
Isolation levelPython packages onlyFull OS + Python + collections
Build timeSecondsMinutes (first build)
ReproducibilityDepends on pip resolutionDeterministic (container layers)
CI/CD integrationManual setupNative with container registry
Team adoptionLow frictionRequires container skills
Production readinessFragileBattle-tested
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
execution-environment.ymlversion: 1Why Your Ansible Setup Is Broken (And EEs Fix It)
requirements.ymlcollections:Building Your First EE
run_playbook.shansible-navigator run site.yml \Running Playbooks with ansible-navigator
debug_ee.shpodman run --rm -it \Debugging EE Failures
.gitlab-ci.ymlstages:CI/CD Integration

Key takeaways

1
EEs containerize the Ansible control plane. Build once, use everywhere.
2
Use ansible-builder to build EEs, ansible-navigator to consume them.
3
Pin versions with semantic tags. Never use :latest in production.
4
Keep EEs focused per team or workflow. Don't build monolithic images.

Common mistakes to avoid

3 patterns
×

Building one monolithic EE for everything

Fix
Create focused EEs per team or workflow. A CI/CD EE shouldn include every collection your infra team uses — keep images small and build times fast.
×

Using :latest tags in production

Fix
Use semantic version tags (my-ee:1.0.0). The :latest tag is mutable and breaks reproducibility.
×

Not pinning collection versions in the EE

Fix
Pin collection versions in requirements.yml within the EE definition. A collection upgrade in the EE can break playbooks just like a library upgrade.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does ansible-navigator handle SSH agent forwarding inside an EE? Wha...
Q02SENIOR
When would you choose an Execution Environment over a simple Python virt...
Q03SENIOR
What happens if you run `ansible-navigator run` without specifying an EE...
Q04JUNIOR
What is the purpose of the `bindep.txt` file in an EE definition?
Q05SENIOR
You have a playbook that runs fine locally but fails in CI with `ModuleN...
Q06SENIOR
How would you design an EE strategy for a large organization with 50+ An...
Q01 of 06SENIOR

How does ansible-navigator handle SSH agent forwarding inside an EE? What happens if the agent socket is not mounted?

ANSWER
ansible-navigator uses --ssh-forward to mount the SSH agent socket into the container. Without it, SSH connections from inside the EE will fail with Permission denied (publickey). The socket path is passed via SSH_AUTH_SOCK environment variable. If not mounted, you must either copy keys into the image (bad practice) or use password authentication (worse).
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What is an Ansible Execution Environment?
02
How do I build an Execution Environment?
03
Do I need ansible-navigator to use Execution Environments?
04
What should go into an Execution Environment?
05
Should I use :latest tags for EEs?
06
How do EEs work with AWX?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 11, 2026
last updated
1,727
articles · all by Naren
🔥

That's Ansible. Mark it forged?

3 min read · try the examples if you haven't

Previous
Ansible Installation Guide
32 / 37 · Ansible
Next
Event-Driven Ansible