Introduction

Running Ansible consistently across teams, CI/CD pipelines, and production requires isolation. Three approaches exist: Python virtualenvs, Docker containers, and Ansible Execution Environments (EE). Each solves different problems at different scales. This guide compares all three with practical setup examples.

Quick Comparison

FeaturevirtualenvDockerExecution Environment
Isolation levelPython packagesFull OS + packagesFull OS + Python + collections
System packages❌ Host only✅ Custom OS packages✅ Custom OS packages
Ansible collectionsManual installManual install✅ Built-in, versioned
Build toolpython -m venvDockerfileansible-builder
Runtimesource bin/activatedocker runansible-navigator or AAP
CI/CD integration⚠️ Fragile✅ Standard✅ Standard
AAP/AWX compatible❌ No❌ Not directly✅ Native
Team sharingrequirements.txtDocker Hub / registryRegistry (Quay, GHCR)

virtualenv — Quick Python Isolation

# Create virtual environment
python3 -m venv ~/ansible-env
source ~/ansible-env/bin/activate

# Install Ansible + dependencies
pip install ansible-core==2.20.0
pip install jmespath netaddr passlib

# Install collections
ansible-galaxy collection install community.general amazon.aws

# Freeze versions
pip freeze > requirements.txt
ansible-galaxy collection list --format yaml > requirements.yml
# requirements.txt
ansible-core==2.20.0
jmespath==1.0.1
netaddr==1.3.0
passlib==1.7.4

# requirements.yml
collections:
  - name: community.general
    version: "9.0.0"
  - name: amazon.aws
    version: "7.0.0"

When virtualenv works

# ✅ Single developer / small team
# ✅ No system-level dependencies (C libraries, CLI tools)
# ✅ Quick experimentation
# ✅ All team members use same OS

# ❌ Doesn't isolate: system packages, C libraries, OS differences
# ❌ "Works on my machine" — macOS vs Ubuntu vs RHEL

Docker — Full OS Isolation

# Dockerfile.ansible
FROM python:3.12-slim

RUN apt-get update && apt-get install -y \
    openssh-client \
    sshpass \
    git \
    && rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir \
    ansible-core==2.20.0 \
    jmespath \
    netaddr

COPY requirements.yml /tmp/requirements.yml
RUN ansible-galaxy collection install -r /tmp/requirements.yml

WORKDIR /ansible
ENTRYPOINT ["ansible-playbook"]
# Build
docker build -f Dockerfile.ansible -t ansible-runner .

# Run
docker run --rm \
  -v $(pwd):/ansible \
  -v ~/.ssh:/root/.ssh:ro \
  ansible-runner site.yml -i inventory/production

CI/CD with Docker

# .github/workflows/ansible.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/myorg/ansible-runner:2.20
    steps:
      - uses: actions/checkout@v4
      - run: ansible-playbook site.yml -i inventory/production

Execution Environments — Ansible-Native Containers

# execution-environment.yml
---
version: 3
dependencies:
  galaxy: requirements.yml
  python: requirements.txt
  system: bindep.txt

images:
  base_image: quay.io/ansible/ansible-runner:latest

additional_build_steps:
  prepend_base:
    - RUN whoami
  append_final:
    - RUN ansible --version
# requirements.yml
collections:
  - community.general>=9.0.0
  - amazon.aws>=7.0.0
  - kubernetes.core>=3.0.0

# requirements.txt
jmespath>=1.0.1
netaddr>=1.3.0
boto3>=1.34.0
kubernetes>=28.0.0

# bindep.txt (system packages)
gcc [compile]
python3-devel [compile]
libxml2-devel [platform:redhat]
# Build the EE
pip install ansible-builder
ansible-builder build --tag myorg/ansible-ee:1.0.0

# Run with ansible-navigator
pip install ansible-navigator
ansible-navigator run site.yml -i inventory/production \
  --execution-environment-image myorg/ansible-ee:1.0.0 \
  --mode stdout

# Push to registry
docker push myorg/ansible-ee:1.0.0

ansible-navigator

# Interactive mode (TUI)
ansible-navigator run site.yml --eei myorg/ansible-ee:1.0.0

# Stdout mode (CI-friendly)
ansible-navigator run site.yml --eei myorg/ansible-ee:1.0.0 -m stdout

# Explore EE contents
ansible-navigator collections --eei myorg/ansible-ee:1.0.0
ansible-navigator images

AAP / AWX Integration

# In AAP/AWX, EEs are first-class:
# 1. Push EE to container registry
# 2. Add registry credential in AAP
# 3. Create EE pointing to your image
# 4. Assign EE to job templates
# → Every job runs in your exact environment

Decision Guide

Single developer, same OS?
├─ Yes → virtualenv (simplest)
└─ No  ↓

Need system packages or C libraries?
├─ Yes ↓
└─ No  → virtualenv + requirements.txt

Using AAP/AWX?
├─ Yes → Execution Environment (required for AAP 2.x)
└─ No  ↓

Team + CI/CD needs reproducibility?
├─ Yes → Docker or Execution Environment
└─ No  → virtualenv

Common Mistakes

# ❌ Installing Ansible globally
sudo pip install ansible  # Pollutes system Python!

# ✅ Always use isolation
python3 -m venv ~/ansible-env

# ❌ Not pinning versions in EE
# requirements.yml
collections:
  - community.general  # Gets latest — may break!

# ✅ Pin versions
collections:
  - name: community.general
    version: ">=9.0.0,<10.0.0"

# ❌ Using Docker when EE exists (reinventing the wheel)
# Dockerfile with manual ansible-galaxy install, pip install...

# ✅ Use ansible-builder — it handles all of this
ansible-builder build --tag myorg/ansible-ee:1.0.0

Conclusion

virtualenv for solo developers or quick experiments — isolates Python but not the OS. Docker for CI/CD and team standardization — full OS isolation with familiar tooling. Execution Environments for enterprise and AAP — purpose-built for Ansible with collections, Python deps, and system packages in one versioned image. If you're using AAP 2.x, EEs are required. If you're running in CI/CD, either Docker or EEs work. If you're a solo dev, start with virtualenv and graduate to EEs when you need reproducibility.