Ansible
Ansible 在控制端读取主机清单和任务,通过 SSH 将模块送到目标 Linux 执行。模块比较当前状态和期望状态,决定是否修改文件、安装软件或管理服务。一次配置分发因此包含三个不同动作:选中正确机器、写入正确文件、让进程加载并验证新配置。
控制端与受控节点如何协作
一次任务的执行位置
控制端
inventory + group_vars/host_vars + playbook + 模板
↓ 选择主机,合并变量,渲染任务参数
connection plugin:SSH / local
↓ 上传或传递模块
受控节点
连接用户 → 必要时 become → Python / 模块依赖
↓ 读取当前状态,执行变更
ok / changed / failed / unreachable
↓
控制端:处理结果,通知 handler,继续或终止批次普通 Linux 管理不需要常驻 Ansible Agent,但许多模块需要目标端 Python。控制端还需要 SSH 客户端、Python 环境和所用 collection;网络设备或其他连接类型有各自要求。Ansible 安装说明区分了控制端与受控节点依赖。
模块返回 changed 表示它执行或预计执行了变更。文件已经写入时,进程可能仍使用旧配置;此时要执行重载或重启并检查运行结果。Ansible 命令结束后通常不会持续监视目标,也不会自动修复之后发生的手工改动。持续任务调度与账号授权可由 AWX 等平台承接,模块、变量及角色用法还可查 Ansible 工具主文。
选择模块与组织任务
| 任务 | 常用模块 | 关键区别 |
|---|---|---|
| 创建目录、设置权限、管理链接 | file | 明确 owner/group/mode,权限字符串加引号 |
| 复制固定文件 | copy | 默认从控制端读取;remote_src 表示从目标端读取 |
| 按变量生成配置 | template | 控制端渲染 Jinja2,可在替换前运行校验器 |
| 安装软件 | apt、dnf、package | package 抽象基础入口,发行版特有选项仍用对应模块 |
| 托管进程 | systemd_service | daemon_reload 读取 unit;restarted 重启进程 |
| 请求接口 | uri | 请求默认在任务目标执行,需区分本机与入口验证 |
| 执行工具 | command | argv 传参数,无 shell 管道与重定向 |
| 必须使用 shell 语法 | shell | 需要自行处理引用、退出码和幂等 |
优先选择能够读取状态的模块。每次 shell 都返回 changed,会触发无意义重启;把 changed_when 强行设成 false 又可能掩盖真实变化。只读命令可以显式不计变更,安装与发布命令则需要依据真实输出或状态确定结果。
安装并完成第一次配置变更
Python、Ansible 与项目目录
以下使用 Ubuntu 24.04、Python3.12、ansible-core2.21.3。该 core 发行线控制端支持 Python3.12—3.14,常规目标端支持 Python3.9—3.14;升级时按 版本支持矩阵核对,不能只更新 core 而保留不受支持的目标 Python。
由普通 Linux 用户执行,只有系统包安装需要 sudo:
sudo apt-get update
sudo apt-get install -y python3 python3-venv openssh-client unzip
mkdir -p ansible-lab
cd ansible-lab
python3 -m venv .venv
. .venv/bin/activate
python -m pip install 'ansible-core==2.21.3'
ansible --versionRHEL/Rocky/AlmaLinux 使用发行版仓库提供的受支持 Python 与 openssh-clients,通过 dnf 安装;若默认 Python 太旧,使用发行版可用的 python3.12 包,并用该解释器建立虚拟环境。不要向系统 Python 强行覆盖依赖。
版本输出应包括 core2.21.3、Python3.12、虚拟环境下的可执行文件路径。没有项目配置时 config file 为 None 是正常现象;配置文件存在却未加载则需检查当前目录和 ANSIBLE_CONFIG。不要在全员可写的目录中运行自动化工程。
下载 完整示例,解压到当前目录。源文件包括两个逻辑目标的 inventory、配置实验、Java 服务模板及部署 playbook;runtime 是运行时生成的目录,不在压缩包里。
ansible-lab/
├── inventory.yml
├── configure.yml
├── app.conf.j2
├── service.yml
├── demo.service.j2
└── runtime/ 执行后生成
├── lab_a/
└── lab_b/不需要远端服务器的幂等实验
inventory.yml 将两个逻辑主机映射到本地连接;不同目标写不同目录,不修改系统服务:
all:
children:
app:
hosts:
lab_a:
lab_b:
vars:
ansible_connection: local
ansible_python_interpreter: "{{ ansible_playbook_python }}"app.conf.j2 只有一行:
workers={{ workers }}configure.yml 完整内容如下。effective.conf 用来模拟进程已经消费的配置,复制文件并不等价于真实服务重载:
- name: Configure two isolated local targets
hosts: app
gather_facts: false
serial: 1
any_errors_fatal: true
vars:
workers: 4
fail_host: ""
lab_dir: "{{ playbook_dir }}/runtime/{{ inventory_hostname }}"
tasks:
- name: Validate workers before writes
ansible.builtin.assert:
that:
- workers | int >= 1
- workers | int <= 16
- name: Create target directory
ansible.builtin.file:
path: "{{ lab_dir }}"
state: directory
mode: '0700'
- name: Render configuration
ansible.builtin.template:
src: app.conf.j2
dest: "{{ lab_dir }}/app.conf"
mode: '0600'
notify: Apply configuration
- name: Apply before reading effective state
ansible.builtin.meta: flush_handlers
- name: Read effective configuration
ansible.builtin.slurp:
src: "{{ lab_dir }}/effective.conf"
register: effective
- name: Check value and deliberate failure switch
ansible.builtin.assert:
that:
- effective.content | b64decode == 'workers=' ~ workers ~ '\n'
- inventory_hostname != fail_host
handlers:
- name: Apply configuration
ansible.builtin.copy:
src: "{{ lab_dir }}/app.conf"
dest: "{{ lab_dir }}/effective.conf"
remote_src: true
mode: '0600'执行前先查看目标集合,再检查语法并运行:
ansible-inventory -i inventory.yml --graph
ansible-playbook -i inventory.yml configure.yml --syntax-check
ansible-playbook -i inventory.yml configure.yml --list-hosts
ansible-playbook -i inventory.yml configure.yml
ansible-playbook -i inventory.yml configure.yml第一次每个目标创建目录、写配置并执行 handler,recap 为 changed=3;第二次文件内容相同,handler 不再执行,每个目标 changed=0。两轮都应 failed=0、unreachable=0。检查实际内容:
cat runtime/lab_a/app.conf runtime/lab_a/effective.conf两行都应是 workers=4。修改变量再执行:
ansible-playbook -i inventory.yml configure.yml -e '{"workers":8}' --diff模板和生效文件各改变一次,两个目标都成为 workers=8。notify 只在对应任务 changed 时排队,flush_handlers 让已通知的 handler 在读取生效状态之前运行。同一个 handler 被多次通知通常只执行一次,执行次序取决于定义顺序。Handler 文档列出了具体规则。
失败如何阻止后续机器
先让两个目标保持 workers=8,再运行反例:
ansible-playbook -i inventory.yml configure.yml \
-e '{"workers":6,"fail_host":"lab_a"}'
echo "$?"
cat runtime/lab_a/effective.conf runtime/lab_b/effective.conf首台已经应用 workers=6,但最终断言故意失败。serial=1 限定当前只有一台,any_errors_fatal 阻止后续批次,因此第二台仍为 workers=8。命令应非零退出。这说明停止后续机器与恢复已变更机器是两项工作;Ansible 不会自动将第一台恢复。
取消反例并重新执行 workers=8,两个目标恢复一致。再传 workers=0,应在写入前被 assert 拒绝,文件不变。正式项目同样要在修改前校验版本格式、目标路径与必要配置。
check mode 可以预览部分变更:
ansible-playbook -i inventory.yml configure.yml \
-e '{"workers":10}' --check --diff它不是完整模拟器。后面的 slurp 仍读取真实文件,可能因为配置没有真正应用而断言失败;有些模块被跳过,有些支持预测 changed。先读 check/diff 行为,再判断哪些结果可用,不能把预演成功当成真实部署验收。
改为 SSH 管理真实 Linux 节点
目标机管理员创建 deploy 用户并配置 SSH 公钥;常规模块所需 Python 由可信管理入口预装。先从控制端用同一账号人工 SSH 登录,核对经独立渠道获得的服务器指纹,不将 ssh-keyscan 的输出直接视为可信认证。
在独立 ssh-inventory.yml 中填写两台获授权测试主机的名称或地址:
all:
children:
app:
hosts:
app_a:
ansible_host: app-a.example.com
app_b:
ansible_host: app-b.example.com
vars:
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3私钥由 SSH agent 管理或使用受限的专用文件,不写进 inventory。使用标准 SSH 配置处理端口、跳板和 known_hosts,连接安全见 SSH。然后执行:
ansible-inventory -i ssh-inventory.yml --graph
ansible -i ssh-inventory.yml app --list-hosts
ansible -i ssh-inventory.yml app -m ansible.builtin.ping
ansible -i ssh-inventory.yml app -m ansible.builtin.command -a 'id'ping 成功返回 pong,验证的是连接、认证与 Python 模块执行,不是 ICMP。id 应显示 deploy;尚未使用 become 时不应是 root。主机别名、连接变量与组关系见 inventory 官方说明。
将配置和版本逐批应用到服务
变量、模板和角色各自保存什么
inventory 按环境拆开,group_vars 保存同组配置,host_vars 只保存真实单机差异。角色 defaults 提供可覆盖默认值;发布时的 extra vars 优先级很高,要限制可传字段,避免调用者随意覆盖目标路径、账号或下载地址。
automation/
├── inventories/staging/hosts.yml
├── inventories/staging/group_vars/app.yml
├── inventories/production/hosts.yml
├── roles/demo_app/
│ ├── defaults/main.yml
│ ├── tasks/main.yml
│ ├── handlers/main.yml
│ └── templates/
└── playbooks/deploy.ymlrole 将相关任务、默认变量、模板和 handler 放在一起。collection 则是发布和依赖分发单位,包含多个模块、插件或角色。使用 FQCN,例如 ansible.builtin.copy,可以明确模块来源;第三方 collection 还应固定版本并审查来源,不能因为是 YAML 就当成普通配置文件。
有校验器的配置应先验证临时文件再替换。例如独立完整 Nginx 配置可以在 template 中设置:
- name: Validate full nginx configuration before replacement
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: '/usr/sbin/nginx -t -c %s'
notify: Reload nginx%s 由模块替换为临时文件路径,validate 不经 shell 执行。包含相对 include 的配置还要考虑临时路径;仅含 server 片段的文件不能当完整 nginx.conf 直接校验。具体临时文件和校验规则见 template 模块。
一份由 systemd 托管的 Java 服务
完整示例中的 service.yml 管理两台已安装 Java25、使用 systemd 的专用 Linux 测试机。普通 Docker 容器通常没有 systemd PID1,不能直接拿配置实验容器验证服务托管。
程序使用 发布脚本实验的 Java 源码与构建入口,先构建 v1、v2 与 bad 三个 JAR。包内 version.txt 决定 /health 返回的版本,bad 返回 503。服务模板为:
[Unit]
Description=Demo Java release
After=network.target
[Service]
Type=simple
User=demoapp
WorkingDirectory=/opt/demo/current
ExecStart={{ java_bin }} --add-modules jdk.httpserver -jar /opt/demo/current/app.jar
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
[Install]
WantedBy=multi-user.target模板固定服务账号与路径,应用没有写程序目录的权限。java_bin 默认为 /usr/bin/java,目标机必须确认它是 Java25。程序不写持久文件;需要写上传、缓存或日志文件的服务应额外设置明确的数据目录和 systemd 写权限,不能直接取消全部保护。
service.yml 的执行顺序为:
校验版本与摘要格式
→ 创建 demoapp 与版本目录
→ 复制 JAR
→ 计算目标文件 SHA-256,和批准摘要比较
→ 写 unit
→ current 指向选定版本
→ 有变化则重启,无变化则确保启动
→ 本机 HTTP 状态与包内版本同时验证service.yml 的完整任务如下:
- name: Install one selected Java release
hosts: app
gather_facts: false
become: true
serial: 1
any_errors_fatal: true
vars:
java_bin: /usr/bin/java
pre_tasks:
- name: Validate release parameters
ansible.builtin.assert:
that:
- app_version is match('^[a-zA-Z0-9][a-zA-Z0-9_-]{0,39}$')
- jar_sha256 is match('^[a-f0-9]{64}$')
- name: Create runtime account
ansible.builtin.user:
name: demoapp
system: true
shell: /usr/sbin/nologin
create_home: false
- name: Create release directory
ansible.builtin.file:
path: "/opt/demo/releases/{{ app_version }}"
state: directory
owner: root
group: root
mode: '0755'
tasks:
- name: Check existing version before any overwrite
ansible.builtin.stat:
path: "/opt/demo/releases/{{ app_version }}/app.jar"
checksum_algorithm: sha256
register: existing
- name: Reject reuse of a version with different bytes
ansible.builtin.assert:
that: not existing.stat.exists or existing.stat.checksum == jar_sha256
- name: Check source bytes before transfer
ansible.builtin.stat:
path: "{{ jar_src }}"
checksum_algorithm: sha256
delegate_to: localhost
become: false
register: source_jar
- name: Require approved source
ansible.builtin.assert:
that:
- source_jar.stat.exists
- source_jar.stat.checksum == jar_sha256
- name: Copy selected jar from control node
ansible.builtin.copy:
src: "{{ jar_src }}"
dest: "/opt/demo/releases/{{ app_version }}/app.jar"
owner: root
group: root
mode: '0644'
register: jar_copy
- name: Hash installed jar before switch
ansible.builtin.stat:
path: "/opt/demo/releases/{{ app_version }}/app.jar"
checksum_algorithm: sha256
register: installed
- name: Require approved checksum
ansible.builtin.assert:
that: installed.stat.checksum == jar_sha256
- name: Install systemd unit
ansible.builtin.template:
src: demo.service.j2
dest: /etc/systemd/system/demo-app.service
owner: root
group: root
mode: '0644'
register: unit_copy
- name: Switch selected release
ansible.builtin.file:
src: "/opt/demo/releases/{{ app_version }}"
dest: /opt/demo/current
state: link
register: release_link
- name: Restart only when inputs changed
ansible.builtin.systemd_service:
name: demo-app
daemon_reload: "{{ unit_copy.changed }}"
enabled: true
state: "{{ 'restarted' if (jar_copy.changed or unit_copy.changed or release_link.changed) else 'started' }}"
- name: Verify running package version
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
return_content: true
use_proxy: false
timeout: 3
register: health
retries: 10
delay: 3
until: health.status == 200 and health.content | trim == app_version复制、校验、切换分别是不同任务,摘要不匹配必须在切换前停止。模板、包或链接发生变化才重启;仅 state: started 不会让运行进程加载新的 JAR。daemon_reload 则只让 systemd 重新读取 unit,两者作用见 systemd_service。
发布输入保存为受控文件 release-v1.yml,真实摘要由准备机的 sha256sum 得到,不能填写演示散列:
app_version: v1
jar_src: /srv/release-input/v1.jar
jar_sha256: REPLACE_WITH_APPROVED_64_HEX_SHA256
java_bin: /usr/bin/java将 jar_src 改为控制端真实文件路径并填入摘要后,先确认目标,再仅发布一台:
ansible-playbook -i ssh-inventory.yml service.yml \
-e @release-v1.yml --list-hosts
ansible-playbook -i ssh-inventory.yml service.yml \
-e @release-v1.yml --limit app_a --ask-become-pass安装账号和 unit 需要 root,应用运行仍是 demoapp。必须理解这次 become 授权的范围:Ansible 往往通过临时 Python 模块提权,不能靠 sudoers 只放行 systemctl 就宣称所有模块都被细粒度限制。对不可信任务,不授予任意 become 权限;授权模型见 become 说明。
最后一个任务在目标机发起回环请求:
- name: Verify running package version
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
return_content: true
use_proxy: false
timeout: 3
register: health
retries: 10
delay: 3
until: health.status == 200 and health.content | trim == app_version最多等待有限次数;网络请求不得被目标机代理环境接管。状态成功且响应正文等于 v1,才说明期望包已在该节点响应。通过入口访问、数据库依赖和业务操作还需分别验证。uri 文档说明请求位置与返回值。
串行部署、恢复与继续执行
service.yml 使用 serial=1 和 any_errors_fatal=true。forks 是控制端的并行 worker 上限,serial 是一批主机数量;两者不是同一个开关。默认 linear 策略在当前批次内按任务推进,首台服务验证失败后不应继续下一批。百分比失败阈值按当前批次计算且有严格超过的规则,使用前查 执行策略。
真实滚动上线还需要在每台变更前摘除入口流量,等待在途请求结束,部署成功后再恢复流量。serial 不会操作负载均衡器,也不保证剩余容量足够。如果两台实例中停掉一台就无法承担负载,先补容量或选择维护窗口。
在测试机上部署 bad,预期健康检查失败、任务非零退出、后续机器保持旧版。保持失败节点摘流,使用原先保存的 v1 参数恢复这台:
ansible-playbook -i ssh-inventory.yml service.yml \
-e @release-v1.yml --limit app_a --ask-become-pass
ansible -i ssh-inventory.yml app_a -m ansible.builtin.uri \
-a 'url=http://127.0.0.1:8080/health status_code=200 return_content=true use_proxy=false'确认返回 v1,再从入口检查并恢复流量。示例不自动摘流,也不自动回滚,因此不应直接用于无人值守生产发布。需要自动恢复时可把切换和验证置于 block,rescue 执行已验证的回退任务;恢复成功后仍用 fail 明确终止本次发布,避免 rescue 将错误转为已处理后继续扩散。
连接 unreachable、语法错误等并不都会进入 rescue。控制端中断或目标断网后,需要另行查询实际链接、进程和响应。具体失败语义见 错误处理。重试前先判断上次做到了哪里,不使用忽略错误来凑齐“全绿”。
正式发布以新版本号创建新目录,禁止覆盖仍在运行的同名版本。需要允许多个操作者同时触发时,由发布入口和目标锁串行化;保存 previous 版本与摘要,再开始切换,不能故障后根据文件时间猜测回退目标。
安全、离线准备与故障定位
连接失败要分层确认
UNREACHABLE 先看错误类别:名称解析、超时、主机密钥、认证还是远程解释器。以同一账号在控制端检查:
ssh -v -o BatchMode=yes deploy@app-a.example.com 'id; python3 --version'
ansible -i ssh-inventory.yml app_a -m ansible.builtin.ping -vv
ansible-config dump --only-changedSSH 都无法登录时先修连接或认证;SSH 成功、模块启动失败则核对 Python 路径和依赖。只有经独立渠道确认服务器更换了主机密钥,才更新对应 known_hosts 项,不关闭全局主机校验。详细输出可能含路径和连接信息,分享前脱敏。
become 报错时检查是否需要密码、sudo 策略是否允许、连接用户是否正确。以最小获授权范围修正后,先在单台用只读 id 验证提权,避免直接重跑整份发布。
文件正确但服务仍旧
检查三个对象:配置或 JAR 文件、systemd 定义、实际进程与响应。
sudo systemctl cat demo-app
sudo systemctl status demo-app --no-pager
sudo journalctl -u demo-app -n 80 --no-pager
readlink -f /opt/demo/current
curl -q --noproxy '*' --fail-with-body http://127.0.0.1:8080/healthhandler 没执行,可能是任务没有 changed,也可能后续失败导致它未运行。不要无条件 force_handlers 重启一套只写了一半的配置;先确认完整配置已校验,再显式应用并复验。服务启动失败则按日志修复 Java 路径、权限或端口占用,保持节点摘流。单独的 changed=0 不能回答进程加载了哪个版本。
Secrets 与日志
Vault 加密变量文件的存储内容,运行时仍需解密。通过提示或受控凭据系统提供密码:
mkdir -p group_vars/app
ansible-vault create --vault-id staging@prompt group_vars/app/vault.yml
ansible-playbook -i ssh-inventory.yml service.yml \
-e @release-v1.yml --vault-id staging@prompt --ask-become-pass这里在 ssh-inventory.yml 和 service.yml 所在目录执行,group_vars/app/vault.yml 对应 inventory 中的 app 组。若以后把 inventory 移入 inventories/staging/,应同时迁移它的 group_vars,或通过显式 vars_files 加载;仅创建一个同名目录不会使当前 inventory 自动读取其中变量。
涉及秘密的任务设置 no_log: true,秘密模板设置 diff: false,不把密码放命令行、Git 或普通构建参数。Vault 不保护解密后的文件、进程和外部系统;它的职责见 Vault 文档。
中国大陆与离线依赖
在联网准备机用相同平台和 Python 小版本准备 wheelhouse。保存当前经过验证的精确依赖列表,再下载:
python -m pip freeze > requirements.lock
python -m pip download -r requirements.lock -d wheelhouse
sha256sum wheelhouse/* > wheelhouse.sha256将文件送达离线控制端后,在新虚拟环境校验并安装:
sha256sum -c wheelhouse.sha256
python -m pip install --no-index --find-links=wheelhouse -r requirements.lock
ansible --version有原生扩展的 wheel 与架构、Python ABI 和系统兼容性相关,不能将任意电脑下载的包直接交付。第三方 collection、目标机系统包、Java、制品与企业 CA 也要准备;控制端有网络不代表目标端能下载软件。优先接入企业 Python/系统包镜像和制品库,在无公网条件下跑一轮单机配置与服务检查,再推广到目标组。
权威资料与规范地址
安装与目标管理
- 安装:https://docs.ansible.com/projects/ansible/latest/installation_guide/intro_installation.html
- 版本支持: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
- Become:https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_privilege_escalation.html
- Vault:https://docs.ansible.com/projects/ansible/latest/vault_guide/index.html
模块与执行语义
- Template:https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/template_module.html
- systemd_service:https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/systemd_service_module.html
- URI:https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/uri_module.html
- Handlers:https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_handlers.html
- Check / diff:https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_checkmode.html
- 执行策略:https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_strategies.html
- 错误处理:https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_error_handling.html
