Linux patching looks deceptively simple until you have a mixed estate—Ubuntu LTS boxes, RHEL subscriptions, the odd SLES node in a regulated environment—each with different package managers, different security-update semantics, and different opinions on what “reboot required” means. A robust playbook handles all of them without becoming a conditional maze.
Package Manager Differences
| Distro Family | Manager | Ansible Module | Security-only flag |
|---|---|---|---|
| Debian / Ubuntu | apt | ansible.builtin.apt |
-security pocket via default_release |
| RHEL / CentOS 8+ | dnf | ansible.builtin.dnf |
security: true |
| RHEL / CentOS 7 | yum | ansible.builtin.yum |
security: true |
| SUSE / SLES | zypper | community.general.zypper |
type: patch |
Use ansible_os_family and ansible_distribution facts to branch cleanly rather than maintaining separate playbooks per distro.
Security-Only vs Full Upgrade
Debian / Ubuntu — security updates only:
- name: Apply security updates (Debian/Ubuntu)
ansible.builtin.apt:
upgrade: dist
update_cache: true
default_release: "-security"
when: ansible_os_family == "Debian"
RHEL / CentOS — security updates only:
- name: Apply security updates (RHEL/CentOS)
ansible.builtin.dnf:
name: "*"
state: latest
security: true
update_cache: true
when: ansible_os_family == "RedHat"
Full upgrade (all packages):
- name: Full upgrade (Debian/Ubuntu)
ansible.builtin.apt:
upgrade: dist
update_cache: true
when: ansible_os_family == "Debian"
- name: Full upgrade (RHEL/CentOS)
ansible.builtin.dnf:
name: "*"
state: latest
when: ansible_os_family == "RedHat"
Kernel Patching and Reboot Detection
Kernel updates require a reboot to take effect. Detect this reliably rather than rebooting unconditionally.
Debian / Ubuntu — check /run/reboot-required:
- name: Check if reboot is required (Debian/Ubuntu)
ansible.builtin.stat:
path: /run/reboot-required
register: reboot_required_file
when: ansible_os_family == "Debian"
- name: Set reboot flag (Debian/Ubuntu)
ansible.builtin.set_fact:
reboot_required: ""
when: ansible_os_family == "Debian"
RHEL / CentOS — use needs-restarting:
- name: Check if reboot is required (RHEL/CentOS)
ansible.builtin.command: needs-restarting -r
register: reboot_check
failed_when: reboot_check.rc not in [0, 1]
changed_when: false
when: ansible_os_family == "RedHat"
- name: Set reboot flag (RHEL/CentOS)
ansible.builtin.set_fact:
reboot_required: ""
when: ansible_os_family == "RedHat"
Controlled Reboot with Service Validation
- name: Reboot if required and allowed
ansible.builtin.reboot:
reboot_timeout: 600 # wait up to 10 minutes for the host to come back
pre_reboot_delay: 30 # wait 30s after sending reboot command
post_reboot_delay: 30 # wait 30s after host returns before continuing
test_command: uptime # confirm the host is responsive
when:
- reboot_required | bool
- patch_reboot_allowed | bool # set per group/host in group_vars
- name: Validate critical services are running post-reboot
ansible.builtin.service:
name: ""
state: started
loop: "" # defined in group_vars
register: service_check
- name: Fail if any service did not recover
ansible.builtin.fail:
msg: "Service is not running after reboot on "
loop: ""
when: item.state != "started"
Handling Package Locks and Failures
Debian — held packages:
- name: List held packages before patching
ansible.builtin.command: apt-mark showhold
register: held_packages
changed_when: false
- name: Warn if held packages exist
ansible.builtin.debug:
msg: "Held packages on : "
when: held_packages.stdout != ""
RHEL — excluded packages in dnf.conf:
- name: Show dnf exclusions
ansible.builtin.command: grep -i exclude /etc/dnf/dnf.conf
register: dnf_exclusions
changed_when: false
failed_when: false
- name: Report exclusions
ansible.builtin.debug:
var: dnf_exclusions.stdout_lines
Complete Rolling Patch Workflow
A full playbook combining detection, conditional reboot, and service validation:
---
- name: Linux rolling patch
hosts: linux_servers
become: true
serial: ""
vars:
critical_services:
- nginx
- sshd
tasks:
- name: Update package cache
ansible.builtin.package:
update_cache: true
when: ansible_os_family in ["Debian", "RedHat"]
- name: Apply security patches
ansible.builtin.dnf:
name: "*"
state: latest
security: true
when: ansible_os_family == "RedHat"
register: patch_result_rhel
- name: Apply security patches
ansible.builtin.apt:
upgrade: dist
default_release: "-security"
when: ansible_os_family == "Debian"
register: patch_result_debian
- name: Check reboot required (Debian/Ubuntu)
ansible.builtin.stat:
path: /run/reboot-required
register: reboot_file
when: ansible_os_family == "Debian"
- name: Check reboot required (RHEL/CentOS)
ansible.builtin.command: needs-restarting -r
register: reboot_cmd
failed_when: reboot_cmd.rc not in [0, 1]
changed_when: false
when: ansible_os_family == "RedHat"
- name: Set unified reboot fact
ansible.builtin.set_fact:
reboot_required: >-
- name: Reboot if required
ansible.builtin.reboot:
reboot_timeout: 600
pre_reboot_delay: 30
post_reboot_delay: 30
when:
- reboot_required | bool
- patch_reboot_allowed | default(true) | bool
- name: Validate services
ansible.builtin.service:
name: ""
state: started
loop: ""
Previous: Advanced Ansible Usage for Enterprise Environments
Next in the series: Windows Server Patching with Ansible — WinRM setup, win_updates, update categories, and the security risks of misconfigured Windows remoting.
