Intermediate

Automating Container Management with Ansible

The goal of this demo is to automate the configuration of Docker and Podman host prerequisites using Ansible.

Table of Contents

  1. Introduction and course overview
  2. Module 1 — Getting started: Ansible, container engines and lab setup
  1. End to end container automation
  1. Standardizing automation with Ansible Execution Environments
  1. Key Command Summary
  2. Summary of Ansible modules used

1. Course Introduction and Overview

Welcome to the Automating Container Management with Ansible training. This training is taught by Andrei Balint, Red Hat Certified System Administrator (RHCSA) and Red Hat Certified Engineer (RHCE), also a Linux instructor specializing in automation, containers and enterprise Linux systems.

Problem

When containers are managed manually using commands like docker run or podman run, configurations can easily become inconsistent. Different administrators may start containers with slightly different options, different environment variables, or different network settings. On a small scale this can work, but as environments grow these differences lead to deployment errors and unpredictable behavior.

What we are looking for above all is consistency.

Solution: Ansible

This is where Ansible comes in. Ansible helps automate container host configuration, container deployment, and the entire image lifecycle in a repeatable and consistent manner.

Course structure

The training is structured into three main modules:

  1. Automate container host configuration: Installing and configuring Docker and Podman, preparing the system for containers to run reliably.
  2. Automate the container lifecycle: manage images and launch containers with defined configuration, storage and networking.
  3. Ansible Execution Environments: Package dependencies and run automation identically on different systems.

2. Getting Started: Ansible, Container Engines, and Lab Setup


1.1 Overview: Ansible Automation for Docker and Podman

Before you start writing playbooks or automating container deployment, it is important to understand the role each technology plays.

The role of Ansible in the container ecosystem

Ansible is an automation tool that allows you to describe what you want (declarative approach) rather than how to do it (imperative approach). In the context of containers:

  • Consistency: the same configurations are applied everywhere
  • Idempotence: restarting a playbook changes nothing if the desired state has already been reached
  • Scalability: manage tens or hundreds of hosts in the same way

Docker vs Podman

Both container engines serve the same fundamental purpose: running containers. The main differences are:

AppearanceDockerPodman
ArchitectureDaemon (dockerd) in backgroundDaemonless
PrivilegesUsually requires rootNatively supports rootless containers
Ansible collectioncommunity.dockercontainers.podman
Main commanddocker runpodman run

With Ansible, you can manage both coherently thanks to dedicated modules in their respective collections.


1.2 Demo: Installing and Configuring Container Engines with Ansible

Laboratory environment

The demonstration environment is made up of three machines, all running Red Hat Enterprise Linux 10:

  • control: Ansible control node, runs Ansible Core
  • docker: managed node that will run the Docker engine
  • podman: managed node that will run the Podman engine

All machines have internet and network connectivity between them. The docker and podman machines are defined in /etc/hosts so that they can be addressed by name rather than by IP address.

Step 1: Creating the user dedicated to automation

On each machine (control, docker, podman), you must create a specialized user for automation:

# Créer l'utilisateur automation
sudo useradd automation

# Définir le mot de passe (redhat dans cet exemple)
echo "redhat" | sudo passwd --stdin automation

# Accorder les privilèges sudo sans mot de passe
sudo vi /etc/sudoers.d/automation

Contents of file /etc/sudoers.d/automation:

automation ALL=(ALL) NOPASSWD: ALL

Step 2: Install ansible-core on the control node

# Passer en utilisateur automation
su - automation

# Installer ansible-core
sudo dnf install ansible-core -y

# Valider l'installation
ansible --version

The version installed in this demo is ansible-core 2.16.

Step 3: Configure Passwordless SSH Access

# Générer une paire de clés SSH
ssh-keygen

# Copier la clé vers le nœud podman
ssh-copy-id automation@podman

# Copier la clé vers le nœud docker
ssh-copy-id automation@docker

Step 4: Creating the Ansible inventory and configuration file

mkdir ~/lab
cd ~/lab

inventory file:

[podman_hosts]
podman

[docker_hosts]
docker

ansible.cfg file:

[defaults]
inventory = inventory
remote_user = automation

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false

Connectivity validation:

ansible all -m ping

Both servers respond with pong, confirming that the configuration is working correctly.

Step 5: Creating the Container Engines Installation Playbook

File container-engine-installation.yml:

---
- name: Install podman
  hosts: podman_hosts
  tasks:
    - name: Install container-tools
      ansible.builtin.dnf:
        name: container-tools
        state: present

    - name: Verify Podman installation
      ansible.builtin.command: podman --version
      register: podman_version

    - name: Show Podman version
      ansible.builtin.debug:
        var: podman_version.stdout

- name: Install Docker
  hosts: docker_hosts
  tasks:
    - name: Remove conflicting packages
      ansible.builtin.dnf:
        name:
          - docker
          - docker-client
          - docker-client-latest
          - docker-common
          - docker-latest
          - docker-latest-logrotate
          - docker-logrotate
          - podman
          - runc
        state: absent
      ignore_errors: true

    - name: Install dnf-plugins-core
      ansible.builtin.dnf:
        name: dnf-plugins-core
        state: present

    - name: Add Docker repository
      ansible.builtin.command: >
        dnf config-manager --add-repo
        https://download.docker.com/linux/rhel/docker-ce.repo

    - name: Install Docker
      ansible.builtin.dnf:
        name:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-buildx-plugin
          - docker-compose-plugin
        state: present

    - name: Verify Docker installation
      ansible.builtin.command: docker --version
      register: docker_version

    - name: Show Docker version
      ansible.builtin.debug:
        var: docker_version.stdout

Execution and results

# Vérification syntaxique
ansible-playbook container-engine-installation.yml --syntax-check

# Exécution du playbook
ansible-playbook container-engine-installation.yml

Results obtained:

  • Podman version 5.6: the first play worked perfectly
  • Docker version 29: the second play worked perfectly

1.3 Host prerequisites for container stability

Unlike virtual machines, containers do not include their own kernel. They share the kernel of the host operating system. This means that container runtimes like Docker and Podman rely heavily on the host system for features like networking, storage, namespaces, and security controls. If the host is not configured correctly, containers may fail to start or behave unpredictably.

Prerequisites for Docker hosts

1. Permission baseline — Docker Group

Adding the automation user to the docker group allows Ansible to manage containers without requiring direct root access.

2. Enabling IPv4 forwarding

IPv4 forwarding (net.ipv4.ip_forward) must be enabled on the host. This allows containers to communicate with external networks and other systems.

3. Kernel module br_netfilter

This module allows the host firewall to inspect traffic crossing Linux bridges, which is relevant for container network environments. On systems like Red Hat Enterprise Linux, this module is often loaded automatically during container network initialization.

4. Docker service (dockerd)

Docker relies on a background daemon called dockerd, which manages containers, networks and images. This service must be started and enabled so that it runs automatically after the system restarts.

5. Time synchronization with chrony

Time synchronization is important to avoid errors when downloading images from Docker Hub or other registries.

Prerequisites for Podman hosts

1. Support for rootless containers

For Podman, you must ensure that the host supports rootless containers. This involves verifying that user namespace mapping exists, which allows containers to run securely without elevated privileges.

The key files are:

  • /etc/subuid: mapping of UIDs for user namespaces
  • /etc/subgid: mapping GIDs for user namespaces

2. Daemonless architecture

Podman is daemonless, unlike Docker. There are no services to enable for Podman itself, which simplifies setup.

3. Time synchronization with chrony

As with Docker, time synchronization remains important.

Summary of prerequisites

PrerequisitesDockerPodman
Dedicated groupdocker groupN/A
User namespaceN/A/etc/subuid + /etc/subgid
IPv4 forwardingRequiredRequired
br_netfilterUsefulUseful
System servicedockerd enabledN/A (daemonless)
Time synchronizationchronychrony

1.4 Demo: Implementing Host Prerequisites with Ansible

The goal of this demo is to automate the configuration of Docker and Podman host prerequisites using Ansible.

Inventory structure

[docker_hosts]
docker

[podman_hosts]
podman

Playbook for Docker hosts

configure-docker.yml file:

---
- name: Configure docker hosts
  hosts: docker_hosts
  tasks:
    - name: Ensure chronyd is enabled
      ansible.builtin.service:
        name: chronyd
        state: started
        enabled: true

    - name: Ensure group docker exists
      ansible.builtin.group:
        name: docker
        state: present

    - name: Add automation to docker group
      ansible.builtin.user:
        name: automation
        groups: docker
        append: true

    - name: Ensure IPv4 forwarding is enabled
      ansible.posix.sysctl:
        name: net.ipv4.ip_forward
        value: '1'
        state: present
        sysctl_set: true
        reload: true

    - name: Ensure docker service is started and enabled
      ansible.builtin.service:
        name: docker
        state: started
        enabled: true

Note: The ansible.posix.sysctl module is part of the ansible.posix collection. The sysctl_set: true and reload: true parameters ensure that the setting is applied immediately and will persist after reboot.

# Vérification syntaxique
ansible-playbook configure-docker.yml --syntax-check

# Exécution
ansible-playbook configure-docker.yml

Validate on Docker host:

ssh docker

# Vérifier que l'utilisateur automation est dans le groupe docker
id automation
# uid=1001(automation) gid=1001(automation) groups=1001(automation),993(docker)

# Vérifier que le service Docker est actif
systemctl status docker
# Active: active (running) ...enabled

# Vérifier le forwarding IPv4
sudo sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 1

Playbook for Podman Prerequisites

File podman-prerequisites.yml:

---
- name: Configure Podman prerequisites
  hosts: podman_hosts
  tasks:
    - name: Ensure chrony is running
      ansible.builtin.service:
        name: chronyd
        state: started
        enabled: true

    - name: Verify subuid
      ansible.builtin.command: grep -E 'automation' /etc/subuid
      register: subuid_check

    - name: Show subuid check
      ansible.builtin.debug:
        var: subuid_check

    - name: Verify subgid
      ansible.builtin.command: grep -E 'automation' /etc/subgid
      register: subguid_check

    - name: Show subgid check
      ansible.builtin.debug:
        var: subguid_check
ansible-playbook podman-prerequisites.yml

Results: The user automation is defined in both /etc/subuid and /etc/subgid files, confirming that the user namespace mapping exists. Podman rootless containers can therefore be run by the automation user.


1.5 Managing container collections with ansible-galaxy

Why are collections needed?

When automating containers with Ansible, most container features are not included in Ansible Core. Container management relies on collections, which are installable packages extending Ansible with new modules and plugins.

For example:

  • community.docker: modules to manage Docker environments
  • containers.podman: modules to manage Podman environments

These collections include their own dependencies and version updates, allowing them to evolve independently of the Ansible Core release cycle. This makes collections the standard way to extend Ansible with specialized automation capabilities.

Typical workflow with collections

Recherche → Listage → Installation → Inspection
  1. Search: on the site galaxy.ansible.com
  2. List the collections already installed:
    ansible-galaxy collection list
    
  3. Install a collection:
    ansible-galaxy collection install community.docker
    ansible-galaxy collection install containers.podman
    
  4. Inspect the modules of a collection:
    ansible-doc -l | grep community.docker
    ansible-doc community.docker.docker_container
    

Demo: Installing collections

# Installer la collection community.docker (version 4.8.1 compatible avec ansible-core 2.16)
ansible-galaxy collection install community.docker:4.8.1

# Installer la collection containers.podman
ansible-galaxy collection install containers.podman

# Lister toutes les collections installées
ansible-galaxy collection list
# community.docker  4.8.1
# containers.podman  x.x.x

# Lister tous les modules Docker disponibles
ansible-doc -l | grep community.docker

# Obtenir la documentation détaillée d'un module spécifique
ansible-doc community.docker.docker_container

# Lister tous les modules Podman disponibles
ansible-doc -l | grep containers.podman

Important: Version 4.8.1 of community.docker was chosen because it is the latest version compatible with ansible-core 2.16 (the stable version of Red Hat used in this course).


3. End to end container automation


2.1 Containers and images

Before you start automating anything, it is necessary to have a very clear mental model of what images and containers are.

Container images

A good way to think about a container image is to compare it to a manufacturing blueprint. It defines exactly how something should be built, what components are needed, how they fit together, and how the final product should behave. But just like a blueprint, it doesn’t do anything on its own. Nothing is running. It’s just a definition.

Layered structure of an image

An image is not a single large file. It is made up of layers stacked on top of each other:

┌─────────────────────────────────┐
│        Code de l'application    │  ← Couche supérieure
├─────────────────────────────────┤
│     Dépendances de l'application│
├─────────────────────────────────┤
│        Bibliothèques système    │
├─────────────────────────────────┤
│           Système de base (OS)  │  ← Couche inférieure
└─────────────────────────────────┘

Each step creates a separate layer, and once created, these layers are read-only. We don’t modify things, we stack them.

The key benefit here is reusability. If two images use the same base OS or libraries, these layers are shared. And if we change something like the application code, only the top layer needs to be rebuilt.

Key Image Features

CharacteristicDescription
ImmutabilityOnce created, an image does not change. To update, we build a new image.
Layered structureEach instruction in the Dockerfile/Containerfile creates a new layer
PortabilityAn image works the same on any compatible system
VersionImages can be tagged with versions (v1, latest, 2.0, etc.)

Containers

A container is a running instance of an image. The relationship is:

Image  →  Conteneur(s)
Blueprint → Instances du produit
  • The image is a snapshot; the container is the execution instance
  • Multiple containers can be created from a single image
  • By default, containers are ephemeral: if a container is deleted, all data it contains is lost

2.2 Managing container images with Ansible

Why use Ansible for image management?

  1. No manual commands: instead of running Docker or Podman commands one by one, we define everything in a playbook. We move from typing commands to describing what we want.

  2. Idempotent behavior: if something is already in the correct state, Ansible will not change anything. You can run the same playbook several times and still get the same result.

  3. Easy scalability: instead of managing a system manually, you can apply the same configuration on several systems simultaneously.

Main modules for image management

EngineModuleCollection
Dockerdocker_imagecommunity.docker
Podmanpodman_imagecontainers.podman

These modules work similarly, so once you understand one, the other is easy to learn.


2.3 Demo: Managing container images with Ansible

The objective of this demo is to use Ansible to: pull images, build them, tag them and archive them.

Source files required

files/index.html:

This image was built with Ansible

files/Dockerfile (for Docker):

FROM httpd:latest
COPY index.html /usr/local/apache2/htdocs/index.html

files/Containerfile (for Podman):

FROM registry.redhat.io/ubi8/httpd-24
COPY index.html /var/www/html/index.html

Docker playbook for image management

File docker.yml:

---
- name: Manage container images
  hosts: docker
  become: false
  tasks:
    - name: Create build directory
      ansible.builtin.file:
        path: /home/automation/webdemo
        state: directory
        mode: '0755'

    - name: Copy index.html
      ansible.builtin.copy:
        src: files/index.html
        dest: /home/automation/webdemo/index.html
        mode: '0644'

    - name: Copy Dockerfile
      ansible.builtin.copy:
        src: /home/automation/lab/Dockerfile
        dest: /home/automation/webdemo/Dockerfile
        mode: '0644'

    - name: Pull the image
      community.docker.docker_image:
        name: httpd
        source: pull
        state: present

    - name: Build custom image
      community.docker.docker_image:
        name: demo-httpd
        tag: v1
        source: build
        build:
          path: /home/automation/webdemo
        state: present

    - name: Tag image
      community.docker.docker_image:
        name: demo-httpd
        tag: v1
        repository: local/demo-httpd
        source: local
        state: present

    - name: Archive image
      community.docker.docker_image:
        name: local/demo-httpd
        tag: v1
        source: local
        archive_path: /home/automation/webdemo/local-demo-http.tar
ansible-playbook docker.yml

Validate on Docker host:

ssh docker

# Lister les images présentes
docker images
# REPOSITORY       TAG       IMAGE ID      CREATED        SIZE
# local/demo-httpd  v1       ...           ...            ...
# demo-httpd        v1       ...           ...            ...
# httpd             latest   ...           ...            ...

# Vérifier l'archive
ls /home/automation/webdemo/
# index.html  Dockerfile  local-demo-http.tar

Podman playbook for image management

podman.yml file:

---
- name: Manage podman images
  hosts: podman
  become: false
  tasks:
    - name: Ensure build directory exists
      ansible.builtin.file:
        path: /home/automation/webdemo
        state: directory
        mode: '0755'

    - name: Copy index.html
      ansible.builtin.copy:
        src: files/index.html
        dest: /home/automation/webdemo/index.html
        mode: '0644'

    - name: Copy Containerfile
      ansible.builtin.copy:
        src: /home/automation/lab/Containerfile
        dest: /home/automation/webdemo/Containerfile
        mode: '0644'

    - name: Log in to registry.redhat.io
      containers.podman.podman_login:
        registry: registry.redhat.io
        username: "{{ redhat_username }}"
        password: "{{ redhat_password }}"

    - name: Pull base image
      containers.podman.podman_image:
        name: registry.redhat.io/ubi8/httpd-24
        tag: latest

    - name: Build custom image
      containers.podman.podman_image:
        name: demo-httpd
        tag: v1
        path: /home/automation/webdemo

    - name: Tag image
      containers.podman.podman_tag:
        image: demo-httpd
        target_names:
          - localhost/demo-httpd:v1

    - name: Archive image
      containers.podman.podman_save:
        image: demo-httpd
        dest: /home/automation/demo-httpd.tar
ansible-playbook podman.yml

Validation on Podman host:

ssh podman

# Lister les images
podman images
# REPOSITORY                          TAG     IMAGE ID   CREATED    SIZE
# localhost/demo-httpd                v1      ...        ...        ...
# registry.redhat.io/ubi8/httpd-24   latest  ...        ...        ...

# Vérifier l'archive
ls /home/automation/
# demo-httpd.tar

2.4 Running and managing containers with Ansible

Container management modules

EngineModuleCollection
Dockerdocker_containercommunity.docker
Podmanpodman_containercontainers.podman

These modules serve the same purpose. They allow you to define how containers should run. The syntax is also very similar, so once you learn how to use one, you can easily switch to the other.

Key concept: we don’t tell the system what to do, we tell it how it should look. This is declarative container management.

Key things to check when running a container

1. The state

state: started   # Le conteneur doit être en cours d'exécution
state: stopped   # Le conteneur doit être arrêté
state: absent    # Le conteneur doit être supprimé

2. Environment variables

Environment variables are a simple way to pass configuration into the container at runtime. For example: application modes, database connection details, API endpoints, etc. The important thing is that we don’t rebuild the image — we keep the image the same and just change its behavior using these variables.

env:
  APP_ENV: "Production"
  LOG_LEVEL: "info"
  FEATURE_FLAG: "true"

3. Resource limits

Containers share the same system, so without limits, one container could hog everything. We define the amount of CPU and memory that a container is allowed to use.

cpus: "1"
memory: "512m"

4. The restart policy

Containers may fail. We define what should happen when they stop:

PoliticsBehavior
noNo automatic restart
on-failureRestart only in case of error
alwaysAlways restarts, even if stopped manually

Demo: Launching Containers with Docker

File docker.yml:

---
- name: Launch and manage Docker containers
  hosts: docker
  become: false
  tasks:
    - name: Launch httpd container
      community.docker.docker_container:
        name: demo-httpd
        image: httpd:latest
        state: started
        recreate: true
        pull: missing
        env:
          APP_ENV: "Production"
          LOG_LEVEL: "info"
          FEATURE_FLAG: "true"
        cpus: "1"
        memory: "512m"
        restart_policy: always

Note: recreate: true means that if the container already exists, it will be recreated. pull: missing indicates to pull the image only if it is not available locally.

ansible-playbook docker.yml

Validate on Docker host:

ssh docker

# Lister les conteneurs en cours d'exécution
docker ps
# CONTAINER ID  IMAGE         COMMAND    STATUS      PORTS  NAMES
# abc123        httpd:latest  ...        Up 2 min           demo-httpd

# Vérifier la variable LOG_LEVEL dans le conteneur
docker exec demo-httpd printenv LOG_LEVEL
# info

# Vérifier la limite mémoire
docker inspect demo-httpd | grep -i memory
# "Memory": 536870912,   ← 512 Mo en octets

# Vérifier la politique de redémarrage
docker inspect demo-httpd | grep -i restart
# "RestartPolicy": {"Name": "always", ...}

Demo: Launching Containers with Podman

podman.yml file:

---
- name: Manage Podman Container
  hosts: podman
  become: false
  tasks:
    - name: Launch podman container
      containers.podman.podman_container:
        name: httpd-demo
        image: registry.redhat.io/ubi8/httpd-24
        state: started
        recreate: true
        env:
          APP_ENV: "production"
          LOG_LEVEL: "info"
          FEATURE_FLAG: "true"
        cpus: "1"
        memory: "512m"
        restart_policy: always
ansible-playbook podman.yml

Validation on Podman host:

ssh podman

# Lister les conteneurs en cours d'exécution
podman ps
# CONTAINER ID  IMAGE                              STATUS    NAMES
# def456        registry.redhat.io/ubi8/httpd-24  Up 1 min  httpd-demo

# Vérifier la variable LOG_LEVEL dans le conteneur
podman exec httpd-demo bash -c "echo $LOG_LEVEL"
# info

# Inspecter le conteneur (variables d'env, limites, restart policy)
podman inspect httpd-demo
# Les variables d'environnement, les limites CPU/mémoire et la restart_policy sont visibles

2.5 Persistent Storage and Container Networking

The problem of ephemerality

By default, containers are ephemeral. This means that if a container is deleted, all the data it contains is lost. For applications like databases or web servers, we need a different approach.

Persistent Storage Options

1. Volumes (managed by the container engine)

Volumes are the simplest option. They are managed by the container engine, so there is no concern about where the data lives. We use them and it works.

# Création d'un volume Docker
community.docker.docker_volume:
  name: httpd_data
  state: present

# Utilisation dans un conteneur
volumes:
  - "httpd_data:/usr/local/apache2/htdocs"

2. Bind mounts (mounts bound to a host path)

Bind mounts are more direct. We choose the exact path on the host, so we have full control over the data. This is powerful, but it also means we need to be more careful about permissions, paths, and management.

volumes:
  - "/home/automation/mountme:/usr/local/apache2/htdocs"

Networking Options

1. Bridge network (default)

Each container is in its own space with controlled access. This is the default mode.

network_mode: bridge
published_ports:
  - "8081:80"   # port_hôte:port_conteneur

2. Network Host

The container uses the host’s network, so there is no isolation. In return, we obtain better network performance.

network_mode: host

3. Custom network

Custom bridge networks can be created to better isolate and organize containers.

networks:
  - name: demo_net

Port mapping

Port mapping is like opening a door and saying, “if someone comes to this port on the host, send them to the application inside the container.”

published_ports:
  - "8081:80"    # trafic vers port 8081 de l'hôte → port 80 du conteneur
  - "8082:80"    # trafic vers port 8082 de l'hôte → port 80 du conteneur

2.6 Demo: Managing Persistent Storage and Container Networking

The objective of this demo is to show how to make data survive and applications accessible, using volumes, bind mounts and different network modes.

Playbook Docker — Storage and networking

File docker.yml:

---
- name: Persistent storage and networking
  hosts: docker
  become: false
  tasks:
    - name: Create volume for persistent storage
      community.docker.docker_volume:
        name: httpd_data
        state: present

    - name: Create host mount directory
      ansible.builtin.file:
        path: /home/automation/mountme
        state: directory
        mode: '0755'

    - name: Create index.html
      ansible.builtin.copy:
        dest: /home/automation/mountme/index.html
        content: "hello world"
        mode: '0644'

    - name: Create bridge network
      community.docker.docker_network:
        name: demo_net
        driver: bridge
        state: present

    - name: Launch container on default bridge with volume
      community.docker.docker_container:
        name: docker-httpd-bridge
        image: httpd:latest
        state: started
        recreate: true
        network_mode: bridge
        published_ports:
          - "8081:80"
        volumes:
          - "httpd_data:/usr/local/apache2/htdocs"
        restart_policy: always

    - name: Launch container with host network and bind mount
      community.docker.docker_container:
        name: docker-httpd-host
        image: httpd:latest
        state: started
        recreate: true
        network_mode: host
        volumes:
          - "/home/automation/mountme:/usr/local/apache2/htdocs"
        restart_policy: always

    - name: Launch container on custom network
      community.docker.docker_container:
        name: docker-httpd-custom
        image: httpd:latest
        state: started
        recreate: true
        networks:
          - name: demo_net
        published_ports:
          - "8082:80"
        restart_policy: always
ansible-playbook docker.yml

Validate on Docker host:

ssh docker

# Lister les conteneurs
docker ps
# 3 conteneurs en cours d'exécution :
# docker-httpd-bridge (port 8081)
# docker-httpd-host
# docker-httpd-custom (port 8082)

# Tester le conteneur sur le réseau bridge
curl localhost:8081
# Apache est opérationnel

# Tester le conteneur sur le réseau host
curl localhost:80
# Réponse "hello world" (depuis le bind mount)

# Tester le conteneur sur le réseau personnalisé
curl localhost:8082
# Apache est opérationnel

# Lister les volumes
docker volume ls
# DRIVER    VOLUME NAME
# local     httpd_data

# Lister les réseaux
docker network ls
# NETWORK ID   NAME      DRIVER   SCOPE
# ...          demo_net  bridge   local

Playbook Podman — Storage and Networking

podman.yml file:

---
- name: Podman persistent storage and networking
  hosts: podman
  become: false
  tasks:
    - name: Create podman volume
      containers.podman.podman_volume:
        name: httpd_data
        state: present

    - name: Create host directory for mounting
      ansible.builtin.file:
        path: /home/automation/mountme
        state: directory
        mode: '0755'

    - name: Create index.html
      ansible.builtin.copy:
        dest: /home/automation/mountme/index.html
        content: "it works"
        mode: '0644'

    - name: Create bridge network
      containers.podman.podman_network:
        name: demo-net
        driver: bridge
        state: present

    - name: Launch container on default bridge with volume
      containers.podman.podman_container:
        name: podman-bridge
        image: registry.redhat.io/ubi8/httpd-24
        state: started
        recreate: true
        network:
          - bridge
        ports:
          - "8081:8080"
        volume:
          - "httpd_data:/var/www"
        restart_policy: always

    - name: Launch container with host network and bind mount
      containers.podman.podman_container:
        name: podman-host
        image: registry.redhat.io/ubi8/httpd-24
        state: started
        recreate: true
        network:
          - host
        volume:
          - "/home/automation/mountme:/var/www/html:Z"
        restart_policy: always

    - name: Launch container on custom network
      containers.podman.podman_container:
        name: podman-custom
        image: registry.redhat.io/ubi8/httpd-24
        state: started
        recreate: true
        network:
          - demo-net
        ports:
          - "8082:8080"
        restart_policy: always

Important Note: For bind mounts with Podman on SELinux, adding :Z to the end of the volume path automatically configures the correct SELinux context, allowing the container process to access files on the host without permission errors.

ansible-playbook podman.yml

Validation on Podman host:

ssh podman

# Lister les conteneurs
podman ps
# 3 conteneurs en cours d'exécution avec les ports correspondants

# Tester l'accès web
curl localhost:8080
# "it works" (depuis le bind mount via le conteneur host)

# Lister les volumes
podman volume ls
# DRIVER      VOLUME NAME
# local       httpd_data

# Lister les réseaux
podman network ls
# NETWORK ID  NAME      VERSION  PLUGINS
# ...         demo-net  0.4.0    bridge,...

4. Standardizing automation with Ansible Execution Environments


3.1 Execution Environments: Concepts and Use Cases

What is an Execution Environment (EE)?

Think of an Execution Environment as a standalone toolbox. It’s a container that runs Ansible and carries every library, collection, and auxiliary tool the runtime needs. In summary: Ansible runs inside a container.

Why do we need Execution Environments?

Problem 1: Every system is different

Your control node today may not be the same tomorrow. Different Python version, different Ansible version, different collections installed — and all these little differences start to matter.

Issue 2: Missing dependencies

We build a playbook. It works perfectly on our machine. But when someone else wants to run it, or when we do it on another device, it fails — not because the playbook is incorrect, but because the environment is different.

Problem 3: Same playbook, different result

This is a direct consequence of the two previous problems.

How Execution Environments solve these problems

A single standard runtime:

Instead of depending on what is installed on the control node, we define everything in advance:

  • Ansible Version
  • Python version
  • Ansible Collections
  • System packages

Everything is grouped together. Every time we run a playbook, we use the exact same environment.

Same dependencies everywhere:

It doesn’t matter if you run the playbook on your laptop or on a server: the environment is identical.

Anatomy of an Execution Environment

┌─────────────────────────────────────────────┐
│               EE Container Image            │
├─────────────────────────────────────────────┤
│  Ansible Core + Collections + Dépendances   │
├─────────────────────────────────────────────┤
│           Image de base (OS)                │
└─────────────────────────────────────────────┘

An EE is simply a layered configuration:

  • Base image at the bottom
  • Ansible on top
  • All required dependencies at top

Workflow with Execution Environments

[Utilisateur] → ansible-navigator run playbook.yml
                        ↓
            Vérification de l'image EE (locale ou pull)
                        ↓
              Démarrage du conteneur EE
                        ↓
          Montage du répertoire projet dans le conteneur
                        ↓
        ansible-runner exécute le playbook avec ansible-core
                        ↓
         ansible-navigator collecte et affiche la sortie

3.2 Building Custom Execution Environments

ansible-builder: The EA construction tool

ansible-builder is the bridge between what we define and the environment we actually run. We describe its requirements (collections, dependencies, base image), and ansible-builder transforms this into a working container image. Instead of manually preparing the systems, we define everything in code and let the tool manage the rest.

Components of an Execution Environment

ComponentDescription
Basic imageFoundation: OS, libraries, runtime
Ansible CoreThe engine that runs playbooks
CollectionsNecessary modules, roles and plugins
Python DependenciesPython libraries required by certain modules
System dependenciesRequired OS packages (e.g.: openssh-clients)

Demo: Building a Custom EE from Scratch

Step 1: Installing prerequisites

# Installer container-tools (Podman est requis par ansible-builder)
sudo dnf install container-tools -y

# Installer Python 3 et pip
sudo dnf install python3 python3-pip -y

# Installer ansible-builder pour l'utilisateur courant
python3 -m pip install ansible-builder --user

# Valider l'installation
ansible-builder --version
# 3.1.1

Step 2: Create the working directory

mkdir ~/builder
cd ~/builder

Step 3: Create the EA definition file

File custom-ee-v1.0.yml:

---
version: 3

images:
  base_image:
    name: registry.redhat.io/ubi10/ubi:latest

dependencies:
  ansible_core:
    package_pip: ansible-core==2.17
  ansible_runner:
    package_pip: ansible-runner
  galaxy: requirements.yml
  python: requirements.txt
  system:
    - python3

Step 4: Create the Galaxy Collections File

File requirements.yml:

---
collections:
  - name: containers.podman
    version: "1.16"
  - name: community.docker
    version: "4.5"
  - name: ansible.posix
    version: "2.1"

Step 5: Create the Python dependency file

File requirements.txt:

jmespath==1.0.1

Step 6: Build the EE

# Construire l'EE et nommer l'image résultante "custom-env"
ansible-builder build \
  -f custom-ee-v1.0.yml \
  -t custom-env

# Ansible-builder va :
# 1. Créer un contexte de construction (Containerfile + contexte)
# 2. Construire l'image via Podman

Step 7: Validate the EA

# Vérifier que l'image est présente
podman images
# REPOSITORY          TAG     IMAGE ID   CREATED    SIZE
# localhost/custom-env  latest  ...        ...        ...

# Créer un conteneur basé sur l'EE pour valider son contenu
podman run -it localhost/custom-env bash

# Dans le conteneur, vérifier les versions :
python3 --version
# Python 3.12

ansible --version
# ansible-core 2.17

ansible-runner --version
# 2.4.3

# Vérifier les collections installées
ansible-galaxy collection list
# containers.podman  1.16
# community.docker   4.5
# ansible.posix      2.1

exit

3.3 Demo: Managing and troubleshooting EE dependencies

This demo takes a deeper dive into defining dependencies and shows how to troubleshoot issues that arise when building or using an EE.

Creation of the EE file for this demo

File execution-environment.yml:

---
version: 3

images:
  base_image:
    name: registry.redhat.io/ubi10/ubi:latest

dependencies:
  package_pip:
    - ansible-core
    - ansible-runner
  galaxy: requirements.yml
  python: requirements.txt
  system: bindep.txt

python_interpreter:
  package_system: python3
  python_path: /usr/bin/python3

File requirements.yml (ansible.netcommon collection):

---
collections:
  - name: ansible.netcommon

About ansible.netcommon: This collection provides common content to automate the management of network, security, and cloud devices. It is organized into three main categories: connection plugins, network modules, and utility modules.

Using the ansible-builder create command

The create command generates the build context without building the image:

ansible-builder create -f execution-environment.yml

This creates a context/ directory containing:

  • An automatically generated Containerfile
  • All necessary dependency files
ls context/
# Containerfile  _build/

The generated Containerfile looks like this:

ARG EE_BASE_IMAGE="registry.redhat.io/ubi10/ubi:latest"
FROM ${EE_BASE_IMAGE}

# Installer les dépendances Python de base
RUN python3 -m pip install ansible-core ansible-runner

# Installer les collections Galaxy
COPY _build/requirements.yml /build/requirements.yml
RUN ansible-galaxy collection install -r /build/requirements.yml

# Dépendances système
COPY _build/bindep.txt /build/bindep.txt
RUN /usr/bin/python3 -m bindep --newstyle > /tmp/packages.txt || true \
    && cat /tmp/packages.txt | xargs dnf install -y

Building the EE image

ansible-builder build \
  -f execution-environment.yml \
  -t execution1
podman images
# localhost/execution1  latest  ...  ...  ...

First problem: Missing SSH

Creating a test playbook:

---
# playbook.yml
- name: Testing
  hosts: podman
  tasks:
    - name: Using ping
      ansible.builtin.ping:

Ansible-navigator configuration:

# ansible-navigator.yml
---
ansible-navigator:
  execution-environment:
    enabled: true
    image: localhost/execution1:latest
    pull:
      policy: missing

Running the playbook with ansible-navigator:

ansible-navigator run playbook.yml -m stdout

Error obtained:

FAILED! => {"msg": "to use the 'ssh' connection type with passwords or
 passphrases, you must install the sshpass program"}

Diagnostic: SSH (openssh-clients) is not installed in the EE. Ansible uses SSH to connect to managed hosts.

Fix: Added system dependencies

File bindep.txt:

openssh-clients

Updating execution-environment.yml to reference bindep.txt:

dependencies:
  package_pip:
    - ansible-core
    - ansible-runner
  galaxy: requirements.yml
  system: bindep.txt

Reconstruction of the EE:

# Supprimer l'ancienne image
podman rmi localhost/execution1

# Reconstruire
ansible-builder build -f execution-environment.yml -t execution1

Restart the playbook:

ansible-navigator run playbook.yml -m stdout
# ✓ SUCCESS — Le ping fonctionne

Second issue: Missing Python dependency

Playbook using ansible.netcommon collection:

---
- name: Testing netconf
  hosts: docker
  become: false
  connection: ansible.netcommon.netconf
  tasks:
    - name: Get config
      ansible.netcommon.netconf_get:
      register: result

    - name: Show result
      ansible.builtin.debug:
        var: result

Context: on the Docker host, a container running a NETCONF server is running with credentials netconf/netconf.

The documentation for the ansible.netcommon collection indicates that the NETCONF login plugin requires the ncclient Python library.

Added Python dependency:

File requirements.txt:

ncclient

Update execution-environment.yml:

dependencies:
  package_pip:
    - ansible-core
    - ansible-runner
  galaxy: requirements.yml
  python: requirements.txt
  system: bindep.txt

Rebuilding and testing:

podman rmi localhost/execution1
ansible-builder build -f execution-environment.yml -t execution1
ansible-navigator run playbook.yml -m stdout
# ✓ SUCCESS — La connexion NETCONF fonctionne

Main Lesson on Troubleshooting EEs

The troubleshooting process always follows the same logic:

  1. Identify error in ansible-navigator output
  2. Read the documentation of the module or plugin to know its dependencies
  3. Add the missing dependency in the appropriate file:
  • System dependency → bindep.txt
  • Python dependency → requirements.txt
  • Galaxy Collection → requirements.yml
  1. Rebuild the EE with ansible-builder
  2. Relaunch the playbook to validate the correction

3.4 Executing playbooks in an Execution Environment

ansible-navigator: The execution tool in an EE

ansible-navigator is essentially a wrapper. It doesn’t replace Ansible or do anything magical on its own. What it does is provide a way to run Ansible in an Execution Environment. So instead of running Ansible directly on the system, navigator makes sure everything runs inside that container.

What happens when running a playbook with ansible-navigator

1. Lancement : ansible-navigator run playbook.yml
       ↓
2. Vérification : L'image EE existe-t-elle localement ?
   → Non : tentative de pull depuis un registre
       ↓
3. Démarrage du conteneur basé sur l'image EE
       ↓
4. Montage du répertoire projet dans le conteneur
   (les playbooks et fichiers sont accessibles)
       ↓
5. ansible-runner prend le contrôle à l'intérieur du conteneur
   et exécute le playbook avec ansible-core
       ↓
6. ansible-navigator collecte la sortie
   → Mode stdout : affichage dans le terminal
   → Mode interactif : interface navigable

Key Point: Even though the command is triggered from the host, the actual execution is done inside the isolated environment of the container.

Installing ansible-navigator

python3 -m pip install ansible-navigator --user

# Valider l'installation
ansible-navigator --version

Configuring ansible-navigator

ansible-navigator.yml file:

---
ansible-navigator:
  execution-environment:
    enabled: true
    image: localhost/custom-env:latest
    pull:
      policy: missing
ParameterDescription
enabled: trueEnables the use of an EE
imageThe EE image to use
pull.policy: missingOnly pull the image if it is not available locally

Creating a playbook to test EE

navigator-play.yml file:

---
- name: Gather facts within execution environment
  hosts: podman
  become: false
  tasks:
    - name: Gather facts about images
      containers.podman.podman_image_info:
      register: podman_images

    - name: Show image info
      ansible.builtin.debug:
        var: podman_images

ansible-navigator execution modes

Stdout mode:

ansible-navigator run navigator-play.yml -m stdout

Output is sent directly to the terminal, as with ansible-playbook.

Interactive mode (default):

ansible-navigator run navigator-play.yml

In interactive mode, you can:

  • See details of each play and each task
  • Navigate through the pages using the numbers displayed on the left side (0, 1, 2, 3, 4…)
  • Enter a number + Enter to access a specific page and go deeper
  • Press Esc to go up one level
  • Type settings to get information about ansible-navigator configuration
  • Type images to get information about EE images
# Spécifier explicitement l'image EE à utiliser
ansible-navigator run navigator-play.yml --eei localhost/custom-env:latest -m stdout

# Utilisation des options courtes
ansible-navigator run navigator-play.yml -m stdout

Artifacts

When ansible-navigator runs, it also generates artifacts. Artifacts look like logs from previous executions. You can view any artifact using the replay command:

ansible-navigator replay artifacts/navigator-play.json

Artifacts contain details about:

  • Play execution (status of each task)
  • Execution Environment used
  • Configuring ansible-navigator at runtime

3.5 Course Conclusion

Throughout this course, we have pursued one main goal: automate end-to-end container infrastructure.

What we have accomplished

Module 1 — Foundations

We started with the basics: understanding container engines like Podman and Docker, and how to prepare and configure hosts for them. We learned to:

  • Create users dedicated to automation
  • Install ansible-core and configure SSH without password
  • Install Docker and Podman via Ansible playbooks
  • Configure system requirements (groups, namespaces, IPv4 forwarding, services)
  • Manage collections with ansible-galaxy

Module 2 — Container management

We dove into actual container management: working with images, running containers, and managing storage and networking with Ansible. We learned to:

  • Understanding the layered structure of images and the ephemeral nature of containers
  • Pull, build, tag and archive images with docker_image and podman_image
  • Run containers with declarative configuration via docker_container and podman_container
  • Manage persistent storage with volumes and bind mounts
  • Configure networking (bridge, host, custom networks) and port mapping

Module 3 — Standardization

Finally, we introduced Execution Environments. This is where everything came together. Because once we started using EEs, we were no longer dependent on our local setup — we were running our automation in a controlled environment. We learned to:

  • Understand why EAs are necessary
  • Building custom EEs with ansible-builder
  • Set and resolve dependencies (system, Python, Galaxy)
  • Run playbooks in an EE with ansible-navigator
  • Use ansible-navigator interactive mode and stdout mode

Key Finding

The main message of this course is not just automation, but standardized automation.


5. Summary of key commands

Initial setup

# Créer l'utilisateur automation
sudo useradd automation
echo "redhat" | sudo passwd --stdin automation

# Configurer sudo sans mot de passe
echo "automation ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/automation

# Installer ansible-core
sudo dnf install ansible-core -y

# Générer et distribuer les clés SSH
ssh-keygen
ssh-copy-id automation@podman
ssh-copy-id automation@docker

# Tester la connectivité
ansible all -m ping

Ansible Collections

# Installer community.docker
ansible-galaxy collection install community.docker:4.8.1

# Installer containers.podman
ansible-galaxy collection install containers.podman

# Lister les collections installées
ansible-galaxy collection list

# Lister les modules d'une collection
ansible-doc -l | grep community.docker
ansible-doc -l | grep containers.podman

# Documentation d'un module spécifique
ansible-doc community.docker.docker_container
ansible-doc containers.podman.podman_container

Running playbooks

# Vérification syntaxique
ansible-playbook playbook.yml --syntax-check

# Exécution normale
ansible-playbook playbook.yml

# Mode verbeux
ansible-playbook playbook.yml -v
ansible-playbook playbook.yml -vvv

Docker (commit commands)

# Lister les images
docker images

# Lister les conteneurs en cours d'exécution
docker ps

# Inspecter un conteneur
docker inspect <nom_conteneur>

# Lister les volumes
docker volume ls

# Lister les réseaux
docker network ls

# Exécuter une commande dans un conteneur
docker exec <conteneur> env

Podman (commit commands)

# Lister les images
podman images

# Lister les conteneurs en cours d'exécution
podman ps

# Inspecter un conteneur
podman inspect <nom_conteneur>

# Lister les volumes
podman volume ls

# Lister les réseaux
podman network ls

# Exécuter une commande dans un conteneur
podman exec <conteneur> bash -c "echo $VARIABLE"

ansible-builder

# Installer ansible-builder
python3 -m pip install ansible-builder --user

# Créer uniquement le contexte de construction (sans construire)
ansible-builder create -f execution-environment.yml

# Construire l'EE
ansible-builder build -f execution-environment.yml -t <nom_image>

# Valider la version
ansible-builder --version

ansible-navigator

# Installer ansible-navigator
python3 -m pip install ansible-navigator --user

# Valider la version
ansible-navigator --version

# Exécuter en mode stdout (sortie terminal)
ansible-navigator run playbook.yml -m stdout

# Exécuter en mode interactif (défaut)
ansible-navigator run playbook.yml

# Spécifier l'image EE
ansible-navigator run playbook.yml --eei localhost/custom-env:latest -m stdout

# Rejouer un artefact
ansible-navigator replay <fichier_artefact>.json

6. Summary of Ansible modules used

Ansible kernel modules (ansible.builtin)

ModuleUse in the course
ansible.builtin.dnfInstalling packages (Docker, Podman, container-tools)
ansible.builtin.serviceActivation and startup of services (docker, chronyd)
ansible.builtin.groupCreation of the docker group
ansible.builtin.userAdding the automation user to the docker group
ansible.builtin.fileCreation of directories (webdemo, mountme)
ansible.builtin.copyCopying files (index.html, Dockerfile, Containerfile)
ansible.builtin.commandRunning commands (version checking, grep)
ansible.builtin.debugDisplaying saved variables
ansible.builtin.pingConnectivity test

Collection ansible.posix

ModuleUse in the course
ansible.posix.sysctlConfiguring net.ipv4.ip_forward

Collection community.docker

ModuleUse in the course
community.docker.docker_imagePull, build, tag and check in Docker images
community.docker.docker_containerCreating and managing Docker containers
community.docker.docker_volumeCreating persistent Docker volumes
community.docker.docker_networkCreation of Docker networks (bridge, custom)

Collection containers.podman

ModuleUse in the course
containers.podman.podman_loginRegistry Authentication (registry.redhat.io)
containers.podman.podman_imagePodman image pull and build
containers.podman.podman_tagPodman Image Tagging
containers.podman.podman_savePodman Image Archiving
containers.podman.podman_containerCreating and managing Podman containers
containers.podman.podman_volumeCreating Persistent Podman Volumes
containers.podman.podman_networkCreation of Podman networks
containers.podman.podman_image_infoCollecting image information

ansible.netcommon collection (used in EE demo)

Module/PluginUse in the course
ansible.netcommon.netconfNETCONF Login Plugin
ansible.netcommon.netconf_getRetrieving the configuration via NETCONF


Search Terms

automating · container · management · ansible · infrastructure · ci/cd · devops · execution · playbook · docker · podman · image · ansible-navigator · environments · managing · prerequisites · collections · containers · environment · networking · storage · automation · collection · images

Interested in this course?

Contact us to book it or get a custom training plan for your team.