Introduction

Ansible Runner (ansible-runner) is a command-line tool that executes Ansible playbooks inside Execution Environments (EE) — container images that package Ansible, Python dependencies, and collections into a portable runtime. This replaces managing Python virtual environments manually and ensures your development and production environments are identical. This article covers installation, project structure, running playbooks in containers, and troubleshooting.

What Are Execution Environments?

Execution Environments are container images (OCI/Docker) that serve as Ansible control nodes. They contain:

  • ansible-core (or full ansible package)
  • Python dependencies (boto3, pywinrm, etc.)
  • Ansible collections (community.general, amazon.aws, etc.)
  • System packages (gcc, openssl-devel, etc.)
┌─────────────────────────────┐
│     Execution Environment   │
│  ┌───────────────────────┐  │
│  │ ansible-core 2.16+    │  │
│  │ Python 3.12           │  │
│  │ amazon.aws collection │  │
│  │ boto3, botocore       │  │
│  │ community.general     │  │
│  └───────────────────────┘  │
└─────────────────────────────┘

Why Use EEs?

ChallengeWithout EEWith EE
Python conflictsManual venv managementIsolated container
Collection versionsGlobal install collisionsPer-EE pinned versions
Dev/Prod parity"Works on my machine"Same image everywhere
CI/CDComplex setup scriptsdocker pull + run
Team consistencyEach dev has different envShared container image

Installation

Install ansible-runner

# Via pip
pip install ansible-runner

# On RHEL/CentOS with AAP subscription
sudo dnf install ansible-runner

# On Fedora
sudo dnf install ansible-runner

# Verify
ansible-runner --version

Install Container Runtime

ansible-runner needs podman or docker:

# Podman (preferred on RHEL/Fedora)
sudo dnf install podman

# Docker
sudo apt install docker.io   # Debian/Ubuntu
# or
sudo dnf install docker-ce   # RHEL/Fedora

Project Structure

ansible-runner expects a specific directory layout:

my-project/
├── project/
│   ├── site.yml              # Your playbook
│   └── roles/
│       └── webserver/
├── inventory/
│   └── hosts                 # Inventory file
├── env/
│   ├── settings              # Runner settings (JSON)
│   ├── envvars               # Environment variables
│   ├── extravars             # Extra variables
│   ├── cmdline               # CLI arguments
│   └── ssh_key               # SSH private key
└── artifacts/                # Output (auto-created)
    └── <uuid>/
        ├── stdout
        ├── stderr
        ├── rc
        └── fact_cache/

Basic Usage

Run a Playbook Locally (No Container)

# Simple run
ansible-runner run /path/to/project -p site.yml

# With inventory
ansible-runner run . -p site.yml --inventory inventory/hosts

Run Inside an Execution Environment

# Run playbook in a container
ansible-runner run . \
  -p ping.yml \
  --inventory inventory/hosts \
  --container-image=quay.io/ansible/ansible-runner:latest

# With a custom EE
ansible-runner run . \
  -p site.yml \
  --inventory inventory/hosts \
  --container-image=my_ee:latest

Practical Example

# Create project structure
mkdir -p my-project/project my-project/inventory

# Create inventory
cat > my-project/inventory/hosts << 'EOF'
[local]
localhost ansible_connection=local
EOF

# Create playbook
cat > my-project/project/ping.yml << 'EOF'
---
- name: Ping test
  hosts: all
  gather_facts: false
  tasks:
    - name: Test connection
      ansible.builtin.ping:
EOF

# Run it
ansible-runner run my-project -p ping.yml --inventory inventory/hosts

Output

PLAY [Ping test] ***************************
TASK [Test connection] *********************
ok: [localhost]
PLAY RECAP *********************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Building Custom Execution Environments

Use ansible-builder to create custom EEs:

Install ansible-builder

pip install ansible-builder

Create EE Definition

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

images:
  base_image:
    name: quay.io/ansible/ansible-runner:latest
# requirements.yml
---
collections:
  - name: amazon.aws
    version: ">=7.0.0"
  - name: community.general
    version: ">=8.0.0"
  - name: ansible.posix
# requirements.txt
boto3>=1.28.0
botocore>=1.31.0
pywinrm>=0.4.3
# bindep.txt
gcc [compile]
python3-devel [compile]

Build the EE

# Build with podman
ansible-builder build --tag my_ee:latest

# Build with docker
ansible-builder build --tag my_ee:latest --container-runtime docker

# Verify
podman images | grep my_ee

Run with Custom EE

ansible-runner run . \
  -p site.yml \
  --container-image=my_ee:latest

Configuration Files

env/settings (JSON)

{
  "process_isolation": true,
  "process_isolation_executable": "podman",
  "container_image": "my_ee:latest",
  "container_volume_mounts": [
    "/home/deploy/.ssh:/home/runner/.ssh:Z"
  ]
}

env/envvars

{
  "AWS_ACCESS_KEY_ID": "AKIAIOSFODNN7EXAMPLE",
  "AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE",
  "ANSIBLE_FORCE_COLOR": "true"
}

env/extravars

{
  "target_env": "production",
  "app_version": "2.1.0"
}

env/cmdline

--tags deploy --limit webservers -v

Advanced Usage

Run with Volume Mounts

# Mount SSH keys into container
ansible-runner run . \
  -p site.yml \
  --container-image=my_ee:latest \
  --container-volume-mount="/home/user/.ssh:/home/runner/.ssh:Z"

Run with Environment Variables

ansible-runner run . \
  -p aws_deploy.yml \
  --container-image=my_ee:latest \
  --cmdline="--extra-vars 'target_env=staging'" \
  --container-volume-mount="/home/user/.aws:/home/runner/.aws:Z"

Transmit Mode (Remote Execution)

# Package project for remote execution
ansible-runner transmit . --cmdline="-p site.yml"

# On remote host
ansible-runner worker < payload.zip

# Collect results
ansible-runner process results/

ansible-runner vs ansible-playbook

Featureansible-playbookansible-runner
Container support❌ No✅ Built-in
Structured outputText onlyJSON artifacts
Artifact storageNoneAutomatic
API integrationCLI onlyPython API available
AAP/Tower compatIndirectDirect
IsolationNoneContainer-based

Troubleshooting

Container Not Found

# Error: container image not found
# Fix: pull the image first
podman pull quay.io/ansible/ansible-runner:latest

Permission Denied on Mounted Volumes

# Fix: use :Z flag for SELinux
--container-volume-mount="/path:/path:Z"

Python Package Missing in EE

# Check what's inside the EE
podman run --rm my_ee:latest pip list

# Rebuild with the missing package
# Add to requirements.txt, then:
ansible-builder build --tag my_ee:latest

Slow First Run

The first run downloads the container image. Subsequent runs use the cached image:

# Pre-pull to avoid delays
podman pull my_ee:latest

Best Practices

  1. Pin collection versions in requirements.yml — avoid surprise updates
  2. Use multi-stage builds — keep EE images small
  3. Tag images with versions — my_ee:1.0.0, not just my_ee:latest
  4. Store EE definitions in Git — version your build configs
  5. Use podman over docker — rootless containers, no daemon
  6. Mount SSH keys read-only — --container-volume-mount="/ssh:/ssh:ro,Z"
  7. Cache pip packages — speed up builds with --build-arg PIP_CACHE_DIR

Conclusion

ansible-runner is the standard way to run Ansible playbooks inside Execution Environments. It provides container isolation, structured JSON output, and direct integration with Ansible Automation Platform. Use ansible-builder to create custom EEs with your required collections and Python dependencies, then run them with ansible-runner run --container-image. This ensures consistent, reproducible automation across development, CI/CD, and production environments.