香港服务器如何用Ansible实现重复部署与失败回滚

同一份发布任务,第一次成功、第二次却改坏配置,或者服务重启正常但业务仍然报错,这类问题通常不是香港服务器本身不稳定,而是部署过程没有明确区分“准备版本、切换版本、验证业务”三个阶段。先判断故障发生在哪个阶段,再决定重试还是回滚,才能避免越修越乱。
建议按“连接与权限 → 发布物与配置 → 当前版本指针 → 服务状态 → 业务健康”的顺序排查。实现上,用 Ansible 管理确定的目标状态,把代码和配置放入独立版本目录;切换后执行健康检查,失败则恢复旧版本并再次验证。可重复部署依靠幂等任务与不可变发布物,失败回滚依靠可恢复的旧状态,而不是简单重跑 Playbook。
高频现象:任务执行成功,不代表部署成功
先区分下面几种现象,通常就能缩小排查范围。
| 现象 | 优先判断 | 核验方法 |
|---|---|---|
第二次执行仍大量显示 changed | 使用了无条件执行的命令,或模板包含动态时间等内容 | 查看具体任务,比较配置内容与文件摘要 |
| 服务显示运行中,但请求失败 | 进程启动不等于应用就绪 | 检查本机健康接口、应用日志和依赖状态 |
| 回滚代码后仍然异常 | 配置、数据库或外部状态没有同步恢复 | 比较旧版本配置及此次变更记录 |
| 单台成功,多台出现版本混杂 | 并发部署、批次中断或部分主机不可达 | 分别读取每台服务器的版本指针和健康接口 |
| 定时任务触发后发布状态反复变化 | 两次部署重叠执行 | 核对调度日志、运行进程和发布锁 |
Ansible 的 ok 只表示任务无需修改,changed 表示模块报告发生了修改;两者都不能代替业务验收。尤其不要为了让输出“好看”,给真实修改状态的命令随意加上 changed_when: false。
按优先级完成低风险核验
以下示例适用于使用 systemd 的 Linux 香港服务器,控制端安装 Ansible,目标端具备相应 Python 环境。应用服务名以 webapp 为例,需替换为已存在的实际服务。
1. 先确认主机范围、连接和权限
ansible-playbook -i inventory.ini deploy.yml --syntax-check
ansible-playbook -i inventory.ini deploy.yml --list-hosts
ansible -i inventory.ini hk_app -m ansible.builtin.ping
ansible -i inventory.ini hk_app -b -m ansible.builtin.command -a "id"
ping 模块成功说明 Ansible 通信与模块执行基本正常,并不是 ICMP 测试。如果报 UNREACHABLE,先排查 SSH、认证和网络可达性;连接正常但提权失败,应修复 become 授权,而不是扩大目录权限。
确认主机清单确实指向预期服务器,不要通过关闭 SSH 主机密钥校验来绕过身份异常。
2. 再确认版本、磁盘与运行状态
在目标服务器上执行这些只读检查:
readlink -f /srv/webapp/current
df -h /srv/webapp
df -i /srv/webapp
systemctl status webapp --no-pager
journalctl -u webapp -n 100 --no-pager
版本路径不存在,说明发布目录或链接有问题;空间或 inode 耗尽,可能导致解包、日志写入失败;服务启动后立即退出,应优先核验配置和运行权限。不要在原因未明时连续重启。
3. 最后核验健康接口
curl --fail --silent --show-error \
--max-time 5 http://127.0.0.1:8080/health
地址和超时为示例参数,应按应用调整。健康接口最好同时返回就绪状态和发布版本,例如:
{"ready": true, "release": "release_20260912_01"}
仅返回 HTTP 200 无法排除“旧进程仍在提供服务”的情况。本机通过而用户入口失败时,还要检查实际访问路径,不能继续把问题归因于发布包。
把代码、配置和运行数据分开管理
建议使用下面的目录约定:
/srv/webapp/
├── releases/
│ ├── release_20260911_01/
│ └── release_20260912_01/
├── shared/
└── current -> releases/release_20260911_01
releases 保存不可变的代码和版本配置,shared 保存上传文件等运行数据,current 指向当前版本。服务启动路径必须通过 current 读取应用及配置,不能仍然引用某个写死的版本目录。
准备发布物时,按以下顺序执行:
- 在控制端确定发布编号、代码版本和包摘要;同一个发布编号不能对应不同内容。
- 使用
ansible.builtin.file创建版本目录,用copy传包并核验摘要,再用unarchive解包。 - 用
template渲染该版本自己的配置;应用支持配置校验命令时,通过模板的validate参数或独立任务先行校验。 - 所有检查通过后才写入
.ready标记。重复部署时,应核对发布清单与摘要,再决定是否跳过准备阶段,不能只凭目录存在就认定准备完成。
只有代码和配置都能随版本指针一起恢复,目录切换才具备完整的应用回滚意义。 如果模板直接覆盖 /etc 下的共享配置,还需要单独备份、记录并恢复这些文件。
敏感配置使用 Ansible Vault 等方式保护,相关任务设置 no_log: true,必要时关闭 diff。普通配置可以审查差异,但不要把密码和令牌输出到发布日志。
用 block/rescue 实现切换失败后的恢复
下面的 deploy.yml 聚焦已经完成准备的版本切换,适用于已有健康旧版本的升级,不包含首次安装。运行前需要满足:
- 新旧目录都有可信的
.ready标记,发布内容已核验。 webapp已配置为读取current下的代码与配置。- 健康接口返回前述 JSON,
release与版本目录名一致。 - 使用 GNU
ln、mv,临时链接与current位于同一文件系统。 - 发布期间禁止其他任务同时修改
current。
此操作会修改版本链接并重启服务,可能造成短暂中断。执行前保留旧版本、配置备份,并确认旧版本仍兼容当前数据。
---
- name: 发布已准备的应用版本
hosts: hk_app
become: true
serial: 1
any_errors_fatal: true
vars:
app_root: /srv/webapp
app_service: webapp
health_url: http://127.0.0.1:8080/health
pre_tasks:
- name: 校验发布编号
ansible.builtin.assert:
that:
- release_id is defined
- (release_id | default('')) is match('^[A-Za-z0-9][A-Za-z0-9_-]*$')
- name: 读取原版本
ansible.builtin.command:
argv: [readlink, -f, "{{ app_root }}/current"]
register: old_release
changed_when: false
- name: 确认原版本位于发布目录
ansible.builtin.assert:
that:
- old_release.stdout.startswith(app_root + '/releases/')
- name: 检查新旧版本准备标记
ansible.builtin.stat:
path: "{{ item }}/.ready"
loop:
- "{{ old_release.stdout }}"
- "{{ app_root }}/releases/{{ release_id }}"
register: ready_checks
- name: 拒绝未准备完成的版本
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.isreg | default(false)
loop: "{{ ready_checks.results }}"
tasks:
- name: 切换并验证
block:
- name: 建立新版本临时链接
ansible.builtin.command:
argv:
- ln
- -sfnT
- "{{ app_root }}/releases/{{ release_id }}"
- "{{ app_root }}/.current-next"
when: old_release.stdout != app_root + '/releases/' + release_id
- name: 原子替换当前链接
ansible.builtin.command:
argv:
- mv
- -Tf
- "{{ app_root }}/.current-next"
- "{{ app_root }}/current"
when: old_release.stdout != app_root + '/releases/' + release_id
- name: 新版本切换后重启服务
ansible.builtin.systemd_service:
name: "{{ app_service }}"
state: restarted
when: old_release.stdout != app_root + '/releases/' + release_id
- name: 检查新版本就绪状态
ansible.builtin.uri:
url: "{{ health_url }}"
return_content: true
timeout: 3
register: new_health
until:
- new_health.status | default(0) == 200
- (new_health.json | default({})).get('ready', false) == true
- (new_health.json | default({})).get('release', '') == release_id
retries: 10
delay: 3
rescue:
- name: 建立原版本恢复链接
ansible.builtin.command:
argv:
- ln
- -sfnT
- "{{ old_release.stdout }}"
- "{{ app_root }}/.current-next"
- name: 恢复原版本指针
ansible.builtin.command:
argv:
- mv
- -Tf
- "{{ app_root }}/.current-next"
- "{{ app_root }}/current"
- name: 重启原版本服务
ansible.builtin.systemd_service:
name: "{{ app_service }}"
state: restarted
- name: 验证原版本恢复
ansible.builtin.uri:
url: "{{ health_url }}"
return_content: true
timeout: 3
register: old_health
until:
- old_health.status | default(0) == 200
- (old_health.json | default({})).get('ready', false) == true
- (old_health.json | default({})).get('release', '') == (old_release.stdout | basename)
retries: 10
delay: 3
- name: 回滚后仍将本次发布标记为失败
ansible.builtin.fail:
msg: "发布验证失败,原版本已恢复并通过健康检查。"
重试次数和超时只是示例,应覆盖应用正常启动所需时间,而不是无限等待。
这里用同目录的重命名替换链接,避免先删除 current 再创建链接产生的路径空窗;但链接替换原子性不等于服务无中断。serial: 1 限制逐台处理,最终的 fail 配合 any_errors_fatal 阻止失败后继续发布后续批次。已经成功升级的前序主机不会自动全部回滚,需要按发布记录决定是否统一恢复。
同一版本再次执行时,切换与重启任务会跳过,只重新验证健康。如果当前版本本来就是目标版本且检查失败,恢复分支只是重启原版本,不会自动寻找更早版本。
定时执行、审计与故障边界
发布前先用 --limit 选择一台主机验证,再扩大范围。手工执行和定时执行必须走同一个互斥入口,例如在单一控制端使用:
flock -n /var/lock/webapp-deploy.lock \
ansible-playbook -i inventory.ini deploy.yml \
-e release_id=release_20260912_01
运行账号必须有权创建锁文件;未获得锁时应报告本次未执行。该锁只对同一控制端有效,多控制端部署需要统一调度互斥。
定时任务可用 ansible.builtin.cron 管理,并固定任务名称,避免重复添加。调度脚本应使用绝对路径、固定工作目录和明确的发布编号,不要每次追踪含义变化的“最新版”。任务日志与交互终端不同,需单独设置保存位置、访问权限和保留策略。
每次发布至少记录:执行人或调度身份、目标主机、原版本、新版本、包摘要、配置版本、健康检查结果和回滚结果。配置仓库有提交记录,不等于服务器最终状态已经被审计。
还要明确三个例外:
- 目标主机不可达:
rescue不会自动处理不可达错误。SSH 中断后,应重新连接读取实际指针,确认切换是否发生,再决定恢复操作。 - 数据变更不兼容:链接回滚不能撤销数据库迁移或外部写入。涉及这类变更时,应先验证新旧程序兼容性,并单独准备数据备份与恢复方案。
- 回滚本身失败:旧版本健康检查不通过时,立即停止后续发布,保留现场日志,不要继续循环切换。
修复后别遗漏二次部署验证
先确认版本指针、健康接口版本、服务日志和实际用户入口一致,再用同一个发布编号执行第二次。预期应是发布物不变、配置无意外差异、服务不重复重启,健康检查仍然通过。
随后在测试环境主动触发一次新版本健康失败,确认旧版本能够恢复,而且发布任务最终仍报告失败。最容易遗漏的复核点是:旧版本不仅要保留在磁盘上,还必须能读取恢复后的配置、兼容当前数据,并真正对外提供服务。