Edited at

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

More than 1 year has passed since last update.


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

Ansibleのcommand系で任意のコマンドが実行される場合、Ansibleで事前にそれを予測して何かの対策を立てることはできない。

任意のコマンドなので、どんなコマンドが実行されるのか分からないからだ。

それで、Ansibleではcommand系について二つの戦略を取っている。


  • コマンドを実行するタスクは常にchangedになる。

  • コマンドの実行結果が0以外のものはfailedになる。

これはAnsibleだけではない、Chefも同様である。

とてもシンプルだが、全てのコマンドに対応ができるベストの戦略とも言える。


冪等性を保つ為には

command系は使わない方がいいだろう。

yumやgitなどのmoduleが使える場合はそれらを使った方がいいのは当たり前だ。

しかし、moduleが提供されてない場合はcommand系を使うしかない。

また、場合によってはcommand系(特にshell)を使った方がplaybookがもっとシンプルになる。

こんな時は、command系を使うべきだ。

command系を使っても冪等性を保つ方法はある。

Ansibleが自動でやってくれないだけだ。

command系で冪等性を保つ方法としてよく使うのがregisterとwhenの組み合わせである。

僕の環境で使っている次のサンプルを見よう。

  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コードを追加して確認して見よう。

+  12 - debug: msg={{ plugin_list }}

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

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

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なので、ここには定義しない。

vagrant_plguins:

- sahara
- vagrant-cachier

whenは条件がtrueの場合はタスクを実行するが、falseの場合はタスクをskipする。

vagrant plugin listの中にvagrant_pluginsに指定したpluginが無い場合はタスクが実行される。

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

  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}}"

homebrew_services:

- mariadb
- mongodb
- postgresql

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

まず、サービスの状態を確認する必要がある。

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)だけだ。

カラム名は要らないから排除し、サービス名とその状態だけを取得するコマンドで組み合わせる。

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で上書きする。