36
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Moleculeに入門してみたよ

Last updated at Posted at 2020-01-15

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の部分を適当に読み替えて下さい
$ 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を作成する必要がある

ロールを実装する

上述の教材を信頼し、何も考えず以下のように実装する、moleculeディレクトリはまだ触らない
いらんディレクトリは消しておく(meta/とか)

.
├── files
│   └── index.html
├── handlers
│   └── main.yml
├── tasks
│   └── main.yml
└── templates
    └── vhost.conf.j2
files/index.html
simple vhost index
handlers/main.yml
---
# handlers file for roles/apache_vhost
- name: restart_httpd
  service:
    name: httpd
    state: restarted
tasks/main.yml
---
- 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
templates/vhost.conf.j2
# {{ 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知らない人でも結構書きやすいはず
molecule/default/tests/test_default.py
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を触る必要があるため、コンテナの設定を変える
molecule/default/molecule.yml
---
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.j2playbook.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モジュールとかあった、さすがです

ちなみにAnsibleをVerifierにする場合、今回作成したtestinfraと同様のテストを行うプレイブックは以下のようになる
このファイルを用意して、molecule/default/molecule.ymlでVerifierにAnsibleを使用する旨を記載するとAnsibleでテストするようになるので気になる人は試してみてクレメンス

molecule/default/verify.yml
---
- 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
36
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?