Ansible の Jinja2 を活用する
これは Ansible Advent Calendar 2015 の 12 月 7 日 (月) の記事です。
Ansible では組み込みのテンプレート言語として Jinja2 が利用できます。
この記事では Ansible の Jinja2 を駆使して リストやディクショナリを含む複雑なデータ構造を操作する方法について 追求してみたいと思います。
Ansible の Jinja2 ドキュメント
Ansible のドキュメントでは、 Jinja2 の利用については次のページで軽く触れられている程度で、 あまり参考になる情報は得られません。
本格的に使いこなしたければ Jinja2 のドキュメントを読む必要がありますが Ansible からの利用に関しては次のページの情報でほとんど間に合います。
式の評価とステートメント
ドキュメントからだけでは読み取るのが難しい事実として、実は
Python の基本型
(int
, str
, list
, dict
など) の大半のメソッドは
Jinja2 からも呼び出せるというものがあります。
次のプレイブックを実行して確認してみてください。
実行例:
','.join(list)
に対する list | join(',')
のように
Jinja2 フィルタとして実装されている機能もありますので、
呼べるからといってなんでも呼び出すのではなく
Jinja2 のドキュメントを調べてから使ったほうがいいでしょう。
list.append(1)
といった
Python のリスト操作の副作用を伴うメソッドも使えるのですが、
重要な注意点として Jinja2 では {% list.append(1) %}
のように式の評価を行うだけのステートメントは許されていません。
{% %}
ステートメントには必ず
List of Control Structures
で挙げられているタグを使わなくてはなりません。
これに do
を追加して {% do list.append(1) %}
と書けるようにする
Expression Statement
という拡張もあるらしいのですが Ansible では有効化されていませんので、面倒ですが
{% if list.append(1) %}{% endif %}
や {% set _ = list.append(1) %}
のように他のステートメントの中で評価したい式を書く必要があります。
リスト・ディクショナリの操作
list.append()
や list.extend()
のように
破壊的ながらも基本的なリスト操作を行う式が使えますので、
これに for
や if
のステートメントを組み合わせれば、
Python では map()
や filter()
や reduce()
で書くような複雑なリスト操作も Jinja2 で実現できるようになります。
次のプレイブックを見てください。
実行例:
最後のタスクはうまく働かない例です。
これまでの例を見て、単に
{% set list = list + [i] %}
や {% set num = num + i %}
でもいいんでないかと思われる方もいらっしゃったかもしれませんが、
Jinja2 ではなぜか {% for %}{% endfor %}
のなどブロックを抜けると
そのブロックの中で更新した変数の値が元に戻ってしまうので、
この方法は使えないのです。
変更を反映させるには list.append()
のような式を評価する必要があります。
またリストだけでなく dict.iteritems()
や dict.update()
といったディクショナリのメソッドも使えます。
次のセクションでこれらの利用例を見てみましょう。
利用例
ここでは StackExchange サイト (StackOverflow, ServerFault, Unix & Linux) や Google Groups (ansible-project, ansible-devel) に投稿された質問で、私が回答したものから具体例をあげたいと思います。
リストの連結
List of all ansible_ssh_host in group より。 次のようなインベントリファイルがあるときに、
master_servers
グループに属するホストの ansible_ssh_host
から次のような Mesos+ZooKeeper 用の URL 文字列を生成したい。
zk://10.0.45.11:2181,10.0.45.12:2181,10.0.45.13:2181/mesos
プレイブック:
実行例:
ディクショナリのフィルタ
Filter list of dicts by dict key より。次のような配列を要素として持つディクショナリから、
{ foo: [A, B, C], bar: [], baz: [D, E] }
配列が空のものを除いたディクショナリを得たい。
{ foo: [A, B, C], baz: [D, E] }
プレイブック:
ディクショナリの全要素を扱うには
for k, v in dict.iteritems()
が便利です。
ディクショナリの更新には dict.update()
が使えます。
実行例:
集合の連結
Wildcard in Ansible in template より、すこし複雑な例。また ZooKeeper がらみのお題ですが偶然です。
インベントリファイル (hosts):
tag_Name_Zookeeper
で始まるグループに属するホスト (重複あり) から
次のような文字列を生成したい。
a:2181,b:2181,c:2181,d:2181
プレイブック:
ディクショナリのフィルタには str.startswith()
というメソッドが使えます。
またディクショナリを集合の代わりに使用し、
最後に dict.keys()
でキー文字列だけの配列を取り出して
Jinja2 フィルタでソート・連結しています。
実行例:
SSH ユーザ・ホスト公開鍵の配布
ここで紹介するプレイブックは hosts
で指定した全ホストにおいて、
become_user
で指定したユーザについて次のことを行います。
- 各ホストでユーザの SSH 秘密鍵
~/.ssh/id_rsa
がなければ生成する (パスフレーズはなし) - 各ホストの
~/.ssh/authorized_keys
に 各ホストのユーザ SSH 公開鍵~/.ssh/id_rsa.pub
を追加する - 各ホストの
~/.ssh/known_hosts
に 各ホストのホスト SSH 公開鍵を追加する。
これにより任意のホスト間でパスフレーズなしの SSH ログインをセットアップします。
プレイブック:
プレイブックの実行対象となるホスト名の配列は play_hosts
で得られます。
各ホストでユーザの SSH 秘密鍵情報を set_fact
モジュールで保存し、
それを各ホストから hostvars
を使って参照しています。
実行例:
become_user
が自分と異なる場合は、
ansible-playbook
に -K
オプションを追加し
sudo パスワードを指定するようにしてください。
$ ansible-playbook -i distpubkey-hosts.txt distpubkey-playbook.yml -K
SUDO password:
PLAY [all] ******************************************************************
...
このプレイブックは便利だと思うので、 後ほど一般的なロールにして Ansible Galaxy に登録しようかと思っています。
まとめ
Ansible における Jinja2 テンプレートを Python 層まで使ってより深く活用する方法を紹介しました。
2015/12/07 07:59:33 JST