Saltar al contenido principal

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.

Video pendiente de grabación

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

EscenarioHerramienta nativaAnsible aporta
Levantar un stack en localdocker compose up— (úsalo nativo)
Desplegar Docker en 50 hostsscripts bashInventario + idempotencia
Aplicar manifiestos K8s en CIkubectl applyPlantillas Jinja2 + Vault
Bootstrap de un clúster K3sbash + scpRoles reutilizables
Mezclar VMs y K8s en un mismo desplieguevariasUn 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óduloPara qué sirve
community.docker.docker_imageConstruir, descargar o eliminar imágenes
community.docker.docker_containerCrear, arrancar, parar contenedores
community.docker.docker_networkGestionar redes Docker
community.docker.docker_volumeVolúmenes persistentes
community.docker.docker_compose_v2Orquestar stacks con Compose
community.docker.docker_loginAutenticarse 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óduloPara qué sirve
kubernetes.core.k8sAplicar/borrar cualquier recurso (equivale a kubectl apply)
kubernetes.core.k8s_infoConsultar recursos existentes (equivale a kubectl get)
kubernetes.core.helmGestionar releases de Helm
kubernetes.core.helm_repositoryAñadir/quitar repositorios Helm
kubernetes.core.k8s_execEjecutar comandos dentro de pods
kubernetes.core.k8s_scaleEscalar 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/absent en lugar de command: kubectl apply ....
  • Tags por entorno: estructura tu playbook con tags dev, staging, prod y 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