Chapter 11: Configuration, Plugins & Performance
Topic 63

ansible.cfg

ConfigPrecedence

ansible.cfg is where you set the defaults that would otherwise be a wall of command-line flags and environment variables on every run — the inventory path, the SSH user, how many hosts to hit in parallel, whether to verify host keys. Ansible looks for it in four places and the first one found wins outright, with no merging across files, so a config sitting one directory up can silently override the one you think is in effect.

Knowing the search order is the difference between "my setting isn't taking" and a five-minute fix. There is no layering to reason about, no precedence stack to merge in your head — exactly one file is read per run, and the whole skill is knowing which one and how to confirm it. The settings that matter most shape every run, so you put them in the repo once and let the CI runner and every engineer pick up the same behavior.

The Four-Place Search Order

Ansible checks four locations in a fixed order and stops at the first hit. ANSIBLE_CONFIG, an environment variable naming an explicit file, beats ./ansible.cfg in the current working directory, which beats ~/.ansible.cfg in the home directory, which beats /etc/ansible/ansible.cfg. The first file found is the only file read — settings do not layer across them. If the repo's cfg is found, the system-wide one at /etc/ansible is never even opened.

Config search order — the first file found wins whole, no merging
ANSIBLE_CONFIG env var
./ansible.cfg in cwd
~/.ansible.cfg
/etc/ansible/ansible.cfg

This is the single most useful fact about the file. People reason about it as if a setting in ~/.ansible.cfg would merge with the repo's ansible.cfg and fill in the gaps — it does not. One file wins whole. A setting you expect to inherit from a "lower" file is simply absent if a "higher" file was found first and didn't define it, which is where most "why isn't my setting applying" confusion comes from.

The World-Writable cwd Rule

There is one trap inside that order. Ansible ignores an ./ansible.cfg if the current directory is world-writable, because a config in a directory anyone can write to is an injection vector — someone could drop a malicious cfg and have your run pick it up. The cfg is skipped silently and you fall through to the next location in the search order. This trips people running playbooks from /tmp, which is world-writable by design, where the local cfg you placed is quietly ignored and the run uses unexpected defaults.

Key Connection Settings

A handful of settings shape every run, and they belong in the repo's cfg rather than repeated on the command line. Under [defaults] you set inventory (the host file or directory), remote_user (the SSH user), host_key_checking (whether to verify host keys), and forks — parallelism, defaulting to 5. Under [ssh_connection] you set pipelining, the single biggest performance win, covered three topics on. Set once in the repo, these give everyone identical behavior with no flags to remember.

A repo-root ansible.cfg for the Larkspur infra repo
[defaults]
inventory = inventories/prod
remote_user = deploy
host_key_checking = true
forks = 20

[ssh_connection]
pipelining = true

Every value here would otherwise be an -i, a -u, a -f, or an exported environment variable on every invocation. Pinning them in the file at the repo root means the deploy command is just ansible-playbook site.yml and behaves identically whether a teammate runs it locally or the GitHub Actions runner runs it in CI.

Repo-Local Config as the Norm

The pattern that makes all of this predictable is shipping the cfg in the repo and running from the repo root. The larkspur-io/infra repo carries its own ansible.cfg at the root, pointing inventory = inventories/prod and setting forks = 20. Because the CI runner checks out the repo and runs from that directory, the ./ansible.cfg location wins, and every engineer who clones the repo gets the same inventory and the same parallelism without exporting anything or remembering any flags. The config travels with the code, reviewed in the same pull requests.

Inspecting the Effective Config

When a setting doesn't seem to apply, two commands answer "which file won and what is it setting" without guessing. ansible --version prints the config file actually in use at the top of its output — read that line and you know which of the four locations was selected. ansible-config dump --only-changed then shows every setting that differs from the built-in default, so you see exactly what the live file changes rather than scrolling the full default dump. Run both before you start debugging a behavior you can't explain.

Common Mistakes
  • Running ansible-playbook from a different directory than expected and picking up ~/.ansible.cfg or /etc/ansible/ansible.cfg instead of the repo's cfg, so forks and inventory quietly revert to defaults and the deploy is slow or hits the wrong hosts.
  • Expecting settings from two ansible.cfg files to merge — they do not; the first file found is read whole and the rest are ignored, so a single setting in ~/.ansible.cfg shadows the entire repo config if the repo one isn't found first.
  • Keeping a deploy script that cds into /tmp (world-writable) before running, so the ./ansible.cfg there is silently skipped and the run uses unexpected defaults with no warning.
  • Setting host_key_checking = false globally in ~/.ansible.cfg to "fix" a prompt, then having no host-key verification anywhere — a setting meant for one throwaway box now disarms MITM protection on prod.
  • Committing secrets like a vault_password_file path or an absolute home path into the repo ansible.cfg, so the config only works on one machine and leaks layout into version control.
Best Practices
  • Ship an ansible.cfg at the repo root and run from the repo root, so the cwd config wins and every engineer plus the CI runner share one source of defaults.
  • Confirm which config is live with ansible --version and audit non-default settings with ansible-config dump --only-changed before debugging "my setting isn't applying."
  • Keep host_key_checking = true and manage known-hosts deliberately rather than disabling verification fleet-wide to silence a prompt.
  • Never run playbooks from a world-writable directory like /tmp; the repo-local cfg is ignored there and you lose your intended defaults without a warning.
  • Keep secrets and machine-specific paths out of the committed cfg, so the same file works for everyone and version control carries no home-directory layout.
Comparable tools Terraform .terraformrc / TF_CLI_CONFIG_FILE, the provisioning-side analog SSH ~/.ssh/config overlaps for connection defaults Puppet puppet.conf plays the same role for that tool

Knowledge Check

A repo has an ./ansible.cfg and the home directory has a ~/.ansible.cfg. You run a playbook from the repo root with no ANSIBLE_CONFIG set. Which config is used, and how do the two combine?

  • Only ./ansible.cfg is read — the first file found wins whole, and the home-directory file is ignored entirely
  • The two files are deep-merged, with each ./ansible.cfg value overriding the matching home-directory one key by key
  • Only ~/.ansible.cfg is read, because the home directory sits higher in the precedence order than the current working directory
  • Both files are read in full and any setting that conflicts between the two raises a startup error until you resolve it

A deploy script cds into /tmp, which holds a carefully written ansible.cfg, then runs the playbook. The settings in that file don't take effect. Why?

  • /tmp is world-writable, so Ansible silently ignores its ./ansible.cfg as an injection risk and falls through to the next location
  • Ansible only honors an ansible.cfg when the current directory is named after the project repository, and /tmp fails that name check
  • A cfg located under /tmp is hard-coded to always sit at lower precedence than the system-wide /etc/ansible/ansible.cfg
  • Config files that live outside an initialized git repository working tree are never read by Ansible at all

A setting you expect isn't applying and you want to know which config file Ansible actually loaded. What tells you?

  • ansible --version prints the config file in use, and ansible-config dump --only-changed shows what it changes from defaults
  • ansible-playbook --check prints the active config file path as a header line at the very top before it runs the whole play through in check mode
  • There is no command that reports it; you must walk the search order and compare each candidate file by hand
  • ansible-inventory --list dumps the resolved data and names which cfg file Ansible ultimately selected

Someone sets host_key_checking = false in ~/.ansible.cfg to stop a prompt on one throwaway box. What is the consequence?

  • Host-key verification is now off for every run that uses that file, removing MITM protection across the fleet — including prod
  • Only the one throwaway box skips verification, since the setting is automatically scoped to the single host that triggered the prompt
  • Nothing changes at all, because host_key_checking is only honored when it is set in the repo-level cfg, never the home one
  • Ansible refuses to start any run until the setting is moved out of [defaults] and into the [ssh_connection] section

You got correct