Toolmingo
Guides28 min read

50 Ansible Interview Questions (With Answers)

Top Ansible interview questions with clear answers and examples — covering playbooks, roles, inventory, modules, Vault, Jinja2 templates, handlers, and production automation patterns.

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:

  1. Ansible reads the inventory to find target hosts
  2. Reads the playbook to determine tasks
  3. Connects via SSH (no agent needed on targets)
  4. Copies modules as temporary Python scripts to remote host
  5. Executes modules, captures results, removes temp files
  6. 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: defaultsgroup_varshost_varsplay/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 output
  • result.stderr — error output
  • result.rc — return code (0 = success)
  • result.stdout_lines — output as list
  • result.changed — whether task changed something
  • result.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_handlers forces 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 host
  • delegate_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):

  1. ANSIBLE_CONFIG env var
  2. ./ansible.cfg (current directory)
  3. ~/.ansible.cfg
  4. /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?

  1. Enable pipelining (pipelining = True)
  2. Increase forks (forks = 50)
  3. Disable fact gathering when not needed (gather_facts: false)
  4. Use fact caching (fact_caching = redis)
  5. Use strategy: free for independent hosts
  6. Use async for long-running tasks
  7. 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.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools