Ansible remains the go-to tool for configuration management and automation. Its agentless architecture and simple YAML syntax make it accessible, but interviews dig deeper—into idempotency, variable precedence, and how Ansible fits into modern infrastructure alongside Terraform and Kubernetes.
This guide covers what actually comes up in DevOps interviews: not just syntax, but the patterns and principles that separate beginners from experienced practitioners.
Ansible Fundamentals
What is Configuration Management?
Configuration management ensures servers are configured consistently and correctly. Instead of manually SSHing into servers and running commands, you define the desired state in code.
Benefits:
- Consistency: Same configuration across all servers
- Version control: Track changes, review, rollback
- Documentation: Code is the documentation
- Automation: No manual intervention
- Drift correction: Re-run to fix manual changes
Ansible vs Other Tools
| Tool | Architecture | Language | Model |
|---|---|---|---|
| Ansible | Agentless (SSH) | YAML | Push |
| Puppet | Agent-based | Puppet DSL | Pull |
| Chef | Agent-based | Ruby | Pull |
| Salt | Agent or agentless | YAML | Push/Pull |
Why Ansible often wins:
- No agents to install or maintain
- YAML is easy to read and write
- Low barrier to entry
- Large module library
- Strong community and Galaxy ecosystem
Agentless Architecture
Ansible connects to targets via SSH (Linux) or WinRM (Windows):
Control Node Managed Nodes
┌──────────────┐ ┌──────────────┐
│ Ansible │──── SSH ────>│ Server 1 │
│ Engine │──── SSH ────>│ Server 2 │
│ │──── SSH ────>│ Server 3 │
└──────────────┘ └──────────────┘
No agents installed on managed nodes
Advantages:
- Nothing to install on targets
- No listening ports or daemons
- No agent updates to manage
- Works on any SSH-accessible system
- Simpler security model
Trade-offs:
- Push-based requires connectivity
- No continuous enforcement (unlike pull-based agents)
- Control node must reach all targets
Ansible vs Terraform
This question comes up constantly. They solve different problems:
| Aspect | Terraform | Ansible |
|---|---|---|
| Purpose | Infrastructure provisioning | Configuration management |
| Model | Declarative | Procedural |
| State | Tracks state file | Stateless |
| Creates | Cloud resources (VMs, VPCs, DBs) | Configures existing servers |
| Idempotency | Built-in via state | Module-dependent |
When to use each:
Terraform: "Create 3 EC2 instances in us-east-1"
Ansible: "Install nginx, configure SSL, deploy app on those instances"
Together:
# 1. Terraform creates infrastructure
terraform apply
# 2. Ansible configures it
ansible-playbook -i inventory configure.ymlInventory Management
Static Inventory
Simple INI or YAML file listing hosts:
# inventory/hosts.ini
[webservers]
web1.example.com
web2.example.com
web3.example.com
[databases]
db1.example.com
db2.example.com
[production:children]
webservers
databases
[webservers:vars]
http_port=80YAML format:
# inventory/hosts.yml
all:
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
vars:
http_port: 80
databases:
hosts:
db1.example.com:
db_port: 5432
db2.example.com:
db_port: 5432Host and Group Variables
Organize variables in directory structure:
inventory/
├── hosts.yml
├── group_vars/
│ ├── all.yml # All hosts
│ ├── webservers.yml # Webserver group
│ └── production.yml # Production group
└── host_vars/
├── web1.example.com.yml
└── db1.example.com.yml
# group_vars/webservers.yml
http_port: 80
nginx_worker_processes: auto
ssl_enabled: true
# host_vars/web1.example.com.yml
nginx_worker_processes: 4 # Override for this hostDynamic Inventory
Query cloud providers for current infrastructure:
# AWS EC2 dynamic inventory
ansible-inventory -i aws_ec2.yml --list
# Using plugin
ansible-playbook -i aws_ec2.yml playbook.ymlAWS EC2 plugin configuration:
# aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
- us-west-2
filters:
tag:Environment: production
keyed_groups:
- key: tags.Role
prefix: role
- key: placement.availability_zone
prefix: az
compose:
ansible_host: public_ip_addressExample question: "How do you handle auto-scaling groups with Ansible?"
Use dynamic inventory. As instances come and go, the inventory updates automatically. Tag instances with their role (web, api, worker) and use tag-based groups in your playbooks.
Inventory Patterns
# Target specific hosts
ansible webservers -m ping
# Multiple groups
ansible 'webservers:databases' -m ping
# Intersection (in both groups)
ansible 'webservers:&production' -m ping
# Exclusion
ansible 'webservers:!web3.example.com' -m ping
# Regex
ansible '~web[0-9]+\.example\.com' -m pingPlaybooks & Tasks
Playbook Structure
# playbook.yml
---
- name: Configure web servers
hosts: webservers
become: yes # Run as root
vars:
http_port: 80
tasks:
- name: Install nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Start nginx
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Restart nginx
service:
name: nginx
state: restartedKey concepts:
- Play: Targets a group of hosts with tasks
- Task: Single action using a module
- Handler: Task triggered by notifications
- become: Privilege escalation (sudo)
Essential Modules
Package management:
# Debian/Ubuntu
- name: Install packages
apt:
name:
- nginx
- postgresql
- python3
state: present
update_cache: yes
# RHEL/CentOS
- name: Install packages
yum:
name: nginx
state: present
# Generic (detects OS)
- name: Install package
package:
name: nginx
state: presentFile operations:
# Copy file
- name: Copy config
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Restart nginx
# Template (Jinja2)
- name: Deploy config from template
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
# Create directory
- name: Ensure directory exists
file:
path: /var/www/app
state: directory
owner: www-data
mode: '0755'
# Create symlink
- name: Create symlink
file:
src: /etc/nginx/sites-available/app
dest: /etc/nginx/sites-enabled/app
state: linkService management:
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: yes
# systemd specific
- name: Reload systemd
systemd:
daemon_reload: yesCommands (use sparingly):
# Avoid when possible - not idempotent
- name: Run script
command: /opt/scripts/setup.sh
args:
creates: /opt/app/.installed # Only run if file doesn't exist
# Shell for pipes and redirects
- name: Check disk space
shell: df -h | grep /dev/sda1
register: disk_result
changed_when: false # Never report changedConditionals
- name: Install Apache on Debian
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Install Apache on RedHat
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
# Multiple conditions
- name: Configure production
template:
src: prod.conf.j2
dest: /etc/app/config
when:
- env == "production"
- ansible_memory_mb.real.total > 4096
# Based on previous task
- name: Check if app exists
stat:
path: /opt/app
register: app_stat
- name: Install app
command: /opt/install.sh
when: not app_stat.stat.existsLoops
# Simple list
- name: Create users
user:
name: "{{ item }}"
state: present
loop:
- alice
- bob
- charlie
# List of dictionaries
- name: Create users with groups
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
loop:
- { name: 'alice', groups: 'admin' }
- { name: 'bob', groups: 'developers' }
# With index
- name: Create numbered files
copy:
content: "File {{ idx }}"
dest: "/tmp/file{{ idx }}.txt"
loop: "{{ range(1, 5) | list }}"
loop_control:
index_var: idx
# Dictionary iteration
- name: Set sysctl values
sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
loop: "{{ sysctl_settings | dict2items }}"
vars:
sysctl_settings:
net.ipv4.ip_forward: 1
net.core.somaxconn: 65535Handlers
Handlers run once at end of play, only if notified:
tasks:
- name: Update nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- Validate nginx config
- Restart nginx
- name: Update SSL cert
copy:
src: ssl.crt
dest: /etc/nginx/ssl/
notify: Restart nginx # Same handler, runs once
handlers:
- name: Validate nginx config
command: nginx -t
changed_when: false
- name: Restart nginx
service:
name: nginx
state: restartedForce handler execution:
- name: Update config
template:
src: app.conf.j2
dest: /etc/app/config
notify: Restart app
- name: Force handlers now
meta: flush_handlers
- name: Continue with app running
uri:
url: http://localhost:8080/healthRoles & Galaxy
Role Structure
roles/
└── nginx/
├── defaults/
│ └── main.yml # Default variables (lowest precedence)
├── vars/
│ └── main.yml # Role variables (high precedence)
├── tasks/
│ └── main.yml # Main task list
├── handlers/
│ └── main.yml # Handlers
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates
├── files/
│ └── index.html # Static files
├── meta/
│ └── main.yml # Role metadata, dependencies
└── README.md
Creating a Role
# roles/nginx/defaults/main.yml
nginx_port: 80
nginx_worker_processes: auto
nginx_sites: []
# roles/nginx/tasks/main.yml
---
- name: Install nginx
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
- name: Configure sites
template:
src: site.conf.j2
dest: "/etc/nginx/sites-available/{{ item.name }}"
loop: "{{ nginx_sites }}"
notify: Restart nginx
- name: Enable sites
file:
src: "/etc/nginx/sites-available/{{ item.name }}"
dest: "/etc/nginx/sites-enabled/{{ item.name }}"
state: link
loop: "{{ nginx_sites }}"
notify: Restart nginx
- name: Start nginx
service:
name: nginx
state: started
enabled: yes
# roles/nginx/handlers/main.yml
---
- name: Restart nginx
service:
name: nginx
state: restartedUsing Roles
# playbook.yml
---
- name: Configure web servers
hosts: webservers
become: yes
roles:
- nginx
- { role: app, app_port: 3000 }
- role: monitoring
vars:
monitoring_enabled: true
when: env == "production"Role dependencies:
# roles/app/meta/main.yml
---
dependencies:
- role: nginx
vars:
nginx_port: 80
- role: nodejs
vars:
nodejs_version: "18"Ansible Galaxy
Public repository of community roles:
# Install role from Galaxy
ansible-galaxy install geerlingguy.nginx
# Install from requirements file
ansible-galaxy install -r requirements.yml
# List installed roles
ansible-galaxy list
# Create role skeleton
ansible-galaxy init my_rolerequirements.yml:
roles:
- name: geerlingguy.nginx
version: "3.1.0"
- name: geerlingguy.postgresql
version: "3.4.0"
- src: https://github.com/org/ansible-role-app.git
scm: git
version: v1.2.0
name: app
collections:
- name: amazon.aws
version: ">=5.0.0"Variables & Templates
Variable Precedence
Ansible has 22 levels of precedence (simplified):
Lowest priority:
1. Role defaults (roles/x/defaults/main.yml)
2. Inventory file or script group vars
3. Inventory group_vars/all
4. Playbook group_vars/all
5. Inventory group_vars/*
6. Playbook group_vars/*
7. Inventory file or script host vars
8. Inventory host_vars/*
9. Playbook host_vars/*
10. Host facts / cached set_facts
11. Play vars
12. Play vars_prompt
13. Play vars_files
14. Role vars (roles/x/vars/main.yml)
15. Block vars
16. Task vars
17. include_vars
18. set_facts / registered vars
19. Role params
20. include params
21. Extra vars (-e) -- ALWAYS WIN
Highest priority
Practical rules:
- Role defaults: For values users should override
- Role vars: For values that shouldn't change
- Extra vars (-e): For CI/CD overrides, always win
Facts and Magic Variables
# Gathering facts (automatic)
- name: Show OS info
debug:
msg: "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
# Useful facts
ansible_hostname # Short hostname
ansible_fqdn # Fully qualified domain name
ansible_default_ipv4.address # Primary IP
ansible_memtotal_mb # Total memory
ansible_processor_vcpus # CPU count
ansible_os_family # Debian, RedHat, etc.
# Magic variables
inventory_hostname # Name in inventory
groups['webservers'] # List of hosts in group
hostvars['web1'] # Variables for another host
ansible_play_hosts # All hosts in current playDisable fact gathering (faster):
- name: Quick playbook
hosts: all
gather_facts: no # Skip if not needed
tasks:
- name: Just copy a file
copy:
src: file.txt
dest: /tmp/Jinja2 Templates
{# templates/nginx.conf.j2 #}
worker_processes {{ nginx_worker_processes | default('auto') }};
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
}
http {
{% for site in nginx_sites %}
server {
listen {{ site.port | default(80) }};
server_name {{ site.domain }};
root {{ site.root }};
{% if site.ssl | default(false) %}
listen 443 ssl;
ssl_certificate {{ site.ssl_cert }};
ssl_certificate_key {{ site.ssl_key }};
{% endif %}
{% for location in site.locations | default([]) %}
location {{ location.path }} {
{{ location.config }}
}
{% endfor %}
}
{% endfor %}
}Common filters:
{{ variable | default('fallback') }}
{{ list | join(', ') }}
{{ string | lower }}
{{ string | upper }}
{{ path | basename }}
{{ path | dirname }}
{{ dict | to_json }}
{{ dict | to_yaml }}
{{ password | password_hash('sha512') }}
{{ list | first }}
{{ list | last }}
{{ number | int }}
{{ value | bool }}Ansible Vault
Encrypt sensitive data:
# Create encrypted file
ansible-vault create secrets.yml
# Encrypt existing file
ansible-vault encrypt secrets.yml
# Edit encrypted file
ansible-vault edit secrets.yml
# View encrypted file
ansible-vault view secrets.yml
# Decrypt file
ansible-vault decrypt secrets.yml
# Encrypt single string
ansible-vault encrypt_string 'mysecret' --name 'db_password'Using vault in playbook:
# group_vars/production/vault.yml (encrypted)
vault_db_password: supersecret
vault_api_key: abc123
# group_vars/production/vars.yml (plain, references vault)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"# Run with vault password
ansible-playbook playbook.yml --ask-vault-pass
ansible-playbook playbook.yml --vault-password-file ~/.vault_passBest Practices & Patterns
Idempotency
Every task should be safe to run multiple times:
# BAD - always reports changed
- name: Add line to file
shell: echo "export PATH=/opt/bin:$PATH" >> /etc/profile
# GOOD - idempotent
- name: Add line to file
lineinfile:
path: /etc/profile
line: 'export PATH=/opt/bin:$PATH'
state: present
# BAD - always runs
- name: Create database
command: createdb myapp
# GOOD - check first
- name: Create database
command: createdb myapp
args:
creates: /var/lib/postgresql/data/myapp # Skip if existsIdempotent modules: apt, yum, copy, template, file, service, user, group Non-idempotent modules: command, shell, raw (use creates/removes args)
Directory Structure
ansible/
├── ansible.cfg
├── inventory/
│ ├── production/
│ │ ├── hosts.yml
│ │ ├── group_vars/
│ │ │ ├── all.yml
│ │ │ └── webservers.yml
│ │ └── host_vars/
│ └── staging/
│ └── ...
├── playbooks/
│ ├── site.yml # Master playbook
│ ├── webservers.yml
│ └── databases.yml
├── roles/
│ ├── common/
│ ├── nginx/
│ └── app/
├── group_vars/ # Shared across inventories
│ └── all.yml
└── requirements.yml # Galaxy dependencies
ansible.cfg:
[defaults]
inventory = inventory/production
roles_path = roles
retry_files_enabled = False
host_key_checking = False
[privilege_escalation]
become = True
become_method = sudo
become_user = rootTesting with Molecule
Test roles in isolated environments:
# Initialize molecule for existing role
cd roles/nginx
molecule init scenario -r nginx -d docker
# Run full test sequence
molecule test
# Just converge (apply role)
molecule converge
# Login to test instance
molecule login
# Destroy test environment
molecule destroymolecule/default/molecule.yml:
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu
image: ubuntu:22.04
pre_build_image: true
- name: centos
image: centos:8
pre_build_image: true
provisioner:
name: ansible
verifier:
name: ansibleCI/CD Integration
GitHub Actions:
name: Ansible
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ansible-lint
uses: ansible/ansible-lint@main
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install ansible molecule molecule-docker
- name: Run Molecule tests
run: molecule test
working-directory: roles/nginx
deploy:
needs: [lint, test]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Run playbook
uses: dawidd6/action-ansible-playbook@v2
with:
playbook: playbooks/site.yml
key: ${{ secrets.SSH_PRIVATE_KEY }}
vault_password: ${{ secrets.VAULT_PASSWORD }}
options: |
--inventory inventory/productionCommon Interview Questions
Scenario: Idempotency Problem
Question: "A task always shows 'changed'. How do you fix it?"
# Problem: shell always reports changed
- name: Check app status
shell: curl -s http://localhost:8080/health
register: health
# Solution 1: changed_when
- name: Check app status
shell: curl -s http://localhost:8080/health
register: health
changed_when: false # Never report changed
# Solution 2: Use uri module (idempotent)
- name: Check app status
uri:
url: http://localhost:8080/health
return_content: yes
register: health
# Problem: command always runs
- name: Initialize database
command: /opt/app/init-db.sh
# Solution: creates argument
- name: Initialize database
command: /opt/app/init-db.sh
args:
creates: /opt/app/.db_initializedScenario: Scaling Ansible
Question: "How do you run Ansible against 1000 servers efficiently?"
# 1. Increase parallelism
# ansible.cfg
[defaults]
forks = 50 # Default is 5
# 2. Use async for long-running tasks
- name: Update packages (async)
apt:
upgrade: dist
async: 3600 # 1 hour timeout
poll: 0 # Don't wait
register: apt_update
- name: Check update status
async_status:
jid: "{{ apt_update.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 60
delay: 60
# 3. Use strategy
- hosts: all
strategy: free # Don't wait for slowest host
tasks: ...
# 4. Pull mode with ansible-pull
# On each host, pull and run playbook
ansible-pull -U https://github.com/org/ansible-config.git
# 5. Use AWX/Tower for large scale
# Web UI, job scheduling, RBAC, audit loggingScenario: Debugging Failures
Question: "A playbook fails intermittently. How do you debug?"
# Increase verbosity
ansible-playbook playbook.yml -vvv
# Step through tasks
ansible-playbook playbook.yml --step
# Start at specific task
ansible-playbook playbook.yml --start-at-task="Configure app"
# Check syntax
ansible-playbook playbook.yml --syntax-check
# Dry run
ansible-playbook playbook.yml --check --diff# Debug task
- name: Debug variables
debug:
var: my_variable
- name: Debug message
debug:
msg: "Value is {{ my_variable }}"
# Pause for inspection
- name: Pause for manual check
pause:
prompt: "Check server state, press enter to continue"Quick Reference
Essential Commands
| Command | Purpose |
|---|---|
ansible all -m ping | Test connectivity |
ansible-playbook site.yml | Run playbook |
ansible-playbook site.yml -C | Dry run (check mode) |
ansible-playbook site.yml -D | Show diff |
ansible-playbook site.yml -l web1 | Limit to host |
ansible-vault encrypt file.yml | Encrypt file |
ansible-galaxy install role | Install role |
ansible-inventory --list | Show inventory |
ansible-doc module_name | Module documentation |
Common Patterns
# Register and use result
- command: whoami
register: result
- debug:
var: result.stdout
# Block with error handling
- block:
- name: Try this
command: /might/fail
rescue:
- name: Handle failure
debug:
msg: "Task failed, recovering..."
always:
- name: Always run
debug:
msg: "Cleanup"
# Delegate to another host
- name: Add to load balancer
command: add-backend {{ inventory_hostname }}
delegate_to: loadbalancer
# Run once (not on every host)
- name: Create shared resource
command: create-resource
run_once: trueRelated Articles
This guide connects to the broader DevOps interview preparation:
Infrastructure as Code:
- Terraform Interview Guide - Complementary IaC tool
DevOps Fundamentals:
- Linux Commands Interview Guide - Linux skills for Ansible
- CI/CD & GitHub Actions Interview Guide - Ansible in pipelines
- Docker Interview Guide - Container configuration
Cloud Platforms:
- AWS Interview Guide - AWS modules and dynamic inventory
- Azure Interview Guide - Azure modules
- GCP Interview Guide - GCP modules
Final Thoughts
Ansible interviews test understanding of configuration management principles, not just syntax. Key areas:
- Idempotency: Every task safe to run multiple times
- Variable precedence: Know what wins and why
- Roles vs playbooks: When to use each
- Vault: Secure secrets management
- Terraform complement: Know when to use which tool
Practice by configuring real servers. Break playbooks, debug them, understand error messages. That hands-on experience shows in interviews.
