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
sudosin 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
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
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
---- 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
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=3El 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 RECAPcompleto 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 conansible-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/baselinepara 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
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
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
---- 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
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=3The 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 RECAPand 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 inansible-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/baselinefor 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 →