skip to content

Ansible

Agentless configuration management over SSH: inventories of hosts, playbooks of idempotent tasks, roles for reuse, and Vault for secrets. Interviewers position it against Terraform — provisioning the box versus configuring what runs on it.

on this pageshow

questions

page 1 of 2

What is an Ansible inventory, and what do the built-in `all` and `ungrouped` groups contain?

level: juniorimportance: must knowfreq 80%

answer

  1. Ansible cannot discover hosts by itself
  2. INI or YAML, or a dynamic source
  3. two groups you never declare
  4. every host belongs to one of them
  5. the other holds hosts with no group

basics

~20 s

An Ansible inventory is the list of managed hosts and the groups holding them, written as static INI or YAML files or produced by a dynamic plugin. Every host is automatically in the group all; a host in no other group is also in ungrouped.

solid answer

~40 s

Ansible is agentless, so it has no way to discover machines — the inventory is the only thing that tells it which hosts exist. It can be a static INI or YAML file, a directory of such files that Ansible merges, or a dynamic plugin that queries a cloud API. The inventory names hosts, arranges them into groups and groups-of-groups via `children`, and can attach variables to a host or a group. Two groups always exist without being declared: `all`, which holds every host in the inventory, and `ungrouped`, which holds hosts that belong to no group other than `all`. Plays target those names — `hosts: webservers`, `hosts: all` — so the inventory is what decides blast radius. Before running anything against an unfamiliar inventory I check it with `ansible-inventory -i <source> --graph`.

code

ini · 13 lines
ini
[webservers]
web01 ansible_host=10.0.1.11
web[02:05].example.com

[dbservers]
db01 ansible_host=10.0.2.21

[prod:children]
webservers
dbservers

[prod:vars]
ntp_server=time.internal

go deeper

for a junior

Be able to write a small inventory in both INI and YAML, put hosts into a group, and say that all covers every host while ungrouped covers hosts with no group.

for a middle

Explain group nesting with children, host ranges, where variables can be attached, and how multiple inventory sources or a directory get merged into one host set.

for a senior

Show that you treat the inventory as the blast-radius control: it lives in the repository, is reviewed, and is verified with ansible-inventory --graph or --list-hosts before a production run.

for a principal

Own the estate-wide question of what an inventory is allowed to contain — whether one inventory may ever address more than one environment, and who reviews changes to it.

## Why an inventory exists Ansible installs no agent. It connects outward, usually over SSH, runs a module and disconnects, which means it cannot discover infrastructure on its own. The inventory is the single source of "which machines may this run touch", and that is why interviewers treat it as a blast-radius question rather than a file-format question. An inventory source can be a static file, a directory of files that Ansible parses and merges, a dynamic inventory plugin configured by a YAML file, or an executable script that prints JSON. You point at it with `-i` (repeatable), or set `inventory` in `ansible.cfg`. The historical default is `/etc/ansible/hosts`, which real projects almost never use because inventory belongs in the repository next to the playbooks. ## The INI format ```ini [webservers] web01 ansible_host=10.0.1.11 web[02:05].example.com [dbservers] db01 ansible_host=10.0.2.21 [prod:children] webservers dbservers [prod:vars] ntp_server=time.internal ``` Three section shapes matter: a bare `[group]` lists hosts, `[group:vars]` attaches variables to the whole group, and `[group:children]` makes a group of groups. `web[02:05].example.com` is a host range, expanding to four hosts. Inline `key=value` after a host name sets host variables. ## The YAML format ```yaml all: children: webservers: hosts: web01: ansible_host: 10.0.1.11 dbservers: hosts: db01: {} prod: children: webservers: {} dbservers: {} vars: ntp_server: time.internal ``` YAML expresses the same model with real nesting and typed values, and it is what most teams standardise on. Note the shape: a group has `hosts:` and/or `children:` and optionally `vars:`. ## Groups, nesting and membership A host may be in many groups, and group membership is transitive upward: a host in `webservers` is also in `prod` when `prod` lists `webservers` as a child. That is what makes `hosts: prod` work without repeating hosts. Groups are just labels — they carry no ordering or ownership beyond variable resolution. ## The implicit groups `all` is created for you and contains every host the inventory produced, including hosts that came from a dynamic plugin. `ungrouped` contains hosts that ended up in no group other than `all` — usually hosts listed at the top of an INI file before any `[section]` header. Seeing hosts in `ungrouped` when you expected them in `webservers` is a fast signal that a section header is missing or misspelled. One special case surprises people: Ansible provides an *implicit localhost* with `ansible_connection=local`, so `hosts: localhost` works even when localhost is nowhere in the inventory. That implicit entry is not matched by `all` or `*`; you must name it. ## Verifying before you run ```bash ansible-inventory -i inventories/prod --graph ansible-inventory -i inventories/prod --list --yaml ansible-playbook -i inventories/prod site.yml --list-hosts ``` `--graph` prints the group tree with hosts under it, `--list` dumps hosts with their resolved variables, and `--list-hosts` on a playbook shows exactly which hosts a play would target. Running these first is the cheap habit that prevents "I thought that group was empty". ## What goes wrong in practice Hosts silently landing in `ungrouped`; a group name typo so `hosts: webserver` matches nothing; assuming `all` includes the control node; and treating the inventory as documentation rather than as the executable definition of blast radius. In a real repository the inventory is reviewed as carefully as the playbooks, because adding one line to the wrong group is enough to reconfigure a production database.

  • If a host appears in two different files inside one inventory directory, what does Ansible do?
    It merges them. Pointing `-i` at a directory makes Ansible parse every file in it and combine the results, so the same host can be declared in one file and added to another group in a second file. That is how teams split a static core inventory from a dynamic cloud source while still targeting one merged set of groups.
  • Why do most teams avoid the default `/etc/ansible/hosts` inventory?
    Because it lives outside the repository, so it is not reviewed, not versioned, and differs between the laptop and the CI runner. Inventory decides which machines a run touches, so it belongs beside the playbooks under `-i inventories/<env>` or an explicit `inventory` setting in a project-local `ansible.cfg`.
  • How do you confirm which hosts a play will actually target before running it?
    `ansible-inventory -i <source> --graph` shows the group tree and its hosts, and `ansible-playbook -i <source> site.yml --list-hosts` prints the hosts each play resolves to without connecting to anything. Both are read-only and take a second, which makes them the standard pre-flight check.

saying these in an interview costs you the question

  • Thinking Ansible auto-discovers hosts on the network
  • Believing you must declare the all group yourself
  • Saying ungrouped means hosts that failed to connect
  • Assuming a host in a child group is not in the parent
  • Claiming inventory must be a single INI file

context

open as a page

What is the difference between Ansible's command and shell modules, and which should you reach for by default?

level: juniorimportance: must knowfreq 66%

basics

~20 s

Ansible's command module executes a program directly with no shell, so pipes, redirects, globs and environment-variable expansion do not work. The shell module runs the command line through a shell on the target, so they do. Prefer command unless you need shell features.

open as a page

In an Ansible role created by `ansible-galaxy init`, what does each standard directory (tasks, handlers, defaults, vars, files, templates, meta) hold, and which files does Ansible load automatically?

level: juniorimportance: must knowfreq 72%

basics

~10 s

An Ansible role is a fixed directory tree - tasks/, handlers/, defaults/, vars/, files/, templates/, meta/. Ansible auto-loads main.yml from each of those directories, and resolves copy and template sources against files/ and templates/.

open as a page

In an Ansible playbook, what does the Jinja2 expression `{{ myvar | default('fallback') }}` do, and how do `default('fallback', true)` and `default(omit)` differ from it?

level: juniorimportance: must knowfreq 58%

basics

~20 s

The default filter supplies a value only when the variable is undefined. Passing true as a second argument also replaces empty or otherwise falsy values. Passing the special omit value removes the parameter from the task entirely, so the module applies its own default.

open as a page

What is Ansible Vault, and what does encrypting a file with it protect you against — and what does it not protect you against?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Ansible Vault symmetrically encrypts variable files or single values with a password so they can be committed to git. Ansible decrypts them in memory at run time. It protects secrets at rest only — anyone holding the password reads everything.

open as a page

Where on disk does Ansible look for `group_vars/` and `host_vars/`, and which file wins when a host belongs to several groups?

level: middleimportance: must knowfreq 66%

basics

~20 s

Ansible loads group_vars/ and host_vars/ from two places: the directory holding the inventory and the directory holding the playbook, with the playbook-adjacent copy winning. Across groups, a child group's file beats its parent's, and host_vars beat every group file.

open as a page

In Ansible, what does a module reporting "changed" actually mean, and why do the command and shell modules report changed on every run?

level: middleimportance: must knowfreq 72%

basics

~20 s

Changed means the module actually altered the host during this run. The command and shell modules run an opaque command and cannot tell whether anything changed, so they report changed every time unless you add creates, removes, or changed_when.

open as a page

Why does an Ansible task using the command or shell module report "changed" on every run, and what do you use to make it report honestly?

level: middleimportance: must knowfreq 68%

basics

~20 s

Ansible cannot inspect what an arbitrary command did, so command and shell report changed whenever they run. Fix it with changed_when, evaluating the return code or output, with changed_when false for read-only commands, or with creates so the task is skipped entirely.

open as a page

In an Ansible playbook, when do handlers actually run, and what has to happen for a handler to fire at all?

level: middleimportance: must knowfreq 72%

basics

~20 s

A handler runs only when a task that notifies it reports changed, and it runs once, at the end of the play, not at the point of notification. A meta flush_handlers task forces queued handlers to run earlier.

open as a page

When authoring an Ansible role, how do you decide whether a variable belongs in defaults/main.yml or vars/main.yml?

level: middleimportance: must knowfreq 62%

basics

~20 s

defaults/main.yml holds the role's overridable knobs: it is the lowest-precedence variable source, so almost anything a consumer sets wins. vars/main.yml sits far higher, so reserve it for role-internal constants the consumer should not have to change.

open as a page

In Ansible, a variable resolves to an unexpected value at runtime. How does Ansible's variable precedence decide which definition wins, and how do you trace where the value actually came from?

level: middleimportance: must knowfreq 72%

basics

~20 s

Ansible layers about 22 variable sources from role defaults at the bottom to command-line extra vars at the top, which always win. Facts sit below play vars; set_fact and registered vars sit near the top. Trace a value with the debug module and ansible-inventory.

open as a page

How does the Ansible Vault password reach an automated CI run, given that nobody is there to type it and it must not be committed to the repository?

level: seniorimportance: must knowfreq 60%

basics

~20 s

The CI system injects the password from its own secret store, the job writes it to a private temporary file, and Ansible reads it with --vault-password-file or the ANSIBLE_VAULT_PASSWORD_FILE variable. Interactive prompting cannot work without a terminal.

open as a page

In an Ansible task, what does the when keyword do, and why is its expression written without the {{ }} delimiters?

level: juniorimportance: should knowfreq 66%

basics

~20 s

The when keyword makes a task run only if its expression is true; otherwise the task is skipped for that host. The value is already evaluated as a Jinja expression, so wrapping it in curly braces is redundant and Ansible warns about it.

open as a page

You add a host to an Ansible inventory under a friendly alias that does not resolve in DNS; it answers SSH on port 2222 as the user `deploy`. Which inventory variables make that work?

level: middleimportance: should knowfreq 58%

basics

~20 s

Set ansible_host to the real address or DNS name, ansible_port to 2222 and ansible_user to deploy. The inventory name stays a label used in patterns and for the host_vars filename; ansible_host is what Ansible actually connects to.

open as a page

In Ansible, what is the difference between a module and a plugin, and where does each one actually execute?

level: middleimportance: should knowfreq 45%

basics

~20 s

Modules are units of work shipped to the managed host and executed there by its Python interpreter, returning JSON. Plugins extend the control node itself — lookup, connection, callback, inventory, strategy, become — and run locally, never on the target.

open as a page

What is an Ansible collection, how does it relate to roles, and what does a fully qualified name such as community.general.timezone refer to?

level: middleimportance: should knowfreq 50%

basics

~20 s

A collection is Ansible's packaging and distribution unit: a namespace.name bundle of modules, plugins, roles and playbooks. Content inside it is addressed by fully qualified collection name, such as community.general.timezone, and installed with ansible-galaxy collection install.

open as a page

In an Ansible playbook, what is the difference between import_role and include_role, and how does the choice affect tags, loops and when?

level: middleimportance: should knowfreq 58%

basics

~20 s

import_role is static: Ansible expands the role when it parses the playbook, so its tasks inherit tags and show up in --list-tasks, but it cannot be looped. include_role is dynamic: resolved at runtime, so it can be looped and skipped as one unit.

open as a page

In Ansible, when do you reach for set_fact versus register versus vars_files, and what is the scope and lifetime of a variable created by set_fact?

level: middleimportance: should knowfreq 55%

basics

~20 s

register captures a task's result object for the host that ran it; set_fact computes a named value per host at run time; vars_files loads static YAML at the start of a play. A set_fact value is per host and survives into later plays of the same run, but not into the next run unless cacheable is set.

open as a page

In an Ansible repository, when would you encrypt an entire vars file with ansible-vault versus using ansible-vault encrypt_string for individual values?

level: middleimportance: should knowfreq 55%

basics

~20 s

Encrypt a whole file when nearly everything in it is secret; use encrypt_string when a mostly-plaintext file needs one or two secret values. Whole-file encryption makes diffs unreadable; inline vault strings keep the surrounding YAML reviewable.

open as a page

Your Ansible runs target EC2 instances that scale up and down all day, and the checked-in static inventory is permanently out of date. How do you move to a dynamic inventory, and why prefer an inventory plugin over an inventory script?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Replace the static file with a dynamic inventory plugin config — for EC2, a YAML file whose name ends in aws_ec2.yml declaring the amazon.aws.aws_ec2 plugin. Plugins are preferred over executable scripts because they are configured declaratively, support caching, and build groups from tags.

open as a page

You are writing a custom Ansible module in Python. What contract must it satisfy to behave like a first-class module?

level: seniorimportance: should knowfreq 32%

basics

~20 s

A custom Ansible module must declare its inputs through an argument_spec on AnsibleModule, compare current state with desired state, act only when they differ, honour check mode, and exit by printing JSON via exit_json with an accurate changed flag or fail_json with a message.

open as a page

In an Ansible play, how do you let a task fail without aborting the run, and what does block/rescue/always give you that ignore_errors does not?

level: seniorimportance: should knowfreq 44%

basics

~20 s

ignore_errors continues past a failed task but still marks it failed and offers no recovery path. block/rescue/always groups tasks, runs the rescue section when any task in the block fails, and always runs cleanup either way, so the host is marked recovered rather than failed.

open as a page

You must apply a config change across 60 web servers with Ansible without taking the whole fleet out at once. Which play-level keywords control that, and how do handlers behave under them?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Set serial on the play to run it in batches of hosts instead of all at once, optionally with max_fail_percentage to abort if a batch goes badly. Because the whole play repeats per batch, handlers flush at the end of each batch, so restarts roll host group by host group.

open as a page

Your Ansible playbooks depend on third-party Galaxy roles and collections. How do you make those dependencies reproducible in CI?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Declare every dependency in a version-controlled requirements.yml with an exact version, install it in CI with ansible-galaxy role install -r and ansible-galaxy collection install -r into a project-local path, and never rely on whatever happens to be on the control node.

open as a page

An Ansible play sets `gather_facts: no` to speed up a run, and a later task referencing `ansible_facts['distribution']` fails with an undefined variable. Why does that happen, and what are your options?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Facts are produced by the setup module, which Ansible runs implicitly at the start of a play. Disabling gathering skips it, so no ansible_facts exist. Options are to re-enable gathering, run setup explicitly for the hosts that need it, narrow it with gather_subset, or enable fact caching.

open as a page

In an Ansible play running against your web servers, how do you use a value belonging to a different host — say the IP address of a database server in another group — and what has to be true for that to work?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Read it through the hostvars magic variable, indexed by the other host's inventory name, and find the hosts with groups. Inventory-defined values are always there, but another host's facts only exist if that host was played in this run or its facts are cached.

open as a page

A team rotates their Ansible Vault password every quarter with ansible-vault rekey. Why might their secrets still be compromised, and what does a real rotation involve?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Rekey changes the password that encrypts the file, not the secrets inside it. Anyone with the old password and an old commit can still read the current credentials, because the plaintext never changed. Real rotation changes the credential at the system that issued it.

open as a page

When an Ansible task has both loop and register, what does the registered variable contain, and what is loop_control's label used for?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

With a loop, the registered variable holds a results list with one entry per iteration, each carrying its own item and module return values, rather than a single result. The loop_control label sets what Ansible prints for each iteration instead of the whole item.

open as a page

In an Ansible playbook targeting a remote server, where does `{{ lookup('file', '/etc/motd') }}` read that file from — the control node or the managed host?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

From the control node. Every Ansible lookup plugin runs locally, in the controller's Python process, using the controller's filesystem, environment and credentials — never on the managed host, regardless of which host the task targets.

open as a page

What problem do Ansible vault IDs (the --vault-id label@source form) solve, and how does Ansible decide which password to use for a given encrypted file?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

A vault ID pairs a label with a password source, such as --vault-id prod@prompt, so one run can carry several vault passwords at once. The label is written into the encrypted file's header and tells Ansible which password to try first.

open as a page

showing 1–30 of 33