不良な更新を停止する

AnsibleBeginner
オンラインで実践に進む

はじめに

Courtyard Sessions の新しいパッケージは、ホームページが正常に見える一方で、訪問者向けダウンロードがありません。この独立環境は、元のサイトが両バックエンドで正常な状態から始まります。準備済み Ansible リリース処理を改良し、候補版が失敗したら以前の利用可能なリリースへ戻し、別ノードの更新前に停止させましょう。

先に「ホストを 1 台ずつ更新する」を完了するか、その点検ツール、委任、順次バッチ、copy/file 操作、HTTP コンテンツ全体の検証と同等の実践経験が必要です。このラボでは、実際のリンク先の記録と block/rescue による復旧を学びます。時間は作者の見積もりであり、初学者の所要時間を実測したものではありません。

失敗から復旧してリリースを停止する

このステップでは、実際の直前リリースを記録し、内容検証の失敗後に現在のノードを復旧して、次のバッチを明示的に止めます。

Website タブを開きます。元の見出しは Good things, around the corner. です。The program と Download the program (CSV) を使い、元の 2 つの催しを確認してください。ゲートウェイと 2 台の SSH バックエンドは前のラボから独立しており、以前のファイルや稼働プロセスは不要です。

Controller タブへ切り替えます。開始位置は /home/labex/project です。準備済みの 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 を使い、block の前で current を記録します。follow: false は、リンク先のディレクトリではなくリンクそのものを調べます。保存した previous.stat.lnk_source は実際の直前のリンク先です。これにより、固定ディレクトリを仮定せず、それまで稼働していたリリースへ戻せます。

復旧では、リンクを戻し、復旧後のホームページと CSV 全体を要求して、両方を直前のパッケージと比較します。basename フィルターは保存したリンク先からリリースディレクトリ名を取り出し、既存の lookup('file', ...) の比較で対応するローカルパッケージを読めるようにします。正常に復旧したノードだけをプールに戻します。復旧後のいずれかのリソースが欠けていたり内容が誤っていたりすると、その rescue タスクが失敗し、ノードは外れたままになります。別のバックエンドは訪問者への提供を続けます。

rescue が成功すると、Ansible の失敗処理の判断において、元のタスク失敗の影響が取り消されます。そのため、復旧成功後は any_errors_fatal だけでは後続バッチを停止できません。rescue の最後で明示的に fail を実行すると、候補版の拒否を報告して次のバッチを止められます。元のサイトが正常に戻っていても、非ゼロ終了はこの演習の意図した結果です。無効な YAML や到達不能な SSH ホストは、この rescue を起動しません。あらゆる問題をロールバックできる保証ではありません。

rollout.yml を次の完全な版で置き換えます。cat > が置き換えるのはこの学習者用プレイブックだけです。引用符付きのヒアドキュメント区切りは 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 $?

通常は 2 など、非ゼロのステータスになります。出力では、新ホームページの検証が成功し、候補のダウンロードは HTTP 404、復旧後のホームページと CSV は検証に成功し、最後に Stop subsequent batches after recovery が意図的に失敗します。HTTP 404 はダウンロードの欠落を示します。SSH 接続は復旧に引き続き利用できます。web2 はリリースタスクへ進んではいけません。

観察ツールを再実行します。

ansible-playbook inspect.yml

両バックエンドが courtyard-preview を示し、両方が有効でなければなりません。Website タブへ切り替え、再読み込みし、The program を開いて CSV を再度ダウンロードし、元の 2 つの催しが利用できることを確かめます。Plan a visit も引き続きアクセシビリティ情報を表示する必要があります。

復旧後のサイトでは、元の 2 つの催しと完全なダウンロードを引き続き利用できます。

復旧した Courtyard Sessions サイトと元のプログラム

通常の YAML やファイル名のミスなら、ls で確認し、プレイブック全体を修正して構文検証をやり直します。復旧処理が機能する前に、自分の編集でリンクを切り替えてしまった場合は、まず上の正しいプレイブックを保存してください。既に学んだ正常なリリース処理を使い、リリースと期待するプログラムの両方に既知の元パッケージを明示して復旧できます。引用符により、2 つの代入を 1 つの --extra-vars 引数にまとめます。

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

この復旧コマンドは成功するはずです。inspect.yml で両ノードを確認してから、元の ansible-playbook rollout.yml コマンドで不完全な候補版を再実行し、必要な失敗と復旧を示します。VM のリセットや候補パッケージの書き換えは行いません。プレイブックを再利用可能に保つため、プールへの要求には提供された pool_api を、ファイルパスには release_root と package_root を、リリースタスクにはここで示した直接のモジュールを使ってください。

まとめ

実際にダウンロードが欠けた失敗から、本当の直前のサイトへ復旧しました。2 つの復旧確認がプールへの復帰を制御し、rescue が成功しても、明示的な失敗により後続の Ansible バッチが停止します。