GuíasGuides · Nivel 3Level 3

Ansible: el primer playbook para parchear servidores Linux uno a unoAnsible: your first playbook to patch Linux servers one at a time

Inventario, conexión por SSH, un playbook que actualiza, reinicia solo si hace falta y comprueba el servicio, ejecutado servidor a servidor con `serial`.Inventory, SSH connection, a playbook that updates, reboots only when needed and checks the service, run one server at a time with `serial`.

Qué vas a conseguir

Sustituir el parcheo manual de la guía de RHEL y Ubuntu por un playbook que hace lo mismo en todos los servidores, en orden, parando si uno falla. Al terminar tendrás un repositorio con el inventario, el playbook y la salida de la primera ejecución para el ticket de cambio.

Antes de empezar

  • Una máquina de control con Python 3 y acceso SSH a los servidores (tu portátil Linux/macOS, WSL o una VM pequeña).
  • Una clave SSH desplegada en los servidores para un usuario con sudo sin contraseña (o con contraseña: Ansible la pide con -K).
  • Los servidores en la guía de Git del Nivel 2: el playbook es código y va bajo control de versiones desde el primer día.

1. Instalar y comprobar la conexión

bash · máquina de control
python3 -m pip install --user ansibleansible --version | head -1ansible [core 2.17.4]mkdir -p ~/ops/ansible && cd ~/ops/ansiblecat > inventory.ini <<'EOF'[web]srv-web-01.contoso.localsrv-web-02.contoso.local [db]srv-db-01.contoso.local [linux:children]webdb [linux:vars]ansible_user=ansibleansible_become=trueEOFansible linux -i inventory.ini -m pingsrv-web-01.contoso.local | SUCCESS => {"changed": false, "ping": "pong"}srv-web-02.contoso.local | SUCCESS => {"changed": false, "ping": "pong"}srv-db-01.contoso.local | SUCCESS => {"changed": false, "ping": "pong"}

ping aquí no es ICMP: es "me conecto por SSH y encuentro Python". Si uno falla, prueba ssh ansible@srv-db-01.contoso.local a mano antes de buscar en Ansible.

2. Comandos sueltos antes del playbook

bash · máquina de control
ansible linux -i inventory.ini -m shell -a "uptime; df -h / | tail -1"srv-web-01.contoso.local | CHANGED | rc=0 >> 08:12:03 up 41 days,  2:17,  0 users,  load average: 0.08, 0.05, 0.01/dev/mapper/rhel-root   50G   31G   20G  62% /...ansible web -i inventory.ini -m service -a "name=httpd state=started" --checksrv-web-01.contoso.local | SUCCESS => {"changed": false, "name": "httpd", "state": "started"}

--check es el modo ensayo: dice qué cambiaría sin tocar nada. Úsalo siempre la primera vez.

3. El playbook de parcheo

patch.yml
---- name: Parcheo mensual de servidores Linux  hosts: linux  serial: 1  max_fail_percentage: 0  tasks:    - name: Actualizar paquetes (RHEL)      ansible.builtin.dnf:        name: "*"        state: latest      when: ansible_os_family == "RedHat"      register: dnf_result     - name: Actualizar paquetes (Ubuntu)      ansible.builtin.apt:        upgrade: dist        update_cache: true      when: ansible_os_family == "Debian"      register: apt_result     - name: ¿Hace falta reiniciar? (RHEL)      ansible.builtin.command: needs-restarting -r      register: nr      failed_when: nr.rc not in [0, 1]      changed_when: false      when: ansible_os_family == "RedHat"     - name: ¿Hace falta reiniciar? (Ubuntu)      ansible.builtin.stat:        path: /var/run/reboot-required      register: rr      when: ansible_os_family == "Debian"     - name: Reiniciar solo si hace falta      ansible.builtin.reboot:        reboot_timeout: 600      when: (nr.rc is defined and nr.rc == 1) or (rr.stat is defined and rr.stat.exists)     - name: Comprobar que el servicio principal está activo      ansible.builtin.service_facts:     - name: Fallar si httpd no está corriendo en los web      ansible.builtin.assert:        that: ansible_facts.services['httpd.service'].state == 'running'        fail_msg: "httpd no arrancó tras el parcheo"      when: "'web' in group_names"

Lo importante está en tres líneas: serial: 1 (un servidor a la vez), max_fail_percentage: 0 (si uno falla, se para todo) y el reboot condicionado. needs-restarting -r devuelve 1 cuando hace falta reiniciar, por eso se acepta como código válido.

4. Ejecutar: ensayo, real y evidencia

bash · máquina de control
ansible-playbook -i inventory.ini patch.yml --check --diffPLAY [Parcheo mensual de servidores Linux] ************************************TASK [Actualizar paquetes (RHEL)] *********************************************changed: [srv-web-01.contoso.local]...PLAY RECAP ********************************************************************srv-web-01.contoso.local : ok=5 changed=1 unreachable=0 failed=0 skipped=3ansible-playbook -i inventory.ini patch.yml | tee logs/patch-2026-10.log...TASK [Reiniciar solo si hace falta] *******************************************changed: [srv-web-01.contoso.local]TASK [Fallar si httpd no está corriendo en los web] ***************************ok: [srv-web-01.contoso.local]PLAY RECAP ********************************************************************srv-web-01.contoso.local : ok=7 changed=2 unreachable=0 failed=0 skipped=2srv-web-02.contoso.local : ok=7 changed=2 unreachable=0 failed=0 skipped=2srv-db-01.contoso.local  : ok=6 changed=1 unreachable=0 failed=0 skipped=3

El PLAY RECAP con failed=0 y el log con tee son la evidencia que adjuntas al ticket de cambio. Si un servidor falla, Ansible se detiene ahí y los siguientes quedan sin tocar: eso es lo que quieres en una ventana de parcheo.

Consejo: ansible-playbook ... --limit srv-web-01.contoso.local ejecuta el mismo playbook en uno solo. Es la forma de probar en el servidor menos crítico antes de la ventana.

5. Lo que apuntas en el ticket

  • Versión del playbook (el commit de Git), inventario usado y la fecha.
  • El PLAY RECAP completo y qué servidores se reiniciaron.
  • Si alguno falló: el error de la tarea y qué se hizo a mano.

Siguientes pasos

  • Separa variables por entorno (group_vars/web.yml) y guarda secretos con ansible-vault, nunca en claro en el repo.
  • Con Windows, Ansible funciona por WinRM o SSH con los módulos ansible.windows.*: el mismo playbook, otro inventario.
  • El siguiente paso natural es un rol: ansible-galaxy init roles/baseline para la configuración base de un servidor nuevo (usuarios, NTP, firewall, agente de monitorización).

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

What you'll get

Replace the manual patching from the RHEL and Ubuntu guides with a playbook that does the same on every server, in order, stopping if one fails. By the end you will have a repository with the inventory, the playbook and the output of the first run for the change ticket.

Before you start

  • A control machine with Python 3 and SSH access to the servers (your Linux/macOS laptop, WSL or a small VM).
  • An SSH key deployed on the servers for a user with passwordless sudo (or with a password: Ansible asks for it with -K).
  • The servers in the Level 2 Git guide: the playbook is code and lives under version control from day one.

1. Install and check the connection

bash · control machine
python3 -m pip install --user ansibleansible --version | head -1ansible [core 2.17.4]mkdir -p ~/ops/ansible && cd ~/ops/ansiblecat > inventory.ini <<'EOF'[web]srv-web-01.contoso.localsrv-web-02.contoso.local [db]srv-db-01.contoso.local [linux:children]webdb [linux:vars]ansible_user=ansibleansible_become=trueEOFansible linux -i inventory.ini -m pingsrv-web-01.contoso.local | SUCCESS => {"changed": false, "ping": "pong"}srv-web-02.contoso.local | SUCCESS => {"changed": false, "ping": "pong"}srv-db-01.contoso.local | SUCCESS => {"changed": false, "ping": "pong"}

ping here is not ICMP: it means "I can SSH in and find Python". If one fails, try ssh ansible@srv-db-01.contoso.local by hand before looking in Ansible.

2. Ad-hoc commands before the playbook

bash · control machine
ansible linux -i inventory.ini -m shell -a "uptime; df -h / | tail -1"srv-web-01.contoso.local | CHANGED | rc=0 >> 08:12:03 up 41 days,  2:17,  0 users,  load average: 0.08, 0.05, 0.01/dev/mapper/rhel-root   50G   31G   20G  62% /...ansible web -i inventory.ini -m service -a "name=httpd state=started" --checksrv-web-01.contoso.local | SUCCESS => {"changed": false, "name": "httpd", "state": "started"}

--check is dry-run mode: it says what would change without touching anything. Always use it the first time.

3. The patching playbook

patch.yml
---- name: Monthly patching of Linux servers  hosts: linux  serial: 1  max_fail_percentage: 0  tasks:    - name: Update packages (RHEL)      ansible.builtin.dnf:        name: "*"        state: latest      when: ansible_os_family == "RedHat"      register: dnf_result     - name: Update packages (Ubuntu)      ansible.builtin.apt:        upgrade: dist        update_cache: true      when: ansible_os_family == "Debian"      register: apt_result     - name: Reboot needed? (RHEL)      ansible.builtin.command: needs-restarting -r      register: nr      failed_when: nr.rc not in [0, 1]      changed_when: false      when: ansible_os_family == "RedHat"     - name: Reboot needed? (Ubuntu)      ansible.builtin.stat:        path: /var/run/reboot-required      register: rr      when: ansible_os_family == "Debian"     - name: Reboot only if needed      ansible.builtin.reboot:        reboot_timeout: 600      when: (nr.rc is defined and nr.rc == 1) or (rr.stat is defined and rr.stat.exists)     - name: Check the main service is up      ansible.builtin.service_facts:     - name: Fail if httpd is not running on web servers      ansible.builtin.assert:        that: ansible_facts.services['httpd.service'].state == 'running'        fail_msg: "httpd did not start after patching"      when: "'web' in group_names"

What matters is in three lines: serial: 1 (one server at a time), max_fail_percentage: 0 (if one fails, everything stops) and the conditional reboot. needs-restarting -r returns 1 when a reboot is needed, which is why it is accepted as a valid code.

4. Run: dry run, real run and evidence

bash · control machine
ansible-playbook -i inventory.ini patch.yml --check --diffPLAY [Monthly patching of Linux servers] **************************************TASK [Update packages (RHEL)] *************************************************changed: [srv-web-01.contoso.local]...PLAY RECAP ********************************************************************srv-web-01.contoso.local : ok=5 changed=1 unreachable=0 failed=0 skipped=3ansible-playbook -i inventory.ini patch.yml | tee logs/patch-2026-10.log...TASK [Reboot only if needed] **************************************************changed: [srv-web-01.contoso.local]TASK [Fail if httpd is not running on web servers] ****************************ok: [srv-web-01.contoso.local]PLAY RECAP ********************************************************************srv-web-01.contoso.local : ok=7 changed=2 unreachable=0 failed=0 skipped=2srv-web-02.contoso.local : ok=7 changed=2 unreachable=0 failed=0 skipped=2srv-db-01.contoso.local  : ok=6 changed=1 unreachable=0 failed=0 skipped=3

The PLAY RECAP with failed=0 and the log from tee are the evidence you attach to the change ticket. If a server fails, Ansible stops there and the rest are left untouched: that is what you want in a patch window.

Tip: ansible-playbook ... --limit srv-web-01.contoso.local runs the same playbook on a single host. It is the way to test on the least critical server before the window.

5. What you write in the ticket

  • Playbook version (the Git commit), inventory used and the date.
  • The full PLAY RECAP and which servers rebooted.
  • If one failed: the task error and what was done by hand.

Next steps

  • Split variables per environment (group_vars/web.yml) and keep secrets in ansible-vault, never in clear text in the repo.
  • With Windows, Ansible works over WinRM or SSH with the ansible.windows.* modules: same playbook, different inventory.
  • The natural next step is a role: ansible-galaxy init roles/baseline for the base configuration of a new server (users, NTP, firewall, monitoring agent).

Is something missing, or different in your environment? Get in touch and I'll fix it.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →