はじめに
Ansibleでちょっと困ったことがあって、なんとかならないか探したり試行錯誤したりしてみました。
下記、Undocumentedな動作かも知れませんのでバージョンを明記します。
ansible-playbook 2.9.24
で試した結果になります。
変数定義の値のベタ書きがとてもつらい
Ansible、便利ですよね。roleやtaskにあわせて構造化された変数を定義しておいてansible-playbook
コマンドで複数ホストに一度に設定を流し込めます。でもちょっと困ったことが起きたのです。
ふたつの環境で変数定義をある程度共有したい
ふわっとした例を書くと、たとえばDatabase serverとWeb serverの2つを用意して、互いのNICを接続するとします。実環境と仮想環境を用意して、仮想環境でテストした結果を実環境で、みたいなことを考えます。
変数定義のイメージとしてはこんなふう。
apt:
name: mysql-server
network_interfaces:
- name: ens4
ip_addresses:
- 192.168.0.10/24
apt:
name: nginx
network_interfaces:
- name: ens4
ip_addresses:
- 192.168.0.20/24
仮想環境が完全に実環境と同じにできればいいのですが、たとえばNICの名前が違ってたりすることは十分ありえます。たとえばこんなふうです。
環境 | Database Server | Web server |
---|---|---|
仮想環境 | ens4 | ens4 |
実環境 | enp1f1 | enp2f1 |
上記YAMLファイルでいうところのens4
の部分を変数にして、別の場所で定義した値で置き換えたい、ということです。
変数定義の中で同一識別子を繰り返し利用する場合、まとめて書き換えたい
上で説明した内容との合わせ技ですが、変数定義の中で特定ワードが繰り返し使用されるケースがあります。たとえば、あるインタフェースにIPアドレスを設定してACLを設定してNATを設定して、のようなパターンです。(それっぽく変数定義を書いてるだけで、これを読めるroleがあるというわけではありません、ねんのため)
network_interfaces:
- name: ens4
ip_addresses:
- 192.168.0.10/24
acl:
tables:
- name: Filter
stage: ingress
interfaces:
- ens4
rules:
- name: Rule1
priority: 100
l4_port: 80
action: forward
nat:
static:
- global_ip: 192.168.0.10
local_ip: 172.21.0.1
nat_type: dnat
interface: ens4
これらの例のようにホスト数や変数が少なくぱっと見てどこを書き換えればいいようなケースであれば、ファイルまるごとコピーして中身をちょこちょこ書き換えるのでも十分です。が、10も20もホストがある場合それぞれ50行とか100行とかある定義の中から書き換える場所を漏らさず見つけて書き換えるのはしんどいです。
YAMLアンカーを試してみた
YAMLアンカーは、ある値を定義してる箇所に &名前
のアンカーをつけて、*名前
でその値を引っ張ってくるというものです。
liesn_if: &lif ens4
apt:
name: nginx
network_interfaces:
- name: *lif
ip_addresses:
- 192.168.0.20/24
定義してる名前の他にアンカーの名前を書くのが煩わしいですが、この例は正しく*lif
がens4
に置き換わります。
が。
YAMLアンカーはどうやら同一ファイル内でしか機能しないようでした。つまり繰り返しの例のように何度も同じワードが出てくる場合、ファイルの先頭付近で定義しておけば後ろを書き換えなくて済むものの、別ファイルに定義をまとめたりはできないということです。
Jinja2的な参照
ネットの海を漂っても、テンプレートファイルを書いて置き換えるかtaskの中で変数を参照する例ばかり。できないのかなあとあきらめかけてたのですが、こんな書き方が変数定義の中でできるという記述が。
liesn_if: ens4
apt:
name: nginx
network_interfaces:
- name: "{{ listen_if }}"
ip_addresses:
- 192.168.0.20/24
やってみたら、できました。しかもこれ、ファイルが分かれていても問題なく動きます。Web serverを3台建てるようなケースで下記のようにできます。
apt:
name: nginx
network_interfaces:
- name: "{{ listen_if }}"
ip_addresses:
- "{{ listen_ip }}"
listen_if: ens4
listen_ip: 192.168.0.10/24
listen_if: enp3f1
listen_ip: 192.168.0.11/24
listen_if: eno1
listen_ip: 192.168.0.12/24
これは、便利……!
制約
なんでもかんでもできるわけではありません。
{% ... %}
は使えない
試した限りですが、エラーになってしまいます。for
とか使いたかったのですが、残念です。
キーの置き換えはできない
# これは失敗する例
listen_if: ens4
route:
bgp:
neighbors:
"{{ listen_if }}":
remote_as: ....
置き換わらずに {{ listen_if }}
というネイバーとして扱われ、最終的にエラーになります。roleが自作であれば、構造を変更して下記のように書けば置換されます。
listen_if: ens4
route:
bgp:
neighbors:
- name: "{{ listen_if }}"
remote_as: ....
置換結果は常に文字列
あるroleを使いVLANを設定しようとしました。変数定義は次のとおりです。
second_vlan: 20
interfaces:
- name: vlan10
vlan_id: 10
- name: vlan20
vlan_id: "{{ second_vlan }}"
直接書いたときは問題ないのに、置換すると正しく動作しないことがありました。上記例でいうとvlan10
は無事作成される(作成済みなら何もしない)のに、vlan20
を削除するという動作になりました。
roleの中でwhen:
による判定があったのですが、factsで現在設定されているvlan_idを参照すると値の型はint
で、上記で置換された値はstr
型だったため比較に失敗していた、というオチでした。"{{ second_vlan | int }}"
と書いたりしましたが無駄でした。roleは自作だったので、数字の比較と文字列の比較両方やることでなんとか回避しました。
"{{ vars }}" は置換前の情報
デバッグのとき変数を参照するため msg: "{{ vars }}"
を書いたりしますが、これで表示される内容は置換前のままです。よしあしはわかりませんが、ご注意ください。
Undocumented?
探し方が悪いのか、このようなことができることを公式ドキュメントで見つけることができませんでした。仕様として明記されてなければ、将来できなくなる可能性もありますし、どのバージョンからこれができるのかも調べてみないとわかりません。便利ですが利用にはご注意を。
さいごに
変数定義の範囲でできることは相当少ないですが、少しでもメンテナンスが楽になればと思います。
ところで、こんなことができるって皆知ってて黙ってたの? 知らなかったのぼくだけ? 早く言ってよー。って感じで、半日くらい費やしました。