【Ansible】Ansible Roleでエラー処理が発生することをテストしたい

## はじめに

&emsp;Ansible Roleに、想定外の状態で停止するエラー処理を実装していること
があると思います。このエラー処理が発生することをテストするにはどうすればよいかを
考えてみました。

## TL;DR

- エラー処理が発生することをテストをするには、`block`セクションでAnsible Roleを
実行してエラーを起こし、`rescue`セクションでエラーの内容を確認する。
- `rescue`セクションでは、変数`ansible_failed_task`と`ansible_failed_result`で
エラーが発生したタスクの状況が確認可能。

## 前提条件

- Ansible Roleの基本的なディレクトリ構成を理解していること。
- Ansibleにおける`block`セクション、`rescue`セクションの使い方を理解していること。

## 実行環境

- Python: 3.6.8
- Ansible: 2.9.7

## 詳細

### 1. テストするAnsible Role

- 今回はテスト用にAnsible Role「check-pass-flag」を作成しました。
メイン処理（tasks/main.yml）はシンプルで、変数`pass_flag`が`true`でない場合に
エラーが発生します。
- Ansible Roleでは、想定通りのタスクでエラーが発生するかを確認できるように、以下に注意しました。
  - タスク名は重複しないようにする
  - assertモジュールにはエラーメッセージ（fail_msg）を明示する

```yml: tasks/main.yml
---
- name: "Task1 - display pass_flag"
  debug:
    var: pass_flag

- name: "Task2 - check if pass_flag is true"
  assert:
    that:
      - pass_flag is boolean
      - pass_flag is true
    fail_msg: "This task failed because pass_flag is not true"    # エラーメッセージ
```

### 2. エラー内容の確認

#### 2-1. エラー内容の確認用Playbook

- ここでは、エラー内容の確認用Playbookを実行します。ポイントは以下の通りです。
  1. 変数`pass_flag`を`false`（trueではない値）にして、あえてAnsible Roleでエラーを
  発生させる
  2. `block`セクションでAnsible Roleを実行してエラーを起こし、`rescue`セクションで
  エラーの内容を確認する。エラーの内容は、変数`ansible_failed_task`と
  `ansible_failed_result`に格納されるので、`debug`モジュールで確認する

```yml: test-check-pass-flag-1.yml
---
- name: "Test Ansible Role check-pass-flag"
  hosts: localhost
  vars:
    pass_flag: false
  gather_facts: false
  tasks:
    - block:
        # ポイント1: 変数pass_flagがfalseなのでエラーが発生する
        - name: "Import check-pass-flag"
          import_role:
            name: "check-pass-flag"
      rescue:
        # ポイント2: blockでエラーが発生すると以下の処理が実行される
        - name: "Display ansible_failed_task"
          debug:
            var: ansible_failed_task

        - name: "Display ansible_failed_result"
          debug:
            var: ansible_failed_result
```

#### 2-2. 変数ansible_failed_taskの確認

&emsp;Playbookを実行すると、想定通りAnsible Roleのタスク「Task2 - check if pass_flag is true」
でエラーが発生したので、まずは変数`ansible_failed_task`の内容を確認します。  
テストとして、エラーメッセージ（args.fail_msg）やエラーしたタスク名（name）が確認できそうです。

```yaml: ansible_failed_task
---
args:
  that:
    - pass_flag is boolean
    - pass_flag is true
  fail_msg: This task failed because pass_flag is not true    # エラーメッセージ
action: assert
async_val: 0
async: 0
changed_when: []
delay: 5
delegate_to: None
delegate_facts: None
failed_when: []
loop: None
loop_control: None
notify: None
poll: 15
register: None
retries: 3
until: []
loop_with: None
name: Task2 - check if pass_flag is true    # エラーしたタスク名
# ～以下、省略～
```

#### 2-3. 変数ansible_failed_resultの確認

&emsp;変数`ansible_failed_result`も同様に確認します。内容は`ansible_failed_task`より
少ないようですが、エラーしたassert文が何かまでわかります。  
テストとして、エラーしたassert文（assertion）とエラーメッセージ（msg）が確認できそうです。

```yaml: ansible_failed_result
---
assertion: pass_flag is true    # エラーしたassert文
changed: false
evaluated_to: false
failed: true
msg: This task failed because pass_flag is not true    # エラーメッセージ
```

### 3. エラー処理の動作確認

#### 3-1. エラー処理の動作確認用Playbook

- ここでは、エラー処理の動作確認用Playbookを実行します。ポイントは以下の通りです。
  1. Ansible Roleでエラーが発生しなかった場合に備えて、failモジュールで強制的にエラーを発生させる
  2. 変数`ansible_failed_task`と`ansible_failed_result`の内容にもとづき、
  assertモジュールで想定通りのエラーが発生していることを確認する

```yml: test-check-pass-flag-2.yml
---
- name: "Test Ansible Role check-pass-flag"
  hosts: localhost
  vars:
    pass_flag: false
  gather_facts: false
  tasks:
    - block:
        # 変数pass_flagがfalseなのでエラーが発生する
        - name: "Import check-pass-flag"
          import_role:
            name: "check-pass-flag"

        # ポイント1: Ansible Roleでエラーが発生しなかった場合、ここでエラーになる
        - name: "Force fail"
          fail:
      rescue:
        # ポイント2: エラーしたタスク名とエラーメッセージを確認
        - name: "Check ansible_failed_task"
          assert:
            that:
              - ansible_failed_task.name == 'Task2 - check if pass_flag is true'
              - >-
                ansible_failed_task.args.fail_msg
                == 'This task failed because pass_flag is not true'

        # ポイント2: エラーしたassert文とエラーメッセージを確認
        - name: "Check ansible_failed_result"
          assert:
            that:
              - ansible_failed_result.assertion == 'pass_flag is true'
              - >-
                ansible_failed_result.msg
                == 'This task failed because pass_flag is not true'
```

#### 3-2. Playbookの実行結果

&emsp;Playbookを実行すると、想定通りAnsible Roleのタスク「Task2 - check if pass_flag is true」
でエラーが発生していますが、`rescue`セクションで想定通りのエラー処理が発生していることを確認できています。

```console
$ ansible-playbook test-check-pass-flag-2.yml

PLAY [Test Ansible Role check-pass-flag] *******************************************************************************

TASK [check-pass-flag : Task1 - display pass_flag] *********************************************************************
ok: [localhost] =>
  pass_flag: false

TASK [check-pass-flag : Task2 - check if pass_flag is true] ************************************************************
fatal: [localhost]: FAILED! => changed=false
  assertion: pass_flag is true
  evaluated_to: false
  msg: This task failed because pass_flag is not true

TASK [Check ansible_failed_task] ***************************************************************************************
ok: [localhost] => changed=false
  msg: All assertions passed

TASK [Check ansible_failed_result] *************************************************************************************
ok: [localhost] => changed=false
  msg: All assertions passed

PLAY RECAP *************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0
```

## さいごに

- `block`セクションでAnsible Roleを実行してエラーを起こし、`rescue`セクションで
エラーの内容を確認することで、エラー処理の動作確認ができました。
- Ansible Roleのテストについて、正常の動作だけでなくエラー処理の動作確認も
することで、より品質の高いAnsible Roleが開発できるのではないかと思います。

## 参考URL

- [Ansible Documentation - Blocks][1]
  - Blocksの使い方、エラー処理の方法など

---

[1]: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_blocks.html