Ansibleのplaybookからコピペをなくすset_factモジュールの使い方

# よくある課題

Ansibleで条件分岐させるにはplayの中でwhen句を使用する。
1つのtrue/falseの条件があってそれぞれ実行させたいものが違う場合はplayを2つ書くことになる。
しかし正直、普通のプログラム言語ならif-elseで書けるところをplayを2つ並べないとならないというのはいささか美しさに欠ける。実行させたいものが全く違うならまだ良いが、ほとんど一緒なら尚更だ。そのほとんど一緒な部分を修正したくなったら2箇所直さなければならない。
それでも2つならとは思わなくないが、さてtrue/falseの条件分岐が3つあったとしよう。playを8つ書けって？ それは勘弁して欲しい。

そんな悩みをある程度解決するのがset_factモジュールである。

# set_factの使い方（基本編）

set_factは変数に値をセットするモジュールである。
この値はスカラーでもシークエンスでもマッピングでも良い。
（これらの呼び名は http://www.yaml.org/spec/1.2/spec.html による。まあシークエンスと言うよりはリストとか配列とか言った方が通りが良いだろうし、マッピングもディクショナリとかハッシュとか連想配列とかの方が通りが良いだろう）

スカラーの場合。

```
- name: scalar
  set_fact:
    key_a: "{{ value_a }}"
```

シークエンスの場合。

```
- name: sequence
  set_fact:
    key_b:
      - "{{ value_b1 }}"
      - "{{ value_b2 }}"
      - "{{ value_b3 }}"
```

マッピングの場合。

```
- name: mapping
  set_fact:
    key_c:
      key_c1: "{{ value_c1 }}"
      key_c2: "{{ value_c2 }}"
      key_c3: "{{ value_c3 }}"
```

もちろんマッピングの中にさらにマッピング書いたりシークエンス書いたり、もっと複雑な組み合わせも書くこともできる。

セットした値の取り出しは`{{ key_a }}`とか`with_items: key_b`とか`{{ key_c.key_c1 }}`とか、まあ考えた通りにできる。

注意点としてはキーの側に{{ }}は使用できず展開されない。
つまり以下のようなのはキーの名前がそのまま{{ key_x }}になってしまうだけである。

```
- name: failure
  set_fact:
    "{{ key_x }}": "{{ value_x }}"
```

# set_factの使い方（応用編）

## コピペをなくそう

以下の例は自作の、Ansibleで各コンピュータを操作する前に、操作対象に向けてssh-copy-idを実行してパスワードの入力をしなくて済むようにしたもの ( https://github.com/yunano/ansible-centos7-roles/blob/master/ssh-copy-id/tasks/main.yml で公開しているもの) を一部取り出したものとなる。
なお、nameの部分については説明の簡単のためここに張り付ける際に変更している。

```
- name: play1
  set_fact:
    identity_file_option: "-i {{ ssh_key_file }} "
  when: ssh_key_file is defined

- name: play2
  set_fact:
    ssh_host: "{{ item }}"
    ssh_host_credentials: "{{ os_credentials[item] | default(os_credentials['<default>']) }}"
  register: result
  with_items: hostnames

- name: play3
  shell: |
    _copy() {
      sshpass -p {{ item.ansible_facts.ssh_host_credentials.password }} ssh-copy-id {{ identity_file_option | default('') }}{{ item.ansible_facts.ssh_host_credentials.name }}@{{ item.ansible_facts.ssh_host }}
    }
    _copy
    if [ $? -eq 6 ]; then
      ssh-keyscan {{ item.ansible_facts.ssh_host }} >> ~/.ssh/known_hosts
      _copy
    fi
  when: include_self or item.ansible_facts.ssh_host != "{{ inventory_hostname }}"
  register: result1
  changed_when: "'ssh-copy-id: WARNING: All keys were skipped' not in result1.stderr"
  with_items: result.results
```

ssh-copy-idは-iオプションで指定した秘密鍵を使用することができるが、省略した場合はデフォルトの秘密鍵 ~/.ssh/id_rsa を使用する。

そこでplay1ではssh_key_fileという変数に値が入っている時だけ前に「-i 」を付けて別の変数identity_file_optionにセットする。
取り出し側のplay3の中では`{{ identity_file_option | default('') }}`とdefaultフィルタを使用して、identity_file_optionに値がセットされていればその値に、セットされていなければ空文字列に展開される。

play2でもset_factモジュールとdefaultフィルタを組み合わせて使用しているが、このようにすることでplaybookの中からコピペを排除していくことが可能になるというわけである。

## おまけ：ループの中のset_fact

また、play2はwith_itemsでループさせた時にset_factモジュールを使用するとどうなるか、という例でもある。
この場合、ループの一番最後に実行されたset_factの際の値だけが指定した変数にセットされる。正確に言えば何度も上書きされた挙句一番最後の値になる。

しかし、registerと組み合わせることによってループさせた各回の値を保存することが実は可能である。`<registerで指定した変数>.results`の中にシークエンスとして各回のset_factでセットしたものが、さらにその中のansible_factsに入っている。

今回はplay2のregisterで「result」という変数にセットしたので、play3では`with_items: result.results`でループを回し、取り出しは`item.ansible_facts.ssh_host`などとして行っている。