はじめに
Ansible Moleculeを使ってみました。手元で動かすだけの内容を記載しているだけのページです。
なお、公式ドキュメントとして下記のGetting Startedが用意されています。
https://ansible.readthedocs.io/projects/molecule/working/getting_started/getting_started/
上記手順に完全に沿っているわけではありませんが、Ansible Moleculeを手元で再現するためにドキュメントが少なく色々と試行錯誤しましたので、その備忘録として残しておきます。
前提
ここでは参考までに「ansible-galaxy install nginxinc.nginx」でインストールされたコレクションのディレクトリレイアウトを参考として作ってみることにします。
自分は初心者でありディレクトリのベストプラクティスを知らないので先人の例を参考にしています。
$ tree ~/.ansible/roles/nginxinc.nginx
~/.ansible/roles/nginxinc.nginx
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SUPPORT.md
├── defaults
│ └── main
│ ├── amplify.yml
│ ├── bsd.yml
│ ├── logrotate.yml
│ ├── main.yml
│ ├── selinux.yml
│ └── systemd.yml
├── files
│ ├── license
│ └── services
│ ├── nginx.conf.upstart
│ ├── nginx.openrc
│ ├── nginx.override.conf
│ ├── nginx.systemd
│ ├── nginx.sysvinit
│ └── nginx.upstart
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── molecule
│ ├── common
│ │ └── Dockerfile.j2
│ ├── default
│ │ ├── converge.yml
│ │ ├── molecule.yml
│ │ └── verify.yml
(snip)
│ ├── upgrade-plus
│ │ ├── converge.yml
│ │ ├── molecule.yml
│ │ ├── prepare.yml
│ │ └── verify.yml
│ └── version
│ ├── converge.yml
│ ├── molecule.yml
│ └── verify.yml
├── tasks
│ ├── amplify
│ │ ├── install-amplify.yml
│ │ ├── setup-debian.yml
│ │ └── setup-redhat.yml
│ ├── config
│ │ ├── debug-output.yml
│ │ ├── modify-systemd.yml
│ │ └── setup-logrotate.yml
│ ├── keys
│ │ └── setup-keys.yml
│ ├── main.yml
│ ├── modules
│ │ └── install-modules.yml
(snip)
│ ├── prerequisites
│ │ ├── install-dependencies.yml
│ │ ├── prerequisites.yml
│ │ └── setup-selinux.yml
│ └── validate
│ └── validate.yml
├── templates
│ ├── logrotate
│ │ └── nginx.j2
│ ├── selinux
│ │ └── nginx-plus-module.te.j2
│ └── services
│ └── nginx.service.override.conf.j2
└── vars
└── main.yml
35 directories, 96 files
検証手順を実施した環境
参考までに今回の手順を検証した環境情報を載せておきます。
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.1 LTS
Release: 22.04
Codename: jammy
$ ansible --version
ansible [core 2.15.2]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/tsuyoshi/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3/dist-packages/ansible
ansible collection location = /home/tsuyoshi/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] (/usr/bin/python3)
jinja version = 3.1.2
libyaml = True
$ molecule --version
molecule 6.0.2 using python 3.10
ansible:2.15.2
azure:23.5.0 from molecule_plugins
containers:23.5.0 from molecule_plugins requiring collections: ansible.posix>=1.3.0 community.docker>=1.9.1 containers.podman>=1.8.1
default:6.0.2 from molecule
docker:23.5.0 from molecule_plugins requiring collections: community.docker>=3.0.2 ansible.posix>=1.4.0
ec2:23.5.0 from molecule_plugins
gce:23.5.0 from molecule_plugins requiring collections: google.cloud>=1.0.2 community.crypto>=1.8.0
podman:23.5.0 from molecule_plugins requiring collections: containers.podman>=1.7.0 ansible.posix>=1.3.0
vagrant:23.5.0 from molecule_plugins
手順 (とりあえず手元で動くまで)
まずはansible collectionの雛形を生成します。
azarashi.utilsというのはnamespace名とcollection名を結合した文字列です。
$ ansible-galaxy collection init azarashi.utils
- Collection azarashi.utils was created successfully
$ tree .
.
└── azarashi
└── utils
├── README.md
├── docs
├── galaxy.yml
├── meta
│ └── runtime.yml
├── plugins
│ └── README.md
└── roles
6 directories, 4 files
続いて、roleの雛形を生成します。
$ cd azarashi/utils/roles/
$ ansible-galaxy role init azarashi.utils
- Role azarashi.utils was created successfully
$ tree .
.
└── azarashi.utils
├── README.md
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
9 directories, 8 files
azarashi.utilsに移動してから、moleculeの雛形をシナリオとして「helloworld」を指定します。
パッケージのインストールのroleを検証するシナリオならば「install」、「uninstall」、「update」などのシナリオ名称になると思われます。
$ cd azarashi.utils/
$ molecule init scenario helloworld
INFO Initializing new scenario helloworld...
PLAY [Create a new molecule scenario] ******************************************
TASK [Check if destination folder exists] **************************************
changed: [localhost]
TASK [Check if destination folder is empty] ************************************
ok: [localhost]
TASK [Fail if destination folder is not empty] *********************************
skipping: [localhost]
TASK [Expand templates] ********************************************************
changed: [localhost] => (item=molecule/helloworld/create.yml)
changed: [localhost] => (item=molecule/helloworld/converge.yml)
changed: [localhost] => (item=molecule/helloworld/molecule.yml)
changed: [localhost] => (item=molecule/helloworld/destroy.yml)
PLAY RECAP *********************************************************************
localhost : ok=3 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
INFO Initialized scenario in /home/tsuyoshi/test/azarashi/utils/roles/azarashi.utils/molecule/helloworld successfully.
上記コマンドを実行したことによりmoleculeディレクトリが生成され、その下にシナリオ名であるhelloworldやmoleculeに必要となるymlファイルが生成されています。
$ tree .
.
├── README.md
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── molecule
│ └── helloworld
│ ├── converge.yml
│ ├── create.yml
│ ├── destroy.yml
│ └── molecule.yml
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
10 directories, 12 files
以上でとりあえずは実行の準備が整いました。
「molecule test」では、デフォルトでカレントディレクトリからの相対パスである「molecule/default/molecule.yml」を探索します。
探索パスを「molecule/helloworld/molecule.yml」とするために--scenario-nameオプションを付与してhelloworldシナリオを指定します。
$ pwd
/home/tsuyoshi/test/azarashi/utils/roles/azarashi.utils
$ molecule test --scenario-name helloworld
WARNING The scenario config file ('/home/tsuyoshi/test/azarashi/utils/roles/azarashi.utils/molecule/helloworld/molecule.yml') has been modified since the scenario was created. If recent changes are important, reset the scenario with 'molecule destroy' to clean up created items or 'molecule reset' to clear current configuration.
INFO helloworld scenario test matrix: dependency, cleanup, destroy, syntax, create, prepare, converge, idempotence, side_effect, verify, cleanup, destroy
INFO Performing prerun with role_name_check=0...
INFO Running from /home/tsuyoshi/test/azarashi/utils/roles/azarashi.utils : ansible-galaxy collection install -vvv --force ../..
INFO Running helloworld > dependency
WARNING Skipping, missing the requirements file.
WARNING Skipping, missing the requirements file.
INFO Running helloworld > cleanup
WARNING Skipping, cleanup playbook not configured.
INFO Running helloworld > destroy
PLAY [Destroy] *****************************************************************
TASK [Populate instance config] ************************************************
ok: [localhost]
TASK [Dump instance config] ****************************************************
skipping: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
INFO Running helloworld > syntax
playbook: /home/tsuyoshi/test/azarashi/utils/roles/azarashi.utils/molecule/helloworld/converge.yml
INFO Running helloworld > create
PLAY [Create] ******************************************************************
TASK [Populate instance config dict] *******************************************
skipping: [localhost]
TASK [Convert instance config dict to a list] **********************************
skipping: [localhost]
TASK [Dump instance config] ****************************************************
skipping: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
INFO Running helloworld > prepare
WARNING Skipping, prepare playbook not configured.
INFO Running helloworld > converge
PLAY [Converge] ****************************************************************
TASK [Replace this task with one that validates your content] ******************
ok: [instance] => {
"msg": "This is the effective test"
}
ok: [molecule-ubuntu] => {
"msg": "This is the effective test"
}
PLAY RECAP *********************************************************************
instance : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
molecule-ubuntu : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
INFO Running helloworld > idempotence
PLAY [Converge] ****************************************************************
TASK [Replace this task with one that validates your content] ******************
ok: [instance] => {
"msg": "This is the effective test"
}
ok: [molecule-ubuntu] => {
"msg": "This is the effective test"
}
PLAY RECAP *********************************************************************
instance : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
molecule-ubuntu : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
INFO Idempotence completed successfully.
INFO Running helloworld > side_effect
WARNING Skipping, side effect playbook not configured.
INFO Running helloworld > verify
INFO Running Ansible Verifier
WARNING Skipping, verify action has no playbook.
INFO Verifier completed successfully.
INFO Running helloworld > cleanup
WARNING Skipping, cleanup playbook not configured.
INFO Running helloworld > destroy
PLAY [Destroy] *****************************************************************
TASK [Populate instance config] ************************************************
ok: [localhost]
TASK [Dump instance config] ****************************************************
skipping: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
INFO Pruning extra files from scenario ephemeral directory
とりあえずはmolecule testを実行できました。
ただし、実行するホスト自体はlocalhostとなっていて、ホストの環境を汚してしまう可能性があります。
そこで、次以降ではdockerイメージでテストを実行するようにしてみます。
手順 (dockerイメージでテストを実行させるまで)
dockerイメージでテストを実行させるにはmolecule配下のymlファイルを整備します。
下記の公式ドキュメントを一部参考にしています(※1)
意図的にconverge.ymlから呼び出すために大元のtaskに下記のtaskを追加しておきます。
$ cat tasks/main.yml
---
- name: Task is running from within the role
ansible.builtin.debug:
msg: "This is a task from my_role."
converge.ymlを下記内容で準備します。(※1)のドキュメントから上記のタスクを呼び出すように追記しています。
$ cat molecule/helloworld/converge.yml
- name: Fail if molecule group is missing
hosts: localhost
tasks:
- name: Print some info
ansible.builtin.debug:
msg: "{{ groups }}"
- name: Assert group existence
ansible.builtin.assert:
that: "'molecule' in groups"
fail_msg: |
molecule group was not found inside inventory groups: {{ groups }}
- name: Converge
hosts: molecule
# We disable gather facts because it would fail due to our container not
# having python installed. This will not prevent use from running 'raw'
# commands. Most molecule users are expected to use containers that already
# have python installed in order to avoid notable delays installing it.
gather_facts: false
tasks:
- name: Check uname
ansible.builtin.raw: uname -a
register: result
changed_when: false
- name: Print some info
ansible.builtin.assert:
that: result.stdout | regex_search("^Linux")
- name: Azarashi's Testing role
ansible.builtin.include_role:
name: azarashi.utils
tasks_from: main.yml
続いて、create.yml, destroy.yml, molecule.yml, requirements.ymlを用意します。内容は(※1)と同じです。
$ cat molecule/helloworld/create.yml
- name: Create
hosts: localhost
gather_facts: false
vars:
molecule_inventory:
all:
hosts: {}
molecule: {}
tasks:
- name: Create a container
community.docker.docker_container:
name: "{{ item.name }}"
image: "{{ item.image }}"
state: started
command: sleep 1d
log_driver: json-file
register: result
loop: "{{ molecule_yml.platforms }}"
- name: Print some info
ansible.builtin.debug:
msg: "{{ result.results }}"
- name: Fail if container is not running
when: >
item.container.State.ExitCode != 0 or
not item.container.State.Running
ansible.builtin.include_tasks:
file: tasks/create-fail.yml
loop: "{{ result.results }}"
loop_control:
label: "{{ item.container.Name }}"
- name: Add container to molecule_inventory
vars:
inventory_partial_yaml: |
all:
children:
molecule:
hosts:
"{{ item.name }}":
ansible_connection: community.docker.docker
ansible.builtin.set_fact:
molecule_inventory: >
{{ molecule_inventory | combine(inventory_partial_yaml | from_yaml) }}
loop: "{{ molecule_yml.platforms }}"
loop_control:
label: "{{ item.name }}"
- name: Dump molecule_inventory
ansible.builtin.copy:
content: |
{{ molecule_inventory | to_yaml }}
dest: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml"
mode: 0600
- name: Force inventory refresh
ansible.builtin.meta: refresh_inventory
- name: Fail if molecule group is missing
ansible.builtin.assert:
that: "'molecule' in groups"
fail_msg: |
molecule group was not found inside inventory groups: {{ groups }}
run_once: true # noqa: run-once[task]
# we want to avoid errors like "Failed to create temporary directory"
- name: Validate that inventory was refreshed
hosts: molecule
gather_facts: false
tasks:
- name: Check uname
ansible.builtin.raw: uname -a
register: result
changed_when: false
- name: Display uname info
ansible.builtin.debug:
msg: "{{ result.stdout }}"
$ cat molecule/helloworld/destroy.yml
- name: Destroy molecule containers
hosts: molecule
gather_facts: false
tasks:
- name: Stop and remove container
delegate_to: localhost
community.docker.docker_container:
name: "{{ inventory_hostname }}"
state: absent
auto_remove: true
- name: Remove dynamic molecule inventory
hosts: localhost
gather_facts: false
tasks:
- name: Remove dynamic inventory file
ansible.builtin.file:
path: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml"
state: absent
$ cat molecule/helloworld/molecule.yml
dependency:
name: galaxy
options:
requirements-file: requirements.yml
platforms:
- name: molecule-ubuntu
image: ubuntu:18.04
$ cat molecule/helloworld/reqirements.yml
collections:
- community.docker
以上で準備が整いましたので実行しますが、事前にdockerイメージが存在しないことを見ておきます。
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ molecule list
INFO Running helloworld > list
╷ ╷ ╷ ╷ ╷
Instance Name │ Driver Name │ Provisioner Name │ Scenario Name │ Created │ Converged
╶─────────────────┼─────────────┼──────────────────┼───────────────┼─────────┼───────────╴
molecule-ubuntu │ default │ ansible │ helloworld │ false │ false
╵ ╵ ╵ ╵ ╵
では、moleculeのtestを下記コマンドにより実行します。
destroy=neverを付与しないと生成したdockerが削除されてしまいましたので意図的に付与しています。
$ molecule test --scenario-name helloworld --destroy=never
INFO helloworld scenario test matrix: dependency, cleanup, destroy, syntax, create, prepare, converge, idempotence, side_effect, verify, cleanup, destroy
INFO Performing prerun with role_name_check=0...
INFO Running from /home/tsuyoshi/test/azarashi/utils/roles/azarashi.utils : ansible-galaxy collection install -vvv --force ../..
INFO Running helloworld > dependency
WARNING Skipping, missing the requirements file.
WARNING Skipping, missing the requirements file.
INFO Running helloworld > cleanup
WARNING Skipping, cleanup playbook not configured.
INFO Running helloworld > destroy
WARNING Skipping, '--destroy=never' requested.
INFO Running helloworld > syntax
[WARNING]: Could not match supplied host pattern, ignoring: molecule
playbook: /home/tsuyoshi/test/azarashi/utils/roles/azarashi.utils/molecule/helloworld/converge.yml
INFO Running helloworld > create
PLAY [Create] ******************************************************************
TASK [Create a container] ******************************************************
changed: [localhost] => (item={'image': 'ubuntu:18.04', 'name': 'molecule-ubuntu'})
TASK [Print some info] *********************************************************
ok: [localhost] => {
"msg": [
{
"ansible_loop_var": "item",
(snip)
TASK [Fail if container is not running] ****************************************
skipping: [localhost] => (item=/molecule-ubuntu)
skipping: [localhost]
TASK [Add container to molecule_inventory] *************************************
ok: [localhost] => (item=molecule-ubuntu)
TASK [Dump molecule_inventory] *************************************************
changed: [localhost]
TASK [Force inventory refresh] *************************************************
TASK [Fail if molecule group is missing] ***************************************
ok: [localhost] => {
"changed": false,
"msg": "All assertions passed"
}
PLAY [Validate that inventory was refreshed] ***********************************
TASK [Check uname] *************************************************************
ok: [molecule-ubuntu]
TASK [Display uname info] ******************************************************
ok: [molecule-ubuntu] => {
"msg": "Linux af07e92dba0a 5.15.0-43-generic #46-Ubuntu SMP Tue Jul 12 10:30:17 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux\n"
}
PLAY RECAP *********************************************************************
localhost : ok=5 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
molecule-ubuntu : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
INFO Running helloworld > prepare
WARNING Skipping, prepare playbook not configured.
INFO Running helloworld > converge
PLAY [Fail if molecule group is missing] ***************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [Print some info] *********************************************************
ok: [localhost] => {
"msg": {
"all": [
"molecule-ubuntu"
],
"molecule": [
"molecule-ubuntu"
],
"ungrouped": []
}
}
TASK [Assert group existence] **************************************************
ok: [localhost] => {
"changed": false,
"msg": "All assertions passed"
}
PLAY [Converge] ****************************************************************
TASK [Check uname] *************************************************************
ok: [molecule-ubuntu]
TASK [Print some info] *********************************************************
ok: [molecule-ubuntu] => {
"changed": false,
"msg": "All assertions passed"
}
TASK [Azarashi's Testing role] *************************************************
TASK [azarashi.utils : Task is running from within the role] *******************
ok: [molecule-ubuntu] => {
"msg": "This is a task from my_role."
}
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
molecule-ubuntu : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
INFO Running helloworld > idempotence
PLAY [Fail if molecule group is missing] ***************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [Print some info] *********************************************************
ok: [localhost] => {
"msg": {
"all": [
"molecule-ubuntu"
],
"molecule": [
"molecule-ubuntu"
],
"ungrouped": []
}
}
TASK [Assert group existence] **************************************************
ok: [localhost] => {
"changed": false,
"msg": "All assertions passed"
}
PLAY [Converge] ****************************************************************
TASK [Check uname] *************************************************************
ok: [molecule-ubuntu]
TASK [Print some info] *********************************************************
ok: [molecule-ubuntu] => {
"changed": false,
"msg": "All assertions passed"
}
TASK [Azarashi's Testing role] *************************************************
TASK [azarashi.utils : Task is running from within the role] *******************
ok: [molecule-ubuntu] => {
"msg": "This is a task from my_role."
}
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
molecule-ubuntu : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
INFO Idempotence completed successfully.
INFO Running helloworld > side_effect
WARNING Skipping, side effect playbook not configured.
INFO Running helloworld > verify
INFO Running Ansible Verifier
WARNING Skipping, verify action has no playbook.
INFO Verifier completed successfully.
INFO Running helloworld > cleanup
WARNING Skipping, cleanup playbook not configured.
INFO Running helloworld > destroy
WARNING Skipping, '--destroy=never' requested.
実行されて意図的にtasks/main.ymlに記載したタスクもconvergeの際に呼び出されていることが確認できました。
Playは冪等性の確認も含めて2度実行されます。
最後に、molecule testの実行によって、dockerイメージが生成されたことを確認しておきます。
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
af07e92dba0a ubuntu:18.04 "sleep 1d" About a minute ago Up About a minute molecule-ubuntu
$ molecule list
INFO Running helloworld > list
╷ ╷ ╷ ╷ ╷
Instance Name │ Driver Name │ Provisioner Name │ Scenario Name │ Created │ Converged
╶─────────────────┼─────────────┼──────────────────┼───────────────┼─────────┼───────────╴
molecule-ubuntu │ default │ ansible │ helloworld │ true │ true
╵ ╵ ╵ ╵ ╵
まとめ
ansible moleculeを手元のlocalhostやdockerイメージ上で動かせる様になるまでの手順を記載しました。