背景
Ansibleは便利な構成管理ツールであり、Roleを開発することでサーバ構築、ネットワーク接続、クラウドサービスの設定などを複数まとめて行ってくれます。
しかし、AnsibleのRoleはRoleそのものの修正や、設定先のOSのバージョンアップやAnsible自体のバージョンアップで修正する必要が出てくる時があります。修正の度にRoleが問題ないかテストする必要が出てきますが、何回もテストをするという煩雑さがありました。
そこで今回は、Moleculeの基礎を触ってAnsible Roleのテスト自動化をできるための準備を進めてみたいと思います。
※参考文献 SoftwareDesign 2020/6月号
(https://gihyo.jp/magazine/SD/archive/2020/202006)
Moleculeとは
MoleculeはAnsible Roleのテストを支援してくれるためのツールです。
https://molecule.readthedocs.io/en/latest/
MoluculeはAnsibleの最新安定バージョンと一個前のバージョンのみサポートされています。
(2020/10現在のAnsibleの最新安定バージョンは2.9系なため、2.8系までサポート対象です)
他にも要求スペックとしては、Python3.6系以上で、2系が対象外であることが挙げられます。
OS毎にインストール手順が異なっていますが、今回はCentOS 8にインストールしてみました。
インストール方法
基本的には公式リファレンスを基にインストール作業を進めています。pipパッケージでインストールしますので、pipに必要なパッケージを事前にインストールします。
※CentOS8の場合
sudo yum install -y gcc python3-pip python3-devel openssl-devel python3-libselinux
pipパッケージのインストールが完了しましたら、pipでmoluculeをインストールします。
python3 -m pip install --user "molecule[lint]"
次に今回テストに使用するpodmanドライバをインストールします。
python3 -m pip install --user 'molecule[podman]'
初期設定
Moleculeのインストールが完了しましたらテンプレートを用意します。
$ molecule init role testmole
--> Initializing new role testmole...
Initialized role in /work/testmole successfully.
$ tree testmole/
testmole/
├── README.md
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── molecule
│ └── default
│ ├── INSTALL.rst
│ ├── converge.yml
│ ├── molecule.yml
│ └── verify.yml
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
10 directories, 12 files
molecule init role ロール名
でmoleculeを実行できるテンプレートディレクトリ構成が構築されます。(ここではtestmole
というディレクトリを作成)
ansible-galaxy init
と似たようなディレクトリ構成ですが、変更点としてmolecule
ディレクトリが作成されていることが大きな特徴となっております。
この段階でテストの試し打ちを行うこともできます。
$ cd testmole && molecule test
--> Test matrix
└── default
├── dependency
├── lint
├── cleanup
├── destroy
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
├── cleanup
└── destroy
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'lint'
--> Lint is disabled.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
--> Sanity checks: 'podman'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) deletion to complete] *******************************
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
--> Scenario: 'default'
--> Action: 'syntax'
playbook: /home/yuhta/Desktop/Molecule/testmole/molecule/default/converge.yml
--> Scenario: 'default'
--> Action: 'create'
PLAY [Create] ******************************************************************
TASK [Log into a container registry] *******************************************
skipping: [localhost] => (item=None)
TASK [Check presence of custom Dockerfiles] ************************************
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Create Dockerfiles from image names] *************************************
skipping: [localhost] => (item=None)
TASK [Discover local Podman images] ********************************************
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Build an Ansible compatible image] ***************************************
skipping: [localhost] => (item={'changed': False, 'skipped': True, 'skip_reason': 'Conditional result was False', 'item': {'image': 'docker.io/pycontribs/centos:8', 'name': 'instance', 'pre_build_image': True}, 'ansible_loop_var': 'item', 'i': 0, 'ansible_index_var': 'i'})
TASK [Determine the CMD directives] ********************************************
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Create molecule instance(s)] *********************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) creation to complete] *******************************
FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (299 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (298 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (297 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (296 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (295 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (294 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (293 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (292 retries left).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~中略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
FAILED - RETRYING: Wait for instance(s) creation to complete (256 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (255 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (254 retries left).
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=5 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'
PLAY [Converge] ****************************************************************
TASK [Gathering Facts] *********************************************************
[WARNING]: Unhandled error in Python interpreter discovery for host instance:
Expecting value: line 1 column 1 (char 0)
ok: [instance]
[WARNING]: Platform linux on host instance is using the discovered Python
interpreter at /usr/bin/python3.6, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
TASK [Include testmole] ********************************************************
PLAY RECAP *********************************************************************
instance : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Running Ansible Verifier
PLAY [Verify] ******************************************************************
TASK [Gathering Facts] *********************************************************
[WARNING]: Unhandled error in Python interpreter discovery for host instance:
Expecting value: line 1 column 1 (char 0)
ok: [instance]
[WARNING]: Platform linux on host instance is using the discovered Python
interpreter at /usr/bin/python3.6, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
TASK [Example assertion] *******************************************************
ok: [instance] => {
"changed": false,
"msg": "All assertions passed"
}
PLAY RECAP *********************************************************************
instance : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) deletion to complete] *******************************
FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
FAILED - RETRYING: Wait for instance(s) deletion to complete (299 retries left).
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
--> Pruning extra files from scenario ephemeral directory
moleculeはシナリオと呼ばれるテスト前準備を含めた一連の処理を行います。先ほどのmolecule test
で、冒頭にScenario: 'default'
と呼ばれるものがそれにあたります。
シナリオの中にはステップと呼ばれるPlaybookに対するテストステップが行われています。
molecule test
コマンドで以下のステップが順次実行されRoleのテストを行います。
ステップ | 実施内容 |
---|---|
dependency | 依存関係を処理 |
lint | 規約チェック |
cleanup | 環境の掃除 |
destroy | テスト環境の削除 |
syntax | 構文チェック |
create | テスト環境の構築 |
prepare | テストの前処理を実行 |
converge | Roleの実行 |
idempotence | Roleの再実行 |
side_effect | テスト実行のための副作用を発生させる |
verify | テストの実行 |
clenup | 環境の掃除 |
destroy | テスト環境の削除 |
※引用 SoftwareDesign 2020/6月号 | |
(https://gihyo.jp/magazine/SD/archive/2020/202006) |
テスト実装
今回はシンプルにCentOS 7と8のDockerコンテナにApacheを導入して、自動起動を有効化し起動できるというRoleを作成し、それに対してMoleculeでテストを行うということを検証していきます。まずは、練習も兼ねてtestmole/molecule/default/molucule.yml
を以下の内容に書き換えます。
---
dependency:
name: galaxy
driver:
# ドライバ定義を指定する。Dockerなども指定可能
name: podman
# ここでテスト対象となるドライバの詳細な定義を設定
platforms:
- name: instance1
image: docker.io/centos:7
pre_build_image: true
privileged: True
command: /sbin/init
- name: instance2
image: docker.io/centos:8
pre_build_image: true
privileged: True
command: /sbin/init
provisioner:
name: ansible
verifier:
name: ansible
このYAMLファイルでは、テスト対象となるコンテナの名前と起動イメージ、起動オプションを定義しております。先ほどのステップはmoleculeのサブコマンドとして用意されていますので、Roleを実行するmolecule converge
を実行します。(実行ディレクトリはロール名ディレクトリの直下です)
$ molecule converge
--> Test matrix
└── default
├── dependency
├── create
├── prepare
└── converge
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'create'
--> Sanity checks: 'podman'
PLAY [Create] ******************************************************************
TASK [Log into a container registry] *******************************************
skipping: [localhost] => (item=None)
skipping: [localhost] => (item=None)
TASK [Check presence of custom Dockerfiles] ************************************
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Create Dockerfiles from image names] *************************************
skipping: [localhost] => (item=None)
skipping: [localhost] => (item=None)
TASK [Discover local Podman images] ********************************************
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Build an Ansible compatible image] ***************************************
skipping: [localhost] => (item={'changed': False, 'skipped': True, 'skip_reason': 'Conditional result was False', 'item': {'command': '/sbin/init', 'image': 'docker.io/pycontribs/centos:7', 'name': 'instance1', 'pre_build_image': True, 'privileged': True}, 'ansible_loop_var': 'item', 'i': 0, 'ansible_index_var': 'i'})
skipping: [localhost] => (item={'changed': False, 'skipped': True, 'skip_reason': 'Conditional result was False', 'item': {'command': '/sbin/init', 'image': 'docker.io/pycontribs/centos:8', 'name': 'instalce2', 'pre_build_image': True, 'privileged': True}, 'ansible_loop_var': 'item', 'i': 1, 'ansible_index_var': 'i'})
TASK [Determine the CMD directives] ********************************************
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Create molecule instance(s)] *********************************************
changed: [localhost] => (item=None)
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) creation to complete] *******************************
FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (299 retries left).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~中略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
FAILED - RETRYING: Wait for instance(s) creation to complete (269 retries left).
FAILED - RETRYING: Wait for instance(s) creation to complete (268 retries left).
changed: [localhost] => (item=None)
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=5 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'
PLAY [Converge] ****************************************************************
TASK [Gathering Facts] *********************************************************
[WARNING]: Unhandled error in Python interpreter discovery for host instance1:
Expecting value: line 1 column 1 (char 0)
[WARNING]: Unhandled error in Python interpreter discovery for host instalce2:
Expecting value: line 1 column 1 (char 0)
ok: [instance1]
[WARNING]: Platform linux on host instance1 is using the discovered Python
interpreter at /usr/bin/python, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
ok: [instalce2]
[WARNING]: Platform linux on host instalce2 is using the discovered Python
interpreter at /usr/bin/python3.6, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
TASK [Include testmole] ********************************************************
PLAY RECAP *********************************************************************
instalce2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
instance1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
molecule list
を実行すれば起動したテスト環境の状態が確認できます。
$ molecule list
Instance Name Driver Name Provisioner Name Scenario Name Created Converged
--------------- ------------- ------------------ --------------- --------- -----------
instance1 podman ansible default true true
instalce2 podman ansible default true true
削除はmolecule destory
で行えます。
$ molecule destroy
--> Test matrix
└── default
├── dependency
├── cleanup
└── destroy
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
--> Sanity checks: 'podman'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) deletion to complete] *******************************
changed: [localhost] => (item=None)
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
--> Pruning extra files from scenario ephemeral directory
さて、ここからは本命となるテストをverify.ymlに記述していきます。以下の内容をtestmole/molecule/default/verify.yml
に記述していきます。
---
# This is httpd install playbook to execute Ansible tests.
- name: Verify
hosts: all
tasks:
- ignore_errors: yes
block:
- name: httpdパッケージの存在を確認する
yum:
list: httpd
register: result_rpm
- name: httpdプロセスが起動していることを確認する
shell: ps -ef | grep http[d]
register: result_proc
- name: httpdサービスが自動起動になっているかを確認する
shell: systemctl is-enabled httpd
register: result_enabled
- name: 結果をまとめて確認する
assert:
that: "{{ result.failed == false }}"
loop:
- "{{ result_rpm }}"
- "{{ result_proc }}"
- "{{ result_enabled }}"
loop_control:
loop_var: result
これで完成かと思い、molecule test
を実行してみますと以下のエラーが出てくると思います。
$ molecule test
--> Test matrix
└── default
├── dependency
├── lint
├── cleanup
├── destroy
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
├── cleanup
└── destroy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~中略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PLAY [Verify] ******************************************************************
TASK [Gathering Facts] *********************************************************
[WARNING]: Unhandled error in Python interpreter discovery for host instalce2:
Expecting value: line 1 column 1 (char 0)
[WARNING]: Unhandled error in Python interpreter discovery for host instance1:
Expecting value: line 1 column 1 (char 0)
ok: [instance1]
[WARNING]: Platform linux on host instance1 is using the discovered Python
interpreter at /usr/bin/python, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
ok: [instalce2]
[WARNING]: Platform linux on host instalce2 is using the discovered Python
interpreter at /usr/bin/python3.6, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
TASK [httpdパッケージの存在を確認する] ******************************************************
ok: [instance1]
ok: [instalce2]
TASK [httpdプロセスが起動していることを確認する] *************************************************
...ignoring
fatal: [instalce2]: FAILED! => {"changed": true, "cmd": "ps -ef | grep http[d]", "delta": "0:00:00.378992", "end": "2020-10-18 14:08:21.534917", "msg": "non-zero return code", "rc": 1, "start": "2020-10-18 14:08:21.155925", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring
fatal: [instance1]: FAILED! => {"changed": true, "cmd": "ps -ef | grep http[d]", "delta": "0:00:00.432263", "end": "2020-10-18 14:08:21.944257", "msg": "non-zero return code", "rc": 1, "start": "2020-10-18 14:08:21.511994", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
TASK [httpdサービスが自動起動になっているかを確認する] **********************************************
...ignoring
fatal: [instance1]: FAILED! => {"changed": true, "cmd": "systemctl is-enabled httpd", "delta": "0:00:00.379746", "end": "2020-10-18 14:08:27.254545", "msg": "non-zero return code", "rc": 1, "start": "2020-10-18 14:08:26.874799", "stderr": "Failed to get unit file state for httpd.service: No such file or directory", "stderr_lines": ["Failed to get unit file state for httpd.service: No such file or directory"], "stdout": "", "stdout_lines": []}
...ignoring
fatal: [instalce2]: FAILED! => {"changed": true, "cmd": "systemctl is-enabled httpd", "delta": "0:00:00.255231", "end": "2020-10-18 14:08:27.240254", "msg": "non-zero return code", "rc": 1, "start": "2020-10-18 14:08:26.985023", "stderr": "Failed to get unit file state for httpd.service: No such file or directory", "stderr_lines": ["Failed to get unit file state for httpd.service: No such file or directory"], "stdout": "", "stdout_lines": []}
TASK [結果をまとめて確認する] *************************************************************
ok: [instance1] => (item={'results': [{'envra': '0:httpd-2.4.6-93.el7.centos.x86_64', 'name': 'httpd', 'repo': 'base', 'epoch': '0', 'version': '2.4.6', 'release': '93.el7.centos', 'yumstate': 'available', 'arch': 'x86_64'}], 'failed': False, 'changed': False}) => {
"ansible_loop_var": "result",
"changed": false,
"msg": "All assertions passed",
"result": {
"changed": false,
"failed": false,
"results": [
{
"arch": "x86_64",
"envra": "0:httpd-2.4.6-93.el7.centos.x86_64",
"epoch": "0",
"name": "httpd",
"release": "93.el7.centos",
"repo": "base",
"version": "2.4.6",
"yumstate": "available"
}
]
}
}
ok: [instalce2] => (item={'msg': '', 'results': [{'name': 'httpd', 'arch': 'x86_64', 'epoch': '0', 'release': '21.module_el8.2.0+494+1df74eae', 'version': '2.4.37', 'repo': 'AppStream', 'nevra': '0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64', 'yumstate': 'available'}], 'failed': False, 'changed': False}) => {
"ansible_loop_var": "result",
"changed": false,
"msg": "All assertions passed",
"result": {
"changed": false,
"failed": false,
"msg": "",
"results": [
{
"arch": "x86_64",
"epoch": "0",
"name": "httpd",
"nevra": "0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64",
"release": "21.module_el8.2.0+494+1df74eae",
"repo": "AppStream",
"version": "2.4.37",
"yumstate": "available"
}
]
}
}
failed: [instalce2] (item={'msg': 'non-zero return code', 'cmd': 'ps -ef | grep http[d]', 'stdout': '', 'stderr': '', 'rc': 1, 'start': '2020-10-18 14:08:21.155925', 'end': '2020-10-18 14:08:21.534917', 'delta': '0:00:00.378992', 'changed': True, 'failed': True, 'stdout_lines': [], 'stderr_lines': []}) => {
"ansible_loop_var": "result",
"assertion": false,
"changed": false,
"evaluated_to": false,
"msg": "Assertion failed",
"result": {
"changed": true,
"cmd": "ps -ef | grep http[d]",
"delta": "0:00:00.378992",
"end": "2020-10-18 14:08:21.534917",
"failed": true,
"msg": "non-zero return code",
"rc": 1,
"start": "2020-10-18 14:08:21.155925",
"stderr": "",
"stderr_lines": [],
"stdout": "",
"stdout_lines": []
}
}
failed: [instance1] (item={'changed': True, 'end': '2020-10-18 14:08:21.944257', 'stdout': '', 'cmd': 'ps -ef | grep http[d]', 'failed': True, 'delta': '0:00:00.432263', 'stderr': '', 'rc': 1, 'start': '2020-10-18 14:08:21.511994', 'msg': 'non-zero return code', 'stdout_lines': [], 'stderr_lines': []}) => {
"ansible_loop_var": "result",
"assertion": false,
"changed": false,
"evaluated_to": false,
"msg": "Assertion failed",
"result": {
"changed": true,
"cmd": "ps -ef | grep http[d]",
"delta": "0:00:00.432263",
"end": "2020-10-18 14:08:21.944257",
"failed": true,
"msg": "non-zero return code",
"rc": 1,
"start": "2020-10-18 14:08:21.511994",
"stderr": "",
"stderr_lines": [],
"stdout": "",
"stdout_lines": []
}
}
failed: [instance1] (item={'changed': True, 'end': '2020-10-18 14:08:27.254545', 'stdout': '', 'cmd': 'systemctl is-enabled httpd', 'failed': True, 'delta': '0:00:00.379746', 'stderr': 'Failed to get unit file state for httpd.service: No such file or directory', 'rc': 1, 'start': '2020-10-18 14:08:26.874799', 'msg': 'non-zero return code', 'stdout_lines': [], 'stderr_lines': ['Failed to get unit file state for httpd.service: No such file or directory']}) => {
"ansible_loop_var": "result",
"assertion": false,
"changed": false,
"evaluated_to": false,
"msg": "Assertion failed",
"result": {
"changed": true,
"cmd": "systemctl is-enabled httpd",
"delta": "0:00:00.379746",
"end": "2020-10-18 14:08:27.254545",
"failed": true,
"msg": "non-zero return code",
"rc": 1,
"start": "2020-10-18 14:08:26.874799",
"stderr": "Failed to get unit file state for httpd.service: No such file or directory",
"stderr_lines": [
"Failed to get unit file state for httpd.service: No such file or directory"
],
"stdout": "",
"stdout_lines": []
}
}
failed: [instalce2] (item={'msg': 'non-zero return code', 'cmd': 'systemctl is-enabled httpd', 'stdout': '', 'stderr': 'Failed to get unit file state for httpd.service: No such file or directory', 'rc': 1, 'start': '2020-10-18 14:08:26.985023', 'end': '2020-10-18 14:08:27.240254', 'delta': '0:00:00.255231', 'changed': True, 'failed': True, 'stdout_lines': [], 'stderr_lines': ['Failed to get unit file state for httpd.service: No such file or directory']}) => {
"ansible_loop_var": "result",
"assertion": false,
"changed": false,
"evaluated_to": false,
"msg": "Assertion failed",
"result": {
"changed": true,
"cmd": "systemctl is-enabled httpd",
"delta": "0:00:00.255231",
"end": "2020-10-18 14:08:27.240254",
"failed": true,
"msg": "non-zero return code",
"rc": 1,
"start": "2020-10-18 14:08:26.985023",
"stderr": "Failed to get unit file state for httpd.service: No such file or directory",
"stderr_lines": [
"Failed to get unit file state for httpd.service: No such file or directory"
],
"stdout": "",
"stdout_lines": []
}
}
PLAY RECAP *********************************************************************
instalce2 : ok=4 changed=2 unreachable=0 failed=1 skipped=0 rescued=0 ignored=2
instance1 : ok=4 changed=2 unreachable=0 failed=1 skipped=0 rescued=0 ignored=2
ERROR:
An error occurred during the test sequence action: 'verify'. Cleaning up.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) deletion to complete] *******************************
changed: [localhost] => (item=None)
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
--> Pruning extra files from scenario ephemeral directory
途中のエラーはRoleの本体がないため仕様が満たせていないことを意味します。testmole/tasks/main.yml
にタスクを記載します。
---
# tasks file for testmole
- name: install httpd package
yum:
name: httpd
state: latest
- name: start httpd
systemd:
name: httpd
state: started
enabled: yes
ここまでできましたら、molecule test
でテストを実行してみます。
$ molecule test
--> Test matrix
└── default
├── dependency
├── lint
├── cleanup
├── destroy
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
├── cleanup
└── destroy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~中略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PLAY [Verify] ******************************************************************
TASK [Gathering Facts] *********************************************************
[WARNING]: Unhandled error in Python interpreter discovery for host instance1:
Expecting value: line 1 column 1 (char 0)
[WARNING]: Unhandled error in Python interpreter discovery for host instalce2:
Expecting value: line 1 column 1 (char 0)
ok: [instance1]
[WARNING]: Platform linux on host instance1 is using the discovered Python
interpreter at /usr/bin/python, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
ok: [instalce2]
[WARNING]: Platform linux on host instalce2 is using the discovered Python
interpreter at /usr/bin/python3.6, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
TASK [httpdパッケージの存在を確認する] ******************************************************
ok: [instalce2]
ok: [instance1]
TASK [httpdプロセスが起動していることを確認する] *************************************************
changed: [instance1]
changed: [instalce2]
TASK [httpdサービスが自動起動になっているかを確認する] **********************************************
changed: [instance1]
changed: [instalce2]
TASK [結果をまとめて確認する] *************************************************************
ok: [instalce2] => (item={'msg': '', 'results': [{'name': 'httpd', 'arch': 'x86_64', 'epoch': '0', 'release': '21.module_el8.2.0+494+1df74eae', 'version': '2.4.37', 'repo': '@System', 'nevra': '0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64', 'yumstate': 'installed'}, {'name': 'httpd', 'arch': 'x86_64', 'epoch': '0', 'release': '21.module_el8.2.0+494+1df74eae', 'version': '2.4.37', 'repo': 'AppStream', 'nevra': '0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64', 'yumstate': 'available'}], 'failed': False, 'changed': False}) => {
"ansible_loop_var": "result",
"changed": false,
"msg": "All assertions passed",
"result": {
"changed": false,
"failed": false,
"msg": "",
"results": [
{
"arch": "x86_64",
"epoch": "0",
"name": "httpd",
"nevra": "0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64",
"release": "21.module_el8.2.0+494+1df74eae",
"repo": "@System",
"version": "2.4.37",
"yumstate": "installed"
},
{
"arch": "x86_64",
"epoch": "0",
"name": "httpd",
"nevra": "0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64",
"release": "21.module_el8.2.0+494+1df74eae",
"repo": "AppStream",
"version": "2.4.37",
"yumstate": "available"
}
]
}
}
ok: [instalce2] => (item={'cmd': 'ps -ef | grep http[d]', 'stdout': 'root 278 1 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 279 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 280 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 281 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 282 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'stderr': '', 'rc': 0, 'start': '2020-10-18 14:20:51.280186', 'end': '2020-10-18 14:20:51.452977', 'delta': '0:00:00.172791', 'changed': True, 'stdout_lines': ['root 278 1 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 279 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 280 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 281 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 282 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND'], 'stderr_lines': [], 'failed': False}) => {
"ansible_loop_var": "result",
"changed": false,
"msg": "All assertions passed",
"result": {
"changed": true,
"cmd": "ps -ef | grep http[d]",
"delta": "0:00:00.172791",
"end": "2020-10-18 14:20:51.452977",
"failed": false,
"rc": 0,
"start": "2020-10-18 14:20:51.280186",
"stderr": "",
"stderr_lines": [],
"stdout": "root 278 1 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 279 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 280 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 281 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 282 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"stdout_lines": [
"root 278 1 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 279 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 280 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 281 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 282 278 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND"
]
}
}
ok: [instalce2] => (item={'cmd': 'systemctl is-enabled httpd', 'stdout': 'enabled', 'stderr': '', 'rc': 0, 'start': '2020-10-18 14:20:56.569809', 'end': '2020-10-18 14:20:56.595759', 'delta': '0:00:00.025950', 'changed': True, 'stdout_lines': ['enabled'], 'stderr_lines': [], 'failed': False}) => {
"ansible_loop_var": "result",
"changed": false,
"msg": "All assertions passed",
"result": {
"changed": true,
"cmd": "systemctl is-enabled httpd",
"delta": "0:00:00.025950",
"end": "2020-10-18 14:20:56.595759",
"failed": false,
"rc": 0,
"start": "2020-10-18 14:20:56.569809",
"stderr": "",
"stderr_lines": [],
"stdout": "enabled",
"stdout_lines": [
"enabled"
]
}
}
ok: [instance1] => (item={'results': [{'envra': '0:httpd-2.4.6-93.el7.centos.x86_64', 'name': 'httpd', 'repo': 'base', 'epoch': '0', 'version': '2.4.6', 'release': '93.el7.centos', 'yumstate': 'available', 'arch': 'x86_64'}, {'envra': '0:httpd-2.4.6-93.el7.centos.x86_64', 'name': 'httpd', 'repo': 'installed', 'epoch': '0', 'version': '2.4.6', 'release': '93.el7.centos', 'yumstate': 'installed', 'arch': 'x86_64'}], 'failed': False, 'changed': False}) => {
"ansible_loop_var": "result",
"changed": false,
"msg": "All assertions passed",
"result": {
"changed": false,
"failed": false,
"results": [
{
"arch": "x86_64",
"envra": "0:httpd-2.4.6-93.el7.centos.x86_64",
"epoch": "0",
"name": "httpd",
"release": "93.el7.centos",
"repo": "base",
"version": "2.4.6",
"yumstate": "available"
},
{
"arch": "x86_64",
"envra": "0:httpd-2.4.6-93.el7.centos.x86_64",
"epoch": "0",
"name": "httpd",
"release": "93.el7.centos",
"repo": "installed",
"version": "2.4.6",
"yumstate": "installed"
}
]
}
}
ok: [instance1] => (item={'changed': True, 'end': '2020-10-18 14:20:51.225378', 'stdout': 'root 337 1 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 338 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 339 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 340 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 341 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 342 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'cmd': 'ps -ef | grep http[d]', 'rc': 0, 'start': '2020-10-18 14:20:50.978546', 'stderr': '', 'delta': '0:00:00.246832', 'stdout_lines': ['root 337 1 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 338 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 339 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 340 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 341 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache 342 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND'], 'stderr_lines': [], 'failed': False}) => {
"ansible_loop_var": "result",
"changed": false,
"msg": "All assertions passed",
"result": {
"changed": true,
"cmd": "ps -ef | grep http[d]",
"delta": "0:00:00.246832",
"end": "2020-10-18 14:20:51.225378",
"failed": false,
"rc": 0,
"start": "2020-10-18 14:20:50.978546",
"stderr": "",
"stderr_lines": [],
"stdout": "root 337 1 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 338 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 339 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 340 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 341 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND\napache 342 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"stdout_lines": [
"root 337 1 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 338 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 339 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 340 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 341 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND",
"apache 342 337 0 14:19 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND"
]
}
}
ok: [instance1] => (item={'changed': True, 'end': '2020-10-18 14:20:56.498353', 'stdout': 'enabled', 'cmd': 'systemctl is-enabled httpd', 'rc': 0, 'start': '2020-10-18 14:20:56.467774', 'stderr': '', 'delta': '0:00:00.030579', 'stdout_lines': ['enabled'], 'stderr_lines': [], 'failed': False}) => {
"ansible_loop_var": "result",
"changed": false,
"msg": "All assertions passed",
"result": {
"changed": true,
"cmd": "systemctl is-enabled httpd",
"delta": "0:00:00.030579",
"end": "2020-10-18 14:20:56.498353",
"failed": false,
"rc": 0,
"start": "2020-10-18 14:20:56.467774",
"stderr": "",
"stderr_lines": [],
"stdout": "enabled",
"stdout_lines": [
"enabled"
]
}
}
PLAY RECAP *********************************************************************
instalce2 : ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
instance1 : ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) deletion to complete] *******************************
changed: [localhost] => (item=None)
FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
--> Pruning extra files from scenario ephemeral directory
エラーがなくテストが問題なく完了していることが確認できます。
(最後にテスト環境は破棄されていますので、molecule list
を実行しますとCreatedがfalseになっていることがわかります)
$ molecule list
Instance Name Driver Name Provisioner Name Scenario Name Created Converged
--------------- ------------- ------------------ --------------- --------- -----------
instance1 podman ansible default false false
instalce2 podman ansible default false false
所感
今回は、Moleculeを単独で使ってAnsible Roleのテストを行いました。これをGitに登録してCIツールと連携させればテストの自動化もできます。
AnsibleのRoleは何度か修正や改訂を行う機会が多く、そのたびに人の手で確認するのは大変なので、Moleculeを使って、インフラ構築もテスト自動化できるように研究を続けていきたいと思います。