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.
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 pingansible-playbook site.ymlansible-galaxy init myroleansible-galaxy install geerlingguy.nginxansible-vault encrypt secrets.ymlansible-doc -l | grep copyAd-hoc Commands 7
ansible webservers -m shell -a "uptime"ansible all -m setup -a "filter=ansible_os_family"ansible all -m copy -a "src=file.txt dest=/tmp/"ansible all -m service -a "name=nginx state=started"ansible webservers -m yum -a "name=nginx state=latest"ansible all -m ping --limit web1ansible all -m shell -a "df -h" -f 10Common Modules 7
- name: Install nginx\n apt:\n name: nginx\n state: present- name: Copy config\n template:\n src: nginx.conf.j2\n dest: /etc/nginx/nginx.conf- name: Start service\n service:\n name: nginx\n state: started\n enabled: yes- name: Create user\n user:\n name: deploy\n shell: /bin/bash- name: Run command\n shell: npm run build\n args:\n chdir: /app- name: Create dir\n file:\n path: /opt/app\n state: directory\n mode: "0755"- name: Git clone\n git:\n repo: https://github.com/x/y.git\n dest: /srv/appPlaybook Structure 6
- hosts: webservers\n become: yes\n tasks: [...]pre_tasks: / post_tasks:handlers:\n - name: restart nginx\n service: name=nginx state=restartedvars:\n nginx_port: 80- name: notify handler\n template: ...\n notify: restart nginx- import_playbook: db.ymlVariables & Templates 6
vars_files:\n - vars/secrets.yml- command: ...\n when: ansible_os_family == "Debian"- user: name={{ item }}\n with_items:\n - alice\n - bob- command: ...\n register: result{{ nginx_port }} / {{ ansible_hostname }}when: result.rc == 0Inventory 5
[webservers]\nweb1 ansible_host=10.0.1.1\nweb2 ansible_host=10.0.1.2[web:children]\nweb1\nweb2ansible all -i hosts.yml --list-hostsansible all -i ec2.py --list-hostsweb1 ansible_host=10.0.1.1 ansible_user=deploy ansible_port=2222Roles & Best Practices 5
ansible-galaxy init myroleroles/myrole/{tasks,handlers,templates,files,defaults,vars}/main.yml- hosts: web\n roles:\n - nginx\n - { role: db, when: enable_db }- import_role:\n name: nginx\n tags: setup[defaults]\ninventory = hosts\nremote_user = deploy\nhost_key_checking = FalseParameter matrix
| Parameter | Effect | Example |
|---|---|---|
ansible all -m ping | ad-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 为 false | ansible 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 = hosts | ansible.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只解析 inventory,确认目标主机列表
ansible all -i hosts.yml --list-hosts列表不对时后面所有任务都白搭,先把主机范围对。
2验证连通性与远端 Python
ansible all -m pingping 失败先解决 SSH、become 与 Python,而不是继续跑任务。
3只看目标主机的关键事实字段
ansible all -m setup -a "filter=ansible_os_family"确认发行版族,跨发行版的条件分支依赖它。
4预演变更,不实际落地
ansible-playbook site.yml --check --diff--check 只预测,--diff 显示文件差异,是上线前最该跑的一步。
5限制到单台主机排查
ansible-playbook site.yml --limit web1缩小爆炸半径,同时 -vvv 看完整输出。
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=restartedhandler 只在任务 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