50+ Ansible Interview Questions 2025: Playbooks, Roles & Vault

·26 min read
ansibledevopsconfiguration-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.

Table of Contents

  1. Ansible Fundamentals Questions
  2. Ansible vs Other Tools Questions
  3. Inventory Management Questions
  4. Playbook Structure Questions
  5. Module and Task Questions
  6. Conditionals and Loops Questions
  7. Handlers Questions
  8. Roles and Galaxy Questions
  9. Variables and Precedence Questions
  10. Templates and Jinja2 Questions
  11. Ansible Vault Questions
  12. Best Practices and Patterns Questions
  13. Scaling and Performance Questions
  14. Debugging and Troubleshooting Questions

Ansible Fundamentals Questions

Understanding the core concepts of configuration management and Ansible's architecture is essential for any DevOps interview.

What is configuration management and why is it important?

Configuration management ensures servers are configured consistently and correctly across your infrastructure. Instead of manually SSHing into servers and running commands, you define the desired state in code, which can be version-controlled, reviewed, and automatically applied.

This approach provides several critical benefits: consistency across all servers (no more "works on my machine"), version control for tracking changes and enabling rollbacks, documentation through code, automation eliminating manual intervention, and drift correction by re-running playbooks to fix unauthorized manual changes.

What is Ansible and how does it work?

Ansible is an agentless automation tool that uses SSH to connect to target machines and execute tasks defined in YAML playbooks. It follows a push-based model where a control node sends configurations to managed nodes, requiring no software installation on the targets.

The architecture consists of a control node (where Ansible runs), managed nodes (target servers), an inventory (list of managed nodes), playbooks (YAML files defining tasks), and modules (units of code that perform specific actions). This simplicity is one of Ansible's greatest strengths—if you can SSH to a server, Ansible can manage it.

flowchart LR
    subgraph control["Control Node"]
        A["Ansible<br/>Engine"]
    end
 
    subgraph managed["Managed Nodes"]
        S1["Server 1"]
        S2["Server 2"]
        S3["Server 3"]
    end
 
    A -->|SSH| S1
    A -->|SSH| S2
    A -->|SSH| S3

Why is Ansible agentless and what are the advantages?

Ansible's agentless architecture means no software needs to be installed on managed nodes—it connects via SSH (Linux) or WinRM (Windows). This design decision has significant implications for security, maintenance, and usability.

The advantages include: nothing to install on target servers, no listening ports or daemons creating attack surface, no agent updates to manage, works immediately on any SSH-accessible system, and a simpler security model. The trade-offs are that push-based execution requires connectivity from the control node to all targets, and there's no continuous enforcement like pull-based agents provide.


Ansible vs Other Tools Questions

Understanding how Ansible compares to other tools shows architectural awareness that interviewers value.

How does Ansible compare to Puppet, Chef, and Salt?

The configuration management landscape includes several tools with different approaches. Understanding the architectural differences helps you choose the right tool for specific situations and demonstrates broad knowledge in interviews.

Ansible is agentless and uses a push model with YAML syntax. Puppet and Chef are agent-based with pull models—Puppet uses its own DSL while Chef uses Ruby. Salt can work either way (agent or agentless) and uses YAML. Ansible often wins for its low barrier to entry, no agents to maintain, and large module library.

ToolArchitectureLanguageModel
AnsibleAgentless (SSH)YAMLPush
PuppetAgent-basedPuppet DSLPull
ChefAgent-basedRubyPull
SaltAgent or agentlessYAMLPush/Pull

What is the difference between Ansible and Terraform?

This question comes up constantly because both tools are used in infrastructure automation, but they solve fundamentally different problems. Understanding this distinction is crucial.

Terraform is declarative infrastructure provisioning—it creates cloud resources like VMs, VPCs, and databases. Ansible is procedural configuration management—it configures servers by running tasks in order. Terraform tracks state in a state file, while Ansible is stateless. They complement each other perfectly: Terraform provisions infrastructure, Ansible configures it.

AspectTerraformAnsible
PurposeInfrastructure provisioningConfiguration management
ModelDeclarativeProcedural
StateTracks state fileStateless
CreatesCloud resources (VMs, VPCs, DBs)Configures existing servers
IdempotencyBuilt-in via stateModule-dependent

Typical workflow:

# 1. Terraform creates infrastructure
terraform apply
 
# 2. Ansible configures it
ansible-playbook -i inventory configure.yml

Inventory Management Questions

The inventory defines what Ansible manages. Understanding static and dynamic inventory patterns is essential.

What is an Ansible inventory and what formats does it support?

The inventory is a file or script that defines the hosts and groups Ansible will manage. It's the foundation of targeting—without it, Ansible doesn't know what servers to configure.

Ansible supports two main inventory formats: INI (simple, legacy format) and YAML (more structured, recommended for complex setups). The inventory also defines variables at the host and group level, enabling different configurations for different environments.

INI format:

# 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

How do you organize host and group variables in Ansible?

Rather than cramming all variables into the inventory file, Ansible supports a directory structure that separates variables by host and group. This organization becomes essential as your infrastructure grows and you need to manage variables for different environments.

The convention is to create group_vars/ and host_vars/ directories alongside your inventory. Files in group_vars/ apply to all hosts in that group, while files in host_vars/ apply to specific hosts. This pattern keeps your inventory clean and variables organized.

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

What is dynamic inventory and when would you use it?

Dynamic inventory queries external sources (cloud providers, CMDBs, container orchestrators) for the current list of hosts rather than maintaining a static file. This is essential for cloud environments where servers are created and destroyed automatically.

With dynamic inventory, as instances come and go through auto-scaling, the inventory updates automatically. You tag instances with their role (web, api, worker) and use tag-based groups in your playbooks. No manual inventory maintenance required.

# aws_ec2.yml - AWS EC2 dynamic inventory plugin
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
# Using dynamic inventory
ansible-inventory -i aws_ec2.yml --list
ansible-playbook -i aws_ec2.yml playbook.yml

What inventory patterns can you use to target specific hosts?

Ansible provides powerful patterns for targeting subsets of your inventory. This is useful when you want to run a playbook against specific groups, combinations of groups, or individual hosts.

Understanding these patterns enables precise targeting for maintenance windows, rolling deployments, and testing changes on subsets before full rollout.

# Target specific group
ansible webservers -m ping
 
# Multiple groups (union)
ansible 'webservers:databases' -m ping
 
# Intersection (hosts in both groups)
ansible 'webservers:&production' -m ping
 
# Exclusion (webservers except web3)
ansible 'webservers:!web3.example.com' -m ping
 
# Regex pattern
ansible '~web[0-9]+\.example\.com' -m ping

Playbook Structure Questions

Playbooks are the core of Ansible automation. Understanding their structure is fundamental.

What is the structure of an Ansible playbook?

A playbook is a YAML file containing one or more plays. Each play targets a group of hosts and defines tasks to execute on them. Understanding the structure helps you organize automation effectively and troubleshoot issues.

The key components are: plays (target hosts with tasks), tasks (individual actions using modules), handlers (tasks triggered by notifications), and variables (data used in tasks and templates).

# 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

What does "become" mean in Ansible and when do you use it?

The become directive enables privilege escalation, allowing tasks to run as a different user (typically root). This is necessary for administrative tasks like installing packages, managing services, or modifying system files.

You can set become: yes at the play level to apply to all tasks, or at individual task level for granular control. By default, become uses sudo, but you can configure other methods like su or doas.


Module and Task Questions

Modules are the building blocks of Ansible tasks. Knowing the right module for each job is essential.

What are the essential Ansible modules for package management?

Ansible provides specialized modules for different package managers, plus a generic package module that auto-detects the OS. Using the right module ensures idempotent package installation.

The apt module works for Debian/Ubuntu, yum for RHEL/CentOS, and package provides a cross-platform option. Each supports installing, removing, and updating packages with various options.

# 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

What is the difference between copy and template modules?

Both modules transfer files to managed nodes, but template processes Jinja2 templates while copy transfers files as-is. Understanding when to use each is important for configuration management.

Use copy for static files that don't need variable substitution. Use template when you need to inject variables, use conditionals, or generate dynamic content based on host facts or inventory data.

# Copy static file
- name: Copy config
  copy:
    src: nginx.conf
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
  notify: Restart nginx
 
# Template with Jinja2
- name: Deploy config from template
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Restart nginx

When should you use command or shell modules vs native modules?

Command and shell modules should be avoided when idempotent alternatives exist. They run arbitrary commands and always report "changed" unless you configure changed_when. Native modules like apt, service, and file are idempotent by design.

Use command/shell only when no native module exists for your task, or for one-off commands. Always add creates or removes arguments to make them idempotent, and use changed_when: false for read-only operations.

# 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 and Loops Questions

Conditionals and loops enable dynamic playbook behavior based on facts and data.

How do you use conditionals in Ansible tasks?

The when directive enables conditional task execution based on variables, facts, or previous task results. This is essential for handling differences between operating systems, environments, or configurations.

Conditions can be simple boolean checks, comparisons, or complex expressions combining multiple conditions. You can also base conditions on registered results from previous tasks.

- 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 (AND)
- 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 result
- 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

How do you implement loops in Ansible?

The loop directive (replacing the older with_items) iterates over lists and dictionaries. This enables creating multiple users, installing multiple packages, or configuring multiple virtual hosts with a single task.

You can loop over simple lists, lists of dictionaries, or use filters like dict2items to iterate over dictionary key-value pairs. Loop control provides access to the index and other metadata.

# 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' }
 
# 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 Questions

Handlers are special tasks that run only when notified, typically for service restarts.

What are handlers and how do they work?

Handlers are tasks that run at the end of a play, only if they've been notified by other tasks. They're typically used for service restarts—you want to restart nginx only once, even if multiple config files changed.

The key behavior: handlers run once at play end, regardless of how many times they're notified. They run in the order defined in the handlers section, not the order notified. This ensures efficient service restarts without unnecessary repetition.

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

How do you force handlers to run immediately?

By default, handlers run at the end of the play. Sometimes you need a service restarted before continuing—for example, to verify health before deploying more changes. The meta: flush_handlers task forces all pending handlers to execute.

This pattern is useful when subsequent tasks depend on the service being restarted, such as health checks or tests that need the new configuration active.

- 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 and Galaxy Questions

Roles are Ansible's mechanism for reusable, shareable automation.

What is an Ansible role and how is it structured?

A role is a standardized directory structure that packages related tasks, handlers, variables, templates, and files into a reusable unit. Roles enable code reuse across projects and sharing through Ansible Galaxy.

The structure follows conventions that Ansible automatically understands: tasks go in tasks/, templates in templates/, variables in vars/ or defaults/, and so on. This organization makes roles self-contained and easy to understand.

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

When should you use roles vs standalone playbooks?

Playbooks are appropriate for simple, one-off tasks or for orchestrating multiple roles. Roles are better when configuration might be reused across projects, you want to share via Galaxy, or tasks are complex enough to benefit from organized structure.

A good rule: if you're copying tasks between playbooks, extract them into a role. Roles also enable testing with Molecule, version control of configuration units, and clear interfaces through documented variables.

How do you use Ansible Galaxy for role management?

Ansible Galaxy is the public repository of community-contributed roles. You can install roles directly or define dependencies in a requirements file for reproducible environments.

The requirements.yml file pins role versions and can include roles from Galaxy, Git repositories, or local paths. This ensures consistent deployments across teams and environments.

# Install role from Galaxy
ansible-galaxy install geerlingguy.nginx
 
# Install from requirements file
ansible-galaxy install -r requirements.yml
 
# 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"

How do you use roles in a playbook?

Roles are invoked in the playbook using the roles directive. You can pass variables to customize role behavior and use conditionals to apply roles selectively.

Role dependencies can be defined in meta/main.yml, ensuring prerequisite roles run first. This enables composing complex configurations from smaller, tested components.

# 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"

Variables and Precedence Questions

Understanding variable precedence is crucial for predictable playbook behavior.

How does Ansible variable precedence work?

Ansible has 22 levels of variable precedence, from role defaults (lowest) to extra vars (highest). Understanding this hierarchy prevents confusion when the same variable is defined in multiple places.

The key rules to remember: extra vars (-e) always win, role defaults always lose, and more specific definitions (host_vars) override less specific ones (group_vars). For predictable behavior, use role defaults for overridable values and avoid setting the same variable in multiple places.

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

What are Ansible facts and magic variables?

Facts are information about managed hosts that Ansible gathers automatically at playbook start. They include OS details, network configuration, hardware information, and more. Magic variables are special variables Ansible provides for accessing inventory and runtime information.

Facts enable conditional logic based on the target system—install different packages on Debian vs RedHat, configure memory-appropriate settings, or target specific IP addresses.

# 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

How do you disable fact gathering to speed up playbooks?

Fact gathering adds overhead—Ansible SSHes to each host and runs a discovery script. For simple tasks that don't need system information, disabling fact gathering significantly speeds up execution.

Set gather_facts: no at the play level when you don't need facts. You can also gather facts selectively using the setup module with specific subsets if you only need certain information.

- name: Quick playbook
  hosts: all
  gather_facts: no  # Skip if not needed
  tasks:
    - name: Just copy a file
      copy:
        src: file.txt
        dest: /tmp/

Templates and Jinja2 Questions

Templates enable dynamic configuration file generation.

How do you use Jinja2 templates in Ansible?

Templates use Jinja2 syntax to generate configuration files with dynamic content. Variables, loops, conditionals, and filters enable flexible configuration generation based on inventory data and facts.

Templates are stored with a .j2 extension in the templates/ directory of a role, or alongside playbooks. They're processed by the template module, which renders them with current variable values before copying to the target.

{# 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 %}
}

What are the most useful Jinja2 filters in Ansible?

Filters transform variable values in templates. Ansible provides many built-in filters for common operations like providing defaults, joining lists, hashing passwords, and converting between formats.

Knowing these filters helps you write cleaner templates and avoid complex logic in playbooks.

{{ 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 Questions

Vault encrypts sensitive data so secrets don't appear in plain text in repositories.

What is Ansible Vault and how do you use it?

Ansible Vault encrypts sensitive data like passwords, API keys, and certificates. You can encrypt entire files or individual variables. Decryption happens at runtime when you provide the vault password.

This enables storing secrets in version control safely—the encrypted content is unreadable without the password, but you maintain the benefits of Git history and collaboration.

# 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'

What is the best practice for organizing vault files?

A common pattern is separating vault variables into their own file, then referencing them from a plain-text variables file. This keeps your structure clear—you can see what variables exist without decrypting, and only the values are encrypted.

Use different vault files and passwords for different environments (production, staging) to limit blast radius if a password is compromised.

# 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 and Patterns Questions

These questions test understanding of Ansible patterns that distinguish experienced practitioners.

What is idempotency and how do you ensure it in Ansible?

Idempotency means running a playbook multiple times produces the same result as running it once. If nginx is already installed, the apt module reports "ok" not "changed". This property enables safe re-runs, drift correction, and reliable automation.

Most Ansible modules are idempotent by design. The exceptions are command and shell, which always report "changed" unless you configure them otherwise. When you must use these modules, add creates, removes, or changed_when to make them idempotent.

# 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

How should you organize an Ansible project directory?

A well-organized directory structure makes projects maintainable and enables multiple environments. The conventional structure separates inventory by environment, roles for reusable code, and playbooks for orchestration.

This structure scales from small projects to enterprise deployments and enables team collaboration with clear ownership of components.

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

How do you test Ansible roles with Molecule?

Molecule is the standard testing framework for Ansible roles. It creates isolated environments (Docker containers, VMs), applies your role, and runs verification tests. This catches bugs before they reach production.

Molecule integrates with CI/CD pipelines to test every change to your roles automatically. It supports multiple test instances for testing across different operating systems.

# Initialize molecule for existing role
cd roles/nginx
molecule init scenario -r nginx -d docker
 
# Run full test sequence
molecule test
 
# Just apply role (converge)
molecule converge
 
# Login to test instance
molecule login
 
# Destroy test environment
molecule destroy

Scaling and Performance Questions

Large-scale Ansible deployments require specific techniques.

How do you run Ansible against thousands of servers efficiently?

Running against large inventories requires parallelism, async tasks, and potentially switching to pull mode. Understanding these options shows experience with production-scale deployments.

The default fork count of 5 is too low for large deployments. Async tasks enable parallel execution without waiting for each host to complete. For very large scales, ansible-pull inverts the model—hosts pull and apply their own configuration.

# 1. Increase parallelism in 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 free strategy (don't wait for slowest host)
- hosts: all
  strategy: free
  tasks: ...
# 4. Pull mode - each host pulls and runs its own config
ansible-pull -U https://github.com/org/ansible-config.git

Debugging and Troubleshooting Questions

Debugging skills show practical experience with Ansible in production.

How do you debug a failing or intermittent playbook?

Ansible provides several debugging options from verbose output to step-by-step execution. Knowing these tools helps you diagnose issues efficiently.

Start with verbosity flags to see what's happening. Use --step for interactive execution, --start-at-task to resume from a specific point, and --check for dry runs. The debug module helps inspect variables during execution.

# 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"

How do you fix a task that always reports "changed"?

Tasks that always report "changed" break idempotency and make it impossible to detect actual changes. This commonly happens with command/shell modules or poorly configured tasks.

The solution depends on the cause: use changed_when: false for read-only operations, use creates/removes arguments for commands that create artifacts, or replace command/shell with native modules when available.

# 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

How do you handle task failures gracefully?

Block/rescue/always provides try-catch-finally semantics in Ansible. This enables error handling, cleanup tasks, and graceful degradation when tasks fail.

Use this pattern when you need to recover from errors, run cleanup regardless of success or failure, or implement complex error handling logic.

# 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"

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

This guide connects to the broader DevOps interview preparation:

Infrastructure as Code:

DevOps Fundamentals:

Cloud Platforms:

Ready to ace your interview?

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

View PDF Guides