【Ansible】moleculeでroleの動作確認がしたい

### はじめに
<バージョン>
ansible: 2.9.17
molecule: 3.3.0

　[[Ansible] molecule でネットワーク機器の設定後のテストを自動化する（いまいち編）](https://tekunabe.hatenablog.jp/entry/2020/04/12/ansible_molecule_nw)の記事
（以後、参考記事）にあるように、moleculeでネットワーク機器用roleのテストが自動化
できそうなので色々調べてみました。
今回はvMXに対してshowコマンドの結果を取得するroleをmoleculeで動作確認します。

### 1. roleの構成
　参考記事の構成をもとにroleを作成しました。roleのディレクトリを
`molecule init role <ロール名(今回はrole_test)>`で作成し、不要なファイルは削除しました。
違いとしては、インベントリがhost.ymlとして別途分けています。

```text
role_test  
│
├── molecule
│   └── default
│       ├── inventory
│       │  └── hosts.yml    ☆ココが違う
│       ├── INSTALL.rst
│       ├── converge.yml
│       ├── molecule.yml
│       └── verify.yml
└── tasks
    └── main.yml
```

### 2. 関連ファイル（molecule.yml）
　今回の記事で一番重要な部分なので単独で章を作ります。案外、参考資料が少なくて調べるのに苦労しました。
ポイントは以下にまとめます。詳細な設定は、[Molecule 3.2.5.dev6 documentation](https://molecule.readthedocs.io/en/latest/configuration.html)に載っています。
__driver__
　接続対象のホストがすでに存在する場合は`name: delegated`と書けばよいです。
moleculeは実行時にdockerでインスタンスを構築して動作確認ということも可能なようです。
[Developing and Testing Ansible Roles with Molecule and Podman - Part 1](https://www.ansible.com/blog/developing-and-testing-ansible-roles-with-molecule-and-podman-part-1)
__provisioner__
　ansibleでいう、ansible.cfgやインベントリの設定が行える部分です。インベントリは、
hosts.ymlを参照するようにして、`config_options`で`gather_facts: false`にする設定を入れました。
※インベントリの内容は次の章で説明します。
__platforms__
　provisionerのインベントリにあるホストの中から、実際どのホストに接続するかを選択します。
__verifier__
　role実行後の確認方法を記載します。今回はplaybook実行後に、別途playbook（verify.yml）
を実行するので、`name: ansible`とします。
__lint__
　playbook実行前にlintを実行したい場合は書いておきましょう。
`yamllint`と`ansible-lint`は別途インストールが必要です。

```yaml:molecule.yml
---
dependency:                        # 依存関係のあるファイル
  name: galaxy 
  enabled: false                     # Ansible Galaxyは使わないのでfalseにしておく
driver:                            # 対象機器を用意する方法
  name: delegated                    # 接続対象が既に存在する場合、delegatedでよい
provisioner:                       # 環境設定
  name: ansible
  inventory:
    links:
      hosts: ./inventory/hosts.yml   # インベントリのパスを指定(基準はrole名/molecule/default)
  config_options:
    defaults:                        # ansible.cfgの内容を記載可能
      gathering: explicit            # gather_facts: false をデフォルトにする
platforms:                         # 確認対象
  - name: host_01                    # 対象host
verifier:                          # 事後確認方法
  name: ansible                      # 確認内容はverify.ymlに記載
lint: |
  set -e
  yamllint .
  ansible-lint
```

### 3. その他関連ファイル
　以下ファイルについては、moleculeを使うにあたっての工夫は特にありません。

__（1）hosts.yml（インベントリ）__

```yaml:hosts.yml
---
all:
  children:
    test_nw:
      hosts:
        host_01:
          ansible_host: <対象機器のIPアドレス>
      vars:
        ansible_port: 22
        ansible_connection: network_cli
        ansible_network_os: junos
        ansible_user: username
        ansible_ssh_pass: password
        ansible_host_key_checking: false
```

__（2）main.yml__
　roleで実際に実行されるplaybookです。

```yaml:main.yml
---
- name: get show
  junos_command:
    commands: show interfaces fxp0 terse
  register: int_results

- name: get result
  copy:
    content: "{{ int_results.stdout[0] }}"
    dest: "./{{ inventory_hostname }}_result.log"
    mode: "644"
```

__（3）verify.yml__
　roleの後に実行されるplaybookです。今回は適当なassertを入れておきます。
role内で定義した変数は、このplaybookに引き継がれないので注意が必要です。

```yaml:verify.yml
---
- name: Verify
  hosts: all
  tasks:
    - name: Example assertion
      assert:
        that: 
          - ansible_host is defined
```

### 4. molecule実行
　今回は主に以下の流れでmoleculeを動作させます。
（1）lint実行
（2）role実行（main.yml）
（3）冪等性確認で、再度role実行（main.yml）
（4）事後確認（verify.yml）

roleが入っているディレクトリに移動して、`molecule test`コマンドで
動作確認を実施しましょう。

```text
cd /home/centos/roles/role_test
molecule test
```

以下が実行ログです。ログの重要な部分に☆マークを付けておきました。
プロセスに必要なファイルが存在しない場合は、`WARNING  Skipping, <メッセージ>`が表示されて、手順がskipされます。
ポイントとしては、冪等性確認で、再度role実行をした際に結果がchangedになってしまうとエラーになります。

```text
(venv) [centos@ip-<ip addr> role_test]$ molecule test
INFO     default scenario test matrix: dependency, lint, cleanup, destroy, syntax, create, prepare, converge, idempotence, side_effect, verify, cleanup, destroy
INFO     Performing prerun...
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > dependency
WARNING  Skipping, dependency is disabled.
WARNING  Skipping, dependency is disabled.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts

☆lint実行
INFO     Running default > lint
COMMAND: set -e
yamllint .
ansible-lint

Loading custom .yamllint config file, this extends our internal yamllint config.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > cleanup
WARNING  Skipping, cleanup playbook not configured.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > destroy
WARNING  Skipping, destroy action has no playbook.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > syntax

playbook: /home/centos/roles/role_test/molecule/default/converge.yml
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > create
WARNING  Skipping, create action has no playbook.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > prepare
WARNING  Skipping, prepare playbook not configured.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > converge

☆role実行（1回目）
PLAY [Converge] ***************************************************************************************************************************************************

TASK [Include role_test] ******************************************************************************************************************************************

TASK [role_test : get show] ***************************************************************************************************************************************
[WARNING]: arguments wait_for, match, rpcs are not supported when using transport=cli
ok: [host_01]

TASK [role_test : get result] *************************************************************************************************************************************
changed: [host_01]    ☆ここではchanged

PLAY RECAP ********************************************************************************************************************************************************
host_01                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > idempotence

☆role実行（2回目）
PLAY [Converge] ***************************************************************************************************************************************************

TASK [Include role_test] ******************************************************************************************************************************************

TASK [role_test : get show] ***************************************************************************************************************************************
[WARNING]: arguments wait_for, match, rpcs are not supported when using transport=cli
ok: [host_01]

TASK [role_test : get result] *************************************************************************************************************************************
ok: [host_01]    ☆ここがOKなので冪等性が確認された

PLAY RECAP ********************************************************************************************************************************************************
host_01                    : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

INFO     Idempotence completed successfully.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > side_effect
WARNING  Skipping, side effect playbook not configured.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > verify
INFO     Running Ansible Verifier

☆事後確認
PLAY [Verify] *****************************************************************************************************************************************************

TASK [Example assertion] ******************************************************************************************************************************************
ok: [host_01] => {
    "changed": false,
    "msg": "All assertions passed"
}

PLAY RECAP ********************************************************************************************************************************************************
host_01                    : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

INFO     Verifier completed successfully.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > cleanup
WARNING  Skipping, cleanup playbook not configured.
INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > destroy
WARNING  Skipping, destroy action has no playbook.
INFO     Pruning extra files from scenario ephemeral directory
```

### 5. エラーパターン
### 5-1. lintでエラーになった場合
　一度moleculeを実行するとroleのディレクトリに`.yamllint`が作成されるので、
yamllintのカスタマイズが可能です。今回は、以下の設定を入れます。これを入れると、
yamlの1行目に"---"が無い場合はエラーになります。詳細は、[yamllint documentation](https://yamllint.readthedocs.io/en/stable/rules.html#module-yamllint.rules.document_start)をご参照ください。
動作確認のために、hosts.ymlの1行目の"---"を削除します。

```text:.yamllint
<修正前>
  document-start: disable
<修正後>
  document-start:
    present: true
```

以下が実行ログです。想定通りyamllintによってエラーが起こりました。

```text:
INFO     Running default > lint
COMMAND: set -e
yamllint .
ansible-lint

./molecule/default/inventory/hosts.yml
  1:1       warning  missing document start "---"  (document-start)    ☆ エラーが発生

Loading custom .yamllint config file, this extends our internal yamllint config.
WARNING  Listing 1 violation(s) that are fatal
yaml: missing document start "---" (document-start)
molecule/default/inventory/hosts.yml:1

You can skip specific rules or tags by adding them to your configuration file:
# .ansible-lint
warn_list:  # or 'skip_list' to silence them completely
  - yaml  # Violations reported by yamllint
Finished with 1 failure(s), 0 warning(s) on 6 files.
CRITICAL Lint failed with error code 2
```

### 5-2. 冪等性でエラーになった場合
　あえて冪等性でエラーにするためにmain.ymlで[random fileter](https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#random-items-or-numbers)を使い、0～100の間
でランダムな値を発生させます。

```yaml:main.yml
---
- name: random number
  copy:
    content: "{{ 100 | random }}"
    dest: "./{{ inventory_hostname }}_result.log"
    mode: "644"
```

以下が実行ログです。ログの重要な部分に☆マークを付けておきました。
ログのように2回目のrole実行でchangedになった部分があるので、
冪等性（英語でidempotence）でエラーになりました

```text:
☆role実行（1回目）
PLAY [Converge] ***************************************************************************************************************************************************

TASK [Include role_test] ******************************************************************************************************************************************

TASK [role_test : random number] **********************************************************************************************************************************
changed: [host_01]   ☆1回目はchanged

PLAY RECAP ********************************************************************************************************************************************************
host_01                    : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

INFO     Inventory /home/centos/roles/role_test/molecule/default/./inventory/hosts.yml linked to /home/centos/.cache/molecule/role_test/default/inventory/hosts
INFO     Running default > idempotence

☆role実行（2回目）
PLAY [Converge] ***************************************************************************************************************************************************

TASK [Include role_test] ******************************************************************************************************************************************

TASK [role_test : random number] **********************************************************************************************************************************
changed: [host_01]   ☆2回目もchanged

PLAY RECAP ********************************************************************************************************************************************************
host_01                    : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

CRITICAL Idempotence test failed because of the following tasks:
*  => role_test : random number    ☆エラーになったtaskの名前
WARNING  An error occurred during the test sequence action: 'idempotence'. Cleaning up.    ☆冪等性でエラーになる
```

### まとめ
__moleculeのいいところ__

- 既存のroleの構成のままでroleの動作確認機能が追加できる
- `molecule test`コマンド一つで、roleの動作確認・冪等性・lintの確認が行える

__少し気になるところ__

- roleごとにmolecule.ymlを書く必要がある
- verify.ymlにassertを書くくらいなら、role内のtaskに書いた方がよい気がする

### 参考資料
[[Ansible] molecule でネットワーク機器の設定後のテストを自動化する（いまいち編）](https://tekunabe.hatenablog.jp/entry/2020/04/12/ansible_molecule_nw)
[Moleculeに入門してみたよ](https://qiita.com/answer_d/items/0119669f2e6151a86fc3)
[Molecule 3.2.5.dev6 documentation](https://molecule.readthedocs.io/en/latest/configuration.html)
[Molecule で ansible の config を設定する](https://qiita.com/nekottyo/items/8a8fd513ad7ada9db0a7)
[[Ansible] デフォルトを gather_facts: false 扱いにする](https://tekunabe.hatenablog.jp/entry/2020/09/18/ansible_gathering_explicit)
[yamllint documentation](https://yamllint.readthedocs.io/en/stable/)