Ansibleをキモくする

もし、あなたが自分のAnsible Playbookに何か物足りなさを感じるとすれば、それは恐らくキモさが欠けているのだろう。なにせYAMLでモジュールにパラメータを渡すだけなので個性が出しづらい。
ここでは僭越ながら私が積み重ねてきた稚拙な工夫を少しばかりだが紹介したい。あなたの書いたPlaybookが同僚から「え、なにこれ・・・」と困惑される一助となれば幸いだ。

# 1. インベントリファイルを作成しない
あなたがこれまでの人生で食べてきたパンの枚数を覚えていないのと同様に、私もこれまでの人生で構築してきたサーバの台数を覚えていない。そんな私にとって構築したサーバの一覧を書き連ねなくてはいけないインベントリファイルは非常に煩わしい存在だ。
以下の例はPlaybook実行時に特定の条件を満たすAWS EC2インスタンスを選んでインベントリに追加して処理を実行する。サーバを毎日大量に作成や削除する場合に便利かも知れない。

```yaml
# ansible-playbook inventory1.yml -e prefix=gunez

- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - register: reg_ec2
      community.aws.ec2_instance_info:
        region: ap-northeast-1
        filters:
          "tag:Name": "{{ prefix }}-*"
          instance-state-name: running
    - loop: "{{ reg_ec2 | json_query('instances[].private_ip_address') }}"
      ansible.builtin.add_host:
        groups: shrike
        name: "{{ item }}"

- hosts: shrike
  gather_facts: false
  vars:
    ansible_user: ec2-user
    ansible_ssh_common_args: -o StrictHostKeyChecking=no
    ansible_ssh_private_key_file: "~/.ssh/oliver.pem"
  tasks:
    - register: reg_cmd
      ansible.builtin.command: >
        hostname
    - ansible.builtin.debug:
        msg: "{{ reg_cmd.stdout }}"
```

# 2. インベントリに好きなものを追加する
私のクローゼットには衣類に限らず、ゲーム機やコーラなど好きなものが沢山入っている。Ansibleのインベントリについても同様に、リモートサーバのホスト名に限らず自分の好きなものを入れたい。
以下の例はインベントリに100個のAWS S3バケットを追加し作成する。loopで作成する場合と異なり-fオプションで作成処理の並列実行数を変更できる。大量のリソースを頻繁に設定変更する場合に処理が速くなるかも知れない。気の短い私にはとても適している。

```yaml
- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - loop: "{{ range(0,100)|list }}"
      ansible.builtin.add_host:
        groups: buckets
        name: "starkjegan{{ item }}"

- hosts: buckets
  connection: local
  gather_facts: false
  tasks:
    - amazon.aws.s3_bucket:
        name: "{{ inventory_hostname }}"
        state: present
```

# 3. 参照する変数を切り替える
私は黒いTシャツを着ることが多いが、娘が吐き戻したミルクで汚れそうだと感じた場合は白いTシャツを着る。Playbookを実行する際もそういった臨機応変な切り替えが大事だ。
以下の例ではPlaybook実行時のパラメータに基づいて参照する変数を変更する。読み込む変数ファイルを切り替えずに変数だけを切り替えたい場合に便利かも知れない。私は滅多に使わないが。

```yaml
# ansible-playbook var_dynamic.yml -e type=human -e age=21

- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - vars:
        animals:
          cat:
            message: meow
          dog:
            message: bow
          human:
            message: mewl
        age_21:
          message: Don't panic, 21 is only half the Truth.
        age_42:
          message: Answer to the Ultimate Question of Life, the Universe, and Everything is 42.
      ansible.builtin.debug:
        msg:
          - "{{ animals[type].message }} - {{ lookup('vars','age_'+age).message }}"
```

# 4. list や dictionary を結合する
レゴブロックはブロック同士を繋げて新たなる世界を創造する。つまり何かと何かを繋げるという処理は可能性の獣である。
以下の例は変数を結合してデータ構造を変更する。データの重複を避けつつタスクに応じて異なるデータ構造で渡す場合に便利かも知れない。MongoDBのURIを組み立てるときに便利だった気がする。

```yaml
- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - ansible.builtin.debug:
        msg: "{{ ['cat','dog','human'] + ['meow','bow','mewl'] }}"
    - ansible.builtin.debug:
        msg: "{{ {'cat':'meow'} | combine({'dog':'bow'}) | combine({'human':'mewl'}) }}"
    - ansible.builtin.debug:
        msg: "{{ ['cat'] | map('join') | product(['01','02','03']) | map('join') | product(['.meow.local']) | map('join') }}"
```

# 5. DictionaryをJSONで書く
チェコ語を話す人にポーランド語で話しかけても何となく意味が通じてしまうのと同様に、AnsibleでYAMLではなくJSONで書いても何となく動いてしまう。
以下の例はDictionaryをJSONで記述することでJinja2テンプレートを使ってIntegerの値を渡す。URIモジュールでIntegerを含むJSONペイロードをPOSTするときに便利かも知れない。もう少しスマートな方法がありそうな気もするが。

```yaml
- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - vars:
        age: 3
        data: >
          {
              "cat": {
                  "age": {{ age }}
              }
          }
      ansible.builtin.debug:
        msg: "{{ data }}"
```

# 6. JSONをフィルタリングする
私にとってJSONフィルタリングは正規表現に次ぐ苦行である。こいつはAWS CLIのqueryやfilterオプション、kubectlのjsonpathオプション、もしくはそれらコマンドの出力をjqに渡すなど様々な場面で登場し、人生における貴重な時間を奪ってくる。Ansibleのjson_queryも残念ながら例外ではない。
以下の例は特定の条件に合致するAWS EC2インスタンスのNameタグとセキュリティグループを取得する。書いておいて何だがこれ普通の処理だった。あまりキモくない。

```yaml
- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - register: reg_ec2
      community.aws.ec2_instance_info:
        region: ap-northeast-1
    - vars:
        q: "instances[?contains(tags.Name,'test')].{name: tags.Name, secgrps: security_groups[].group_name}"
      ansible.builtin.debug:
        msg: "{{ reg_ec2 | json_query(q) }}"
```

# 7. 変数ごとに暗号化する
Ansible Vaultでファイル丸ごと暗号化すると碌な事がない。テキスト検索できないしマージコンフリクトなんて解消できる気がしない。少し面倒だが、パスワード部分の文字列だけ暗号化すれば平文と同様の扱いが出来る。
以下の例は指定された文字列をAnsible Vaultで暗号化しAnsibleの変数として表示する。

```bash
#!/bin/bash -eu
if [ ${#} -ne 3 ];then
    echo "Usage: $(basename "$0") <VARIABLE_NAME> <STRING_TO_ENCRYPT> <VAULT_PASSWORD_FILE>"
    exit 1
fi
echo "${1}: "'!vault |'
echo -n "${2}" | ansible-vault encrypt --vault-password-file ${3} | awk '{printf "%2s%s\n","",$0}'
```

# 8. ドキュメントに無いパラメータを使う
AnsibleのAWS用モジュールは渡されたすべてのパラメータを検証している訳ではない。ドキュメントに記載されていないパラメータを指定した場合、パラメータが検証されずにboto3に渡されて期待通りの動作をしてくれることもある。
以下の例はcloudfront_distributionモジュールがcache_behaviorのパラメータ検証時にfunction_associationsを検証しないことを利用して強引にCloudFunctionをリンクする。教えてくれたのは次のコメントだ。
- https://github.com/ansible-collections/community.aws/issues/750#issuecomment-1688809743

```yaml
- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - community.aws.cloudfront_distribution:
        caller_reference: rickdias
        state: present
        origins:
          - id: example
            domain_name: example.com
        default_cache_behavior:
          target_origin_id: example
          forwarded_values:
            headers: []
          function_associations:
            quantity: 1
            items:
              - event_type: viewer-response
                function_a_r_n: "arn:aws:cloudfront::1234567890:function/cf"
```

おしまい