Automate Linux Patch Management with Ansible: Zero-Touch Updates for Your Fleet

Last Updated: July 18, 2025
Keeping your servers secure and compliant shouldn’t be a full-time job. In this step-by-step guide, you’ll learn how to automate Linux patching across Ubuntu and CentOS servers using Ansible; scalable, hands-off, and production-ready.
You will learn how to:
- Set up Ansible on your local machine or a dedicated management node
- Build a flexible inventory to manage both Ubuntu and CentOS servers (and beyond)
- Write and understand a patching playbook for full automation
- Patch all available updates, or only those you explicitly approve
- Handle reboots and service restarts in a safe, controlled way
- Schedule patching as a background job for true “set and forget” ops
- Troubleshoot and resolve common issues you might encounter
- Adapt the workflow for your own compliance, approval, and security requirements
- Tested on:
- Ubuntu 24.04.2 LTS (WSL2, x86_64)
- CentOS Stream 9 (tested in local VM).
- What you need:
- Download the CentOS VM from OSBoxes, (If you don’t already have a development CentOS server/VM) and import it into VirtualBox or VMware Workstation Player.
- Drop a line in the comments if you need help with import instructions
- Control node with Ansible (your local machine or a management VM)
- SSH access (key-based) to all target servers
- Sudo/root privileges on managed hosts
- Download the CentOS VM from OSBoxes, (If you don’t already have a development CentOS server/VM) and import it into VirtualBox or VMware Workstation Player.
- Already comfortable with the CLI and server management basics? Great! If you’re new to CLI or SSH, check out our Getting Started with AWS EC2 via CLI post for foundational skills.
- WSL: Lets you run multiple Linux distributions side by side (Ubuntu, Debian, Kali, even CentOS with some extra steps). Quick to set up for learning, but networking is more limited. Each instance has its own hostname, but IPs are not persistent and cross-WSL SSH may require port forwarding.
- VMs: Best for simulating real-world infrastructure. Each VM can be assigned a static IP and behaves like a true remote server. Ideal for testing Ansible’s ability to patch across multiple hosts and network segments.
Tip: For production-like patching, use VMs. For quick demos or single-server automation, WSL is fine (just be aware of its networking limitations).
- You are comfortable using the Linux command line
- You have administrative (sudo/root) access to the systems you want to manage
- Your control node (local PC, WSL, or a VM) is running Linux and can install Ansible
- You can connect to your target servers over SSH (with key-based auth preferred)
- All target machines have Python 3 installed (default on most modern distros; otherwise, install manually)
- Your target systems are either Ubuntu 18.04 or later, or CentOS 7/8 (or similar derivatives)
- You have backed up any critical configuration or data before applying updates (see our Copy-Paste Bash Backup Playbook for an easy backup workflow)
- Centralize control: Patch 1 or 1000 servers from a single command.
- Standardize workflows: No more “snowflake” servers.
- Automate safely: Control reboots, get notifications, test before you apply.
sudo apt update
sudo apt install ansible -y
On CentOS/RHEL:
sudo dnf install epel-release -y
sudo dnf install ansible -y
Check your install:
ansible --version
[ubuntu]
ubuntu1 ansible_host=192.168.1.101 ansible_user=ubuntu
[centos]
centos1 ansible_host=192.168.1.102 ansible_user=centos
[all:vars]
ansible_python_interpreter=/usr/bin/python3
- Explanation:
- This inventory tracks both Ubuntu and CentOS hosts.
- Replace IPs and users with your actual hosts.
- The
ansible_python_interpreter
variable ensures Ansible finds Python 3, which is default for most modern distros.
ssh-keygen -t ed25519
ssh-copy-id ubuntu@192.168.1.101
ssh-copy-id centos@192.168.1.102
---
- name: Universal Linux patch management
hosts: all
become: yes
tasks:
- name: Update & upgrade on Ubuntu/Debian
ansible.builtin.apt:
update_cache: yes
upgrade: dist
autoremove: yes
autoclean: yes
when: ansible_facts['os_family'] == "Debian"
- name: Upgrade all packages on CentOS/RHEL 7/8 (yum)
ansible.builtin.yum:
name: "*"
state: latest
update_cache: yes
autoremove: yes
when:
- ansible_facts['os_family'] == "RedHat"
- ansible_facts['distribution_major_version'] is defined
- ansible_facts['distribution_major_version'] | int < 9
- name: Upgrade all packages on CentOS Stream 9+/RHEL 9+ (dnf)
ansible.builtin.dnf:
name: "*"
state: latest
update_cache: yes
autoremove: yes
when:
- ansible_facts['os_family'] == "RedHat"
- ansible_facts['distribution_major_version'] is defined
- ansible_facts['distribution_major_version'] | int >= 9
- name: Reboot if required (Debian/Ubuntu)
ansible.builtin.reboot:
reboot_timeout: 600
when: ansible_facts['os_family'] == "Debian"
- name: Reboot if required (CentOS/RHEL)
ansible.builtin.reboot:
reboot_timeout: 600
when: ansible_facts['os_family'] == "RedHat"
What’s happening here?
- The playbook uses Ansible facts to detect each server’s OS family and major version.
- It runs the correct module for each case:
apt
for Debian/Ubuntuyum
for CentOS/RHEL 7/8dnf
for CentOS Stream 9+ and RHEL 9+
become: yes
runs each task with root privileges, required for patching.- After patching, each server is rebooted if needed.
This approach lets you manage a mixed fleet (Ubuntu, CentOS, RHEL, etc.) with one universal playbook, and ensures the right package manager is used every time.
vars:
approved_packages:
- openssl
- sudo
- curl
tasks:
- name: Upgrade only approved packages on Ubuntu/Debian
ansible.builtin.apt:
name: "{{ approved_packages }}"
state: latest
when: ansible_facts['os_family'] == "Debian"
- name: Upgrade only approved packages on CentOS/RHEL 7/8 (yum)
ansible.builtin.yum:
name: "{{ approved_packages }}"
state: latest
update_cache: yes
when:
- ansible_facts['os_family'] == "RedHat"
- ansible_facts['distribution_major_version'] is defined
- ansible_facts['distribution_major_version'] | int < 9
- name: Upgrade only approved packages on CentOS Stream 9+/RHEL 9+ (dnf)
ansible.builtin.dnf:
name: "{{ approved_packages }}"
state: latest
update_cache: yes
when:
- ansible_facts['os_family'] == "RedHat"
- ansible_facts['distribution_major_version'] is defined
- ansible_facts['distribution_major_version'] | int >= 9
ansible-playbook -i hosts.ini patch-linux.yml
You’ll see colored output for each server, showing changed or ok states. If there’s an error, double-check SSH, sudo, or inventory config.
- Tags for Selective Runs:
Addtags: patch
to your main tasks. Then run only patching tasks via:
ansible-playbook -i hosts.ini patch-linux.yml --tags patch
- Dry-Run Mode:
Preview changes without making them:
ansible-playbook -i hosts.ini patch-linux.yml --check
- Email Notification:
Add a final task using themail
module (requiresmail
setup on your node).
Want more on automating notifications or backups before patching? See Copy-Paste Bash Backup Playbook.
- Save a wrapper script:
#!/bin/bash
ansible-playbook -i /path/to/hosts.ini /path/to/patch-linux.yml
- Make executable:
chmod +x patch-scheduler.sh
- Add to crontab for automatic weekly runs:
0 4 * * 0 /home/moderntechops/patch-scheduler.sh >> /home/moderntechops/patch.log 2>&1
Step 8: Validation, Troubleshooting, and Best Practices
- Verify packages:
- Ubuntu:
sudo apt list --upgradable
- CentOS:
sudo yum check-update
- Ubuntu:
- Check logs:
tail -f /home/moderntechops/patch.log
- Troubleshoot:
- Add
-vvv
to your playbook command for verbose debug output.
- Add
- Best practices:
- Test playbooks in dev/staging first.
- Consider pre-patch config backups (see our backup playbook).
- Review and tweak reboot logic to fit your operational policies.
Symptom | Fix/Solution |
---|---|
SSH Failures Error: “Permission denied” or “Host unreachable” | Double-check your SSH keys and usernames. Try logging in manually:ssh user@host If on WSL, verify the host’s SSH service is running and firewall is open. |
Sudo Privileges Required Error: “FAILED! => ‘msg’: ‘sudo: a password is required’” | Make sure your Ansible user has passwordless sudo, or use the --ask-become-pass flag for interactive sudo. |
Python Not Found on Remote Host Error: “python interpreter not found” | 1. Install Python 3 on your target machine. 2. Specify its location in your inventory with: ansible_python_interpreter=/usr/bin/python3 |
Unreachable Hosts Error: “UNREACHABLE!” | Double-check your hostnames/IPs, ensure hosts are powered on, and that your firewall isn’t blocking SSH. |
Module Not Found Error: “The module apt/yum was not found” | 1. Make sure you’re using a recent version of Ansible. 2. Confirm that the module is supported for your OS. |
Locked Package Managers Error: “Could not get lock /var/lib/dpkg/lock” | Wait and try again, or ensure no other package process (like unattended-upgrades) is running. |
Other Issues? | Use ansible-playbook ... -vvv for detailed logs.Check the Ansible documentation for module-specific tips. |
What’s Next?
- Want to automate patching on AWS, Azure, or GCP VMs? See Getting Started with AWS EC2 via CLI.
- Ready for “Ansible vs. Puppet vs. Native Tools”? Stay tuned for our next deep dive.
- Looking to make this bulletproof? We’ll cover dynamic inventories, patch notifications, and error handling in future posts.
Related Posts
Building a full cloud-native alerting pipeline
Building a full end-to-end, cloud-native alerting pipeline Last Updated: July 14, 2025 Why Cloud-Native Alerting…
Intro to systemd: Managing Services and Boot Processes Like a Pro
Intro to systemd: Managing Services and Boot Processes Like a Pro Last Updated: June 23,…
10 Must-Know Linux Terminal Productivity Hacks
10 Must-Know Linux Terminal Productivity Hacks Boost your efficiency by mastering these ten terminal tricks….
Automate Daily Log Rotation with Logrotate & Cron
Automate Daily Log Rotation with Logrotate & Cron Managing logs effectively is critical for system…
