Help us understand the problem. What is going on with this article?

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

More than 3 years have 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で上書きする。
park-jh
プルスタックエンジニアになれるまで頑張ろう。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away