Introduction
A new Courtyard Sessions package has a convincing homepage, but its visitor download is missing. This independent environment starts with the original website healthy on both backends. Improve the prepared Ansible rollout so a failed candidate restores the previous usable release and stops before updating another node.
Complete Update One Host at a Time first, or have equivalent practice with its inspection tool, delegation, serial batches, copy/file operations and full HTTP content checks. This lab teaches actual-link capture and block/rescue recovery. The duration is an author estimate, not a measured beginner completion time.
Restore a Failed Release and Stop the Rollout
In this step, you will capture the actual previous release, recover the current node after a failed content check and stop the next batch explicitly.
Open the Website Tab. The original heading is Good things, around the corner. Use The program and Download the program (CSV) to check the two-session original program. The gateway and two SSH backends are independent of the previous lab; no previous files or running processes are required.
Switch to the Controller Tab, starting at /home/labex/project. The prepared inspect.yml and rollout.yml are the tools from the previous lab. Read the selected package and current state:
cat group_vars/all.yml
ls packages/courtyard-incomplete
ansible-playbook inspect.yml
release_id selects courtyard-incomplete. Its homepage and identity exist, but schedule.csv does not. Both live backends still report courtyard-preview. Keep all supplied package bytes unchanged: adding a CSV to the candidate would hide the release defect instead of exercising recovery. Do not run the unprotected rollout yet.
A block groups tasks that can fail; its rescue tasks run after a normal failed task in that block. Capture current before the block using stat, which reads filesystem metadata. follow: false reads the link itself rather than the directory it points to. The saved previous.stat.lnk_source is the actual previous target, so recovery restores what was running instead of assuming a fixed directory.
Recovery must restore the link, request the restored homepage and complete CSV, and compare both with that previous package. The basename filter extracts the release folder name from the saved target, allowing the existing lookup('file', ...) comparisons to read the matching local package. Only a healthy restored node may rejoin. If either restored resource is missing or wrong, that rescue task fails and the node stays drained; the other backend continues serving visitors.
A successful rescue clears the original task failure for Ansible's failure-handling decisions. any_errors_fatal alone therefore does not stop later batches after successful recovery. An explicit fail at the end of rescue reports the rejected candidate and stops the next batch. A nonzero exit is the intended result of this exercise, even though the previous website is healthy again. Invalid YAML and unreachable SSH hosts do not trigger this rescue mechanism; it is not a general rollback guarantee.
Replace rollout.yml with this complete version. cat > replaces only this learner playbook; the quoted here-document delimiter preserves Ansible expressions. Keep the spaces as shown:
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: Remember the actual previous release link
ansible.builtin.stat:
path: "{{ release_root }}/current"
follow: false
register: previous
- name: Update this backend or recover it
block:
- 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
rescue:
- name: Restore the previous release link
ansible.builtin.file:
src: "{{ previous.stat.lnk_source }}"
dest: "{{ release_root }}/current"
state: link
- name: Read the restored homepage
ansible.builtin.uri:
url: "http://{{ hostvars[inventory_hostname].ansible_host }}:{{ backend_port }}/"
return_content: true
delegate_to: controller
register: restored_home
- name: Require the complete previous homepage
ansible.builtin.assert:
that: restored_home.content == lookup('file', package_root ~ '/' ~ (previous.stat.lnk_source | basename) ~ '/index.html', rstrip=False)
- name: Read the restored download
ansible.builtin.uri:
url: "http://{{ hostvars[inventory_hostname].ansible_host }}:{{ backend_port }}/schedule.csv"
return_content: true
delegate_to: controller
register: restored_download
- name: Require the complete previous program
ansible.builtin.assert:
that: restored_download.content == lookup('file', package_root ~ '/' ~ (previous.stat.lnk_source | basename) ~ '/schedule.csv', rstrip=False)
- name: Rejoin only after both restoration checks
ansible.builtin.uri:
url: "{{ pool_api }}/pool"
method: POST
body_format: json
body:
node: "{{ inventory_hostname }}"
enabled: true
delegate_to: controller
- name: Stop subsequent batches after recovery
ansible.builtin.fail:
msg: Candidate failed; the previous release is healthy again. Stop this rollout.
YAML
Validate its syntax before changing either backend:
ansible-playbook rollout.yml --syntax-check
Then attempt the incomplete release. This command is expected to fail after recovery:
ansible-playbook rollout.yml
Read the final exit status immediately. $? is the previous command's exit status; echo prints it. Another command would replace that saved shell value:
echo $?
Expect a nonzero status, normally 2. In the output, the new homepage check succeeds, the candidate download returns HTTP 404, the restored homepage and CSV pass, and Stop subsequent batches after recovery fails intentionally. HTTP 404 identifies the missing download. The SSH connection remains usable for recovery. web2 must not enter the release tasks.
Run the observation tool again:
ansible-playbook inspect.yml
Both backends must report courtyard-preview, and both remain active. Switch to the Website Tab, reload, visit The program, download the CSV again and verify that the original two sessions remain available. Plan a visit must still show the accessibility information.
The restored website still offers the two original sessions and the complete download.

For an ordinary YAML or filename mistake, use ls, correct the complete playbook and retry the syntax check. If your own earlier edit switched the link before recovery was working, first save the correct playbook above. You can use the same taught healthy-release workflow to restore the known original package by explicitly selecting it for both the release and expected program. The quotes keep these two assignments together as one --extra-vars argument:
ansible-playbook rollout.yml --extra-vars "release_id=courtyard-preview approved_release=courtyard-preview"
That restoration command should succeed. Confirm both nodes with inspect.yml, then rerun the incomplete candidate with the original ansible-playbook rollout.yml command to demonstrate the required failure and recovery. Do not reset the VM or rewrite the supplied candidate. Keep the playbook reusable: pool requests must use the supplied pool_api, file paths the supplied release_root and package_root, and deployment tasks the direct modules shown here.
Summary
You recovered the actual previous website after a real missing-download failure. Both restoration checks control rejoining, and an explicit failure stops later Ansible batches even when rescue succeeds.



