介绍
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:

切回 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 只重新加载发生变化的配置。随后,你在写路由前拒绝了一个可访问但未经批准的来源,保留了当前网站。下一个独立实验会更换服务成员,同时保留原来源及其数据。



