Jesse A. Green / notebook

Ansible — User Guide

Generated May 2026 from ansible/ansible at ae4d2b8

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Important

This guide covers ansible-core (the ansible/ansible repository), the foundational engine beneath all Ansible automation. Collections, roles, and Galaxy content are separate packages that build on top of ansible-core. Python 3.12 or higher is required on the controller node.


Table of Contents

  1. Quick Start
  2. Installation
  3. Configuration
  4. Inventory
  5. Ad-Hoc Commands
  6. Playbooks
  7. Roles
  8. Collections
  9. Vault — Secrets Management
  10. CLI Reference
  11. Built-in Module Reference
  12. Common Tasks & Workflow Recipes
  13. Troubleshooting
  14. FAQ

1. Quick Start

Get from zero to running your first Ansible command in under five minutes.

Prerequisites

  • A controller machine running Linux, macOS, or WSL2 with Python 3.12+
  • One or more managed nodes reachable via SSH (Linux/macOS) or WinRM (Windows)
  • SSH key-based authentication set up, or passwords available

Install and verify

pip install ansible-core
ansible --version

Expected output:

ansible [core 2.19.x]
  config file = None
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3/dist-packages/ansible
  ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.12.x (main, ...) [GCC ...]
  jinja2 version = 3.1.x
  libyaml = True

Create a minimal inventory

# inventory.ini
[webservers]
web1.example.com
web2.example.com

[dbservers]
db1.example.com ansible_user=postgres

Ping all hosts

ansible -i inventory.ini all -m ping

Expected output:

web1.example.com | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
web2.example.com | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
db1.example.com | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Run your first playbook

# site.yml
---
- name: Ensure nginx is running
  hosts: webservers
  become: true

  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Start and enable nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true
ansible-playbook -i inventory.ini site.yml

Tip

Always run ansible-playbook --syntax-check site.yml before executing a playbook against live hosts. It catches YAML/Jinja2 errors without touching any managed node.


2. Installation

System requirements

Component Requirement
Controller OS Linux, macOS, WSL2 (Windows not supported as controller)
Controller Python 3.12 or higher
Managed node Python 2.7+ or 3.x (varies by module; most require 3.x)
Managed node SSH OpenSSH 6.0+
Jinja2 >= 3.1.0
PyYAML >= 5.1
cryptography Latest stable
packaging Latest stable
resolvelib >= 0.8.0, < 2.0.0

Warning

Windows, native macOS with Python < 3.12, or any Python 2.x on the controller are not supported. The controller is where you run Ansible commands — managed nodes have separate, much looser requirements.

# Create an isolated virtualenv (best practice)
python3.12 -m venv ~/.venvs/ansible
source ~/.venvs/ansible/bin/activate

# Install ansible-core
pip install ansible-core

# Verify
ansible --version

Install a specific version

pip install ansible-core==2.19.0

Install with all optional dependencies

pip install ansible-core[cryptography]

Install from source (development / edge)

git clone https://github.com/ansible/ansible.git
cd ansible
pip install -e .

Distribution packages

Ubuntu / Debian:

sudo apt update
sudo apt install ansible-core

RHEL / CentOS / Fedora:

sudo dnf install ansible-core

macOS (Homebrew):

brew install ansible

Note

Distribution packages often lag behind the latest release. Use pip for the most current version of ansible-core.

Upgrading

pip install --upgrade ansible-core

Verify installation

# Check ansible-core version
ansible --version

# Confirm all CLI tools are available
ansible-playbook --version
ansible-galaxy --version
ansible-vault --version
ansible-config --version
ansible-doc --version
ansible-inventory --version
ansible-pull --version
ansible-console --version

Post-install: configure SSH

Ansible uses SSH by default. Ensure your SSH key is available:

# Generate key if needed
ssh-keygen -t ed25519 -C "ansible-controller"

# Copy to managed nodes
ssh-copy-id [email removed]

# Test connectivity
ansible -i inventory.ini all -m ping

3. Configuration

Ansible reads configuration from several sources, in priority order (highest first):

Source Example
Environment variables ANSIBLE_CONFIG=/path/to/ansible.cfg
./ansible.cfg Project-local config file
~/.ansible.cfg Per-user config file
/etc/ansible/ansible.cfg System-wide config file

Key environment variables

Variable Purpose Default
ANSIBLE_CONFIG Override config file path (auto-detected)
ANSIBLE_ASYNC_DIR Directory for async job files ~/.ansible_async
GIT_SSH SSH binary for git operations (system git)
GIT_SSH_COMMAND Full SSH command for git (system git)
GIT_SSH_OPTS Additional SSH options for git (none)
XDG_RUNTIME_DIR Runtime directory (used by some plugins) (system default)
SYSTEMD_OFFLINE Disable systemd queries (unset)
GITHUB_TOKEN Token for Galaxy/GitHub operations (none)

Generate a default ansible.cfg

ansible-config init --disabled > ansible.cfg

This writes every supported configuration key (commented out) so you can uncomment and tune what you need.

View current configuration

# Show all settings and their current values
ansible-config dump

# Show only settings that differ from defaults
ansible-config dump --only-changed

# Show all available config keys with documentation
ansible-config list

# View the active config file
ansible-config view

Validate your configuration

ansible-config validate

Common ansible.cfg settings

[defaults]
# Inventory source
inventory = ./inventory

# Remote user
remote_user = ubuntu

# SSH private key
private_key_file = ~/.ssh/id_ed25519

# Control number of parallel forks
forks = 10

# Disable host key checking (dev only — do not use in production)
host_key_checking = False

# Retry files
retry_files_enabled = False

# Timeout for SSH connections
timeout = 30

# Disable cowsay output
nocows = 1

# Stdout callback (yaml makes output more readable)
stdout_callback = yaml

# Log to file
log_path = /var/log/ansible.log

# Default module
module_name = command

# Roles path
roles_path = ./roles:~/.ansible/roles:/usr/share/ansible/roles

# Collections path
collections_path = ./collections:~/.ansible/collections

[privilege_escalation]
become = False
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
# Pipelining speeds up execution significantly
pipelining = True

# SSH multiplexing for performance
control_path_dir = ~/.ansible/cp
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s

Tip

Enable pipelining = True in [ssh_connection] for a significant performance boost. It reduces the number of SSH connections required per task. Requires that requiretty is not set for the remote user in /etc/sudoers.


4. Inventory

Inventory tells Ansible which hosts to manage and how to connect to them.

Inventory file formats

INI (default):

# inventory.ini

# Ungrouped hosts
ungrouped-host.example.com

[webservers]
web1.example.com
web2.example.com ansible_port=2222

[dbservers]
db1.example.com ansible_user=postgres ansible_python_interpreter=/usr/bin/python3

[datacenters:children]
webservers
dbservers

[datacenters:vars]
ansible_ssh_private_key_file=~/.ssh/dc_key

YAML:

# inventory.yml
all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
          ansible_port: 2222
    dbservers:
      hosts:
        db1.example.com:
          ansible_user: postgres
          ansible_python_interpreter: /usr/bin/python3
  vars:
    ansible_ssh_private_key_file: ~/.ssh/id_ed25519

Host variables

[webservers]
web1.example.com ansible_host=192.168.1.10 http_port=80 max_connections=200
web2.example.com ansible_host=192.168.1.11 http_port=8080

Group variables via directory

inventory/
  hosts.ini
  group_vars/
    all.yml          # vars for every host
    webservers.yml   # vars for webservers group
    dbservers/
      main.yml
      secrets.yml
  host_vars/
    web1.example.com.yml   # vars specific to web1

group_vars/webservers.yml:

http_port: 80
nginx_worker_processes: auto

Common host connection variables

Variable Purpose Example
ansible_host IP or hostname to connect to 192.168.1.10
ansible_port SSH port 22
ansible_user SSH user ubuntu
ansible_password SSH password (prefer keys) —
ansible_ssh_private_key_file SSH key path ~/.ssh/id_ed25519
ansible_python_interpreter Python path on remote /usr/bin/python3
ansible_become Enable privilege escalation true
ansible_become_user Become this user root
ansible_become_method Escalation method sudo
ansible_connection Connection plugin ssh, local, docker

Dynamic inventory

Dynamic inventory scripts or plugins return JSON that Ansible parses at runtime. The ansible-inventory CLI can wrap and inspect any inventory source:

# List all hosts in JSON
ansible-inventory -i inventory/ --list

# Show details for a specific host
ansible-inventory -i inventory/ --host web1.example.com

# Draw the group hierarchy
ansible-inventory -i inventory/ --graph

# Export as YAML
ansible-inventory -i inventory/ --list --yaml

# Export as TOML
ansible-inventory -i inventory/ --list --toml

# Include variable values in graph
ansible-inventory -i inventory/ --graph --vars

Inventory patterns

Pattern Meaning
all Every host in inventory
* Every host (wildcard)
webservers All hosts in group
webservers:dbservers Union of two groups
webservers:&dbservers Intersection (in both groups)
webservers:!dbservers In webservers but NOT in dbservers
web*.example.com Glob match
webservers[0] First host in group
webservers[0:2] First three hosts
~web[0-9]+\.example\.com Regex match

5. Ad-Hoc Commands

The ansible command runs a single module against hosts without a playbook.

Syntax

ansible <pattern> [options] -m <module> -a '<arguments>'

Essential flags

Flag Purpose Example
-i Inventory file/directory -i inventory.ini
-m Module name -m ping
-a Module arguments -a 'cmd=uptime'
-u / --user Remote user -u ubuntu
-b / --become Escalate privileges -b
--become-user User to become --become-user root
-k / --ask-pass Prompt for SSH password -k
-K / --ask-become-pass Prompt for sudo password -K
--private-key SSH private key file --private-key ~/.ssh/key
-f / --forks Parallel forks -f 20
-C / --check Dry-run mode -C
-D / --diff Show file diffs -D
-v / -vv / -vvv Verbosity -vvv
-o / --one-line Compact output -o
-t / --tree Save output to directory -t /tmp/out
--timeout Connection timeout (seconds) --timeout 30
-l / --limit Limit to subset of hosts -l web1.example.com
-e / --extra-vars Extra variables -e "key=val"
--list-hosts Show matching hosts, don't run --list-hosts

Common ad-hoc examples

# Ping all hosts
ansible all -i inventory.ini -m ping

# Run a shell command
ansible webservers -i inventory.ini -m shell -a 'uptime'

# Run a raw command (no Python required on remote)
ansible all -i inventory.ini -m raw -a 'uname -a'

# Copy a file
ansible webservers -i inventory.ini -m copy -a 'src=/etc/hosts dest=/tmp/hosts'

# Fetch a file from remote
ansible webservers -i inventory.ini -m fetch -a 'src=/var/log/nginx/access.log dest=/tmp/logs/ flat=yes'

# Install a package
ansible webservers -i inventory.ini -m package -a 'name=nginx state=present' -b

# Remove a package
ansible webservers -i inventory.ini -m package -a 'name=nginx state=absent' -b

# Restart a service
ansible webservers -i inventory.ini -m service -a 'name=nginx state=restarted' -b

# Create a directory
ansible all -i inventory.ini -m file -a 'path=/opt/myapp state=directory mode=0755' -b

# Delete a file
ansible all -i inventory.ini -m file -a 'path=/tmp/oldfile state=absent'

# Check disk space (gather_facts-style)
ansible all -i inventory.ini -m shell -a 'df -h'

# Reboot all hosts
ansible all -i inventory.ini -m reboot -b

# Gather facts and show them
ansible web1.example.com -i inventory.ini -m setup

# Filter facts
ansible web1.example.com -i inventory.ini -m setup -a 'filter=ansible_distribution*'

# Run as a different user
ansible webservers -i inventory.ini -m shell -a 'whoami' -b --become-user postgres

6. Playbooks

Playbooks are YAML files that define automation as ordered lists of plays. Each play runs tasks against a set of hosts.

Playbook anatomy

---
- name: Configure web servers        # Play name (required for clarity)
  hosts: webservers                  # Host pattern
  become: true                       # Privilege escalation for whole play
  gather_facts: true                 # Gather system facts (default: true)
  vars:                              # Play-level variables
    http_port: 80
    app_name: myapp
  vars_files:                        # Load vars from files
    - vars/common.yml
    - vars/secrets.yml

  pre_tasks:                         # Run before roles
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

  roles:                             # Include roles
    - nginx
    - { role: app_deploy, app_version: "1.2.3" }

  tasks:                             # Main task list
    - name: Ensure config exists
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: Reload nginx           # Trigger handler on change

    - name: Ensure firewall allows HTTP
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        destination_port: "{{ http_port }}"
        jump: ACCEPT

  post_tasks:                        # Run after roles
    - name: Verify nginx responds
      ansible.builtin.uri:
        url: http://localhost
        status_code: 200

  handlers:                          # Triggered by notify
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Running playbooks

# Basic run
ansible-playbook -i inventory.ini site.yml

# Dry run (check mode)
ansible-playbook -i inventory.ini site.yml --check

# Dry run with diffs
ansible-playbook -i inventory.ini site.yml --check --diff

# Syntax check (no execution)
ansible-playbook site.yml --syntax-check

# List all tasks (without running)
ansible-playbook site.yml --list-tasks

# List all tags
ansible-playbook site.yml --list-tags

# List all matching hosts
ansible-playbook -i inventory.ini site.yml --list-hosts

# Step-through interactively (confirm each task)
ansible-playbook -i inventory.ini site.yml --step

# Start from a specific task
ansible-playbook -i inventory.ini site.yml --start-at-task "Ensure config exists"

# Run only tasks with specific tags
ansible-playbook -i inventory.ini site.yml --tags "nginx,config"

# Skip tasks with specific tags
ansible-playbook -i inventory.ini site.yml --skip-tags "testing"

# Limit to specific hosts
ansible-playbook -i inventory.ini site.yml --limit webservers

# Pass extra variables
ansible-playbook -i inventory.ini site.yml -e "app_version=1.2.3 env=production"

# Pass extra variables from a file
ansible-playbook -i inventory.ini site.yml -e "@vars/overrides.yml"

# Increase verbosity
ansible-playbook -i inventory.ini site.yml -vvv

# Vault password
ansible-playbook -i inventory.ini site.yml --vault-password-file ~/.vault_pass
ansible-playbook -i inventory.ini site.yml --ask-vault-pass

Important

--start-at-task as of ansible-core 2.19 no longer implicitly gathers facts before starting at the named task (see GitHub issue #86268). If your tasks depend on facts, add an explicit gather_facts: true task before the start point, or run a full play first.

Task control

Tags

tasks:
  - name: Install packages
    ansible.builtin.package:
      name: "{{ item }}"
      state: present
    loop:
      - nginx
      - curl
    tags:
      - packages
      - install

  - name: Configure nginx
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    tags:
      - config
      - nginx

Conditionals

tasks:
  - name: Install on Debian/Ubuntu
    ansible.builtin.apt:
      name: nginx
      state: present
    when: ansible_os_family == "Debian"

  - name: Install on RHEL/CentOS
    ansible.builtin.dnf:
      name: nginx
      state: present
    when: ansible_os_family == "RedHat"

  - name: Run only in production
    ansible.builtin.debug:
      msg: "Production task"
    when:
      - env == "production"
      - ansible_memtotal_mb > 4096

Loops

tasks:
  - name: Create multiple users
    ansible.builtin.user:
      name: "{{ item.name }}"
      groups: "{{ item.groups }}"
      state: present
    loop:
      - { name: alice, groups: wheel }
      - { name: bob, groups: docker }
      - { name: carol, groups: wheel,docker }

  - name: Install many packages
    ansible.builtin.package:
      name: "{{ item }}"
      state: present
    loop: "{{ packages }}"

  - name: Loop with index
    ansible.builtin.debug:
      msg: "Item {{ ansible_loop.index }}: {{ item }}"
    loop: [a, b, c]
    loop_control:
      label: "{{ item }}"

Error handling

tasks:
  - name: Attempt risky operation
    ansible.builtin.command: /opt/risky-script.sh
    ignore_errors: true
    register: result

  - name: Check result
    ansible.builtin.fail:
      msg: "Script failed with: {{ result.stderr }}"
    when: result.rc != 0 and result.rc != 2

  - name: Run with retries
    ansible.builtin.uri:
      url: http://api.example.com/health
      status_code: 200
    register: health
    until: health.status == 200
    retries: 10
    delay: 5

  - name: Always clean up
    ansible.builtin.file:
      path: /tmp/workdir
      state: absent
    always:  # runs even if play fails (use in block)

Blocks

tasks:
  - name: Handle deployment errors
    block:
      - name: Deploy application
        ansible.builtin.command: /opt/deploy.sh

      - name: Run smoke test
        ansible.builtin.uri:
          url: http://localhost/health
    rescue:
      - name: Roll back on failure
        ansible.builtin.command: /opt/rollback.sh

      - name: Send alert
        ansible.builtin.debug:
          msg: "Deployment failed, rolled back"
    always:
      - name: Clean up temp files
        ansible.builtin.file:
          path: /tmp/deploy_work
          state: absent

Async tasks

tasks:
  - name: Start long-running job
    ansible.builtin.command: /opt/long-job.sh
    async: 3600   # max seconds to run
    poll: 0       # fire-and-forget (poll=0 means don't wait)
    register: job

  - name: Check job status later
    ansible.builtin.async_status:
      jid: "{{ job.ansible_job_id }}"
    register: status
    until: status.finished
    retries: 60
    delay: 30

Handlers

Handlers run once at the end of a play if notified, and only if changes occurred.

tasks:
  - name: Update nginx config
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify:
      - Validate nginx config
      - Reload nginx

handlers:
  - name: Validate nginx config
    ansible.builtin.command: nginx -t
    listen: Validate nginx config

  - name: Reload nginx
    ansible.builtin.service:
      name: nginx
      state: reloaded

Variables

# Variable precedence (low to high)
# role defaults → inventory file vars → inventory group_vars/all →
# inventory group_vars/* → inventory host_vars/* → playbook group_vars/all →
# playbook group_vars/* → playbook host_vars/* → host facts →
# play vars → play vars_prompt → play vars_files → role vars →
# block vars → task vars → include_vars → set_fact →
# registered vars → extra vars (-e)

vars:
  app_name: myapp
  app_port: 8080

vars_files:
  - vars/defaults.yml
  - "vars/{{ ansible_os_family }}.yml"   # conditional file loading

vars_prompt:
  - name: deploy_pass
    prompt: "Deployment password"
    private: true

set_fact

- name: Compute derived value
  ansible.builtin.set_fact:
    app_url: "http://{{ ansible_host }}:{{ app_port }}"
    cacheable: true   # persist across plays

Registered variables

- name: Get current version
  ansible.builtin.command: myapp --version
  register: version_output
  changed_when: false

- name: Show version
  ansible.builtin.debug:
    msg: "Running version: {{ version_output.stdout }}"

Jinja2 templating

vars:
  name: world

tasks:
  - name: String interpolation
    ansible.builtin.debug:
      msg: "Hello, {{ name }}!"

  - name: Conditional expression
    ansible.builtin.debug:
      msg: "{{ 'prod' if env == 'production' else 'dev' }}"

  - name: Default filter
    ansible.builtin.debug:
      msg: "{{ optional_var | default('fallback') }}"

  - name: List manipulation
    ansible.builtin.debug:
      msg: "{{ my_list | join(', ') }}"

  - name: Dict operations
    ansible.builtin.debug:
      msg: "{{ my_dict | dict2items | selectattr('key', 'match', '^app_') | list }}"

Import vs. Include

# Static import (processed at parse time, tags work fully)
- import_tasks: tasks/setup.yml
- import_role:
    name: nginx

# Dynamic include (processed at runtime, can use variables in path)
- include_tasks: "tasks/{{ ansible_os_family }}.yml"
- include_role:
    name: "{{ chosen_role }}"

# Import a whole playbook
- import_playbook: playbooks/database.yml

Note

Use import_* (static) when you need tags and conditionals at parse time. Use include_* (dynamic) when the filename or role name must be computed at runtime from variables. They cannot be freely interchanged.


7. Roles

Roles package reusable automation into a standard directory structure.

Role directory structure

roles/
  myrole/
    defaults/
      main.yml       # Default variables (lowest precedence)
    vars/
      main.yml       # Role variables (high precedence)
    tasks/
      main.yml       # Main task list
      install.yml    # Sub-task files (imported from main.yml)
    handlers/
      main.yml       # Handlers for this role
    templates/
      nginx.conf.j2  # Jinja2 templates
    files/
      index.html     # Static files
    meta/
      main.yml       # Role metadata, dependencies
    tests/
      inventory
      test.yml
    README.md

Create a role scaffold

ansible-galaxy role init myrole

Expected output:

- Role myrole was created successfully

Using roles in playbooks

# Method 1: roles key
- hosts: webservers
  roles:
    - nginx
    - role: app_deploy
      vars:
        app_version: "1.2.3"
      tags: [deploy]
    - role: monitoring
      when: enable_monitoring | bool

# Method 2: import_role (static)
- hosts: webservers
  tasks:
    - import_role:
        name: nginx
      vars:
        http_port: 8080

# Method 3: include_role (dynamic)
- hosts: webservers
  tasks:
    - include_role:
        name: "{{ selected_role }}"

Role dependencies (meta/main.yml)

# roles/myrole/meta/main.yml
galaxy_info:
  author: yourname
  description: My role description
  license: MIT
  min_ansible_version: "2.15"
  platforms:
    - name: Ubuntu
      versions: [22.04, 24.04]

dependencies:
  - role: geerlingguy.nginx
    vars:
      nginx_vhosts: []
  - role: common

8. Collections

Collections are distribution packages for Ansible content: modules, plugins, roles, and playbooks grouped under a namespace.

ansible-galaxy collection commands

# Install a collection
ansible-galaxy collection install community.general

# Install a specific version
ansible-galaxy collection install community.general:==8.0.0

# Install from requirements file
ansible-galaxy collection install -r requirements.yml

# List installed collections
ansible-galaxy collection list

# Download (without installing)
ansible-galaxy collection download community.general -p ./downloads

# Verify a collection
ansible-galaxy collection verify community.general

# Build a collection (from inside the collection)
ansible-galaxy collection build

# Publish to Galaxy
ansible-galaxy collection publish namespace-name-1.0.0.tar.gz --token YOUR_TOKEN

requirements.yml

---
collections:
  - name: community.general
    version: ">=8.0.0"
  - name: ansible.posix
  - name: community.crypto
    version: "2.21.0"

roles:
  - name: geerlingguy.nginx
    version: "3.2.0"
  - src: https://github.com/example/role
    name: custom_role
    version: main
# Install both collections and roles from requirements.yml
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install -r requirements.yml

ansible-galaxy role commands

# Initialize a new role
ansible-galaxy role init myrole

# Install a role from Galaxy
ansible-galaxy role install geerlingguy.nginx

# Install to a specific path
ansible-galaxy role install geerlingguy.nginx -p ./roles

# List installed roles
ansible-galaxy role list

# Search Galaxy for roles
ansible-galaxy role search nginx --author geerlingguy

# Get role info
ansible-galaxy role info geerlingguy.nginx

# Remove a role
ansible-galaxy role remove geerlingguy.nginx

# Import role to Galaxy (requires Galaxy account)
ansible-galaxy role import github_user github_repo

# Delete role from Galaxy
ansible-galaxy role delete namespace rolename

# Setup GitHub webhooks for auto-import
ansible-galaxy role setup github github_user github_repo secret

Using FQCN (Fully Qualified Collection Name)

tasks:
  # Unqualified (relies on module search path)
  - name: Create file
    file:
      path: /tmp/test
      state: touch

  # Fully qualified (unambiguous, best practice)
  - name: Create file
    ansible.builtin.file:
      path: /tmp/test
      state: touch

  # From a collection
  - name: Use community module
    community.general.ufw:
      rule: allow
      port: 80

Tip

Always use FQCN (e.g., ansible.builtin.file) in production playbooks. It makes the module source unambiguous and avoids conflicts when multiple collections provide modules with the same name.


9. Vault — Secrets Management

ansible-vault encrypts sensitive data so it can be safely stored in version control.

Create an encrypted file

ansible-vault create secrets.yml

Opens your editor ($EDITOR). Write your secrets in YAML, then save and close. The file is encrypted with AES-256.

Encrypt an existing file

ansible-vault encrypt vars/database.yml
ansible-vault encrypt vars/database.yml --vault-id prod@prompt

Decrypt a file (permanent, in place)

ansible-vault decrypt vars/database.yml

View encrypted file contents

ansible-vault view secrets.yml

Edit an encrypted file

ansible-vault edit secrets.yml

Encrypt a single string value

ansible-vault encrypt_string 'my-secret-password' --name 'db_password'

Output:

db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          38613561303...

Paste this directly into a vars file.

Re-key (change the vault password)

ansible-vault rekey secrets.yml

Using vault in playbooks

# Prompt for vault password
ansible-playbook site.yml --ask-vault-pass

# Read password from file
ansible-playbook site.yml --vault-password-file ~/.vault_pass

# Use vault-id (multiple passwords)
ansible-playbook site.yml --vault-id dev@~/.vault_pass_dev --vault-id prod@prompt

# Inline vault password file in ansible.cfg
# vault_password_file = ~/.vault_pass

Multiple vault IDs

# Encrypt with specific vault ID
ansible-vault encrypt_string 'prod-secret' --vault-id prod@prompt --name prod_db_pass
ansible-vault encrypt_string 'dev-secret' --vault-id dev@~/.dev_vault --name dev_db_pass

Best practices for vault

# Store vault password file outside repo
echo 'my-strong-password' > ~/.vault_pass
chmod 600 ~/.vault_pass

# Reference in ansible.cfg
# vault_password_file = ~/.vault_pass

# Only encrypt the values, not the whole file
# vars/secrets.yml (commit this)
db_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  ...
db_host: db1.example.com    # not sensitive, not encrypted

# Or use a separate encrypted vars file
# vars_files:
#   - vars/public.yml
#   - vars/secrets.yml    # fully encrypted

Caution

Never store unencrypted secrets in version control. If a secret is accidentally committed, rotate it immediately — git history is permanent. Use ansible-vault encrypt_string for individual values within otherwise-plain YAML files.


10. CLI Reference

ansible — Ad-hoc command executor

ansible <pattern> [options]

Core options:

Option Short Description
--inventory -i Inventory host file or source
--module-name -m Module to execute (default: command)
--args -a Module arguments
--user -u Remote user
--become -b Run operations with privilege escalation
--become-user User to become (default: root)
--become-method Escalation method (sudo, su, etc.)
--ask-pass -k Prompt for SSH password
--ask-become-pass -K Prompt for privilege escalation password
--private-key SSH private key file
--forks -f Number of parallel processes (default: 5)
--check -C Dry-run mode
--diff -D Show file changes
--one-line -o Condense output
--tree -t Log output to directory
--limit -l Limit to host/group subset
--extra-vars -e Set variables as k=v or YAML/JSON
--list-hosts Show matching hosts only
--timeout Override connection timeout
--verbose -v Verbosity (repeat for more: -vvvv)
--version Show version and exit

ansible-playbook — Playbook executor

ansible-playbook [options] playbook.yml [playbook2.yml ...]

All options from ansible plus:

Option Short Description
--syntax-check Check YAML/Jinja2 syntax only
--list-tasks List all tasks that will run
--list-tags List all available tags
--list-hosts List matching hosts
--tags -t Only run tasks with these tags
--skip-tags Skip tasks with these tags
--step Confirm each task interactively
--start-at-task Start at this task name
--flush-cache Clear fact cache for hosts
--force-handlers Run handlers even if a task fails
--vault-id Vault identity and source
--vault-password-file Vault password file
--ask-vault-pass Prompt for vault password

ansible-galaxy — Content manager

Collection commands:

ansible-galaxy collection init namespace.collection_name
ansible-galaxy collection build [path] [--output-path dir]
ansible-galaxy collection publish archive.tar.gz [--token TOKEN]
ansible-galaxy collection install namespace.name [--upgrade]
ansible-galaxy collection install -r requirements.yml [--upgrade]
ansible-galaxy collection list [namespace.name]
ansible-galaxy collection download namespace.name [-p path]
ansible-galaxy collection verify namespace.name

Role commands:

ansible-galaxy role init role_name [--offline] [--role-skeleton path]
ansible-galaxy role install namespace.role [--force] [-p path]
ansible-galaxy role install -r requirements.yml
ansible-galaxy role list [role_name]
ansible-galaxy role remove role_name [role_name2 ...]
ansible-galaxy role search search_term [--author NAME] [--galaxy-tags TAGS]
ansible-galaxy role info namespace.role_name
ansible-galaxy role import github_user github_repo [--branch BRANCH]
ansible-galaxy role setup github github_user github_repo secret
ansible-galaxy role delete namespace rolename

Global options:

Option Description
--server Galaxy server URL
--token Authentication token
--timeout Socket timeout
-c / --ignore-certs Ignore TLS certificate errors
-p / --roles-path Roles path

ansible-vault — Encryption tool

ansible-vault create [options] file.yml
ansible-vault decrypt [options] file.yml
ansible-vault edit [options] file.yml
ansible-vault encrypt [options] file.yml [file2.yml ...]
ansible-vault encrypt_string [options] [string] --name VAR_NAME
ansible-vault rekey [options] file.yml [file2.yml ...]
ansible-vault view [options] file.yml

Common options:

Option Description
--vault-id Vault identity to use (label@source)
--vault-password-file Password file path
--ask-vault-pass Interactive password prompt
--new-vault-id New vault-id for rekey
--new-vault-password-file New password file for rekey
--encrypt-vault-id Vault ID to use for encrypting
--output Output file (for decrypt/encrypt, default: in-place)

ansible-config — Configuration inspector

ansible-config list [--format {default,ini,env,vars}] [filter_pattern]
ansible-config dump [--only-changed] [filter_pattern]
ansible-config view
ansible-config init [--disabled] [--format {ini,env}]
ansible-config validate

Options:

Option Description
--only-changed Show only settings that differ from defaults
--disabled Include commented-out keys in init output
--format Output format
--type Limit to settings of this type

ansible-doc — Plugin documentation viewer

ansible-doc [options] [plugin_name ...]
Option Short Description
--type -t Plugin type: module, become, cache, callback, cliconf, connection, httpapi, inventory, lookup, netconf, shell, vars, strategy, test, filter, role, keyword
--list -l List available plugins
--snippet -s Show playbook snippet
--json -j Output as JSON
--module-path -M Additional module paths
--playbook-dir Base playbook directory
# Get documentation for a module
ansible-doc ansible.builtin.file

# Show a task snippet
ansible-doc -s ansible.builtin.file

# List all built-in modules
ansible-doc -l -t module ansible.builtin

# JSON output
ansible-doc -j ansible.builtin.copy

# List lookup plugins
ansible-doc -l -t lookup

# List connection plugins
ansible-doc -l -t connection

ansible-inventory — Inventory inspector

ansible-inventory [options] [host|group]
Option Description
--list List all host variables as JSON
--host HOST Output JSON for specific host
--graph [GROUP] Show group hierarchy graph
--yaml Use YAML output format
--toml Use TOML output format
--vars Include variables in --graph output
--export Use --list output for export (less readable)
--output FILE Write to file instead of stdout
-i Inventory source
-l / --limit Limit to hosts/groups

ansible-console — Interactive REPL

ansible-console [options] [pattern]

Launches an interactive shell where you can run modules against hosts:

ansible-console -i inventory.ini webservers
Welcome to the ansible console.
Type help or ? to list commands.

root@webservers (2)[f:5]$ ping
web1.example.com | SUCCESS => { "changed": false, "ping": "pong" }
web2.example.com | SUCCESS => { "changed": false, "ping": "pong" }

root@webservers (2)[f:5]$ shell uptime
web1.example.com | CHANGED | rc=0 >>
 10:23:01 up 14 days,  3:12,  1 user,  load average: 0.02, 0.04, 0.01

root@webservers (2)[f:5]$ cd dbservers
root@dbservers (1)[f:5]$ become true
root@dbservers (1)[f:5]$ service name=postgresql state=restarted

Console commands: cd [group], become [true/false], become_user [user], forks [N], verbosity [N], exit/quit.


ansible-pull — Pull-mode automation

Ansible-pull inverts the usual push model: managed nodes pull their own playbooks from git and run them locally.

ansible-pull [options] [playbook.yml]
Option Description
-U / --url Git repository URL (required)
-C / --checkout Branch/tag/commit to checkout
-d / --directory Directory to clone repo into
-i / --inventory Inventory source
-m / --module-name Module for checkout (default: git)
--accept-host-key Accept the host key of the checkout server
--clean Modified files in the repo will be discarded
--full Do a full clone (not shallow)
--track-subs Track submodule updates
--verify-commit Verify GPG signature on commit
--sleep Sleep random interval before pull (prevents stampede)
--key-file Private key for SSH checkout
# Basic pull
ansible-pull -U https://github.com/example/ansible-config.git

# Pull from private repo with key
ansible-pull -U git@github.com:example/config.git --key-file ~/.ssh/deploy_key

# Pull specific branch
ansible-pull -U https://github.com/example/config.git -C production

# With random sleep (for cron jobs with many nodes)
ansible-pull -U https://github.com/example/config.git --sleep 60

Note

GitHub issue #86889 proposes deprecating ansible-pull. Check the release notes for your version to see if deprecation warnings apply.


11. Built-in Module Reference

All 72 built-in modules are in the ansible.builtin namespace. Use ansible-doc ansible.builtin.<module> for full parameter documentation.

Package management

Module Purpose Platforms
ansible.builtin.package Generic package manager (auto-detects) All
ansible.builtin.apt Apt package manager Debian/Ubuntu
ansible.builtin.apt_key Manage apt keys Debian/Ubuntu
ansible.builtin.apt_repository Manage apt repos Debian/Ubuntu
ansible.builtin.deb822_repository Manage deb822-format apt repos Debian/Ubuntu
ansible.builtin.debconf Configure packages via debconf Debian/Ubuntu
ansible.builtin.dpkg_selections Manage dpkg package selections Debian/Ubuntu
ansible.builtin.dnf DNF package manager RHEL/Fedora
ansible.builtin.dnf5 DNF5 package manager RHEL/Fedora 40+
ansible.builtin.rpm_key Manage RPM keys RPM-based
ansible.builtin.yum_repository Manage yum/dnf repos RPM-based
ansible.builtin.pip Python pip package manager All

Common package examples:

- name: Install single package
  ansible.builtin.package:
    name: nginx
    state: present

- name: Install multiple packages
  ansible.builtin.package:
    name:
      - nginx
      - curl
      - git
    state: present

- name: Remove package
  ansible.builtin.package:
    name: apache2
    state: absent

- name: Install specific version (apt)
  ansible.builtin.apt:
    name: nginx=1.18.*
    state: present
    update_cache: true
    cache_valid_time: 3600

- name: Install pip package into virtualenv
  ansible.builtin.pip:
    name: django
    version: "4.2"
    virtualenv: /opt/myapp/venv

Service management

Module Purpose
ansible.builtin.service Generic service manager
ansible.builtin.systemd_service systemd-specific service management
- name: Start and enable service
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

- name: Reload service
  ansible.builtin.service:
    name: nginx
    state: reloaded

- name: Systemd service with daemon-reload
  ansible.builtin.systemd_service:
    name: myapp
    state: started
    enabled: true
    daemon_reload: true

- name: Get service facts
  ansible.builtin.service_facts:

- name: Check if nginx is running
  ansible.builtin.debug:
    msg: "nginx is {{ ansible_facts.services['nginx'].state }}"
  when: "'nginx' in ansible_facts.services"

File management

Module Purpose
ansible.builtin.file Manage files, dirs, symlinks
ansible.builtin.copy Copy files to remote
ansible.builtin.template Process Jinja2 template and copy
ansible.builtin.fetch Fetch files from remote
ansible.builtin.find Find files/dirs on remote
ansible.builtin.stat Get file/dir status
ansible.builtin.lineinfile Manage single lines in files
ansible.builtin.replace Replace text in files (regex)
ansible.builtin.assemble Assemble config from fragments
ansible.builtin.unarchive Unpack archives
ansible.builtin.tempfile Create temporary files/dirs
ansible.builtin.slurp Read file content as base64
- name: Create directory
  ansible.builtin.file:
    path: /opt/myapp
    state: directory
    owner: appuser
    group: appgroup
    mode: '0755'

- name: Symlink
  ansible.builtin.file:
    src: /opt/myapp/current
    dest: /opt/myapp/live
    state: link

- name: Copy file with checksum
  ansible.builtin.copy:
    src: files/nginx.conf
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    backup: true

- name: Template with variables
  ansible.builtin.template:
    src: templates/app.conf.j2
    dest: /etc/myapp/app.conf
    owner: appuser
    mode: '0640'
    validate: '/usr/sbin/myapp --check %s'

- name: Ensure line in file
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^PasswordAuthentication'
    line: 'PasswordAuthentication no'
    backup: true

- name: Replace pattern
  ansible.builtin.replace:
    path: /etc/hosts
    regexp: '(\s+)old\.hostname(\s+.*)?$'
    replace: '\1new.hostname\2'

- name: Find old log files
  ansible.builtin.find:
    paths: /var/log/myapp
    patterns: "*.log"
    age: 30d
    recurse: true
  register: old_logs

- name: Remove old logs
  ansible.builtin.file:
    path: "{{ item.path }}"
    state: absent
  loop: "{{ old_logs.files }}"

- name: Get file stats
  ansible.builtin.stat:
    path: /opt/myapp/config.yml
  register: config_stat

- name: Act if file exists
  ansible.builtin.debug:
    msg: "Config exists, size: {{ config_stat.stat.size }}"
  when: config_stat.stat.exists

- name: Unpack archive
  ansible.builtin.unarchive:
    src: https://example.com/myapp-1.0.tar.gz
    dest: /opt/myapp
    remote_src: true   # download on remote node
    creates: /opt/myapp/myapp-1.0

- name: Read file contents
  ansible.builtin.slurp:
    src: /etc/ssl/cert.pem
  register: cert_content

- name: Use slurped content
  ansible.builtin.debug:
    msg: "{{ cert_content.content | b64decode }}"

User and group management

Module Purpose
ansible.builtin.user Manage user accounts
ansible.builtin.group Manage groups
- name: Create user
  ansible.builtin.user:
    name: appuser
    group: appgroup
    groups: [docker, sudo]
    shell: /bin/bash
    home: /home/appuser
    create_home: true
    password: "{{ user_password | password_hash('sha512') }}"
    state: present

- name: Create system user (no home, no login)
  ansible.builtin.user:
    name: nginx
    system: true
    shell: /usr/sbin/nologin
    state: present

- name: Create group
  ansible.builtin.group:
    name: appgroup
    gid: 1500
    state: present

Command execution

Module Purpose
ansible.builtin.command Run command (no shell, no pipes)
ansible.builtin.shell Run shell command (full shell features)
ansible.builtin.raw Run raw SSH command (no Python required)
ansible.builtin.script Run local script on remote
ansible.builtin.expect Run interactive command with expected responses
- name: Run command
  ansible.builtin.command:
    cmd: /opt/myapp/bin/migrate
    chdir: /opt/myapp
    creates: /opt/myapp/.migrated   # skip if this file exists

- name: Shell with pipes
  ansible.builtin.shell:
    cmd: "ps aux | grep nginx | grep -v grep"
  register: nginx_procs
  changed_when: false

- name: Run local script
  ansible.builtin.script:
    cmd: scripts/setup.sh
    chdir: /opt/myapp

- name: Bootstrap a new node (no Python yet)
  ansible.builtin.raw: "apt-get install -y python3"

- name: Interactive command
  ansible.builtin.expect:
    command: passwd appuser
    responses:
      'New password:': "{{ new_password }}"
      'Retype new password:': "{{ new_password }}"

Note

Prefer ansible.builtin.command over shell when you don't need shell features (pipes, redirects, environment variable expansion). command is safer and its output is more predictable.


Network and web

Module Purpose
ansible.builtin.uri Interact with web services (HTTP/HTTPS)
ansible.builtin.get_url Download files from URLs
ansible.builtin.iptables Manage iptables firewall rules
ansible.builtin.known_hosts Manage SSH known_hosts
ansible.builtin.wait_for Wait for conditions (port, file, etc.)
ansible.builtin.wait_for_connection Wait for host to become reachable
- name: POST to API
  ansible.builtin.uri:
    url: https://api.example.com/deploy
    method: POST
    body_format: json
    body:
      version: "{{ app_version }}"
      env: production
    headers:
      Authorization: "Bearer {{ api_token }}"
    status_code: [200, 201]
  register: api_response

- name: Download binary
  ansible.builtin.get_url:
    url: https://github.com/example/releases/download/v1.0/app-linux-amd64
    dest: /usr/local/bin/app
    mode: '0755'
    checksum: sha256:abc123...

- name: Allow HTTP through iptables
  ansible.builtin.iptables:
    chain: INPUT
    protocol: tcp
    destination_port: "80"
    jump: ACCEPT
    state: present
    comment: "Allow HTTP"

- name: Add host to known_hosts
  ansible.builtin.known_hosts:
    name: github.com
    key: "{{ lookup('pipe', 'ssh-keyscan github.com') }}"
    state: present

- name: Wait for port to be open
  ansible.builtin.wait_for:
    host: "{{ inventory_hostname }}"
    port: 8080
    timeout: 60
    state: started

- name: Wait for host after reboot
  ansible.builtin.wait_for_connection:
    delay: 10
    timeout: 300

Source control

Module Purpose
ansible.builtin.git Manage git repositories
ansible.builtin.subversion Manage subversion checkouts
- name: Clone repository
  ansible.builtin.git:
    repo: https://github.com/example/myapp.git
    dest: /opt/myapp
    version: main
    depth: 1
    force: false

- name: Deploy specific tag
  ansible.builtin.git:
    repo: git@github.com:example/myapp.git
    dest: /opt/myapp
    version: "v1.2.3"
    key_file: /home/deploy/.ssh/deploy_key
    accept_hostkey: true

Warning

The git module can lose local commits if force: true is set and the repository has diverged from the remote (GitHub issue #83367). Only use force: true when you are certain local changes should be discarded.


System management

Module Purpose
ansible.builtin.setup Gather system facts
ansible.builtin.package_facts Gather installed package facts
ansible.builtin.gather_facts Explicitly trigger fact gathering
ansible.builtin.hostname Manage system hostname
ansible.builtin.cron Manage cron jobs
ansible.builtin.mount Manage mounts
ansible.builtin.reboot Reboot hosts
ansible.builtin.pause Pause playbook execution
ansible.builtin.getent Query getent database
- name: Set hostname
  ansible.builtin.hostname:
    name: web1.example.com

- name: Add cron job
  ansible.builtin.cron:
    name: "Backup database"
    minute: "0"
    hour: "2"
    job: "/opt/backup.sh >> /var/log/backup.log 2>&1"
    user: backup

- name: Mount NFS share
  ansible.builtin.mount:
    src: nfs.example.com:/exports/data
    path: /mnt/data
    fstype: nfs
    opts: ro,nosuid
    state: mounted

- name: Reboot and wait for return
  ansible.builtin.reboot:
    reboot_timeout: 300
    msg: "Rebooting for kernel update"

- name: Pause for confirmation
  ansible.builtin.pause:
    prompt: "Press enter to continue deployment to production"

- name: Wait N seconds
  ansible.builtin.pause:
    seconds: 30

- name: Lookup user by name
  ansible.builtin.getent:
    database: passwd
    key: nginx

Control flow modules

Module Purpose
ansible.builtin.assert Fail if conditions are not true
ansible.builtin.fail Explicitly fail with message
ansible.builtin.debug Print debug information
ansible.builtin.set_fact Set host variables
ansible.builtin.set_stats Set stats for callback plugins
ansible.builtin.meta Execute Ansible meta-actions
ansible.builtin.add_host Add a host to inventory at runtime
ansible.builtin.group_by Group hosts by fact values
ansible.builtin.ping Test connectivity
ansible.builtin.async_status Check async job status
ansible.builtin.async_wrapper Internal async wrapper
ansible.builtin.validate_argument_spec Validate task arguments
- name: Assert Python version
  ansible.builtin.assert:
    that:
      - ansible_python_version is version('3.8', '>=')
    fail_msg: "Python 3.8+ required, found {{ ansible_python_version }}"
    success_msg: "Python version OK: {{ ansible_python_version }}"

- name: Fail with message
  ansible.builtin.fail:
    msg: "Disk space below 20%: {{ disk_usage }}%"
  when: disk_usage | int > 80

- name: Debug variable
  ansible.builtin.debug:
    var: ansible_hostname
    verbosity: 1   # only show at -v or higher

- name: Debug message
  ansible.builtin.debug:
    msg: "Deploying {{ app_name }} version {{ app_version }}"

- name: Add provisioned host to inventory
  ansible.builtin.add_host:
    name: "{{ new_server_ip }}"
    groups: webservers
    ansible_user: ubuntu

- name: Group hosts by OS
  ansible.builtin.group_by:
    key: "os_{{ ansible_distribution }}"

- name: Flush handlers mid-play
  ansible.builtin.meta: flush_handlers

- name: Clear facts for this host
  ansible.builtin.meta: clear_facts

Import and include modules

Module Purpose
ansible.builtin.import_playbook Import a playbook (static)
ansible.builtin.import_role Import a role (static)
ansible.builtin.import_tasks Import tasks from file (static)
ansible.builtin.include_role Include a role (dynamic)
ansible.builtin.include_tasks Include tasks from file (dynamic)
ansible.builtin.include_vars Load variables from file (dynamic)

12. Common Tasks & Workflow Recipes

Recipe 1 — Bootstrap a new server

---
- name: Bootstrap new server
  hosts: new_servers
  gather_facts: false
  become: true

  pre_tasks:
    - name: Wait for SSH
      ansible.builtin.wait_for_connection:
        timeout: 120

    - name: Gather facts
      ansible.builtin.setup:

  tasks:
    - name: Update all packages
      ansible.builtin.package:
        name: "*"
        state: latest

    - name: Install essential tools
      ansible.builtin.package:
        name:
          - vim
          - curl
          - wget
          - git
          - htop
          - fail2ban
          - ufw
        state: present

    - name: Set hostname
      ansible.builtin.hostname:
        name: "{{ inventory_hostname }}"

    - name: Set timezone
      ansible.builtin.command: timedatectl set-timezone UTC
      changed_when: true

    - name: Create ops user
      ansible.builtin.user:
        name: ops
        groups: sudo
        shell: /bin/bash
        create_home: true

    - name: Authorize ops SSH key
      ansible.posix.authorized_key:
        user: ops
        key: "{{ ops_ssh_pub_key }}"
        state: present

Recipe 2 — Deploy a web application

---
- name: Deploy web application
  hosts: webservers
  become: true
  vars:
    app_name: myapp
    app_version: "1.2.3"
    app_user: www-data
    app_dir: "/opt/{{ app_name }}"
    app_repo: https://github.com/example/myapp.git

  tasks:
    - name: Create app directory
      ansible.builtin.file:
        path: "{{ app_dir }}"
        state: directory
        owner: "{{ app_user }}"
        mode: '0755'

    - name: Clone/update application
      ansible.builtin.git:
        repo: "{{ app_repo }}"
        dest: "{{ app_dir }}/source"
        version: "v{{ app_version }}"
      become_user: "{{ app_user }}"
      notify: Restart app service

    - name: Install Python dependencies
      ansible.builtin.pip:
        requirements: "{{ app_dir }}/source/requirements.txt"
        virtualenv: "{{ app_dir }}/venv"
      become_user: "{{ app_user }}"

    - name: Run database migrations
      ansible.builtin.command:
        cmd: "{{ app_dir }}/venv/bin/python manage.py migrate --noinput"
        chdir: "{{ app_dir }}/source"
      become_user: "{{ app_user }}"
      changed_when: true

    - name: Deploy nginx config
      ansible.builtin.template:
        src: templates/nginx_vhost.conf.j2
        dest: "/etc/nginx/sites-available/{{ app_name }}"
        mode: '0644'
      notify: Reload nginx

    - name: Enable nginx vhost
      ansible.builtin.file:
        src: "/etc/nginx/sites-available/{{ app_name }}"
        dest: "/etc/nginx/sites-enabled/{{ app_name }}"
        state: link
      notify: Reload nginx

  handlers:
    - name: Restart app service
      ansible.builtin.systemd_service:
        name: "{{ app_name }}"
        state: restarted
        daemon_reload: true

    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Recipe 3 — Rolling update with serial

---
- name: Rolling update
  hosts: webservers
  serial: 1          # Update one host at a time
  max_fail_percentage: 0
  become: true

  pre_tasks:
    - name: Remove from load balancer
      ansible.builtin.uri:
        url: "http://lb.example.com/api/nodes/{{ inventory_hostname }}"
        method: DELETE
      delegate_to: localhost

    - name: Wait for connections to drain
      ansible.builtin.pause:
        seconds: 15

  tasks:
    - name: Deploy application
      ansible.builtin.git:
        repo: https://github.com/example/myapp.git
        dest: /opt/myapp
        version: "{{ app_version }}"
      notify: Restart app

  post_tasks:
    - name: Smoke test
      ansible.builtin.uri:
        url: http://localhost:8080/health
        status_code: 200
      retries: 5
      delay: 10

    - name: Re-add to load balancer
      ansible.builtin.uri:
        url: "http://lb.example.com/api/nodes"
        method: POST
        body_format: json
        body:
          host: "{{ inventory_hostname }}"
      delegate_to: localhost

  handlers:
    - name: Restart app
      ansible.builtin.systemd_service:
        name: myapp
        state: restarted

Recipe 4 — Gather and use facts

---
- name: System inventory report
  hosts: all
  gather_facts: true

  tasks:
    - name: Display system info
      ansible.builtin.debug:
        msg: |
          Host: {{ ansible_fqdn }}
          OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
          Kernel: {{ ansible_kernel }}
          CPU: {{ ansible_processor_count }} cores
          RAM: {{ (ansible_memtotal_mb / 1024) | round(1) }} GB
          Disk: {{ ansible_mounts | map(attribute='size_total') | sum | filesizeformat }}
          Python: {{ ansible_python_version }}

    - name: Gather package facts
      ansible.builtin.package_facts:

    - name: Show nginx version if installed
      ansible.builtin.debug:
        msg: "nginx version: {{ ansible_facts.packages.nginx[0].version }}"
      when: "'nginx' in ansible_facts.packages"

Recipe 5 — Manage cron jobs

- name: Set up scheduled jobs
  ansible.builtin.cron:
    name: "Database backup"
    minute: "0"
    hour: "3"
    weekday: "1-5"     # Mon-Fri
    job: "/opt/backup.sh > /var/log/backup.log 2>&1"
    user: backup

- name: Remove old cron job
  ansible.builtin.cron:
    name: "Old backup"
    state: absent

- name: Add environment variable to crontab
  ansible.builtin.cron:
    name: MAILTO
    env: true
    value: [email removed]
    user: root

Recipe 6 — Vault-encrypted deployment secrets

# vars/secrets.yml (encrypted with ansible-vault)
db_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  66386439653236336462626566653063...

# playbook.yml
---
- name: Deploy with secrets
  hosts: dbservers
  vars_files:
    - vars/secrets.yml

  tasks:
    - name: Configure database
      ansible.builtin.template:
        src: templates/database.conf.j2
        dest: /etc/myapp/database.conf
        mode: '0600'
      no_log: true   # Do not log task output (contains secret)

Recipe 7 — Conditional OS-based configuration

---
- name: Multi-OS configuration
  hosts: all
  become: true

  tasks:
    - name: Install nginx (Debian/Ubuntu)
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
      when: ansible_os_family == "Debian"

    - name: Install nginx (RHEL/CentOS)
      ansible.builtin.dnf:
        name: nginx
        state: present
        enablerepo: epel
      when: ansible_os_family == "RedHat"

    - name: Install nginx (generic)
      ansible.builtin.package:
        name: nginx
        state: present
      when: ansible_os_family not in ["Debian", "RedHat"]

    - name: Include OS-specific vars
      ansible.builtin.include_vars:
        file: "vars/{{ ansible_os_family | lower }}.yml"

Recipe 8 — Async long-running tasks

- name: Start multiple long tasks in parallel
  ansible.builtin.command: "/opt/heavy-task-{{ item }}.sh"
  async: 3600
  poll: 0
  loop: [alpha, beta, gamma]
  register: async_jobs

- name: Wait for all tasks to complete
  ansible.builtin.async_status:
    jid: "{{ item.ansible_job_id }}"
  loop: "{{ async_jobs.results }}"
  register: job_results
  until: job_results.finished
  retries: 120
  delay: 30

- name: Verify all succeeded
  ansible.builtin.assert:
    that: "item.rc == 0"
  loop: "{{ job_results.results }}"

Recipe 9 — Use set_fact and register together

- name: Get running containers
  ansible.builtin.command: docker ps --format '{% raw %}{{.Names}}{% endraw %}'
  register: containers
  changed_when: false

- name: Parse container list
  ansible.builtin.set_fact:
    container_list: "{{ containers.stdout_lines }}"

- name: Show containers
  ansible.builtin.debug:
    msg: "Running: {{ container_list | join(', ') }}"

Recipe 10 — Check mode and diff mode

# Preview what would change without applying
ansible-playbook site.yml --check --diff

# Individual tasks that work differently in check mode:
- name: Show what would be deployed
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/app.conf
  check_mode: true     # always run in check mode (even when not --check)

- name: Never skip this task in check mode
  ansible.builtin.command: /opt/must-run.sh
  check_mode: false    # always execute, even with --check

Recipe 11 — Dynamic groups with group_by

- name: Group hosts by OS for later plays
  ansible.builtin.group_by:
    key: "os_{{ ansible_distribution | lower }}"

# Now use in the next play
- hosts: os_ubuntu
  tasks:
    - name: Ubuntu-specific task
      ansible.builtin.debug:
        msg: "I am Ubuntu"

Recipe 12 — Delegate tasks to another host

- name: Take snapshot before upgrade
  community.vmware.vmware_guest_snapshot:
    hostname: "{{ vcenter_host }}"
    name: "{{ inventory_hostname }}"
    snapshot_name: "pre-upgrade"
  delegate_to: localhost   # run this task on the controller

- name: Update hosts file on all web servers
  ansible.builtin.lineinfile:
    path: /etc/hosts
    line: "{{ hostvars['db1']['ansible_host'] }} db1.internal"
  delegate_to: "{{ item }}"
  loop: "{{ groups['webservers'] }}"
  run_once: true

Recipe 13 — Templating a configuration file

Template: templates/app.conf.j2

# Generated by Ansible — do not edit manually
# Managed host: {{ inventory_hostname }}

[server]
host = {{ ansible_host | default(inventory_hostname) }}
port = {{ app_port | default(8080) }}
workers = {{ ansible_processor_count * 2 }}

[database]
{% for db in databases %}
  [database.{{ db.name }}]
  host = {{ db.host }}
  port = {{ db.port | default(5432) }}
  name = {{ db.name }}
{% endfor %}

[logging]
level = {{ log_level | default('INFO') }}
path = {{ log_path | default('/var/log/app') }}

Playbook:

- name: Deploy app config
  ansible.builtin.template:
    src: templates/app.conf.j2
    dest: /etc/myapp/app.conf
    owner: myapp
    group: myapp
    mode: '0640'
    validate: /opt/myapp/bin/validate-config %s
  notify: Restart myapp

Recipe 14 — Install pip packages into virtualenv

- name: Create virtualenv
  ansible.builtin.pip:
    name: pip
    state: latest
    virtualenv: /opt/myapp/venv
    virtualenv_command: python3.12 -m venv

- name: Install requirements
  ansible.builtin.pip:
    requirements: /opt/myapp/requirements.txt
    virtualenv: /opt/myapp/venv
    extra_args: "--no-deps --no-compile"

- name: Install specific packages
  ansible.builtin.pip:
    name:
      - django==4.2
      - gunicorn
      - psycopg2-binary
    virtualenv: /opt/myapp/venv

Recipe 15 — Manage SSH known hosts

- name: Add GitHub to known hosts
  ansible.builtin.known_hosts:
    name: github.com
    key: "{{ lookup('pipe', 'ssh-keyscan -t ed25519 github.com') }}"
    state: present
    path: /home/deploy/.ssh/known_hosts

- name: Remove stale host key
  ansible.builtin.known_hosts:
    name: old-server.example.com
    state: absent

Recipe 16 — Post-task verification (smoke tests)

post_tasks:
  - name: Verify web server responds
    ansible.builtin.uri:
      url: "http://{{ ansible_host }}:{{ http_port }}/health"
      status_code: 200
      timeout: 10
    retries: 3
    delay: 5
    register: health_check

  - name: Verify database connection
    ansible.builtin.command:
      cmd: /opt/myapp/bin/check-db
    register: db_check
    changed_when: false
    failed_when: db_check.rc != 0

Recipe 17 — Inventory-driven variable loading

# inventory/group_vars/all/common.yml
ntp_servers:
  - pool.ntp.org
  - time.cloudflare.com

# inventory/group_vars/production/main.yml
log_level: WARNING
replicas: 3

# inventory/group_vars/development/main.yml
log_level: DEBUG
replicas: 1

Recipe 18 — Multi-stage playbook with imports

# site.yml
---
- import_playbook: playbooks/00-bootstrap.yml
- import_playbook: playbooks/10-base.yml
- import_playbook: playbooks/20-database.yml
- import_playbook: playbooks/30-webservers.yml
- import_playbook: playbooks/40-monitoring.yml
- import_playbook: playbooks/99-smoke-tests.yml

Recipe 19 — Collecting and aggregating facts

- name: Collect IPs of all web servers
  ansible.builtin.set_fact:
    web_ips: "{{ groups['webservers'] | map('extract', hostvars, ['ansible_default_ipv4', 'address']) | list }}"
  run_once: true

- name: Generate HAProxy config with all backends
  ansible.builtin.template:
    src: haproxy.cfg.j2
    dest: /etc/haproxy/haproxy.cfg
  delegate_to: loadbalancer
  run_once: true

Recipe 20 — Validate argument spec in roles

# roles/myrole/tasks/main.yml
- name: Validate role arguments
  ansible.builtin.validate_argument_spec:
    argument_spec: "{{ lookup('file', role_path + '/meta/argument_specs.yml') | from_yaml }}"
    provided_arguments: "{{ hostvars[inventory_hostname] }}"

Recipe 21 — Using ansible-pull for node self-management

# On each node, add a cron job:
*/15 * * * * /usr/bin/ansible-pull \
  -U https://github.com/example/node-config.git \
  -C main \
  --sleep 60 \
  site.yml >> /var/log/ansible-pull.log 2>&1

Recipe 22 — Encrypt a variable inline

# Get the encrypted string
ansible-vault encrypt_string 'super-secret-value' --name 'api_key'

Paste the output into vars/secrets.yml:

# vars/secrets.yml
api_key: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  62313365396...
api_endpoint: https://api.example.com  # not sensitive, plain text

Recipe 23 — Use tags for targeted runs

tasks:
  - name: Install packages
    ansible.builtin.package:
      name: nginx
      state: present
    tags: [install, packages]

  - name: Deploy config
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    tags: [config, nginx]

  - name: Restart service
    ansible.builtin.service:
      name: nginx
      state: restarted
    tags: [restart, never]   # 'never' tag: only runs when explicitly requested
# Only run config tasks
ansible-playbook site.yml --tags config

# Run restart (which has 'never' tag)
ansible-playbook site.yml --tags restart

Recipe 24 — Limit scope at execution time

# Run against one host only
ansible-playbook site.yml --limit web1.example.com

# Run against a group
ansible-playbook site.yml --limit webservers

# Run against intersection
ansible-playbook site.yml --limit "webservers:&production"

# Exclude hosts
ansible-playbook site.yml --limit "all:!maintenance"

Recipe 25 — Ansible-console for ad-hoc exploration

# Start console against a group
ansible-console -i inventory.ini webservers

# Inside the console:
# ping all hosts
ping
# run a shell command
shell df -h
# switch to another group
cd dbservers
# install a package (with become)
become true
package name=htop state=present

Recipe 26 — Run playbook from CI/CD pipeline

# .github/workflows/deploy.yml (example)
- name: Run Ansible playbook
  run: |
    pip install ansible-core
    echo "${{ secrets.VAULT_PASS }}" > /tmp/.vault_pass
    ansible-playbook \
      -i inventory/production \
      --vault-password-file /tmp/.vault_pass \
      --private-key /tmp/deploy_key \
      -e "app_version=${{ github.ref_name }}" \
      site.yml

Recipe 27 — Combining inventory sources

# Use multiple inventory sources
ansible-playbook -i inventory/static.ini -i inventory/aws.py site.yml

# Or set in ansible.cfg
# [defaults]
# inventory = inventory/

All files in an inventory directory are automatically merged.


Recipe 28 — Gather inventory as JSON for external tools

# Full inventory dump for use by other tools
ansible-inventory -i inventory/ --list > inventory.json

# Compact inventory for a specific group
ansible-inventory -i inventory/ --list --limit webservers | jq '.webservers.hosts'

13. Troubleshooting

Error: Permission denied (publickey)

Cause: SSH key not authorized on the managed node, wrong user, or key not loaded.

Solution:

# Verify key is authorized
ssh -i ~/.ssh/id_ed25519 user@host

# Specify key explicitly
ansible all -i inventory.ini -m ping --private-key ~/.ssh/id_ed25519

# Check ansible.cfg
# private_key_file = ~/.ssh/id_ed25519
# remote_user = ubuntu

Prevention: Use ssh-copy-id when provisioning nodes; test SSH manually before running Ansible.


Error: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh"}

Cause: Host unreachable, SSH service not running, firewall blocking port 22, or wrong ansible_host.

Solution:

# Test basic connectivity
ping host.example.com

# Test SSH directly
ssh -vvv [email removed]

# Check inventory host variable
ansible-inventory -i inventory.ini --host hostname

# Increase timeout
ansible all -i inventory.ini -m ping --timeout 60

Prevention: Verify network path and SSH service availability before running playbooks.


Error: sudo: a password is required

Cause: become: true set but sudo requires a password and none was provided.

Solution:

# Prompt for become password
ansible-playbook site.yml -K

# Or configure passwordless sudo on managed node:
# username ALL=(ALL) NOPASSWD: ALL

# Or specify password in inventory (not recommended for production)
# ansible_become_pass: yourpassword

Prevention: Configure NOPASSWD sudo for the automation user, or use --ask-become-pass flag.


Error: MODULE FAILURE ... No module named 'json'

Cause: Python on the managed node is too old or the wrong Python interpreter is being used.

Solution:

# Set explicitly in inventory
[webservers]
web1.example.com ansible_python_interpreter=/usr/bin/python3

# Or in group_vars/all.yml
ansible_python_interpreter: /usr/bin/python3

Prevention: Use auto_silent interpreter discovery (set in ansible.cfg: interpreter_python = auto_silent).


Error: Error: ansible requires a newer version of python on the remote host

Cause: Managed node Python is below the minimum required version for the modules being used.

Solution:

# Bootstrap Python on the node first
ansible all -i inventory.ini -m raw -a "apt-get install -y python3" -b

# Then run your playbook
ansible-playbook site.yml

Error: [WARNING]: Found variable using reserved name: ...

Cause: A variable in your inventory or vars files uses a name reserved by Ansible (e.g., environment, vars, name).

Solution: Rename the variable. Ansible reserved names include: add_host, always, any_errors_fatal, become, become_user, block, check_mode, collections, connection, debugger, delegate_facts, delegate_to, diff, environment, failed_when, force_handlers, gather_facts, handlers, hosts, ignore_errors, ignore_unreachable, listen, loop, meta, module_defaults, name, no_log, notify, port, post_tasks, pre_tasks, rescue, roles, run_once, serial, tags, tasks, timeout, vars, vars_files, vars_prompt, when.


Error: Timeout (12s) waiting for privilege escalation prompt

Cause: Sudo prompt appearing when pipelining is enabled, or slow network causing timeout.

Solution:

# ansible.cfg
[defaults]
timeout = 30

[ssh_connection]
pipelining = False   # disable if sudo prompts appear

Prevention: Ensure requiretty is disabled in /etc/sudoers when using pipelining = True.


Error: Template variable warning: Undefined variable

Cause: A Jinja2 template references a variable that is not defined.

Solution:

# Use default filter
msg: "{{ my_var | default('fallback_value') }}"

# Or check with defined test
when: my_var is defined and my_var | length > 0

# Or set in defaults/main.yml in roles

Prevention: Define defaults for all optional variables in defaults/main.yml.


Error: [WARNING]: could not match supplied host pattern

Cause: The host pattern specified (in hosts: or --limit) does not match any hosts in inventory.

Solution:

# See what hosts exist
ansible-inventory -i inventory.ini --list

# Check pattern against inventory
ansible -i inventory.ini 'yourpattern' --list-hosts

# Fix typo in hosts pattern or --limit value

Error: fatal: [host]: FAILED! => {"msg": "The task includes an option with an undefined variable"}

Cause: A task variable is undefined at execution time.

Solution:

# Run with extra verbosity to see which variable
ansible-playbook site.yml -vvv

# Check variable precedence — maybe it's defined in the wrong place
ansible-playbook site.yml --extra-vars "debug_var=true"

Error: failed: [host] (item=...) => {"msg": "Timeout (12s) waiting for...

Cause: Task is waiting for a network resource or condition that is taking too long.

Solution:

- name: Wait longer for slow service
  ansible.builtin.wait_for:
    port: 8080
    timeout: 120    # increase from default

- name: Retry more times
  ansible.builtin.uri:
    url: http://localhost/health
  retries: 20       # more retries
  delay: 15         # longer between retries

Error: ERROR! 'dict object' has no attribute '...'

Cause: Accessing a nonexistent key on a registered variable or fact dict.

Solution:

# Check the actual structure
- debug:
    var: registered_var

# Use default for missing keys
msg: "{{ mydict.get('optional_key', 'default') }}"

# Test for key existence
when: "'mykey' in mydict"

Error: AnsibleParserError: Syntax Error while loading YAML

Cause: Invalid YAML in a playbook, vars file, or inventory file.

Solution:

# Validate YAML syntax
python3 -c "import yaml; yaml.safe_load(open('playbook.yml'))"

# Common YAML pitfalls:
# - Indentation with tabs (use spaces only)
# - Unquoted strings with special chars (: { } [ ] # &)
# - Boolean values need quotes if used as strings: "true" not true

Prevention: Use an editor with YAML validation (VS Code + Ansible extension).


Error: fatal: [host]: FAILED! => {"changed": false, "msg": "git ... fatal: destination path ... already exists"}

Cause: Git module trying to clone into a directory that already exists but is not a git repo.

Solution:

- name: Clone repo (handle existing dir)
  ansible.builtin.git:
    repo: https://github.com/example/repo.git
    dest: /opt/app
    force: false
    update: true   # update if already exists

Error: no_log does not suppress variable output in all cases

Cause: Known issue #83323 — no_log: true may not fully suppress output when the value appears through variable interpolation in certain contexts.

Solution:

# Use no_log on the task
- name: Set secret
  ansible.builtin.set_fact:
    secret_value: "{{ vault_secret }}"
  no_log: true

# Also avoid printing registered variables from no_log tasks
- name: Do not debug no_log task output
  ansible.builtin.debug:
    var: secret_result
  no_log: true

Prevention: Avoid using vault-encrypted values in debug tasks; use when: false in development.


Error: [DEPRECATION WARNING]: six module ...

Cause: As of Ansible 2.22, lib/ansible/module_utils/six/__init__.py is deprecated (issue #86789).

Solution: Import six directly from the installed package rather than via ansible.module_utils.six. Update any custom module code:

# Before
from ansible.module_utils.six import string_types
# After
from six import string_types

Error: url lookup plugin deprecated

Cause: lib/ansible/plugins/lookup/url.py is deprecated in Ansible 2.22 (issue #86790).

Solution: Use the ansible.builtin.uri module or the community.general.url_content lookup instead.


Error: variable performance regression in 2.19

Cause: Known regression (#86881) causing slow variable resolution in large inventories or playbooks with many variables in Ansible 2.19.

Solution: Check the GitHub issue for the latest patch status. As a workaround:

# Minimize variable fan-out
# Avoid deeply nested variable structures
# Use set_fact sparingly inside loops

# Check if upgrading to a later 2.19.x patch resolves it
pip install --upgrade ansible-core

Error: rpm_key fails with ftp:// URL

Cause: Known issue #83321 — ansible.builtin.rpm_key does not support ftp:// URLs.

Solution: Download the key first, then import from local file:

- name: Download RPM key
  ansible.builtin.get_url:
    url: ftp://example.com/keys/RPM-GPG-KEY-Example
    dest: /tmp/RPM-GPG-KEY-Example

- name: Import RPM key
  ansible.builtin.rpm_key:
    key: /tmp/RPM-GPG-KEY-Example
    state: present

Error: known_hosts: cannot create ~/.ssh directory

Cause: Known issue #84952 — the known_hosts module does not create ~/.ssh if it does not exist.

Solution:

- name: Ensure .ssh directory exists
  ansible.builtin.file:
    path: "~/.ssh"
    state: directory
    mode: '0700'

- name: Add known host
  ansible.builtin.known_hosts:
    name: github.com
    key: "{{ lookup('pipe', 'ssh-keyscan github.com') }}"
    state: present

Error: --start-at-task does not gather facts

Cause: Known change in 2.19 (issue #86268) — --start-at-task no longer implicitly gathers facts before starting at the named task.

Solution:

# Option 1: Use cached facts (run a full play first with fact caching)
ansible-playbook site.yml --flush-cache  # clear first
ansible-playbook site.yml                # run to populate cache

# Option 2: Add explicit fact gathering before the start task
# In playbook:
#   gather_facts: true (default, makes sure facts are gathered even mid-play)
# Or add an early task:
- name: Gather facts
  ansible.builtin.setup:
  tags: [always]   # run even when start-at-task skips earlier tasks

Error: git module can lose local commits with force: true

Cause: Known issue #83367 — if the repository has diverged from the remote and force: true is set, local commits can be permanently lost.

Solution:

# Only use force: false (the default)
- name: Update repo safely
  ansible.builtin.git:
    repo: https://github.com/example/repo.git
    dest: /opt/app
    version: main
    force: false    # default — safe

Prevention: Never set force: true on repos with local changes. Create a backup before force operations.


Error: sudo: validate sudo become plugin against sudo-rs

Cause: Issue #85837 — the sudo become plugin may behave differently with sudo-rs (a Rust rewrite of sudo).

Solution: If using sudo-rs on your managed nodes, test privilege escalation explicitly:

ansible all -i inventory.ini -m command -a 'id' -b --become-method sudo

If it fails, specify become_method: sudo in inventory vars and report the behavior to the ansible-core issue tracker.


Error: AnsibleConnectionError: Error while fetching: ...

Cause: Network interruption or remote host closed connection mid-task.

Solution:

# Retry the playbook
ansible-playbook site.yml --limit failed-host

# Increase connection timeout
ansible-playbook site.yml --timeout 60

# Enable SSH keepalives in ansible.cfg:
# [ssh_connection]
# ssh_args = -C -o ServerAliveInterval=30 -o ServerAliveCountMax=10

Error: FAILED! => {"msg": "Failed to import the required Python library (cryptography)"}

Cause: The cryptography Python package is missing on the controller or managed node.

Solution:

# On controller
pip install cryptography

# On managed node via Ansible
- name: Install cryptography
  ansible.builtin.pip:
    name: cryptography
    state: present

Error: ERROR! You appear to be running Ansible in a world writable directory

Cause: Ansible detects the current directory as world-writable, which is a security risk.

Solution:

chmod o-w /path/to/your/project

Error: [WARNING]: Unable to parse inventory source ... as an inventory plugin

Cause: Inventory file format cannot be detected, or a required inventory plugin is not installed.

Solution:

# Explicitly specify format
ansible-playbook -i inventory.ini site.yml   # force INI
ansible-playbook -i inventory.yml site.yml   # force YAML

# Check if inventory plugin is available
ansible-doc -t inventory -l

Error: YAML Inventory host ... has no source attribute

Cause: YAML inventory format is missing required structure.

Solution:

# Correct YAML inventory format:
all:
  hosts:
    web1.example.com:    # trailing colon required even with no vars
  children:
    webservers:
      hosts:
        web2.example.com:

Error: No valid OpenSSH-style private keys found

Cause: Private key format is not recognized (e.g., PuTTY PPK format) or file permissions are too open.

Solution:

# Fix permissions
chmod 600 ~/.ssh/id_ed25519

# Convert PuTTY key to OpenSSH
puttygen mykey.ppk -O private-openssh -o mykey.pem

# Verify key is valid
ssh-keygen -y -f ~/.ssh/id_ed25519

14. FAQ

Q: What is the difference between ansible-core and ansible? ansible-core is the minimal engine: the CLI tools, built-in modules (72 of them), and the core framework. The ansible package is a larger bundle that includes ansible-core plus a curated set of community collections. For production environments, install ansible-core and explicitly add only the collections you need via ansible-galaxy collection install.

Q: Do I need to install anything on managed nodes? Managed nodes typically need Python 3.x and a working SSH server. Some modules (like raw and script) work without Python. For Windows managed nodes, WinRM or SSH (OpenSSH on Windows Server 2019+) is required, plus no Python needed — Ansible connects via WinRM and runs PowerShell.

Q: What Python version is required on the controller? Python 3.12 or higher. This is a hard requirement for ansible-core. Check with python3 --version and use pyenv or a virtual environment if your system Python is older.

Q: How do I speed up Ansible execution? Key strategies: (1) Enable pipelining = True in [ssh_connection] — biggest single win. (2) Increase forks in [defaults]. (3) Enable SSH ControlMaster with ControlPersist. (4) Use gather_facts: false in plays that do not need facts. (5) Enable fact caching (file or redis). (6) Use async and poll: 0 for independent long-running tasks.

Q: How do I use Ansible with Windows managed nodes? Set ansible_connection: winrm or ansible_connection: ssh (Windows OpenSSH). Install pywinrm on the controller (pip install pywinrm). Use ansible.windows.* modules (from the ansible.windows collection) rather than built-in Linux modules.

Q: What is the difference between import_tasks and include_tasks? import_tasks is static — processed at parse time. Tags applied to import_tasks apply to all imported tasks. Conditional when on import_tasks applies to every task inside. include_tasks is dynamic — processed at runtime, so you can use variables in the filename. Tags on include_tasks only apply to the include statement itself, not the included tasks. For most cases, use import_tasks unless you need dynamic filenames.

Q: How do I pass a variable on the command line? Use -e / --extra-vars: ansible-playbook site.yml -e "env=production version=1.2.3". For complex values: ansible-playbook site.yml -e '{"config": {"port": 8080}}'. For a file: ansible-playbook site.yml -e "@vars/overrides.yml". Extra vars have the highest precedence.

Q: Why is my handler not running? Handlers only run if the notifying task reports changed. If the task is idempotent and the state is already correct, changed is false and the handler does not fire. To force a handler run: ansible-playbook site.yml --force-handlers. To flush handlers mid-play: use meta: flush_handlers.

Q: How do I run Ansible against localhost?

ansible localhost -m ping -c local
# Or in playbook:
# hosts: localhost
# connection: local

Or add to inventory: localhost ansible_connection=local.

Q: How do I debug a failing playbook? Run with -vvv for verbose SSH/module output. Use ansible.builtin.debug tasks to print variables. Use --step to step through task by task. Use --start-at-task to jump to the failing task after fixing it. Set ANSIBLE_LOG_PATH to capture full output.

Q: What is the changed_when directive? By default, command and shell modules always report changed. Use changed_when to define when a task should report a change:

- command: /opt/check-state.sh
  register: result
  changed_when: result.rc == 1    # changed only if script exits 1
  failed_when: result.rc > 1      # failed only if exits >1

Q: How do I store passwords securely in Ansible? Use ansible-vault encrypt_string to inline-encrypt individual values within otherwise-plain YAML files. Store the vault password in a file (~/.vault_pass, chmod 600) outside version control. Reference in ansible.cfg via vault_password_file. Never commit unencrypted secrets.

Q: What is a fact and how do I use it? Facts are system properties gathered automatically at the start of each play (unless gather_facts: false). Access them via ansible_* variables: {{ ansible_hostname }}, {{ ansible_distribution }}, {{ ansible_default_ipv4.address }}. Run ansible hostname -m setup to see all available facts for a host.

Q: How do I run a task only once, even with many hosts? Add run_once: true to the task. Ansible runs it on the first host in the batch. Use delegate_to: localhost combined with run_once: true to run it on the controller.

Q: How do I limit a playbook to specific hosts without modifying the playbook? Use --limit: ansible-playbook site.yml --limit web1.example.com,web2.example.com. Or limit to a group: --limit webservers. Or exclude: --limit 'all:!maintenance'.

Q: What is the delegate_to directive? delegate_to runs a task on a different host than the current one in the play:

- name: Update DNS before deployment
  some.module:
    hostname: "{{ inventory_hostname }}"
  delegate_to: dns-server.example.com

Useful for load balancer operations, DNS updates, or anything that must run on a specific host.

Q: How do I use Ansible with a jump host (bastion server)?

# ansible.cfg
[ssh_connection]
ssh_args = -o ProxyJump=bastion.example.com

# Or per-host in inventory:
# ansible_ssh_common_args='-o ProxyJump=bastion.example.com'

Q: How do I test playbooks without making changes? Use --check (dry-run mode). Some modules support check mode natively and report what would change. Combine with --diff to see file content changes: ansible-playbook site.yml --check --diff.

Q: Can I use Ansible without SSH? Yes. Connection plugins include: local (controller itself), docker (Docker containers), kubectl (Kubernetes pods), paramiko (Python SSH library), and others from collections. Set ansible_connection per host or group.

Q: How do I update a collection?

ansible-galaxy collection install community.general --upgrade
# Or pin to latest compatible:
ansible-galaxy collection install "community.general:>=8.0.0,<9.0.0"

Q: What is the serial keyword? serial controls how many hosts run the play concurrently in a "batch". serial: 1 = one host at a time (safest for rolling updates). serial: "30%" = 30% of hosts per batch. serial: [1, 5, 10] = progressive batches.

Q: How do I generate a random password with Ansible?

- name: Generate random password
  ansible.builtin.set_fact:
    random_password: "{{ lookup('password', '/dev/null length=20 chars=ascii_letters,digits') }}"

Q: What does ansible_managed do in templates? ansible_managed is a string inserted into templates to indicate the file is managed by Ansible. Configure it in ansible.cfg:

[defaults]
ansible_managed = Managed by Ansible on {host} by {uid}@{host} on {date}

Use in templates: # {{ ansible_managed }}

Q: How do I see what hosts match an inventory pattern?

ansible -i inventory.ini 'webservers:&production' --list-hosts
ansible-inventory -i inventory.ini --graph

Q: How do I use multiple vault passwords?

# Encrypt with different vault IDs
ansible-vault encrypt_string 'dev-secret' --vault-id dev@~/.vault_dev --name dev_key
ansible-vault encrypt_string 'prod-secret' --vault-id prod@~/.vault_prod --name prod_key

# Run with multiple vault IDs
ansible-playbook site.yml \
  --vault-id dev@~/.vault_dev \
  --vault-id prod@~/.vault_prod

Q: Is there a way to see Ansible's version requirements for a module?

ansible-doc ansible.builtin.dnf5
# Look for "Requirements" and "Notes" sections in the documentation

Q: How do I contribute to ansible-core? The project is on GitHub at https://github.com/ansible/ansible. Licensed under GPL-3.0+. See CONTRIBUTING.md in the repo for the full contribution guide, including how to run tests with ansible-test.