Red Hat Certified Engineer (RHCE) #8 エラー処理: block/rescue/always、failed_when、ignore_errors

読了 10分

#7 Jinja2 テンプレート でホストごとの設定ファイルを動的に生成する方法を覚えました。ところがプレイブックは 1 つの task が失敗すると、そのホストの残りの task をスキップして止まります。試験で「特定のステップが失敗しても後片付けは必ず実行せよ」あるいは「command の結果を見て失敗かどうかを判定せよ」のような要求が出ると、デフォルトの動作だけでは点数を取りにくいです。今回の記事では、プレイブックの失敗の流れを自分で制御する エラー処理 (error handling) を扱います。

エラー処理は 2 つに分かれます。1 つは 失敗が起きたときにどう反応するか を決める block/rescue/always と ignore_errors で、もう 1 つは 何を失敗と見なすか を定義し直す failed_when と changed_when です。特に command・shell のように冪等性のないモジュールを扱うとき、changed_when を扱えるかどうかが試験の定番ポイントです。

デフォルトの動作: 失敗すると止まる #

エラー処理を理解するには、まず Ansible のデフォルトの失敗動作を知る必要があります。プレイブックを回すと、Ansible はホストごとに task を上から下へ実行します。あるホストで 1 つの task が失敗すると、そのホストはすぐに止まり、残りの task を実行しません。他のホストは自分の番まで進んでから、同じ地点で止まります。

このデフォルトの動作が合理的なのは、前のステップが失敗した状態で後のステップを強行すると、より大きな損傷を招きうるからです。そのためエラー処理とは、このデフォルトの動作を 意図的に変える ことです。失敗を無視するか、失敗しても後片付けは回すか、何を失敗と数えるかを明示的に決めます。

ignore_errors: 失敗を流す #

ignore_errors: yes を付けると、その task が失敗しても Ansible は失敗として表示するだけで、次の task へ進みます。ホストを止めません。

ignore_errorsの例
- name: ignore_errors の例
  hosts: webservers
  become: true
  tasks:
    - name: あるかもしれないしないかもしれないパッケージの削除を試す
      ansible.builtin.dnf:
        name: legacy-tool
        state: absent
      ignore_errors: yes

    - name: この task は上が失敗しても実行される
      ansible.builtin.debug:
        msg: "後片付けのステップへ進みました"

注意点が 2 つあります。1 つ目、ignore_errors は task が失敗で終わった後 にその失敗を無視するものであって、失敗そのものをなくすわけではありません。実行結果の要約には依然として失敗した task として集計されます。2 つ目、ignore_errors は ホストに接続すらできない unreachable の状況は無視できません。unreachable は別のカテゴリーであり、それを無視するには ignore_unreachable: yes を使います。

ignore_errors は「失敗しても構わない 1 つか 2 つの task」に局所的に使う道具です。複数の task をまとめて失敗の流れを構造的に扱うには block を使います。

block/rescue/always: 例外を構造化する #

block は複数の task を 1 つの単位にまとめます。これに rescue と always を加えると、他の言語の try・catch・finally と同じ構造になります。

  • block。正常な流れで実行する task をまとめます。
  • rescue。block の中で task が失敗したときだけ実行されます。
  • always。成功・失敗と無関係に常に最後に実行されます。
block/rescue/alwaysの例
- name: block/rescue/always の例
  hosts: webservers
  become: true
  tasks:
    - name: アプリケーションのデプロイ試行と復旧
      block:
        - name: 新しい設定ファイルを配置
          ansible.builtin.template:
            src: app.conf.j2
            dest: /etc/myapp/app.conf

        - name: サービスを再起動
          ansible.builtin.service:
            name: myapp
            state: restarted
      rescue:
        - name: 失敗時にバックアップ設定へロールバック
          ansible.builtin.copy:
            src: /etc/myapp/app.conf.bak
            dest: /etc/myapp/app.conf
            remote_src: true

        - name: ロールバック後にサービスを再起動
          ansible.builtin.service:
            name: myapp
            state: restarted
      always:
        - name: 処理結果を記録
          ansible.builtin.debug:
            msg: "デプロイの試行が終わりました。ログを確認します"

動作の核心を整理します。block の中のどれかの task が失敗すると、その時点で block の残りの task はスキップして rescue へ進みます。rescue の task がすべて成功すると、そのホストは失敗ではなく 成功と見なされます。block が失敗なく終わった場合、rescue は実行されません。always は両方の場合で最後に実行されます。

rescue が成功するとホストが成功として処理されるという点は、試験で特に役立ちます。「X を試みるが失敗したら Y で復旧し、プレイブックは続けて進めよ」のような要求を、まさにこの構造で表現します。

rescue の中で原因を確認する magic 変数 #

rescue ブロックでは、失敗の原因を突き止めるいくつかの変数を使えます。

  • ansible_failed_task。失敗した task の情報を保持します。ansible_failed_task.name で名前を得ます。
  • ansible_failed_result。失敗した task の結果 (戻り値) を保持します。
失敗したtaskの確認
      rescue:
        - name: どの task が失敗したかを出力
          ansible.builtin.debug:
            msg: "失敗した task: {{ ansible_failed_task.name }}"

failed_when: 失敗条件を自分で定義する #

デフォルトでは、Ansible はモジュールが返す状態で成功・失敗を判定します。ところが command や shell モジュールはコマンドの終了コードだけを見るため、「終了コードは 0 だが出力の中にエラー文がある」場合を失敗として捉えられません。こういうときは failed_when何を失敗と見なすか を自分で決めます。

まず command の結果を register で変数に入れ、その変数を条件として判定するパターンが標準です。

failed_whenの例
- name: failed_when の例
  hosts: appservers
  tasks:
    - name: ヘルスチェックコマンドを実行
      ansible.builtin.command: /usr/local/bin/healthcheck
      register: health
      failed_when: "'ERROR' in health.stdout"

    - name: 終了コードと出力内容を合わせて判定
      ansible.builtin.command: /usr/local/bin/deploy
      register: deploy
      failed_when: deploy.rc != 0 or 'FAILED' in deploy.stdout

failed_when は条件が真ならその task を失敗として表示します。逆に failed_when: false にしておくと、何があっても失敗とは見なしません。これは ignore_errors と似て見えますが意味が違います。ignore_errors は「失敗したが無視」で、failed_when: false は「そもそも失敗として数えない」です。後者は実行要約でも失敗として集計されません。

register 結果の構造 #

failed_when と changed_when をきちんと使うには、register で入れた結果の構造を知る必要があります。command・shell 系でよく使うキーは次のとおりです。

  • rc。コマンドの終了コード (return code) です。
  • stdout。標準出力全体を 1 つの文字列として保持します。
  • stdout_lines。標準出力を行単位のリストとして保持します。
  • stderr。標準エラー出力です。

条件で特定の文を探すときは '文' in result.stdout の形を使い、行単位で扱うときは stdout_lines を使います。

changed_when: 変更条件を自分で定義する #

changed_when何を変更と見なすか を決めます。失敗判定ではなく changed 状態の判定を扱うという点で failed_when と対をなします。このキーが RHCE で重要な理由は、command・shell モジュールの冪等性の問題です。

command や shell モジュールは 実行しさえすれば無条件で changed と報告します。実際にシステムを変えない照会系のコマンドであっても changed と集計され、2 回目の実行で changed が 0 になりません。冪等性が採点の核心である試験では、これは減点要因です。

そのため照会だけする command には changed_when: false を付けて 変更なし と明示します。

changed_whenで冪等性補完
- name: changed_when で冪等性を補う
  hosts: appservers
  tasks:
    - name: 現在のバージョンを確認 (照会のみ、変更ではない)
      ansible.builtin.command: /usr/local/bin/myapp --version
      register: appver
      changed_when: false

    - name: 結果に応じて変更かどうかを判定
      ansible.builtin.command: /usr/local/bin/sync-config
      register: sync
      changed_when: "'updated' in sync.stdout"
      failed_when: sync.rc != 0

2 番目の task のように、failed_when と changed_when を 1 つの task に一緒に使う場合が多いです。「終了コードが 0 でなければ失敗として、出力に updated があれば変更として」判定する形です。command モジュールを使わざるをえない状況でこの 2 つのキーを加えると、専用モジュールに近い冪等性を真似できます。

command の代わりにモジュールを先に検討する #

changed_when で冪等性を補う前に、原則は command・shell を専用モジュールに置き換える ことです。パッケージのインストールは dnf で、ファイルの配置は copy・template で、サービスの制御は service で処理すれば、冪等性がモジュールの中に入っています。command・shell は適切なモジュールがないときだけ使い、そのときに changed_when・failed_when で冪等性と失敗判定を補う順序を勧めます。

プレイ全体の失敗を扱うキー #

ここまでは task・block 単位の処理でした。複数のホストを扱うプレイ全体の失敗戦略を決めるキーもあるので、1 行ずつ整理します。

  • any_errors_fatal: true。1 つのホストでも task で失敗すると、その batch の すべてのホストをすぐに中断 します。1 台でもずれたら全体を止めるべき作業に使います。
  • max_fail_percentage: 30。失敗したホストの比率が指定したパーセントを超えるとプレイを中断します。「30% を超えて失敗したら止まれ」のような許容限界を表現します。
  • force_handlers: true。プレイが途中で失敗しても、すでに notify で予約された handler を 終わる前に強制的に実行 します。普通 handler は失敗すると実行されないので、必ず回さなければならない後片付け系の handler があるときに使います。

これらのキーは task ではなく play レベル に置きます。よく出題されるわけではありませんが、要求事項に「1 台でも失敗したら全体中断」のような文が見えたら any_errors_fatal を思い浮かべます。

試験ポイント #

エラー処理で点数が分かれる地点を整理します。

  • command の結果で失敗判定。command・shell を register で入れた後、failed_when で出力内容や終了コードを見て失敗を判定するパターンが定番です。'文' in result.stdout の表現を手に馴染ませておきます。
  • command の冪等性補完。照会系の command には changed_when: false を付けて、2 回目の実行で changed が 0 になるようにします。冪等性の検証でよく減点される部分です。
  • block/rescue で復旧フローを作成。「試みるが失敗したら復旧して続行」は block/rescue で表現します。rescue が成功するとホストが成功として処理される点を覚えておきます。
  • always で後片付けを保証。成功・失敗と無関係に回さなければならない後片付けは always に置きます。
  • failed_when: false と ignore_errors の違い。前者は失敗として数えず、後者は失敗を無視するだけで集計には残ります。要求の文を見て 2 つを区別して使います。
  • 2 回回して検証。書いたプレイブックを 2 回実行し、2 回目で changed・failed が意図どおりに出るかを確認する習慣を付けます。

まとめ #

今回の記事で押さえたこと:

  • デフォルトの動作。task が失敗するとそのホストはすぐに止まり、残りの task をスキップします。
  • ignore_errors。失敗を無視して次の task へ進むが、実行要約には失敗として残ります。unreachable は無視できません。
  • block/rescue/always。try・catch・finally の構造。rescue が成功するとホストは成功として処理され、always は常に最後に実行されます。
  • failed_when。何を失敗と見なすかを自分で決めます。command の結果を register で入れ、出力・終了コードで判定します。
  • changed_when。何を変更と見なすかを決めます。照会系の command に changed_when: false で冪等性を補います。
  • play レベルのキー。any_errors_fatal、max_fail_percentage、force_handlers でプレイ全体の失敗戦略を決めます。

次へ: Tag と条件付き #

失敗の流れを制御する方法を押さえました。今度は task を いつ実行するか を決める条件と、同じ task を 繰り返し実行する 流れを扱う番です。

#9 Tag と条件付き: when、loop、until では、when で条件付き実行をかける方法、loop でリストを繰り返し処理する方法、until で条件が満たされるまで再試行する方法、そして tag でプレイブックの一部だけを選んで実行する試験定番のパターンまで、自分で書きながら整理します。

X