逐台更新主机

AnsibleBeginner
立即练习

介绍

Courtyard Sessions 的网站已经在两个后端上正常运行。新版节目单已准备好,但同时更新两个节点可能让访客无站可用。你将用 Ansible 检查运行状态,再逐台发布新包。

你需要具备基本终端导航能力,并练习过小型 Ansible inventory、变量和 file/copy 任务。网关、SSH 访问和发布包均已准备好。本实验会在使用前解释执行位置、委派、批次、版本链接和完整 HTTP 内容检查。时长是作者估计,包含阅读和普通错误修正。

看清每个任务在哪里执行

在这一步,你将制作一个只读检查 playbook,显示每个后端正在运行的版本,以及实际参与流量分配的节点列表。

Courtyard Sessions 的访客网站已经可用。打开 Website Tab,阅读 **Good things, around the corner.**,然后点击 The program。页面会滚动到两个节目,首个是 Coffee in the courtyard。点击 Download the program (CSV) 并打开下载文件:表头为 time,session,place,后面是页面展示的两个节目。CSV 是用逗号分隔字段的纯文本表格。更新期间,这份真实的访客下载必须保持可用。

网关将短 HTTP 请求分配给两个后端。将某个后端移出活动流量池,会停止向它分配新的网关请求;将它加入后,它便重新参与轮换。这个准备好的小型网关不负责排空长连接,也不模拟完整的云端发布平台。两个后端都是通过 SSH 管理的容器,共享训练 VM 的内核。控制端是运行 Ansible 和网关的机器。

切换到 Controller Tab。起始目录为 /home/labex/project。pwd 打印当前目录,cat 打印文件内容:

pwd
cat inventory.yml
cat group_vars/all.yml

inventory 使用兼容 YAML 的结构化数据。website 组包含 web1 和 web2,分别保存实际的 ansible_host 地址和准备好的 SSH 连接信息。control 组包含使用本地连接的 controller。请使用项目 ansible.cfg 选定的 inventory.yml;镜像初始生成的 inventory.ini 不是本课程的 inventory。

组变量提供了 backend_port(8090)、仅供控制端使用的 pool_api、允许管理的 release_root、本地 package_root 和版本标识。baseline_release 为 courtyard-preview,approved_release 为 courtyard-weekend。它们是目录名称,不是 Ansible 或软件的版本号。HTTP 端口标识某个地址上监听的服务。/identity 返回 JSON,以结构化文本标识后端实际运行的版本。/pool 返回控制端当前的活动节点列表。

如果命令显示“No such file or directory”,用 ls 检查拼写,必要时通过 cd /home/labex/project 返回工作目录。读取命令可以安全重复执行。请保留所有提供的包文件、连接数据、原始版本文件和私密的组织者备注。

默认情况下,Ansible 通过 SSH 在每个选定的后端上执行任务。在后端上,127.0.0.1 指的是该后端自身。delegate_to: controller 则让这一个任务在控制端执行,而控制端自己的 127.0.0.1 上运行着流量池 API。委派不会改变 inventory_hostname:它仍然表示当前正在管理的后端。因此,控制端请求可以引用正确的后端。

创建 inspect.yml。cat > filename 会写入文件,并替换同名现有文件。<<'YAML' 开始一个 here-document:请一直粘贴到独占一行的结束标记 YAML。带引号的分隔符让粘贴的文本保持原样;Ansible 在运行 playbook 时处理表达式。缩进使用空格。{{ variable }} 插入 Ansible 变量。gather_facts: false 跳过无关机器事实的收集。

uri 模块发出真实 HTTP 请求;默认方法 GET 用于读取资源。register 将实际结果保存到指定名称下。对于 JSON 响应,.json 保存解析后的字段。debug 打印这些已保存的观察结果,不会创建版本或流量池状态。

cat > inspect.yml <<'YAML'
- name: Inspect the running website without changing it
  hosts: website
  gather_facts: false
  tasks:
    - name: Read this backend on its own loopback address
      ansible.builtin.uri:
        url: "http://127.0.0.1:{{ backend_port }}/identity"
      register: backend
    - name: Read the pool on the controller
      ansible.builtin.uri:
        url: "{{ pool_api }}/pool"
      delegate_to: controller
      register: pool
    - name: Show the actual release and active pool
      ansible.builtin.debug:
        msg:
          node: "{{ inventory_hostname }}"
          release: "{{ backend.json.release }}"
          active: "{{ pool.json.active }}"
YAML

运行 playbook。ansible-playbook 读取任务,并将其应用到 inventory 中的主机:

ansible-playbook inspect.yml

每个后端都应显示 release: courtyard-preview,而 active 应包含 web1 和 web2。输出格式和地址可能不同。注意,第一个 uri 在后端执行,而流量池读取会显示类似 web1 -> controller 的委派执行位置。这两个任务都是只读的。

如果 YAML 缩进有误,重新复制完整代码块来修正文件,然后重试。这个 playbook 不应复制文件或改变流量池。你现在有了一个实用的观察工具,发布后可以再次运行它。

逐台发布后端版本

在这一步,你将通过 SSH 交付获准的网站包,验证实际访客内容,并在处理下一台前让当前后端重新提供服务。

留在 Controller Tab 的 /home/labex/project 目录。列出发布包,再读取获准节目单。ls 用于列出目录条目:

ls packages
cat packages/courtyard-weekend/schedule.csv

获准节目单包含三个节目:Bread and stories、Acoustic afternoon 和 Print a postcard。仅首页正常还不够,访客需要与之对应的完整下载。courtyard-incomplete 包留待下一实验使用;这里选择 courtyard-weekend,现有的 release_id 变量已经如此设置。

playbook 使用 serial: 1,每个批次处理一台主机。在这个批次内,它将当前节点移出流量池、复制发布包、切换活动版本链接、检查两项业务资源,再将节点加回。之后才开始下一批。order: sorted 让示例从 web1 开始。any_errors_fatal: true 显式传播未处理的失败,以停止整个 play。在这里的一台主机批次中,这台主机失败就意味着整个批次失败。这个设置不会撤销已经完成的任务。下一实验会加入恢复机制。

流量池 API 接受发往 /pool 的 HTTP POST,请求体为 JSON:node 指定受管节点,enabled 为布尔值 false 时移除,为 true 时加入。body_format: json 正确序列化这个请求体。流量池任务在控制端执行,文件任务则在选定的 SSH 后端执行。

file 搭配 state: directory 创建目录;模式 '0755' 允许所有者写入,并允许所有人读取和进入目录。copy 把选定的控制端发布包发送到后端。src 末尾的 / 表示复制目录内容,并用 '0644' 模式生成可读文件。file 搭配 state: link 创建或更新符号链接:current 指向一个版本目录。准备好的 HTTP 服务每次请求都通过这个链接读取真实文件。切换链接即可改变活动网站,无需重启服务,原始版本目录则保持完整。

对于在控制端执行的 HTTP 探测,hostvars[inventory_hostname].ansible_host 显式取得受管后端的地址。委派时,不限定来源的连接变量可能指向被委派的主机。return_content: true 将实际响应正文保存为 .content。

assert 要求 that 表达式成立。lookup('file', ...) 在控制端读取预期包文件,~ 拼接路径字符串。rstrip=False 保留结尾换行,因此比较的是完整正文,而非去掉尾部空白的片段。加回节点前,完整候选首页和完整获准 CSV 都必须匹配。404 表示服务器已响应,但该资源缺失;连接错误表示无法到达服务。两者都不能视为健康版本。

写入完整发布流程。here-document 采用与检查工具相同的文件写入方法:

cat > rollout.yml <<'YAML'
- name: Release one website backend at a time
  hosts: website
  order: sorted
  gather_facts: false
  serial: 1
  any_errors_fatal: true
  tasks:
    - name: Drain this backend from the pool
      ansible.builtin.uri:
        url: "{{ pool_api }}/pool"
        method: POST
        body_format: json
        body:
          node: "{{ inventory_hostname }}"
          enabled: false
      delegate_to: controller
    - name: Create this release directory
      ansible.builtin.file:
        path: "{{ release_root }}/releases/{{ release_id }}"
        state: directory
        mode: '0755'
    - name: Copy the selected package over SSH
      ansible.builtin.copy:
        src: "{{ package_root }}/{{ release_id }}/"
        dest: "{{ release_root }}/releases/{{ release_id }}/"
        mode: '0644'
    - name: Activate the selected release
      ansible.builtin.file:
        src: "{{ release_root }}/releases/{{ release_id }}"
        dest: "{{ release_root }}/current"
        state: link
    - name: Read the real candidate homepage
      ansible.builtin.uri:
        url: "http://{{ hostvars[inventory_hostname].ansible_host }}:{{ backend_port }}/"
        return_content: true
      delegate_to: controller
      register: homepage
    - name: Require the complete candidate homepage
      ansible.builtin.assert:
        that: homepage.content == lookup('file', package_root ~ '/' ~ release_id ~ '/index.html', rstrip=False)
    - name: Read the real candidate download
      ansible.builtin.uri:
        url: "http://{{ hostvars[inventory_hostname].ansible_host }}:{{ backend_port }}/schedule.csv"
        return_content: true
      delegate_to: controller
      register: download
    - name: Require the complete approved program
      ansible.builtin.assert:
        that: download.content == lookup('file', package_root ~ '/' ~ approved_release ~ '/schedule.csv', rstrip=False)
    - name: Rejoin only after both business checks
      ansible.builtin.uri:
        url: "{{ pool_api }}/pool"
        method: POST
        body_format: json
        body:
          node: "{{ inventory_hostname }}"
          enabled: true
      delegate_to: controller
YAML

先检查 YAML 和任务结构,不执行发布。--syntax-check 验证语法,但不能证明业务结果正确:

ansible-playbook rollout.yml --syntax-check

如果检查报告拼写或缩进错误,重新复制完整文件并重试。随后执行真实发布:

ansible-playbook rollout.yml

web1 的任务应先完成,之后才处理 web2。两台主机最终都应为 failed=0、unreachable=0。移出和加回流量池的操作请求可能显示 ok;HTTP 是否成功及其实际效果,比汇总中的 changed 计数更重要。如果普通编辑错误中断运行,修正完整文件后,用同一个获准包重新运行。重复这个流程仍会执行移除、检查和加回操作,并非完全不做操作的部署。

用原先的工具检查运行状态:

ansible-playbook inspect.yml

两个节点现在都应报告 courtyard-weekend,且都在活动流量池中。切换到 Website Tab 并重新加载页面,标题应为 A little room for a good afternoon.。点击 The program,核对三个节目,再次下载 CSV 并打开新文件,确认三行数据完整。已有下载时,浏览器可能在新文件名中加上数字。Plan a visit 仍应通向花园入口的无障碍访问信息。

更新后的节目单包含全部三个获准节目,真实 CSV 下载链接就在列表旁。

更新后的 Courtyard Sessions 网站及其三个节目

这些结果证明,在准备好的短请求环境中,两个节点的网站更新正常完成;它们不能证明任意流量或应用在生产环境中都能零停机发布。

总结

你制作了只读检查工具,并用 Ansible 逐台发布真实网站及其完整节目单下载。委派的流量池操作与真实内容检查,让每次发布判断都对应到当前受管后端。