上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2 天前 阅读量:11
韩国服务器如何用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 入口验证证书、响应和页面内容。只有旧版本可验证、新版本有独立发布目录、切换后能核对版本、失败后能再次验证旧版本同时成立,链接回滚才算在这次发布中得到验证。

目录结构
全文