Saltar a contenido

Ansible


6. Un playbook real: despliegue de servidor web

6.1 Objetivo y planteamiento

En esta sección se construirá un playbook completo que instala Apache en los nodos gestionados, clona un sitio web desde un repositorio Git y lo deja sirviendo. El playbook se irá construyendo de forma incremental: cada apartado introduce un concepto nuevo que se integra en el mismo playbook. Al final se dispondrá de algo funcional y representativo de lo que se hace en entornos reales.

El punto de partida es el mínimo que cumple el objetivo: instalar Apache, clonar el repositorio y arrancar el servicio.

deploy_web.yml — versión inicial
---
- name: Despliegue de servidor web Apache
  hosts: all
  become: true

  tasks:
    - name: Instalar git y Apache2
      apt:
        name:
          - apache2
          - git
        state: present
        update_cache: yes

    - name: Clonar o actualizar repositorio web
      git:
        repo: https://github.com/SaurabhDahibhate/HelloWorld
        dest: /var/www/html
        update: yes
        force: true

    - name: Asegurar que Apache está arrancado y habilitado
      service:
        name: apache2
        state: started
        enabled: yes
El módulo git

El módulo git clona el repositorio si el directorio de destino no existe o no es un repositorio Git, y lo actualiza si ya está clonado. Con update: yes (valor por defecto) y force: true, Ansible garantiza que el contenido del servidor siempre coincide con el repositorio, descartando cualquier cambio local. Es necesario que git esté instalado en los nodos gestionados, de ahí que se incluya en la tarea de instalación.

Este playbook ya funciona, pero tiene un problema evidente: las rutas, el nombre del servicio y la URL del repositorio están escritas literalmente en el código. Si mañana se quiere cambiar el repositorio o el directorio de destino, hay que editar el playbook buscando cada ocurrencia. Las variables resuelven esto.

6.2 Variables

Las variables permiten separar los valores concretos de la lógica del playbook. Se definen en la sección vars del play y se referencian en las tareas usando la sintaxis de Jinja2: {{ nombre_variable }}.

deploy_web.yml — con variables
---
- name: Despliegue de servidor web Apache
  hosts: all
  become: true
  vars:
    servicio: apache2
    repo_url: https://github.com/SaurabhDahibhate/HelloWorld
    web_root: /var/www/html

  tasks:
    - name: Instalar git y Apache2
      apt:
        name:
          - "{{ servicio }}"
          - git
        state: present
        update_cache: yes

    - name: Clonar o actualizar repositorio web
      git:
        repo: "{{ repo_url }}"
        dest: "{{ web_root }}"
        update: yes
        force: true

    - name: Asegurar que Apache está arrancado y habilitado
      service:
        name: "{{ servicio }}"
        state: started
        enabled: yes

El playbook hace exactamente lo mismo que antes, pero ahora cualquier cambio se hace en un único lugar: la sección vars. Esto es especialmente valioso cuando el mismo valor aparece en varias tareas.

Las comillas con Jinja2

Cuando el valor de un argumento YAML empieza directamente por {{, hay que entrecomillarlo: dest: "{{ web_root }}". Sin las comillas, YAML interpreta {{ como inicio de un diccionario y produce un error. Si la variable va en medio de una cadena no es necesario: dest: /srv/{{ web_root }}/html.

Otras fuentes de variables

Las variables pueden definirse en muchos otros sitios además de vars: en el inventario (como se vio en el apartado 4.3), en archivos externos, pasadas por línea de comandos con -e, o generadas automáticamente por Ansible como los facts. La precedencia entre todas estas fuentes tiene sus reglas, que se verán más adelante.

6.3 Condicionales: when

A veces interesa que una tarea solo se ejecute si se cumple cierta condición. Para eso existe when, que acepta una expresión Jinja2 que Ansible evalúa antes de ejecutar la tarea.

Un caso habitual es actuar sobre un servicio solo si se sabe que existe en el sistema. El módulo service_facts recopila información sobre todos los servicios del nodo y la almacena en ansible_facts.services. Combinado con when, permite tomar decisiones basadas en el estado real del sistema:

deploy_web.yml — con when
---
- name: Despliegue de servidor web Apache
  hosts: all
  become: true
  vars:
    servicio: apache2
    repo_url: https://github.com/SaurabhDahibhate/HelloWorld
    web_root: /var/www/html

  tasks:
    - name: Instalar git y Apache2
      apt:
        name:
          - "{{ servicio }}"
          - git
        state: present
        update_cache: yes

    - name: Clonar o actualizar repositorio web
      git:
        repo: "{{ repo_url }}"
        dest: "{{ web_root }}"
        update: yes
        force: true

    - name: Recoger información de servicios
      service_facts:

    - name: Asegurar que Apache está arrancado y habilitado
      service:
        name: "{{ servicio }}"
        state: started
        enabled: yes
      when: (servicio ~ '.service') in ansible_facts.services

La condición (servicio ~ '.service') in ansible_facts.services construye el nombre completo del servicio (apache2.service) usando el operador de concatenación de Jinja2 (~), y comprueba si existe en la lista de servicios conocidos. Solo si es así se ejecuta la tarea.

¿Por qué apache2.service y no apache2?

service_facts almacena los servicios con su nombre completo de systemd, incluyendo la extensión .service. Buscar solo apache2 no encontraría ninguna coincidencia.

when acepta múltiples condiciones

when puede combinar varias condiciones mediante and y or:

when: ansible_os_family == "Debian" and ansible_distribution_major_version | int >= 24

También puede escribirse como una lista. En ese caso todas las condiciones deben cumplirse como un and implícito:

1
2
3
when:
  - ansible_os_family == "Debian"
  - ansible_distribution_major_version | int >= 24

6.4 register y debug

register captura el resultado de una tarea en una variable, que luego puede consultarse, mostrarse o usarse en condiciones. debug permite mostrar mensajes o el valor de variables durante la ejecución, lo que resulta muy útil para entender qué está ocurriendo o para depurar.

Se añaden ambos al playbook para mostrar si el repositorio ha cambiado desde la última ejecución:

deploy_web.yml — con register y debug
---
- name: Despliegue de servidor web Apache
  hosts: all
  become: true
  vars:
    servicio: apache2
    repo_url: https://github.com/SaurabhDahibhate/HelloWorld
    web_root: /var/www/html

  tasks:
    - name: Instalar git y Apache2
      apt:
        name:
          - "{{ servicio }}"
          - git
        state: present
        update_cache: yes

    - name: Clonar o actualizar repositorio web
      git:
        repo: "{{ repo_url }}"
        dest: "{{ web_root }}"
        update: yes
        force: true
      register: resultado_git

    - name: Mostrar si el sitio web se ha actualizado
      debug:
        msg: "El repositorio ha cambiado: nueva versión desplegada en {{ web_root }}."
      when: resultado_git.changed

    - name: Recoger información de servicios
      service_facts:

    - name: Asegurar que Apache está arrancado y habilitado
      service:
        name: "{{ servicio }}"
        state: started
        enabled: yes
      when: (servicio ~ '.service') in ansible_facts.services

La variable resultado_git contiene toda la información que el módulo git devuelve sobre lo que ha hecho. La propiedad .changed es común a todos los módulos: vale true si la tarea realizó algún cambio, false si el sistema ya estaba en el estado correcto.

Explorar el contenido de register

Para ver todo lo que contiene una variable registrada, se puede usar debug con var en lugar de msg:

1
2
3
    - name: Ver todo el resultado
      debug:
        var: resultado_git
Esto vuelca el diccionario completo, lo que ayuda a descubrir qué propiedades están disponibles para usar en condiciones posteriores.

6.5 Tags

Los tags permiten etiquetar tareas y ejecutar selectivamente solo las que tengan una etiqueta concreta. Son útiles cuando el playbook tiene muchas tareas y solo se quiere aplicar una parte: por ejemplo, solo el despliegue del código sin reinstalar paquetes.

Además, como se mencionó en el apartado anterior, el módulo git gestiona tanto la clonación inicial como las actualizaciones posteriores. Sin embargo, puede darse la situación en que se necesite partir de cero: borrar el contenido desplegado y volver a clonar desde el repositorio. Para eso se añade una tarea con el tag especial never, que garantiza que no se ejecutará en condiciones normales y solo cuando se pida explícitamente:

deploy_web.yml — con tags
---
- name: Despliegue de servidor web Apache
  hosts: all
  become: true
  vars:
    servicio: apache2
    repo_url: https://github.com/SaurabhDahibhate/HelloWorld
    web_root: /var/www/html

  tasks:
    - name: Instalar git y Apache2
      apt:
        name:
          - "{{ servicio }}"
          - git
        state: present
        update_cache: yes
      tags: instalar

    - name: Eliminar directorio web para reinstalación limpia
      file:
        path: "{{ web_root }}"
        state: absent
      tags:
        - never
        - reset

    - name: Clonar o actualizar repositorio web
      git:
        repo: "{{ repo_url }}"
        dest: "{{ web_root }}"
        update: yes
        force: true
      register: resultado_git
      tags: despliegue

    - name: Mostrar si el sitio web se ha actualizado
      debug:
        msg: "El repositorio ha cambiado: nueva versión desplegada en {{ web_root }}."
      when: resultado_git.changed
      tags: despliegue

    - name: Recoger información de servicios
      service_facts:
      tags: servicio

    - name: Asegurar que Apache está arrancado y habilitado
      service:
        name: "{{ servicio }}"
        state: started
        enabled: yes
      when: (servicio ~ '.service') in ansible_facts.services
      tags: servicio

Para ejecutar solo las tareas con un tag concreto se usa --tags:

Ejemplos
# ansible-playbook deploy_web.yml --tags despliegue
# ansible-playbook deploy_web.yml --tags instalar,servicio

Para ejecutar todo excepto las tareas con un tag determinado, --skip-tags:

Ejemplo
# ansible-playbook deploy_web.yml --skip-tags instalar

Y para forzar una reinstalación limpia, combinando el tag reset con despliegue:

Reinstalación limpia
# ansible-playbook deploy_web.yml --tags reset,despliegue
Tags especiales

Ansible reserva dos tags con comportamiento especial:

  • always: la tarea se ejecuta siempre, aunque se use --tags con otro valor.
  • never: la tarea nunca se ejecuta salvo que se pida explícitamente con --tags never o con el nombre de otro tag que también tenga asignado, como reset en el ejemplo anterior.

6.6 Handlers

Un handler es una tarea especial que solo se ejecuta si ha sido notificada por otra tarea, y únicamente cuando esta ha producido un cambio. Se definen en la sección handlers del play, al mismo nivel que tasks.

El caso de uso más habitual es reiniciar o recargar un servicio cuando cambia su configuración o su contenido. Con un handler, la recarga solo ocurre cuando git detecta commits nuevos:

deploy_web.yml — con handlers
---
- name: Despliegue de servidor web Apache
  hosts: all
  become: true
  vars:
    servicio: apache2
    repo_url: https://github.com/SaurabhDahibhate/HelloWorld
    web_root: /var/www/html

  tasks:
    - name: Instalar git y Apache2
      apt:
        name:
          - "{{ servicio }}"
          - git
        state: present
        update_cache: yes
      tags: instalar

    - name: Eliminar directorio web para reinstalación limpia
      file:
        path: "{{ web_root }}"
        state: absent
      tags:
        - never
        - reset

    - name: Clonar o actualizar repositorio web
      git:
        repo: "{{ repo_url }}"
        dest: "{{ web_root }}"
        update: yes
        force: true
      register: resultado_git
      notify: Recargar Apache
      tags: despliegue

    - name: Mostrar si el sitio web se ha actualizado
      debug:
        msg: "El repositorio ha cambiado: nueva versión desplegada en {{ web_root }}."
      when: resultado_git.changed
      tags: despliegue

    - name: Recoger información de servicios
      service_facts:
      tags: servicio

    - name: Asegurar que Apache está arrancado y habilitado
      service:
        name: "{{ servicio }}"
        state: started
        enabled: yes
      when: (servicio ~ '.service') in ansible_facts.services
      tags: servicio

  handlers:
    - name: Recargar Apache
      service:
        name: "{{ servicio }}"
        state: reloaded

    - name: Confirmar recarga de Apache
      debug:
        msg: "Apache ha sido recargado porque el repositorio ha cambiado."
      listen: "Recargar Apache"

La palabra clave notify en la tarea git le dice a Ansible que, si esa tarea produce un cambio, dispare el evento Recargar Apache. Todos los handlers que tengan ese nombre o lo escuchen mediante listen se ejecutarán en orden al final del play.

La palabra clave listen permite a un handler suscribirse a un evento sin necesidad de llamarse igual que el notify que lo dispara. En este caso el segundo handler usa listen: "Recargar Apache" para ejecutarse junto al primero y confirmar mediante debug que la recarga ha tenido lugar.

Comportamiento de los handlers

Tres propiedades importantes a tener en cuenta:

  • Si una tarea notifica un handler pero no produce ningún cambio (changed=false), el handler no se ejecuta.
  • Si varias tareas notifican el mismo handler, este se ejecuta una sola vez al final, no una vez por cada notificación.
  • listen permite agrupar varios handlers bajo un mismo evento, lo que en proyectos grandes ayuda a desacoplar quién notifica de quién reacciona.

6.7 El playbook completo

El resultado final, reproducido aquí como referencia:

deploy_web.yml
---
- name: Despliegue de servidor web Apache
  hosts: all
  become: true
  vars:
    servicio: apache2
    repo_url: https://github.com/SaurabhDahibhate/HelloWorld
    web_root: /var/www/html

  tasks:
    - name: Instalar git y Apache2
      apt:
        name:
          - "{{ servicio }}"
          - git
        state: present
        update_cache: yes
      tags: instalar

    - name: Eliminar directorio web para reinstalación limpia
      file:
        path: "{{ web_root }}"
        state: absent
      tags:
        - never
        - reset

    - name: Clonar o actualizar repositorio web
      git:
        repo: "{{ repo_url }}"
        dest: "{{ web_root }}"
        update: yes
        force: true
      register: resultado_git
      notify: Recargar Apache
      tags: despliegue

    - name: Mostrar si el sitio web se ha actualizado
      debug:
        msg: "El repositorio ha cambiado: nueva versión desplegada en {{ web_root }}."
      when: resultado_git.changed
      tags: despliegue

    - name: Recoger información de servicios
      service_facts:
      tags: servicio

    - name: Asegurar que Apache está arrancado y habilitado
      service:
        name: "{{ servicio }}"
        state: started
        enabled: yes
      when: (servicio ~ '.service') in ansible_facts.services
      tags: servicio

  handlers:
    - name: Recargar Apache
      service:
        name: "{{ servicio }}"
        state: reloaded

    - name: Confirmar recarga de Apache
      debug:
        msg: "Apache ha sido recargado porque el repositorio ha cambiado."
      listen: "Recargar Apache"

Para ejecutarlo:

Ejecución completa
# ansible-playbook deploy_web.yml
Solo instalar paquetes
# ansible-playbook deploy_web.yml --tags instalar
Solo desplegar o actualizar el sitio
# ansible-playbook deploy_web.yml --tags despliegue
Reinstalación limpia desde cero
# ansible-playbook deploy_web.yml --tags reset,despliegue
Simulación sin aplicar cambios
# ansible-playbook deploy_web.yml --check

📅 Documento escrito el 01/06/2026 · Última revisión: 02/07/2026 · Versión: v1.0