Ansible を使っていて、不思議な動作に遭遇したので深入りしてみました。
今回の落とし穴
同じグループに属するホストのうち、特定のホストのみで実行したいタスクがある、ということがたまにあります。そういうとき、私はインベントリファイルでホスト変数を定義して、この変数で処理を分岐させる、ということをしていました。
例えば、Webserversグループのうち、Webサーバ1番のみで実行したいタスクがある場合、インベントリファイルを以下のように書きます。
[webservers] web1.example.com var1=true web2.example.com var1=false
そして、以下のように when ステートメントを使えば、web1 のみでコマンドが実行されます。
- command: /usr/bin/do_something.sh when: var1
しかし、ホストの追加時に変数 var1 の定義をさぼってしまうと、実行時に「var1 が未定義である」というエラーが発生し、playbook の実行が止まってしまいます。
[webservers] web1.example.com var1=true web2.example.com var1=false web3.example.com
それならタスクの方で var1 の中身を調べる前に、var1 が定義済みかどうかを調べればよいのでは?と思い、タスクを以下のように書きなおしてみました。
- command: /usr/bin/do_something.sh when: var1 is defined and var1
しかし、これを実行すると、web1(var1=true)だけでなくて web2(var1=false)でもこのコマンドが実行される という問題が発生しました。えっ……? どういうこと?!
この問題の原因っぽい(でも原因の説明にならない)もの
この問題の原因っぽいものとして、「インベントリファイルの中で定義した変数は、すべて string として解釈される」という Ansible の仕様があります。
Values passed in using the key=value syntax are interpreted as strings. Use the JSON format if you need to pass in anything that shouldn’t be a string (Booleans, integers, floats, lists etc).
Variables — Ansible Documentation
しかし、これは when: var1
と書いた場合と when: var1 is defined and var1
と書いた場合で結果が違ってしまうことの説明にはなりません。
いろいろなパターンを試す
こうなってくると when 自体どこまで信用していいのか不安になってきたので、いろいろなパターンで動作検証してみました。また、Ansible 1 系から 2 系へのアップグレード中にこの問題に遭遇したため、Ansible 1.9.4 と 2.0.1 の両方で検証しました。
動作検証の方法
3種類のインベントリファイルを用意しました。違いは、変数 var1 のみです。
inventory_true
localhost var1=true
inventory_false
localhost var1=false
inventory_null
localhost var1=
これらのインベントリファイルを使って、色々な when ステートメントを記載した playbook を実行しました。タスクの詳細は↓をご覧ください。
ansible-2.0-sample/main.yml at master · muziyoshiz/ansible-2.0-sample
動作検証の結果
Ansible 1.9.4 と 2.0.1 の両方で、ほとんど同じ結果になりました。ただし、var1=
と書いた場合は、Ansible 1.9.4 だと when: var1
の場合に限り、fatal エラーが出て異常終了しました。
when statement | var1=true (1.9/2.0) | var1=false (1.9/2.0) | var1= (1.9) | var1= (2.0) |
---|---|---|---|---|
when: var1 is defined |
ok | ok | ok | ok |
when: var1 |
ok | skipping | fatal | skipping |
when: "{{ var1 | bool }}" |
ok | skipping | skipping | skipping |
when: var1 == true |
skipping | skipping | skipping | skipping |
when: var1 == "true" |
ok | skipping | skipping | skipping |
when: var1 == false |
skipping | skipping | skipping | skipping |
when: var1 == "false" |
skipping | ok | skipping | skipping |
when: var1 is defined and var1 |
ok | ok | skipping | skipping |
when: var1 is defined and {{ var1 | bool }} |
ok | skipping | skipping | skipping |
when: var1 is defined and var1 == true |
skipping | skipping | skipping | skipping |
when: var1 is defined and var1 == "true" |
ok | skipping | skipping | skipping |
when: var1 is defined and var1 == false |
skipping | skipping | skipping | skipping |
when: var1 is defined and var1 == "false" |
skipping | ok | skipping | skipping |
この結果をざっくりまとめると、以下のようになります。
when: var1
と書いた場合は、var1 が文字列 "true" なら true、"false" なら false と解釈される。これは、普通の人が期待しそうな動作(と思う)。when: var1 is defined and var1
と書いた場合は、var1 に文字列が何かしら設定されていれば、それが "true" でも "false" でも true と判定してしまう。- この奇妙な動作は、Ansible 2.0 へのバージョンアップが原因ではない。
{{ var1 | bool }}
と boolean への変換を明示すれば、期待通りに動作する。- var1 を文字列 "true", "false" と比較すれば、期待通りに動作する。
- その一方、var1 と、boolean 型の true, false との比較は、常に false になる。
true/false を True/False に変えた場合の動作(2016-04-01追記)
この件について GitHub で issue として報告してみたところ、true じゃなくて True(先頭が大文字)と書け、という返信をもらいました。
Inconsistent behavior of when statement · Issue #15218 · ansible/ansible
そこで、
inventory_large_true
localhost var1=True
inventory_large_false
localhost var1=False
を新たに用意して、改めて動作確認してみた結果はこちら。動作に一貫性がなさそうに見える箇所に (inconsistent) と書いています。
when statement | var1=true | var1=false | var1=True | var1=False |
---|---|---|---|---|
when: var1 is defined |
ok | ok | ok | ok |
when: var1 |
ok | skipping (inconsistent) | ok | skipping |
when: var1 | bool |
ok | skipping | ok | skipping |
when: var1 == "true" |
ok | skipping | skipping | skipping |
when: var1 == "True" |
skipping | skipping | skipping | skipping |
when: var1 == "false" |
skipping | ok | skipping | skipping |
when: var1 == "False" |
skipping | skipping | skipping | skipping |
when: var1 is defined and var1 |
ok | ok (inconsistent) | ok | skipping |
when: var1 is defined and var1 | bool |
ok | skipping | ok | skipping |
when: var1 is defined and var1 == "true" |
ok | skipping | skipping | skipping |
when: var1 is defined and var1 == "True" |
skipping | skipping | skipping | skipping |
when: var1 is defined and var1 == false |
skipping | skipping | skipping | ok |
when: var1 is defined and var1 == "false" |
skipping | ok | skipping | skipping |
when: var1 is defined and var1 == "False" |
skipping | skipping | skipping | skipping |
この結果を見る限り、ホスト変数として True/False と書くと boolean として解釈されるようです。また、文字列としての "True"/"False" とは一致しなくなりました。
こうなると、ホスト変数に true/false と書いた場合に、どうして when: var1
だけは変数を boolean として扱ってくれる(扱ってしまう)のか気になってきます。中途半端に型の概念が導入されていて、個人的にはなんだか気持ち悪くなってきました……。
結論:特定のホストのみで実行したいタスクがある場合、結局どうすればいいのか(2016-04-01修正)
インベントリファイルでホスト変数を定義するときに、True/False の先頭を大文字にしないと正しく動作しない こともある と認識した上で、playbook を以下のように書くのが良さそうです。
inventory
host1 var1=True host2 host3
playbook
- command: /usr/bin/do_something.sh when: var1 is defined and var1
ただ、処理を分岐させたい場所すべてで、var1 is defined and
という冗長な記載が必要になってしまうのが、嫌なところです。かといって、この記載をサボると、var1 の定義がないホスト(上記の host2, host3)で異常終了してしまいます。
代案としては、記載を簡潔にするために、変数定義の有無だけをフラグとして使う方法も考えられます。つまり、インベントリファイルには host1 var1=True
と書くものの、playbook には
- command: /usr/bin/do_something.sh when: var1 is defined
と書いて、変数 var1 の中身は検査しないという方法です。ただ、この方法は、事情を知らない人には「var1=False
と修正すれば動作を変更できる」と誤解される危険性があります。なかなか悩ましいところです……。