grpc
+
sails
+
+
+
+
+
+
pycharm
+
pascal
<=
+
&
+
sublime
preact
suse
+
solidity
+
+
apex
+
mongo
+
dask
+
graphql
android
macos
delphi
+
+
+
hack
+
+
js
+
solid
gentoo
+
+
sklearn
+
swc
jwt
+
unix
+
->
qdrant
+
+
mysql
next
+
+
+
+
+
android
+
jenkins
fastapi
!!
prometheus
quarkus
rollup
->
+
+
+
esbuild
stencil
yaml
vercel
qwik
delphi
+
+
+
+
+
+
+
arch
Back to Blog
almalinux ansible automation

Automating System Updates with Ansible on AlmaLinux

Published Jul 15, 2025

Learn how to automate system updates, patch management, and security updates across your AlmaLinux infrastructure using Ansible playbooks and best practices for enterprise environments

18 min read
0 views
Table of Contents

In today’s enterprise environments, keeping systems updated with the latest security patches and bug fixes is crucial but can be time-consuming and error-prone when done manually. This comprehensive guide will show you how to automate system updates across your AlmaLinux infrastructure using Ansible, ensuring consistency, security, and efficiency.

Understanding the Need for Automated Updates

Managing updates across dozens or hundreds of servers manually is not only inefficient but also increases the risk of human error and security vulnerabilities. Automation with Ansible provides:

  • Consistency: Ensure all systems receive the same updates
  • Scheduling: Update during maintenance windows
  • Rollback capability: Quickly revert problematic updates
  • Audit trails: Track what was updated and when
  • Reduced downtime: Coordinate updates efficiently

Prerequisites

Before implementing automated updates, ensure you have:

  • AlmaLinux 9 systems (controller and managed nodes)
  • Ansible 2.9 or later installed
  • SSH key-based authentication configured
  • Sudo privileges on managed nodes
  • Python 3.6+ on all systems

Setting Up Ansible for AlmaLinux

Installing Ansible on the Controller

First, install Ansible on your control node:

# Enable EPEL repository
sudo dnf install epel-release -y

# Install Ansible
sudo dnf install ansible-core python3-pip -y

# Install additional collections
ansible-galaxy collection install ansible.posix
ansible-galaxy collection install community.general

# Verify installation
ansible --version

Configuring Ansible Inventory

Create a structured inventory for your AlmaLinux systems:

# /etc/ansible/inventory/production.ini
[webservers]
web01.example.com ansible_host=192.168.1.10
web02.example.com ansible_host=192.168.1.11
web03.example.com ansible_host=192.168.1.12

[databases]
db01.example.com ansible_host=192.168.1.20
db02.example.com ansible_host=192.168.1.21

[monitoring]
monitor01.example.com ansible_host=192.168.1.30

[almalinux:children]
webservers
databases
monitoring

[almalinux:vars]
ansible_user=ansible
ansible_become=yes
ansible_python_interpreter=/usr/bin/python3

Creating the Ansible Configuration

Configure Ansible for optimal performance:

# /etc/ansible/ansible.cfg
[defaults]
inventory = /etc/ansible/inventory/production.ini
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400
stdout_callback = yaml
callback_whitelist = profile_tasks, timer
forks = 20
timeout = 30

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
pipelining = True

Creating Update Playbooks

Basic System Update Playbook

Create a basic playbook for system updates:

---
# update-systems.yml
- name: Update AlmaLinux Systems
  hosts: almalinux
  gather_facts: yes
  serial: "{{ serial_percentage | default('25%') }}"
  max_fail_percentage: "{{ max_fail | default(25) }}"
  
  vars:
    reboot_timeout: 600
    update_cache_valid_time: 3600
    
  tasks:
    - name: Check current kernel version
      command: uname -r
      register: kernel_version_before
      changed_when: false

    - name: Update package cache
      dnf:
        update_cache: yes
        cache_valid_time: "{{ update_cache_valid_time }}"
      when: ansible_os_family == "RedHat"

    - name: Upgrade all packages
      dnf:
        name: '*'
        state: latest
        exclude: "{{ exclude_packages | default([]) }}"
      register: update_result

    - name: Check if reboot is required
      stat:
        path: /var/run/reboot-required
      register: reboot_required_file

    - name: Check for kernel updates
      shell: |
        LAST_KERNEL=$(rpm -q --last kernel | head -1 | awk '{print $1}' | sed 's/kernel-//')
        CURRENT_KERNEL=$(uname -r)
        if [ "$LAST_KERNEL" != "$CURRENT_KERNEL" ]; then
          echo "reboot required"
        fi
      register: kernel_update_check
      changed_when: false

    - name: Display update results
      debug:
        msg: |
          Updated packages: {{ update_result.results | length }}
          Reboot required: {{ reboot_required_file.stat.exists or 'reboot required' in kernel_update_check.stdout }}

    - name: Create update report
      template:
        src: update_report.j2
        dest: "/var/log/ansible/update_{{ inventory_hostname }}_{{ ansible_date_time.epoch }}.log"
      delegate_to: localhost

Advanced Update Playbook with Pre/Post Checks

Create a more sophisticated playbook with health checks:

---
# advanced-update.yml
- name: Advanced System Update with Health Checks
  hosts: almalinux
  gather_facts: yes
  serial: 2
  
  vars:
    health_check_services:
      - sshd
      - NetworkManager
      - firewalld
    backup_configs:
      - /etc/ssh/sshd_config
      - /etc/sysctl.conf
      - /etc/security/limits.conf
    
  pre_tasks:
    - name: Run pre-update health checks
      block:
        - name: Check disk space
          shell: df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
          register: disk_usage
          failed_when: disk_usage.stdout|int > 85

        - name: Check system load
          shell: uptime | awk -F'load average:' '{print $2}' | awk -F', ' '{print $1}'
          register: system_load
          failed_when: system_load.stdout|float > 4.0

        - name: Verify critical services
          systemd:
            name: "{{ item }}"
            state: started
          loop: "{{ health_check_services }}"
          register: service_status

        - name: Backup critical configurations
          archive:
            path: "{{ backup_configs }}"
            dest: "/var/backup/pre-update-{{ ansible_date_time.epoch }}.tar.gz"
            format: gz

        - name: Create system snapshot (if LVM)
          command: |
            lvcreate -L 5G -s -n pre_update_snap /dev/vg0/root
          ignore_errors: yes
          when: ansible_lvm is defined and ansible_lvm.vgs.vg0 is defined

  tasks:
    - name: Perform security updates only
      dnf:
        name: '*'
        state: latest
        security: yes
        bugfix: yes
      when: security_only | default(false) | bool

    - name: Perform full system update
      dnf:
        name: '*'
        state: latest
        exclude: "{{ exclude_packages | default([]) }}"
      when: not (security_only | default(false) | bool)
      register: update_result

    - name: Clean package cache
      dnf:
        autoremove: yes
        autoclean: yes

    - name: Update package database
      command: rpm --rebuilddb
      when: update_result.changed

  post_tasks:
    - name: Run post-update health checks
      block:
        - name: Verify critical services are running
          systemd:
            name: "{{ item }}"
            state: started
          loop: "{{ health_check_services }}"

        - name: Test network connectivity
          uri:
            url: https://www.google.com
            timeout: 10
          delegate_to: "{{ inventory_hostname }}"

        - name: Check for failed systemd services
          shell: systemctl --failed --no-pager
          register: failed_services
          failed_when: failed_services.stdout != ""
          changed_when: false

    - name: Generate detailed update report
      template:
        src: detailed_update_report.j2
        dest: "/var/log/ansible/detailed_update_{{ inventory_hostname }}_{{ ansible_date_time.epoch }}.html"
      delegate_to: localhost

Staged Rollout Playbook

Implement a staged rollout strategy:

---
# staged-rollout.yml
- name: Stage 1 - Update Development Systems
  hosts: development
  vars:
    stage: "development"
    notification_email: "[email protected]"
  import_playbook: update-systems.yml

- name: Wait for manual verification
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Pause for dev verification
      pause:
        prompt: "Development systems updated. Verify and press enter to continue to staging"
      when: not auto_continue | default(false) | bool

- name: Stage 2 - Update Staging Systems
  hosts: staging
  vars:
    stage: "staging"
    notification_email: "[email protected]"
  import_playbook: update-systems.yml

- name: Run integration tests on staging
  hosts: staging
  tasks:
    - name: Execute test suite
      uri:
        url: "http://{{ inventory_hostname }}/health"
        status_code: 200
      delegate_to: localhost

- name: Stage 3 - Update Production Systems
  hosts: production
  serial: "10%"
  vars:
    stage: "production"
    notification_email: "[email protected]"
    max_fail_percentage: 10
  import_playbook: update-systems.yml

Handling Reboots Safely

Intelligent Reboot Management

Create a playbook for safe system reboots:

---
# managed-reboot.yml
- name: Managed System Reboot
  hosts: "{{ target_hosts | default('almalinux') }}"
  serial: 1
  gather_facts: yes
  
  vars:
    reboot_timeout: 600
    health_check_delay: 30
    
  tasks:
    - name: Check if reboot is needed
      block:
        - name: Check for kernel updates
          shell: |
            LAST_KERNEL=$(rpm -q --last kernel | head -1 | awk '{print $1}' | sed 's/kernel-//')
            CURRENT_KERNEL=$(uname -r)
            [ "$LAST_KERNEL" != "$CURRENT_KERNEL" ] && echo "yes" || echo "no"
          register: kernel_reboot_needed
          changed_when: false

        - name: Check for glibc updates
          shell: |
            needs-restarting -r
          register: needs_restarting
          changed_when: false
          failed_when: false

        - name: Set reboot requirement
          set_fact:
            reboot_required: "{{ kernel_reboot_needed.stdout == 'yes' or needs_restarting.rc == 1 }}"

    - name: Prepare for reboot
      when: reboot_required | bool
      block:
        - name: Notify monitoring system
          uri:
            url: "http://monitor.example.com/api/maintenance"
            method: POST
            body_format: json
            body:
              host: "{{ inventory_hostname }}"
              action: "reboot"
              duration: "{{ reboot_timeout }}"
          delegate_to: localhost
          ignore_errors: yes

        - name: Stop application services
          systemd:
            name: "{{ item }}"
            state: stopped
          loop: "{{ app_services | default([]) }}"
          ignore_errors: yes

        - name: Sync filesystems
          command: sync

    - name: Reboot system
      reboot:
        reboot_timeout: "{{ reboot_timeout }}"
        pre_reboot_delay: 10
        post_reboot_delay: "{{ health_check_delay }}"
        test_command: whoami
        msg: "Ansible-initiated reboot for system updates"
      when: reboot_required | bool

    - name: Post-reboot validation
      when: reboot_required | bool
      block:
        - name: Wait for services to stabilize
          wait_for:
            timeout: "{{ health_check_delay }}"

        - name: Verify system is running latest kernel
          shell: |
            LAST_KERNEL=$(rpm -q --last kernel | head -1 | awk '{print $1}' | sed 's/kernel-//')
            CURRENT_KERNEL=$(uname -r)
            [ "$LAST_KERNEL" == "$CURRENT_KERNEL" ]
          changed_when: false

        - name: Check all services are running
          systemd:
            name: "{{ item }}"
            state: started
          loop: "{{ health_check_services | default(['sshd', 'NetworkManager']) }}"

        - name: Clear maintenance mode
          uri:
            url: "http://monitor.example.com/api/maintenance/clear"
            method: DELETE
            body_format: json
            body:
              host: "{{ inventory_hostname }}"
          delegate_to: localhost
          ignore_errors: yes

Security Considerations

Implementing Security Best Practices

Create a security-focused update playbook:

---
# security-updates.yml
- name: Security-Focused System Updates
  hosts: almalinux
  gather_facts: yes
  
  vars:
    security_scan_enabled: true
    vulnerability_threshold: "high"
    
  tasks:
    - name: Install security scanning tools
      dnf:
        name:
          - openscap
          - openscap-scanner
          - scap-security-guide
        state: present

    - name: Run pre-update security scan
      command: |
        oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis \
        --results /tmp/pre-update-scan.xml \
        /usr/share/xml/scap/ssg/content/ssg-almalinux9-ds.xml
      register: pre_scan
      when: security_scan_enabled | bool
      ignore_errors: yes

    - name: Apply security updates only
      dnf:
        name: '*'
        state: latest
        security: yes
      register: security_updates

    - name: Update SELinux policies
      command: |
        semodule -u /usr/share/selinux/targeted/*.pp
      when: ansible_selinux.status == "enabled"

    - name: Verify GPG signatures
      lineinfile:
        path: /etc/dnf/dnf.conf
        regexp: '^gpgcheck='
        line: 'gpgcheck=1'

    - name: Run post-update security scan
      command: |
        oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis \
        --results /tmp/post-update-scan.xml \
        /usr/share/xml/scap/ssg/content/ssg-almalinux9-ds.xml
      register: post_scan
      when: security_scan_enabled | bool
      ignore_errors: yes

    - name: Generate security compliance report
      shell: |
        oscap xccdf generate report /tmp/post-update-scan.xml > \
        /var/log/security-compliance-{{ ansible_date_time.epoch }}.html
      when: security_scan_enabled | bool and post_scan is succeeded

Scheduling and Automation

Setting Up Automated Update Schedule

Create an Ansible Tower/AWX job template or use cron:

---
# schedule-updates.yml
- name: Configure Automated Update Schedule
  hosts: localhost
  gather_facts: no
  
  tasks:
    - name: Create update schedule script
      copy:
        dest: /usr/local/bin/ansible-update-systems.sh
        mode: '0755'
        content: |
          #!/bin/bash
          
          # Ansible automated update script
          ANSIBLE_LOG_PATH="/var/log/ansible/updates-$(date +%Y%m%d-%H%M%S).log"
          export ANSIBLE_LOG_PATH
          
          # Run updates with different strategies based on day
          DAY_OF_WEEK=$(date +%u)
          
          case $DAY_OF_WEEK in
            3) # Wednesday - Development and Staging
              ansible-playbook /etc/ansible/playbooks/update-systems.yml \
                --limit "development,staging" \
                -e "security_only=true"
              ;;
            6) # Saturday - Production security updates
              ansible-playbook /etc/ansible/playbooks/update-systems.yml \
                --limit "production" \
                -e "security_only=true" \
                -e "serial_percentage=10%"
              ;;
            7) # Sunday - Full updates for non-critical systems
              ansible-playbook /etc/ansible/playbooks/update-systems.yml \
                --limit "monitoring,backup" \
                -e "security_only=false"
              ;;
          esac
          
          # Send report
          mail -s "Ansible Update Report - $(date)" [email protected] < $ANSIBLE_LOG_PATH

    - name: Configure cron job for automated updates
      cron:
        name: "Ansible automated system updates"
        user: ansible
        job: "/usr/local/bin/ansible-update-systems.sh"
        hour: "2"
        minute: "0"
        weekday: "3,6,7"
        state: present

    - name: Create systemd timer for updates (alternative)
      copy:
        dest: /etc/systemd/system/ansible-updates.timer
        content: |
          [Unit]
          Description=Ansible System Updates Timer
          Requires=ansible-updates.service
          
          [Timer]
          OnCalendar=Wed,Sat,Sun 02:00
          Persistent=true
          
          [Install]
          WantedBy=timers.target

    - name: Create systemd service for updates
      copy:
        dest: /etc/systemd/system/ansible-updates.service
        content: |
          [Unit]
          Description=Ansible System Updates
          After=network-online.target
          Wants=network-online.target
          
          [Service]
          Type=oneshot
          ExecStart=/usr/local/bin/ansible-update-systems.sh
          User=ansible
          Group=ansible
          StandardOutput=journal
          StandardError=journal

    - name: Enable and start timer
      systemd:
        name: ansible-updates.timer
        enabled: yes
        state: started
        daemon_reload: yes

Monitoring and Reporting

Creating Comprehensive Reports

Generate detailed update reports:

{# templates/detailed_update_report.j2 #}
<!DOCTYPE html>
<html>
<head>
    <title>System Update Report - {{ inventory_hostname }}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .header { background-color: #2c3e50; color: white; padding: 20px; }
        .section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; }
        .success { color: #27ae60; }
        .warning { color: #f39c12; }
        .error { color: #e74c3c; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
    </style>
</head>
<body>
    <div class="header">
        <h1>System Update Report</h1>
        <p>Host: {{ inventory_hostname }} | Date: {{ ansible_date_time.iso8601 }}</p>
    </div>
    
    <div class="section">
        <h2>System Information</h2>
        <table>
            <tr><th>Property</th><th>Value</th></tr>
            <tr><td>Hostname</td><td>{{ ansible_hostname }}</td></tr>
            <tr><td>OS Version</td><td>{{ ansible_distribution }} {{ ansible_distribution_version }}</td></tr>
            <tr><td>Kernel</td><td>{{ ansible_kernel }}</td></tr>
            <tr><td>Architecture</td><td>{{ ansible_architecture }}</td></tr>
            <tr><td>Total Memory</td><td>{{ ansible_memtotal_mb }} MB</td></tr>
            <tr><td>CPU Cores</td><td>{{ ansible_processor_vcpus }}</td></tr>
        </table>
    </div>
    
    <div class="section">
        <h2>Update Summary</h2>
        <table>
            <tr><th>Package</th><th>Old Version</th><th>New Version</th><th>Repository</th></tr>
            {% for package in update_result.results %}
            <tr>
                <td>{{ package.name }}</td>
                <td>{{ package.old_version | default('N/A') }}</td>
                <td>{{ package.new_version | default('N/A') }}</td>
                <td>{{ package.repo | default('Unknown') }}</td>
            </tr>
            {% endfor %}
        </table>
    </div>
    
    <div class="section">
        <h2>Service Status</h2>
        <table>
            <tr><th>Service</th><th>Status</th><th>Active Since</th></tr>
            {% for service in service_status.results %}
            <tr>
                <td>{{ service.item }}</td>
                <td class="{% if service.status.ActiveState == 'active' %}success{% else %}error{% endif %}">
                    {{ service.status.ActiveState }}
                </td>
                <td>{{ service.status.ActiveEnterTimestamp | default('N/A') }}</td>
            </tr>
            {% endfor %}
        </table>
    </div>
</body>
</html>

Integration with Monitoring Systems

Create playbook for monitoring integration:

---
# monitoring-integration.yml
- name: Integrate Updates with Monitoring
  hosts: almalinux
  gather_facts: yes
  
  vars:
    prometheus_pushgateway: "http://prometheus-pushgateway.example.com:9091"
    elasticsearch_endpoint: "http://elasticsearch.example.com:9200"
    
  tasks:
    - name: Push update metrics to Prometheus
      uri:
        url: "{{ prometheus_pushgateway }}/metrics/job/system_updates/instance/{{ inventory_hostname }}"
        method: POST
        body: |
          # TYPE system_updates_total counter
          system_updates_total{host="{{ inventory_hostname }}"} {{ update_result.results | length }}
          # TYPE system_update_timestamp gauge
          system_update_timestamp{host="{{ inventory_hostname }}"} {{ ansible_date_time.epoch }}
          # TYPE system_reboot_required gauge
          system_reboot_required{host="{{ inventory_hostname }}"} {{ reboot_required | bool | int }}
      delegate_to: localhost
      when: update_result is defined

    - name: Send update event to Elasticsearch
      uri:
        url: "{{ elasticsearch_endpoint }}/ansible-updates/_doc"
        method: POST
        body_format: json
        body:
          timestamp: "{{ ansible_date_time.iso8601 }}"
          host: "{{ inventory_hostname }}"
          update_count: "{{ update_result.results | length }}"
          packages: "{{ update_result.results | map(attribute='name') | list }}"
          success: true
          reboot_required: "{{ reboot_required | default(false) }}"
      delegate_to: localhost
      when: update_result is defined

Best Practices and Tips

1. Testing Update Procedures

Always test updates in a safe environment:

# test-updates.yml
- name: Test Update Procedures
  hosts: test_systems
  tasks:
    - name: Create VM snapshot before testing
      vmware_guest_snapshot:
        hostname: "{{ vcenter_hostname }}"
        username: "{{ vcenter_username }}"
        password: "{{ vcenter_password }}"
        datacenter: "{{ datacenter_name }}"
        name: "{{ inventory_hostname }}"
        state: present
        snapshot_name: "pre_update_test_{{ ansible_date_time.epoch }}"
      delegate_to: localhost
      when: virtualization_type == "VMware"

2. Rollback Procedures

Implement rollback capabilities:

# rollback-updates.yml
- name: Rollback System Updates
  hosts: "{{ target_host }}"
  tasks:
    - name: Downgrade packages to previous version
      dnf:
        name: "{{ item.name }}-{{ item.old_version }}"
        state: present
        allow_downgrade: yes
      loop: "{{ packages_to_rollback }}"
      when: packages_to_rollback is defined

    - name: Restore from LVM snapshot
      command: |
        lvconvert --merge /dev/vg0/pre_update_snap
      when: use_lvm_snapshot | default(false) | bool

3. Handling Special Cases

Deal with specific scenarios:

# Handle database servers differently
- name: Update database servers with extra care
  hosts: databases
  serial: 1
  tasks:
    - name: Put database in maintenance mode
      command: mysql -e "SET GLOBAL read_only = ON;"
      when: database_type == "mysql"

    - name: Perform updates
      import_tasks: update-tasks.yml

    - name: Remove maintenance mode
      command: mysql -e "SET GLOBAL read_only = OFF;"
      when: database_type == "mysql"

Troubleshooting Common Issues

Debugging Failed Updates

# debug-updates.yml
- name: Debug Update Issues
  hosts: "{{ problem_host }}"
  tasks:
    - name: Check DNF history
      command: dnf history info last
      register: dnf_history

    - name: Check for broken dependencies
      command: dnf check
      register: dependency_check
      failed_when: false

    - name: Review system logs
      command: journalctl -u dnf --since "1 hour ago"
      register: system_logs

    - name: Display debug information
      debug:
        msg: |
          DNF History: {{ dnf_history.stdout }}
          Dependency Check: {{ dependency_check.stdout }}
          System Logs: {{ system_logs.stdout | truncate(500) }}

Conclusion

Automating system updates with Ansible on AlmaLinux provides a robust, scalable solution for maintaining your infrastructure. By implementing the strategies and playbooks outlined in this guide, you can ensure your systems remain secure, up-to-date, and stable while minimizing manual intervention and potential errors.

Remember to always test your update procedures thoroughly, maintain good rollback strategies, and monitor the results of your automated updates. With proper implementation, you’ll save time, reduce errors, and improve the overall security posture of your AlmaLinux infrastructure.