Ansible interviews test your understanding of agentless automation, YAML playbook structure, inventory management, roles, Vault, and idempotent task design. This guide covers the 50 most common questions — with concise answers and real examples.
Quick Reference
| Topic | Key Concepts |
|---|---|
| Core | playbooks, inventory, modules, tasks, handlers |
| Inventory | static/dynamic, groups, host_vars, group_vars |
| Playbooks | plays, tasks, variables, conditionals, loops |
| Roles | tasks, handlers, templates, files, vars, defaults, meta |
| Modules | command, shell, copy, template, service, yum/apt, file |
| Variables | precedence (22 levels), register, facts, Jinja2 |
| Vault | ansible-vault encrypt/decrypt, vault IDs |
| Performance | forks, async, serial, strategy: free |
| Error handling | block/rescue/always, ignore_errors, failed_when |
1. Core Concepts
1. What is Ansible and what makes it different from Chef/Puppet?
Ansible is an agentless, push-based IT automation tool that uses SSH (or WinRM for Windows) and YAML playbooks to configure systems, deploy applications, and orchestrate workflows.
Key differentiators:
| Feature | Ansible | Chef | Puppet |
|---|---|---|---|
| Architecture | Agentless (SSH) | Agent + Server | Agent + Server |
| Language | YAML | Ruby (DSL) | Puppet DSL |
| Learning curve | Low | High | Medium |
| Push vs Pull | Push (default) | Pull | Pull |
| Setup | Minutes | Hours/Days | Hours/Days |
| Execution | Linear (sequential) | Convergent | Convergent |
| Idempotency | Module-level | Built-in | Built-in |
| Windows support | WinRM (limited) | Good | Good |
When to choose Ansible: simpler setups, mixed OS, no agent overhead, quick adoption.
2. Explain Ansible's architecture and how it works.
Control Node
│
├── Inventory (hosts/groups)
├── Playbooks (YAML automation)
├── Modules (task implementations)
└── SSH/WinRM
│
├── managed_host_1
├── managed_host_2
└── managed_host_3
Execution flow:
- Ansible reads the inventory to find target hosts
- Reads the playbook to determine tasks
- Connects via SSH (no agent needed on targets)
- Copies modules as temporary Python scripts to remote host
- Executes modules, captures results, removes temp files
- Reports changed/ok/failed per task
No daemon runs on managed nodes — connections are ephemeral.
3. What is idempotency in Ansible? Why does it matter?
Idempotency means running the same playbook multiple times produces the same result without unintended side effects.
# Idempotent — only creates file if it doesn't exist
- name: Create config file
ansible.builtin.copy:
src: app.conf
dest: /etc/app/app.conf
owner: root
mode: '0644'
# NOT idempotent — appends on every run
- name: Add line to file (BAD)
ansible.builtin.shell: echo "config=true" >> /etc/app.conf
Why it matters:
- Safe to re-run playbooks (no drift)
- Play again after failures without damage
- Declarative intent — you describe desired state, not steps
Force idempotency for shell/command:
- name: Initialize database (only once)
ansible.builtin.command: /usr/bin/db-init
args:
creates: /var/lib/db/.initialized # skip if file exists
4. What is an Ansible playbook? Show the structure.
A playbook is a YAML file containing one or more plays that map hosts to tasks.
---
# webserver.yml
- name: Configure web servers # Play 1
hosts: webservers # target group from inventory
become: true # sudo privilege escalation
vars:
http_port: 80
max_clients: 200
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx # trigger handler
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
post_tasks:
- name: Verify nginx responds
ansible.builtin.uri:
url: "http://localhost:{{ http_port }}"
status_code: 200
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
5. What is inventory in Ansible? Explain static vs dynamic inventory.
Inventory defines the hosts Ansible manages.
Static inventory (INI format):
[webservers]
web1.example.com
web2.example.com ansible_port=2222
[dbservers]
db1.example.com ansible_user=ubuntu ansible_python_interpreter=/usr/bin/python3
[production:children]
webservers
dbservers
[webservers:vars]
http_port=80
Static inventory (YAML format):
all:
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
ansible_port: 2222
dbservers:
hosts:
db1.example.com:
ansible_user: ubuntu
Dynamic inventory: A script or plugin that queries an external source (AWS EC2, GCP, Azure, Kubernetes, Vault, etc.) and outputs JSON.
# Use AWS EC2 dynamic inventory
ansible-playbook -i aws_ec2.yml playbook.yml
# aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
keyed_groups:
- key: tags.Environment
prefix: env
| Feature | Static | Dynamic |
|---|---|---|
| Format | INI or YAML file | Script/Plugin |
| Use case | Fixed infra | Cloud/container |
| Maintenance | Manual updates | Auto-discovers |
| Examples | bare metal, VMs | EC2, GCP, K8s |
6. What are Ansible modules? Name 15 commonly used ones.
Modules are units of work — pre-built Python programs executed on managed hosts.
- name: Example module usage
ansible.builtin.copy: # module name (FQCN)
src: file.txt # module parameters
dest: /tmp/file.txt
Essential modules:
| Module | Purpose |
|---|---|
ansible.builtin.copy |
Copy files to remote hosts |
ansible.builtin.template |
Render Jinja2 templates |
ansible.builtin.file |
Manage files/directories/symlinks |
ansible.builtin.apt |
Manage apt packages (Debian/Ubuntu) |
ansible.builtin.yum / dnf |
Manage packages (RHEL/CentOS) |
ansible.builtin.package |
OS-agnostic package management |
ansible.builtin.service |
Start/stop/enable system services |
ansible.builtin.command |
Run commands (no shell features) |
ansible.builtin.shell |
Run commands via /bin/sh |
ansible.builtin.user |
Manage user accounts |
ansible.builtin.group |
Manage groups |
ansible.builtin.lineinfile |
Manage lines in a file |
ansible.builtin.get_url |
Download files via HTTP |
ansible.builtin.unarchive |
Extract archives |
ansible.builtin.uri |
Make HTTP requests |
ansible.builtin.debug |
Print debug messages |
ansible.builtin.set_fact |
Set host variables |
ansible.builtin.include_tasks |
Include task files dynamically |
ansible.builtin.import_tasks |
Import task files statically |
ansible.posix.synchronize |
rsync wrapper |
7. What is the difference between command and shell modules?
# command — does NOT use a shell; safer but limited
- name: Run binary
ansible.builtin.command: /usr/bin/myapp --config /etc/myapp.conf
# shell — runs via /bin/sh; supports pipes, redirects, globs
- name: Count log entries
ansible.builtin.shell: grep ERROR /var/log/app.log | wc -l
register: error_count
| Feature | command |
shell |
|---|---|---|
Shell features (pipes, >, &&) |
No | Yes |
| Environment variables | Limited | Full |
| Security | Safer (no injection) | Risk of injection |
| Use case | Simple binaries | Complex one-liners |
| Idempotency | Low | Low |
Rule: prefer command for safety; use shell only when you need shell features. For both, prefer dedicated modules when available.
2. Inventory & Variables
8. Explain Ansible variable precedence (from lowest to highest).
Ansible has 22 variable precedence levels. Key levels (lowest → highest):
1. command line values (e.g., -u)
2. role defaults (role/defaults/main.yml)
3. inventory file / script group vars
4. inventory group_vars/all
5. playbook group_vars/all
6. inventory group_vars/*
7. playbook group_vars/*
8. inventory host vars
9. playbook host_vars/*
10. host facts / set_facts with cacheable=true
11. play vars
12. play vars_prompt
13. play vars_files
14. role vars (role/vars/main.yml) ← overrides role defaults
15. block vars (current block)
16. task vars (only current task)
17. include_vars
18. set_facts / registered vars
19. role params
20. include params
21. extra vars (-e "key=val") ← HIGHEST PRIORITY
Memory aid: defaults → group_vars → host_vars → play/task vars → -e (always wins)
# Extra vars override everything
ansible-playbook site.yml -e "env=production debug=false"
9. What are group_vars and host_vars? How are they structured?
inventory/
├── hosts.yml # inventory file
├── group_vars/
│ ├── all.yml # vars for ALL hosts
│ ├── webservers.yml # vars for webservers group
│ └── dbservers/
│ ├── vars.yml
│ └── vault.yml # encrypted with ansible-vault
└── host_vars/
├── web1.example.com.yml
└── db1.example.com.yml
# group_vars/all.yml
ntp_server: pool.ntp.org
timezone: UTC
# group_vars/webservers.yml
http_port: 80
https_port: 443
nginx_worker_processes: auto
# host_vars/web1.example.com.yml
ansible_host: 192.168.1.10
server_role: primary
10. What is the register keyword? Give an example.
register captures the output of a task into a variable.
- name: Check if app is installed
ansible.builtin.command: which myapp
register: myapp_check
ignore_errors: true
- name: Print result
ansible.builtin.debug:
msg: "myapp found at {{ myapp_check.stdout }}"
when: myapp_check.rc == 0
- name: Install myapp if missing
ansible.builtin.apt:
name: myapp
state: present
when: myapp_check.rc != 0
- name: Get service status
ansible.builtin.service_facts:
- name: Show nginx status
ansible.builtin.debug:
var: ansible_facts.services['nginx.service'].state
Common registered variable attributes:
result.stdout— standard outputresult.stderr— error outputresult.rc— return code (0 = success)result.stdout_lines— output as listresult.changed— whether task changed somethingresult.failed— whether task failed
11. What are Ansible facts? How do you gather and use them?
Facts are variables automatically discovered from managed hosts (OS, IP, RAM, CPU, etc.).
- name: Use facts in playbook
hosts: all
gather_facts: true # default; set false to skip (faster)
tasks:
- name: Print OS information
ansible.builtin.debug:
msg: "{{ inventory_hostname }} runs {{ ansible_distribution }} {{ ansible_distribution_version }}"
- name: Install package (OS-aware)
ansible.builtin.package:
name: "{{ 'nginx' if ansible_os_family == 'Debian' else 'nginx' }}"
state: present
- name: Only run on Ubuntu 22
ansible.builtin.debug:
msg: "Ubuntu 22 specific task"
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_major_version == "22"
Common facts:
| Fact | Example value |
|---|---|
ansible_hostname |
web1 |
ansible_fqdn |
web1.example.com |
ansible_os_family |
Debian, RedHat |
ansible_distribution |
Ubuntu, CentOS |
ansible_distribution_version |
22.04 |
ansible_default_ipv4.address |
192.168.1.10 |
ansible_memtotal_mb |
8192 |
ansible_processor_vcpus |
4 |
Custom facts: place scripts in /etc/ansible/facts.d/*.fact on managed hosts → accessible via ansible_local.
12. How do you set variables dynamically during a play?
- name: Dynamic variable examples
hosts: webservers
tasks:
- name: Set a variable
ansible.builtin.set_fact:
app_version: "2.1.0"
app_dir: "/opt/myapp"
- name: Set variable from command output
ansible.builtin.command: date +%Y%m%d
register: date_output
- ansible.builtin.set_fact:
deploy_date: "{{ date_output.stdout }}"
- name: Use computed variable
ansible.builtin.debug:
msg: "Deploying {{ app_version }} on {{ deploy_date }}"
# vars_prompt — interactive input
- name: Prompt for confirmation
ansible.builtin.pause:
prompt: "Deploy to production? (yes/no)"
register: confirmation
when: env == "production"
3. Playbook Patterns
13. Explain handlers and when they are triggered.
Handlers are tasks that run only when notified, and only once at the end of the play — even if notified multiple times.
- name: Web server setup
hosts: webservers
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
notify: Start nginx # notify handler
- name: Copy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- Validate nginx config # notify multiple handlers
- Reload nginx
- name: Copy SSL certificate
ansible.builtin.copy:
src: cert.pem
dest: /etc/ssl/certs/cert.pem
notify: Reload nginx # same handler, runs once
handlers:
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
- name: Validate nginx config
ansible.builtin.command: nginx -t
changed_when: false
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Key handler behaviors:
- Run at the end of the play (not immediately)
- Run only once even if notified multiple times
- Do NOT run if the notifying task had
changed: false meta: flush_handlersforces immediate execution
14. How do you use conditionals (when) in Ansible?
- name: Conditionals examples
hosts: all
tasks:
# Simple condition
- name: Install Apache on Debian
ansible.builtin.apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
# Multiple conditions (AND)
- name: Run only on prod Ubuntu servers
ansible.builtin.debug:
msg: "Production Ubuntu task"
when:
- ansible_distribution == "Ubuntu"
- env == "production"
# OR condition
- name: Install on Ubuntu or Debian
ansible.builtin.apt:
name: vim
state: present
when: ansible_distribution in ["Ubuntu", "Debian"]
# Check registered variable
- name: Run command
ansible.builtin.command: /usr/bin/check-status
register: status_result
- name: Act on result
ansible.builtin.debug:
msg: "Service is running"
when: "'running' in status_result.stdout"
# Negate condition
- name: Skip on containers
ansible.builtin.service:
name: sshd
state: started
when: ansible_virtualization_type != "docker"
15. How do you use loops in Ansible?
- name: Loop examples
hosts: all
become: true
vars:
packages:
- nginx
- git
- curl
users:
- name: alice
uid: 1001
- name: bob
uid: 1002
tasks:
# Simple list loop
- name: Install packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
# Loop with dict items
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
state: present
loop: "{{ users }}"
# Loop with index
- name: Create numbered files
ansible.builtin.file:
path: "/tmp/file{{ item }}"
state: touch
loop: "{{ range(1, 6) | list }}"
# Loop with dict
- name: Set multiple facts
ansible.builtin.set_fact:
"{{ item.key }}": "{{ item.value }}"
loop: "{{ {'app_port': 8080, 'app_env': 'prod'} | dict2items }}"
# Loop control
- name: Install with custom label
ansible.builtin.apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
loop:
- {name: nginx, state: present}
- {name: apache2, state: absent}
loop_control:
label: "{{ item.name }}" # cleaner output
pause: 1 # pause between iterations
16. What are Ansible roles? Explain their directory structure.
Roles are a way to organize playbooks into reusable, shareable units.
roles/
└── webserver/
├── tasks/
│ └── main.yml # main task list
├── handlers/
│ └── main.yml # handler definitions
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates
├── files/
│ └── index.html # static files to copy
├── vars/
│ └── main.yml # role variables (high precedence)
├── defaults/
│ └── main.yml # default variables (lowest precedence)
├── meta/
│ └── main.yml # role metadata + dependencies
├── tests/
│ ├── inventory
│ └── test.yml
└── README.md
# roles/webserver/defaults/main.yml
http_port: 80
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
# roles/webserver/tasks/main.yml
---
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
notify: Restart nginx
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
# roles/webserver/meta/main.yml
dependencies:
- role: common
- role: ssl-certs
vars:
cert_domain: "{{ domain_name }}"
Using roles in a playbook:
- name: Configure web servers
hosts: webservers
roles:
- common
- webserver
- { role: monitoring, vars: { alert_email: ops@example.com } }
17. What is include_tasks vs import_tasks?
# import_tasks — STATIC (processed at parse time)
- name: Include static tasks
ansible.builtin.import_tasks: setup_db.yml
# ✓ Can use tags on imported tasks
# ✗ Cannot use variables in the file path
# ✗ Cannot be used in loops
# include_tasks — DYNAMIC (processed at run time)
- name: Include dynamic tasks
ansible.builtin.include_tasks: "setup_{{ env }}.yml" # variable in path ✓
# ✓ Can use variables in file path
# ✓ Can be used in loops
# ✗ Tags on include don't apply to included tasks
| Feature | import_tasks |
include_tasks |
|---|---|---|
| Processing | Parse time (static) | Run time (dynamic) |
| Variable in path | No | Yes |
| Tags apply to subtasks | Yes | No |
| Use in loop | No | Yes |
when applies to |
All sub-tasks | Include task only |
Same distinction applies to roles: import_role (static) vs include_role (dynamic).
4. Templates & Files
18. How do Jinja2 templates work in Ansible?
Templates (.j2 files) use Jinja2 syntax to render dynamic configuration files.
{# nginx.conf.j2 #}
worker_processes {{ ansible_processor_vcpus }};
worker_connections {{ nginx_worker_connections | default(1024) }};
server {
listen {{ http_port }};
server_name {{ inventory_hostname }} {{ ansible_fqdn }};
{# Conditional block #}
{% if enable_ssl %}
listen {{ https_port }} ssl;
ssl_certificate /etc/ssl/certs/{{ domain }}.pem;
{% endif %}
{# Loop over locations #}
{% for location in app_locations %}
location {{ location.path }} {
proxy_pass {{ location.backend }};
}
{% endfor %}
}
# Playbook using the template
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: nginx -t -c %s # validate before replacing
notify: Reload nginx
Jinja2 filters (frequently used):
| Filter | Example | Result |
|---|---|---|
default |
{{ var | default('none') }} |
fallback value |
upper / lower |
{{ name | upper }} |
ALICE |
replace |
{{ url | replace('http', 'https') }} |
https:// |
join |
{{ list | join(', ') }} |
a, b, c |
selectattr |
{{ users | selectattr('active') }} |
filter list |
to_json |
{{ dict | to_json }} |
JSON string |
from_json |
{{ json_str | from_json }} |
parse JSON |
b64encode |
{{ secret | b64encode }} |
base64 |
19. How do you manage files with lineinfile and blockinfile?
- name: File management examples
hosts: all
become: true
tasks:
# Ensure a line exists
- name: Set max open files
ansible.builtin.lineinfile:
path: /etc/security/limits.conf
line: "* soft nofile 65536"
state: present
# Replace a line matching a pattern
- name: Change SSH port
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port '
line: "Port 2222"
backup: true
notify: Restart sshd
# Insert before/after a line
- name: Add include before last line
ansible.builtin.lineinfile:
path: /etc/nginx/nginx.conf
insertbefore: '^}'
line: " include /etc/nginx/conf.d/*.conf;"
# Manage a block of config
- name: Configure kernel parameters
ansible.builtin.blockinfile:
path: /etc/sysctl.conf
marker: "# {mark} ANSIBLE MANAGED BLOCK"
block: |
net.ipv4.tcp_fin_timeout = 30
net.core.somaxconn = 65536
vm.swappiness = 10
backup: true
# Ensure line is absent
- name: Remove deprecated config
ansible.builtin.lineinfile:
path: /etc/app/config.ini
regexp: '^deprecated_setting='
state: absent
5. Ansible Vault
20. What is Ansible Vault and how do you use it?
Ansible Vault encrypts sensitive data (passwords, API keys, certificates) at rest.
# Encrypt a file
ansible-vault encrypt group_vars/production/vault.yml
# Decrypt
ansible-vault decrypt group_vars/production/vault.yml
# Edit encrypted file
ansible-vault edit group_vars/production/vault.yml
# Encrypt a single value
ansible-vault encrypt_string 'super_secret' --name 'db_password'
# Output:
# db_password: !vault |
# $ANSIBLE_VAULT;1.1;AES256
# ...
# View without decrypting to disk
ansible-vault view group_vars/production/vault.yml
# Rekey (change password)
ansible-vault rekey group_vars/production/vault.yml
# group_vars/production/vault.yml (encrypted at rest)
vault_db_password: "super_secret_password"
vault_api_key: "abcd1234efgh5678"
# group_vars/production/vars.yml (plain text, references vault)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
Running with vault:
# Prompt for vault password
ansible-playbook site.yml --ask-vault-pass
# Use password file (for automation)
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# Multiple vault IDs (different passwords for different vaults)
ansible-playbook site.yml \
--vault-id dev@~/.vault_dev \
--vault-id prod@~/.vault_prod
21. What are Vault IDs and why are they useful?
Vault IDs allow using multiple vault passwords in a single project — useful when dev and production secrets need different access controls.
# Encrypt with a named vault ID
ansible-vault encrypt_string 'dev_pass' --name 'db_password' --vault-id dev@prompt
ansible-vault encrypt_string 'prod_pass' --name 'db_password' --vault-id prod@prompt
# The vault ID label is embedded in the encrypted string header
# $ANSIBLE_VAULT;1.2;AES256;dev
# ...
# Decrypt with the right vault ID
ansible-playbook site.yml \
--vault-id dev@~/.vault_dev_pass \
--vault-id prod@~/.vault_prod_pass
6. Error Handling
22. How do you handle errors in Ansible?
- name: Error handling examples
hosts: all
tasks:
# Ignore errors and continue
- name: Try to stop optional service
ansible.builtin.service:
name: optional-svc
state: stopped
ignore_errors: true
# Custom failure condition
- name: Check disk space
ansible.builtin.command: df -h /
register: disk_usage
failed_when: "'100%' in disk_usage.stdout"
# Custom changed condition
- name: Run idempotent script
ansible.builtin.command: /usr/local/bin/check-state.sh
register: state_check
changed_when: state_check.rc == 1 # rc=0 means no change needed
# Block/rescue/always (try/catch/finally)
- name: Deploy application
block:
- name: Stop application
ansible.builtin.service:
name: myapp
state: stopped
- name: Deploy new version
ansible.builtin.unarchive:
src: myapp-2.0.tar.gz
dest: /opt/myapp/
- name: Start application
ansible.builtin.service:
name: myapp
state: started
rescue:
- name: Rollback on failure
ansible.builtin.command: /usr/local/bin/rollback.sh
- name: Send alert
ansible.builtin.uri:
url: "{{ slack_webhook }}"
method: POST
body_format: json
body:
text: "Deploy failed on {{ inventory_hostname }}"
always:
- name: Cleanup temp files
ansible.builtin.file:
path: /tmp/deploy
state: absent
23. What does any_errors_fatal do? When do you use it?
- name: Rolling deployment
hosts: webservers
any_errors_fatal: true # stop ALL hosts if any host fails
tasks:
- name: Deploy application
ansible.builtin.unarchive:
src: app.tar.gz
dest: /opt/app
# If this fails on web2, immediately stops web3, web4 etc.
Without any_errors_fatal, Ansible continues on remaining hosts even if some fail. Use it for:
- Database migrations (all-or-nothing)
- Cluster operations where partial state is dangerous
- Strict consistency requirements
7. Performance & Scale
24. How does Ansible execute tasks across many hosts?
# ansible.cfg
[defaults]
forks = 50 # parallel connections (default: 5)
# Or per-playbook
- name: High-performance play
hosts: all
strategy: free # hosts proceed independently (vs linear)
Execution strategies:
| Strategy | Behavior |
|---|---|
linear (default) |
All hosts complete task N before moving to N+1 |
free |
Each host proceeds as fast as possible independently |
host_pinned |
Like free but no fork reuse between hosts |
Async tasks (fire and forget):
- name: Run long backup job
ansible.builtin.command: /usr/bin/backup.sh
async: 3600 # max wait: 1 hour
poll: 0 # don't wait (fire and forget)
register: backup_job
- name: Wait for backup to finish
ansible.builtin.async_status:
jid: "{{ backup_job.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 60
delay: 30
25. What is serial and how does it enable rolling updates?
- name: Rolling web deployment
hosts: webservers
serial: "25%" # update 25% of hosts at a time
# Also: serial: 2 (absolute), serial: [1, 5, 25%] (batches)
max_fail_percentage: 10 # abort if >10% fail
tasks:
- name: Remove from load balancer
ansible.builtin.uri:
url: "http://lb/remove/{{ inventory_hostname }}"
method: POST
- name: Deploy new version
ansible.builtin.unarchive:
src: app-v2.tar.gz
dest: /opt/app/
- name: Restart application
ansible.builtin.service:
name: myapp
state: restarted
- name: Health check
ansible.builtin.uri:
url: "http://{{ ansible_host }}:8080/health"
status_code: 200
retries: 5
delay: 10
- name: Add back to load balancer
ansible.builtin.uri:
url: "http://lb/add/{{ inventory_hostname }}"
method: POST
serial batching: serial: [1, 5, "25%"] — deploy to 1 host first, then 5, then 25% at a time. Useful for canary-style rollouts.
26. How do you use tags in Ansible?
- name: Full server setup
hosts: all
tasks:
- name: Install base packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: [vim, curl, htop]
tags:
- packages
- base
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags:
- nginx
- config
- name: Deploy application
ansible.builtin.unarchive:
src: app.tar.gz
dest: /opt/app/
tags:
- app
- deploy
# Run only tagged tasks
ansible-playbook site.yml --tags "nginx,config"
ansible-playbook site.yml --tags deploy
# Skip tagged tasks
ansible-playbook site.yml --skip-tags packages
# Special tags
ansible-playbook site.yml --tags always # 'always' tagged tasks always run
ansible-playbook site.yml --tags never # 'never' tagged tasks only run when explicitly requested
8. Ansible Galaxy & Collections
27. What is Ansible Galaxy? How do you use it?
Ansible Galaxy is the community hub for sharing roles and collections.
# Install a role
ansible-galaxy role install geerlingguy.nginx
# Install to specific path
ansible-galaxy role install geerlingguy.nginx -p roles/
# Install from requirements file
ansible-galaxy role install -r requirements.yml
# Install a collection
ansible-galaxy collection install community.general
ansible-galaxy collection install amazon.aws
# requirements.yml
---
roles:
- name: geerlingguy.nginx
version: "3.2.0"
- src: https://github.com/company/ansible-role-myapp
name: myapp
collections:
- name: community.general
version: ">=7.0.0"
- name: amazon.aws
version: "6.0.0"
28. What are Ansible Collections?
Collections are distribution format for Ansible content (roles, modules, plugins, playbooks).
my_namespace.my_collection/
├── docs/
├── galaxy.yml # collection metadata
├── plugins/
│ ├── modules/ # custom modules
│ ├── inventory/ # inventory plugins
│ └── callback/ # callback plugins
├── roles/
│ └── my_role/
├── playbooks/
└── tests/
# Use FQCN (Fully Qualified Collection Name)
- name: Use community module
community.general.slack:
token: "{{ slack_token }}"
msg: "Deploy complete"
channel: "#deployments"
9. Advanced Patterns
29. How do you create a custom Ansible module?
#!/usr/bin/python
# library/my_module.py
from ansible.module_utils.basic import AnsibleModule
def run_module():
module_args = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present',
choices=['present', 'absent']),
value=dict(type='int', default=0)
)
result = dict(
changed=False,
original_value='',
new_value=''
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# Module logic here
name = module.params['name']
state = module.params['state']
if module.check_mode:
module.exit_json(**result)
# Simulate work
result['original_value'] = 'old'
result['new_value'] = name
result['changed'] = True
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
# Use custom module
- name: Use custom module
my_module:
name: "test-resource"
state: present
value: 42
register: module_result
30. What is a callback plugin? Name common ones.
Callback plugins extend Ansible output behavior.
# ansible.cfg
[defaults]
stdout_callback = yaml # pretty YAML output
callback_whitelist = profile_tasks, timer
[callback_profile_tasks]
task_output_limit = 20
sort_order = descending
Common built-in callbacks:
| Plugin | Purpose |
|---|---|
default |
Standard output |
yaml |
YAML-formatted output |
json |
Machine-readable JSON |
minimal |
Minimal output |
debug |
More verbose debugging |
profile_tasks |
Show task execution times |
timer |
Total play duration |
mail |
Email on failures |
slack |
Slack notifications |
unixy |
Unix-style minimal output |
31. How do you test Ansible roles? (Molecule)
Molecule is the standard testing framework for Ansible roles.
# Install
pip install molecule molecule-docker
# Initialize new role with Molecule
molecule init role my_role --driver-name docker
# Role structure with Molecule
my_role/
├── molecule/
│ └── default/
│ ├── molecule.yml # test configuration
│ ├── converge.yml # playbook to test
│ ├── verify.yml # assertions
│ └── prepare.yml # pre-test setup
├── tasks/
├── handlers/
└── ...
# molecule/default/molecule.yml
driver:
name: docker
platforms:
- name: instance
image: geerlingguy/docker-ubuntu2204-ansible
pre_build_image: true
provisioner:
name: ansible
verifier:
name: ansible
# Test lifecycle
molecule create # create test instances
molecule converge # apply the role
molecule verify # run assertions
molecule destroy # clean up
molecule test # full test cycle (create+converge+verify+destroy)
32. What is delegate_to and when do you use it?
delegate_to runs a task on a different host than the current one.
- name: Rolling deployment with LB management
hosts: webservers
tasks:
- name: Drain from load balancer (run on LB, not web server)
community.general.haproxy:
state: disabled
backend: web_backend
host: "{{ inventory_hostname }}"
delegate_to: lb.example.com
- name: Deploy app (runs on web server)
ansible.builtin.unarchive:
src: app.tar.gz
dest: /opt/app/
- name: Re-enable in load balancer (run on LB)
community.general.haproxy:
state: enabled
backend: web_backend
host: "{{ inventory_hostname }}"
delegate_to: lb.example.com
# Run once on localhost (useful for notifications)
- name: Send deployment notification
ansible.builtin.uri:
url: "{{ webhook_url }}"
method: POST
body_format: json
body: {text: "Deploy complete"}
delegate_to: localhost
run_once: true
33. What is run_once and how does it differ from delegate_to: localhost?
- name: Database migration
hosts: appservers
tasks:
# run_once: runs on only the FIRST host in the batch
- name: Run database migration
ansible.builtin.command: /opt/app/manage.py migrate
run_once: true
# Combined: run once, on localhost
- name: Create release tag in monitoring
ansible.builtin.uri:
url: "{{ datadog_api }}/events"
method: POST
body_format: json
body:
title: "Deploy v{{ app_version }}"
tags: ["deploy", "production"]
delegate_to: localhost
run_once: true
Key difference:
run_once: true— executes once on the first target hostdelegate_to: localhost+run_once: true— executes once on the control node
10. Configuration & Best Practices
34. How do you configure Ansible with ansible.cfg?
# ansible.cfg (project-level, takes precedence over global)
[defaults]
inventory = ./inventory
remote_user = ubuntu
private_key_file = ~/.ssh/deploy_key
host_key_checking = False
retry_files_enabled = False
forks = 20
timeout = 30
stdout_callback = yaml
deprecation_warnings = False
# Roles path (multiple paths separated by colon)
roles_path = ./roles:~/.ansible/roles
# Collections
collections_paths = ./collections
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
pipelining = True # faster; needs requiretty=false in sudoers
[persistent_connection]
connect_timeout = 30
command_timeout = 30
ansible.cfg search order (first found wins):
ANSIBLE_CONFIGenv var./ansible.cfg(current directory)~/.ansible.cfg/etc/ansible/ansible.cfg
35. What is pipelining and how does it improve performance?
Without pipelining, each task requires 2 SSH connections (one to copy the module, one to execute it).
With pipelining enabled, the module is sent via stdin in a single SSH connection — typically 30-50% faster.
# ansible.cfg
[ssh_connection]
pipelining = True
Requirement: managed hosts must have requiretty = false in /etc/sudoers:
# /etc/sudoers or /etc/sudoers.d/ansible
Defaults !requiretty
36. How do you use check mode (dry run) in Ansible?
# Run in check mode — shows what WOULD happen, no changes made
ansible-playbook site.yml --check
# Combine with diff to see file changes
ansible-playbook site.yml --check --diff
# Tasks that don't support check mode can be marked
- name: Restart service (skip in check mode)
ansible.builtin.service:
name: nginx
state: restarted
when: not ansible_check_mode
# Force a task to always run even in check mode
- name: Always gather facts
ansible.builtin.setup:
check_mode: false
11. Ansible vs Other Tools
37. Compare Ansible vs Terraform vs Chef/Puppet.
| Dimension | Ansible | Terraform | Chef/Puppet |
|---|---|---|---|
| Primary use | Configuration mgmt, app deploy | Infrastructure provisioning | Configuration mgmt |
| Agent | Agentless (SSH) | None (API calls) | Agent required |
| Language | YAML | HCL | Ruby DSL / Puppet DSL |
| State | Stateless (re-applies) | Stateful (.tfstate) |
Stateful (catalog) |
| Idempotency | Module-level | Built-in | Built-in |
| Cloud provisioning | Limited | Excellent | Limited |
| Learning curve | Low | Medium | High |
| Execution model | Procedural (sequential) | Declarative (graph) | Declarative |
| Best for | Post-provision config, orchestration | Cloud infra lifecycle | Large-scale config mgmt |
Common pattern: Terraform provisions → Ansible configures.
38. When would you use Ansible over a shell script?
| Scenario | Shell Script | Ansible |
|---|---|---|
| Idempotency | Manual if checks |
Built-in |
| Error handling | Manual set -e |
Automatic |
| Cross-platform | Hard to maintain | OS-aware modules |
| Secret management | Manual | Vault integration |
| Parallel execution | Complex | forks built-in |
| Rollback | Manual | Block/rescue |
| Audit trail | None | Logs + callback plugins |
| Scale (1000s of hosts) | Very difficult | Easy |
Use shell scripts for: quick one-offs, single-host tasks, when Ansible overhead is too much. Use Ansible for: repeatable, multi-host, production-grade automation.
12. Common Scenario Questions
39. How do you deploy a Docker container with Ansible?
- name: Deploy containerized application
hosts: appservers
become: true
vars:
app_image: "myregistry.example.com/myapp"
app_version: "2.1.0"
app_port: 8080
tasks:
- name: Install Docker
ansible.builtin.apt:
name:
- docker.io
- python3-docker # required for community.docker modules
state: present
- name: Start Docker service
ansible.builtin.service:
name: docker
state: started
enabled: true
- name: Log in to container registry
community.docker.docker_login:
registry_url: myregistry.example.com
username: "{{ registry_username }}"
password: "{{ registry_password }}"
- name: Pull latest image
community.docker.docker_image:
name: "{{ app_image }}:{{ app_version }}"
source: pull
- name: Run container
community.docker.docker_container:
name: myapp
image: "{{ app_image }}:{{ app_version }}"
state: started
restart_policy: unless-stopped
ports:
- "{{ app_port }}:8080"
env:
DB_HOST: "{{ db_host }}"
DB_PASSWORD: "{{ vault_db_password }}"
volumes:
- /opt/myapp/config:/app/config:ro
- /var/log/myapp:/app/logs
40. How do you manage AWS infrastructure with Ansible?
- name: Provision AWS infrastructure
hosts: localhost
gather_facts: false
collections:
- amazon.aws
vars:
region: us-east-1
instance_type: t3.medium
ami_id: ami-0c02fb55956c7d316
tasks:
- name: Create security group
amazon.aws.ec2_security_group:
name: web-sg
description: Web server security group
region: "{{ region }}"
rules:
- proto: tcp
ports: [80, 443]
cidr_ip: 0.0.0.0/0
- proto: tcp
ports: [22]
cidr_ip: 10.0.0.0/8
- name: Launch EC2 instances
amazon.aws.ec2_instance:
name: web-server
region: "{{ region }}"
instance_type: "{{ instance_type }}"
image_id: "{{ ami_id }}"
security_groups: [web-sg]
tags:
Environment: production
Role: webserver
count: 3
wait: true
register: ec2_result
- name: Add to in-memory inventory
ansible.builtin.add_host:
hostname: "{{ item.public_ip_address }}"
groups: webservers
loop: "{{ ec2_result.instances }}"
- name: Configure newly launched instances
hosts: webservers
become: true
roles:
- common
- webserver
41. How do you structure a large Ansible project?
ansible-project/
├── ansible.cfg
├── site.yml # master playbook
├── webservers.yml # per-role playbooks
├── dbservers.yml
├── inventory/
│ ├── production/
│ │ ├── hosts.yml
│ │ ├── group_vars/
│ │ │ ├── all/
│ │ │ │ ├── vars.yml
│ │ │ │ └── vault.yml # ansible-vault encrypted
│ │ │ └── webservers.yml
│ │ └── host_vars/
│ └── staging/
│ └── hosts.yml
├── roles/
│ ├── common/
│ ├── webserver/
│ └── database/
├── playbooks/
│ ├── deploy.yml
│ ├── rollback.yml
│ └── maintenance.yml
├── library/ # custom modules
├── filter_plugins/ # custom Jinja2 filters
├── collections/
│ └── requirements.yml
└── .gitlab-ci.yml # CI/CD integration
42. How do you integrate Ansible with CI/CD pipelines?
# .github/workflows/ansible-deploy.yml
name: Deploy with Ansible
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: pip install ansible ansible-lint
- name: Install collections
run: ansible-galaxy collection install -r collections/requirements.yml
- name: Run ansible-lint
run: ansible-lint site.yml
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
- name: Create vault password file
run: echo "${{ secrets.ANSIBLE_VAULT_PASS }}" > ~/.vault_pass
- name: Run playbook (check mode first)
run: |
ansible-playbook site.yml \
-i inventory/production/hosts.yml \
--vault-password-file ~/.vault_pass \
--check
- name: Deploy
run: |
ansible-playbook site.yml \
-i inventory/production/hosts.yml \
--vault-password-file ~/.vault_pass \
--extra-vars "deploy_version=${{ github.sha }}"
13. Common Mistakes
Common Ansible Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
Using shell when a module exists |
Not idempotent, error-prone | Use apt, service, copy etc. |
| Hardcoding secrets in playbooks | Security risk | Use Ansible Vault |
ignore_errors: true everywhere |
Hides real failures | Use block/rescue or failed_when |
No validate on config templates |
Broken config deployed | Use validate param in template |
Missing backup: true on file edits |
Can't recover | Add backup: true to lineinfile |
| Not using roles for reuse | Duplicate code | Extract to roles |
gather_facts: true on all plays |
Slow for large inventories | Disable when facts not needed |
| No tags on tasks | Can't run selectively | Add meaningful tags |
Ansible vs Related Tools
| Tool | Type | Agent | Language | Best For |
|---|---|---|---|---|
| Ansible | Config mgmt + Orchestration | None (SSH) | YAML | General automation, app deployment |
| Terraform | IaC provisioning | None (API) | HCL | Cloud infra lifecycle |
| Chef | Config mgmt | Required | Ruby DSL | Complex configs, large orgs |
| Puppet | Config mgmt | Required | Puppet DSL | Enterprise compliance |
| SaltStack | Config mgmt + Remote exec | Optional | YAML/Python | High-scale, fast execution |
| Fabric | Remote execution | None | Python | Simple task runners |
| Bash scripts | Ad-hoc | None | Shell | Quick one-offs |
Frequently Asked Questions
Q: Is Ansible push or pull?
Push by default — the control node SSHes into managed nodes. Pull mode is possible with ansible-pull (managed nodes pull and apply from a Git repo), useful for large fleets or when firewall rules prevent inbound SSH.
Q: Can Ansible manage Windows hosts?
Yes — using WinRM (Windows Remote Management) or SSH. Windows modules use PowerShell under the hood. Set ansible_connection: winrm and ansible_winrm_transport: ntlm (or kerberos/credssp) in inventory.
Q: What is the difference between vars and defaults in a role?
defaults/main.yml has the lowest variable precedence — any inventory var, group var, or playbook var overrides it. Use defaults for values users should customize. vars/main.yml has high precedence — only extra vars (-e) and task vars override it. Use vars for internal role values that shouldn't be overridden.
Q: How do you speed up slow Ansible runs?
- Enable pipelining (
pipelining = True) - Increase forks (
forks = 50) - Disable fact gathering when not needed (
gather_facts: false) - Use fact caching (
fact_caching = redis) - Use
strategy: freefor independent hosts - Use
asyncfor long-running tasks - Profile with
callback_whitelist = profile_tasks
Q: What's the difference between notify and calling a handler directly?
notify is conditional — the handler only runs if the task actually changed something. Calling a handler via tasks: would run it unconditionally. This is the core of Ansible idempotency: install nginx → if it was already installed (changed: false) → handler is NOT triggered → no unnecessary restarts.
Q: Can you run Ansible without SSH (for local development)?
Yes — use connection: local to run tasks on the control node itself, or ansible_connection: local in inventory. Useful for local development, testing, or managing the control node itself.