Moleculeってなに?
MoleculeはAnsibleロールをテストするためのツール
分子って意味らしい
pythonモジュールで実装されているので、pip install molecule
で使える
使い方もとってもかんたん、molecule test
これで、
- プレイブックが正しい構文になっているよね、コードがある規則に基づいて記述されているよね、を確認する(lint)
- dockerコンテナを立てて、作成したプレイブックを流す
- ↑のコンテナにもう一度同じプレイブックを流して冪等性を確認
- 事前に記述したテストを実行して、狙い通りの動きをしているかチェック
みたいなことをやってくれる
つまりAnsibleロールのテスト自動化ができるツール
もちろんテストの内容やmolecule test
実行時の動作はカスタマイズできる
なにがうれしいのよ?
Ansibleロールは一回書いて終わりじゃなくて、何度も修正していくはず
そんな中で、何度もロールの動作が正しいことを手で確認するのは大変であることは明らか
こんなときに、Moleculeのようなテスト自動化が役に立つ
テストが自動化されていれば以下のようなうまみが得られる
- 人間が毎回毎回手でロールの正しさを保証する必要がなくなる
- 機械がテストをしてくれるので定義された試験項目をもれなく実施できる
- 人間がやるとサボりが発生するのでデグレードの心配をしなければならない
- 特にlintとか構文チェックとかって人間は毎回やらない
- だいたい「まぁあってるやろー」→「あっ…」ってなるのがデフォなので機械的に救うのは理にかなっている
更に、ロールをGitみたいなバージョン管理ツールに入れておいて、コードに修正が入ったとき自動でテストが流れるようなパイプラインを作っておけば、コード修正時にテストを自動化するといったこともできて素敵になる
要するに、IaCをするならテストは自動化されているべきで、それをAnsibleロールにおいて実現してくれるのがMoleculeだよ、って話
Moleculeを使ってロールの開発をやってみる
ハイパー実際にMoleculeしてみようタイム
作業環境
品目 | バージョン |
---|---|
OS | macOS Catalina 10.15.2 |
シェル | fish, version 3.0.2 |
Python | 3.8.0 |
Ansible | 2.9.2 |
Molecule | 2.22 |
Docker Desktop | community 2.1.0.5 (40693) |
Docker Engine | 19.03.5 |
インストール
- Docker Desktopを入れておく
- 適当な作業ディレクトリにcdしておく
- pythonの仮想環境を作ってアクティベート
- 私はfishシェルを使っているので、bashやzshの方は
source
の部分を適当に読み替えて下さい
- 私はfishシェルを使っているので、bashやzshの方は
$ python -m venv .venv
$ source .venv/bin/activate.fish
- Moleculeをインストールする
$ pip install molecule
- ロール置き場を作って移動
$ mkdir roles
$ cd roles
molecule init
でロールの雛形を作成する
- 今回はAnisbleもくもく会の教材になっている、httpdを構築するロールを題材にする
- ので、ロール名は
apache_vhost
として、Moleculeに用意されているロール雛形生成コマンドを実行するmolecule init role -r <ロール名>
$ molecule init role -r apache_vhost
--> Initializing new role apache_vhost...
Initialized role in /Users/answer_d/repos/molecule_test/roles/apache_vhost successfully.
$ cd apache_vhost
- こんなかんじのディレクトリが生成される
apache_vhost
├── README.md
├── defaults
│ └── main.yml
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── molecule
│ └── default
│ ├── Dockerfile.j2
│ ├── INSTALL.rst
│ ├── molecule.yml
│ ├── playbook.yml
│ └── tests
│ ├── __pycache__
│ │ └── test_default.cpython-38.pyc
│ └── test_default.py
├── tasks
│ └── main.yml
└── vars
└── main.yml
※ この時点ではmain.yml
の中身は空
molecule/
ディレクトリに、Molecule関連ファイルが入っているので覗いてみる
molecule
└── default
├── Dockerfile.j2
├── INSTALL.rst
├── molecule.yml
├── playbook.yml
└── tests
├── __pycache__
│ └── test_default.cpython-38.pyc
└── test_default.py
-
default/
ディレクトリ -
これはMoleculeにおいて「シナリオ」と呼ばれる、ロールのテストスイートを示す
- デフォルトでは
default
ディレクトリが作成されるが、別の名前をつけることもできる-
default
以外のシナリオを実行する場合は、molecule test
コマンドの実行時に明示的にシナリオ名を記述したりする必要がある
-
- デフォルトでは
-
Dockerfile.j2
- Moleculeはこのファイルを使用して、ロールをテストするためのdockerイメージを作成する
- テスト環境を生成する仕組みをMoleculeでは「ドライバ」と呼び、デフォルトはDockerであるため、このファイルが自動生成される
- ちなみにDocker以外には、VagrantやEC2、Azureなどが選択できる
# Molecule managed {% if item.registry is defined %} FROM {{ item.registry.url }}/{{ item.image }} {% else %} FROM {{ item.image }} {% endif %} {% if item.env is defined %} {% for var, value in item.env.items() %} {% if value %} ENV {{ var }} {{ value }} {% endif %} {% endfor %} {% endif %} RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates iproute2 && apt-get clean; \ elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash iproute && dnf clean all; \ elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \ elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml iproute2 && zypper clean -a; \ elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \ elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates iproute2 && xbps-remove -O; fi
-
INSTALL.rst
- Moleculeがドライバと正常に接続できるようにするために必要なセットアップ手順が書いてある説明書
- ただの説明書なのでなくても良い
-
molecule.yml
- Moleculeがロールをテストするときの動作を構成したりするための設定ファイル
- 例えばドライバをDockerから別のものに変更したい場合はこのファイルを書き換えることになる
--- dependency: name: galaxy driver: name: docker lint: name: yamllint platforms: - name: instance image: centos:7 provisioner: name: ansible lint: name: ansible-lint verifier: name: testinfra lint: name: flake8
-
playbook.yml
- ロールを呼び出すプレイブック
- Moleculeはこのプレイブックを
ansible-playbook
で呼び出し、ドライバによって作成されたインスタンスに対して実行する
--- - name: Converge hosts: all roles: - role: apache_vhost
-
test/
- テストコードを配置するディレクトリ
- テストツールに何を使用するかは「Verifier」の指定により変更できるが、デフォルトではtestinfraであるためこのディレクトリが生成される
- Verifierはtestinfraのほか、Ansibleなどが選択できる
- AnsibleをVerifierとする場合はこのディレクトリは不要で、代わりに
verify.yml
を作成する必要がある
- AnsibleをVerifierとする場合はこのディレクトリは不要で、代わりに
ロールを実装する
上述の教材を信頼し、何も考えず以下のように実装する、molecule
ディレクトリはまだ触らない
いらんディレクトリは消しておく(meta/
とか)
.
├── files
│ └── index.html
├── handlers
│ └── main.yml
├── tasks
│ └── main.yml
└── templates
└── vhost.conf.j2
simple vhost index
---
# handlers file for roles/apache_vhost
- name: restart_httpd
service:
name: httpd
state: restarted
---
- name: install httpd
yum:
name: httpd
state: present
- name: start and enable httpd service
service:
name: httpd
state: started
enabled: true
- name: ensure vhost directory is present
file:
path: "/var/www/vhosts/{{ ansible_hostname }}"
state: directory
- name: deliver html content
copy:
src: index.html
dest: "/var/www/vhosts/{{ ansible_hostname }}"
- name: template vhost file
template:
src: vhost.conf.j2
dest: /etc/httpd/conf.d/vhost.conf
owner: root
group: root
mode: 0644
notify:
- restart_httpd
# {{ ansible_managed }}
Listen 8080
<VirtualHost *:8080>
ServerAdmin webmaster@{{ ansible_fqdn }}
ServerName {{ ansible_fqdn }}
ErrorLog logs/{{ ansible_hostname }}-error.log
CustomLog logs/{{ ansible_hostname }}-common.log common
DocumentRoot /var/www/vhosts/{{ ansible_hostname }}/
<Directory /var/www/vhosts/{{ ansible_hostname }}/>
Options +Indexes +FollowSymlinks +Includes
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
テストを実装する
-
想定するテストケースは以下とする(ちょっと冗長かもだけどあまり気にしない)
- ポート8080番がオープンである
- httpdサービスが起動しており、自動起動がオンである
- index.htmlが取得できる(= ステータスコード200が返ってくる)
-
これを実現するtestinfraのコードとして以下を実装
- テストケースごとに関数作ってassert文を並べるだけなので、python知らない人でも結構書きやすいはず
import os
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')
def test_port_open(host):
local = host.addr("localhost")
assert local.port(8080).is_reachable
def test_httpd_enabled_n_started(host):
httpd_svc = host.service("httpd")
assert httpd_svc.is_running
assert httpd_svc.is_enabled
def test_index(host):
index = host.run("curl -LI http://localhost:8080 -s")
assert index.rc == 0
assert "200 OK" in index.stdout.split("\r\n")[0]
- プレイブックでserviceモジュールを使っている = Dockerコンテナでsystemctlを触る必要があるため、コンテナの設定を変える
-
platforms:
に、privileged: true
とcommand: /sbin/init
を追加する - Docker ドライバは内部的に
ansble_container
モジュールを使っているのでオプションはこの辺が使えるっぽい
-
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
platforms:
- name: instance
image: centos:7
privileged: true
command: /sbin/init
provisioner:
name: ansible
lint:
name: ansible-lint
verifier:
name: testinfra
lint:
name: flake8
-
Dockerfile.j2
とplaybook.yml
はそのままでOK
Dockerドライバを使用するため、pipでdockerをインストールする
$ pip install docker
Docker Desktopだけじゃ動かなかった (なんで?)
テストを実行する
$ molecule test
これでテストが実行される
実行結果を上から見ていくと…
--> Test matrix
└── default
├── lint
├── dependency
├── cleanup
├── destroy
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
├── cleanup
└── destroy
defaultシナリオが実行されていて、こんな感じのステップで構成されてるよー的なやつ
lint
--> Action: 'lint'
--> Executing Yamllint on files found in /Users/answer_d/repos/molecule_test/roles/apache_vhost/...
Lint completed successfully.
--> Executing Flake8 on files found in /Users/answer_d/repos/molecule_test/roles/apache_vhost/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /Users/answer_d/repos/molecule_test/roles/apache_vhost/molecule/default/playbook.yml...
Lint completed successfully.
以下の3つのlintが実行されている
- 作成したロール
apache_vhost
に対するYamllint - testinfraで実装したテストコードに対するFlake8
- Flake8はpythonのlinter
-
playbook.yml
に対するAnsible Lint
create
--> Action: 'create'
PLAY [Create] ******************************************************************
TASK [Log into a Docker registry] **********************************************
skipping: [localhost] => (item=None)
TASK [Create Dockerfiles from image names] *************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Determine which docker image info module to use] *************************
ok: [localhost]
TASK [Discover local Docker images] ********************************************
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Build an Ansible compatible image (new)] *********************************
ok: [localhost] => (item=molecule_local/centos:7)
TASK [Build an Ansible compatible image (old)] *********************************
skipping: [localhost] => (item=molecule_local/centos:7)
TASK [Create docker network(s)] ************************************************
TASK [Determine the CMD directives] ********************************************
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Create molecule instance(s)] *********************************************
changed: [localhost] => (item=instance)
TASK [Wait for instance(s) creation to complete] *******************************
FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=7 changed=3 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
Dockerfileからイメージを作って、Dockerコンテナを作成している
Moleculeが内部的に持つプレイブックで実行されていることがわかる
converge
--> Action: 'converge'
PLAY [Converge] ****************************************************************
TASK [Gathering Facts] *********************************************************
ok: [instance]
TASK [apache_vhost : install httpd] ********************************************
changed: [instance]
TASK [apache_vhost : start and enable httpd service] ***************************
changed: [instance]
TASK [apache_vhost : ensure vhost directory is present] ************************
changed: [instance]
TASK [apache_vhost : deliver html content] *************************************
changed: [instance]
TASK [apache_vhost : template vhost file] **************************************
changed: [instance]
RUNNING HANDLER [apache_vhost : restart_httpd] *********************************
changed: [instance]
PLAY RECAP *********************************************************************
instance : ok=7 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
自分が作ったロールを先程作成したインスタンスに対して実行している
idempotence
--> Action: 'idempotence'
Idempotence completed successfully.
結果からは見えにくいが、ここで冪等性の確認を行っている(idempotenceは「冪等性」という意味)
convergeで実行したプレイブックをもう一度実行し、changedステータスが返らないことを検証しているらしい
verify
--> Action: 'verify'
--> Executing Testinfra tests found in /Users/answer_d/repos/molecule_test/roles/apache_vhost/molecule/default/tests/...
============================= test session starts ==============================
platform darwin -- Python 3.8.0, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
rootdir: /Users/answer_d/repos/molecule_test/roles/apache_vhost/molecule/default
plugins: testinfra-3.4.0
collected 3 items
tests/test_default.py ... [100%]
============================== 3 passed in 2.64s ===============================
Verifier completed successfully.
testinfraコードをインスタンス上で実行する
3件テストし、全てパスしていることが確認できる
destroy
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=instance)
TASK [Wait for instance(s) deletion to complete] *******************************
FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Delete docker network(s)] ************************************************
PLAY RECAP *********************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
--> Pruning extra files from scenario ephemeral directory
テストに使用したインスタンスを削除する
できた
これでわたしもMoleculer
今回は一発で成功する例を示しましたが、プレイブックに意図的に変な空白を入れてみたり、プレイブックやテストコードの処理内容を変えてみたりすることでエラーが検出できることを確認できます、興味があればやってみて下さい
おまけ
-
molecule test
で実行される一部のステップだけ取り出して実行することもできる-
molecule create
: dependency, create, prepare- インスタンスが作成されるとこまで
-
molecule converge
: dependency, create, prepare, converge- create+Playbookの実行まで
-
molecule destroy
: dependency, cleanup, destroy- インスタンスの削除
-
-
なのでテスト自動化前提でなくて、ロールを作るだけでも結構うれしい、Dockerコンテナ上げて動作テストしながらーって手でやるとまぁまぁ手間だけど、Moleculeならいい感じにしてくれていい感じ(語彙力)
- 作ったコンテナにログインしてくれる
molecule login
とかもある(vagrant ssh
みたいな感じで使える) - サーバの状態確認するときラクラクの楽になって便利
- 作ったコンテナにログインしてくれる
-
VerifierはAnsibleにすることもできる
- testinfraだとpythonコードになってしまうので、プログラムかけないインフラマンはAnsibleをVerifierにするととっつきやすい
- といってもtestinfraだけだったらpythonでも十分書きやすいと思われるのでぶっちゃけどっちでも良いと思う、好みとテストでやりたいこと次第で決めれば良い
- AnsibleをVerifierにした場合でも、テスト用モジュールはちゃんとある
- ポートチェックとかどうやるの?って思ったら
wait_for
モジュールとかあった、さすがです
- ポートチェックとかどうやるの?って思ったら
- testinfraだとpythonコードになってしまうので、プログラムかけないインフラマンはAnsibleをVerifierにするととっつきやすい
ちなみにAnsibleをVerifierにする場合、今回作成したtestinfraと同様のテストを行うプレイブックは以下のようになる
このファイルを用意して、molecule/default/molecule.yml
でVerifierにAnsibleを使用する旨を記載するとAnsibleでテストするようになるので気になる人は試してみてクレメンス
---
- name: Verify
hosts: all
gather_facts: false
pre_tasks:
- name: get httpd status
service_facts:
- name: get index.html
uri:
url: http://localhost:8080
method: GET
register: response
tasks:
- name: test port 8080 open
wait_for:
port: 8080
timeout: 5
- name: test http enabled and started
assert:
that:
- ansible_facts.services['httpd.service'].state == "running"
- ansible_facts.services['httpd.service'].status == "enabled"
- name: test httpd returns status 200 and has content
assert:
that:
- response.status == 200
- response.content is not none