Important
This guide covers ansible-core (the
ansible/ansiblerepository), 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
- Quick Start
- Installation
- Configuration
- Inventory
- Ad-Hoc Commands
- Playbooks
- Roles
- Collections
- Vault — Secrets Management
- CLI Reference
- Built-in Module Reference
- Common Tasks & Workflow Recipes
- Troubleshooting
- 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.ymlbefore 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.
Install via pip (recommended)
# 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
pipfor 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 = Truein[ssh_connection]for a significant performance boost. It reduces the number of SSH connections required per task. Requires thatrequirettyis 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-taskas 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 explicitgather_facts: truetask 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. Useinclude_*(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_stringfor 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.commandovershellwhen you don't need shell features (pipes, redirects, environment variable expansion).commandis 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
gitmodule can lose local commits ifforce: trueis set and the repository has diverged from the remote (GitHub issue #83367). Only useforce: truewhen 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.