# Ansible CLI Cheat Sheet

> **Tool:** Ansible (ansible-playbook, ansible, ansible-vault)
> **Category:** Infrastructure as Code
> **Verified against:** ansible-core 2.20.0, flags verified via `<cmd> --help` and `ansible-galaxy role init` tested against a live role scaffold; the roles-directory layout and `ansible-lint` sections verified via official docs (docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html, ansible.readthedocs.io/projects/lint), not executed locally, 2026-08-21
> **Official docs:** https://docs.ansible.com/ansible/latest/cli/

Ad-hoc one-off commands, running playbooks, encrypting secrets with Vault, and inspecting inventory.

## Ad-hoc commands (no playbook)

```bash
ansible all -i inventory.ini -m ping                          # connectivity check against every host
ansible webservers -i inventory.ini -a "systemctl status nginx"   # -a with no -m defaults to the 'command' module
ansible webservers -i inventory.ini -m yum -a "name=nginx state=present" -b   # -b = become (sudo)
ansible webservers -i inventory.ini -m ping -l "web01,web02"   # limit to a subset of the matched pattern
```

`-m ping` is the standard first check when troubleshooting connectivity — it verifies SSH access and that Python is reachable on the target, without changing anything. `-a` without `-m` implicitly uses the `command` module, running the string as a raw shell command.

## Running a playbook

```bash
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml --limit web01
ansible-playbook -i inventory.ini site.yml --tags "deploy,config"
ansible-playbook -i inventory.ini site.yml --skip-tags "slow-tests"
ansible-playbook -i inventory.ini site.yml -e "app_version=1.4.2"   # pass extra vars from the command line
```

## Dry-run and diff before applying

```bash
ansible-playbook -i inventory.ini site.yml --check              # predict changes without making them
ansible-playbook -i inventory.ini site.yml --check --diff       # + show the actual file/template diffs
ansible-playbook -i inventory.ini site.yml --syntax-check       # validate YAML/module syntax only, no connection
ansible-playbook -i inventory.ini site.yml --list-tasks         # show what would run, in order
```

`--check` mode is not a guarantee — modules that don't support check mode (some shell/command tasks) either skip or report inaccurately. Treat it as a strong signal for well-behaved modules, not an absolute preview for every task type.

## Debugging a playbook run

```bash
ansible-playbook -i inventory.ini site.yml -v      # verbose (stack -vv, -vvv, -vvvv for more detail per level)
ansible-playbook -i inventory.ini site.yml --start-at-task="Install package"   # resume from a specific task
ansible-playbook -i inventory.ini site.yml --step  # confirm each task interactively before running it
```

## Ansible Vault — encrypting secrets

```bash
ansible-vault create secrets.yml                    # create a new encrypted file
ansible-vault edit secrets.yml                       # decrypt, open in $EDITOR, re-encrypt on save
ansible-vault view secrets.yml                        # print decrypted contents, don't write to disk
ansible-vault encrypt group_vars/prod/vault.yml        # encrypt an existing plaintext file in place
ansible-vault decrypt group_vars/prod/vault.yml
ansible-playbook -i inventory.ini site.yml --ask-vault-pass       # prompt for the vault password at runtime
ansible-playbook -i inventory.ini site.yml --vault-password-file=.vault-pass   # read it from a file instead
```

## Inventory

```bash
ansible-inventory -i inventory.ini --list          # full inventory as JSON
ansible-inventory -i inventory.ini --graph          # human-readable group/host tree
ansible-inventory -i inventory.ini --host web01      # variables resolved for one specific host
```

Static inventory files use INI or YAML — group hosts under `[groupname]` headers in INI, with `[groupname:vars]` for group-level variables and `[groupname:children]` to nest groups. `--graph` is the fastest way to sanity-check that a nested group structure actually resolved the way you intended before running a playbook against it.

## Roles — directory structure and scaffolding

```bash
ansible-galaxy role init myrole    # scaffold the standard role directory layout
```

```
myrole/
├── defaults/main.yml    # lowest-precedence variables, meant to be overridden
├── files/                # static files referenced by the `copy` module
├── handlers/main.yml     # tasks triggered by `notify`
├── meta/main.yml         # role metadata + dependencies on other roles
├── tasks/main.yml        # the role's actual task list
├── templates/            # Jinja2 templates referenced by the `template` module
├── tests/                # a minimal test inventory + playbook
└── vars/main.yml         # higher-precedence variables, not meant to be overridden
```

A role is included from a playbook with `roles: [myrole]` or, for finer control over ordering relative to other tasks, `import_role`/`include_role`. Ansible discovers a role by name in `roles/` next to the playbook, or in any path listed in `ANSIBLE_ROLES_PATH` — no explicit path is needed if it lives in the conventional location.

## Installing roles and collections from Galaxy

```bash
ansible-galaxy install geerlingguy.docker              # install a single role from Ansible Galaxy
ansible-galaxy install -r requirements.yml              # install everything listed in a requirements file
ansible-galaxy role list                                 # show installed roles + versions
ansible-galaxy collection install community.general      # install a collection instead of a role
ansible-galaxy collection install -r requirements.yml -p ./collections   # into a project-local path
```

A `requirements.yml` can pin both roles and collections with version constraints in one file — check it into the repo alongside the playbooks so `ansible-galaxy install -r requirements.yml` reproduces the exact dependency set on any machine, rather than relying on whatever happens to already be installed.

## Handlers — running tasks only on change

```yaml
tasks:
  - name: Update nginx config
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify: Restart nginx

handlers:
  - name: Restart nginx
    ansible.builtin.service:
      name: nginx
      state: restarted
```

Handlers only run if a task that `notify`s them actually reports `changed`, and by default they run once, after all regular tasks in the play finish — not immediately after the notifying task. Use `--force-handlers` on `ansible-playbook` to run notified handlers even if a later task in the play fails, or `meta: flush_handlers` in the task list to run them early, mid-play.

## Tags in depth

```bash
ansible-playbook -i inventory.ini site.yml --list-tags     # see every tag defined in the playbook, without running it
ansible-playbook -i inventory.ini site.yml --tags "always,deploy"   # combine the implicit 'always' tag with a specific one
```

Two tag names are special: a task or role tagged `always` runs on every invocation regardless of `--tags`/`--skip-tags` (unless explicitly skipped), and one tagged `never` is skipped by default unless its exact tag is requested with `--tags`. Tags applied to a `roles:` entry or an `import_playbook`/`import_tasks` propagate down to every task inside it — useful for tagging an entire role's inclusion in one place instead of every task within it.

## Privilege escalation with become

```bash
ansible-playbook -i inventory.ini site.yml -b                          # become the default target user (root)
ansible-playbook -i inventory.ini site.yml -b --become-user deploy     # become a specific non-root user
ansible-playbook -i inventory.ini site.yml -b -K                       # prompt for the become password
ansible-playbook -i inventory.ini site.yml -b --become-method su       # use su instead of the default sudo
ansible-doc -t become -l                                                # list every become plugin available
```

`become` can also be set per-task or per-play in YAML (`become: true`, `become_user: deploy`) rather than globally on the CLI — task/play-level settings override whatever was passed on the command line, so a task explicitly marked `become: false` stays unprivileged even if `-b` was passed to `ansible-playbook`.

## Linting playbooks with ansible-lint

```bash
ansible-lint site.yml                    # lint a single playbook
ansible-lint                              # lint the whole current directory (roles, playbooks, etc.)
ansible-lint --profile production         # run the stricter production rule profile instead of the default
ansible-lint -x yaml[line-length]         # exclude a specific rule by its rule id
```

`ansible-lint` is a separate PyPI/pipx package (`pipx install ansible-lint`), not bundled with `ansible-core` — it catches style and correctness issues `ansible-playbook --syntax-check` doesn't, like deprecated module names, missing `name:` keys, and risky `command`/`shell` usage where a dedicated module exists.
