--- title: "Ejemplos prácticos de playbooks de Ansible en Ubuntu 24.04" description: "Crea un proyecto de Ansible para Ubuntu 24.04: una base de seguridad, un playbook de Nginx con plantillas y handlers, y un despliegue escalonado sin cortes." category: "Herramientas de Automatización" language: es source: https://cubepath.com/es/docs/herramientas-de-automatización/ansible-playbooks-ejemplos-practicos updated: 2026-09-25 --- # Ejemplos prácticos de playbooks de Ansible en Ubuntu 24.04 Los playbooks cortos que instalan un paquete son un buen comienzo, pero la infraestructura real necesita una estructura que pueda crecer: variables compartidas, plantillas, handlers que reinician servicios solo cuando hace falta y despliegues que nunca tiran todos los servidores a la vez. En este tutorial crearás un proyecto de Ansible pequeño pero completo para servidores Ubuntu 24.04 con tres playbooks: una base de seguridad, la configuración de un servidor web Nginx y un despliegue escalonado del contenido del sitio, todo unido por un único `site.yml`. ## Requisitos previos Para seguir esta guía necesitas: - Un nodo de control con Ansible instalado y acceso SSH con clave a tus servidores, como se explica en la guía Cómo instalar y usar Ansible en Ubuntu 24.04. - Dos servidores gestionados con Ubuntu 24.04, por ejemplo VPS de CubePath, llamados `web1` (`web1_server_ip`) y `web2` (`web2_server_ip`). - Un usuario no root con privilegios `sudo` en ambos servidores (`your_user`) que ya pueda iniciar sesión con clave SSH. - Un nombre de dominio, `your_domain`, si quieres acceder al sitio por nombre. Para las pruebas basta con la dirección IP. ## Paso 1: Crear la estructura del proyecto Una estructura predecible hace que el proyecto sea fácil de recorrer y permite que Ansible encuentre variables y plantillas automáticamente. Ansible carga `group_vars/.yml` para cada host de ese grupo, y busca los orígenes de `template` y `copy` en `templates/` y `files/`, junto al playbook. Crea los directorios: ```bash mkdir -p ~/ansible-web/{group_vars,templates,files/site} cd ~/ansible-web ``` El proyecto terminado tendrá este aspecto: | Ruta | Función | |---|---| | `ansible.cfg` | Valores por defecto del proyecto | | `inventory.ini` | Servidores y grupos | | `group_vars/all.yml` | Variables para todos los servidores | | `group_vars/webservers.yml` | Variables para los servidores web | | `templates/nginx-site.conf.j2` | Plantilla del virtual host de Nginx | | `files/site/` | Contenido estático del sitio que se despliega | | `baseline.yml`, `webserver.yml`, `deploy.yml` | Los tres playbooks | | `site.yml` | Punto de entrada que los ejecuta todos | Crea el inventario: ```bash nano inventory.ini ``` ```ini [webservers] web1 ansible_host=web1_server_ip web2 ansible_host=web2_server_ip [all:vars] ansible_user=your_user ``` Crea el archivo de configuración: ```bash nano ansible.cfg ``` ```ini [defaults] inventory = inventory.ini interpreter_python = auto_silent [ssh_connection] pipelining = True ``` Comprueba que los dos servidores responden: ```bash ansible all -m ping ``` Ambos hosts deben devolver `"ping": "pong"`. ## Paso 2: Definir variables compartidas Las variables sacan de los playbooks los valores que cambian entre proyectos. Crea las variables que se aplican a todos los servidores: ```bash nano group_vars/all.yml ``` ```yaml --- server_timezone: Europe/Madrid base_packages: - curl - htop - vim - unattended-upgrades ``` Después, las variables de los servidores web: ```bash nano group_vars/webservers.yml ``` ```yaml --- server_name: your_domain web_root: /var/www/your_domain ``` Confirma que Ansible combina ambos archivos para `web1`: ```bash ansible-inventory --host web1 ``` ```output { "ansible_host": "web1_server_ip", "ansible_user": "your_user", "base_packages": [ "curl", "htop", "vim", "unattended-upgrades" ], "server_name": "your_domain", "server_timezone": "Europe/Madrid", "web_root": "/var/www/your_domain" } ``` ## Paso 3: Escribir un playbook de base de seguridad El playbook base aplica los ajustes que debe tener cualquier servidor: paquetes comunes, zona horaria, bastionado de SSH y cortafuegos. Introduce dos funciones importantes: `validate`, que prueba un archivo de configuración antes de ponerlo en su sitio, y los handlers, que reinician un servicio solo cuando su configuración ha cambiado de verdad. ```bash nano baseline.yml ``` ```yaml --- - name: Aplicar la configuración base hosts: all become: true tasks: - name: Instalar los paquetes base ansible.builtin.apt: name: "{{ base_packages }}" state: present update_cache: true cache_valid_time: 3600 - name: Configurar la zona horaria community.general.timezone: name: "{{ server_timezone }}" - name: Bastionar el demonio SSH ansible.builtin.copy: dest: /etc/ssh/sshd_config.d/10-hardening.conf content: | PasswordAuthentication no KbdInteractiveAuthentication no PermitRootLogin prohibit-password owner: root group: root mode: "0644" validate: /usr/sbin/sshd -t -f %s notify: Reiniciar SSH - name: Permitir SSH en UFW community.general.ufw: rule: allow name: OpenSSH - name: Activar UFW denegando el tráfico entrante por defecto community.general.ufw: state: enabled policy: deny direction: incoming handlers: - name: Reiniciar SSH ansible.builtin.systemd_service: name: ssh state: restarted ``` Algunos detalles que conviene conocer: - Pasar una lista a `name` en el módulo `apt` instala todos los paquetes en una sola transacción, mucho más rápido que un bucle. - Los ajustes de SSH van en un archivo en `/etc/ssh/sshd_config.d/`. sshd usa el primer valor que lee para cada opción, y los archivos se leen en orden alfabético, así que `10-hardening.conf` se impone al `50-cloud-init.conf` que algunas imágenes traen con `PasswordAuthentication yes`. - `validate` ejecuta `sshd -t` contra el archivo nuevo antes de sustituir el real. Si la sintaxis es incorrecta, la tarea falla y la configuración anterior se mantiene. > Advertencia: Este playbook desactiva el inicio de sesión SSH con contraseña. Asegúrate de que tu clave SSH funciona para todos los usuarios que necesiten acceso antes de ejecutarlo. Ejecútalo. `-K` pide tu contraseña de sudo: ```bash ansible-playbook baseline.yml -K ``` En la primera ejecución, el handler se ejecuta después de todas las tareas, una vez por host: ```output RUNNING HANDLER [Reiniciar SSH] ************************************************ changed: [web1] changed: [web2] PLAY RECAP ********************************************************************* web1 : ok=7 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 web2 : ok=7 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` Comprueba que el acceso con contraseña está desactivado en un servidor: ```bash ssh -t your_user@web1_server_ip "sudo sshd -T | grep -i passwordauthentication" ``` ```output passwordauthentication no ``` Ejecuta el playbook una segunda vez. Todas las tareas informan de `ok` y el handler no se ejecuta, porque no ha cambiado nada. ## Paso 4: Configurar Nginx con una plantilla y handlers Las plantillas permiten que un mismo archivo sirva para todos los servidores: Ansible sustituye los marcadores Jinja2 con las variables de cada host. Crea la plantilla del virtual host: ```bash nano templates/nginx-site.conf.j2 ``` ```nginx # Gestionado por Ansible. Los cambios locales se sobrescribirán. server { listen 80; listen [::]:80; server_name {{ server_name }}; root {{ web_root }}; index index.html; access_log /var/log/nginx/{{ server_name }}.access.log; error_log /var/log/nginx/{{ server_name }}.error.log; location / { try_files $uri $uri/ =404; } } ``` Ahora crea el playbook del servidor web: ```bash nano webserver.yml ``` ```yaml --- - name: Configurar los servidores web Nginx hosts: webservers become: true tasks: - name: Instalar Nginx ansible.builtin.apt: name: nginx state: present update_cache: true cache_valid_time: 3600 - name: Crear la raíz web ansible.builtin.file: path: "{{ web_root }}" state: directory owner: www-data group: www-data mode: "0755" - name: Desplegar el virtual host ansible.builtin.template: src: nginx-site.conf.j2 dest: "/etc/nginx/sites-available/{{ server_name }}.conf" owner: root group: root mode: "0644" notify: Recargar Nginx - name: Activar el virtual host ansible.builtin.file: src: "/etc/nginx/sites-available/{{ server_name }}.conf" dest: "/etc/nginx/sites-enabled/{{ server_name }}.conf" state: link notify: Recargar Nginx - name: Desactivar el sitio por defecto ansible.builtin.file: path: /etc/nginx/sites-enabled/default state: absent notify: Recargar Nginx - name: Permitir HTTP y HTTPS en UFW community.general.ufw: rule: allow port: "{{ item }}" proto: tcp loop: - "80" - "443" - name: Arrancar Nginx y activarlo en el arranque ansible.builtin.systemd_service: name: nginx state: started enabled: true handlers: - name: Probar la configuración de Nginx ansible.builtin.command: nginx -t changed_when: false listen: Recargar Nginx - name: Recargar el servicio Nginx ansible.builtin.systemd_service: name: nginx state: reloaded listen: Recargar Nginx ``` Tres tareas notifican `Recargar Nginx`, pero los handlers se ejecutan una sola vez por host al final del play. Los dos handlers escuchan el mismo tema y se ejecutan en el orden en que están definidos, así que `nginx -t` comprueba primero la configuración completa. Si la prueba falla, el play falla en ese host y la recarga no llega a ocurrir, de modo que una plantilla rota no puede tirar el sitio en marcha. Ejecuta el playbook: ```bash ansible-playbook webserver.yml -K ``` ```output RUNNING HANDLER [Probar la configuración de Nginx] ***************************** ok: [web1] ok: [web2] RUNNING HANDLER [Recargar el servicio Nginx] *********************************** changed: [web1] changed: [web2] ``` Revisa el archivo generado en uno de los servidores: ```bash ansible web1 -b -K -m ansible.builtin.command -a "cat /etc/nginx/sites-enabled/your_domain.conf" ``` La salida muestra la plantilla con `server_name` y `root` ya rellenados. ## Paso 5: Publicar el contenido del sitio sin cortes Si despliegas en todos los servidores a la vez, una versión defectuosa rompe todos los servidores en el mismo momento. La palabra clave `serial` hace que Ansible ejecute el play completo en un lote de hosts antes de pasar al siguiente. Combinada con una comprobación de salud y `max_fail_percentage: 0`, el despliegue se detiene en el primer servidor que falla y el resto sigue sirviendo la versión anterior. Crea una página sencilla para desplegar: ```bash nano files/site/index.html ``` ```html your_domain

Versión 1

``` Crea el playbook de despliegue: ```bash nano deploy.yml ``` ```yaml --- - name: Desplegar el contenido del sitio servidor a servidor hosts: webservers become: true serial: 1 max_fail_percentage: 0 tasks: - name: Copiar los archivos del sitio ansible.builtin.copy: src: site/ dest: "{{ web_root }}/" owner: www-data group: www-data mode: u=rwX,g=rX,o=rX - name: Comprobar que el sitio responde con HTTP 200 ansible.builtin.uri: url: http://127.0.0.1/ headers: Host: "{{ server_name }}" status_code: 200 register: health until: health.status == 200 retries: 5 delay: 3 ``` - `src: site/` se resuelve dentro de `files/`, y la barra final copia el contenido de la carpeta, no la carpeta en sí. - `mode: u=rwX,g=rX,o=rX` da `0644` a los archivos y `0755` a los directorios. - La tarea `uri` pide el sitio desde el propio servidor con la cabecera `Host` correcta y lo reintenta hasta cinco veces antes de fallar. Si los servidores están detrás de un balanceador de carga, este es también el sitio donde sacarías cada servidor del pool antes de copiar y lo volverías a añadir después de la comprobación de salud. Ejecuta el despliegue: ```bash ansible-playbook deploy.yml -K ``` La salida muestra dos plays separados, uno por servidor: ```output PLAY [Desplegar el contenido del sitio servidor a servidor] ******************** TASK [Copiar los archivos del sitio] ******************************************* changed: [web1] TASK [Comprobar que el sitio responde con HTTP 200] **************************** ok: [web1] PLAY [Desplegar el contenido del sitio servidor a servidor] ******************** TASK [Copiar los archivos del sitio] ******************************************* changed: [web2] ... ``` Compruébalo desde el nodo de control: ```bash curl -H "Host: your_domain" http://web2_server_ip/ ``` ```output ...

Versión 1

... ``` Para publicar una versión nueva, edita `files/site/index.html` y vuelve a ejecutar `deploy.yml`. ## Paso 6: Unirlo todo con site.yml Un único punto de entrada facilita montar un servidor nuevo desde cero o volver a aplicarlo todo. Crea `site.yml`: ```bash nano site.yml ``` ```yaml --- - name: Base ansible.builtin.import_playbook: baseline.yml tags: baseline - name: Servidores web ansible.builtin.import_playbook: webserver.yml tags: web - name: Despliegue ansible.builtin.import_playbook: deploy.yml tags: deploy ``` Las etiquetas de `import_playbook` se aplican a todas las tareas de ese playbook, así que puedes ejecutar solo una parte del proyecto. Algunas invocaciones habituales: ```bash ansible-playbook site.yml -K ``` ```bash ansible-playbook site.yml -K --tags web ``` ```bash ansible-playbook site.yml -K --limit web2 ``` La primera lo ejecuta todo, la segunda solo la configuración de Nginx y la tercera lo aplica todo únicamente a `web2`, por ejemplo después de añadir un servidor nuevo al inventario. Antes de tocar producción, previsualiza el efecto con el modo check y los diffs: ```bash ansible-playbook site.yml -K --check --diff ``` `--diff` muestra las líneas exactas que cambiarían en los archivos generados con plantillas o copiados. ## Solución de problemas **`Could not find or access 'nginx-site.conf.j2'`**: Ansible busca las plantillas en un directorio `templates/` junto al playbook. Ejecuta el comando desde la raíz del proyecto y mantén los playbooks y `templates/` en la misma carpeta. **El handler de SSH se ejecutó pero el acceso con contraseña sigue funcionando**: otro archivo de `/etc/ssh/sshd_config.d/` va antes que `10-hardening.conf` en orden alfabético, o `/etc/ssh/sshd_config` define la opción antes de su línea `Include`. Revisa el valor efectivo con `sudo sshd -T | grep -i passwordauthentication`. **Falla `Probar la configuración de Nginx`**: ejecuta `sudo nginx -t` en el servidor para ver la línea exacta, corrige la plantilla y vuelve a ejecutar el playbook. La configuración anterior sigue activa porque la recarga no se hizo. **La comprobación de salud falla con `Status code was 404`**: la cabecera `Host` no coincide con `server_name` y Nginx sirve otro sitio, o `web_root` no tiene `index.html`. Revisa `group_vars/webservers.yml` y el contenido de `files/site/`. ## Conclusión Has creado un proyecto de Ansible con variables compartidas, un playbook base que valida sus cambios, un playbook de Nginx basado en una plantilla y handlers, y un despliegue escalonado que se detiene en la primera comprobación de salud fallida. Cada playbook es idempotente y se puede ejecutar por separado o mediante `site.yml`. Como siguientes pasos, convierte cada playbook en un rol con `ansible-galaxy role init`, guarda secretos como tokens de API con `ansible-vault` y añade HTTPS con Certbot cuando tu dominio apunte a los servidores.