Ansible
本文范围
这篇讲 Ansible 在多机器运维里的落地链路:控制节点安装、受控节点准备、SSH 免密、inventory、ad-hoc、playbook、变量、模板、handlers、常用模块、roles、滚动执行、失败重试、回滚、权限、安全、日志和排障。
不把 Ansible 写成 Terraform、CMDB、堡垒机、工单系统或 K8S Operator。创建云资源、持续调谐 K8S 对象、审批发布权限这些工作要交给对应系统;Ansible 主要负责把“已有机器上的安装、配置、文件、服务和发布动作”自动化。
前置条件
先准备一台控制节点和几台受控节点。控制节点需要能安装 Python 包并访问受控节点 SSH;受控节点需要有 Python、SSH 服务和可控的 sudo 策略。
| 项目 | 要求 |
|---|---|
| 控制节点 | Python 版本满足所选 ansible-core;能安装 ansible、ansible-core 或企业离线包 |
| 受控节点 | SSH 可达,Python 可用,sudo 策略明确 |
| 凭证 | 推荐专用部署用户和密钥,不把密码、私钥、token 写进 playbook |
| 网络 | 控制节点能访问所有目标机器;企业内网准备 pip wheel、collection tarball 和系统包 |
| 版本 | 以 Ansible release/maintenance 表和 Python 兼容矩阵为准,旧系统不要盲目追最新 |
Ansible 官方安装文档明确把 pip/pipx 作为官方支持路径之一,系统包也可以用,但要核对发行版仓库里的 ansible-core 版本。内网环境要提前准备 collections 及其依赖,否则 playbook 到现场才会因为模块缺失失败。
先用一句大白话说清楚 Ansible
如果你只有一台服务器,登录上去执行几条命令,SSH 就够了。
但是机器一多,问题马上就来了:10 台服务器都要安装 JDK,10 台服务器都要发配置文件,10 台服务器都要重启服务,还要保证先发一台、验证没问题再发下一台。这时候你如果还靠手工 SSH,一定会遇到这几类现场问题:
- 有一台机器漏执行了。
- 有一台机器命令执行错目录了。
- 配置文件复制过去了,但是权限不对。
- 服务重启了,但是没有做健康检查。
- 失败时不知道停在哪一台,也不知道能不能重跑。
Ansible 解决的就是这类问题。它不是让服务器变得神奇,而是把你原来手工做的 SSH、scp、yum、systemctl、改配置、重启、检查这些动作,整理成一份可以重复执行的自动化手册。
先记住三个角色:
- 控制节点:你执行
ansible和ansible-playbook的机器。 - 受控节点:被你批量管理的服务器。
- inventory:告诉 Ansible 有哪些机器、怎么分组、用什么用户登录。
Ansible 默认不需要在受控节点安装 agent。一般情况下,控制节点通过 SSH 登录受控节点,把模块传过去执行,然后把结果带回来。所以第一步永远不是写 playbook,而是先把 SSH、Python、sudo 这条链路打通。
这里要注意,Ansible 不是 Terraform,也不是 K8S Operator。创建云主机、VPC、云数据库这类资源,应该优先看 Terraform 或云厂商 IaC;K8S 集群内部持续调谐资源,应该看 Operator 或控制器。Ansible 更适合管“已有机器上的安装、配置、文件、服务和发布动作”。
下面的示例会用这几台机器做演示:
控制节点:ops-control
应用服务器:
app-01 192.0.2.11
app-02 192.0.2.12
入口服务器:
web-01 192.0.2.21
远程登录用户:
deploy实际落地时,把这些主机名、IP、用户和端口换成你的生产信息。不要把真实密码、私钥、token、内网拓扑写进文章、脚本或仓库。
第一关:先安装 Ansible
Ansible 只需要安装在控制节点上。受控节点通常不需要安装 Ansible 本体,但要能 SSH 登录,并且要有 Python。
先看控制节点 Python:
python3 --version如果这里连 Python 都没有,先别急着装 Ansible,先把控制节点基础环境补齐。Ansible 新版本对 Python 版本有要求,旧系统上硬装最新版经常会遇到依赖冲突。
截至 2026-07-10,官方维护表里 ansible 社区包 13.x 是 current-latest,依赖 ansible-core 2.20;ansible-core 2.21 已 GA,控制节点 Python 支持范围是 3.12 到 3.14,受控节点 Python 支持范围是 3.9 到 3.14。生产环境不要只追最新版本,要看操作系统 Python、企业安全基线、离线包来源和回滚能力。
新环境推荐用 pipx 安装:
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install --include-deps ansible这几条命令解决的是“不要把 Ansible 和系统 Python 包混在一起”的问题。pipx 会给命令行工具单独建环境,后面升级、卸载都清楚。
安装完成后验证:
ansible --version
ansible-playbook --version正常情况下,你应该看到 Ansible 版本、Python 版本、配置文件路径、模块搜索路径等信息。如果提示 ansible: command not found,一般是 pipx ensurepath 后当前 shell 还没加载新 PATH,重新打开终端或执行:
export PATH="$HOME/.local/bin:$PATH"如果你只想安装最小核心能力,也可以用:
pipx install ansible-core这里要注意,ansible-core 比 ansible 小,适合团队自己管理 collections 的场景;ansible 包含更多社区 collections,开箱会方便一些。
中国大陆、内网和离线环境怎么装
很多生产现场不是公网开发机,而是企业内网、国产化系统、离线机房。这个时候安装 Ansible 的核心不是“找一条命令”,而是准备依赖闭环。
如果控制节点可以访问企业内网 PyPI,先配置 pip 源:
python3 -m pip config set global.index-url https://pypi.example.com/simple
python3 -m pip config set global.trusted-host pypi.example.com这两条命令解决的是 Python 包下载入口问题。配置后再安装:
pipx install --include-deps ansible如果是完全离线,先在联网机器下载 wheelhouse:
mkdir -p wheelhouse
python3 -m pip download "ansible==13.*" -d wheelhouse
tar -czf ansible-wheelhouse.tar.gz wheelhouse
sha256sum ansible-wheelhouse.tar.gz > ansible-wheelhouse.tar.gz.sha256把压缩包带到离线控制节点后安装:
sha256sum -c ansible-wheelhouse.tar.gz.sha256
tar -xzf ansible-wheelhouse.tar.gz
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --no-index --find-links=wheelhouse "ansible==13.*"
ansible --version这里要看两个结果:
sha256sum -c必须显示 OK,说明包没被改坏。ansible --version能输出版本,说明离线安装链路可用。
如果 playbook 用到了额外 collections,也要提前下载。联网机器执行:
cat > requirements.yml <<'EOF'
---
collections:
- name: ansible.posix
- name: community.general
EOF
mkdir -p collections-cache
ansible-galaxy collection download -r requirements.yml -p collections-cache离线控制节点安装:
mkdir -p collections
ansible-galaxy collection install collections-cache/*.tar.gz -p collections然后在 ansible.cfg 里指定:
[defaults]
collections_path = ./collections生产建议是:能用 ansible.builtin 内置模块解决的事情,先不要引入额外 collections。离线环境里,每多一个依赖,就多一个版本、校验、审计和升级成本。
第二关:准备受控节点
受控节点不一定要安装 Ansible,但要满足三个条件:
- SSH 服务可用。
- Python 可用。
- 需要提权时 sudo 可用。
Ubuntu / Debian 上执行:
sudo apt update
sudo apt install -y python3 python3-apt sudo openssh-server
sudo systemctl enable --now ssh这几条命令做了三件事:安装 Python、安装 apt 模块需要的 python3-apt、启动 SSH 服务。
RHEL / Rocky / AlmaLinux / CentOS Stream / 国产化 RHEL 兼容系统执行:
sudo dnf install -y python3 python3-dnf sudo openssh-server
sudo systemctl enable --now sshd这里多装的 python3-dnf 是给 Ansible 的 dnf 模块用的。如果没有它,后面安装软件包时可能会报 Python 库缺失。
CentOS 7 这类旧系统可能还是 yum:
sudo yum install -y python3 sudo openssh-server
sudo systemctl enable --now sshd这里要注意,Ansible 最新文档里 ansible.builtin.yum 已经重定向到 ansible.builtin.dnf,YUM backend 在 ansible-core 2.17 后移除。新 playbook 优先写 dnf 或 package,yum 只作为老系统兼容场景处理。
验证受控节点:
python3 --version
command -v sudo
systemctl status sshd --no-pager || systemctl status ssh --no-pager如果 SSH 服务没起来,控制节点一定连不上;如果 Python 不存在,Ansible 的大部分模块执行不了;如果 sudo 不可用,后面安装包、写 systemd、重启服务都会失败。
第三关:先把 SSH 免密打通
Ansible 连接失败,十次有七八次是 SSH 问题。这里不要急着写 YAML,先用原生 SSH 跑通。
在控制节点生成密钥:
ssh-keygen -t ed25519 -C "ansible-deploy-20260709" -f ~/.ssh/id_ed25519_ansible如果要兼容很老的系统,再用 RSA:
ssh-keygen -t rsa -b 4096 -C "ansible-legacy-20260709" -f ~/.ssh/id_rsa_ansible把公钥复制到受控节点:
ssh-copy-id -i ~/.ssh/id_ed25519_ansible.pub deploy@192.0.2.11
ssh-copy-id -i ~/.ssh/id_ed25519_ansible.pub deploy@192.0.2.12没有 ssh-copy-id 时,可以手工追加:
cat ~/.ssh/id_ed25519_ansible.pub | ssh deploy@192.0.2.11 \
'umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys'修正客户端私钥权限:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_ansible
chmod 644 ~/.ssh/id_ed25519_ansible.pub验证免密:
ssh -i ~/.ssh/id_ed25519_ansible \
-o PreferredAuthentications=publickey \
-o PasswordAuthentication=no \
deploy@192.0.2.11 'whoami && hostname && python3 --version'执行后应该看到远程用户名、主机名和 Python 版本。如果这里还让你输入密码,说明免密没有成功;如果提示 Permission denied,优先检查私钥是否用对、服务端 authorized_keys 是否写到 deploy 用户家目录、权限是否是 700 和 600。
生产环境不要为了省事长期关闭 host key checking。更稳的做法是提前收集主机指纹:
ssh-keyscan -H 192.0.2.11 >> ~/.ssh/known_hosts
ssh-keyscan -H 192.0.2.12 >> ~/.ssh/known_hosts如果出现主机指纹冲突,不要直接清空整个 known_hosts。先确认是不是服务器重装、IP 复用或 DNS 指错了,再删除指定记录:
ssh-keygen -R 192.0.2.11第四关:写 inventory,把机器分组
SSH 能通以后,再告诉 Ansible 有哪些机器。新建一个项目目录:
mkdir -p ansible-ops/inventories/prod/group_vars
mkdir -p ansible-ops/playbooks
mkdir -p ansible-ops/files
mkdir -p ansible-ops/templates
mkdir -p ansible-ops/logs
cd ansible-ops一般不要直接改 /etc/ansible/hosts。项目自带 inventory,迁移、审计和多人协作都会清楚很多。
新建 inventories/prod/hosts.yml:
---
all:
children:
app:
hosts:
app-01:
ansible_host: 192.0.2.11
app-02:
ansible_host: 192.0.2.12
web:
hosts:
web-01:
ansible_host: 192.0.2.21
vars:
ansible_user: deploy
ansible_port: 22
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: ~/.ssh/id_ed25519_ansible这里的 app、web 是主机组。后面你可以对 app 组批量发应用,对 web 组批量发入口配置。
验证 inventory:
ansible-inventory -i inventories/prod/hosts.yml --graph正常会看到类似:
@all:
|--@ungrouped:
|--@app:
| |--app-01
| |--app-02
|--@web:
| |--web-01查看某台主机最终变量:
ansible-inventory -i inventories/prod/hosts.yml --host app-01如果这里看不到 ansible_host、ansible_user,说明 inventory 层级写错了。YAML 最常见的问题就是缩进错一级。
再建一个项目级 ansible.cfg:
[defaults]
inventory = inventories/prod/hosts.yml
host_key_checking = True
forks = 20
timeout = 15
log_path = ./logs/ansible.log
retry_files_enabled = False
stdout_callback = default
[privilege_escalation]
become = False
become_method = sudo
become_ask_pass = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s这里要注意 Ansible 配置文件的查找顺序:先看 ANSIBLE_CONFIG,再看当前目录 ansible.cfg,再看 ~/.ansible.cfg,最后才是 /etc/ansible/ansible.cfg。生产执行前可以用下面命令确认到底加载了哪个配置:
ansible --version
ansible-config dump --only-changed官方还有一个安全提醒:如果当前目录是世界可写目录,Ansible 不会自动加载里面的 ansible.cfg。所以不要在 /tmp 这类目录里跑生产 playbook。
Ansible 执行链路与权限边界
到这里为止,inventory、SSH 和 ansible.cfg 都已经准备好了。正式跑任务前,先把 Ansible 的执行链路看清楚:控制节点不只是“远程执行命令”,它会读取配置和 inventory,建立 SSH 连接,把模块和参数传到受控节点,再由远端 Python 执行;需要系统权限时,才通过 become 进入 sudo 边界。
这个图的重点是边界:SSH 登录用户、远端 Python、sudo 权限和模块执行是四个不同层次。ansible_user 能登录,不代表能装包;become_user 写了 root,不代表启用了 become: true;ping 能通,也不代表后面的 template、systemd_service 有权限写目标路径。
上线前先用下面几组命令验证链路,而不是直接跑部署 playbook:
ansible --version
ansible-config dump --only-changed
ansible-inventory --graph
ansible all -m ping
ansible all -m setup -a "filter=ansible_python*"
ansible app -b -m command -a "sudo -n id"
ansible app -b -m command -a "systemctl status demo-app --no-pager"正常结果应该能证明四件事:配置文件加载的是项目内那份,inventory 分组正确,远端 Python 可用,deploy 用户的非交互 sudo 符合预期。UNREACHABLE 先查 SSH 和网络;FAILED 再查模块参数、权限、路径、包管理器和服务日志。
常见坑是把所有问题都归到 Ansible。比如 SSH 指纹冲突、sudoers 没放行、远端没有 python3-dnf、ansible.cfg 没被加载、日志里打印了敏感变量,本质上都不是 YAML 语法问题。生产上要固定专用部署用户、最小 sudo 白名单、日志留存位置和敏感值 no_log 规则。
第五关:用 ping 模块验证 Ansible 链路
现在执行第一条 Ansible 命令:
ansible all -m ping正常结果类似:
app-01 | SUCCESS => {
"changed": false,
"ping": "pong"
}这个 ping 不是 ICMP ping,它是 Ansible 通过 SSH 登录远程机器、执行 Python 模块、返回结果的测试。能返回 pong,说明 SSH、Python、inventory 基本都通了。
只测 app 组:
ansible app -m ping只测一台:
ansible all --limit app-01 -m ping如果出现 UNREACHABLE,先不要改 playbook,用原生 SSH 排查:
ssh -vvv deploy@192.0.2.11 'whoami'如果出现 Python 相关错误,先看远程解释器:
ansible app-01 -m raw -a "command -v python3 || command -v python"第六关:ad-hoc 命令,先批量做小动作
ad-hoc 命令适合临时检查和小范围批量操作。格式很简单:
ansible <主机或主机组> -m <模块名> -a "<模块参数>"查看主机名:
ansible app -m command -a "hostname"这里用的是 command 模块,它不会经过 shell,所以不能写管道、重定向、变量展开。它适合执行简单命令。
查看磁盘:
ansible app -m command -a "df -h"执行后每台机器都会输出自己的磁盘情况。生产发布前经常先跑这条,避免发包时才发现磁盘满了。
需要管道时用 shell:
ansible app -m shell -a "ss -lntp | grep ':8080' || true"这里要注意,shell 能力更强,风险也更大。能用专用模块就不要用 shell;必须用 shell 时,要清楚它是否会重复执行、是否会改变系统状态。
复制文件:
ansible app -m copy -a "src=files/demo.conf dest=/tmp/demo.conf mode=0644"这条命令会把控制节点的 files/demo.conf 复制到所有 app 机器的 /tmp/demo.conf。执行后看结果里的 changed:
- 第一次复制通常是
changed=true。 - 第二次执行如果内容没变,应该是
changed=false。
这就是 Ansible 模块的幂等感:目标状态已经满足时,不重复制造变更。
安装软件包:
# Ubuntu / Debian
ansible app -b -m apt -a "name=nginx state=present update_cache=yes"
# RHEL / Rocky / AlmaLinux / CentOS Stream
ansible app -b -m dnf -a "name=nginx state=present"
# 简单跨发行版场景
ansible app -b -m package -a "name=curl state=present"这里的 -b 表示 become,也就是提权。安装包通常需要 root 权限。如果提示 sudo 密码,可以临时加:
ansible app -b -K -m command -a "id"生产自动化里不建议长期依赖 -K 交互输入密码。更推荐给 deploy 用户配置有限 sudo 权限。
重启服务:
ansible app -b -m service -a "name=demo-app state=restarted"查看 facts:
ansible app -m setup -a "filter=ansible_distribution*"
ansible app -m setup -a "filter=ansible_python*"facts 是 Ansible 收集的系统信息。后面你想按 Ubuntu 和 Rocky 写不同安装命令,就要用到这些变量。
第七关:从一条命令升级成 playbook
ad-hoc 命令适合临时操作,但只要动作要重复执行,就应该写 playbook。
新建 playbooks/check.yml:
---
- name: Check app nodes
hosts: app
gather_facts: true
tasks:
- name: Show hostname
ansible.builtin.command: hostname
changed_when: false
- name: Show distribution
ansible.builtin.debug:
msg: "{{ ansible_distribution }} {{ ansible_distribution_version }}"执行:
ansible-playbook playbooks/check.yml你应该看到每台 app 机器的主机名和系统版本。
这里有两个小细节:
gather_facts: true会收集系统信息,所以后面能用ansible_distribution。changed_when: false表示这条命令只是查看,不算变更。
执行 playbook 前建议先做语法检查:
ansible-playbook playbooks/check.yml --syntax-check如果 YAML 缩进有问题,这一步就会直接报出来。不要等跑到生产机器上才发现缩进错了。
再看一次预演和差异:
ansible-playbook playbooks/check.yml --check --diff不是所有模块都完整支持 check mode,特别是 command、shell、自定义脚本。预演只能帮你提前发现一部分问题,不能替代小批次真实执行。
变量和模板:配置分发不要靠手改
真实生产里最常见的需求是:同一份应用,不同环境配置不一样。比如端口、JVM 参数、数据库地址、开关参数都不同。
先写 app 组变量 inventories/prod/group_vars/app.yml:
---
app_name: demo-app
app_user: app
app_group: app
app_base_dir: /opt/demo-app
app_port: 8080
app_env: prod
java_opts: "-Xms512m -Xmx512m"
health_url: "http://127.0.0.1:8080/actuator/health"再写模板 templates/app.env.j2:
APP_NAME={{ app_name }}
APP_ENV={{ app_env }}
SERVER_PORT={{ app_port }}
JAVA_OPTS={{ java_opts }}写一个分发配置的 playbook:
---
- name: Render app config
hosts: app
become: true
tasks:
- name: Ensure config directory
ansible.builtin.file:
path: "{{ app_base_dir }}/shared"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0755"
- name: Render app env
ansible.builtin.template:
src: ../templates/app.env.j2
dest: "{{ app_base_dir }}/shared/app.env"
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0640"
backup: true执行:
ansible-playbook playbooks/render_config.yml --diff--diff 会告诉你配置文件变了哪些内容。第一次执行应该看到文件被创建;第二次如果变量没变,应该是绿色的 ok,不会重复 changed。
如果提示变量未定义,比如:
'app_base_dir' is undefined先用下面命令看变量有没有进来:
ansible-inventory --host app-01常见原因是 group_vars 放错目录,或者 inventory 里组名不是 app。
敏感变量不要明文写在 group_vars。可以用 Vault:
ansible-vault create inventories/prod/group_vars/vault.yml
ansible-vault edit inventories/prod/group_vars/vault.yml执行时:
ansible-playbook playbooks/site.yml --ask-vault-pass写密码、token、证书私钥时加 no_log: true:
- name: Write database password
ansible.builtin.copy:
content: "{{ db_password }}"
dest: "{{ app_base_dir }}/shared/db.password"
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0600"
no_log: true这里要注意,no_log 不能阻止你自己用 debug 把密码打印出来。生产 playbook 不要随手 debug: var=hostvars。
常用模块怎么选
刚开始写 Ansible,最容易犯的错是把 Shell 脚本原封不动搬进 YAML。这样能跑,但很难幂等,也不好排障。
创建目录用 file:
- name: Ensure app directory
ansible.builtin.file:
path: /opt/demo-app
state: directory
owner: app
group: app
mode: "0755"复制固定文件用 copy:
- name: Copy application config
ansible.builtin.copy:
src: files/application.yml
dest: /opt/demo-app/conf/application.yml
owner: app
group: app
mode: "0640"
backup: true按变量渲染配置用 template:
- name: Render service config
ansible.builtin.template:
src: app.env.j2
dest: /opt/demo-app/shared/app.env
mode: "0640"安装包用 apt、dnf 或 package:
- name: Install Java on Debian
ansible.builtin.apt:
name: openjdk-17-jre-headless
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Install Java on RedHat
ansible.builtin.dnf:
name: java-17-openjdk-headless
state: present
when: ansible_os_family == "RedHat"这里要注意,package 不会帮你翻译包名。不同系统包名不一样时,还是要用变量区分。
管理服务用 service 或 systemd_service:
- name: Ensure service is started
ansible.builtin.systemd_service:
name: demo-app
state: started
enabled: true如果改了 systemd unit 文件,一般还要:
- name: Reload systemd
ansible.builtin.systemd_service:
daemon_reload: true必须执行命令时,优先用 command:
- name: Check Java version
ansible.builtin.command: java -version
changed_when: false需要管道和重定向时才用 shell:
- name: Check listening port
ansible.builtin.shell: "ss -lntp | grep ':8080' || true"
register: port_check
changed_when: false必须用 shell 改系统状态时,要写清幂等条件:
- name: Initialize data once
ansible.builtin.shell: "/opt/demo-app/bin/init-data.sh"
args:
creates: /opt/demo-app/data/.initializedcreates 的意思是:如果这个文件已经存在,就不再执行命令。这样重复跑 playbook 时就不会反复初始化数据。
handlers:配置变了才重启
很多初学者会在 playbook 最后直接写一个 restart。这样每次执行都会重启服务,哪怕配置根本没变。
更稳的写法是 handlers:
---
- name: Update app config
hosts: app
become: true
tasks:
- name: Render app env
ansible.builtin.template:
src: ../templates/app.env.j2
dest: /opt/demo-app/shared/app.env
mode: "0640"
notify: Restart demo app
handlers:
- name: Restart demo app
ansible.builtin.systemd_service:
name: demo-app
state: restarted这里的逻辑是:模板内容真的发生变化,才通知 handler 重启服务。第二次执行如果配置没变,服务不会重启。
如果你后面马上要做健康检查,可以让 handler 立刻执行:
- name: Flush handlers before health check
ansible.builtin.meta: flush_handlers否则 handler 默认会在当前 play 的任务结束时统一执行。
实战:批量部署一个 demo-app 服务
下面我们把前面的东西串起来,部署一个由 systemd 托管的 Java 服务。这个例子不是为了讲 Java,而是为了把“安装包、建用户、发制品、发配置、注册 systemd、滚动重启、健康检查”这一套生产动作跑完整。
准备目录:
ansible-ops/
ansible.cfg
inventories/prod/hosts.yml
inventories/prod/group_vars/app.yml
playbooks/deploy_app.yml
files/demo-app.jar
templates/app.env.j2
templates/demo-app.service.j2把构建好的应用包放到:
files/demo-app.jar这里要注意,真实生产更推荐从制品库、对象存储或内网 HTTP 下载制品,而不是每次用 Ansible 从控制节点复制大文件。小团队和小包可以先用 copy,规模上来以后要切换到制品仓库。
写 systemd 模板 templates/demo-app.service.j2:
[Unit]
Description={{ app_name }}
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ app_user }}
Group={{ app_group }}
EnvironmentFile={{ app_base_dir }}/shared/app.env
WorkingDirectory={{ app_base_dir }}/current
ExecStart=/usr/bin/java $JAVA_OPTS -jar {{ app_base_dir }}/current/{{ app_name }}.jar
Restart=on-failure
RestartSec=5
SuccessExitStatus=143
StandardOutput=append:/var/log/{{ app_name }}/stdout.log
StandardError=append:/var/log/{{ app_name }}/stderr.log
[Install]
WantedBy=multi-user.target写部署 playbook playbooks/deploy_app.yml:
---
- name: Deploy demo app
hosts: app
become: true
gather_facts: true
serial: 1
max_fail_percentage: 30
pre_tasks:
- name: Validate app_version
ansible.builtin.assert:
that:
- app_version is defined
- app_version | length > 0
fail_msg: "app_version is required, example: -e app_version=1.2.3"
- name: Install Java on Debian family
ansible.builtin.apt:
name: openjdk-17-jre-headless
state: present
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Java on RedHat family
ansible.builtin.dnf:
name: java-17-openjdk-headless
state: present
when: ansible_os_family == "RedHat"
tasks:
- name: Ensure app group
ansible.builtin.group:
name: "{{ app_group }}"
system: true
- name: Ensure app user
ansible.builtin.user:
name: "{{ app_user }}"
group: "{{ app_group }}"
system: true
shell: /sbin/nologin
create_home: false
- name: Ensure directories
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner | default(app_user) }}"
group: "{{ item.group | default(app_group) }}"
mode: "{{ item.mode | default('0755') }}"
loop:
- path: "{{ app_base_dir }}"
owner: root
group: root
- path: "{{ app_base_dir }}/releases/{{ app_version }}"
- path: "{{ app_base_dir }}/shared"
- path: "/var/log/{{ app_name }}"
- name: Upload application jar
ansible.builtin.copy:
src: "../files/{{ app_name }}.jar"
dest: "{{ app_base_dir }}/releases/{{ app_version }}/{{ app_name }}.jar"
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0644"
- name: Render app env
ansible.builtin.template:
src: "../templates/app.env.j2"
dest: "{{ app_base_dir }}/shared/app.env"
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0640"
backup: true
notify: Restart demo app
- name: Render systemd unit
ansible.builtin.template:
src: "../templates/demo-app.service.j2"
dest: "/etc/systemd/system/{{ app_name }}.service"
owner: root
group: root
mode: "0644"
notify:
- Reload systemd
- Restart demo app
- name: Switch current symlink
ansible.builtin.file:
src: "{{ app_base_dir }}/releases/{{ app_version }}"
dest: "{{ app_base_dir }}/current"
state: link
force: true
notify: Restart demo app
- name: Flush handlers before health check
ansible.builtin.meta: flush_handlers
- name: Ensure service is enabled and started
ansible.builtin.systemd_service:
name: "{{ app_name }}"
enabled: true
state: started
- name: Wait for app port
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ app_port }}"
timeout: 60
- name: Check health endpoint
ansible.builtin.uri:
url: "{{ health_url }}"
method: GET
status_code: 200
register: health_check
retries: 10
delay: 3
until: health_check.status == 200
handlers:
- name: Reload systemd
ansible.builtin.systemd_service:
daemon_reload: true
- name: Restart demo app
ansible.builtin.systemd_service:
name: "{{ app_name }}"
state: restarted先做语法检查:
ansible-playbook playbooks/deploy_app.yml --syntax-check如果语法通过,再先发布一台:
ansible-playbook \
playbooks/deploy_app.yml \
-e "app_version=1.2.3" \
--limit app-01 \
--diff执行过程中重点看四类输出:
- 安装 Java 是否成功。
Upload application jar是否 changed。Restart demo app是否执行。Check health endpoint是否返回 200。
一台验证没问题,再滚动发布全组:
ansible-playbook \
playbooks/deploy_app.yml \
-e "app_version=1.2.3" \
--diff执行后验证:
ansible app -b -m command -a "systemctl status demo-app --no-pager"
ansible app -m uri -a "url=http://127.0.0.1:8080/actuator/health status_code=200"
ansible app -m command -a "readlink -f /opt/demo-app/current"正常情况下,服务状态应该是 running,健康接口应该返回成功,current 软链应该指向 /opt/demo-app/releases/1.2.3。
滚动执行:不要一把梭
serial 控制一次执行多少台。比如:
serial: 1表示一台一台执行。适合入口服务、核心应用、容量紧张的业务。
也可以一次两台:
serial: 2或者按比例:
serial: "30%"还可以分阶段扩大:
serial:
- 1
- 2
- "50%"这个写法很适合生产发布:先灰度 1 台,再 2 台,最后扩大到一半。
forks 控制并发:
ansible-playbook playbooks/deploy_app.yml -f 20这里要注意,并发不是越大越好。安装包会打仓库,下载制品会打制品库,重启服务会影响业务容量。发布类 playbook 要先看业务承载能力,再调大并发。
滚动执行与回滚状态机
滚动发布的关键不是“慢一点执行”,而是每一批都有明确状态:开始前检查、执行变更、健康检查、进入下一批或停止扩大影响。只要把状态机画清楚,serial、max_fail_percentage、until、--limit 和回滚 playbook 就不再是零散参数。
这个状态机有两个生产含义。第一,失败后不要继续扩大批次,先把失败主机留在现场里定位;第二,回滚不是重新跑一遍旧版本号,而是要确认旧制品、旧配置、数据库和缓存都兼容,最后还要健康检查通过。
发布前验证:
ansible-playbook playbooks/deploy_app.yml --syntax-check
ansible-playbook playbooks/deploy_app.yml --check --diff
ansible-playbook playbooks/deploy_app.yml -e "app_version=1.2.3" --limit app-01 --diff发布中观察:
ansible-playbook playbooks/deploy_app.yml -e "app_version=1.2.3" --diff
ansible app -m uri -a "url=http://127.0.0.1:8080/actuator/health status_code=200"
ansible app -m command -a "readlink -f /opt/demo-app/current"失败后止血:
ansible-playbook playbooks/deploy_app.yml -e "app_version=1.2.3" --limit app-02 --diff
ansible-playbook playbooks/rollback_app.yml -e "rollback_version=1.2.2" --limit app-02这里最容易翻车的是三件事:serial 写了但健康检查太弱,导致坏版本一批批扩散;run_once 和 serial 混用,结果每个批次都执行一次本该全局只执行一次的任务;回滚 playbook 只切软链,不验证服务端口、健康接口和日志。生产约束是:先单台、再小批量、再扩大;失败主机用 --limit 修,不把全组重跑当默认动作;回滚版本必须提前保留制品和配置,不在事故现场临时找包。
失败重试:先分清连接失败和任务失败
Ansible 失败大概分两类:
UNREACHABLE:连接都没连上。FAILED:连接成功了,但任务执行失败。
连接失败先查 SSH、网络、防火墙、用户名、私钥、sudo。任务失败再查模块参数、权限、目标路径、系统依赖和服务日志。
健康检查这类临时失败,用 until/retries/delay:
- name: Check health endpoint
ansible.builtin.uri:
url: "{{ health_url }}"
status_code: 200
register: health_check
retries: 20
delay: 3
until: health_check.status == 200意思是每 3 秒检查一次,最多 20 次。服务刚启动时经常需要这个等待过程。
机器重启后等 SSH 恢复:
- name: Wait for connection
ansible.builtin.wait_for_connection:
timeout: 300
delay: 5如果某台失败,修好以后用 --limit 重跑:
ansible-playbook playbooks/deploy_app.yml -e "app_version=1.2.3" --limit app-02很多老教程会提 .retry 文件,但当前官方默认 retry_files_enabled = False。不要把生产重试流程建立在“一定会生成 retry 文件”上。需要的话可以显式开启:
[defaults]
retry_files_enabled = True
retry_files_save_path = ./.ansible-retry并把 .ansible-retry/ 加到 .gitignore。
回滚:不是把版本号改回去这么简单
如果应用是软链发布,回滚可以切回旧版本:
- name: Rollback current symlink
ansible.builtin.file:
src: "{{ app_base_dir }}/releases/{{ rollback_version }}"
dest: "{{ app_base_dir }}/current"
state: link
force: true
- name: Restart service after rollback
ansible.builtin.systemd_service:
name: "{{ app_name }}"
state: restarted执行:
ansible-playbook playbooks/rollback_app.yml -e "rollback_version=1.2.2" --limit app-01这里要注意,回滚能不能成功,不只看 Ansible。还要看旧制品是否还在、配置是否兼容、数据库变更能不能回退、缓存和消息是否有兼容问题。
可以在部署任务里用 block/rescue 做失败处理:
- name: Deploy with rescue
block:
- name: Switch current symlink
ansible.builtin.file:
src: "{{ app_base_dir }}/releases/{{ app_version }}"
dest: "{{ app_base_dir }}/current"
state: link
force: true
- name: Restart service
ansible.builtin.systemd_service:
name: "{{ app_name }}"
state: restarted
- name: Check health
ansible.builtin.uri:
url: "{{ health_url }}"
status_code: 200
register: health_check
retries: 10
delay: 3
until: health_check.status == 200
rescue:
- name: Show failed host
ansible.builtin.debug:
msg: "deploy failed on {{ inventory_hostname }}"生产上不要把 rescue 写成“失败就悄悄吞掉”。失败要明确暴露,保留现场,再决定是否自动回滚。
roles:playbook 长了就拆
如果一个 playbook 写到几百行,维护起来就会很难。这个时候用 role。
创建 role:
ansible-galaxy role init roles/app_deploy目录一般长这样:
roles/app_deploy/
defaults/main.yml
files/
handlers/main.yml
tasks/main.yml
templates/
vars/main.ymldefaults/main.yml 放可覆盖默认值:
---
app_name: demo-app
app_user: app
app_group: app
app_base_dir: /opt/demo-app
app_port: 8080tasks/main.yml 放主要步骤:
---
- name: Ensure app user
ansible.builtin.user:
name: "{{ app_user }}"
system: true
shell: /sbin/nologin
- name: Ensure app base directory
ansible.builtin.file:
path: "{{ app_base_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0755"playbook 调用 role:
---
- name: Deploy demo app
hosts: app
become: true
roles:
- role: app_deploy这里要注意 role 的边界。app_deploy 可以负责应用用户、目录、制品、配置、systemd 和健康检查;不要顺手把数据库初始化、Nginx、监控告警、内核参数都塞进去。role 越清楚,复用越容易。
权限、安全和日志
生产环境不建议长期使用 root 直接 SSH。更常见的做法是:
- SSH 使用普通用户,比如
deploy。 - 需要系统权限时用
become: true。 - sudoers 只放开必要命令。
- 私钥有口令,或者由安全系统托管。
- 执行日志能追溯。
sudoers 示例:
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart demo-app
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl status demo-app
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload这里要注意,很多团队为了省事写:
deploy ALL=(ALL) NOPASSWD: ALL这确实省事,但安全边界太大。除非是临时交付环境,并且有明确回收时间,否则不要这么放。
私钥权限:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_ansible使用 ssh-agent:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_ansible日志可以在 ansible.cfg 开:
[defaults]
log_path = ./logs/ansible.log也可以执行时保存一份:
mkdir -p logs
ansible-playbook playbooks/deploy_app.yml -e "app_version=1.2.3" \
2>&1 | tee "logs/deploy-demo-app-$(date +%F-%H%M%S).log"日志里可能有参数和敏感信息。涉及密码、token、私钥时要 no_log: true,同时不要用 debug 打印敏感变量。
.gitignore 建议至少包含:
.vault-pass
*.retry
.ansible-retry/
logs/
*.log常见排障手册
1、连接不上
现象:
UNREACHABLE! => Failed to connect to the host via ssh先看端口:
nc -vz 192.0.2.11 22再看 SSH:
ssh -vvv deploy@192.0.2.11 'whoami'再看 Ansible:
ansible app-01 -m ping -vvvv常见原因是 IP 写错、端口不通、安全组没放、私钥不对、用户名不对、host key 冲突。
2、免密失败
现象:
Permission denied (publickey,password)检查客户端:
ls -l ~/.ssh/id_ed25519_ansible检查服务端:
ssh deploy@192.0.2.11 'ls -ld ~/.ssh && ls -l ~/.ssh/authorized_keys'权限一般应该是:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys3、sudo 卡住
现象是任务一直不动,或者提示:
Missing sudo password直接在远程测:
ssh deploy@192.0.2.11 'sudo -n id'如果失败,说明非交互 sudo 没配好。临时执行可以 -K,生产自动化要改 sudoers 或凭据方案。
4、Python 找不到
现象:
/usr/bin/python: not found查看远程 Python:
ansible app -m raw -a "command -v python3 || command -v python"inventory 里指定:
ansible_python_interpreter: /usr/bin/python3最小化系统可以先用 raw 初始化 Python,但正式任务不要长期靠 raw:
ansible app -b -m raw -a "test -e /usr/bin/python3 || (dnf install -y python3 || yum install -y python3 || (apt-get update && apt-get install -y python3))"5、apt 或 dnf 模块失败
Debian 系如果提示:
python3-apt must be installed执行:
sudo apt update
sudo apt install -y python3-aptRHEL 系如果提示:
No module named dnf执行:
sudo dnf install -y python3-dnf如果提示包管理锁:
ps -ef | grep -E 'apt|dpkg|dnf|yum' | grep -v grep不要一上来就删 lock 文件。先确认是不是有别的安装进程正在跑。
6、模板变量未定义
现象:
'db_host' is undefined先看变量有没有加载:
ansible-inventory --host app-01再做语法检查:
ansible-playbook playbooks/deploy_app.yml --syntax-check解决办法一般是:变量放到正确的 group_vars 或 host_vars,role 默认值补齐,必填变量用 assert 提前校验。
7、service 找不到
现象:
Could not find the requested service demo-app看 systemd 是否识别:
ansible app -b -m command -a "systemctl list-unit-files | grep demo-app"
ansible app -b -m command -a "ls -l /etc/systemd/system/demo-app.service"如果刚下发 unit 文件,别忘了:
- name: Reload systemd
ansible.builtin.systemd_service:
daemon_reload: true8、防火墙和 SELinux
服务本机健康检查成功,但别的机器访问失败,按层检查:
ss -lntp | grep 8080
curl -fsS http://127.0.0.1:8080/actuator/health
curl -fsS http://192.0.2.11:8080/actuator/health
sudo firewall-cmd --list-all
getenforce不要把所有问题都归到 Ansible。Ansible 只是执行器,真正问题可能在监听地址、防火墙、安全组、SELinux、路由或服务自身。
落地工程深水区
Ansible 的深水区不在 YAML 语法,而在“批量动作一旦出错,会同时影响很多机器”。
幂等和可重跑:playbook 必须允许失败后从中断点重跑。能用 copy、template、package、systemd_service 就不要用裸 shell;确实用 shell 时加 creates、removes、changed_when 和 failed_when。
滚动和止血:生产发布不要 all 一把梭。先 --limit app-01 跑一台,验证通过后再批量;批量时使用 serial、max_fail_percentage 和健康检查。失败后第一动作是停止扩大影响,不是立刻重跑全部机器。
权限和审计:受控节点使用专用部署用户,sudo 命令白名单化。Ansible 日志里可能出现环境变量、命令参数和错误输出,敏感值要用 vault、外部密钥系统或 CI 凭证注入,不要直接明文打印。
网络和连接稳定性:大量机器并发时,SSH 连接数、堡垒机限制、防火墙和 DNS 都会成为瓶颈。执行前先小批量验证:
ansible all -m ping
ansible app -m setup -a "filter=ansible_python*"
ansible app -b -m command -a "sudo -n id"版本和依赖闭环:role、collection、Python wheel、系统包都要锁版本。离线环境把 requirements.yml、collection tarball、pip wheelhouse、系统包清单和校验文件放进同一个交付包。
团队协作:inventory 分组、变量命名、role 目录和发布入口必须标准化。不要让每个项目各写一套 ad-hoc 脚本,否则 Ansible 很快会退化成“批量执行混乱命令”。
发布前后检查清单
发布前一般要看这些:
ansible-inventory --graph
ansible all -m ping
ansible app -b -m command -a "sudo -n id"
ansible-playbook playbooks/deploy_app.yml --syntax-check
ansible-playbook playbooks/deploy_app.yml --check --diff这里要注意,--check --diff 只能说明一部分风险。真正上线前还是要先 --limit app-01 跑一台。
发布时:
ansible-playbook playbooks/deploy_app.yml -e "app_version=1.2.3" --limit app-01 --diff
ansible-playbook playbooks/deploy_app.yml -e "app_version=1.2.3" --diff发布后:
ansible app -b -m command -a "systemctl status demo-app --no-pager"
ansible app -m uri -a "url=http://127.0.0.1:8080/actuator/health status_code=200"
ansible app -m command -a "readlink -f /opt/demo-app/current"如果这些命令都清楚,Ansible 就不只是一个“批量执行命令的工具”,而是一套能落到生产现场的自动化运维流程。
官方文档入口
版本升级、模块行为变化、离线 collections 和安全配置,最终都要回到官方文档确认:
- Ansible 安装指南:https://docs.ansible.com/projects/ansible/latest/installation_guide/intro_installation.html
- Ansible release 与维护表:https://docs.ansible.com/projects/ansible/latest/reference_appendices/release_and_maintenance.html
- Inventory 指南:https://docs.ansible.com/projects/ansible/latest/inventory_guide/intro_inventory.html
- Ad-hoc 命令指南:https://docs.ansible.com/projects/ansible/latest/command_guide/intro_adhoc.html
- Playbook 指南:https://docs.ansible.com/projects/ansible/latest/playbook_guide/index.html
- Roles 指南:https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_reuse_roles.html
- 执行策略与滚动发布:https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_strategies.html
- become 提权:https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_privilege_escalation.html
- Ansible 日志:https://docs.ansible.com/projects/ansible/latest/reference_appendices/logging.html
- Ansible 配置:https://docs.ansible.com/projects/ansible/latest/reference_appendices/config.html
- 内置模块索引:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html
- Collections 安装:https://docs.ansible.com/projects/ansible/latest/collections_guide/collections_installing.html
- pip config:https://pip.pypa.io/en/stable/cli/pip_config/
- pip download:https://pip.pypa.io/en/stable/cli/pip_download/
- pip install:https://pip.pypa.io/en/stable/cli/pip_install/
