Ansible y contenedores 🐳
Hasta ahora hemos usado Ansible para gestionar servidores tradicionales. En este capítulo damos el salto al mundo de los contenedores: aprenderás a orquestar Docker y Kubernetes desde Ansible, sustituyendo scripts bash frágiles por playbooks idempotentes y reutilizables.
Suscríbete al canal de YouTube para recibir la notificación.
🎯 ¿Por qué usar Ansible con contenedores?
La analogía: el contenedor y el estibador
Si Docker es la caja de mercancías estandarizada que viaja por todo el mundo, Ansible es el estibador que decide qué cajas se cargan en qué barco, en qué orden, y con qué etiquetas. Docker resuelve el "cómo se empaqueta una aplicación"; Ansible responde al "cómo desplegamos cientos de esas cajas en producción de forma repetible".
Cuándo Ansible aporta valor frente a Compose o kubectl
| Escenario | Herramienta nativa | Ansible aporta |
|---|---|---|
| Levantar un stack en local | docker compose up | — (úsalo nativo) |
| Desplegar Docker en 50 hosts | scripts bash | Inventario + idempotencia |
| Aplicar manifiestos K8s en CI | kubectl apply | Plantillas Jinja2 + Vault |
| Bootstrap de un clúster K3s | bash + scp | Roles reutilizables |
| Mezclar VMs y K8s en un mismo despliegue | varias | Un solo playbook |
Regla de oro: Ansible brilla cuando hay que coordinar la capa de infraestructura (instalar Docker, configurar el daemon, gestionar redes, certificados) junto con la capa de aplicación contenerizada. Para flujos puramente declarativos dentro del clúster, Helm o Kustomize son mejores.
🐳 Parte 1: Ansible + Docker
Instalación de la colección community.docker
Las funcionalidades de Docker en Ansible viven en una colección externa que hay que instalar primero:
ansible-galaxy collection install community.docker
pip install docker # SDK de Python requerido en el host destino
Módulos clave
| Módulo | Para qué sirve |
|---|---|
community.docker.docker_image | Construir, descargar o eliminar imágenes |
community.docker.docker_container | Crear, arrancar, parar contenedores |
community.docker.docker_network | Gestionar redes Docker |
community.docker.docker_volume | Volúmenes persistentes |
community.docker.docker_compose_v2 | Orquestar stacks con Compose |
community.docker.docker_login | Autenticarse en registries |
Ejemplo: instalar Docker y ejecutar hello-world en los servidores
- name: Instalación de docker
hosts: all
become: true
vars:
docker_arch_map:
x86_64: amd64
aarch64: arm64
armv7l: armhf
tasks:
- name: Actualizar índice de paquetes
apt:
update_cache: yes
cache_valid_time: 3600
- name: Instalar dependencias para Docker
apt:
name:
- ca-certificates
- curl
- python3-debian
state: present
- name: Añadir repositorio de Docker
deb822_repository:
name: docker
types: deb
uris: "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}"
suites: "{{ ansible_facts['distribution_release'] }}"
components: stable
architectures: "{{ docker_arch_map[ansible_facts['architecture']] | default(ansible_facts['architecture']) }}"
signed_by: "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg"
state: present
- name: Instalar Docker CE
apt:
name: docker-ce
state: present
- name: Arrancar servicio de Docker
service:
name: docker
state: started
enabled: yes
- name: Añadir usuario ansible al grupo docker
user:
name: ansible
groups: docker
append: yes # Añade el grupo sin borrar los existentes
- name: Instalar paquete Pip
apt:
name: pip
state: present
- name: Instalar librerías de Python para Docker
apt:
name: python3-docker
state: present
- name: Comprobar la instalación de Docker
hosts: all
become: true
tasks:
- name: Ejecutar hello-world
command: docker run --rm hello-world
register: hello_world_result
changed_when: false
- name: Mostrar salida de hello-world
debug:
var: hello_world_result.stdout_lines
Orquestar un stack Compose desde Ansible
Un caso de uso clásico: Ansible prepara el servidor (instala Docker, configura credenciales) y luego delega el ciclo de vida de los contenedores a Compose. Aquí usamos pabpereza/quotes, una API FastAPI + PostgreSQL, como ejemplo real.
- name: Desplegar quotes con Docker Compose
hosts: target1
become: true
vars:
app_dir: /opt/quotes
repo_url: https://github.com/pabpereza/quotes.git
tasks:
- name: Clonar repositorio quotes
git:
repo: "{{ repo_url }}"
dest: "{{ app_dir }}"
version: main
force: yes
- name: Levantar stack quotes con Compose
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: present
pull: always
Este patrón es ideal para bootstrapping: Ansible prepara el host (firewall, certificados, variables de entorno renderizadas con Jinja2) y luego delega el ciclo de vida de los contenedores a Compose.
Construir y publicar la imagen en un pipeline
Cuando quieres gestionar el build desde Ansible (por ejemplo, en un pipeline CI/CD propio), el módulo docker_image permite construir y hacer push a un registry. Aquí publicamos la imagen de quotes en GitHub Container Registry.
- name: Construir y publicar imagen de quotes
hosts: target1
become: true
vars:
app_dir: /opt/quotes
image_name: pabpereza/quotes
docker_user: pabpereza
tasks:
- name: Autenticarse en GHCR
community.docker.docker_login:
registry: docker.io
username: "{{ docker_user }}"
password: "{{ docker_token }}"
- name: Construir y publicar imagen de quotes
community.docker.docker_image:
name: "{{ image_name }}"
source: build
build:
path: "{{ app_dir }}"
pull: true
push: true
⚓ Parte 2: Ansible + Kubernetes
Instalación de la colección kubernetes.core
ansible-galaxy collection install kubernetes.core
pip install kubernetes openshift pyyaml jsonpatch
Necesitas un kubeconfig válido en el host donde corre Ansible (normalmente el control node).
Módulos clave
| Módulo | Para qué sirve |
|---|---|
kubernetes.core.k8s | Aplicar/borrar cualquier recurso (equivale a kubectl apply) |
kubernetes.core.k8s_info | Consultar recursos existentes (equivale a kubectl get) |
kubernetes.core.helm | Gestionar releases de Helm |
kubernetes.core.helm_repository | Añadir/quitar repositorios Helm |
kubernetes.core.k8s_exec | Ejecutar comandos dentro de pods |
kubernetes.core.k8s_scale | Escalar deployments y statefulsets |
Ejemplo: desplegar una aplicación en Kubernetes
---
- name: Desplegar NotaStack en K8s
hosts: localhost
gather_facts: false
vars:
app_namespace: notastack
app_image: registry.local/notastack-api:1.4.2
tasks:
- name: Crear namespace
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ app_namespace }}"
- name: Aplicar manifiestos renderizados con Jinja2
kubernetes.core.k8s:
state: present
namespace: "{{ app_namespace }}"
template: "manifests/{{ item }}.yaml.j2"
loop:
- configmap
- deployment
- service
- ingress
- name: Esperar a que el deployment esté disponible
kubernetes.core.k8s_info:
kind: Deployment
namespace: "{{ app_namespace }}"
name: notastack-api
register: dep
until: >
dep.resources[0].status.availableReplicas | default(0) ==
dep.resources[0].spec.replicas
retries: 30
delay: 10
Plantilla Jinja2 de un Deployment
# manifests/deployment.yaml.j2
apiVersion: apps/v1
kind: Deployment
metadata:
name: notastack-api
spec:
replicas: {{ replicas | default(2) }}
selector:
matchLabels:
app: notastack-api
template:
metadata:
labels:
app: notastack-api
spec:
containers:
- name: api
image: {{ app_image }}
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: "{{ db_host }}"
Este patrón —manifiestos K8s renderizados con Jinja2 desde Ansible— es muy potente: te da variables, condicionales y bucles que Helm sólo ofrece de forma limitada, y se integra con tu inventario de Ansible para distinguir entornos (dev/staging/prod).
Instalar charts con Helm desde Ansible
- name: Añadir repo de Bitnami
kubernetes.core.helm_repository:
name: bitnami
repo_url: https://charts.bitnami.com/bitnami
- name: Instalar PostgreSQL
kubernetes.core.helm:
name: db
chart_ref: bitnami/postgresql
release_namespace: notastack
create_namespace: true
values:
auth:
postgresPassword: "{{ pg_password }}"
primary:
persistence:
size: 20Gi
✅ Buenas prácticas
- Usa Vault para tokens de registry, kubeconfigs y credenciales (lo vimos en el capítulo 7).
- No reinventes Helm: si ya existe un chart oficial, instálalo desde Ansible en lugar de traducirlo a manifiestos sueltos.
- Separa bootstrap (infra) de despliegue (app): Ansible es excelente para el primero; para el segundo, considera usar GitOps (ArgoCD/Flux) y limitar Ansible al pipeline.
- Idempotencia: usa siempre
state: present/absenten lugar decommand: kubectl apply .... - Tags por entorno: estructura tu playbook con tags
dev,staging,prody combínalo con inventarios separados.
🧪 Práctica: WordPress con Docker Compose
Aplicamos los módulos de community.docker desplegando un stack WordPress completo (WordPress + MySQL) sobre Docker, con el docker-compose.yml renderizado mediante Jinja2 y orquestado desde Ansible.
Estructura del proyecto
wordpress-compose/
├── inventory.ini
├── wordpress.yml
└── templates/
└── docker-compose.yml.j2
Template templates/docker-compose.yml.j2
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: "{{ mysql_root_password }}"
MYSQL_DATABASE: wordpress
MYSQL_USER: wp_user
MYSQL_PASSWORD: "{{ mysql_password }}"
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
retries: 5
wordpress:
image: wordpress:6.7-apache
depends_on:
db:
condition: service_healthy
ports:
- "{{ wp_port }}:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: "{{ mysql_password }}"
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
restart: unless-stopped
volumes:
db_data:
wp_data:
Fíjate cómo las variables de Ansible ({{ mysql_password }}, {{ wp_port }}) se inyectan directamente en el Compose. El template se renderiza en el host destino antes de pasárselo a Docker, sin necesidad de archivos .env separados.
Playbook wordpress.yml
---
- name: Desplegar WordPress con Docker Compose
hosts: target1
become: true
vars:
mysql_root_password: "rootpass123"
mysql_password: "wppass123"
wp_port: 80
compose_dir: /opt/wordpress
tasks:
- name: Crear directorio del proyecto
file:
path: "{{ compose_dir }}"
state: directory
mode: '0755'
- name: Renderizar docker-compose.yml con variables
template:
src: ./compose.yml.j2
dest: "{{ compose_dir }}/docker-compose.yml"
mode: '0644'
- name: Levantar stack WordPress
community.docker.docker_compose_v2:
project_src: "{{ compose_dir }}"
state: present
pull: missing
- name: Esperar a que WordPress responda
uri:
url: "http://localhost:{{ wp_port }}"
status_code: [200, 302]
register: wp_health
until: wp_health.status in [200, 302]
retries: 30
delay: 5
- name: Mostrar mensaje de éxito
debug:
msg: "WordPress desplegado correctamente en http://localhost:{{ wp_port }}"
Ejecutar y verificar idempotencia
# Primera ejecución: instala Docker y despliega el stack
ansible-playbook wordpress.yml
# Segunda ejecución: debe reportar changed=0
ansible-playbook wordpress.yml
# Simular cambios antes de aplicar
ansible-playbook wordpress.yml --check --diff
WordPress estará disponible en http://localhost:8080.
Limpieza
# Parar el stack y eliminar volúmenes
ansible target1 -b -m community.docker.docker_compose_v2 \
-a "project_src=/opt/wordpress state=absent remove_volumes=true"
# Eliminar el directorio del proyecto
ansible target1 -b -m file -a "path=/opt/wordpress state=absent"
📚 Recursos
- Documentación oficial
community.docker - Documentación oficial
kubernetes.core - Curso de Docker y Kubernetes en pabpereza.dev