Ansibleのjoinフィルタの秘密

# 課題

例えば、たくさんのオプションがあるコマンドを、ある時はいくつかのオプションの組み合わせで、またある時は別のオプションの組み合わせでAnsibleを使って実行させたい場合。

つまり、こう実行させたい場合もあれば、

```
# command --opt1 val1a --opt2 val2a --opt3 val3a 
```

こう実行させたい場合もある、

```
# command --opt1 val1b --opt2 val2b --opt4 val4b 
```

という時、まず後者を実行させようとして、かつ後で書き換える時に見易いようにPlaybookのvarsにオプションをこんな感じで書いたりするのがきれいではないだろうか。

```yaml:site.yml
- hosts: localhost
  vars:
    opts:
      opt1: val1b
      opt2: val2b
      opt4: val4b
```

しかし、これをどうやって`--opt1 val1b --opt2 val2b --opt4 val4b`という文字列に変換しようか、というのが今回の課題。

# まず最初に考えた方法（失敗）

以下でいけるんじゃないかと思ったが上手くいかなかった。

```yaml:site.yml
- hosts: localhost
  vars:
    opts:
      opt1: val1b
      opt2: val2b
      opt4: val4b
  tasks:
    - set_fact:
        opt_str: '{{ opt_str | default("") }} --{{ item.key }} "{{ item.value }}"'
      with_dict: opts
    - shell: command{{ opt_str }}
```

`ansible-playbook site.yml -v`を実行してみると以下のようになった（commandはechoで代用）。

```txt:出力の一部
TASK: [shell echo{{ opt_str }}] **********************************************
changed: [localhost] => {"changed": true, "cmd": "echo --opt2 \"val2b\"", "delta": "0:00:00.003142", "end": "2014-09-14 22:44:56.400165", "rc": 0, "start": "2014-09-14 22:44:56.397023", "stderr": "", "stdout": "--opt2 val2b"}
```

オプション1つしか使われてないじゃないか。
ループを回せば変数opt_strに文字列追加できると思っていたのだが、どうもループの最後に来た1つだけがセットされるっぽい。

# 解答

以下で達成できることがわかった。

```yaml:site.yml
- hosts: localhost
  vars:
    opts:
      opt1: val1b
      opt2: val2b
      opt4: val4b
  tasks:
    - set_fact:
        opt_str: '--{{ item.key }} "{{ item.value }}"'
      register: result
      with_dict: opts
    - shell: command {{ result.results | join(' ', attribute='ansible_facts.opt_str')}}
```

`ansible-playbook site.yml -v`を実行してみると以下のようになる（こちらもcommandはechoで代用）。

```txt:出力の一部
TASK: [shell echo {{ result.results | join(' ', attribute='ansible_facts.opt_str')}}] ***
changed: [localhost] => {"changed": true, "cmd": "echo --opt4 \"val4b\" --opt1 \"val1b\" --opt2 \"val2b\"", "delta": "0:00:00.002560", "end": "2014-09-14 22:52:25.639652", "rc": 0, "start": "2014-09-14 22:52:25.637092", "stderr": "", "stdout": "--opt4 val4b --opt1 val1b --opt2 val2b"}
```

期待通りである。

説明すると以下のようになる。

1. set_factモジュールでループとともにregisterを使用すると、registerで指定した変数にresultsというキーができ、その値としてregisterした結果の配列が入る。
2. 配列なら文字列に結合するのに、[Ansibleのドキュメント](http://docs.ansible.com/playbooks_variables.html)にあるようにjoinフィルタを使用することができる。しかしresultsの配列の中は連想配列の階層になっており、set_factした結果はansible_factsというキーの中にさらにset_factした時のキーの名前で入っている(*)。
3. そこで[jinja2のドキュメント](http://jinja.pocoo.org/docs/dev/templates/)を見るとjoinフィルタにはattributeというオプションがあるようだ。これを使用することでjoin時に配列の中身が連想配列やその階層構造であっても、そこから特定のキーの値を取り出して結合することができる。なので今回は`{{ result.results | join(' ', attribute='ansible_facts.opt_str')}}`とすると期待通りのものを得ることができる。

(*) 以下のような構造になっている。

```
'results': 
  [
    {
      'invocation': {
        'module_name': u'set_fact',
        'module_args': ''
      },
      'item': {
        'value': 'val4b',
        'key': 'opt4'
      },
      'ansible_facts': {
        'opt_str': u'--opt4 \"val4b\"'
      }
    },
    {
      'invocation': {
        'module_name': u'set_fact',
        'module_args': ''
      },
      'item': {
        'value': 'val1b',
        'key': 'opt1'
      },
      'ansible_facts': {
        'opt_str': u'--opt1 \"val1b\"'
      }
    },
    {
      'invocation': {
        'module_name': u'set_fact',
        'module_args': ''
      },
      'item': {
        'value': 'val2b',
        'key': 'opt2'
      },
      'ansible_facts': {
        'opt_str': u'--opt2 \"val2b\"'
      }
    }
  ]
}
```

# 結論

もちろんこの課題の例程度なら以下のようにしておけばいい、という話はあるだろう。

```yaml:site.yml
- hosts: localhost
  vars:
    opts: "--opt1 val1b --opt2 val2b --opt4 val4b"
  tasks:
    - shell: command {{ opts }}
```

しかしJSONで渡されたとか、などともなればこういう解決をする必要も出てくるのではないかと思う。