【Ansible】Ansibleで集合論フィルターを使いたい

## はじめに

&emsp;Ansibleで2つのリストを比較し、両方に含まれる要素やどちらかだけに含まれる要素を
抽出したいことがあると思います。その場合は、集合論フィルターが便利なので実際に使ってみました。

## TL;DR

- Ansibleでは以下の集合論フィルターが使用可能。リストだけでなく、辞書のリストに対しても使用可能。

  | 集合論フィルター             | 演算子     | 説明                              | 記載例                                              |
  |----------------------|---------|---------------------------------|--------------------------------------------------|
  | unique               | -       | 重複を排除                           | {{ list_A &#124; unique }}                       |
  | intersect            | A ∩ B   | 積集合（AかつB）                       | {{ list_A &#124; intersect(list_B) }}            |
  | union                | A ∪ B   | 和集合（AまたはB）<br/>※AかつBも含まれる       | {{ list_A &#124; union(list_B) }}                |
  | symmetric_difference | A xor B | 排他的論理和（AのみまたはBのみ）<br/>※AかつBは含まれない | {{ list_A &#124; symmetric_difference(list_B) }} |
  | difference           | A ー B   | 差集合（AにはあるがBにはない）                | {{ list_A &#124; difference(list_B) }}           |

- 辞書のリスト同士を比較した際に同じ要素とみなされるには、辞書内のすべてのキーと値が一致している必要がある。
- 集合論フィルターを活用することで、辞書のリスト同士の比較が容易になる。

## 前提条件

- Ansibleにおけるフィルターの使い方を理解していること

## 実行環境

- Python: 3.9.16
- Ansible: 2.9.7
- Jinja2: 3.0.3

## 詳細

### 1. リストに対して集合論フィルターを使用

#### 1-1. 実行するPlaybook

- Playbookでは、以下2つのことを確認します
  - 変数`list_unique`に対し、リストの重複が排除されること（unique）
  - 変数`list_A`と`list_B`に対し、正しい演算が行われること（intersect/union/symmetric_difference/difference）

```yaml:list_set_theory_filters.yml
---
- name: "List set theory filters"
  hosts: localhost
  vars:
    list_unique: [1, 2, 3, 2, 3, 4]    # 2と3が重複
    list_A: [1, 2, 3, 4]   # 3と4が重複
    list_B: [3, 4, 5, 6]
  gather_facts: false
  tasks:
    - name: Test unique
      debug:
        msg: "{{ list_unique | unique }}"

    - name: Test intersect
      debug:
        msg: "{{ list_A | intersect(list_B) }}"

    - name: Test union
      debug:
        msg: "{{ list_A | union(list_B) }}"

    - name: Test symmetric_difference
      debug:
        msg: "{{ list_A | symmetric_difference(list_B) }}"

    - name: Test difference
      debug:
        msg: "{{ list_A | difference(list_B) }}"
```

#### 1-2. 実行結果

想定通りの結果を得ることができました。

```
$ ansible-playbook list_set_theory_filters.yml
PLAY [List set theory filters] *******************************************************************

TASK [Test unique] *******************************************************************************
ok: [localhost] =>
  msg:
  - 1
  - 2
  - 3
  - 4

TASK [Test intersect] ****************************************************************************
ok: [localhost] =>
  msg:
  - 3
  - 4

TASK [Test union] ********************************************************************************
ok: [localhost] =>
  msg:
  - 1
  - 2
  - 3
  - 4
  - 5
  - 6

TASK [Test symmetric_difference] *****************************************************************
ok: [localhost] =>
  msg:
  - 1
  - 2
  - 5
  - 6

TASK [Test difference] ***************************************************************************
ok: [localhost] =>
  msg:
  - 1
  - 2

PLAY RECAP ***************************************************************************************
localhost                  : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

### 2. 辞書のリストに対して集合論フィルターを使用

#### 2-1. 実行するPlaybook

- Playbookでは1つ前と同様に、以下2つのことを確認します
  - 変数`list_unique`に対し、リストの重複が排除されること（unique）
  - 変数`list_A`と`list_B`に対し、正しい演算が行われること（intersect/union/symmetric_difference/difference）

```yaml:dict_set_theory_filters.yml
---
- name: "Dict set theory filters"
  hosts: localhost
  vars:
    list_unique:
      - {id: 1, name: "test 1"}
      - {id: 2, name: "test 2"}
      - {id: 3, name: "test 3"}
      - {id: 2, name: "test 2"}
      - {id: 3, name: "test 3"}
      - {id: 4, name: "test 4"}
    list_A:
      - {id: 1, name: "test 1"}
      - {id: 2, name: "test 2"}
      - {id: 3, name: "test 3"}
      - {id: 4, name: "test 4"}
    list_B:
      - {id: 3, name: "test 3"}
      - {id: 4, name: "test 4"}
      - {id: 5, name: "test 5"}
      - {id: 6, name: "test 6"}
  gather_facts: false
  tasks:
    - name: Test unique
      debug:
        msg: "{{ list_unique | unique }}"

    - name: Test intersect
      debug:
        msg: "{{ list_A | intersect(list_B) }}"

    - name: Test union
      debug:
        msg: "{{ list_A | union(list_B) }}"

    - name: Test symmetric_difference
      debug:
        msg: "{{ list_A | symmetric_difference(list_B) }}"

    - name: Test difference
      debug:
        msg: "{{ list_A | difference(list_B) }}"
```

#### 2-2. 実行結果

辞書のリストに対しても、想定通りの結果を得ることができました。

```
$ ansible-playbook dict_set_theory_filters.yml
PLAY [Dict set theory filters] **********************************************************

TASK [Test unique] **********************************************************************
ok: [localhost] =>
  msg:
  - id: 1
    name: test 1
  - id: 2
    name: test 2
  - id: 3
    name: test 3
  - id: 4
    name: test 4

TASK [Test intersect] *******************************************************************
ok: [localhost] =>
  msg:
  - id: 3
    name: test 3
  - id: 4
    name: test 4

TASK [Test union] ***********************************************************************
ok: [localhost] =>
  msg:
  - id: 1
    name: test 1
  - id: 2
    name: test 2
  - id: 3
    name: test 3
  - id: 4
    name: test 4
  - id: 5
    name: test 5
  - id: 6
    name: test 6

TASK [Test symmetric_difference] ********************************************************
ok: [localhost] =>
  msg:
  - id: 1
    name: test 1
  - id: 2
    name: test 2
  - id: 5
    name: test 5
  - id: 6
    name: test 6

TASK [Test difference] ******************************************************************
ok: [localhost] =>
  msg:
  - id: 1
    name: test 1
  - id: 2
    name: test 2

PLAY RECAP ******************************************************************************
localhost                  : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

#### 2-3. 注意点

- 辞書のリスト同士を比較した際に同じ要素とみなされるには、辞書内のすべてのキーと値が一致している必要があります。
  - 例1）intersectフィルターで`id`と`name`の値は一致しているが、`list_B`にのみキー`hoge`があるので同じとみなされない

    - タスク

      ```yaml
      - name: Test intersect
        debug:
          msg: "{{ list_A | intersect(list_B) }}"
        vars:
          list_A:
            - {id: 1, name: "test 1"}
          list_B:
            - {id: 1, name: "test 1", hoge: "hoge"}
      ```

    - 実行結果

      ```
      TASK [Test intersect] ***************************************************************
      ok: [localhost] =>
        msg: []
      ```

  - 例2）intersectフィルターで両方にキー`id`と`name`はあるが、キー`name`の値が異なるので同じとみなされない

    - タスク

      ```yaml
      - name: Test intersect
        debug:
          msg: "{{ list_A | intersect(list_B) }}"
        vars:
          list_A:
            - {id: 1, name: "test 1"}
          list_B:
            - {id: 1, name: "hoge"}
      ```

    - 結果

      ```
      TASK [Test intersect] ***************************************************************
      ok: [localhost] =>
        msg: []
      ```

### 3. 実践例

集合論フィルターを活用することで、辞書のリスト同士の比較が容易になります。

#### 3-1. list_Aとlist_Bに重複がないことの確認

`list_A`が`list_B`に重複がないことを確認する場合の例が以下です。例では、`list_A`と`list_B`の両方に`1`があるのでエラーになります。

- タスク

  ```yaml
  - name: Check that there are no duplicates in list_A and list_B
    assert:
      that:
        -  (list_A | intersect(list_B)) | length == 0
      fail_msg:
        - "list_A and list_B: {{ list_A | intersect(list_B) }}"
        - "Only list_A: {{ list_A | difference(list_B) }}"
        - "Only list_B: {{ list_B | difference(list_A) }}"
    vars:
      list_A: [1]
      list_B: [1, 2]
  ```

- 実行結果

  ```
  TASK [Check that there are no duplicates in list_A and list_B] **********************
  fatal: [localhost]: FAILED! => changed=false
    assertion: (list_A | intersect(list_B)) | length == 0
    evaluated_to: false
    msg:
    - 'list_A and list_B: [1]'
    - 'Only list_A: []'
    - 'Only list_B: [2]'
  ```

#### 3-2. list_Aがlist_Bに含まれることの確認（A⊂B）

`list_A`が`list_B`に含まれることを確認する場合の例が以下です。例では、`list_A`に`5`がに含まれているのでエラーになります。

- タスク

  ```yaml
  - name: Check that list_A is in list_B
    assert:
      that:
        -  (list_A | intersect(list_B)) == list_A
      fail_msg:
        - "list_A and list_B: {{ list_A | intersect(list_B) }}"
        - "Only list_A: {{ list_A | difference(list_B) }}"
        - "Only list_B: {{ list_B | difference(list_A) }}"
    vars:
      list_A: [1, 2, 5]
      list_B: [1, 2, 3, 4]
  ```

- 実行結果

  ```
  TASK [Check that list_A is in list_B] ***********************************
  fatal: [localhost]: FAILED! => changed=false
    assertion: (list_A | intersect(list_B)) == list_A
    evaluated_to: false
    msg:
    - 'list_A and list_B: [1, 2]'
    - 'Only list_A: [5]'
    - 'Only list_B: [3, 4]'
  ```

## さいごに

- 集合論フィルターを活用することで、辞書のリスト同士の比較が容易になるので積極的に活用しましょう。
- 集合論フィルターの使い方を整理しながら、高校数学の「集合と論理」を思い出しました。懐かしいですね。

## 参考URL

- [Ansible Documentation - Filters][1]
  - 集合論フィルターの使い方

---

[1]: https://docs.ansible.com/ansible/2.9_ja/user_guide/playbooks_filters.html#set-theory-filters