Ansibleのcheck modeを正しく使うために気を付けたいこと

Ansibleで既存環境に手を加える時、あらかじめどのような変更が加わるかを把握できればいいですよね。
そんな時に使えるのが、check modeです。

#check modeは簡単？
使い方はとても簡単、コマンドに`--check`をつけるだけです。

```
$ ansible-playbook -i hosts site.yml --check
```

既存の本番環境にAnsibleを流す前はcheck modeでまず実行すれば、変更箇所を簡単に把握することができる！
じゃあ作ったPlaybookに何でもかんでも`--check`を付ければ完璧！***というわけにはいきません。***
check modeを使いたい時は、Playbookの作り方に気を付けましょう。

#shell/commandモジュールのcheck modeで気を付けたいパターン

###1.変更予定箇所がskippedと判定されてしまうパターン

これは基本的なことなのでみんな知っているかもしれませんが、一応。
`shell:`と`command:`タスクはcheck modeの場合Ansibleの仕様により無条件でskippedとなります。
そのため、以下のPlaybookをcheck modeで実行した場合、changedにはなりません。

```yaml:1.yml
- shell: /usr/bin/some-command
  register: version
  ignore_errors: yes
```

check modeの場合：

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

通常実行の場合：

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

これは、shell/commandモジュールがcheck modeに対応していないためですね。
skippedになるのは仕様であるため、`changed_when`等を付けても意味はありません。
そういうものだと思って受け入れましょう。

###2.変更予定かskip予定かわからないパターン

前述のパターンの場合は常に実行されると思っておけばいいですが、以下のように`shell:`タスクに実行条件を指定するパターンはまた違います。
これは(Ansibleの画面上では)タスクが実際には実行されるのかスキップされるのかわかりません。

```yaml:2-ng.yml
- name: バージョンの確認
  shell: /usr/bin/some-command --version
  register: version
  changed_when: no
  ignore_errors: yes
  check_mode: no

- name: 上書きインストール
  shell: /tmp/some-software/install.sh
  when: >
    version.stdout is defined
    and '1.0' not in version.stdout
```

これを実行した場合、`version`に登録されたバージョンが1.0であろうとなかろうと、check modeで実行した場合の「上書きインストール」は必ずskippedとなります。
果たして/tmp/some-software/install.shが実行されるのかどうかは、`--check`を外して実際にPlaybookを流すまでわかりません。

対処としては、以下のように`shell:`タスクを別ファイルにする書き方が考えられます。
私の我流なので、もっといいやり方があるかもしれません。

```yaml:2-ok-1.yml
- name: バージョンの確認
  shell: /usr/bin/some-command --version
  register: version
  changed_when: no
  ignore_errors: yes
  check_mode: no

- name: 上書きインストール処理をimport
  import_tasks: 2-ok-2.yml
  when: >
    version.stdout is defined
    and '1.0' not in version.stdout
```

```yaml:2-ok-2.yml
- name: 上書きインストール
  shell: /tmp/some-software/install.sh
```

こうしておけば、`version`が1.0だった場合はそもそも処理フローが「上書きインストール」 を通らないので、`--check`を外した場合でも「上書きインストール」は実行されないと判断することができます。

###3.変更されない箇所がchanged(またはskipped)に判定されてしまうパターン

また別のパターンとして、以下を考えてみましょう。
あるコマンドの実行結果を元に`file:`タスクを実行するPlaybookがあったとします。

```yaml:3-ng.yml
- name: バージョンの確認
  shell: /usr/bin/some-command --version
  register: version
  changed_when: no
  ignore_errors: yes

- name: 既存バージョンの削除
  file:
    name: /some/dir
    state: absent
  when: >
    version.stdout is defined
    and '1.0' not in version.stdout
```

このPlaybookはcheck modeでは期待通りの動作しません。

なぜならば、check modeではshellはskippedになり、変数`version`には何も格納されないからです。
たとえ1.0のソフトがすでにインストールされていたとしても、check modeではインストールされていないとみなされて「既存バージョンの削除」がchangedになります。
え？削除されちゃうの！？と焦ることになります。

この問題を回避するためには、タスクにcheck_modeオプションを設定します。
`check_mode: no`が設定された場合、そのタスクだけ`--check`を無視して強制実行することができます。
先ほどのPlaybookの場合、以下のように修正すればOKです。

```yaml:3-ok.yml
- name: バージョンの確認
  shell: /usr/bin/some-command --version
  register: version
  changed_when: no
  ignore_errors: yes
  check_mode: no

- name: 既存バージョンの削除
  file:
    name: /some/dir
    state: absent
  when: >
    version.stdout is defined
    and '1.0' not in version.stdout
```

`changed_when: no`と`check_mode: no`を設定した場合、check modeであっても実行されて、かつchangedにすらならないので実行するコマンドが絶対にシステムに影響を与えないことを担保する必要があります。


#長々と書きましたが
Ansibleでは冪等性という考え方があるので、そこをちゃんと考慮したPlaybookを作ってさえいればcheck modeでいくつchangedになろうがいくつskippedになろうが、最終的には同じ状態に収束するはずなのです。
なのでcheck modeはあくまでおまけ程度に考えておいた方がいいのかもしれません。

冪等性を担保するためにはそれはそれで色々考える必要があるのですが、それはまた別の機会に書きます。

#参考
https://docs.ansible.com/ansible/latest/user_guide/playbooks_checkmode.html
https://medium.com/opsops/understanding-ansibles-check-mode-299fd8a6a532