Ansible Cheat Sheet
Ansible is an agentless automation tool that uses SSH to configure servers, deploy applications, and orchestrate infrastructure. This cheat sheet covers everything from first commands to production-grade playbooks.
Quick Reference
| Task | Command / Syntax |
|---|---|
| Run ad-hoc command | ansible all -m ping |
| Run playbook | ansible-playbook site.yml |
| Check syntax | ansible-playbook site.yml --syntax-check |
| Dry run | ansible-playbook site.yml --check |
| Limit to host | ansible-playbook site.yml --limit web01 |
| Pass variable | ansible-playbook site.yml -e "env=prod" |
| List hosts | ansible all --list-hosts |
| List tasks | ansible-playbook site.yml --list-tasks |
| Encrypt string | ansible-vault encrypt_string 'secret' |
| Run with vault | ansible-playbook site.yml --ask-vault-pass |
| Install role | ansible-galaxy role install geerlingguy.nginx |
| Init role | ansible-galaxy role init myrole |
Installation
# Ubuntu/Debian
sudo apt update && sudo apt install ansible -y
# RHEL/CentOS
sudo dnf install ansible -y
# macOS
brew install ansible
# pip (any OS, get latest)
pip install ansible
# Verify
ansible --version
Inventory
The inventory file tells Ansible which hosts to manage.
Static Inventory (/etc/ansible/hosts or custom file)
# Simple list
192.168.1.10
192.168.1.11
# Named group
[webservers]
web01 ansible_host=192.168.1.10
web02 ansible_host=192.168.1.11
[dbservers]
db01 ansible_host=192.168.1.20 ansible_port=22
# Group of groups
[production:children]
webservers
dbservers
# Group variables
[webservers:vars]
http_port=80
max_connections=200
YAML Inventory
# inventory.yml
all:
hosts:
bastion:
ansible_host: 1.2.3.4
children:
webservers:
hosts:
web01:
ansible_host: 192.168.1.10
web02:
ansible_host: 192.168.1.11
vars:
http_port: 80
dbservers:
hosts:
db01:
ansible_host: 192.168.1.20
Common Host Variables
| Variable | Purpose |
|---|---|
ansible_host |
Target IP/hostname |
ansible_port |
SSH port (default 22) |
ansible_user |
SSH user |
ansible_ssh_private_key_file |
Path to SSH key |
ansible_become |
Enable privilege escalation |
ansible_become_user |
User to become (default root) |
ansible_python_interpreter |
Python path on remote |
ansible_connection |
Connection type (ssh/local/winrm) |
Ad-Hoc Commands
Run one-off commands without a playbook.
# Ping all hosts
ansible all -m ping
# Ping a group
ansible webservers -m ping
# Run a shell command
ansible all -m shell -a "uptime"
ansible all -m command -a "df -h" # safer: no shell expansion
# Copy a file
ansible all -m copy -a "src=/local/file dest=/remote/file"
# Install a package (apt)
ansible webservers -m apt -a "name=nginx state=present" --become
# Restart a service
ansible webservers -m service -a "name=nginx state=restarted" --become
# Create a user
ansible all -m user -a "name=deploy shell=/bin/bash" --become
# Gather facts (hardware/OS info)
ansible web01 -m setup
# Filter facts
ansible web01 -m setup -a "filter=ansible_distribution*"
# Use custom inventory
ansible -i inventory.yml all -m ping
# Run as different user
ansible all -u ubuntu -m ping
# Use SSH key
ansible all --private-key=~/.ssh/mykey.pem -m ping
Playbook Structure
---
- name: Configure web servers # play name
hosts: webservers # target group or host
become: yes # sudo
become_user: root
gather_facts: yes # collect host info (default: yes)
vars:
http_port: 80
app_version: "1.2.3"
vars_files:
- vars/main.yml
- vars/secrets.yml # encrypted with Vault
pre_tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
roles:
- common
- nginx
tasks:
- name: Install nginx
apt:
name: nginx
state: present
- name: Start nginx
service:
name: nginx
state: started
enabled: yes
post_tasks:
- name: Verify nginx is running
uri:
url: "http://localhost:{{ http_port }}"
status_code: 200
handlers:
- name: Reload nginx
service:
name: nginx
state: reloaded
Common Modules
File & System
# Copy file
- name: Copy config
copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
backup: yes
# Template (Jinja2)
- name: Render template
template:
src: templates/app.conf.j2
dest: /etc/app/app.conf
mode: "0640"
# Create directory
- name: Ensure log directory exists
file:
path: /var/log/myapp
state: directory
owner: www-data
mode: "0755"
# Symlink
- name: Create symlink
file:
src: /opt/app/current
dest: /opt/app/latest
state: link
# Delete file
- name: Remove old config
file:
path: /etc/old.conf
state: absent
# Fetch file from remote
- name: Download log
fetch:
src: /var/log/app.log
dest: logs/{{ inventory_hostname }}/
flat: no
Package Management
# apt (Debian/Ubuntu)
- name: Install packages
apt:
name:
- nginx
- python3-pip
- git
state: present
update_cache: yes
# yum/dnf (RHEL/CentOS)
- name: Install nginx
yum:
name: nginx
state: latest
# pip
- name: Install Python packages
pip:
name:
- flask
- gunicorn
state: present
virtualenv: /opt/app/venv
Services
- name: Manage nginx
service:
name: nginx
state: started # started / stopped / restarted / reloaded
enabled: yes # start on boot
Users & Groups
- name: Create deploy user
user:
name: deploy
shell: /bin/bash
groups: sudo,docker
append: yes # add to groups without removing existing
create_home: yes
- name: Add SSH key
authorized_key:
user: deploy
key: "{{ lookup('file', 'keys/deploy.pub') }}"
state: present
- name: Create group
group:
name: webadmin
state: present
Shell & Command
# command — safer, no shell features
- name: Run script
command: /opt/deploy.sh
args:
chdir: /opt/app
# shell — full shell, use when needed
- name: Check if migrated
shell: cat /var/db_migrated | grep done
register: migration_result
failed_when: false
# script — run local script on remote
- name: Run local script
script: scripts/setup.sh arg1 arg2
# raw — no Python needed on target
- name: Install Python (bootstrap)
raw: apt-get install -y python3
Git & Archives
- name: Clone repo
git:
repo: https://github.com/org/app.git
dest: /opt/app
version: main
force: yes
- name: Extract archive
unarchive:
src: files/app-1.2.3.tar.gz # local file
dest: /opt/
creates: /opt/app-1.2.3 # skip if exists
remote_src: no
- name: Download and extract
unarchive:
src: https://example.com/app.tar.gz
dest: /opt/
remote_src: yes
Network & URI
- name: Check HTTP endpoint
uri:
url: https://api.example.com/health
method: GET
status_code: 200
return_content: yes
register: health_check
- name: POST JSON
uri:
url: https://api.example.com/deploy
method: POST
body_format: json
body:
version: "{{ app_version }}"
headers:
Authorization: "Bearer {{ api_token }}"
status_code: [200, 201]
Variables
Defining Variables
# In playbook
vars:
db_host: localhost
db_port: 5432
# From file
vars_files:
- vars/main.yml
# Inline with -e flag
ansible-playbook site.yml -e "env=prod version=1.2"
# From file with -e
ansible-playbook site.yml -e "@config.yml"
Variable Precedence (highest wins)
| Priority | Source |
|---|---|
| 1 (highest) | Extra vars -e |
| 2 | include_vars / set_fact |
| 3 | Task vars: |
| 4 | Block vars: |
| 5 | Role vars/main.yml |
| 6 | Play vars: / vars_files: |
| 7 | Host vars (host_vars/) |
| 8 | Group vars (group_vars/) |
| 9 (lowest) | Role defaults/main.yml |
Registering & Using Results
- name: Get OS info
command: uname -r
register: kernel_version
- name: Print kernel
debug:
msg: "Kernel: {{ kernel_version.stdout }}"
- name: Check if file exists
stat:
path: /opt/app/.installed
register: installed_flag
- name: Run install only if needed
command: /opt/install.sh
when: not installed_flag.stat.exists
Facts (Built-in Variables)
# Access gathered facts
{{ ansible_hostname }} # server01
{{ ansible_distribution }} # Ubuntu
{{ ansible_distribution_version }} # 22.04
{{ ansible_architecture }} # x86_64
{{ ansible_memtotal_mb }} # 8192
{{ ansible_default_ipv4.address }} # 192.168.1.10
{{ ansible_interfaces }} # list of interfaces
{{ ansible_os_family }} # Debian / RedHat
# Disable fact gathering (faster for large inventories)
gather_facts: no
Loops
# Simple loop
- name: Install packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- git
- curl
# Loop with dict
- name: Create users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
loop:
- { name: alice, groups: sudo }
- { name: bob, groups: docker }
# Loop with index
- name: Numbered task
debug:
msg: "Item {{ ansible_loop.index }}: {{ item }}"
loop: "{{ packages }}"
loop_control:
label: "{{ item }}" # cleaner output
# with_items (legacy, use loop instead)
- name: Old style
apt:
name: "{{ item }}"
with_items:
- nginx
- git
# Loop over dict
- name: Set sysctl values
sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
loop: "{{ sysctl_settings | dict2items }}"
Conditionals
# Basic when
- name: Install on Ubuntu only
apt:
name: nginx
when: ansible_distribution == "Ubuntu"
# Multiple conditions (AND)
- name: Configure for prod
template:
src: prod.conf.j2
dest: /etc/app.conf
when:
- env == "production"
- app_version is defined
# OR condition
- name: Install on Debian family
apt:
name: nginx
when: ansible_os_family == "Debian" or ansible_distribution == "Ubuntu"
# Check if variable is defined
- name: Use optional var
debug:
msg: "{{ custom_port }}"
when: custom_port is defined
# Check command result
- name: Restart only if changed
service:
name: nginx
state: restarted
when: config_result.changed
Handlers
Handlers run once at the end of the play, only if notified.
tasks:
- name: Update nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx # trigger handler
- name: Update SSL cert
copy:
src: cert.pem
dest: /etc/nginx/ssl/cert.pem
notify:
- Reload nginx # same handler, runs once
handlers:
- name: Reload nginx
service:
name: nginx
state: reloaded
- name: Restart app
service:
name: myapp
state: restarted
Roles
Roles package tasks, variables, templates, and files into reusable units.
Role Directory Structure
roles/
└── nginx/
├── defaults/
│ └── main.yml # default variables (lowest priority)
├── vars/
│ └── main.yml # role variables (high priority)
├── tasks/
│ └── main.yml # tasks
├── handlers/
│ └── main.yml # handlers
├── templates/
│ └── nginx.conf.j2
├── files/
│ └── favicon.ico
├── meta/
│ └── main.yml # dependencies
└── README.md
Create and Use a Role
# Create role scaffold
ansible-galaxy role init roles/nginx
# Use in playbook
---
- hosts: webservers
roles:
- common
- nginx
- { role: app, app_version: "1.2.3" } # pass variables
# Or with include_role
tasks:
- name: Include nginx role
include_role:
name: nginx
vars:
http_port: 8080
Galaxy — Public Roles
# Search
ansible-galaxy search nginx --author geerlingguy
# Install
ansible-galaxy role install geerlingguy.nginx
# Install from requirements.yml
ansible-galaxy install -r requirements.yml
# requirements.yml
# requirements.yml
roles:
- name: geerlingguy.nginx
version: "3.2.0"
- name: geerlingguy.mysql
collections:
- name: community.general
- name: amazon.aws
version: ">=5.0"
Ansible Vault
Encrypt sensitive data at rest.
# Encrypt a file
ansible-vault encrypt vars/secrets.yml
# Decrypt
ansible-vault decrypt vars/secrets.yml
# View encrypted file
ansible-vault view vars/secrets.yml
# Edit encrypted file
ansible-vault edit vars/secrets.yml
# Encrypt a single string (embed in YAML)
ansible-vault encrypt_string 'MyP@ssw0rd' --name db_password
# Re-key (change password)
ansible-vault rekey vars/secrets.yml
# Run playbook with vault
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Vault in Variables
# vars/secrets.yml (encrypted)
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
30363833623639303...
# Use like any variable
- name: Configure DB
template:
src: db.conf.j2
dest: /etc/db.conf
# In template: {{ db_password }}
Templates (Jinja2)
Ansible uses Jinja2 for dynamic configuration files.
{# /templates/nginx.conf.j2 #}
server {
listen {{ http_port | default(80) }};
server_name {{ ansible_hostname }};
{# Conditional block #}
{% if ssl_enabled %}
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/cert.pem;
{% endif %}
{# Loop #}
{% for upstream in app_servers %}
upstream app{{ loop.index }} {
server {{ upstream }}:8080;
}
{% endfor %}
location / {
proxy_pass http://{{ app_host }};
}
}
Common Jinja2 Filters
# Default value
{{ variable | default('fallback') }}
# String operations
{{ name | upper }}
{{ name | lower }}
{{ name | replace('old', 'new') }}
{{ list | join(', ') }}
# Type conversion
{{ "42" | int }}
{{ 3.14 | string }}
{{ "[1,2,3]" | from_json }}
# List operations
{{ list | sort }}
{{ list | unique }}
{{ list | length }}
{{ list | first }}
{{ list | last }}
# Dict operations
{{ dict | dict2items }}
{{ items | items2dict }}
{{ dict | combine(other_dict) }}
# Conditional (ternary)
{{ 'yes' if enabled else 'no' }}
# Hash
{{ 'secret' | password_hash('sha512') }}
Error Handling
# Ignore errors (continue play on failure)
- name: Try optional step
command: /opt/optional-setup.sh
ignore_errors: yes
# Change what counts as failure
- name: Check log for errors
command: grep ERROR /var/log/app.log
register: error_check
failed_when: error_check.rc == 0 # fail if grep found errors
# Change what counts as changed
- name: Run idempotent script
command: /opt/migrate.sh
changed_when: false # never mark as changed
# Block/rescue/always (try/catch/finally)
- name: Deploy with rollback
block:
- name: Deploy app
command: /opt/deploy.sh
- name: Run migrations
command: /opt/migrate.sh
rescue:
- name: Rollback on failure
command: /opt/rollback.sh
- name: Notify team
uri:
url: "{{ slack_webhook }}"
method: POST
body_format: json
body:
text: "Deployment failed on {{ inventory_hostname }}"
always:
- name: Clean temp files
file:
path: /tmp/deploy
state: absent
Includes & Imports
# Static import (parsed at playbook load time)
- import_tasks: tasks/setup.yml
- import_playbook: deploy.yml
# Dynamic include (evaluated at runtime, can use variables)
- include_tasks: "tasks/{{ env }}-setup.yml"
- include_vars: "vars/{{ ansible_os_family }}.yml"
Useful CLI Flags
| Flag | Purpose |
|---|---|
-i inventory.yml |
Specify inventory file |
-u ubuntu |
SSH as this user |
-k |
Prompt for SSH password |
--become / -b |
Enable privilege escalation |
--become-user root |
Escalate to this user |
-K |
Prompt for sudo password |
--check / -C |
Dry run (no changes) |
--diff / -D |
Show file diffs |
--tags web,ssl |
Run only tagged tasks |
--skip-tags cleanup |
Skip these tags |
--limit web01 |
Target only this host |
--start-at-task "Install" |
Start from named task |
-v / -vv / -vvv |
Verbosity |
--flush-cache |
Clear fact cache |
-f 10 |
Fork count (parallelism) |
ansible.cfg Configuration
[defaults]
inventory = ./inventory.yml
remote_user = ubuntu
private_key_file = ~/.ssh/deploy_key
host_key_checking = False
retry_files_enabled = False
roles_path = ./roles:~/.ansible/roles
stdout_callback = yaml # nicer output
timeout = 30
forks = 10 # parallel hosts (default 5)
[privilege_escalation]
become = True
become_method = sudo
become_user = root
[ssh_connection]
pipelining = True # faster, reduces SSH connections
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
Real-World Patterns
Deploy Application
---
- name: Deploy App
hosts: appservers
become: yes
serial: "30%" # rolling update: 30% of hosts at a time
vars:
app_dir: /opt/myapp
app_version: "{{ version | default('latest') }}"
tasks:
- name: Pull latest code
git:
repo: https://github.com/org/app.git
dest: "{{ app_dir }}"
version: "{{ app_version }}"
notify: Restart app
- name: Install dependencies
pip:
requirements: "{{ app_dir }}/requirements.txt"
virtualenv: "{{ app_dir }}/venv"
- name: Run migrations
command: "{{ app_dir }}/venv/bin/python manage.py migrate"
args:
chdir: "{{ app_dir }}"
environment:
DATABASE_URL: "{{ database_url }}"
- name: Collect static files
command: "{{ app_dir }}/venv/bin/python manage.py collectstatic --noinput"
args:
chdir: "{{ app_dir }}"
handlers:
- name: Restart app
service:
name: myapp
state: restarted
Provision New Server
---
- name: Bootstrap server
hosts: new_servers
become: yes
tasks:
- name: Update system
apt:
upgrade: dist
update_cache: yes
- name: Install essentials
apt:
name:
- curl
- git
- htop
- ufw
- fail2ban
state: present
- name: Create deploy user
user:
name: deploy
shell: /bin/bash
create_home: yes
- name: Add SSH key
authorized_key:
user: deploy
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
- name: Configure UFW
ufw:
rule: allow
port: "{{ item }}"
loop: [22, 80, 443]
- name: Enable UFW
ufw:
state: enabled
policy: deny
Common Mistakes
| Mistake | Fix |
|---|---|
Quotes around when: values |
Use bare when: var == "val", not when: "var == 'val'" (causes errors) |
Using shell: for simple commands |
Use command: unless you need pipes/redirects — safer |
Forgetting become: yes for system tasks |
Add become: yes on the play or task |
| Not using handlers for service restarts | Handlers deduplicate restarts; direct service: state=restarted always runs |
| Hardcoding secrets in playbooks | Use Ansible Vault or vars_prompt |
Missing loop_control: label: on loops |
Complex loop items print full dict; set label: "{{ item.name }}" |
| Ignoring changed_when on scripts | Scripts always report changed; use changed_when: result.stdout != "" |
Using with_items in new code |
with_items is legacy; use loop: instead |
Ansible vs Alternatives
| Tool | Agent | Language | Best For |
|---|---|---|---|
| Ansible | Agentless (SSH) | YAML | General automation, easy start |
| Terraform | Agentless (API) | HCL | Infrastructure provisioning |
| Puppet | Agent-based | Puppet DSL | Large-scale config management |
| Chef | Agent-based | Ruby DSL | Ruby-heavy shops |
| SaltStack | Agent optional | YAML/Jinja2 | High-speed, event-driven |
| Fabric | Agentless (SSH) | Python | Simple scripted deploys |
FAQ
Q: When should I use command: vs shell:?
Use command: by default — it's safer because it doesn't invoke a shell, so no glob expansion or variable injection risks. Use shell: only when you need pipes (|), redirects (>), or shell builtins.
Q: How do I run a playbook against just one host?
Use --limit: ansible-playbook site.yml --limit web01. You can also use patterns: --limit 'web*' or --limit 'webservers:!web03'.
Q: What's the difference between import_tasks and include_tasks?import_tasks is static — parsed before execution, so tags and when on the import apply to all tasks inside. include_tasks is dynamic — evaluated at runtime, enabling variable file names like include_tasks: "{{ env }}.yml".
Q: How do I speed up Ansible for large inventories?
Enable pipelining = True in ansible.cfg, increase forks, use gather_facts: no where facts aren't needed, and cache facts with fact_caching = jsonfile.
Q: How do I test playbooks without applying changes?
Use --check for dry run and --diff to see what would change: ansible-playbook site.yml --check --diff. For full testing use Molecule — it spins up Docker containers and tests your role end-to-end.
Q: Should I use Ansible for provisioning infrastructure or just configuration?
Ansible can do both, but Terraform is better for provisioning (cloud VMs, networks, DNS). Use Ansible for configuration management after infrastructure exists — it's the most common pattern: Terraform provisions, Ansible configures.