読者です 読者をやめる 読者になる 読者になる

無印吉澤

ソフトウェア開発、運用管理(俗にいう DevOps)、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

Ansible でインベントリのホスト変数を when に渡した際の不思議な動作(Ansible 1.9/2.0 共通)

f:id:muziyoshiz:20160331232512p:plain:w300

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 と修正すれば動作を変更できる」と誤解される危険性があります。なかなか悩ましいところです……。