Toolmingo
Guides14 min read

Ansible Cheat Sheet: Commands, Playbooks & Modules

A complete Ansible cheat sheet — ad-hoc commands, playbook syntax, inventory, variables, loops, conditionals, roles, Vault, and production patterns with copy-ready examples.

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.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

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

Browse all tools