잘못된 업데이트 중단하기

AnsibleBeginner
지금 연습하기

소개

Courtyard Sessions의 새 패키지는 그럴듯한 홈페이지를 갖추었지만 방문자 다운로드가 없습니다. 이 독립 환경은 두 서버에서 원래 사이트가 작동하는 상태로 시작합니다. 후보 검사에 실패하면 이전의 사용 가능한 릴리스를 복구하고 다른 노드의 업데이트 전에 멈추도록 준비된 Ansible 배포를 개선하세요.

먼저 「한 번에 호스트 하나씩 업데이트하기」를 완료하거나 그 실습의 확인, 위임, 순차 배치, copy/file 작업, 전체 HTTP 검사에 해당하는 경험이 필요합니다. 이 실습은 실제 링크 기록과 block/rescue 복구를 가르칩니다. 시간은 초보자에게 측정한 값이 아니라 저자의 예상치입니다.

실패한 릴리스 복구 후 배포 중단하기

이 단계에서는 실제 이전 릴리스를 기록하고 검사 실패 후 현재 노드를 복구한 뒤 다음 배치를 명시적으로 중단합니다.

Website 탭을 여세요. 원래 제목은 **Good things, around the corner.**입니다. The program과 **Download the program (CSV)**으로 원래 두 세션을 확인하세요. 게이트웨이와 두 SSH 서버는 이전 실습과 독립적이며 이전 파일이나 프로세스가 필요하지 않습니다.

시작 경로가 /home/labex/project인 Controller 탭으로 전환하세요. 준비된 inspect.yml과 rollout.yml은 이전 실습의 도구입니다. 선택한 패키지와 현재 상태를 읽으세요.

cat group_vars/all.yml
ls packages/courtyard-incomplete
ansible-playbook inspect.yml

release_id는 courtyard-incomplete를 선택합니다. 홈페이지와 식별 정보는 있지만 schedule.csv는 없습니다. 두 실제 서버는 여전히 courtyard-preview를 보고합니다. 모든 패키지의 바이트를 보존하세요. 후보에 CSV를 추가하면 복구를 연습하는 대신 결함을 숨기게 됩니다. 아직 보호되지 않은 배포를 실행하지 마세요.

block은 실패할 수 있는 작업을 묶고, 블록 내 작업이 일반적인 실패로 끝나면 rescue 작업이 실행됩니다. 블록 전에 파일 시스템 메타데이터를 읽는 stat으로 current를 기록하세요. follow: false는 대상 디렉터리 대신 링크 자체를 읽습니다. 저장한 previous.stat.lnk_source는 실제 이전 대상이므로 고정 디렉터리를 가정하지 않고 실제 실행 중이던 버전을 복구합니다.

복구는 링크를 되돌리고 복원된 전체 홈페이지와 CSV를 요청해 이전 패키지와 비교해야 합니다. basename 필터는 저장된 대상에서 릴리스 디렉터리 이름을 추출해 기존 lookup('file', ...) 비교가 일치하는 로컬 패키지를 읽게 합니다. 복원되고 정상인 노드만 다시 넣습니다. 복원된 리소스 중 하나라도 없거나 내용이 잘못되면 해당 rescue 작업이 실패하고 노드는 계속 제외됩니다. 다른 서버는 방문자를 계속 응대합니다.

rescue가 성공하면 Ansible의 오류 처리 판단에서 원래 실패의 영향이 해소됩니다. 따라서 복구 성공 시 any_errors_fatal만으로는 이후 배치가 중단되지 않습니다. rescue 끝의 명시적인 fail이 거부를 보고하고 다음 배치를 중단합니다. 이전 사이트가 다시 작동해도 0이 아닌 종료는 의도된 결과입니다. 잘못된 YAML과 SSH 접속 불가 호스트는 이 rescue 메커니즘을 실행하지 않으며, 이것이 일반적인 롤백 보장은 아닙니다.

rollout.yml을 아래 전체 버전으로 바꾸세요. cat >는 이 학습용 playbook만 덮어쓰며, 따옴표 구분자는 Ansible 표현식을 유지합니다. 표시된 공백을 지키세요.

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

어느 서버든 변경하기 전에 문법을 검사하세요.

ansible-playbook rollout.yml --syntax-check

그다음 불완전한 릴리스 게시를 시도하세요. 이 명령은 사이트 복구 후 실패하는 것이 예상 결과입니다.

ansible-playbook rollout.yml

바로 종료 상태를 읽으세요. $?에는 직전 명령의 종료 상태가 있고 echo가 이를 출력합니다. 다른 명령을 실행하면 셸이 저장한 값이 바뀝니다.

echo $?

0이 아닌 상태, 보통 2를 예상하세요. 새 페이지 검사는 성공하고 후보 다운로드는 HTTP 404를 반환하며 복원된 페이지와 CSV는 통과합니다. 이후 Stop subsequent batches after recovery가 의도적으로 실패합니다. HTTP 404는 다운로드가 없음을 뜻합니다. 복구에 필요한 SSH 연결은 계속 사용할 수 있습니다. web2는 게시 작업에 진입하면 안 됩니다.

관찰 도구를 다시 실행하세요.

ansible-playbook inspect.yml

두 서버 모두 courtyard-preview를 표시하고 활성 상태여야 합니다. Website로 전환해 새로 고치고 The program으로 이동한 뒤 CSV를 다시 다운로드하여 원래 두 세션이 여전히 있는지 확인하세요. Plan a visit는 접근성 안내를 계속 보여야 합니다.

복구된 사이트는 원래 두 세션과 전체 다운로드를 계속 제공합니다.

원래 프로그램을 제공하는 복구된 Courtyard Sessions 사이트

일반적인 YAML이나 파일명 오류라면 ls를 사용하고 전체 playbook을 고친 뒤 문법 검사를 반복하세요. 복구가 작동하기 전에 이전 편집으로 링크가 바뀌었다면 위의 올바른 playbook을 먼저 저장하세요. 앞에서 배운 정상 배포를 사용해 알려진 원래 패키지를 복원할 수 있습니다. 릴리스와 예상 프로그램 양쪽에 그 패키지를 선택하세요. 따옴표는 두 할당을 하나의 --extra-vars 인수로 유지합니다.

ansible-playbook rollout.yml --extra-vars "release_id=courtyard-preview approved_release=courtyard-preview"

이 복원 명령은 성공해야 합니다. inspect.yml로 두 노드를 확인한 뒤 원래 명령 ansible-playbook rollout.yml로 불완전한 후보를 다시 시도해 요구된 실패와 복구를 증명하세요. VM을 초기화하거나 후보를 다시 쓰지 마세요. 재사용 가능한 playbook을 유지하세요. 풀 요청은 제공된 pool_api를, 경로는 release_root와 package_root를, 작업은 표시된 직접 모듈을 사용해야 합니다.

요약

실제 다운로드 누락 실패 후 실제 이전 사이트를 복구했습니다. 두 복원 검사가 노드의 재편입을 제어하며, rescue가 성공해도 명시적인 실패가 다음 Ansible 배치를 중단합니다.