The Problem
You trigger an Ansible playbook on a fresh Ubuntu or Debian instance, only for it to crash instantly. Usually, the failure looks like this:
FAILED! => {
"msg": "'/usr/bin/apt-get dist-upgrade' failed: E: Could not get lock /var/lib/dpkg/lock-frontend - open (11: Resource temporarily unavailable)",
"rc": 100
}
What is happening?
The apt package manager uses lock files like /var/lib/dpkg/lock-frontend to prevent multiple processes from altering the database at once. If another tool is busy installing software, Ansible is blocked.
The usual suspects include:
- **Unattended Upgrades:** Modern Ubuntu versions often run `apt update` automatically within 60 seconds of booting.
- **Overlapping Tasks:** You might be running multiple playbooks or high-fork counts targeting the same node.
- **Zombies:** A previous installation attempt crashed, leaving a stale lock file behind.
Solution 1: The "Patience" Parameter (Best Practice)
Starting with Ansible 2.8, the apt module includes a built-in way to wait for the lock. Instead of failing, Ansible will wait for the specified number of seconds for the lock to disappear.
- name: Install Nginx with a lock timeout
apt:
name: nginx
state: present
lock_timeout: 300
This tells Ansible to wait up to 5 minutes. It is the cleanest solution because it handles background updates without extra logic or complex shell commands.
Solution 2: Manual Retry Logic
If you are on an older version of Ansible or want more control, use until. This retries the task 10 times with a 10-second gap, giving background processes 100 seconds to finish.
- name: Install Nginx with retries
apt:
name: nginx
state: present
update_cache: yes
register: apt_result
until: apt_result is success
retries: 10
delay: 10
Solution 3: The Pre-Flight Check
Sometimes you want to clear the air before running any package tasks. You can add a task at the very start of your playbook to wait until the apt process is free.
- name: Wait for apt lock to be released
shell: fuser /var/lib/dpkg/lock-frontend
register: lock_check
until: lock_check.rc != 0
retries: 20
delay: 10
failed_when: false
changed_when: false
In this snippet, fuser returns a non-zero exit code when no process is using the file. Ansible will loop until that happens.
Solution 4: Disabling Automatic Updates
On CI/CD runners or immutable infrastructure, you might want to stop the OS from updating itself entirely. This ensures Ansible is the only entity managing packages.
- name: Stop and disable unattended-upgrades
systemd:
name: unattended-upgrades
state: stopped
enabled: no
- name: Disable apt daily timers
systemd:
name: "{{ item }}"
state: stopped
enabled: no
loop:
- apt-daily.timer
- apt-daily-upgrade.timer
The Last Resort: Brute Force
If a lock is truly stuck—perhaps from a disconnected SSH session—you can manually remove it. Warning: Use this only if you are certain no real update is running. Deleting these during an active upgrade can corrupt your dpkg database.
- name: Force remove stale lock files
file:
path: "{{ item }}"
state: absent
loop:
- /var/lib/dpkg/lock
- /var/lib/dpkg/lock-frontend
- /var/lib/apt/lists/lock
How to Verify
To see which process is currently holding your playbook hostage, log into the server and run:
sudo lsof /var/lib/dpkg/lock-frontend
If it returns a PID (Process ID), that is the culprit. If it returns nothing, the lock is free. When running Ansible, use -vvv to see exactly how many times your tasks are retrying before they succeed.

