エラーハンドリング¶
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
に登録済みのアカウントは追加や更新してはいけません。登録済みのときはエラーで処理を中断します。
- アドホックコマンドで
/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
モジュールはパイプなどを処理できません。
- (ざっくりとした)処理を考えます。
/etc/passwd
ファイルに登録したいアカウントが登録されているかどうか確認shell
モジュールregister
ディレクティブfailed_when
ディレクティブ
アカウントを登録
user
モジュールbecome
ディレクティブ(targets
セクションで定義していたらタスクでの指定は不要)
- プレイを作成・実行します。
- アドホックコマンドで管理対象ホスト node1 の
/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]$ 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]$