Ansibleのcommand系（command、shell）を使う時の冪等性を保つ方法について


## Ansibleのcommand系が取ってる戦略とは

Ansibleのcommand系で任意のコマンドが実行される場合、Ansibleで事前にそれを予測して何かの対策を立てることはできない。
任意のコマンドなので、どんなコマンドが実行されるのか分からないからだ。
それで、Ansibleではcommand系について二つの戦略を取っている。

* __コマンドを実行するタスクは常にchangedになる。__
* __コマンドの実行結果が０以外のものはfailedになる。__

これはAnsibleだけではない、Chefも同様である。
とてもシンプルだが、全てのコマンドに対応ができるベストの戦略とも言える。

## 冪等性を保つ為には

command系は使わない方がいいだろう。
yumやgitなどのmoduleが使える場合はそれらを使った方がいいのは当たり前だ。
しかし、moduleが提供されてない場合はcommand系を使うしかない。
また、場合によってはcommand系（特にshell）を使った方がplaybookがもっとシンプルになる。
こんな時は、command系を使うべきだ。
command系を使っても冪等性を保つ方法はある。
Ansibleが自動でやってくれないだけだ。
command系で冪等性を保つ方法としてよく使うのがregisterとwhenの組み合わせである。
僕の環境で使っている次のサンプルを見よう。

```ruby
  1 - name: set up vagrant
  2   shell: vagrant init
  3   args:
  4     creates: ~/.vagrant
  5
  6 - name: check if plugin is installed
  7   shell: vagrant plugin list | awk '{print $1}'
  8   register: plugin_list
  9   changed_when: false
 10   failed_when: false
 11
 12 - debug: msg={{ plugin_list.stdout.split('\n') }}
 13
 14 - name: install vagrant's plugins
 15   shell: vagrant plugin install {{ item }}
 16   when: not item in (plugin_list.stdout.split('\n'))
 17   with_items:
 18     - "{{ vagrant_plugins }}"
```

vagrantの環境を構築するplaybookである。
homebrewでvagrantを設置するタスクは他のplaybookにある。
4つのタスクで構成されている。
各タスクについて説明する。

* set up vagrant

vagrantを初期化して環境を構築する。しかし、~/.vagrantのディレクトリが存在する場合は、vagrantが初期化されたと判断してこのタスクは実行しない。
~/.vagrantのディレクトリはvagrant initを実行するとき生成されるものだ。
createsはcommandやshellを使うとき冪等性を保つ為によく使われる。

* check if plugin is installed

vagrantにはいくつか有用なpluginが存在する。
Ansibleでvagrantのpluginを設置する為には今のところcommand系を使うしかない。
その時の冪等性を保つ為にvagrantのpluginの一覧を取得して変数に保存して置く。

Ansibleのタスクは変更があったときchangedになり、changedの場合はタスクが実行される。
変更がなければ、okになりタスクは実行されない。
またタスクが失敗した場合はfailedになり、playbookの実行を止まる。

__changed_whenとfailed_whenはchangedとfailedの条件を上書きするものだ。__
__両方falseになっているので、changedにならないしタスクが失敗しない。__
__冪等性を保つためのregisterで変数を定義する時に定番ように使われる。__

* debug

予想通りのデータが保存されているのか確認する。
jinja2の中ではpythonを使える。
plugin_list.stdoutはワンラインでstringの結果を返す。
各値が改行文字（\n）で繋がっている文字列になる。

次のようにdebugコードを追加して確認して見よう。

```diff
+  12 - debug: msg={{ plugin_list }}
+  13 - debug: msg={{ plugin_list.stdout }}
   14 - debug: msg={{ plugin_list.stdout.split('\n') }}
```

playbookを実行すると次のような結果が表示される。

```sh
TASK [mac : debug] ******************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": {
        "changed": false,
        "cmd": "vagrant plugin list | awk '{print $1}'",
        "delta": "0:00:00.804493",
        "end": "2017-10-08 23:33:40.952168",
        "failed": false,
        "failed_when_result": false,
        "rc": 0,
        "start": "2017-10-08 23:33:40.147675",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "sahara\nvagrant-cachier\nvagrant-share",
        "stdout_lines": [
            "sahara",
            "vagrant-cachier",
            "vagrant-share"
        ]
    }
}

TASK [mac : debug] ******************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "sahara\nvagrant-cachier\nvagrant-share"
}

TASK [mac : debug] ******************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        "sahara",
        "vagrant-cachier",
        "vagrant-share"
    ]
}
```

12行のdebugの結果からstdoutに注目する。
13行のdebugの結果がそれだ。
14行のdebugではsplitを用いて改行文字（'\n'）で割っている。

__実はplugin_list.stdout.split('\n')の代わりにplugin_list.stdout_linesを使えばいい。__
この記事の作成中に気づいたが、jinja2の中ではpythonを使えることを見せるため、そのままにした。
pythonの文法をそのまま使うのが、filterを使うことより使い勝手がいい。

* install vagrant's plugins

変数で設定されたvagrantのpluginをloopで回しながら、設置するタスクである。
次はvagrant_plguinsという変数に設定されたvagrantのpluginのリストだ。
vagrant-shareはデフォルトで設置されるpluginなので、ここには定義しない。

```ruby
vagrant_plguins:
  - sahara
  - vagrant-cachier
```

whenは条件がtrueの場合はタスクを実行するが、falseの場合はタスクをskipする。
vagrant plugin listの中にvagrant_pluginsに指定したpluginが無い場合はタスクが実行される。

もう一つのサンプルも見よう。

```ruby
  1 - name: check if services are running with homebrew
  2   shell: brew services list | awk 'NR>1 {print $1, $2}'
  3   register: service_list
  4   changed_when: false
  5   failed_when: false
  6
  7 - debug: msg={{ service_list.stdout_lines }}
  8
  9 - debug: msg={{ item + ' started' }}
 10   with_items:
 11     - "{{ homebrew_services}}"
 12
 13 - name: run services with homebrew
 14   shell: brew services start {{ item }}
 15   when: not (item + ' started') in service_list.stdout_lines
 16   with_items:
 17     - "{{ homebrew_services}}"
```

```ruby
homebrew_services:
  - mariadb
  - mongodb
  - postgresql
```

homebrewのサービス一覧を確認してhomebrew_servicesにて指定されたサービスが起動してない場合は、該当サービスを起動するplaybookである。
まず、サービスの状態を確認する必要がある。

```sh
brew services list
Name       Status  User     Plist
postgresql started devtopia /Users/devtopia/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
mariadb    started devtopia /Users/devtopia/Library/LaunchAgents/homebrew.mxcl.mariadb.plist
mongodb    started devtopia /Users/devtopia/Library/LaunchAgents/homebrew.mxcl.mongodb.plist
```

この一覧から必要な情報は実際のサービス名とその状態（started、stopped）だけだ。
カラム名は要らないから排除し、サービス名とその状態だけを取得するコマンドで組み合わせる。

```sh
brew services list | awk 'NR>1 {print $1, $2}'
postgresql started
mariadb started
mongodb started
```

サービス名（空白）startedの場合はサービスが起動されているので、このタスクをskipする。

## 要点

* ターゲットノードに影響を及ぼすタスクでcommand系を使う場合は、whenを用いて実行可否判定をする。
* そのwhenで使う変数をcommand系とregisterを組み合わせて用意する。
* この場合、条件式で使うための変数を作ることだけなので、ターゲットノードに影響はない。
* ターゲットノードに影響が無い変数保存用のタスクがcommand系を使ったとしてchanged、failedになるのは意味がない。
* それで、changed_whenとfailed_whenをfalseで上書きする。