TL;DR: Ansible connects over SSH, copies a module (a small Python program) to the target, runs it, collects JSON back, and removes it. Inventory says which hosts, playbooks say what to do in order, modules do the work. Agentless means nothing to install or patch on the targets, at the cost of a connection per task per host.
How to approach it
Describe the execution model first, because it explains everything else in the answer. Then be precise about idempotency, since "Ansible is idempotent" is a claim about well-written modules rather than about anything the tool enforces, and that distinction is the follow-up an experienced interviewer reaches for.
A strong answer
Inventory is the list of hosts, grouped, with variables. A static INI or YAML file for a fixed estate, or a dynamic inventory plugin that queries a cloud API so the list is generated from tags rather than maintained by hand. Groups are what make a playbook reusable: webservers, dbservers, and a production group containing both.
Modules do the actual work. apt, copy, service, user, template. Each is a program that takes parameters, makes the change, and reports JSON saying whether it changed anything. There are thousands, and the rule is to use one rather than shelling out, because modules report change state and raw commands cannot.
Playbooks are YAML: a list of plays, each targeting a group and running an ordered list of tasks.
- hosts: webservers
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Write the site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
The handler is the part worth pointing at. It only runs if a task that notified it actually reported a change, so the config being already correct means no reload. That is the mechanism for "restart the service only when its configuration changed", and it is the bit people reimplement badly with when conditions.
Roles package tasks, handlers, templates, files and default variables into a reusable directory structure, which is how a playbook stays readable past about fifty lines.
Execution: the control node connects over SSH (WinRM for Windows), copies the module to a temporary directory on the target, executes it with the Python interpreter that is already there, reads the JSON result, and cleans up. Nothing is installed permanently, which is what agentless means. No agent to deploy, patch, or have a CVE. The cost is that every task is a round trip per host, so a hundred-task playbook across five hundred hosts is a lot of SSH, and you reach for forks, pipelining, and async to manage it. That is the honest trade against agent-based tools which poll and converge locally.
On idempotency: running the playbook twice should report zero changes the second time. That is a property of using the right modules, not of Ansible. state: present is idempotent; command: apt-get install -y nginx is not, and will report changed every run. A playbook reporting changes on every run has lost its most useful signal, because you can no longer tell a real change from noise, and --check mode stops meaning anything.
What interviewers probe next
"Where does Ansible stop and Terraform start?" Terraform creates infrastructure and tracks it in state; Ansible configures what exists and keeps no state. Provisioning a VM with Ansible works and gives you no record of what you own.
"What is --check?" A dry run: modules report what they would change without changing it. It only works if your tasks are written with proper modules, which is another reason raw commands hurt.
"How do you handle secrets?" Ansible Vault encrypts variable files at rest, with the key supplied at run time. For anything shared, pulling from a real secrets manager at run time is better than distributing a vault password.
Common mistakes
Using command or shell where a module exists, which loses change reporting, check mode and idempotency in one go.
Ignoring a playbook that reports changes on every run, which discards the signal that tells you something actually moved.
Keeping inventory by hand for a cloud estate that changes daily, when a dynamic inventory reads it from tags.
Restarting services unconditionally instead of using a handler, so every run bounces production.