68 errors and symptoms Ansible users meet, each with the exact message, what it means, every cause that produces it with a check that proves it and the fix for that cause, and how to prevent it. Then learn it for good: break and fix each one yourself in the browser playground. Generated from the same data as the interactive reference.
UNREACHABLE: Permission denied (publickey)
Error
fatal: [web-04]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: root@10.0.1.14: Permission denied (publickey).", "unreachable": true}
What it means
Ansible reached the host's SSH server, but the server accepted none of the keys offered for that user. root@10.0.1.14 is the user Ansible logged in as and the address it used; (publickey) means key login is the only method the server allows, so no password was tried.
Causes, most likely first
Ansible logs in as the wrong user. An ansible_user in the inventory beats -u on the command line, so a stale value wins and that user has no copy of your key.
Check
ansible-inventory --host <host> # ansible_user: does it match the user in the message?
Fix
Correct ansible_user in the inventory, group_vars or host_vars; set it once per group rather than per host.
The right user, but the public key is not in that user's ~/.ssh/authorized_keys on the host, so the server rejects every key offered.
Check
ssh -i ~/.ssh/<key> -o IdentitiesOnly=yes <user>@<address> # also denied: the key is not installed for that user
Fix
Install the public key for that user (ssh-copy-id, or ansible.posix.authorized_key run from a login that still works).
Ansible offers a different key than you think. ansible_ssh_private_key_file, ~/.ssh/config and ssh-agent all add keys, and a key file that is missing or passphrase-protected without an agent is silently skipped.
Check
ansible <host> -m ansible.builtin.ping -vvvv # the 'Offering public key' lines name each key tried; yours is missing or never offered
ssh-add -l # the agent's key fingerprints (no private key is printed)
Fix
Point ansible_ssh_private_key_file at the right key for that group, load it into the agent, and add -o IdentitiesOnly=yes to ansible_ssh_common_args when the agent holds many keys.
The key is installed but sshd refuses to use it: the user's ~/.ssh or authorized_keys has the wrong owner or is writable by others (StrictModes), or it is a DSA key, which OpenSSH 7.0 and later reject by default.
Check
sudo journalctl -u ssh -n 20 # on the host (sshd on RHEL): 'Authentication refused: bad ownership or modes' means permissions
Fix
On the host: make the user own ~/.ssh, then chmod 700 ~/.ssh and chmod 600 ~/.ssh/authorized_keys; replace a DSA key with an ed25519 key.
Prevent it
Set ansible_user once in group_vars/all; avoid per-host overrides
Bake the deploy user and key into your base image
Run ansible all -m ansible.builtin.ping in CI before a deploy
fatal: [db-02]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: connect to host 10.0.2.21 port 22: Connection timed out", "unreachable": true}
What it means
Ansible's SSH client sent a connection request to 10.0.2.21 port 22 and nothing answered before the timeout. No SSH conversation started, so users, keys and sudo are not involved yet: this is an address, network or firewall problem.
Causes, most likely first
The address is wrong (a typo or an old IP), so the packets go to a machine that is not there.
Check
ansible-inventory --host <host> # ansible_host: is it the host's current address?
Fix
Correct ansible_host, or use a dynamic inventory plugin so cloud addresses stay current.
A firewall or security group drops port 22 from the control node. A dropped packet gets no reply, so SSH waits until it times out.
Check
nc -vz -w 5 <address> 22 # times out: dropped on the way; 'refused': the host answered, see Connection refused
Fix
Allow port 22 from the control node's address only (security group or host firewall), not from 0.0.0.0/0.
SSH listens on another port, and nothing answers on 22.
Check
ansible-inventory --host <host> # no ansible_port, but the host uses a custom port
Fix
Set ansible_port for that host or group in the inventory.
The host is only reachable through a bastion. Your own ~/.ssh/config may have a ProxyJump that the CI runner does not.
Check
ansible <host> -m ansible.builtin.ping -vvvv # the ssh command line: no ProxyJump or ProxyCommand where one is needed
Fix
Set ansible_ssh_common_args: '-o ProxyJump=<user>@<bastion>' for that group in the inventory.
The host is down or has no route from here.
Check
ssh -v <user>@<address> # stops at 'Connecting to': nothing answered at all
Fix
Start the host or fix the route; then ansible.builtin.wait_for_connection before the next play.
Prevent it
Use a dynamic inventory plugin for cloud hosts so addresses never go stale
Keep ansible_port and bastion settings in the inventory, not in a personal ~/.ssh/config
fatal: [web-06]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: connect to host 10.0.1.16 port 22: Connection refused", "unreachable": true}
What it means
The host answered, but with a refusal: nothing accepts connections on that port. Unlike a timeout, the network path works; the problem is on the host or in the port number.
Causes, most likely first
sshd is stopped, not installed, or still starting after a reboot, so the kernel refuses the connection.
Check
sudo systemctl status ssh # on the host, from its console (sshd on RHEL): inactive or failed
Fix
Start it: sudo systemctl enable --now ssh (sshd on RHEL), or wait for it after a reboot with ansible.builtin.wait_for_connection.
SSH listens on another port and ansible_port is not set, so Ansible knocks on 22.
Check
ansible-inventory --host <host> # ansible_port missing or wrong
nc -vz <address> <port> # 'succeeded' on the real port
Fix
Set ansible_port with the host in the inventory.
A host firewall rejects (rather than drops) the connection, which also shows as refused.
Check
sudo firewall-cmd --list-services # on the host (RHEL family): ssh missing; 'sudo ufw status' on Ubuntu
Fix
Allow SSH from the control node's address in the host firewall.
ansible_host points at another machine that does not run SSH.
Check
ansible-inventory --host <host> # ansible_host: is it the address you expect?
Fix
Correct ansible_host.
Prevent it
After creating or rebooting hosts, wait with ansible.builtin.wait_for_connection before the next play
Keep ansible_port with the host in the inventory, not in a personal ~/.ssh/config
fatal: [web-03]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\r\n@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\r\n... Host key verification failed.", "unreachable": true}
What it means
The host sent an SSH host key that differs from the one stored for its address in known_hosts. SSH stops because it cannot tell a rebuilt host from an attacker in the middle; it never got as far as logging in.
Causes, most likely first
The host was rebuilt or reinstalled and generated a new host key. This is the usual reason, but confirm it before trusting the new key.
Check
ssh-keygen -F <address> # the stored key (grep can miss it: Debian and Ubuntu hash known_hosts)
ssh-keyscan <address> 2>/dev/null | ssh-keygen -lf - # the fingerprint offered now: matches the console or cloud boot log means a genuine rebuild
Fix
After the fingerprint matches: ssh-keygen -R <address> (and -R <name> if stored by name), then add the new key with ssh-keyscan <address> >> ~/.ssh/known_hosts.
A different machine now has that address (a reused IP, or a load balancer address shared by several hosts).
Check
ansible-inventory --host <host> # ansible_host: is this still the right machine?
Fix
Point the inventory at the right address; for shared addresses, connect to each host's own address.
Someone is intercepting the connection. If the fingerprint does not match what the console shows, assume this.
Check
ssh-keyscan <address> 2>/dev/null | ssh-keygen -lf - # does not match the console: do not connect
Fix
Do not replace the key; stop and investigate the network path.
Prevent it
Do not set host_key_checking = False: it removes protection for every host
Publish host keys from your image pipeline, or use SSH host certificates
fatal: [web-05]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Host key verification failed.", "unreachable": true}
What it means
SSH has no stored key for this host and would normally ask you to confirm the fingerprint. Ansible cannot answer that prompt, so SSH gives up. Nothing is wrong with the host; it is unknown to this control node.
Causes, most likely first
The host is new, or the CI runner starts with an empty known_hosts.
Check
ssh-keygen -F <address> # no output: SSH has never stored this host's key
Fix
Compare ssh-keyscan <address> 2>/dev/null | ssh-keygen -lf - with the fingerprint from the host's console, then append ssh-keyscan <address> to ~/.ssh/known_hosts.
The key is stored under another name. SSH looks up the address it connects to (ansible_host), not the inventory name.
Check
ssh-keygen -F <name> # found by name but not by address
Fix
Store the key under the address Ansible uses, or under both.
Ansible runs as another user (sudo ansible-playbook, a CI service account), so it reads a different ~/.ssh/known_hosts.
Check
whoami; echo ~ # the user and home directory Ansible runs with
Fix
Run Ansible as the user whose known_hosts holds the keys, or write that user's file from a reviewed list.
Prevent it
In CI, write known_hosts from a stored, reviewed file instead of disabling checking
For short-lived hosts, StrictHostKeyChecking=accept-new accepts a first key but still refuses a changed one
fatal: [db-03]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname db-03: Name or service not known", "unreachable": true}
What it means
SSH tried to turn the name db-03 into an address and DNS (or /etc/hosts) had no answer. No connection was attempted.
Causes, most likely first
The host has no ansible_host, so SSH uses the inventory name, and that name is not in DNS.
Check
ansible-inventory --host <host> # no ansible_host
Fix
Set ansible_host to the address, or add the name to DNS.
A typo in ansible_host or in the inventory name.
Check
getent hosts <name> # the exact name from the message; no output means it does not resolve here
Fix
Correct the name.
The name resolves only on another network or machine: a VPN, a private DNS zone, or an entry in your /etc/hosts or ~/.ssh/config that the CI runner does not have.
Check
ssh -G <name> | grep -i '^hostname' # what ~/.ssh/config turns the name into on this machine
Fix
Put the address in the inventory, or give the runner the same DNS (VPN, resolver).
Prevent it
Keep inventory names and DNS in one source, such as a dynamic inventory
The task uses become, sudo on the host asked for a password, and Ansible had none to give. The SSH login itself worked.
Causes, most likely first
No become password was supplied: neither -K nor ansible_become_password for this host. A vaulted value in a group the host is not in counts as none.
Check
ansible <host> -m ansible.builtin.debug -a 'msg={{ ansible_become_password is defined }}' # False: no password reaches this host (the value is not printed)
Fix
Run with -K (--ask-become-pass), or set ansible_become_password in Vault for the group the host is in.
sudo on that host requires a password for ansible_user, where you expected NOPASSWD.
Check
ssh <user>@<address> sudo -n true # 'a password is required': sudo wants a password for this user
Fix
Give the password as above, or add a reviewed NOPASSWD rule for a dedicated automation user.
A NOPASSWD rule exists but lists specific commands. Ansible runs /bin/sh -c ... through sudo, so a command-limited rule never matches and sudo asks for a password.
Check
ssh -t <user>@<address> sudo -l # NOPASSWD for some commands only, not ALL
Fix
Use a become password, or a rule Ansible can use for the automation user; sudo command lists do not work with Ansible's wrapper.
Prevent it
Decide per environment: NOPASSWD sudo for a dedicated deploy user, or a vaulted become password
Ansible started sudo (or another become method) and waited for either its password prompt or the success marker; neither came before the timeout. 12s is the connection timeout (10s by default) plus 2 seconds.
Causes, most likely first
sudo asks for a password Ansible does not send, or prints a prompt Ansible does not recognise, so Ansible keeps waiting.
Check
ansible <host> -m ansible.builtin.command -a whoami --become -vvvv # the sudo command and what came back before the timeout
ssh <user>@<address> 'sudo -n true' # 'a password is required': sudo wants a password
Fix
Give the password with -K or ansible_become_password from Vault, or allow NOPASSWD for the automation user.
sudo is slow to start because the host cannot resolve its own hostname, so it waits on DNS.
Check
ssh <user>@<address> 'getent hosts $(hostname)' # no output: the host cannot resolve its own name
Fix
Add the host's own name to /etc/hosts.
The host is overloaded and answers after the timeout.
Check
ssh <user>@<address> uptime # a very high load average
Fix
Raise the wait: timeout = 30 in the [defaults] section of ansible.cfg (or ANSIBLE_TIMEOUT=30).
The become method or user is wrong for this host (su where sudo is used, or a user that does not exist).
Check
ansible-inventory --host <host> # ansible_become_method and ansible_become_user
Fix
Set the method and user that host really uses.
Prevent it
Decide once per environment: NOPASSWD sudo for an automation user, or a become password in Vault
fatal: [web-07]: FAILED! => {"msg": "to use the 'ssh' connection type with passwords or pkcs11_provider, you must install the sshpass program"}
What it means
Ansible was asked to log in with an SSH password. Up to ansible-core 2.18 it does that with the sshpass program, which is not installed on the control node. From 2.19 the default is password_mechanism = ssh_askpass, which needs no sshpass; there the message reads to use the password_mechanism=sshpass, you must install the sshpass program and appears only when sshpass was chosen.
Causes, most likely first
A password login was requested on purpose, and sshpass is missing (ansible-core 2.18 or earlier, or password_mechanism = sshpass).
Check
command -v sshpass # on the control node: no output means not installed
ansible --version # the ansible-core version decides whether sshpass is needed
Fix
Better: log in with an SSH key. Otherwise install sshpass on the control node (sudo apt install sshpass, or sudo dnf install sshpass; on RHEL it comes from AppStream or EPEL depending on the release), or on 2.19+ remove the password_mechanism = sshpass setting.
ansible_password (or ansible_ssh_pass) is set somewhere you did not expect, such as group_vars, so Ansible tries a password login.
Check
grep -rlE 'ansible_password|ansible_ssh_pass' inventory group_vars host_vars # file names only, so no password is printed
Fix
Remove the password variable and use a key, or move it into Vault if you really need it.
Prevent it
Use keys for automation; keep any password that remains in Vault
Add sshpass to a CI image only when you really need password logins
fatal: [db-01]: FAILED! => {"msg": "Failed to set permissions on the temporary files Ansible needs to create when becoming an unprivileged user (rc: 1, err: chmod: invalid mode: 'A+user:postgres:rx:allow'\n...}). For information on working around this, see https://docs.ansible.com/ansible-core/2.18/playbook_guide/playbooks_privilege_escalation.html#risks-of-becoming-an-unprivileged-user"}
What it means
Ansible copied the module to a temporary file owned by the login user and needed to let the become user (here postgres) read it. It tries setfacl first, then fallbacks such as chmod +a (the macOS form, which is the A+user:... error you see), and every method failed.
Causes, most likely first
The host has no setfacl (the acl package), so the first and safest method fails and the fallbacks do too.
Check
ansible <host> -m ansible.builtin.command -a 'which setfacl' # rc 1 and no path: acl is not installed
Fix
Install the acl package on the host (for example in the base role).
Pipelining is off, so Ansible needs a temporary module file at all. Copy and template still transfer files even with pipelining.
MODULE FAILURE: sudo parse error in /etc/sudoers.d
Error
fatal: [web-01]: FAILED! => {"changed": false, "module_stderr": ">>> /etc/sudoers.d/monitoring: syntax error near line 1 <<<\nsudo: parse error in /etc/sudoers.d/monitoring near line 1\nsudo: no valid sudoers sources found, quitting\n", "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error", "rc": 1}
What it means
sudo found a syntax error in /etc/sudoers.d/monitoring and refuses to run at all, for every user. Ansible's become uses sudo, so every become task on that host fails until the file is repaired.
Cause
A file with a syntax error was written to /etc/sudoers.d without validation.
Check
ssh <user>@<address> sudo -n true # the same parse error
visudo -c # from the console or a root session: names the file and line
Fix
From the console or a root session, repair it with visudo -f /etc/sudoers.d/<file>, or remove the file.
Prevent it
Always deploy sudoers files with validate: visudo -cf %s
Could not match supplied host pattern / skipping: no hosts matched
Error
[WARNING]: Could not match supplied host pattern, ignoring: webservers
PLAY [Message of the day] ****
skipping: no hosts matched
What it means
The play's hosts: pattern (here webservers) matched no host in the inventory Ansible loaded, so the play was skipped. A warning, not an error: the run ends green although nothing ran.
Causes, most likely first
The play names a group that does not exist, or with different case (group names are case-sensitive).
Check
ansible-inventory --graph # the real group names
Fix
Use the real group name in hosts:, or add the group to the inventory.
Ansible used another inventory than you meant (-i, or the inventory setting of another ansible.cfg).
Check
ansible --version # config file = the ansible.cfg in use
ansible-playbook <playbook> --list-hosts # the hosts each play would target
Fix
Pass the right -i, or run from the project directory so its ansible.cfg is used.
The group lives in an inventory source that failed to parse, so it is missing.
Check
ansible-inventory --graph 2>&1 | grep -i 'unable to parse' # an earlier warning about that source
Fix
Fix that inventory source (see Unable to parse ... as an inventory source).
Prevent it
Treat warnings as failures in CI, and check the recap lists the hosts you expect
[WARNING]: Could not match supplied host pattern, ignoring: web01
ERROR! Specified inventory, host pattern and/or --limit leaves us with no hosts to target.
What it means
After applying --limit, the set of hosts was empty, so ansible-playbook stopped before running anything. The warning above it names the part of the limit that matched nothing.
Causes, most likely first
A typo in the host or group given to --limit.
Check
ansible-inventory --graph # the names that exist
Fix
Use a name from the inventory; preview with --list-hosts.
--limit matches inventory names, not addresses, so limiting by an IP fails unless the IP is the inventory name.
Check
ansible-inventory --host <name> # works only for an inventory name
Fix
Limit by the inventory name or a group.
The limit and the play's hosts: do not overlap: the host exists but is not in the play's group.
Check
ansible-playbook <playbook> --limit <host> --list-hosts # the play's host list is empty
Fix
Limit to a host in the play's group, or fix the host's group membership.
Prevent it
Always preview a limit with --list-hosts before changing anything
[WARNING]: Unable to parse /home/ops/project/inventory/hostz.ini as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
What it means
Ansible tried every enabled inventory plugin on the file and none could read it, so it fell back to the implicit localhost only. Every host pattern then matches nothing.
Causes, most likely first
The path given with -i, or the inventory setting in ansible.cfg, does not exist.
Check
ls -l <inventory path> # 'No such file or directory'
Fix
Point -i or ansible.cfg's inventory at the real file.
Ansible read a different ansible.cfg than you expected, with another inventory path.
Check
ansible --version # config file = which ansible.cfg was read
Fix
Run from the project directory, or set ANSIBLE_CONFIG to the project's ansible.cfg.
The file exists but no plugin accepts it: a YAML syntax error, YAML content in an .ini file, or a plugin config (such as aws_ec2.yml) whose collection is missing or whose name does not end the way the plugin requires.
Check
ansible-inventory -i <path> --graph -vvv # each plugin's reason for rejecting the file
Fix
Fix the syntax, or install the plugin's collection and name the file as its documentation requires (for example ending in aws_ec2.yml).
Prevent it
Keep ansible.cfg in the project and run from the project directory
Run ansible-inventory --graph in CI and fail on warnings
Ansible is being run in a world writable directory
Error
[WARNING]: Ansible is being run in a world writable directory (/mnt/project), ignoring it as an ansible.cfg source. For more information see https://docs.ansible.com/ansible/devel/reference_appendices/config.html#cfg-in-world-writable-dir
What it means
Ansible refuses to load an ansible.cfg from a directory every user can write to, because anyone could plant settings there. It falls back to ~/.ansible.cfg or /etc/ansible/ansible.cfg, so the project's inventory, roles path or remote user are silently not used.
Cause
The project sits on a shared mount where every directory is world writable (Vagrant shares, WSL's /mnt/c, some network file systems).
Check
ls -ld . # drwxrwxrwx means world writable
ansible --version # config file = shows which ansible.cfg is really in use
Fix
Remove world write with chmod o-w ., or move the project into your home directory on a Linux file system. If neither is possible, name the file explicitly: export ANSIBLE_CONFIG=$PWD/ansible.cfg.
Prevent it
Keep projects in your home directory, not on shared mounts
Print ansible --version in CI so the log shows which config was loaded
TASK [Run database migrations] does not appear in the output of ansible-playbook deploy.yml --tags deploy
What it means
With --tags, Ansible runs only tasks that carry one of those tags (or always) and skips the rest without a skipping line. A task that does not appear at all was filtered out by tags.
Causes, most likely first
The task has no tag, so --tags deploy leaves it out silently.
Check
ansible-playbook <playbook> --tags <tag> --list-tasks # the task is not listed
Fix
Tag the task, or tag it always when every run needs it.
Tags on include_tasks apply to the include statement only, not to the tasks inside it.
Check
ansible-playbook <playbook> --list-tags # the included tasks have no tags of their own
Fix
Use import_tasks, or apply: { tags: [...] } on the include_tasks.
The task is tagged never, which runs only when that tag is asked for by name.
Check
grep -rn -B3 'never' roles <playbook> # tags: [never, ...] on the task
Fix
Ask for its tag explicitly, or remove never.
Prevent it
Review --list-tasks for the exact tags your pipeline uses
ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each:
JSON: Expecting value: line 1 column 1 (char 0)
Syntax Error while loading YAML.
mapping values are not allowed in this context
What it means
The YAML parser found a key: value where it expected a plain value, so it cannot tell which mapping the line belongs to. The line and column it names are where it gave up; the mistake is often on the line above. On ansible-core 2.19 and later it reads YAML parsing failed: Mapping values are not allowed in this context.
Causes, most likely first
A line is indented deeper than its sibling keys, so YAML reads it as part of the previous value.
Check
ansible-playbook <playbook> --syntax-check # the line and column
cat -n <playbook> # compare that line's indent with the line above
Fix
Line up the module with name: of its task, and its options two spaces deeper.
A plain value contains : (colon and space), which YAML reads as a new key.
Check
grep -n ': .*: ' <playbook> # values with a second ': '
Fix
Quote the value: msg: "Status: ok".
Prevent it
Use an editor with YAML support and run yamllint and ansible-lint in CI
found unacceptable key (unhashable type: 'AnsibleMapping')
Error
Syntax Error while loading YAML.
found unacceptable key (unhashable type: 'AnsibleMapping')
...
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value.
What it means
A value starts with {{ without quotes, so YAML reads { ... } as an inline dictionary and the inner { x } as a dictionary used as a key, which is not allowed. On ansible-core 2.19 and later it reads This may be an issue with missing quotes around a template block.
Cause
A value that begins with {{ is not quoted.
Check
ansible-playbook <playbook> --syntax-check # the line and column
grep -nE ':\s+\{\{' <file> # unquoted values starting with {{
Fix
Quote the whole value: dest: "{{ app_dir }}/releases".
Prevent it
Quote every value that starts with {{; ansible-lint flags it
Syntax Error while loading YAML.
did not find expected key
What it means
Inside a mapping (or a list) the parser reached a line at an indentation it did not expect. On ansible-core 2.19 and later a list version reads did not find expected '-' indicator.
Causes, most likely first
A line is indented by an odd amount, between two levels.
Check
cat -n <file> # the named line does not line up with its siblings
ansible-playbook <playbook> --syntax-check
Fix
Make the line's indentation match its siblings.
A list item (-) where a key was expected, or a key where a list item was expected.
Check
cat -n <file> # a '-' at a level that holds keys, or the reverse
Fix
Make each level all keys or all list items.
Prevent it
Two spaces per level, consistently; yamllint in CI
Syntax Error while loading YAML.
found character that cannot start any token
What it means
The YAML parser met a character that no YAML token can start with. On ansible-core 2.19 and later a tab gives YAML parsing failed: Tabs are usually invalid in YAML.
Causes, most likely first
A tab character in indentation. YAML allows only spaces there.
Check
grep -nP '\t' <file> # every line with a tab
Fix
Replace the tabs with spaces.
A plain value starts with @ or a backtick, which YAML reserves.
Check
grep -nE ':\s+[@`]' <file> # values starting with @ or `
Fix
Quote the value: user: "@ops".
Prevent it
Configure the editor to insert spaces in YAML files
Ansible read a keyword it does not accept at that level: here when directly on a play. The name in quotes is the keyword, and the last word says where it was found (Play, Task, Block, Role).
Causes, most likely first
A task keyword (when, loop, register) used at play level.
Check
ansible-playbook <playbook> --syntax-check # the line it points at
Fix
Move the keyword to a task or block.
A tasks file (such as roles/web/tasks/main.yml) run as a playbook. Each task is read as a play, so the module name is reported: 'debug' is not a valid attribute for a Play.
Check
head -5 <file> # a list of tasks with no hosts:
Fix
Run the playbook that uses the role, or wrap the tasks in a play with hosts: and tasks:.
A play-level - include: other.yml, removed in ansible-core 2.16, gives 'include' is not a valid attribute for a Play.
Check
grep -n '^- include:' <playbook>
Fix
Use - import_playbook: other.yml.
A misspelt keyword (become_users, gather_fact) or a task indented out of its tasks: list.
Check
cat -n <playbook> # the keyword's spelling and indent
Fix
Fix the spelling or indentation.
Prevent it
ansible-lint's schema checks catch these before review
ERROR! couldn't resolve module/action 'community.general.ufw'. This often indicates a misspelling, missing collection, or incorrect module path.
What it means
While loading the playbook, Ansible looked for a module or action named community.general.ufw in every collection path and found none. Nothing has run yet.
Causes, most likely first
The collection that provides it is not installed on this control node (common on a fresh CI runner).
Check
ansible-galaxy collection list community.general # not listed
Fix
Add the collection to requirements.yml and run ansible-galaxy collection install -r requirements.yml.
The collection is installed, but where this ansible-core does not look: another virtualenv, another user's home, or a collections_path set in an ansible.cfg that was not loaded.
Check
ansible --version # which Python and config file this ansible uses
ansible-config dump | grep -i collections_path # the paths really searched
Fix
Install into one of the searched paths, or set collections_path in the project's ansible.cfg.
A typo, or a short name (ufw) for a module in a collection that is not installed.
Check
ansible-doc <module> # 'module not found'
Fix
Use the full name from the collection's documentation.
The module moved to another collection in a newer release (for example the Docker modules moved from community.general to community.docker).
Check
ansible-doc -l | grep <module> # the module under another collection name
Fix
Use the new full name and install that collection.
Prevent it
Pin every collection in requirements.yml and install from it in CI
The module received an option it does not have (here package_name). The message lists every option this installed version accepts; names in brackets are aliases.
Causes, most likely first
A misspelt or invented option.
Check
ansible-doc <module> # the options this version accepts
Fix
Use an option name ansible-doc lists.
An option from another module, or from a newer collection version than the one installed. The docs you read online may be for a newer release.
Check
ansible-galaxy collection list <namespace.collection> # the installed version
ansible-doc <module> # describes the installed version
Fix
Upgrade the collection in requirements.yml, or use the option the installed version has.
"module_stdout": "/bin/sh: 1: /usr/bin/python3: not found\r\n", "msg": "The module failed to execute correctly, you probably need to set the interpreter.\nSee stdout/stderr for the exact error", "rc": 127
What it means
Ansible ran its module with the Python interpreter it chose for the host, and the shell found no program at that path (rc 127). Most modules need Python on the host.
Causes, most likely first
The host has no Python at all (minimal and container images).
Check
ansible <host> -m ansible.builtin.raw -a 'command -v python3 || echo none' # 'none': no Python
Fix
Install it with raw: ansible <host> -m ansible.builtin.raw -a 'apt-get update && apt-get install -y python3' -b (dnf install -y python3 on the RHEL family).
ansible_python_interpreter (or interpreter_python in ansible.cfg) pins a path that does not exist on this host, such as /usr/bin/python where only python3 exists.
fatal: [db-01]: FAILED! => {"changed": false, "msg": "Failed to import the required Python library (psycopg2) on db-01's Python /usr/bin/python3. Please read the module documentation and install it in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter"}
What it means
The module itself ran, but it imports a Python library (here psycopg2) that the named interpreter on the named host does not have. The host and Python path in the message tell you exactly where it is missing.
Causes, most likely first
The library is not installed on the host the module runs on.
Check
ssh <user>@<address> "/usr/bin/python3 -c 'import psycopg2'" # ModuleNotFoundError: not installed for that Python
Fix
Install it in a task before the module runs, for example ansible.builtin.package: name=python3-psycopg2.
It is installed for another Python than the one Ansible uses on that host.
Check
ansible <host> -m ansible.builtin.setup -a 'filter=ansible_python' # the executable Ansible uses
Fix
Install it for that Python, or set ansible_python_interpreter to the Python that has it.
The module runs on localhost (most cloud modules). If localhost is listed in the inventory, Ansible discovers /usr/bin/python3 for it instead of the virtualenv ansible-playbook runs in.
Check
ansible localhost -m ansible.builtin.setup -a 'filter=ansible_python' # not your virtualenv's Python
Fix
Set ansible_python_interpreter: "{{ ansible_playbook_python }}" for localhost, and install the library in that virtualenv.
Prevent it
Install what a module needs in the same role that uses it
Read the Requirements section of each module's documentation
[WARNING]: Platform linux on host web-02 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.17/reference_appendices/interpreter_discovery.html for more information.
What it means
No interpreter was set for the host, so Ansible found one by itself and warns that installing another Python later could change its choice. The play works. On ansible-core 2.19 and later the wording is Host 'web-02' is using the discovered Python interpreter at ....
Cause
ansible_python_interpreter is not set and interpreter_python is auto, which warns.
Check
ansible <host> -m ansible.builtin.setup -a 'filter=ansible_python' # the Python it found
Fix
Set ansible_python_interpreter: /usr/bin/python3 in group_vars, or keep discovery without the warning: interpreter_python = auto_silent in the [defaults] section of ansible.cfg.
Prevent it
Set ansible_python_interpreter once per group in the inventory
fatal: [web-02]: FAILED! => {"changed": false, "failures": ["No package apache2 available."], "msg": "Failed to install some of the specified packages", "rc": 1, "results": []}
What it means
The package manager on the host searched its enabled repositories and found no package by that name. No package apache2 available. is the dnf/yum form; apt says No package matching 'apache2' is available.
Causes, most likely first
Package names differ between OS families (apache2 on Debian, httpd on RHEL), and the task runs on both.
Check
ansible <host> -m ansible.builtin.setup -a 'filter=ansible_os_family' # RedHat for a name from Debian, or the reverse
Fix
Load per-OS names with include_vars: "{{ ansible_facts['os_family'] }}.yml".
The repository that has it is not enabled: EPEL or CodeReady Builder on RHEL, an unregistered RHEL host with no repositories, or a missing PPA.
Check
ansible <host> -a 'dnf repolist' # RHEL family: the enabled repositories
ansible <host> -a 'apt-cache policy <package>' # Debian family: 'Candidate: (none)'
Fix
Enable the repository first (ansible.builtin.yum_repository, ansible.builtin.deb822_repository, or register the host).
The apt cache is stale, so a package added since the last update is unknown.
Check
ansible <host> -a 'stat -c %y /var/lib/apt/lists' # an old date
Fix
Add update_cache: true (with cache_valid_time) to the apt task.
A pinned version that the repository does not have.
Check
ansible <host> -a 'dnf list --showduplicates <package>' # or apt-cache madison <package>: the versions on offer
Fix
Pin a version the repository offers, or add the repository that has it.
Prevent it
Test roles on every OS you support (Molecule in CI)
The apt module ran apt-get update and it failed. unknown reason means the module could not classify the failure; the real reason is in apt's own output.
Causes, most likely first
A repository in /etc/apt/sources.list.d is wrong or gone, for example a PPA with no Release file for this Ubuntu release.
Check
ansible <host> -m ansible.builtin.command -a 'apt-get update' --become # 'does not have a Release file' names the repository
Fix
Remove or correct the bad repository (for example ansible.builtin.apt_repository with state: absent), then run again.
A repository's signing key is missing or expired.
Check
ansible <host> -m ansible.builtin.command -a 'apt-get update' --become # NO_PUBKEY or EXPKEYSIG
Fix
Install the key with the repository, for example ansible.builtin.deb822_repository with signed_by (ansible-core 2.15 and later).
The host's clock is wrong, so apt rejects Release files as not yet valid or expired.
Check
ansible <host> -a 'date -u' # far from the real time
Fix
Fix time sync (chrony or systemd-timesyncd), then update again.
The host cannot reach the mirror (proxy, DNS or firewall).
Check
ansible <host> -m ansible.builtin.command -a 'apt-get update' --become # 'Could not resolve' or 'Connection timed out'
Fix
Fix the proxy setting or network path to the mirror.
Prevent it
Manage every repository and key with Ansible, never by hand on one host
fatal: [web-01]: FAILED! => {"changed": false, "msg": "Failed to lock apt for exclusive operation: Failed to lock directory /var/lib/apt/lists/: E:Could not open lock file /var/lib/apt/lists/lock - open (13: Permission denied)"}
What it means
apt keeps lock files so only one apt or dpkg runs at a time. The module could not take the lock. 13: Permission denied means the task did not run as root; Resource temporarily unavailable or a lock held by process means another apt run has it.
Causes, most likely first
The task does not use become, so it runs as a normal user who may not open the lock file.
Check
The message says (13: Permission denied)
ansible <host> -m ansible.builtin.command -a whoami # not root
Fix
Add become: true to the task.
Another apt run holds the lock: unattended-upgrades, apt-daily, or cloud-init on a new host's first boot.
Check
ansible <host> -b -m ansible.builtin.command -a 'fuser -v /var/lib/dpkg/lock-frontend' # the process holding it (fuser is in psmisc)
ansible <host> -m ansible.builtin.command -a 'cloud-init status' # 'running' on a new host
Fix
Let apt wait: lock_timeout: 600 on the apt task (the default is 60 seconds); on new hosts, run cloud-init status --wait first. Never delete lock files or kill dpkg mid-run: it can leave packages half installed.
Prevent it
Use become per task that needs root; schedule unattended upgrades outside deploy windows
fatal: [web-01]: FAILED! => {"changed": false, "msg": "Could not find the requested service httpd: host"}
What it means
The service module asked the host's init system for the named service (here httpd) and it does not exist there. The trailing : host is part of the module's message, not a host name.
Causes, most likely first
The service name differs by OS (apache2 on Debian, httpd on RHEL).
Check
ansible <host> -a "systemctl list-unit-files 'httpd*' 'apache2*'" # the name this host uses
Fix
Use the right name per OS from a per-OS vars file.
The package that provides the unit is not installed, or failed to install.
Check
ansible <host> -a 'systemctl list-unit-files <name>.service' # 0 unit files listed
Fix
Install the package before the service task.
A unit file was just written, and systemd has not reloaded its configuration.
Check
ansible <host> -a 'systemctl cat <name>' # 'No files found' although the file exists
Fix
Use ansible.builtin.systemd with daemon_reload: true after writing unit files.
The host does not run systemd (many containers), so the module uses another backend that does not know the unit.
Check
ansible <host> -m ansible.builtin.setup -a 'filter=ansible_service_mgr' # not systemd
Fix
Start the process another way in containers, or test on a host with the same init system as production.
Prevent it
Keep package and service names in per-OS vars files
fatal: [web-01]: FAILED! => {"changed": false, "msg": "Unable to restart service app: Job for app.service failed because the control process exited with error code.\nSee \"systemctl status app.service\" and \"journalctl -xeu app.service\" for details.\n"}
What it means
The module asked systemd to (re)start the service and the service's own start command failed. Ansible only passes systemd's message on: the real reason is in the service's journal.
Causes, most likely first
The config file is invalid: literal {{ }} from copy instead of template, or a typo.
Check
ansible <host> -b -a 'journalctl -u <service> -n 30 --no-pager' # the service's own error, often naming the file and line
Fix
Fix the file (use template for .j2), then restart.
The service cannot read a file it needs (owner or mode).
Check
ansible <host> -m ansible.builtin.stat -a path=<config> # pw_name and mode: readable by the service user?
Fix
Set owner, group and mode on the file task.
SELinux denies the service a file or a port: a file copied with the wrong label, or a non-standard port.
Check
ansible <host> -b -a 'ausearch -m AVC -ts recent' # RHEL family: a denial naming the service
Fix
Fix the label (restorecon, or setype on the file task) or allow the port with community.general.seport. Do not disable SELinux.
The port is already in use by another process.
Check
ansible <host> -b -a 'ss -ltnp' # another program on the port
Fix
Stop the other process or change the port.
Prevent it
Use validate: on config templates (nginx -t -c %s, visudo -cf %s)
fatal: [web-01]: FAILED! => {"changed": false, ... "msg": "Status code was 503 and not [200]: HTTP Error 503: Service Unavailable", "status": 503, "url": "http://localhost:8080/healthz"}
What it means
The uri module requested the URL and got a status other than the one expected (status_code, 200 by default). -1 means no HTTP response at all, for example connection refused. The uri module runs on the managed host, so localhost in the URL is that host.
Causes, most likely first
The app is not running the new configuration yet: its restart handler runs only at the end of the play, after the check.
Check
ansible <group> -b -a 'systemctl status <service>' # started before the config change
Fix
Add meta: flush_handlers before the check.
A step the release needs was skipped, for example a migration left out by --tags.
Check
ansible-playbook <playbook> --tags <tag> --list-tasks # the step is missing
Fix
Tag the step so it runs, or run it, then check again.
The release itself is broken.
Check
ansible <group> -b -a 'journalctl -u <service> -n 30 --no-pager' # errors from the new version
Fix
Roll back to the previous version.
The check uses the wrong port or path.
Check
ansible <group> -m ansible.builtin.uri -a 'url=http://localhost:8080/healthz' # try the right port and path by hand
Fix
Correct the URL in the check.
Prevent it
Deploy with serial so a failing check stops the rollout after the first batch
fatal: [web-01]: FAILED! => {"changed": false, "cmd": "/usr/local/bin/lb-ctl disable web-01", "msg": "[Errno 2] No such file or directory: b'/usr/local/bin/lb-ctl'", "rc": 2}
What it means
The command module tried to start the program directly (no shell) and the operating system found no file at that path. rc: 2 is the errno, not the program's exit code: the program never started. On ansible-core 2.19 and later the message starts Error executing command: [Errno 2] ....
Causes, most likely first
The program does not exist on that host.
Check
ansible <host> -m ansible.builtin.stat -a path=<program> # exists: false
Fix
Install the program, or use the module that does the job.
The program is on another host, and the task should run there.
Check
ansible <other host> -m ansible.builtin.stat -a path=<program> # exists: true there
Fix
Add delegate_to: the host that has it.
The program is installed but not on the PATH of a non-interactive SSH session (for example /usr/local/bin or ~/.local/bin added only in .bashrc).
Check
ansible <host> -m ansible.builtin.shell -a 'command -v <program>; echo $PATH' # shell, because command -v is a shell builtin
Fix
Use the full path, or set environment: { PATH: ... } on the task.
The task uses pipes, redirects or &&, which the command module passes to the program as arguments, or the first word is a shell builtin.
Check
grep -n -A2 'command:' <file> # | > && or cd in the command
Fix
Use ansible.builtin.shell when you need shell features, or a module.
Could not find or access ... on the Ansible Controller
Error
fatal: [web-01]: FAILED! => {"changed": false, "msg": "Could not find or access 'app.conf.j2'\nSearched in:\n\t/home/ops/project/roles/app/templates/app.conf.j2\n\t/home/ops/project/roles/app/app.conf.j2\n\t/home/ops/project/templates/app.conf.j2\n\t/home/ops/project/app.conf.j2\n on the Ansible Controller.\nIf you are using a module and expect the file to exist on the remote, see the remote_src option"}
What it means
copy or template looked for src on the control node, in the order listed under Searched in:, and found it nowhere. Nothing reached the managed host.
Causes, most likely first
The file is not in the role's templates/ (for template) or files/ (for copy) directory.
Check
ls roles/<role>/templates roles/<role>/files # the file is elsewhere or missing
Fix
Move the file to one of the searched paths.
A typo in src, or a difference in case.
Check
Compare the name in the message with the ls output, letter by letter
Fix
Correct src.
The file is on the managed host, not the control node.
Check
ansible <host> -m ansible.builtin.stat -a path=<file> # exists: true on the host
Fix
Set remote_src: true on copy.
The path is relative to somewhere else than you think: the role, not the playbook or your shell's directory.
Check
Read the Searched in: list: those are the only places it looks
Fix
Give src relative to the role's files/ or templates/, or build it from role_path or playbook_dir.
Prevent it
Keep templates in the role's templates/ and static files in files/, and give src relative to them
A --check run in CI finds a missing src before a real deploy
The command ran and exited with a status other than 0, which Ansible treats as failure by default. rc is the program's exit code; the reason is in stderr and stdout of the result. On ansible-core 2.19 and later the message reads The command exited with a non-zero return code.
Causes, most likely first
The command really failed.
Check
ansible-playbook <playbook> -v # the full result: read stderr
Fix
Fix the command or its input.
The command uses a non-zero exit for a normal case (grep with no match returns 1, diff returns 1 when files differ).
Check
The tool's manual page: what each exit code means
Fix
Define success with failed_when: result.rc not in [0, 1].
It works in your terminal but not under Ansible: a non-interactive session has another PATH, HOME or locale, and no terminal.
Check
ansible <host> -m ansible.builtin.shell -a 'echo $PATH; id; locale' # compare with your interactive shell
Fix
Use full paths and set what it needs with environment: on the task.
Prevent it
Use block/rescue for steps that may fail and need a rollback
The task includes an option with an undefined variable
Error
fatal: [api-01]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'nginx_worker_connections' is undefined. 'nginx_worker_connections' is undefined\n\nThe error appears to be in ..."}
What it means
A task option uses a variable that has no value for this host, so Ansible cannot build the task. The name in quotes is the missing variable. On ansible-core 2.19 and later the message reads Error while resolving value for '<option>': '<name>' is undefined.
Causes, most likely first
The variable is defined for some groups only, and this host is not in them.
Check
ansible <host> -m ansible.builtin.debug -a var=<variable> # VARIABLE IS NOT DEFINED! ('is undefined' on 2.19+)
ansible-inventory --host <host> # inventory variables only: no role defaults, play vars or facts
Fix
Define it in the role's defaults/main.yml, or in group_vars/all.
A typo in the variable name, in the task or where it is set.
Check
grep -rn <variable> group_vars host_vars roles
Fix
Use one spelling.
It comes from a fact or a register that did not run on this host (gather_facts: false, a skipped task).
Check
ansible-playbook <playbook> -v # the registering task was skipped for this host
Fix
Make sure the task runs, or use | default(...) where a default makes sense.
The group_vars directory is next to neither the inventory nor the playbook, so Ansible never loads it.
Check
ls <inventory dir>/group_vars <playbook dir>/group_vars
Fix
Move group_vars next to the inventory file or the playbook.
Prevent it
Every role variable gets a default; consider argument_specs for roles
The error was: 'dict object' has no attribute 'stdout'. 'dict object' has no attribute 'stdout'
What it means
A template asked for a key (here stdout) that the dictionary does not have. On ansible-core 2.19 and later it reads object of type 'dict' has no attribute 'stdout'.
Causes, most likely first
A task registered in a loop stores one result per item under .results, not .stdout.
Check
ansible-playbook <playbook> # with a task: debug: var=<name>, shows a results list
Fix
Use <name>.results | map(attribute='stdout') | list, or loop over <name>.results.
with_items over a dictionary gives the whole dictionary as one item.
Check
debug: var=item inside the loop # one item holding every key
Fix
Use loop: "{{ my_dict | dict2items }}" with item.key and item.value.
A typo in the key name.
Check
debug: var=<name> # the keys it really has
Fix
Use the key the debug output shows.
Prevent it
Print registered variables once while writing the play
HostVarsVars object has no attribute 'ansible_default_ipv4'
Error
fatal: [lb-01]: FAILED! => {"changed": false, "msg": "AnsibleUndefinedVariable: 'ansible.vars.hostvars.HostVarsVars object' has no attribute 'ansible_default_ipv4'. ..."}
What it means
A template read hostvars[...] for another host and that host has no such fact in this run. Facts exist only for hosts that gathered them (or come from a fact cache).
Causes, most likely first
The run never gathered facts from those hosts (they are not in any play of this run, or gather_facts is false).
Check
ansible-playbook <playbook> --list-hosts # those hosts are in no play
Fix
Add a play that gathers facts from those hosts first, or use inventory values such as ansible_host.
They were excluded by --limit, so their facts were not gathered.
Check
The command line or CI job has --limit
Fix
Enable fact caching, or include those hosts in the limit.
Facts were gathered with a gather_subset that leaves the network facts out.
Check
ansible <host> -m ansible.builtin.setup -a 'filter=ansible_default_ipv4' # the fact exists when gathered fully
grep -rn gather_subset <playbook>
Fix
Include network in gather_subset for that play.
Prevent it
Do not read hostvars facts of hosts outside the run
fatal: [web-01]: FAILED! => {"msg": "Invalid data passed to 'loop', it requires a list, got this instead: {'alice': {'shell': '/bin/bash'}}. Hint: If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup."}
What it means
loop: must be given a list, and it received something else (here a dictionary). On ansible-core 2.19 and later it reads: The loop value must resolve to a 'list', not 'dict'.
Causes, most likely first
The variable holds a dictionary.
Check
ansible <host> -m ansible.builtin.debug -a var=<variable> # { } braces: a dictionary
Fix
Use loop: "{{ my_dict | dict2items }}" with item.key and item.value.
The variable holds a string, such as a comma-separated list, or a lookup returned a string.
Check
ansible <host> -m ansible.builtin.debug -a var=<variable> # quotes around the whole value: a string
Fix
Store a real YAML list, use query() instead of lookup(), or split(',').
Prevent it
Keep lists as lists in group_vars; convert dicts explicitly
when: treats any non-empty string as true, and -e key=value always passes strings, so false arrives as the string 'false'. On ansible-core 2.19 and later the task fails instead: Conditionals must have a boolean result.
Causes, most likely first
The value came from -e key=value, which passes strings.
Check
ansible <host> -m ansible.builtin.debug -a 'var=<variable>' -e <variable>=false # shows "false" in quotes: a string
Fix
Use when: <variable> | bool, or pass JSON: -e '{"<variable>": false}'.
A quoted 'false' or 'no' in YAML is a string too.
Check
grep -rn "<variable>: ['\"]" group_vars host_vars roles # a quoted value
Fix
Remove the quotes, or use | bool.
Prevent it
Filter every externally supplied boolean with | bool
ansible-inventory --host web-02 shows "app_version": "2.1" while group_vars/web.yml says "2.4"
What it means
Ansible merges variables from many places, and the strongest source wins for each host. A green run with the wrong value means another source beat the one you edited.
Causes, most likely first
A stronger source wins: host_vars beat group_vars, role vars/ beat inventory, set_fact beats both, and -e beats everything.
Check
ansible <host> -m ansible.builtin.debug -a var=<variable> # the value the host really gets (outside a role)
grep -rn <variable> inventory group_vars host_vars roles # every place it is set
Fix
Remove the stronger value, or set yours at a level that wins; move role tunables from vars/ to defaults/.
group_vars next to the playbook beat group_vars next to the inventory, and ansible-inventory does not read them unless given --playbook-dir.
Check
ansible-inventory --host <host> --playbook-dir <playbook dir> # differs from the same command without --playbook-dir
Fix
Keep each variable in one of the two places.
Two groups at the same depth set it; the one whose name sorts last wins unless ansible_group_priority says otherwise.
Check
ansible <host> -m ansible.builtin.debug -a var=group_names # the groups it is in
Fix
Set it in one group, or set ansible_group_priority.
A stale override left in host_vars.
Check
ls host_vars/<host>* # a file you forgot
Fix
Delete the stale value.
Prevent it
Set each variable in one place; keep role tunables in defaults/main.yml
fatal: [web-01]: FAILED! => {"msg": "template error while templating string: Could not load \"to_nice_yml\": 'to_nice_yml'. String: {{ app_config | to_nice_yml }}. Could not load \"to_nice_yml\": 'to_nice_yml'"}
What it means
Jinja2 could not parse or run an expression. Could not load "to_nice_yml" (ansible-core 2.15 to 2.18) or No filter named 'to_nice_yml' (older releases and 2.19+) means no filter by that name is installed here.
Causes, most likely first
A misspelt filter or test (the filter is to_nice_yaml, not to_nice_yml).
Check
ansible-doc -t filter -l | grep <name> # no line: no filter by that name here
Fix
Correct the name.
A filter from a collection that is not installed, or used without its full name, such as community.general.json_query.
Check
ansible-galaxy collection list # the collection is missing
Fix
Install the collection and use the filter's full name.
Unbalanced braces or quotes in the expression.
Check
The String: part of the message: count the {{ and }}
Fix
Close every {{ with }} and quote the whole value in YAML.
Prevent it
List the collections you use in requirements.yml and install them in CI
Run the play with --check in CI so template errors show before a deploy
fatal: [localhost]: FAILED! => {"msg": "The 'file' lookup had an issue accessing the file 'files/deploy_key.pub'. file not found, use -vvvvv to see paths searched"}
What it means
Lookups run on the control node, so lookup('file', ...) looked for the file there and did not find it. ansible-core 2.15 to 2.18 print the message above, older releases could not locate file in lookup, and 2.19 and later The lookup plugin 'file' failed: Unable to access the file.
Causes, most likely first
The file is on the managed host, but lookups only read the control node.
Check
ls -l <file> # on the control node: not there
Fix
To read a file that is on the managed host, use ansible.builtin.slurp instead of a lookup.
A relative path is searched in the role's or playbook's files/ directory, not in your shell's current directory.
ERROR! The requested handler 'reload nginx' was not found in either the main handlers list nor in the listening handlers list
What it means
A task with notify: changed something, and no handler has that exact name or a matching listen:, so Ansible stops. The change it made stays on the host.
Causes, most likely first
notify does not match a handler name exactly (case and spaces matter).
Check
grep -rn -i '<name>' <playbook> roles/*/handlers # the handler's real spelling
Fix
Make notify match the handler's name, or add listen: <name> to the handler.
The handler is in a role that this play does not load.
Check
grep -rln '<name>' roles/*/handlers # the role it lives in
Fix
Add the role to the play, or move the handler.
Prevent it
Use listen: topics for handlers several tasks notify
After fixing, reload once by hand: the changed file will not notify again
Handler did not run: service still uses old config
Error
TASK [Write nginx.conf] ****
ok: [web-01] (no RUNNING HANDLER)
What it means
Handlers run only when a notifying task reports changed, and only at the end of the play (or at meta: flush_handlers). Here the task reported ok, so nothing was notified.
Causes, most likely first
A previous run changed the file and failed before handlers ran. This run sees the file already right, reports ok, and notifies nothing.
Check
ansible <host> -b -a 'systemctl show <service> -p ActiveEnterTimestamp' # started before the file changed
ansible <host> -m ansible.builtin.stat -a path=<file> # mtime newer than the start time
Fix
Restart or reload once by hand.
Handlers run at the end of the play, after the tasks that needed the change.
Check
ansible-playbook <playbook> -v # RUNNING HANDLER only after the last task
Fix
Add meta: flush_handlers where later tasks need the change.
The notifying task has changed_when: false, so it never reports changed.
In check mode Ansible asks each module to predict its change without making it. command and shell cannot predict, so they are skipped, and --check --diff shows nothing for them.
Causes, most likely first
The change is made by a command or shell task.
Check
ansible-playbook <playbook> --check --diff -v # 'Command would have run if not in check mode' or skipping
Fix
Manage the state with a module (lineinfile, template) that supports check and diff.
A later task reads the skipped task's register and fails or misleads.
Check
ansible-playbook <playbook> --check -v # a later failure about the register
Fix
Set check_mode: false on read-only commands that later tasks depend on.
Prevent it
Prefer modules; set check_mode: false only on read-only commands
ERROR! Attempting to decrypt but no vault secrets found
What it means
Ansible met vault-encrypted data and had no vault password at all to try. The data is fine; no password reached this run.
Causes, most likely first
No vault password was given: no -J (--ask-vault-password), --vault-password-file, --vault-id or vault_password_file setting.
Check
ansible-config dump | grep -i vault # DEFAULT_VAULT_PASSWORD_FILE(default) = None and an empty DEFAULT_VAULT_IDENTITY_LIST
Fix
Pass --vault-password-file <file> or -J, or set vault_password_file in the project's ansible.cfg.
vault_password_file is set in an ansible.cfg that was not loaded (another directory, a world writable directory), or ANSIBLE_VAULT_PASSWORD_FILE is set in your shell but not in CI.
Check
ansible --version # config file = which ansible.cfg was read
Fix
Run from the project directory, or set the variable in the CI job.
Prevent it
Set vault_password_file in ansible.cfg to a file outside the repository, or a script that reads your secret store
Decryption failed (no vault secrets were found that could decrypt)
Error
ERROR! Decryption failed (no vault secrets were found that could decrypt) on /home/ops/project/inventory/group_vars/all/vault.yml
What it means
Ansible had one or more vault passwords and none of them opened this file. The file is named at the end of the message.
Causes, most likely first
The wrong password or password file.
Check
ansible-vault view <file> --vault-password-file <password file> > /dev/null && echo opens # tests the password without printing the secrets
Fix
Use the right password.
The file was rekeyed, or encrypted with another vault id's password.
Check
head -1 <file> # $ANSIBLE_VAULT;1.2;AES256;prod: encrypted for the prod vault id
Fix
Pass --vault-id <label>@<file> for each password the run needs.
The password file is executable, so Ansible runs it as a script and uses what it prints; a script that prints the wrong value fails here. (A plain file with the x bit fails earlier with Problem running vault password script.)
Check
ls -l <password file> # an x in the mode: it is run, not read
Fix
Fix the script, or chmod -x a plain password file.
Prevent it
Use vault ids per environment (dev, prod) so files say which key they need
ERROR! input is not vault encrypted data. group_vars/all.yml is not a vault encrypted file for group_vars/all.yml
What it means
ansible-vault view, edit and decrypt work on whole encrypted files, and this file has no $ANSIBLE_VAULT header. On ansible-core 2.19 and later it reads Failed to view 'group_vars/all.yml': Input is not vault encrypted data.
Causes, most likely first
The file is plain text, perhaps decrypted and committed by mistake.
Check
head -1 <file> # no $ANSIBLE_VAULT;1.1;AES256 line
Fix
Encrypt it: ansible-vault encrypt <file>; rotate any secret that was committed in plain text.
The file is plain YAML holding inline encrypted values (!vault | from encrypt_string). ansible-vault view and edit do not open those.
Check
grep -c '!vault' <file> # a number above 0: inline values
Fix
Leave the file as it is; to change a value, make a new one with ansible-vault encrypt_string and replace it.
Prevent it
A pre-commit hook that rejects unencrypted files named vault*
mode: 644 without a leading zero sets odd permissions
Error
TASK [Write the config] ****
changed: [web-01] (the task sets mode: 644)
$ ls -l /etc/app/app.conf
--w----r-T 1 root root 312 Oct 30 10:02 /etc/app/app.conf
What it means
YAML reads an unquoted mode: 644 as the decimal number 644, which is 1204 in octal: the sticky bit plus -w----r--. Ansible applies that, so ls -l shows --w----r-T.
Cause
An unquoted three-digit mode without a leading zero.
Check
stat -c '%a %A %n' <path> # on the host: 1204 --w----r-T
grep -rnE 'mode: [0-7]{3}\s*$' roles playbooks # every unquoted mode
Fix
Quote it with a leading zero, mode: "0644", or use a symbolic mode such as u=rw,g=r,o=r, then run the play again to correct the files it already wrote.
ERROR! [DEPRECATED]: ansible.builtin.include has been removed. Use include_tasks or import_tasks instead. This feature was removed from ansible-core in a release after 2023-05-16. Please update your playbooks.
What it means
The old include: action was removed in ansible-core 2.16. The message depends on how it was written: include: file=x.yml gives the text above, the more common include: x.yml gives this task 'include' has extra params, which is only allowed in the following modules: ... on 2.16 to 2.18, and 2.19 and later say The 'ansible.builtin.include' action plugin has been removed.
Causes, most likely first
Tasks still use include: after an upgrade to ansible-core 2.16 or later.
Check
ansible --version # core 2.16 or later
grep -rnE '^\s*-?\s*(ansible\.builtin\.)?include:' . # every old include
Fix
Replace include: with import_tasks: (static) or include_tasks: (dynamic).
A play-level - include: other.yml that pulls in a playbook.
Check
grep -n '^- include:' *.yml
Fix
Use - import_playbook: other.yml.
Prevent it
Pin ansible-core in CI, read the porting guide, and act on deprecation warnings
ERROR! Failed to resolve the requested dependencies map. Could not satisfy the following requirements:
* community.genral:* (direct request)
What it means
ansible-galaxy could not find a set of collection versions that satisfies requirements.yml. The lines after it name each requirement it could not meet and where it came from.
Causes, most likely first
A typo in the collection name (here community.genral).
Check
cat requirements.yml # the name as written
Fix
Correct the name.
A version constraint that no release satisfies, or two collections that need conflicting versions of a dependency.
Check
ansible-galaxy collection install -r requirements.yml -vvv # the versions it found and the conflict
Fix
Loosen or correct the constraint.
The collection is on Automation Hub or a private Galaxy that server_list does not include, or the control node cannot reach the server (proxy, certificate).
Check
ansible-config dump | grep -i galaxy_server # the servers it asks
Fix
Add the server to [galaxy] server_list, or fix the proxy or CA settings.
Prevent it
Pin exact versions and mirror collections for air-gapped CI
Playbook run took 0 days, 0 hours, 53 minutes, 20 seconds
What it means
Ansible runs each task across hosts in batches of forks (5 by default) and waits for the batch, so a big fleet with defaults spends most of its time waiting.
Causes, most likely first
forks is 5 by default, so hosts are handled five at a time.
One of ansible-playbook's worker processes on the control node exited unexpectedly, so the run aborts. It is about the control node, not the managed hosts.
Causes, most likely first
forks is set higher than the control node's memory allows, and the kernel's out-of-memory killer ended a worker.
Check
sudo dmesg | grep -i 'killed process' # on the control node: an ansible-playbook process
Fix
Lower forks, or give the control node more memory.
A module or plugin crashed the worker.
Check
ansible-playbook <playbook> -vvv # the last task before the abort
Fix
Upgrade or fix that plugin.
Prevent it
Size forks to the control node; run large fleets from several nodes
fatal: [db-01]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: client_loop: send disconnect: Broken pipe", "unreachable": true}
What it means
The SSH connection was open and then broke while Ansible was still using it. Something between the hosts, or the host itself, ended the session.
Causes, most likely first
A firewall or NAT gateway drops SSH sessions that look idle during a long task.
Check
ssh -o ServerAliveInterval=30 <user>@<address> 'sleep 600' # survives with keepalives, fails without
Fix
Add keepalives with ansible_ssh_common_args: '-o ServerAliveInterval=30' (ssh_args replaces the defaults, so keep ControlMaster and ControlPersist in it if you use that), or run the task with async and poll.
The host rebooted or ran out of memory.
Check
ansible <host> -a uptime # a very short uptime: it rebooted
ansible <host> -b -a 'journalctl -k -n 50 --no-pager' # out of memory messages
Fix
Find why it rebooted or ran out of memory; use ansible.builtin.reboot for planned reboots.
fatal: [web-01]: FAILED! ...
fatal: [web-02]: FAILED! ...
NO MORE HOSTS LEFT
What it means
Without serial, each task runs on every host before the next task, so a bad release reaches all hosts before any health check can stop it. NO MORE HOSTS LEFT means every host in the play has failed.
Causes, most likely first
The play has no serial, so there is no batch to stop after.
Check
grep -n serial <playbook> # no output
Fix
Roll back with -e version=<previous>, then add serial: 1 (or a percentage) and max_fail_percentage.
No rollback when a step fails.
Check
grep -n rescue <playbook> # no output
Fix
Wrap the deploy in block/rescue that restores the previous release.
Prevent it
Rolling updates with serial, health checks after restarts, and block/rescue rollbacks