Introduction

Keeping your fleet of Debian-based servers consistently updated is one of the most critical — and time-consuming — tasks in system administration. The Ansible ansible.builtin.apt module automates package updates across Debian, Ubuntu, Linux Mint, Kali Linux, and all other APT-based distributions. This article covers single-package updates, full system upgrades, rolling update strategies with serial, reboot handling, and production best practices.

Module Overview

The ansible.builtin.apt module manages packages on Debian-like systems. For rolling updates, the key parameters are:

ParameterTypeDescription
namestring/listPackage name(s), or "*" for all packages
statestringpresent, absent, latest, fixed
update_cachebooleanRun apt-get update before install
cache_valid_timeintegerSkip cache update if refreshed within N seconds
upgradestringno, safe, full, dist
autoremovebooleanRemove unused dependencies
autocleanbooleanClean local repository of old packages
force_apt_getbooleanUse apt-get instead of aptitude
dpkg_optionsstringAdditional dpkg options
only_upgradebooleanOnly upgrade, don't install new

Update Strategies

Strategy 1: Update a Single Package

---
- name: Update nginx
  hosts: webservers
  become: true
  tasks:
    - name: Ensure nginx is latest
      ansible.builtin.apt:
        name: nginx
        state: latest
        update_cache: true

Strategy 2: Update All Packages

---
- name: Full system update
  hosts: all
  become: true
  tasks:
    - name: Update all packages
      ansible.builtin.apt:
        name: "*"
        state: latest
        update_cache: true

Strategy 3: Safe Upgrade

Uses aptitude safe-upgrade — only upgrades packages that don't require removing other packages:

---
- name: Safe upgrade
  hosts: all
  become: true
  tasks:
    - name: Safe upgrade all packages
      ansible.builtin.apt:
        upgrade: safe
        update_cache: true

Strategy 4: Full Upgrade

Uses aptitude full-upgrade — may remove packages if needed for the upgrade:

---
- name: Full upgrade
  hosts: all
  become: true
  tasks:
    - name: Full upgrade all packages
      ansible.builtin.apt:
        upgrade: full
        update_cache: true

Strategy 5: Distribution Upgrade

Uses apt-get dist-upgrade — handles changing dependencies, commonly used for major version upgrades:

---
- name: Distribution upgrade
  hosts: all
  become: true
  tasks:
    - name: Dist upgrade
      ansible.builtin.apt:
        upgrade: dist
        update_cache: true

Rolling Updates with serial

In production, you should never update all servers at once. Use serial to update servers in batches:

---
- name: Rolling update - web tier
  hosts: webservers
  become: true
  serial: 2  # Update 2 servers at a time
  max_fail_percentage: 25
  tasks:
    - name: Update all packages
      ansible.builtin.apt:
        upgrade: safe
        update_cache: true
      register: apt_result

    - name: Display updated packages
      ansible.builtin.debug:
        msg: "Updated packages: {{ apt_result.stdout_lines | default([]) }}"
      when: apt_result.changed

Serial Options

# Fixed number
serial: 1        # One at a time (safest)
serial: 3        # Three at a time

# Percentage
serial: "25%"    # 25% of hosts at a time

# Escalating batches
serial:
  - 1            # First: 1 host (canary)
  - 3            # Then: 3 hosts
  - "50%"        # Then: 50% of remaining

Complete Rolling Update Playbook

A production-ready playbook with pre-checks, update, reboot handling, and verification:

---
- name: Rolling update Debian servers
  hosts: all
  become: true
  serial: 1
  max_fail_percentage: 0
  pre_tasks:
    - name: Check if server is healthy
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}/health"
        status_code: 200
      register: health_check
      failed_when: health_check.status != 200
      when: health_check_enabled | default(false)

  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600  # Skip if updated in last hour

    - name: Upgrade all packages (safe)
      ansible.builtin.apt:
        upgrade: safe
      register: upgrade_result

    - name: Remove unused dependencies
      ansible.builtin.apt:
        autoremove: true
      when: upgrade_result.changed

    - name: Clean apt cache
      ansible.builtin.apt:
        autoclean: true

    - name: Check if reboot is required
      ansible.builtin.stat:
        path: /var/run/reboot-required
      register: reboot_file

    - name: Reboot if required
      ansible.builtin.reboot:
        msg: "Reboot triggered by system updates"
        reboot_timeout: 300
        pre_reboot_delay: 5
        post_reboot_delay: 30
      when: reboot_file.stat.exists

  post_tasks:
    - name: Verify server is back online
      ansible.builtin.wait_for:
        port: 22
        timeout: 120
      delegate_to: localhost
      become: false

Handling Reboots

Some updates (kernel, glibc, systemd) require a reboot. Detect and handle this:

- name: Check reboot required
  ansible.builtin.stat:
    path: /var/run/reboot-required
  register: reboot_required

- name: Display packages requiring reboot
  ansible.builtin.command: cat /var/run/reboot-required.pkgs
  register: reboot_pkgs
  when: reboot_required.stat.exists
  changed_when: false

- name: Show reboot packages
  ansible.builtin.debug:
    msg: "Reboot needed for: {{ reboot_pkgs.stdout_lines }}"
  when: reboot_required.stat.exists

- name: Reboot the server
  ansible.builtin.reboot:
    reboot_timeout: 300
  when: reboot_required.stat.exists

Security Updates Only

Install only security updates using unattended-upgrades or apt filtering:

- name: Install security updates only
  ansible.builtin.apt:
    upgrade: safe
    update_cache: true
  environment:
    APT_LISTBUGS_FRONTEND: none
    DEBIAN_FRONTEND: noninteractive

# Alternative: use unattended-upgrades
- name: Ensure unattended-upgrades is installed
  ansible.builtin.apt:
    name: unattended-upgrades
    state: present

- name: Run security updates
  ansible.builtin.command: unattended-upgrade --dry-run
  register: security_updates
  changed_when: false

- name: Apply security updates
  ansible.builtin.command: unattended-upgrade
  when: "'Packages that will be upgraded' in security_updates.stdout"

Preventing Interactive Prompts

Some package updates prompt for user input. Prevent this:

- name: Non-interactive upgrade
  ansible.builtin.apt:
    upgrade: dist
    update_cache: true
    dpkg_options: "force-confold,force-confdef"
  environment:
    DEBIAN_FRONTEND: noninteractive
    NEEDRESTART_MODE: a
dpkg OptionDescription
force-confoldKeep existing config files
force-confdefUse default for new config options
force-confnewAlways use package's config file

Updating Specific Package Sets

# Update security-critical packages
- name: Update critical packages
  ansible.builtin.apt:
    name:
      - openssl
      - libssl3
      - openssh-server
      - openssh-client
      - linux-image-generic
    state: latest
    update_cache: true

# Pin a package version
- name: Install specific version
  ansible.builtin.apt:
    name: nginx=1.24.0-1~jammy
    state: present

Monitoring Update Results

- name: Upgrade packages
  ansible.builtin.apt:
    upgrade: safe
    update_cache: true
  register: apt_output

- name: Show upgrade summary
  ansible.builtin.debug:
    msg: |
      Changed: {{ apt_output.changed }}
      Packages upgraded: {{ apt_output.stdout_lines | select('match', '^Inst') | list | length }}
  when: apt_output.changed

Scheduling with Cron

Automate regular updates:

- name: Schedule weekly security updates
  ansible.builtin.cron:
    name: "Weekly apt security update"
    weekday: "0"  # Sunday
    hour: "3"
    minute: "0"
    job: "DEBIAN_FRONTEND=noninteractive apt-get -y --only-upgrade install $(apt-get -s upgrade 2>/dev/null | awk '/^Inst.*security/ {print $2}') > /var/log/auto-update.log 2>&1"

Best Practices

  1. Always use serial in production — never update all servers simultaneously
  2. Start with a canary — use serial: [1, "25%", "50%"] to detect issues early
  3. Set max_fail_percentage: 0 — stop immediately if any server fails
  4. Handle reboots — check /var/run/reboot-required after updates
  5. Use cache_valid_time — avoid redundant apt-get update calls
  6. Set DEBIAN_FRONTEND: noninteractive — prevent prompts from hanging automation
  7. Use dpkg_options: force-confold — keep existing config files during upgrades
  8. Clean up after updates — autoremove and autoclean to free disk space
  9. Test upgrades in staging first — never roll out untested updates to production
  10. Log everything — register results and store in audit logs

Conclusion

Rolling updates with the Ansible apt module give you controlled, reproducible package management across your Debian/Ubuntu fleet. Use serial for batch processing, handle reboots automatically, set DEBIAN_FRONTEND: noninteractive to prevent prompt hang-ups, and always clean up with autoremove. Combined with health checks and canary deployments, this approach ensures zero-downtime updates at scale.