エラーハンドリング


Ansible でタスクの実行時にエラーが発生したときの動作

Ansible Documentation 内の「Modules That Are Useful for Testing」に次の一文があります。

Ansible is a fail-fast system, so when there is an error creating that user, it will stop the playbook run. You do not have to check up behind it.

Google 翻訳の結果です。

「 Ansible はフェイルファストシステムであるため、そのユーザーの作成中にエラーが発生すると、プレイブックの実行が停止します。 背後でチェックする必要はありません。」

少し補足して言い直すと次のようになります。

  • ある管理対象ホストでタスクでエラーが発生したら、その管理対象ホストはエラーが発生したタスクで終了です。
  • エラーが発生したタスク以降のタスク(= 後続のタスク)は実行しません。

プレイの実行して動作を確認します。tasksセクション内に task-1 、task-2 、task-3 の 3 つのタスクがあります。タスク "task-2" は管理対象ホスト node2 だけにエラーが発生し、他の管理対象ホストは実行をスキップします。

- name: task error play
  hosts: all
  gather_facts: no

  tasks:
  - name: task-1
    debug:
  - name: task-2
    command: /bin/false
    when: inventory_hostname == "node2"
  - name: task-3
    debug:

実行結果です。管理対象ホスト node2 はタスク "task-2" でにエラーが発生したため、このタスクで終了です。タスク "task-3" は実行しません。 node2 以外の管理対象ホストは task-3 まで実行します。

[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml task-error.yml

PLAY [task error play] **************************************************************************************************************************************

TASK [task-1] ***********************************************************************************************************************************************
ok: [node1] => {
    "msg": "Hello world!"
}
ok: [node3] => {
    "msg": "Hello world!"
}
ok: [node2] => {
    "msg": "Hello world!"
}

TASK [task-2] ***********************************************************************************************************************************************
skipping: [node3]
skipping: [node1]
fatal: [node2]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": ["/bin/false"], "delta": "0:00:00.005796", "end": "2020-05-09 22:03:45.298726", "msg": "non-zero return code", "rc": 1, "start": "2020-05-09 22:03:45.292930", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}

TASK [task-3] ***********************************************************************************************************************************************
ok: [node1] => {
    "msg": "Hello world!"
}
ok: [node3] => {
    "msg": "Hello world!"
}

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
node2                      : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
node3                      : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

[vagrant@ansible ansible-files]$

エラーハンドリング

「エラーハンドリング」とは発生したエラーに対処することです。 Ansible は fail-fast システムなので、エラーが発生したらそこで実行は終了です。べき等性を保つ観点から、多くの場合はこれで問題ありません。しかし、commandモジュールやshellモジュールを使用して Linux コマンドを実行するとき、これでは問題が発生することがあります。

Linux コマンドを実行すると、必ず戻り値が設定されます。具体的には次の値が設定されます。

  • コマンドが正常終了したとき → 0 が設定されます
  • コマンドが異常終了(エラーが発生した)とき → 0 以外が設定されます

上述のプレイで使用した/bin/falseコマンドの戻り値を確認します。

[vagrant@ansible ansible-files]$ /bin/false
[vagrant@ansible ansible-files]$ echo $?
1
[vagrant@ansible ansible-files]$

lsコマンドの戻り値を確認します。ファイルが存在したときの戻り値は 0 、ファイルが存在しないときの戻り値は 2 です。

[vagrant@ansible ansible-files]$ ls hosts.yml
hosts.yml
[vagrant@ansible ansible-files]$ echo $?
0
[vagrant@ansible ansible-files]$ ls abc
ls: cannot access abc: No such file or directory
[vagrant@ansible ansible-files]$ echo $?
2
[vagrant@ansible ansible-files]$

commandモジュールやshellモジュールを使用して Linux コマンドを実行したときの戻り値が 0 以外の場合、Ansible はタスクの実行が失敗したと判断し fatal として処理します。これが上述のプレイでエラーが発生した理由です。


レジスタ変数

registerディレクティブで定義するレジスタ変数を使用すると、モジュールの実行結果や戻り値を確認できます。

次のプレイでレジスタ変数の値を確認します。

- name: レジスタ変数を確認するプレイ
  hosts: node1
  gather_facts: no

  tasks:
  - name: Linux コマンドを実行
    command: 'ls -a'
    register: result
  - name: レジスタ変数の確認
    debug:
      var: result

実行結果です。レジスタ変数 result が表示されています。

[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml register-variables.yml

PLAY [レジスタ変数を確認するプレイ] ***************************************************************************************************************************************

TASK [Linux コマンドを実行] ****************************************************************************************************************************************
changed: [node1]

TASK [レジスタ変数の確認] ********************************************************************************************************************************************
ok: [node1] => {
    "result": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": true,
        "cmd": [
            "ls",
            "-a"
        ],
        "delta": "0:00:00.007731",
        "end": "2020-05-09 23:03:17.472226",
        "failed": false,
        "rc": 0,
        "start": "2020-05-09 23:03:17.464495",
        "stderr": "",
        "stderr_lines": [],
        "stdout": ".\n..\n.ansible\n.bash_history\n.bash_logout\n.bash_profile\n.bashrc\n.ssh",
        "stdout_lines": [
            ".",
            "..",
            ".ansible",
            ".bash_history",
            ".bash_logout",
            ".bash_profile",
            ".bashrc",
            ".ssh"
        ]
    }
}

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[vagrant@ansible ansible-files]$ cat register-variables.yml

レジスタ変数の主な内容です。

changed
  • タスクの実行で対象ノードが変更されたか否かの結果が設定されます

    • true : 対象ノードが変更されました
    • false : 対象ノードが変更されませんでした
failed
  • 対象ノードでタスクの実行が失敗したか否かの結果が設定されます

    • true : タスクの実行が失敗しました
    • false : タスクの実行が失敗しませんでした
msg
  • モジュール実行時のメッセージが設定されます
rc
  • return code の略

  • タスクの終了ステータス(戻り値)が設定されます

    • 0 : 成功
    • 1 : 失敗(使用するモジュールなどにより 1 以外のこともある)
skipped
  • 対象ノードでタスクの実行がスキップされたか否かの結果が設定されます

    • true : タスクの実行がスキップされました
    • false : タスクの実行がスキップされませんでした
stderr
  • 標準エラー出力に出力された文字列が設定されます
  • 改行は改行文字nで表され、 1 行で設定されます
stderr_lines
  • stderr を 1 行ごとに分割した内容(= stderr の内容をリストで格納したもの)です
stdout
  • 標準出力に出力された文字列です
  • 改行は改行文字nで表され、 1 行で設定されます
stdout_lines
  • stdout を 1 行ごとに分割した内容(= stdout の内容をリストで格納したもの)です

Ansible のエラーハンドリング

Ansible は次のいずれかの方法でエラーハンドリングします。

エラーを無視する

タスクにignore_errorsディレクティブを指定すると、タスクでエラーが発生してもそのエラーを無視します。

/bin/falseコマンドを実行するタスクにignore_errorsディレクティブとregisterディレクティブを指定して実行します。

- name: task error play
  hosts: all
  gather_facts: no

  tasks:
  - name: task-1
    debug:
  - name: task-2
    command: /bin/false
    ignore_errors: yes
    register: result
    when: inventory_hostname == "node2"
  - name: task-3
    debug:
      var: result

実行結果です。管理対象ホスト node2 がタスク "task-2" の実行時エラーを無視したので...ignoringが表示されました。エラーを無視したので node2 もタスク "task-3" を実行しています。

タスク "task-3" は "task-2" の実行結果の表示です。 node2 に"failed": true,"rc": 1,の表示があり、エラーが発生したことがわかります。RECAP にignored=1の表示があり、ここでもエラーを無視したことがわかります。

[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml task-error.yml

PLAY [task error play] **************************************************************************************************************************************

TASK [task-1] ***********************************************************************************************************************************************
ok: [node1] => {
    "msg": "Hello world!"
}
ok: [node3] => {
    "msg": "Hello world!"
}
ok: [node2] => {
    "msg": "Hello world!"
}

TASK [task-2] ***********************************************************************************************************************************************
skipping: [node1]
skipping: [node3]
fatal: [node2]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": ["/bin/false"], "delta": "0:00:00.005399", "end": "2020-05-09 23:40:32.337710", "msg": "non-zero return code", "rc": 1, "start": "2020-05-09 23:40:32.332311", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring

TASK [task-3] ***********************************************************************************************************************************************
ok: [node1] => {
    "result": {
        "changed": false,
        "skip_reason": "Conditional result was False",
        "skipped": true
    }
}
ok: [node3] => {
    "result": {
        "changed": false,
        "skip_reason": "Conditional result was False",
        "skipped": true
    }
}
ok: [node2] => {
    "result": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": true,
        "cmd": [
            "/bin/false"
        ],
        "delta": "0:00:00.005399",
        "end": "2020-05-09 23:40:32.337710",
        "failed": true,
        "msg": "non-zero return code",
        "rc": 1,
        "start": "2020-05-09 23:40:32.332311",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "",
        "stdout_lines": []
    }
}

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
node2                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1
node3                      : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

[vagrant@ansible ansible-files]$

エラーとして扱う条件を設定し、条件に合致したときだけエラーとして扱う

エラーハンドリング」で説明したとおり、Ansible は戻り値が 0 以外の時にエラーが発生したと判断し処理します。多くの場合、これで問題ありません。しかし、次のような場合に問題が生じます。

diffコマンドを使用してファイルの内容が処理の前後で差異が発生していることを確認する」

diffコマンドの戻り値です。

  • 2 つのファイルの内容に差異がなかったとき → 0
  • 2 つのファイルの内容に差異があったととき → 1
  • ファイルのどちらかまたは両方が存在しないとき → 2

処理としては差異が発生している状態(= 戻り値が 1 )が正しく、それ以外は誤りになります。このようなときは、タスクにレジスタ変数とfailed_whenディレクティブを組み合わせて、エラーとして扱う条件を設定します。設定した条件を満たすとき、タスクはエラーとして判断し処理します。

次のプレイでfailed_whenディレクティブの動作を確認します。

- name: 2 つのファイルを比較する
  hosts: node1
  gather_facts: no

  tasks:
  - name: ファイルの比較
    command: 'diff file-1 file-2'
    register: diff_result
    failed_when: diff_result['rc'] != 1
  - name: レジスタ変数の内容確認
    debug:
      var: diff_result

ファイルの内容が異なる場合の実行ログです。本来ならタスク "ファイルの比較" でエラーになり処理が中断されますが、failde_whenディレクティブでエラーの条件を変更しているため最後まで処理できました。

[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml file-diff.yml

PLAY [2 つのファイルを比較する] ****************************************************************************************************************************************

TASK [ファイルの比較] **********************************************************************************************************************************************
changed: [node1]

TASK [レジスタ変数の内容確認] ******************************************************************************************************************************************
ok: [node1] => {
    "diff_result": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": true,
        "cmd": [
            "diff",
            "file-1",
            "file-2"
        ],
        "delta": "0:00:00.006412",
        "end": "2020-05-10 13:25:45.035609",
        "failed": false,
        "failed_when_result": false,
        "msg": "non-zero return code",
        "rc": 1,
        "start": "2020-05-10 13:25:45.029197",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "1c1\n< abc\n---\n> xyz",
        "stdout_lines": [
            "1c1",
            "< abc",
            "---",
            "> xyz"
        ]
    }
}

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[vagrant@ansible ansible-files]$

ファイルの内容が同じ場合の実行ログです。タスク "ファイルの比較" のエラーメッセージ内に"rc": 0が含まれています。本来ならエラーにならないのですが、failed_whenディレクティブに設定したエラーとして扱う条件に合致したためエラーとなり処理を中断しました。

[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml file-diff.yml

PLAY [2 つのファイルを比較する] ****************************************************************************************************************************************

TASK [ファイルの比較] **********************************************************************************************************************************************
fatal: [node1]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": ["diff", "file-1", "file-2"], "delta": "0:00:00.004663", "end": "2020-05-10 13:30:46.133139", "failed_when_result": true, "rc": 0, "start": "2020-05-10 13:30:46.128476", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

[vagrant@ansible ansible-files]$

ファイル file-2 が存在しない場合の実行ログです。タスク "ファイルの比較" のエラーメッセージ内に"rc": 2が含まれています。failed_whenディレクティブに設定したエラーとして扱う条件に合致したためエラーとなり処理を中断しました。

[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml file-diff.yml

PLAY [2 つのファイルを比較する] ****************************************************************************************************************************************

TASK [ファイルの比較] **********************************************************************************************************************************************
fatal: [node1]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": ["diff", "file-1", "file-2"], "delta": "0:00:00.007034", "end": "2020-05-10 13:34:52.272081", "failed_when_result": true, "msg": "non-zero return code", "rc": 2, "start": "2020-05-10 13:34:52.265047", "stderr": "diff: file-2: No such file or directory", "stderr_lines": ["diff: file-2: No such file or directory"], "stdout": "", "stdout_lines": []}

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

[vagrant@ansible ansible-files]$

上述のプレイにignore_errorsディレクティブを指定しました。

- name: 2 つのファイルを比較する
  hosts: node1
  gather_facts: no

  tasks:
  - name: ファイルの比較
    command: 'diff file-1 file-2'
    register: diff_result
    failed_when: diff_result['rc'] != 1
    ignore_errors: yes
  - name: レジスタ変数の内容確認
    debug:
      var: diff_result

この場合はfailed_whenの条件に合致してエラーと判断した後、ignore_errorsディレクティブでエラーを無視します。ファイル file-2 が存在しないときの実行ログです。

[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml file-diff.yml

PLAY [2 つのファイルを比較する] ****************************************************************************************************************************************

TASK [ファイルの比較] **********************************************************************************************************************************************
fatal: [node1]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": ["diff", "file-1", "file-2"], "delta": "0:00:00.006167", "end": "2020-05-10 13:36:33.737414", "failed_when_result": true, "msg": "non-zero return code", "rc": 2, "start": "2020-05-10 13:36:33.731247", "stderr": "diff: file-2: No such file or directory", "stderr_lines": ["diff: file-2: No such file or directory"], "stdout": "", "stdout_lines": []}
...ignoring

TASK [レジスタ変数の内容確認] ******************************************************************************************************************************************
ok: [node1] => {
    "diff_result": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": true,
        "cmd": [
            "diff",
            "file-1",
            "file-2"
        ],
        "delta": "0:00:00.006167",
        "end": "2020-05-10 13:36:33.737414",
        "failed": true,
        "failed_when_result": true,
        "msg": "non-zero return code",
        "rc": 2,
        "start": "2020-05-10 13:36:33.731247",
        "stderr": "diff: file-2: No such file or directory",
        "stderr_lines": [
            "diff: file-2: No such file or directory"
        ],
        "stdout": "",
        "stdout_lines": []
    }
}

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1

[vagrant@ansible ansible-files]$

エラーハンドリングの演習

管理対象ホスト node1 にアカウントを追加します。ただし、/etc/passwdに登録済みのアカウントは追加や更新してはいけません。登録済みのときはエラーで処理を中断します。

  1. アドホックコマンドで/etc/passwdにアカウントが登録されているとき、登録されていないときの戻り値を確認します。
[vagrant@ansible ansible-files]$ ansible node1 -i hosts.yml -m shell -a 'cat /etc/passwd | grep vagrant'
node1 | CHANGED | rc=0 >>
vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash
[vagrant@ansible ansible-files]$ ansible node1 -i hosts.yml -m shell -a 'cat /etc/passwd | grep hogehoge'
node1 | FAILED | rc=1 >>
non-zero return code
[vagrant@ansible ansible-files]$

ちなみに

パイプやリダイレクトなどを使用するときはshellモジュールを使用します。commandモジュールはパイプなどを処理できません。

  1. (ざっくりとした)処理を考えます。
  • /etc/passwdファイルに登録したいアカウントが登録されているかどうか確認

    • shellモジュール
    • registerディレクティブ
    • failed_whenディレクティブ
  • アカウントを登録

    • userモジュール
    • becomeディレクティブ(targetsセクションで定義していたらタスクでの指定は不要)
  1. プレイを作成・実行します。
  2. アドホックコマンドで管理対象ホスト node1 の/etc/passwdファイルの内容を参照し、アカウントが登録されていることを確認します。
  3. 登録済みアカウントに変更し、エラーで処理を中断することを確認します。

解答

[vagrant@ansible ansible-files]$ ansible node1 -i hosts.yml -m shell -a 'cat /etc/passwd | grep vagrant'
node1 | CHANGED | rc=0 >>
vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash
[vagrant@ansible ansible-files]$ ansible node1 -i hosts.yml -m shell -a 'cat /etc/passwd | grep hogehoge'
node1 | FAILED | rc=1 >>
non-zero return code
[vagrant@ansible ansible-files]$ vim useradd.yml
[vagrant@ansible ansible-files]$ cat useradd.yml
- name: アカウントを登録する
  hosts: node1
  gather_facts: no

  tasks:
  - name: アカウントが登録されているか確認
    shell: 'cat /etc/passwd | grep hogehoge'
    register: result
    failed_when: result['rc'] != 1
  - name: アカウント登録
    user:
      name: "hogehoge"
      state: present
    become: yes
[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml useradd.yml

PLAY [アカウントを登録する] *******************************************************************************************************************************************

TASK [アカウントが登録されているか確認] *************************************************************************************************************************************
changed: [node1]

TASK [アカウント登録] **********************************************************************************************************************************************
changed: [node1]

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[vagrant@ansible ansible-files]$ ansible node1 -i hosts.yml -m shell -a 'cat /etc/passwd | grep hogehoge'
node1 | CHANGED | rc=0 >>
hogehoge:x:1001:1001::/home/hogehoge:/bin/bash
[vagrant@ansible ansible-files]$ vim useradd.yml
[vagrant@ansible ansible-files]$ cat useradd.yml
- name: アカウントを登録する
  hosts: node1
  gather_facts: no

  tasks:
  - name: アカウントが登録されているか確認
    shell: 'cat /etc/passwd | grep vagrant'
    register: result
    failed_when: result['rc'] != 1
  - name: アカウント登録
    user:
      name: "vagrant"
      state: present
    become: yes
[vagrant@ansible ansible-files]$ ansible-playbook -i hosts.yml useradd.yml

PLAY [アカウントを登録する] *******************************************************************************************************************************************

TASK [アカウントが登録されているか確認] *************************************************************************************************************************************
fatal: [node1]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": "cat /etc/passwd | grep vagrant", "delta": "0:00:00.009987", "end": "2020-05-10 14:45:01.615450", "failed_when_result": true, "rc": 0, "start": "2020-05-10 14:45:01.605463", "stderr": "", "stderr_lines": [], "stdout": "vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash", "stdout_lines": ["vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash"]}

PLAY RECAP **************************************************************************************************************************************************
node1                      : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

[vagrant@ansible ansible-files]$