[Ansible]dictのkeyを一部削除する方法まとめ

# はじめに

この記事は、[Ansible Advent Calendar](https://qiita.com/advent-calendar/2021/ansible)の10日目の記事になります。

モジュールにdictでパラメータを渡すときに手順の問題で

1. dictの一部を削除したものをパラメータとして利用
1. 別作業
1. dictのすべてをパラメータとして利用

とする必要がありました。

同じdictを利用したかったのですが、公式のfilterからはできるようなものは見つからず・・・

いろいろと方法を探してみたのでメリットデメリット含めてまとめたものを共有したいと思います。

# 方法

dictのkeyを削除する方法としては、以下の通り5種類ありました。
\#ほかにも方法があるかもしれずご存じの方いればお教えください。

おすすめとしては、5のカスタムフィルタを作成するか、フィルタを作成しないポリシーであれば2のjinjaテンプレートを使う方法でいいのかなと思います。

なお、今回は、5のカスタムフィルタで対応しました。

|#|方法|変数空間を汚すか|メリット|デメリット|
|---|---|---|---|---|
|1|dictをゼロから作成|Yes|keyごとにdict代入しているかがログに出るのでデバッグしやすい|loopを使っており、タスクが3つに分かれるため、行数が長くなる|
|2|jinjaテンプレートでkey削除|No|カスタマイズ性が高い|jinjaテンプレートをPlaybookで使うため、わかりづらい|
|3|keyにomit代入|No|Ansibleが提供しているfilterのみで完結|特になし。複雑なdictへの対応が難しい？|
|4|list変換して再度dict化|No|Ansibleが提供しているfilterのみで完結|変数の動きがわかりづらい|
|5|カスタムフィルターを作成|No|Playbookがシンプルになる|カスタムフィルタを作成する必要がある|
|6|JSONQueryフィルターを利用|No|柔軟性が高い|`jsmepath`をインストールしておく必要がある|


`dict`内の`a`というkeyを削除して表示したときの各方法のPlaybookは下記のとおりです。

なお、方法5のカスタムフィルターについてはPlaybook実行フォルダ配下に`filter_plugins/remove_dict_key.py`を作成しています。

<details><summary>playbook</summary><div>

```yaml:playbook.yml
---
- name: test
  hosts: localhost
  gather_facts: no

  vars:
    dict:
      a: test1
      b: test2
      c: test3

  tasks:
    - name: 元dictをコピー
      set_fact:
        d1: "{{ dict }}"
        d2: "{{ dict }}"
        d3: "{{ dict }}"
        d4: "{{ dict }}"
        d5: "{{ dict }}"
        d6: "{{ dict }}"

    # 1. dictをゼロから作成  
    - name: ①：空dict作成
      set_fact:
        d1_: {}
    - name: ①：空dictにkeyを追加
      set_fact:
        d1_: "{{ d1_ | combine({ item.key: item.value }) }}"
      when: item.key not in ['a']
      with_dict: "{{ d1 }}"
    - name: ①：表示(d1_は変数空間に残る)
      debug:
        msg: "{{ d1_ }}"
    # TASK [①：表示(d1_は変数空間に残る)] *********************************************************************************************************************************************************************************************************
    # ok: [localhost] => {
    #     "msg": {
    #         "b": "test2",
    #         "c": "test3"
    #     }
    # }

    # 2. jinjaテンプレートでkey削除
    - name: ②jinja内で変数作成してkeyを除外して表示
      debug:
        msg: |
          {% set d2_=d2.copy() %}
          {% set _r=d2_.pop('a') %}
          {{ d2_ }}

    - name: ②d2は変数空間に残らない
      debug:
        msg: "{{ d2_ }}"
      ignore_errors: yes
    # TASK [②jinja内で変数作成してkeyを除外して表示] *********************************************************************************************************************************************************************************************
    # ok: [localhost] => {
    #     "msg": {
    #         "b": "test2",
    #         "c": "test3"
    #     }
    # }
    # TASK [②d2は変数空間に残らない] **************************************************************************************************************************************************************************************************************
    # fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'd2_' is undefined\n\nThe error appears to be in '/mnt/c/Users/y_miyashita.IT-PC-2012-0517/git/act_training/test.yml': line 41, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n    - name: ②d2は変数空間に残らない\n      ^ here\n"}
    # ...ignoring

    # 3. keyにomit代入
    - name: ③keyの値をomitにして表示
      debug:
        msg: |
          {{ d3 | combine({'a': omit}) }}
    # TASK [③keyの値をomitにして表示] *************************************************************************************************************************************************************************************************************
    # ok: [localhost] => {
    #     "msg": {
    #         "b": "test2",
    #         "c": "test3"
    #     }
    # }

    # 4. list変換して再度dict化
    - name: ④list変換・再度dict化して表示
      debug:
        msg: |
          {{ dict | dict2items | rejectattr("key", "eq", "a") | list | items2dict }}
      # TASK [④list変換・再度dict化して表示] ******************************************************************************************************************************************************************************************************
      # ok: [localhost] => {
      #     "msg": {
      #         "b": "test2",
      #         "c": "test3"
      #     }
      # }

      # 5. カスタムフィルターを作成
    - name: ⑤フィルターを利用して表示
      debug:
        msg: |
          {{ d5 | remove_dict_key('a') }}

    - name: ⑤d5に影響はない
      debug:
        msg: "{{ d5 }}"
    # TASK [⑤フィルターを利用して表示] ***************************************************************************************************************************************************************************************
    # ok: [localhost] => {
    #     "msg": {
    #         "b": "test2",
    #         "c": "test3"
    #     }
    # }
    # TASK [⑤d5に影響はない] **********************************************************************************************
    # ok: [localhost] => {
    #     "msg": {
    #         "a": "test1",
    #         "b": "test2",
    #         "c": "test3"
    #     }
    # }

    # 6. JSON Queryを利用
    - name: ⑥JSON Queryを利用して表示
      debug:
        msg: |
          {{ d6 | json_query('{b: b, c: c}') }}
    # TASK [⑥JSON Queryを利用して表示] ************************************************************************************
    # ok: [localhost] => {
    #     "msg": {
    #         "b": "test2",
    #         "c": "test3"
    #     }
    # }
```
</div></details>

<details><summary>filter</summary><div>

```python:filter_plugins/remove_dict_key.py
#!/usr/bin/env python

from ansible.errors import AnsibleFilterError

class FilterModule(object):
    def filters(self):
        return {
            'remove_dict_key': self.remove_dict_key,
        }

    def remove_dict_key(self, d, key):
        if not isinstance(d, dict):
            raise AnsibleFilterError("|failed expects a dictionary")

        if key in d:
            del d[key]

        return d
```
</div></details>


# さいごに

すでに定義しているdictをいじることは結構あると思うので、プラグインで定義しておくと後々有効活用できそうで良いかなと思います。

3のcombineを使う方法では、null代入しかできないと思っていましたが、後々調べてみるとomitでも問題なさそうなので気軽にkey削除する場合はその方法でもいいですね！

余談ですが、今回filterを作成する際にGitHub Copilotを有効にしていたのですが`def remove_dict`と打ったタイミングで、関数の中身が丸々が候補として出てきており、同じようなfilterを作っている人は多いんだなと思った反面GitHub Copilotすごい！と改めて痛感しました。

# 参考リンク

- https://stackoverflow.com/questions/40496021/how-to-remove-a-single-key-from-an-ansible-dictionary
- https://newbedev.com/how-to-remove-a-single-key-from-an-ansible-dictionary