Ansible Tips: copyモジュールで(Chef cookbook_fileリソースの様に)ファイル配布元を自動的に選択させる例


## 目的1: copy元を自動的に選択

Ansibleのcopyモジュールに相当するChefのcookbook_fileリソースでは、copy元は、COOKBOOK_NAME/files/下で以下に最初にマッチしたファイルが使用される。

1. /host-\$fqdn/\$source
1. /\$platform-\$platform_version/\$source
1. /\$platform/\$source
1. /default/\$source
1. /\$source

https://docs.chef.io/resources/cookbook_file/
https://docs.chef.io/resources/cookbook_file/#cookbook_file_specificity

しかしansibleのcopyモジュールには、その様なcopy元自動選択機能はない。
その様な機能を実装するにはどうするか。

## 目的2 ファイル配布元をinventory_dir/files下とする

Ansible copyモジュールのファイル配布元は、相対パス指定の場合、playbook_dir/files, playbook_dir 下から探される様子。

しかし、playbookはロジック、inventoryはデータと切り分けをしたい場合、配布元はplaybook_dir下よりinventory_dir下が望ましい。

### 追記/訂正

ansible-playbookコマンドの-iオプションで指定可能なインベントリーには、-i inventory_dir の様にディレクトリーを指定することも可能。
インベントリーファイルそのものやinventory_dir/host_varsやinventory_dir/group_varsを配置して利用可能。
しかし、inventory_dir/filesの様に本来のインベントリー情報以外のファイルが含まれると、エラーとなってしまう。その為、ディレクトリー指定を考慮するならば、inventory_dirの外部に置いたほうが良い。




## 最終目的 

Chefのcookbook_fileリソースの様に、以下の順序でcopy元を探索し、最初に見つかったファイルを配布したい。

1. "{{ inventory_dir }}/files/{{ inventory_hostname }}" 下
1. "{{ inventory_dir }}/files/{{ ansible_distribution }}-{{ ansible_distribution_version }}" 下
1. "{{ inventory_dir }}/files/{{ ansible_distribution }}" 下
1. "{{ inventory_dir }}/files/default" 下
1. "{{ inventory_dir }}/files" 下


## 検証環境

- ansible 2.9.4

## inventory_dirイメージ

```
inventory_dir
│
├── hosts             # inventory file
│
├── host_vars         # host_varsはinventory_dir下を使用している
│   ├── host01
│   │   └── main.yml
│   └── host02
│       └── main.yml
│
├── group_vars        # group_varsもinventory_dir下を使用している
│   └── all
│       └── main.yml
│
└── files             # **** ファイル配布元もinventory_dir下に置きたい ****
    ├── default　　　  # 個別のノードで指定しない場合のデフォルト(Chef準拠)
    │   └── etc
    │       └── hosts
    ├── host01        # 個別のノードで指定する場合(Chef準拠)
    │   └── etc
    │       └── hosts
```

## 対応するtask例

```yaml
  - name: copy files from inventory_dir/files
    copy:
      src: "{{ lookup('first_found', paths|map('regex_replace','$',item)|list ) }}"
      dest: "{{ item }}"
    vars:
      paths:
      - "{{ inventory_dir }}/files/{{ inventory_hostname }}"
      - "{{ inventory_dir }}/files/{{ ansible_distribution }}-{{ ansible_distribution_version }}"
      - "{{ inventory_dir }}/files/{{ ansible_distribution }}"
      - "{{ inventory_dir }}/files/default"
      - "{{ inventory_dir }}/files"
    loop:
    - "/etc/hosts"
```	

```
TASK [copy files from inventory_dir/files] *************************************************************************************
ok: [host01] => (item=/etc/hosts)
```

- lookup('first_found', ... にて、コントローラーノード上でリスト順で最初に見つかったファイルをソースとする。
    - with_first_found: を用いると、ファイル名でのloopを行うには、ファイルを分けてimport_tasksなどを使用しなければいけなくなる。
    - jinja2によるfor loopと組み合わせる事でも対応可能。(可読性は落ちるが、柔軟な処理が可能)
- mapフィルターにて、Arrayであるpathsの各要素に操作を行う。
    - 操作を行うフィルターへのパラメーターは、map内での第2パラメーター以降で設定する。
        - つまり、各要素に | regex_replace('$',item) というフィルターを掛け、各要素の最後にファイル名を付与する
    - 最後にlistフィルターでArrayに戻す。
- pathsに探索順にパスを定義
    - inventory_dir や inventory_hostname は Ansible が設定する値
- copy対象ファイルはloop:にて回す
    - パス情報の管理を容易とするために絶対パス指定としている

  
## 参照

first_found – return first file found from list
https://docs.ansible.com/ansible/latest/plugins/lookup/first_found.html

ansible 1.6 > using with_first_found in a with_items loop?
https://stackoverrun.com/ja/q/6205524

map(*args, **kwargs)
https://jinja.palletsprojects.com/en/2.11.x/templates/#map

Regular Expression Filters
https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#regular-expression-filters

Ansible Jinja2 filters 正規表現で変数の文字列を置換、抽出する
https://qiita.com/tbuchi888/items/80fded8d11366e967290