Table of Contents
- Introduction and course overview
- Module 1 — Getting started: Ansible, container engines and lab setup
- 1.1 Overview: Ansible Automation for Docker and Podman
- 1.2 Demo: Installation and configuration of container engines with Ansible
- 1.3 Host prerequisites for container stability
- 1.4 Demo: Implementing Host Prerequisites with Ansible
- 1.5 Managing container collections with ansible-galaxy
- 2.1 Containers and images
- 2.2 Managing container images with Ansible
- 2.3 Demo: Managing container images with Ansible
- 2.4 Running and managing containers with Ansible
- 2.5 Persistent Storage and Container Networking
- 2.6 Demo: Managing Persistent Storage and Container Networking
- 3.1 Execution Environments: Concepts and use cases
- 3.2 Building Custom Execution Environments
- 3.3 Demo: Management and troubleshooting of EE dependencies
- 3.4 Executing playbooks in an Execution Environment
- 3.5 Course Conclusion
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:
- Automate container host configuration: Installing and configuring Docker and Podman, preparing the system for containers to run reliably.
- Automate the container lifecycle: manage images and launch containers with defined configuration, storage and networking.
- 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:
| Appearance | Docker | Podman |
|---|---|---|
| Architecture | Daemon (dockerd) in background | Daemonless |
| Privileges | Usually requires root | Natively supports rootless containers |
| Ansible collection | community.docker | containers.podman |
| Main command | docker run | podman 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
| Prerequisites | Docker | Podman |
|---|---|---|
| Dedicated group | docker group | N/A |
| User namespace | N/A | /etc/subuid + /etc/subgid |
| IPv4 forwarding | Required | Required |
br_netfilter | Useful | Useful |
| System service | dockerd enabled | N/A (daemonless) |
| Time synchronization | chrony | chrony |
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.sysctlmodule is part of theansible.posixcollection. Thesysctl_set: trueandreload: trueparameters 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 environmentscontainers.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
- Search: on the site galaxy.ansible.com
- List the collections already installed:
ansible-galaxy collection list - Install a collection:
ansible-galaxy collection install community.docker ansible-galaxy collection install containers.podman - 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.dockerwas 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
| Characteristic | Description |
|---|---|
| Immutability | Once created, an image does not change. To update, we build a new image. |
| Layered structure | Each instruction in the Dockerfile/Containerfile creates a new layer |
| Portability | An image works the same on any compatible system |
| Version | Images 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?
-
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.
-
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.
-
Easy scalability: instead of managing a system manually, you can apply the same configuration on several systems simultaneously.
Main modules for image management
| Engine | Module | Collection |
|---|---|---|
| Docker | docker_image | community.docker |
| Podman | podman_image | containers.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
| Engine | Module | Collection |
|---|---|---|
| Docker | docker_container | community.docker |
| Podman | podman_container | containers.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:
| Politics | Behavior |
|---|---|
no | No automatic restart |
on-failure | Restart only in case of error |
always | Always 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: truemeans that if the container already exists, it will be recreated.pull: missingindicates 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
:Zto 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
| Component | Description |
|---|---|
| Basic image | Foundation: OS, libraries, runtime |
| Ansible Core | The engine that runs playbooks |
| Collections | Necessary modules, roles and plugins |
| Python Dependencies | Python libraries required by certain modules |
| System dependencies | Required 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:
- Identify error in ansible-navigator output
- Read the documentation of the module or plugin to know its dependencies
- Add the missing dependency in the appropriate file:
- System dependency →
bindep.txt - Python dependency →
requirements.txt - Galaxy Collection →
requirements.yml
- Rebuild the EE with ansible-builder
- 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
| Parameter | Description |
|---|---|
enabled: true | Enables the use of an EE |
image | The EE image to use |
pull.policy: missing | Only 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
settingsto get information about ansible-navigator configuration - Type
imagesto 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_imageandpodman_image - Run containers with declarative configuration via
docker_containerandpodman_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)
| Module | Use in the course |
|---|---|
ansible.builtin.dnf | Installing packages (Docker, Podman, container-tools) |
ansible.builtin.service | Activation and startup of services (docker, chronyd) |
ansible.builtin.group | Creation of the docker group |
ansible.builtin.user | Adding the automation user to the docker group |
ansible.builtin.file | Creation of directories (webdemo, mountme) |
ansible.builtin.copy | Copying files (index.html, Dockerfile, Containerfile) |
ansible.builtin.command | Running commands (version checking, grep) |
ansible.builtin.debug | Displaying saved variables |
ansible.builtin.ping | Connectivity test |
Collection ansible.posix
| Module | Use in the course |
|---|---|
ansible.posix.sysctl | Configuring net.ipv4.ip_forward |
Collection community.docker
| Module | Use in the course |
|---|---|
community.docker.docker_image | Pull, build, tag and check in Docker images |
community.docker.docker_container | Creating and managing Docker containers |
community.docker.docker_volume | Creating persistent Docker volumes |
community.docker.docker_network | Creation of Docker networks (bridge, custom) |
Collection containers.podman
| Module | Use in the course |
|---|---|
containers.podman.podman_login | Registry Authentication (registry.redhat.io) |
containers.podman.podman_image | Podman image pull and build |
containers.podman.podman_tag | Podman Image Tagging |
containers.podman.podman_save | Podman Image Archiving |
containers.podman.podman_container | Creating and managing Podman containers |
containers.podman.podman_volume | Creating Persistent Podman Volumes |
containers.podman.podman_network | Creation of Podman networks |
containers.podman.podman_image_info | Collecting image information |
ansible.netcommon collection (used in EE demo)
| Module/Plugin | Use in the course |
|---|---|
ansible.netcommon.netconf | NETCONF Login Plugin |
ansible.netcommon.netconf_get | Retrieving 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