Ansible: 変数定義の中で別の変数を参照する

# はじめに

Ansibleでちょっと困ったことがあって、なんとかならないか探したり試行錯誤したりしてみました。

下記、Undocumentedな動作かも知れませんのでバージョンを明記します。
 `ansible-playbook 2.9.24` で試した結果になります。

# 変数定義の値のベタ書きがとてもつらい

Ansible、便利ですよね。roleやtaskにあわせて構造化された変数を定義しておいて`ansible-playbook`コマンドで複数ホストに一度に設定を流し込めます。でもちょっと困ったことが起きたのです。

## ふたつの環境で変数定義をある程度共有したい

ふわっとした例を書くと、たとえばDatabase serverとWeb serverの２つを用意して、互いのNICを接続するとします。実環境と仮想環境を用意して、仮想環境でテストした結果を実環境で、みたいなことを考えます。

変数定義のイメージとしてはこんなふう。

```yaml:db.yml
apt:
  name: mysql-server

network_interfaces:
  - name: ens4
    ip_addresses:
     - 192.168.0.10/24
```

```yaml:web.yml
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があるというわけではありません、ねんのため)

```yaml:nathost.yml
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アンカーは、ある値を定義してる箇所に `&名前` のアンカーをつけて、`*名前` でその値を引っ張ってくるというものです。

```yaml:web.yml
liesn_if: &lif ens4

apt:
  name: nginx

network_interfaces:
  - name: *lif
    ip_addresses:
     - 192.168.0.20/24
```

定義してる名前の他にアンカーの名前を書くのが煩わしいですが、この例は正しく`*lif`が`ens4`に置き換わります。

が。

YAMLアンカーはどうやら同一ファイル内でしか機能しないようでした。つまり繰り返しの例のように何度も同じワードが出てくる場合、ファイルの先頭付近で定義しておけば後ろを書き換えなくて済むものの、別ファイルに定義をまとめたりはできないということです。

# Jinja2的な参照

ネットの海を漂っても、テンプレートファイルを書いて置き換えるかtaskの中で変数を参照する例ばかり。できないのかなあとあきらめかけてたのですが、こんな書き方が**変数定義の中で**できるという記述が。

```yaml:web.yml
liesn_if: ens4

apt:
  name: nginx

network_interfaces:
  - name: "{{ listen_if }}"
    ip_addresses:
     - 192.168.0.20/24
```

やってみたら、**できました**。しかもこれ、ファイルが分かれていても問題なく動きます。Web serverを3台建てるようなケースで下記のようにできます。

```yml:group_vars/web
apt:
  name: nginx

network_interfaces:
  - name: "{{ listen_if }}"
    ip_addresses:
     - "{{ listen_ip }}"
```

```yaml:host_vars/web0
listen_if: ens4
listen_ip: 192.168.0.10/24
```

```yaml:host_vars/web1
listen_if: enp3f1
listen_ip: 192.168.0.11/24
```

```yaml:host_vars/web0
listen_if: eno1
listen_ip: 192.168.0.12/24
```

**これは、便利……!**

# 制約

なんでもかんでもできるわけではありません。

## `{% ... %}` は使えない

試した限りですが、エラーになってしまいます。`for`とか使いたかったのですが、残念です。

## キーの置き換えはできない

```yaml:error.yml
# これは失敗する例
listen_if: ens4

route:
  bgp:
    neighbors:
      "{{ listen_if }}":
        remote_as: ....
```

置き換わらずに `{{ listen_if }}` というネイバーとして扱われ、最終的にエラーになります。roleが自作であれば、構造を変更して下記のように書けば置換されます。

```yaml:valid.yml
listen_if: ens4

route:
  bgp:
    neighbors:
      - name: "{{ listen_if }}"
        remote_as: ....
```

## 置換結果は常に文字列

あるroleを使いVLANを設定しようとしました。変数定義は次のとおりです。

```yaml:vlan.yml
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?

探し方が悪いのか、このようなことができることを公式ドキュメントで見つけることができませんでした。仕様として明記されてなければ、将来できなくなる可能性もありますし、どのバージョンからこれができるのかも調べてみないとわかりません。便利ですが利用にはご注意を。

# さいごに

変数定義の範囲でできることは相当少ないですが、少しでもメンテナンスが楽になればと思います。

ところで、こんなことができるって皆知ってて黙ってたの? 知らなかったのぼくだけ? 早く言ってよー。って感じで、半日くらい費やしました。