接通活动页面

AnsibleBeginner
立即练习

介绍

Riverlight Weekend 的首页已经准备好,但访客还无法打开节目或场地页面。两个来源服务本身都已正常运行。你将用 Ansible 把入口连接到选定的 inventory 成员,只在配置变化时重新加载,并防止不合适的替代来源破坏可用路由。

你需要掌握基本终端导航,并练习过阅读小型 Ansible inventory、变量与 playbook。本实验会在使用前解释 HTTP 服务角色、分组成员、跨主机变量、JSON、只读探测和重新加载 handler。所有软件与活动服务均已准备好。时长为作者估计,包含阅读和普通错误修正。

接通节目与到访页面

在本步骤中,你将把活动网站连接到准备好的节目和场地来源,使用 inventory 成员关系,而不是在模板中写死地址。

Riverlight Weekend 已有可用的首页和访客指南。它的节目与到访链接目前会打开不可用提示。入口是一个准备好的小型 HTTP 反向代理:它将请求转发到另一个服务,再返回该服务的实际响应,不会把节目数据复制进首页。入口运行在控制节点上;两个来源节点是共享这台训练 VM 内核的容器,并非三台独立 VM。软件和服务已经就绪,你只需要管理入口的连接。

打开 Event Site Tab。首页应显示 **A little closer to the river.**。点击 Explore the program:由于路由尚未连接,出现 This page is taking a break. 是预期的 HTTP 502 响应。使用页面的 Back to the weekend 链接返回,再点击导航中的 Visitor guide。指南已经可用,并且必须保持可用。

切换到 Controller Tab,起始目录是 /home/labex/project。pwd 用于打印当前目录:

pwd

如果之后找不到当前工作目录,用 cd /home/labex/project 返回。文件名拼错会出现“No such file or directory”;核对拼写,必要时用 ls templates 列出该目录,再重新尝试,无需重置 VM。

cat 用于打印文件。阅读获准的服务选择、inventory 与各主机数据:

cat approved-services.txt
cat inventory.ini
cat host_vars/web1.yml
cat host_vars/web2.yml

inventory 中的 [program] 与 [venue] 分组各选择一个来源。web1 提供当前节目,web2 提供场地信息。主机文件给出实际 service_address 和服务端口。不同 VM 的地址可能不同。一些节点也运行归档服务;可以访问,并不表示它就是获准的当前节目。

[gateway] 分组包含 entry。ansible_connection=local 表示 Ansible 在控制节点本地管理入口,不需要再通过 SSH 跳转。/usr/bin/python3 是准备好的本地 Python 解释器。HTTP 的端口用来标识某个地址上正在监听的服务;同一节点可以在不同端口运行多个服务。

修改入口前,先直接读取所选来源。准备好的 curl 配置文件保存了实际 URL。--config 从文件读取 curl 选项;fail 让 HTTP 错误导致命令失败,silent 隐藏进度条,show-error 保留有用的报错。这些文件不包含密码或令牌。

cat requests/web1.conf
curl --config requests/web1.conf
cat requests/web2.conf
curl --config requests/web2.conf

JSON 响应标识了 program 与 venue、活动标识 riverlight 及其版本。JSON 是包含命名字段的结构化文本。来源请求成功说明该来源能被直接访问,还不能证明网站路由已经正常。

接着创建路由模板。Ansible 的 groups 将分组名映射到成员列表;[0] 取出单成员分组中的第一个成员。hostvars 读取该成员的变量。下面的 playbook 把这两个成员分别命名为 program_host 和 venue_host。在 Jinja 表达式中,~ 将值连接成文本,也能处理数字端口。| to_json 生成引号正确的 JSON 字符串,避免不安全地手工拼接引号。

下面的 shell 命令用 > 替换这一个文件。<<'EOF' 把后续各行按原样送给 cat;单引号避免 shell 解释 Jinja 花括号。请一直粘贴到单独占一行的结束标记 EOF。如果遗漏该分隔符,shell 停在续行提示符等待输入,按 Ctrl+C,再重新粘贴完整命令。

cat > templates/routes.json.j2 <<'EOF'
{
  "program": {{ ('http://' ~ hostvars[program_host].service_address ~ ':' ~ hostvars[program_host].program_port) | to_json }},
  "venue": {{ ('http://' ~ hostvars[venue_host].service_address ~ ':' ~ hostvars[venue_host].venue_port) | to_json }}
}
EOF

现在编写入口 playbook。gather_facts: false 避免收集用不到的机器事实。vars 定义从当前 inventory 求值的表达式。template 渲染文件,设置模式 0644(所有者可写,所有人可读),并且只在路由文件变化时通知 handler。handler 是收到通知后执行的任务,通常在 play 结束时运行。

入口把路由保存在内存中,因此只替换文件还不够。准备好的重新加载辅助程序只向该入口的现有进程发送真实的 SIGHUP 重新加载信号,并等待确认。handler 的 command 使用 argv,即由独立参数组成的列表。playbook_dir 是此 playbook 所在目录,让受管理的入口路径限定在本项目中。你不需要 sudo 或重启服务。

cat > connect.yml <<'EOF'
- name: Connect the event services
  hosts: gateway
  gather_facts: false
  vars:
    program_host: "{{ groups['program'][0] }}"
    venue_host: "{{ groups['venue'][0] }}"
  tasks:
    - name: Write the inventory-derived routes
      ansible.builtin.template:
        src: templates/routes.json.j2
        dest: "{{ playbook_dir }}/entry/routes.json"
        mode: '0644'
      notify: Reload entry
  handlers:
    - name: Reload entry
      ansible.builtin.command:
        argv:
          - python3
          - /opt/labex/service-links/reload.py
          - "{{ playbook_dir }}/entry"
EOF

先检查 YAML 结构,再预览预计的文件变化。--syntax-check 不会应用任务;--check 预测受支持的变更;--diff 显示旧内容和拟写入内容。这个预览不会向网关发送信号,也不能证明最终网站已经正常。

ansible-playbook connect.yml --syntax-check
ansible-playbook connect.yml --check --diff

diff 应把未连接的 127.0.0.1:9 路由替换为当前来源地址与端口。正常应用 playbook:

ansible-playbook connect.yml

你应看到模板发生变化,以及 RUNNING HANDLER [Reload entry]。返回 Event Site,使用导航中的 Program 查看当前三个活动:Paper and ink、Acoustic afternoon 和 Shared supper。点击 Download the schedule 下载 schedule.csv;其中的时间、活动和地点应与表格一致。再通过 Visit 查看 Riverside Yard,随后打开 Visitor guide。首页与指南必须仍然可用。

接通后的节目页应显示这三个当前活动,表格正下方是 Download the schedule:

当前 Riverlight 节目与节目单下载

切回 Controller。也可以在 VM 内保存并查看实际下载。在 curl 中,-f 使 HTTP 错误导致失败,-sS 隐藏进度条但显示报错,-o 指定输出文件名:

curl -fsS http://127.0.0.1:8081/program/schedule.csv -o schedule.csv
cat schedule.csv

预期内容:

time,session,place
10:30,Paper and ink,Making table
13:00,Acoustic afternoon,River stage
16:00,Shared supper,Long table

127.0.0.1 表示这台控制节点,入口在其 8081 端口监听。网站的公共浏览器地址也转发到同一个服务。

再次运行相同 playbook:

ansible-playbook connect.yml

汇总应报告 changed=0 和 failed=0,不应运行重新加载 handler。这就是收敛:当前声明已与文件一致。实际页面和下载共同证明成果可用,不能只看执行汇总。

在替换前保护可用路由

在本步骤中,你将在写入路由前加入真实 HTTP 身份检查,再拒绝一个可访问的归档服务,同时保留可用网站。

继续使用 Controller,工作目录为 /home/labex/project。HTTP 200 只表示服务器成功回答,不能证明服务类型、活动或版本正确。当前节目与场地通过 /identity 表明身份。准备好的 group_vars/all.yml 保存了获准的活动标识与版本:

cat group_vars/all.yml

加入前置检查,即在变更之前执行的检查。assert 会在条件为假时停止 play。第一个断言要求每个服务分组恰有一个成员。每个 uri 任务从控制节点发出真实 HTTP GET 请求。return_content: true 保留响应正文;JSON 响应也可通过 .json 读取。register 将任务结果保存到一个变量名下,供后续任务使用。最后的断言将实际身份与批准值比较。

HTTP 任务特意设置 check_mode: false:这些只读请求在预览时也必须执行。默认的 uri 任务可能在 check mode 下跳过。此设置只属于只读探测,并不允许在预览期间应用模板或重新加载。三秒请求超时可以避免缺失的来源长时间阻塞练习。

用下面的完整版本替换 playbook。保留相同模板,所有身份断言都必须放在模板任务之前:

cat > connect.yml <<'EOF'
- name: Connect the event services
  hosts: gateway
  gather_facts: false
  vars:
    program_host: "{{ groups['program'][0] }}"
    venue_host: "{{ groups['venue'][0] }}"
  tasks:
    - name: Require one member for each service
      ansible.builtin.assert:
        that:
          - groups['program'] | length == 1
          - groups['venue'] | length == 1
    - name: Read the selected program identity
      ansible.builtin.uri:
        url: "http://{{ hostvars[program_host].service_address }}:{{ hostvars[program_host].program_port }}/identity"
        return_content: true
        timeout: 3
      check_mode: false
      register: program_probe
    - name: Read the selected venue identity
      ansible.builtin.uri:
        url: "http://{{ hostvars[venue_host].service_address }}:{{ hostvars[venue_host].venue_port }}/identity"
        return_content: true
        timeout: 3
      check_mode: false
      register: venue_probe
    - name: Check the actual service identities
      ansible.builtin.assert:
        that:
          - program_probe.json.service == 'program'
          - program_probe.json.site == event_site
          - program_probe.json.edition == program_edition
          - venue_probe.json.service == 'venue'
          - venue_probe.json.site == event_site
          - venue_probe.json.edition == venue_edition
    - name: Write the inventory-derived routes
      ansible.builtin.template:
        src: templates/routes.json.j2
        dest: "{{ playbook_dir }}/entry/routes.json"
        mode: '0644'
      notify: Reload entry
  handlers:
    - name: Reload entry
      ansible.builtin.command:
        argv:
          - python3
          - /opt/labex/service-links/reload.py
          - "{{ playbook_dir }}/entry"
EOF
ansible-playbook connect.yml --syntax-check

现在保存可用路由文件的指纹。sha256sum 计算内容哈希;> 将输出保存到单独文件,以便随后比较:

sha256sum entry/routes.json > routes-before.sha256

准备好的另一个 inventory 把归档选作节目。先阅读它,再有意用 -i 尝试;该选项仅为这一次命令选择 inventory 文件,不会修改正常 inventory:

cat inventory-wrong-service.ini
ansible-playbook -i inventory-wrong-service.ini connect.yml

这个命令应以非零状态失败,并报告 failed=1。归档能访问,也返回 JSON,但其版本未经批准。身份断言应在 Write the inventory-derived routes 之前失败;汇总应显示 changed=0,不应运行重新加载 handler。这与连接拒绝不同:连接拒绝时,uri 根本无法取得响应。这两种情况都必须阻止写入。

检查已保存的指纹。-c 要求 sha256sum 将当前文件与记录的哈希比较:

sha256sum -c routes-before.sha256

预期显示 entry/routes.json: OK。在 Event Site 中打开 Program,再次下载当前节目单。拒绝一个提议后,当前节目仍须可用,而不只是留下一个文件。

返回 Controller,使用正常 inventory:

ansible-playbook connect.yml --check --diff
ansible-playbook connect.yml

两个所选身份都应通过。已有正确路由无需变更,因此汇总报告 changed=0,不运行 handler。如果出现 YAML 缩进错误,对照上方完整代码块,使用空格而非 Tab,重新写入 connect.yml;语法错误并不代表前置检查成功拒绝了来源。

你的管理范围是 connect.yml、templates/routes.json.j2,以及通过该 playbook 管理的入口路由。保留提供的 inventory、主机数据、获准版本、curl 请求文件、后端内容、组织者便签、首页和访客指南。重新加载时,使用提供的 /opt/labex/service-links/reload.py,并指定本项目的入口目录。不要在可复用模板或探测中写死当前节点名或地址,不要把来源页面复制到入口、掩盖探测失败、重启来源节点或修改准备好的网关实现。inventory 改变时,配置中的成员和端口必须继续决定实际连接。

总结

你通过由 inventory 推导的路由接通了真实节目与场地服务,查看了网站及内容准确的 CSV 下载,并用 handler 只重新加载发生变化的配置。随后,你在写路由前拒绝了一个可访问但未经批准的来源,保留了当前网站。下一个独立实验会更换服务成员,同时保留原来源及其数据。