ホストグループを例にAnsibleのプレイブックでリストを連結・除外する

# はじめに

本記事は[Ansible 3 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ansible3)の18日目のエントリになります。

# インベントリファイルをいじっちゃだめと言われたら…

様々な大人の事情によって、(サーバー設定などの仕様の変更が発生した場合にその範囲のみ)プレイブックは編集OKだけど(処理対象に変更は発生しないはずという理由で)インベントリは編集不可という条件下で、プレイブック(プレイ)のみ編集してホストグループなどのリストを連結したり特定項目を除外する操作について説明します。

文書だとわかりづらいので実際の入出力を見ていきましょう。

## インベントリファイル例

こんなホスト構成のインベントリがあったとします。

```text
[master]
master1.example.org
master2.example.org
master3.example.org

[nodes]
master1.example.org
master2.example.org
master3.example.org
node1.example.org
node2.example.org
node3.example.org

[dns]
ns1.example.org
ns2.example.org

[nas]
nas.example.org

[db]
db1.example.org
db2.example.org
```

こういうインベントリファイルを作っており、最初はそれで問題無かったのに、「追加の設定変更が発生して`dns`と`db`だけを対象にAnsibleで処理したい要件が発生」とか、「`master`以外の`nodes`に対して処理」みたいなのを後からやる必要が発生し、プレイブックの編集のみでどう書けばいいか、という内容になります。

# Targetセクションのリスト操作

```yml:playbook.yml
---
- hosts: group_name
```

この部分です。

※ 2021.01.28追記: ターゲットホストの結合については、公式ドキュメントに記載があります。
[Patterns: targeting hosts and groups — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/intro_patterns.html)
記事末尾にも情報追加

## 連結

連結は、`,`で羅列すればOKです。

```yml
- hosts: dns, master
  gather_facts: False
  tasks:
    - ping:
```

```text:結果
PLAY [dns, master] *************************************************************

TASK [ping] ********************************************************************
ok: [ns2.example.org]
ok: [master2.example.org]
ok: [ns1.example.org]
ok: [master3.example.org]
ok: [master1.example.org]
```

## 連結(重複要素は自動排除)

(例が悪いのだけど)`nodes`と`master`を連結すると、`master1.example.org`,`master2.example.org`,`master3.example.org`がダブってしまうけど、タスクの処理では1回しか実行されないのでご安心を。

```yml
- hosts: nodes, master
  gather_facts: False
  tasks:
    - ping:
```

```text:結果
PLAY [nodes, master] ***********************************************************

TASK [ping] ********************************************************************
ok: [node2.example.org]
ok: [node1.example.org]
ok: [master2.example.org]
ok: [master3.example.org]
ok: [master1.example.org]
ok: [node3.example.org]
```

## 除外

「`dns`以外すべて」という場合、`dns`以外を連結してもいいですが、`all`から`dns`を引くと楽。
除外の場合は頭に`!`を指定します。

```yml
- hosts: all, !dns
  gather_facts: False
  tasks:
    - ping:
```

```text:結果
PLAY [all, !dns] ***************************************************************

TASK [ping] ********************************************************************
ok: [master2.example.org]
ok: [master1.example.org]
ok: [master3.example.org]
ok: [db1.example.org]
ok: [nas.example.org]
ok: [node1.example.org]
ok: [db2.example.org]
ok: [node2.example.org]
ok: [node3.example.org]
```

## 除外(1件のみ)

`master`グループ、ただし`master3.example.org`を除く場合。
ユースケース的には`localhost`以外の`all`、みたいな場合に使うかな？

```yml
- hosts: master, !master3.example.org
  gather_facts: False
  tasks:
    - ping:
```

```text:結果
PLAY [master, !master3.example.org] ********************************************

TASK [ping] ********************************************************************
ok: [master2.example.org]
ok: [master1.example.org]
```

## グループ中1件のみ

`master`グループのうち任意の1台で実行されればOKの場合。
`kubectl`とか`oc`などの、どのホストで実行してもOKな場合とか。

```yml
- hosts: master[0]
  gather_facts: False
  tasks:
    - ping:
```

```text:結果
PLAY [master[0]] ***************************************************************

TASK [ping] ********************************************************************
ok: [master1.example.org]
```

これはリスト操作でなく、`run_once`を使っても同様の動作になります。

```yml
- hosts: master
  gather_facts: False
  tasks:
    - ping:
      run_once: True
```

```text:結果
PLAY [master] ******************************************************************

TASK [ping] ********************************************************************
ok: [master1.example.org]
```

# with_items/loopでのリスト操作

使用しているフィルターの説明はこちら：[Set Theory Filters](https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#set-theory-filters)

## 連結(重複あり)

Pythonで配列を連結する`+`を使えば、単純にリストが連結される。
そのため、`nodes`と`master`を連結すると、`master1`,`master2`,`master3`が重複します。

```yml
  - name: concat by "+"
    debug:
      msg: "{{ item }}"
    with_items:
    - "{{ groups.nodes + groups.master }}"
```

```text:結果
TASK [concat by "+"] ***********************************************************
ok: [localhost] => (item=master1.example.org) => {
    "msg": "master1.example.org"
}
ok: [localhost] => (item=master2.example.org) => {
    "msg": "master2.example.org"
}
ok: [localhost] => (item=master3.example.org) => {
    "msg": "master3.example.org"
}
ok: [localhost] => (item=node1.example.org) => {
    "msg": "node1.example.org"
}
ok: [localhost] => (item=node2.example.org) => {
    "msg": "node2.example.org"
}
ok: [localhost] => (item=node3.example.org) => {
    "msg": "node3.example.org"
}
ok: [localhost] => (item=master1.example.org) => {
    "msg": "master1.example.org"
}
ok: [localhost] => (item=master2.example.org) => {
    "msg": "master2.example.org"
}
ok: [localhost] => (item=master3.example.org) => {
    "msg": "master3.example.org"
}
```

## 連結(uniqueによる重複排除)

重複要素のあるリストは`unique`フィルターを使うことで重複分を除去できます。

```yml
  - name: concat "+" | unique
    debug:
      msg: "{{ item }}"
    with_items:
    - "{{ (groups.nodes + groups.master) | unique }}"
```

```text:結果
TASK [concat "+" | unique] *****************************************************
ok: [localhost] => (item=master1.example.org) => {
    "msg": "master1.example.org"
}
ok: [localhost] => (item=master2.example.org) => {
    "msg": "master2.example.org"
}
ok: [localhost] => (item=master3.example.org) => {
    "msg": "master3.example.org"
}
ok: [localhost] => (item=node1.example.org) => {
    "msg": "node1.example.org"
}
ok: [localhost] => (item=node2.example.org) => {
    "msg": "node2.example.org"
}
ok: [localhost] => (item=node3.example.org) => {
    "msg": "node3.example.org"
}
```

## 連結(unionによる重複排除)

リストの連結時に`+`でなく`union`を使うと、初めから重複要素が除かれます。

```yml
  - name: concat by union
    debug:
      msg: "{{ item }}"
    with_items:
    - "{{ groups.nodes | union(groups.master) }}"
```

```text:結果
TASK [concat by union] *********************************************************
ok: [localhost] => (item=master1.example.org) => {
    "msg": "master1.example.org"
}
ok: [localhost] => (item=master2.example.org) => {
    "msg": "master2.example.org"
}
ok: [localhost] => (item=master3.example.org) => {
    "msg": "master3.example.org"
}
ok: [localhost] => (item=node1.example.org) => {
    "msg": "node1.example.org"
}
ok: [localhost] => (item=node2.example.org) => {
    "msg": "node2.example.org"
}
ok: [localhost] => (item=node3.example.org) => {
    "msg": "node3.example.org"
}
```

## 除外

`nodes`のうち`master`を含まないもの、という場合。

```yml
  - name: nodes - master by difference
    debug:
      msg: "{{ item }}"
    with_items:
    - "{{ groups.nodes | difference(groups.master) }}"
```

```text:結果
TASK [nodes - master by difference] ********************************************
ok: [localhost] => (item=node1.example.org) => {
    "msg": "node1.example.org"
}
ok: [localhost] => (item=node2.example.org) => {
    "msg": "node2.example.org"
}
ok: [localhost] => (item=node3.example.org) => {
    "msg": "node3.example.org"
}
```

## 除外(1件のみ)

`nodes`のうち`master3`だけ除く、の場合。

```yml
  - name: nodes - "master3"
    debug:
      msg: "{{ item }}"
    with_items:
    - "{{ groups.nodes | difference('master3.example.org') }}"
```

```text:結果
TASK [nodes - "master3"] *******************************************************
ok: [localhost] => (item=master1.example.org) => {
    "msg": "master1.example.org"
}
ok: [localhost] => (item=master2.example.org) => {
    "msg": "master2.example.org"
}
ok: [localhost] => (item=node1.example.org) => {
    "msg": "node1.example.org"
}
ok: [localhost] => (item=node2.example.org) => {
    "msg": "node2.example.org"
}
ok: [localhost] => (item=node3.example.org) => {
    "msg": "node3.example.org"
}
```

## グループ中1件のみ

`master`の1件のみ、という場合。
(`with_items`使う必要はないんだけど、まぁ例ということで)

```yml
  - name: one of master
    debug:
      msg: "{{ item }}"
    with_items:
    - "{{ groups.master[0] }}"
```

```text:結果
TASK [one of master] ***********************************************************
ok: [localhost] => (item=master1.example.org) => {
    "msg": "master1.example.org"
}
```

ちなみに`symmetric_difference`ってなんだろ。よくわからなかった。

# Jinja2テンプレートでのリスト操作

`with_items`などでの動作と同じです。

## 連結(重複あり)

```jinja
{% for item in (groups.nodes + groups.master) %}
- {{ item }}
{% endfor %}
```

```text:結果
- master1.example.org
- master2.example.org
- master3.example.org
- node1.example.org
- node2.example.org
- node3.example.org
- master1.example.org
- master2.example.org
- master3.example.org
```

## uniqueによる重複除外した連結

```jinja
{% for item in (groups.nodes + groups.master) | unique %}
- {{ item }}
{% endfor %}
```

```text:結果
- master1.example.org
- master2.example.org
- master3.example.org
- node1.example.org
- node2.example.org
- node3.example.org
```

## unionによる重複除外した連結

```jinja
{% for item in (groups.nodes | union(groups.master)) %}
- {{ item }}
{% endfor %}
```

```text:結果
- master1.example.org
- master2.example.org
- master3.example.org
- node1.example.org
- node2.example.org
- node3.example.org
```

## 除外

```jinja
{% for item in (groups.nodes | difference(groups.master)) %}
- {{ item }}
{% endfor %}
```

```text:結果
- node1.example.org
- node2.example.org
- node3.example.org
```

# まとめ

ということで、インベントリファイルのグループの親子設定等をしなくても、プレイブック側でリストの操作ができることを確認してみました。
ただし、YAMLの特徴である可読性の高さを失う恐れがあるので、プロジェクトの偉い人を頑張って説得してインベントリファイルの修正を素直に行いましょう(本音)

# おまけ

ホストグループのホスト名からfactsのIPアドレスを取得するJinja2テンプレート。
(事前に`all`で`gather_facts: True`しておくこと)

```jinja
{% for item in (groups.all | difference(groups.master)) %}
{{ hostvars[item].ansible_facts.default_ipv4.address }}  {{ item }}
{% endfor %}
```

`/etc/hosts`作成やDNSサーバー構築用に備忘録。

---

参考

- [Filters / Set Theory Filters](https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#set-theory-filters)
- [[Ansible] リスト内の要素の重複を排除してユニークにする unique フィルター - てくなべ (tekunabe)](https://tekunabe.hatenablog.jp/entry/2019/12/05/ansible_unique)

---

# 2021.01.28追記: ターゲットホストのパターン

[Patterns: targeting hosts and groups — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/intro_patterns.html)

表になって見やすくなってるけど、`or`や`and`条件などの指定方法について書かれている。

|記述|内容|
|---|---|
|webservers:dbservers|`webservers`グループと`dbservers`グループどちらかにあるホスト|
|webservers:!atlanta|`webservers`グループのうち`atlanta`グループに含まれないホスト|
|webservers:&staging|`webservers`グループのうち`staging`グループにも含まれるホスト|