韩国服务器如何用Ansible实现网站环境重复部署与失败回滚

同一份 Ansible Playbook 再运行一次没有报错,并不等于网站环境可以重复部署:如果它直接覆盖正在提供服务的目录,第二次运行仍可能改掉线上文件;如果健康检查只确认 Nginx 进程存在,也可能把无法访问的版本当作成功。在韩国服务器上排查这类问题,应先划清两件事:配置管理负责让网站运行环境保持一致,发布与回滚负责切换网站版本。以下以 Debian/Ubuntu 上的 Nginx 静态网站为例,演示一套可执行的做法;已有应用的启动和健康检查方式需要按实际程序替换。
排查顺序应从低风险检查开始:先确认 Ansible 能连接目标机、具有所需权限,再检查 Nginx 配置和当前网站是否健康;随后核对待发布文件及版本标识,最后才切换版本并验证。如果连接失败,不应改动网站;如果原版本已经不健康,应先处理现有故障,不能指望一次发布证明回滚有效;如果切换后的检查失败,则恢复切换前的链接,并再次检查旧版本。
重复部署与回滚的边界
示例采用三个路径:
/srv/www/example/releases/版本号/:每个版本独立存放,发布后不再原地修改。/srv/www/example/current:Nginx 读取的网站目录,它是指向某个版本的符号链接。/srv/www/example/releases/.版本号.tar.gz:上传到服务器的发布包,供校验和留存。
重复部署同一版本时,Playbook 不重新解压或覆盖该版本,而是检查已存文件是否与发布时记录的清单一致,再检查网站响应。要更新内容,就生成新的版本号。这样,“重跑”才不会暗中改变已上线的版本。
这里的自动回滚范围是 current 链接。安装软件包、修改 Nginx 配置、数据库结构或外部依赖,不会因为链接恢复而自动撤销。首次发布没有旧版本可切回;Ansible 因断连、进程被终止而无法继续执行时,也不能保证自动回滚,需要人工核对现场。
先把环境配置与网站发布分开
控制端需要能运行 Ansible,并通过 SSH 连接韩国服务器;目标机需要是使用 apt、systemd 和 GNU mv 的 Debian/Ubuntu 环境。下面的任务会安装 Nginx、curl、cron,写入站点配置并启动服务。执行前应备份现有 Nginx 配置,确认 www.example.com 不与已有 server_name 冲突,并确认端口 80 的处理方式符合当前部署;不要直接套用到已有复杂站点。
项目可以按以下路径组织:
inventory.ini
group_vars/web.yml
templates/site.conf.j2
init.yml
deploy.yml
artifacts/
清单和变量示例:
# inventory.ini
[web]
kr-web-01 ansible_host=你的服务器地址 ansible_user=你的运维用户
# group_vars/web.yml
site_domain: www.example.com
site_root: /srv/www/example
站点配置始终读取 current,不把具体版本号写进 Nginx 配置:
# templates/site.conf.j2
server {
listen 80;
server_name {{ site_domain }};
root {{ site_root }}/current;
index index.html;
location = /health.txt {
add_header Cache-Control "no-store";
}
location = /SHA256SUMS {
return 404;
}
location ~ /\. {
deny all;
}
location / {
try_files $uri $uri/ =404;
}
}
初始化 Playbook 如下。它会备份被模板覆盖的目标文件,但备份文件并不等于自动恢复机制;若 nginx -t 失败,应先根据报错和备份修复配置,不要继续发布。
# init.yml
- name: Prepare website environment
hosts: web
become: true
tasks:
- name: Install required packages
ansible.builtin.apt:
name:
- nginx
- curl
- cron
state: present
- name: Create website directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- "{{ site_root }}"
- "{{ site_root }}/releases"
- name: Install site configuration
ansible.builtin.template:
src: templates/site.conf.j2
dest: "/etc/nginx/sites-available/{{ site_domain }}.conf"
owner: root
group: root
mode: "0644"
backup: true
- name: Enable site configuration
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ site_domain }}.conf"
dest: "/etc/nginx/sites-enabled/{{ site_domain }}.conf"
state: link
- name: Check Nginx configuration
ansible.builtin.command: nginx -t
changed_when: false
- name: Start or reload Nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
enabled: true
- name: Start cron
ansible.builtin.systemd:
name: cron
state: started
enabled: true
在没有发布版本时运行 init.yml,站点可能返回 404,这是尚无 current 链接的预期结果,不代表初始化已经具备回滚能力。若服务器已有站点,先用 nginx -T 核对生效的虚拟主机,再决定是否执行初始化。配置变更本身应单独验证和恢复,不要把它混进一次需要快速回滚的网站内容发布。
制作不可复用的发布包
以下示例假定控制端的 build/ 内已有待上线的 index.html。健康文件写入版本号,既用于检查 HTTP 是否通,也用于判断返回的是否是刚切换的版本:
release_id=r2026-09-website-01
mkdir -p artifacts
printf '%s\n' "$release_id" > build/health.txt
(
cd build &&
sha256sum index.html health.txt > SHA256SUMS &&
tar -czf "../artifacts/${release_id}.tar.gz" \
index.html health.txt SHA256SUMS
)
sha256sum "artifacts/${release_id}.tar.gz" |
awk '{print $1}' > "artifacts/${release_id}.tar.gz.sha256"
这些命令在控制端执行,会写入 build/health.txt 和 build/SHA256SUMS,并生成发布包;运行前应确认 build/ 是本次构建目录,避免覆盖需要保留的本地文件。只应部署自己构建、检查过内容的压缩包,不要把来源不明的压缩包交给目标机解压。同一版本号及其发布包应保留原样;修复内容时另建版本。
发布时校验、切换,并在失败后恢复链接
下面的 deploy.yml 先检查控制端压缩包的 SHA-256,再检查服务器上是否已有该版本。已有版本必须通过清单校验;新版本先上传、校验压缩包并解压。切换前会检查旧版本的健康文件;切换后通过本机 HTTP 请求核对新版本号。
# deploy.yml
- name: Deploy website release
hosts: web
become: true
serial: 1
vars:
archive_path: "{{ playbook_dir }}/artifacts/{{ release_id }}.tar.gz"
archive_sha: >-
{{ lookup('ansible.builtin.file', archive_path + '.sha256') | trim }}
release_path: "{{ site_root }}/releases/{{ release_id }}"
uploaded_path: "{{ site_root }}/releases/.{{ release_id }}.tar.gz"
next_link: "{{ site_root }}/.next-{{ release_id }}"
pre_tasks:
- name: Validate release identifier
ansible.builtin.assert:
that:
- release_id is match('^[A-Za-z0-9][A-Za-z0-9._-]*$')
- archive_sha is match('^[0-9a-fA-F]{64}$')
- name: Calculate local archive checksum
ansible.builtin.command:
argv:
- sha256sum
- "{{ archive_path }}"
delegate_to: localhost
become: false
register: local_hash
changed_when: false
- name: Compare local archive checksum
ansible.builtin.assert:
that:
- local_hash.stdout.split()[0] == archive_sha
tasks:
- name: Inspect current link
ansible.builtin.stat:
path: "{{ site_root }}/current"
follow: false
register: current
- name: Reject an unexpected current path
ansible.builtin.assert:
that:
- >-
not current.stat.exists or
(current.stat.islnk | default(false) and
(current.stat.lnk_source | default('')).startswith(
site_root + '/releases/'))
- name: Check the existing version before switching
when: current.stat.exists
block:
- name: Request the existing health file
ansible.builtin.uri:
url: http://127.0.0.1/health.txt
headers:
Host: "{{ site_domain }}"
return_content: true
status_code: 200
register: old_health
- name: Verify the existing version
ansible.builtin.assert:
that:
- >-
old_health.content | trim ==
current.stat.lnk_source | basename
- name: Inspect target release
ansible.builtin.stat:
path: "{{ release_path }}"
register: existing_release
- name: Upload and unpack a new release
when: not existing_release.stat.exists
block:
- name: Upload archive
ansible.builtin.copy:
src: "{{ archive_path }}"
dest: "{{ uploaded_path }}"
owner: root
group: root
mode: "0644"
- name: Calculate uploaded archive checksum
ansible.builtin.stat:
path: "{{ uploaded_path }}"
checksum_algorithm: sha256
register: uploaded
- name: Verify uploaded archive
ansible.builtin.assert:
that:
- uploaded.stat.checksum == archive_sha
- name: Create release directory
ansible.builtin.file:
path: "{{ release_path }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Unpack release
ansible.builtin.unarchive:
src: "{{ uploaded_path }}"
dest: "{{ release_path }}"
remote_src: true
- name: Record archive checksum
ansible.builtin.copy:
content: "{{ archive_sha }}\n"
dest: "{{ release_path }}/.artifact.sha256"
owner: root
group: root
mode: "0644"
- name: Read recorded archive checksum
ansible.builtin.slurp:
src: "{{ release_path }}/.artifact.sha256"
register: recorded_hash
- name: Verify release identity
ansible.builtin.assert:
that:
- recorded_hash.content | b64decode | trim == archive_sha
- name: Verify released files
ansible.builtin.command:
argv:
- sha256sum
- -c
- SHA256SUMS
chdir: "{{ release_path }}"
changed_when: false
- name: Decide whether switching is required
ansible.builtin.set_fact:
switch_needed: >-
{{ not current.stat.exists or
(current.stat.lnk_source | default('')) != release_path }}
- name: Switch and test the website
block:
- name: Prepare the next link
ansible.builtin.file:
src: "{{ release_path }}"
dest: "{{ next_link }}"
state: link
force: true
when: switch_needed
- name: Replace current link
ansible.builtin.command:
argv:
- mv
- -Tf
- --
- "{{ next_link }}"
- "{{ site_root }}/current"
when: switch_needed
- name: Request the new health file
ansible.builtin.uri:
url: http://127.0.0.1/health.txt
headers:
Host: "{{ site_domain }}"
return_content: true
status_code: 200
register: new_health
- name: Verify the served version
ansible.builtin.assert:
that:
- new_health.content | trim == release_id
rescue:
- name: Prepare rollback link
ansible.builtin.file:
src: "{{ current.stat.lnk_source }}"
dest: "{{ next_link }}"
state: link
force: true
when: current.stat.exists
- name: Restore previous link
ansible.builtin.command:
argv:
- mv
- -Tf
- --
- "{{ next_link }}"
- "{{ site_root }}/current"
when: current.stat.exists
- name: Remove failed first-deployment link
ansible.builtin.file:
path: "{{ site_root }}/current"
state: absent
when: not current.stat.exists
- name: Verify the restored website
ansible.builtin.uri:
url: http://127.0.0.1/health.txt
headers:
Host: "{{ site_domain }}"
return_content: true
status_code: 200
register: restored_health
when: current.stat.exists
- name: Verify the restored version
ansible.builtin.assert:
that:
- >-
restored_health.content | trim ==
current.stat.lnk_source | basename
when: current.stat.exists
- name: Report failed deployment
ansible.builtin.fail:
msg: "发布失败;已执行链接恢复或首次发布清理,请检查前面的任务结果。"
示例中的 mv -Tf 在同一文件系统内替换链接,避免让 Nginx 长时间读到一个不存在的 current;这不意味着业务请求、缓存或外部依赖也具有事务性。rescue 只有在 Ansible 收到任务失败时才会执行。若解压中断后留下不完整的版本目录,下一次运行会因缺少校验记录或文件校验失败而停止:先核对日志和目录内容,再决定是否备份并移走该目录,不要让 Playbook 自动删除不明现场。
执行顺序及控制端留证示例:
ansible -i inventory.ini web -m ping
ansible-playbook -i inventory.ini init.yml
mkdir -p logs
set -o pipefail
ansible-playbook -i inventory.ini deploy.yml \
-e release_id=r2026-09-website-01 |
tee "logs/r2026-09-website-01.log"
init.yml 会改动软件包和站点配置,应先在维护窗口核对影响;deploy.yml 会上传文件并切换线上链接,应在已确认旧版本健康、发布包可追溯后运行。set -o pipefail 适用于 Bash,可避免 tee 成功掩盖 Playbook 失败。日志应限制访问权限,且不要把密码、令牌放进命令参数或发布包。
按结果处理失败,而不是反复重跑
| 检查点 | 结果含义 | 下一步 |
|---|---|---|
ansible ... -m ping 失败 | SSH、账号、权限或连接尚未满足前提 | 先修复连接;不要尝试切换链接 |
nginx -t 失败 | 当前磁盘上的 Nginx 配置不合法 | 查看报错,核对变更和备份,修复后重新运行配置检查 |
| 旧版本健康检查失败 | 发布前就存在站点故障,无法证明旧版本可作为回滚目标 | 暂停发布,先核对 current、Nginx 日志和实际响应 |
| 本地或上传包 SHA-256 不一致 | 构建产物、记录值或传输后的文件不一致 | 停止使用该版本号,核对原始产物并重新制作发布包 |
| 已有版本的清单校验失败 | 版本目录不完整或被原地修改 | 保留现场排查;不要覆盖该目录以“修好”同一版本 |
| 切换后返回 404、非预期内容或请求失败 | 新版本未被正确提供,或站点匹配、文件内容有问题 | 检查 rescue 的恢复结果,再查链接目标、站点配置与访问日志 |
每次失败后都应分清“发布失败”和“恢复失败”。前者可以通过恢复到健康旧版本控制影响;后者需要立即人工介入。尤其是首次部署,本来就没有旧版本,清理失败链接只能避免误把失败版本当成成功版本,不能让网站凭空恢复服务。
定时检查、审计与最终验证
定时任务适合发现故障,不宜在只知道一次 HTTP 请求失败时自动切换版本:失败可能来自 Nginx 配置、磁盘文件、监听状态等原因,盲目回滚会掩盖问题。若目标机已通过 init.yml 安装并启动 cron,可用 Ansible 管理一个只记录异常的检查任务:
- name: Log local health request failures
ansible.builtin.cron:
name: example website health check
minute: "*/5"
user: root
job: >-
curl -fsS --max-time 5
-H 'Host: {{ site_domain }}'
http://127.0.0.1/health.txt
>/dev/null ||
logger -t site-health 'health request failed'
将该任务加入环境配置 Playbook 后,按现场告警要求调整频率,并用系统日志确认它确实在执行。它只检查请求是否成功;版本号核对仍由发布 Playbook 完成。发布审计至少保留执行时间、目标主机、版本号、发布包校验值、Ansible 输出以及发布后检查结果,便于确认一次“成功”究竟切到了哪个版本。
修复或回滚后,在服务器上检查链接和 Nginx 配置,再从实际用户访问入口检查页面。下面前两条适用于示例中的 Debian/Ubuntu 目标机;最后一条应在能访问网站的控制端执行,并将域名换成实际域名:
readlink -f /srv/www/example/current
nginx -t
curl -fsS -H 'Host: www.example.com' http://127.0.0.1/health.txt
curl -fsS http://www.example.com/health.txt
本机检查正确而外部访问仍失败,说明不能只凭服务器内的健康请求宣布恢复,应继续核对实际对外访问路径;若网站实际使用 HTTPS,还须通过其真实 HTTPS 入口验证证书、响应和页面内容。只有旧版本可验证、新版本有独立发布目录、切换后能核对版本、失败后能再次验证旧版本同时成立,链接回滚才算在这次发布中得到验证。