Ansible Interview Guide: Configuration Management Fundamentals

·16 min read
devopsansibleconfiguration-managementautomationinfrastructureinterview-preparation

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

ToolArchitectureLanguageModel
AnsibleAgentless (SSH)YAMLPush
PuppetAgent-basedPuppet DSLPull
ChefAgent-basedRubyPull
SaltAgent or agentlessYAMLPush/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:

AspectTerraformAnsible
PurposeInfrastructure provisioningConfiguration management
ModelDeclarativeProcedural
StateTracks state fileStateless
CreatesCloud resources (VMs, VPCs, DBs)Configures existing servers
IdempotencyBuilt-in via stateModule-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.yml

Inventory 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=80

YAML 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: 5432

Host 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 host

Dynamic 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.yml

AWS 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_address

Example 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 ping

Playbooks & 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: restarted

Key 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: present

File 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: link

Service management:

- name: Ensure nginx is running
  service:
    name: nginx
    state: started
    enabled: yes
 
# systemd specific
- name: Reload systemd
  systemd:
    daemon_reload: yes

Commands (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 changed

Conditionals

- 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.exists

Loops

# 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: 65535

Handlers

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: restarted

Force 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/health

Roles & 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: restarted

Using 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_role

requirements.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 play

Disable 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_pass

Best 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 exists

Idempotent 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 = root

Testing 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 destroy

molecule/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: ansible

CI/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/production

Common 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_initialized

Scenario: 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 logging

Scenario: 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

CommandPurpose
ansible all -m pingTest connectivity
ansible-playbook site.ymlRun playbook
ansible-playbook site.yml -CDry run (check mode)
ansible-playbook site.yml -DShow diff
ansible-playbook site.yml -l web1Limit to host
ansible-vault encrypt file.ymlEncrypt file
ansible-galaxy install roleInstall role
ansible-inventory --listShow inventory
ansible-doc module_nameModule 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: true

Related Articles

This guide connects to the broader DevOps interview preparation:

Infrastructure as Code:

DevOps Fundamentals:

Cloud Platforms:


Final Thoughts

Ansible interviews test understanding of configuration management principles, not just syntax. Key areas:

  1. Idempotency: Every task safe to run multiple times
  2. Variable precedence: Know what wins and why
  3. Roles vs playbooks: When to use each
  4. Vault: Secure secrets management
  5. 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.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides