Ansible Cheatsheet - Playbook, Modules & Ad-Hoc Commands

Ansible's defining trait is that it installs no agent on managed machines and executes everything over SSH, which sets its boundary precisely: if you can SSH to it, you can manage it, across clouds, datacenters and ephemeral containers alike. The second trait is module idempotence - running the same task twice should change nothing - which is what makes it safe to re-run, and what separates it fundamentally from batch shell scripting, where a second pass often breaks things because no state is inspected. This sheet covers core commands, ad-hoc commands, common modules, playbook structure, variables and templates, inventory management, and roles with best practices. Reach for it by starting from an ad-hoc one-liner to prove connectivity, then promoting it into a playbook; before any change, dry-run with --check, or validate syntax alone with --syntax-check. Roles are where team collaboration turns: variables in defaults, reusable tasks in roles, secrets in ansible-vault - get those three right and a playbook becomes safe for someone else to edit.

SysOps·42 commands·Last updated 2026-07-21

Typical Use Case

For the ops engineer who must change dozens of identical machines and cannot accept a script with one machine quietly missed. The core is idempotence: a second run should produce no further change, which is why modules beat shell, and shell is a last resort guarded by creates or removes. Covers ad-hoc commands, common modules, playbook structure with handlers and roles, variables and templating, inventory groups, and ansible-galaxy. The habit to build is asking what a second run would do.

Core Commands 6

ansible all -m ping
Test connectivity to all hosts
ansible-playbook site.yml
Run a playbook
ansible-galaxy init myrole
Init a role directory skeleton
ansible-galaxy install geerlingguy.nginx
Install a role from Galaxy
ansible-vault encrypt secrets.yml
Encrypt a secrets file
ansible-doc -l | grep copy
List modules and search usage

Ad-hoc Commands 7

ansible webservers -m shell -a "uptime"
Run a shell command on a group
ansible all -m setup -a "filter=ansible_os_family"
Gather host facts
ansible all -m copy -a "src=file.txt dest=/tmp/"
Copy a file to remote hosts
ansible all -m service -a "name=nginx state=started"
Start a service
ansible webservers -m yum -a "name=nginx state=latest"
Install or upgrade a package
ansible all -m ping --limit web1
Run only on matching hosts
ansible all -m shell -a "df -h" -f 10
Set fork count to parallelize

Common Modules 7

- name: Install nginx\n apt:\n name: nginx\n state: present
apt/yum package module
- name: Copy config\n template:\n src: nginx.conf.j2\n dest: /etc/nginx/nginx.conf
template rendering module
- name: Start service\n service:\n name: nginx\n state: started\n enabled: yes
service management module
- name: Create user\n user:\n name: deploy\n shell: /bin/bash
user management module
- name: Run command\n shell: npm run build\n args:\n chdir: /app
shell command module
- name: Create dir\n file:\n path: /opt/app\n state: directory\n mode: "0755"
file/directory module
- name: Git clone\n git:\n repo: https://github.com/x/y.git\n dest: /srv/app
git checkout module

Playbook Structure 6

- hosts: webservers\n become: yes\n tasks: [...]
Target hosts and enable sudo
pre_tasks: / post_tasks:
Tasks before/after roles
handlers:\n - name: restart nginx\n service: name=nginx state=restarted
Define a handler (triggered by notify)
vars:\n nginx_port: 80
Inline variables in a playbook
- name: notify handler\n template: ...\n notify: restart nginx
Notify a handler after a change
- import_playbook: db.yml
Import another playbook

Variables & Templates 6

vars_files:\n - vars/secrets.yml
Load variables from a file
- command: ...\n when: ansible_os_family == "Debian"
Conditional run with Jinja2
- user: name={{ item }}\n with_items:\n - alice\n - bob
Loop over a list
- command: ...\n register: result
Register task output to a variable
{{ nginx_port }} / {{ ansible_hostname }}
Jinja2 variable syntax
when: result.rc == 0
Condition on the previous task's result

Inventory 5

[webservers]\nweb1 ansible_host=10.0.1.1\nweb2 ansible_host=10.0.1.2
INI-format inventory
[web:children]\nweb1\nweb2
Define group nesting
ansible all -i hosts.yml --list-hosts
List matched hosts with an inventory
ansible all -i ec2.py --list-hosts
Use a dynamic inventory (cloud)
web1 ansible_host=10.0.1.1 ansible_user=deploy ansible_port=2222
Set host connection params

Roles & Best Practices 5

ansible-galaxy init myrole
Generate a standard role layout
roles/myrole/{tasks,handlers,templates,files,defaults,vars}/main.yml
Standard role directory convention
- hosts: web\n roles:\n - nginx\n - { role: db, when: enable_db }
Use roles in a playbook
- import_role:\n name: nginx\n tags: setup
Statically import a role with tags
[defaults]\ninventory = hosts\nremote_user = deploy\nhost_key_checking = False
ansible.cfg global config

Parameter matrix

ParameterEffectExample
ansible all -m pingad-hoc 模式跑 ping 模块,验证连通性与 Python 可用性,是任何操作前的第一步ansible all -m ping
ansible webservers -m shell -a "uptime"在 webservers 组内执行命令;shell 不幂等,重跑就会再执行一次ansible webservers -m shell -a "uptime"
ansible all -m setup -a "filter=ansible_os_family"采集事实并按 filter 只取需要的字段,比全量 setup 输出可读得多ansible all -m setup -a "filter=ansible_os_family"
ansible all -m copy -a "src=file.txt dest=/tmp/"幂等拷贝;文件内容相同则 reported changed 为 falseansible all -m copy -a "src=file.txt dest=/tmp/"
ansible all -m service -a "name=nginx state=started"声明服务处于 running 状态;已经是 running 就不会重启ansible all -m service -a "name=nginx state=started"
ansible all -m ping --limit web1把作用范围收敛到单台主机,是分批灰度与排障时的关键开关ansible all -m ping --limit web1
ansible all -m shell -a "df -h" -f 10-f 指定并发数;默认 5 并发,几百台机器时并发太低调高效率也容易打满控制端ansible all -m shell -a "df -h" -f 10
ansible-playbook site.yml执行 playbook,是所有正式变更的唯一入口ansible-playbook site.yml
ansible-galaxy init myrole生成符合官方约定的角色骨架(tasks/handlers/templates/files/defaults/vars)ansible-galaxy init myrole
ansible-galaxy install geerlingguy.nginx从 Galaxy 安装第三方角色;执行前应固定版本,否则升级会引入未预期变更ansible-galaxy install geerlingguy.nginx
ansible-vault encrypt secrets.yml加密敏感变量文件,解密需要 vault 密码,密码本身不要放进仓库ansible-vault encrypt secrets.yml
ansible-doc -l | grep copy查模块清单与文档,参数记不清时用它比搜索引擎更准ansible-doc -l | grep copy
apt: name=nginx state=present声明包处于已安装状态,比 command + dpkg 幂等得多- name: Install nginx\n apt:\n name: nginx\n state: present
template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf用 Jinja2 模板下发配置,模板变化才会触发实际写入- name: Copy config\n template:\n src: nginx.conf.j2\n dest: /etc/nginx/nginx.conf
service: name=nginx state=restarted enabled=yes重启服务并设置开机自启;state=restarted 是无条件重启,需配合 handler 才合理- name: Start service\n service:\n name: nginx\n state: started\n enabled: yes
shell: npm run build\n args:\n chdir: /app在指定目录执行命令;chdir 是必需的,否则会在控制端的当前目录跑- name: Run command\n shell: npm run build\n args:\n chdir: /app
file: path=/opt/app state=directory mode="0755"幂等创建目录与设置权限;mode 要写成字符串,否则 0755 会被当十进制解析- name: Create dir\n file:\n path: /opt/app\n state: directory\n mode: "0755"
git: repo=... dest=/srv/app幂等拉取代码,可加 version 指定分支或 tag- name: Git clone\n git:\n repo: https://github.com/x/y.git\n dest: /srv/app
hosts: webservers + become: yes任务作用范围与提权开关;become 是最常见的权限坑来源- hosts: webservers\n become: yes\n tasks: [...]
pre_tasks: / post_tasks:把动作插在 roles 前后,用来做前置断言与收尾校验pre_tasks: / post_tasks:
handlers + notify任务变更时才触发一次收尾动作;用 notify 而不是直接写重启可避免配置没变也重启- name: notify handler\n template: ...\n notify: restart nginx
import_playbook: db.yml把 playbook 拆分后复用,避免一个巨型 playbook 失控- import_playbook: db.yml
vars_files: - vars/secrets.yml注入变量文件,与 inventory 的 group_vars/host_vars 共同构成优先级链vars_files:\n - vars/secrets.yml
when: ansible_os_family == "Debian"条件执行,跨发行版 playbook 靠它分流包管理器与服务名- command: ...\n when: ansible_os_family == "Debian"
register: result + when: result.rc == 0捕获前一步结果再做判断,是把非幂等命令包成幂等的常用手段- command: ...\n register: result
with_items:对列表逐项展开任务,比写 N 条重复任务更容易维护- user: name={{ item }}\n with_items:\n - alice\n - bob
[web:children]组的嵌套,让 web 组自动包含 child 组,删掉主机会自动从组里消失[web:children]\nweb1\nweb2
ansible all -i hosts.yml --list-hosts只解析 inventory 并列出目标主机,不执行任何任务,是排 inventory 问题的第一步ansible all -i hosts.yml --list-hosts
web1 ansible_host=10.0.1.1 ansible_user=deploy ansible_port=2222主机级变量;连不上时几乎总是这里的三项之一配错web1 ansible_host=10.0.1.1 ansible_user=deploy ansible_port=2222
roles/myrole/{tasks,handlers,templates,files,defaults,vars}/main.yml官方约定的角色目录结构,defaults 放可覆盖默认值、vars 放内部变量roles/myrole/{tasks,handlers,templates,files,defaults,vars}/main.yml
- { role: db, when: enable_db }在 roles 列表里给单个角色加条件,实现「按开关启用一组任务」- hosts: web\n roles:\n - nginx\n - { role: db, when: enable_db }
tags: setup打标签后可用 --tags 只跑一部分,是大 playbook 的必备逃生阀- import_role:\n name: nginx\n tags: setup
[defaults]\ninventory = hostsansible.cfg 默认段,集中管理 inventory 路径与 host_key_checking 等行为[defaults]\ninventory = hosts\nremote_user = deploy\nhost_key_checking = False

Common pitfalls

Symptom模板已经修改了配置,但服务没有重启,变更看起来没生效。

Cause重启被写在任务里而不是 handler 里,或者模板内容实际没变化导致任务 reported changed=false。

Fix把重启写成 handler 并用 notify 触发;先确认任务输出里确实出现 changed,再用 `--check --diff` 预演模板差异。

Symptom任务报 "command not found" 或找不到 node / npm,终端里同样的命令正常。

Cause非交互 shell 没加载 nvm / pyenv 的环境,PATH 里没有这些目录。

Fix在任务里用 command 加 `chdir` 与 `environment`,或先执行 profile 脚本;不要指望 login shell 的 rc 生效。

Symptom首次运行成功,第二次运行依然报大量 changed,幂等性失效。

Cause用了 shell / command 模块且缺少 creates / removes 之类的判断条件,任务本身不幂等。

Fix优先换成对应模块(file / copy / service / user);确实只能用 command 时加 creates 或先用 stat 注册结果再 when 跳过。

Symptommode: 0755 权限设错,实际变成了 0755 之外的奇怪值或被当成十进制 755。

CauseYAML 会把 0755 解析成八进制或十进制,取决于写法与解析器行为,字符串更安全。

Fix把 mode 写成字符串 `"0755"`,需要运算时用 `mode: "{{ 0o755 | int }}"` 明确转换。

Symptom变量明明在 group_vars 里写了,模板里却是空的。

Cause优先级链顺序被忽略:命令行 > role defaults > inventory group_vars/host_vars > role vars,变量名写错层级也看不出来。

Fix用 `ansible all -m debug -a "var=nginx_port"` 在目标主机上确认实际生效值,再核对变量名拼写。

Symptomplaybook 卡在某台主机上不动,后面所有任务都被拖住。

Cause默认 ssh 连接会长时间等待超时,且 host 数量多时没有并发上限。

Fix给 SSH 配置加 ConnectTimeout 与 ssh_args 的 ControlMaster 复用,playbook 里用 `-f` 限制并发并用 serial 分批。

Symptom运行时报 "MODULE FAILURE" 或 skipped 掉一批主机。

Cause目标主机没有 Python 解释器(老旧发行版常见),或远端 Python 版本不满足要求。

Fix先 `ansible all -m ping` 确认连通性;对无 Python 的机器用 raw 模块做最小引导后再切回正常模块。

Symptomgalaxy 装完角色能跑,但换台机器就失败。

Causerole 依赖未写进 requirements.yml,且默认装的是最新版本,行为随上游漂移。

Fix把角色依赖与版本固定进 requirements.yml,部署流程里先跑 galaxy install 再跑 playbook。

Troubleshooting

  1. 1只解析 inventory,确认目标主机列表

    ansible all -i hosts.yml --list-hosts

    列表不对时后面所有任务都白搭,先把主机范围对。

  2. 2验证连通性与远端 Python

    ansible all -m ping

    ping 失败先解决 SSH、become 与 Python,而不是继续跑任务。

  3. 3只看目标主机的关键事实字段

    ansible all -m setup -a "filter=ansible_os_family"

    确认发行版族,跨发行版的条件分支依赖它。

  4. 4预演变更,不实际落地

    ansible-playbook site.yml --check --diff

    --check 只预测,--diff 显示文件差异,是上线前最该跑的一步。

  5. 5限制到单台主机排查

    ansible-playbook site.yml --limit web1

    缩小爆炸半径,同时 -vvv 看完整输出。

  6. 6查看某个模块的完整参数说明

    ansible-doc -l | grep copy

    参数记不清时以模块文档为准,不要凭记忆拼参数。

Command Examples

Check connectivity before touching production

ansible all -i hosts.yml --list-hosts ansible all -m ping

--list-hosts 先确认匹配到哪些主机,避免组名写错把任务打到意外的范围。ping 验证 SSH 与 Python 依赖是否就绪。

Output

hosts (4):
  web1
  web2
  db1
  db2

web1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Deploy config with a template; restart only on change

- name: Render nginx.conf template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf notify: restart nginx handlers: - name: restart nginx service: name=nginx state=restarted

handler 只在任务 report changed 时触发,配置没变就不重启服务,避免无意义的连接中断。这是 handler 最主要的用途。

Dry-run with --check

ansible-playbook site.yml --check --diff

--check 不真正执行,--diff 显示将发生的变更。上生产前用它确认影响面,尤其配合限制目标 --limit web1。

Output

TASK [Gathering Facts] *************************************
TASK [template : Render nginx.conf] ************************
--- before
+++ after
@@ -1 +1 @@
-worker_processes auto;
+worker_processes 4;

Common Pitfalls

  • ad-hoc -m shell is not guaranteed idempotent, so rerunning it can cause side effects; write a Playbook with idempotent modules for anything repeated.
  • Handlers run at the end of the play, not right after the task that triggered them; use a regular task when timing matters.
  • A mistyped group name silently matches nothing — run --list-hosts to confirm the target set before executing.
  • Template rendering failures, such as undefined Jinja2 variables, abort the task; encrypt sensitive variables with ansible-vault instead of inlining them.
  • register inside a loop accumulates into a list, so referencing it directly yields only the last item rather than all of them.

Tips

  • Ansible connects over SSH - no agent needed on targets.
  • template renders configs with Jinja2, more flexible than copy.
  • --check (dry-run) previews changes without applying them.
  • Role layout: tasks/main.yml, handlers/main.yml, templates/, files/, defaults/main.yml.
  • Encrypt secrets with ansible-vault via vars_files; never hardcode.
  • group_vars/ and host_vars/ auto-load per group/host - easier to maintain.

FAQ

When do Ansible handlers actually run?

A handler runs only when a task that notified it reported a change, it runs once, and it executes after all regular tasks have completed. That is the core distinction: ordinary tasks run every time, handlers run only when something changed. So if the rendered config is identical, the service is not restarted, avoiding pointless service churn. If you always want it to run, use a regular task instead.

When should I use ad-hoc commands versus a Playbook?

Ad-hoc is right for one-off work and quick triage — ansible webservers -m shell -a "uptime" answers a state question immediately. Playbooks are for anything repeated, version-controlled, or involving logic: they can be code reviewed and dry-run with --check. The rule of thumb: if you will run the operation again, write a Playbook.

What does idempotent mean, and why do Ansible modules emphasize it?

Idempotent means running an operation N times leaves the same result as running it once. Copying a config whose source has not changed transfers nothing and leaves mtime untouched; a service module with state=started means ensure it is running, doing nothing if it already is. Preferring these purpose-built modules over shell commands buys you idempotency for free and avoids hand-rolled state checks.

How do I run a task only under certain conditions?

Use the when field with a Jinja2 expression, for example when: ansible_os_family == "Debian", so the task runs only on Debian-family hosts. Extract the expression into a variable when reusing it across tasks. For iteration use with_items: - user: name={{ item }} with with_items executes the task once per element.

How should sensitive data be handled?

Encrypt the variables file with ansible-vault encrypt secrets.yml and reference it from vars_files. For templating use the template module with Jinja2 like {{ nginx_port }} rather than building strings by hand. Connection settings such as host_key_checking belong in ansible.cfg so they are configured once instead of scattered across playbooks.

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.

Maintained by LaoHand

Publicly updated on Jul 21, 2026, continuously proofread against official docs.

Contact Us

Wrong command or description? Send us corrections, business inquiries or product feedback by email.

Contact Us