Ubuntu 18.04でMoleculeを使用してAnsibleロールをテストする方法

著者は、Write for DOnationsプログラムの一部として寄付を受け取るためにMozilla Foundationを選択しました。

前書き

Ansibleでの単体テストは、役割が意図したとおりに機能することを確認するための鍵です。 Moleculeを使用すると、さまざまな環境に対して役割をテストするシナリオを指定できるため、このプロセスが簡単になります。 Moleculeは、内部でAnsibleを使用して、構成された環境に役割をデプロイするプロビジョナーに役割をオフロードし、検証者(Testinfraなど)を呼び出して構成のドリフトをチェックします。 これにより、ロールがその特定のシナリオで環境に対して予想されるすべての変更を行ったことを確認できます。

このガイドでは、Apacheをホストにデプロイし、CentOS 7でfirewalldを構成するAnsibleロールを作成します。 この役割が意図したとおりに機能することをテストするには、Dockerをドライバーとして使用し、サーバーの状態をテストするためのPythonライブラリであるTestinfraを使用してMoleculeでテストを作成します。 MoleculeはDockerコンテナーをプロビジョニングしてロールをテストし、Testinfraはサーバーが意図したとおりに構成されていることを確認します。 完了したら、環境全体のビルド用に複数のテストケースを作成し、Moleculeを使用してこれらのテストを実行できます。

前提条件

このガイドを始める前に、次のものが必要です。

[[step-1 -—- preparing-the-environment]] ==ステップ1-環境の準備

前提条件を満たしている場合は、Python 3、venv、Dockerがインストールされ、正しく構成されている必要があります。 AnsibleをMoleculeでテストするための仮想環境を作成することから始めましょう。

非rootユーザーとしてログインし、新しい仮想環境を作成することから始めます。

python3 -m venv my_env

それをアクティブにして、アクションがその環境に制限されるようにします。

source my_env/bin/activate

次に、アクティブ化された環境で、wheelパッケージをインストールします。これは、pipがAnsibleのインストールに使用するbdist_wheelsetuptools拡張機能を提供します。

python3 -m pip install wheel

これで、moleculeおよびdockerpipとともにインストールできます。 AnsibleはMoleculeの依存関係として自動的にインストールされます。

python3 -m pip install molecule docker

これらの各パッケージの機能は次のとおりです。

  • molecule:これは、ロールのテストに使用するメインのMoleculeパッケージです。 moleculeをインストールすると、Ansibleが他の依存関係とともに自動的にインストールされ、Ansibleプレイブックを使用して役割とテストを実行できるようになります。

  • docker:このPythonライブラリは、MoleculeがDockerとインターフェイスするために使用します。 Dockerをドライバーとして使用しているため、これが必要になります。

次に、Moleculeでロールを作成しましょう。

[[step-2 -—- creating-a-role-in-molecule]] ==ステップ2—分子での役割の作成

環境をセットアップしたら、Moleculeを使用して、Apacheのインストールのテストに使用する基本的な役割を作成できます。 このロールは、ディレクトリ構造といくつかの初期テストを作成し、ドライバーとしてDockerを指定して、Moleculeがテストを実行するためにDockerを使用するようにします。

ansible-apacheという新しい役割を作成します。

molecule init role -r ansible-apache -d docker

-rフラグはロールの名前を指定し、-dはドライバーを指定します。ドライバーは、テストで使用するMoleculeのホストをプロビジョニングします。

新しく作成されたロールのディレクトリに移動します。

cd ansible-apache

デフォルトのロールをテストして、Moleculeが適切にセットアップされているかどうかを確認します。

molecule test

デフォルトの各テストアクションをリストする出力が表示されます。 テストを開始する前に、Moleculeは構成ファイルmolecule.ymlを検証して、すべてが正常であることを確認します。 また、テストアクションの順序を指定するこのテストマトリックスを出力します。

Output--> Validating schema /home/sammy/ansible-apache/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix

└── default
    ├── lint
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    └── destroy
...

ロールを作成してテストをカスタマイズしたら、各テストアクションについて詳しく説明します。 今のところ、各テストのPLAY_RECAPに注意し、デフォルトのアクションのいずれもfailedステータスを返さないことを確認してください。 たとえば、デフォルトの'create'アクションのPLAY_RECAPは、次のようになります。

Output...
PLAY RECAP *********************************************************************
localhost                  : ok=5    changed=4    unreachable=0    failed=0

次に、ロールを変更してApacheとfirewalldを構成します。

[[step-3 -—- configuring-apache-and-firewalld]] ==ステップ3—ApacheとFirewalldの設定

Apacheとfirewalldを構成するには、インストールするパッケージと有効にするサービスを指定して、ロールのタスクファイルを作成します。 これらの詳細は、デフォルトのApacheインデックスページを置き換えるために使用する変数ファイルとテンプレートから抽出されます。

まだansible-apacheディレクトリに、nanoまたはお気に入りのテキストエディタを使用して、ロールのタスクファイルを作成します。

nano tasks/main.yml

ファイルが既に存在することがわかります。 そこにあるものを削除し、次のコードに置き換えて必要なパッケージをインストールし、正しいサービス、HTMLデフォルト、ファイアウォール設定を有効にします。

~/ansible-apache/tasks/main.yml

---
- name: "Ensure required packages are present"
  yum:
    name: "{{ pkg_list }}"
    state: present

- name: "Ensure latest index.html is present"
  template:
    src: index.html.j2
    dest: /var/www/html/index.html

- name: "Ensure httpd service is started and enabled"
  service:
    name: "{{ item }}"
    state: started
    enabled: true
  with_items: "{{ svc_list }}"

- name: "Whitelist http in firewalld"
  firewalld:
    service: http
    state: enabled
    permanent: true
    immediate: true

このプレイブックには4つのタスクが含まれています。

  • "Ensure required packages are present":このタスクは、pkg_listの下の変数ファイルにリストされているパッケージをインストールします。 変数ファイルは~/ansible-apache/vars/main.ymlにあり、このステップの最後に作成します。

  • "Ensure latest index.html is present":このタスクは、テンプレートページindex.html.j2をコピーし、Apacheによって生成されたデフォルトのインデックスファイル/var/www/html/index.htmlに貼り付けます。 この手順では、新しいテンプレートも作成します。

  • "Ensure httpd service is started and enabled":このタスクは、変数ファイルのsvc_listにリストされているサービスを開始して有効にします。

  • "Whitelist http in firewalld":このタスクは、firewalldhttpサービスをホワイトリストに登録します。 Firewalldは、CentOSサーバーにデフォルトで存在する完全なファイアウォールソリューションです。 httpサービスを機能させるには、必要なポートを公開する必要があります。 firewalldにサービスをホワイトリストに登録するように指示すると、サービスに必要なすべてのポートがホワイトリストに登録されます。

完了したら、ファイルを保存して閉じます。

次に、index.html.j2テンプレートページ用のtemplatesディレクトリを作成しましょう。

mkdir templates

ページ自体を作成します。

nano templates/index.html.j2

次の定型コードを貼り付けます。

~/ansible-apache/templates/index.html.j2

Managed by Ansible

ファイルを保存して閉じます。

ロールを完了するための最後のステップは、変数ファイルを作成することです。このファイルは、パッケージとサービスの名前をメインのロールプレイブックに提供します。

nano vars/main.yml

pkg_listsvc_listを指定する次のコードを使用して、デフォルトのコンテンツを貼り付けます。

~/ansible-apache/vars/main.yml

---
pkg_list:
  - httpd
  - firewalld
svc_list:
  - httpd
  - firewalld

これらのリストには、次の情報が含まれています。

  • pkg_list:これには、ロールがインストールするパッケージの名前(httpdおよびfirewalld)が含まれます。

  • svc_list:これには、ロールが開始して有効にするサービスの名前が含まれます:httpdおよびfirewalld

[.note]#Note:変数ファイルに空白行がないことを確認してください。空白行がない場合、リンティング中にテストが失敗します。

役割の作成が完了したので、Moleculeが意図したとおりに機能するかどうかをテストするように設定します。

[[step-4 -—- formifying-the-role-for-running-tests]] ==ステップ4—テストを実行するためのロールの変更

この場合、Moleculeの構成には、Molecule構成ファイルmolecule.ymlを変更してプラットフォーム仕様を追加することが含まれます。 httpd systemdサービスを構成して開始する役割をテストしているため、systemdが構成され、特権モードが有効になっているイメージを使用する必要があります。 このチュートリアルでは、milcom/centos7-systemdイメージavailable on Docker Hubを使用します。 特権モードでは、コンテナをホストマシンのほぼすべての機能で実行できます。

これらの変更を反映するようにmolecule.ymlを編集してみましょう。

nano molecule/default/molecule.yml

強調表示されたプラットフォーム情報を追加します。

~/ansible-apache/molecule/default/molecule.yml

---
dependency:
  name: galaxy
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: centos7
    image: milcom/centos7-systemd
    privileged: true
provisioner:
  name: ansible
  lint:
    name: ansible-lint
scenario:
  name: default
verifier:
  name: testinfra
  lint:
    name: flake8

完了したら、ファイルを保存して閉じます。

テスト環境の構成が正常に完了したので、ロールの実行後にMoleculeがコンテナーに対して実行するテストケースの作成に移りましょう。

[[step-5 --- writing-test-cases]] ==ステップ5—テストケースの作成

この役割のテストでは、次の条件を確認します。

  • httpdおよびfirewalldパッケージがインストールされていること。

  • httpdおよびfirewalldサービスが実行され、有効になっていること。

  • ファイアウォール設定でhttpサービスが有効になっていること。

  • そのindex.htmlには、テンプレートファイルで指定されたものと同じデータが含まれています。

これらすべてのテストに合格すると、ロールは意図したとおりに機能します。

これらの条件のテストケースを作成するために、~/ansible-apache/molecule/default/tests/test_default.pyでデフォルトのテストを編集しましょう。 Testinfraを使用して、Moleculeクラスを使用するPython関数としてテストケースを記述します。

test_default.pyを開きます:

nano molecule/default/tests/test_default.py

ファイルの内容を削除して、テストを最初から作成できるようにします。

[.note]#Note:テストを作成するときは、2つの新しい行で区切られていることを確認してください。そうしないと、失敗します。

必要なPythonモジュールをインポートすることから始めます。

~/ansible-apache/molecule/default/tests/test_default.py

import os
import pytest

import testinfra.utils.ansible_runner

これらのモジュールは次のとおりです。

  • os:この組み込みのPythonモジュールは、オペレーティングシステムに依存する機能を有効にし、Pythonが基盤となるオペレーティングシステムとインターフェイスできるようにします。

  • pytestpytestモジュールはテスト書き込みを有効にします。

  • testinfra.utils.ansible_runner:このTestinfraモジュールは、コマンドの実行にAnsible as the backendを使用します。

モジュールのインポートの下に、Ansibleバックエンドを使用して現在のホストインスタンスを返す次のコードを追加します。

~/ansible-apache/molecule/default/tests/test_default.py

...
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')

Ansibleバックエンドを使用するようにテストファイルを構成したら、ユニットテストを作成してホストの状態をテストしましょう。

最初のテストでは、httpdfirewalldがインストールされていることを確認します。

~/ansible-apache/molecule/default/tests/test_default.py

...

@pytest.mark.parametrize('pkg', [
  'httpd',
  'firewalld'
])
def test_pkg(host, pkg):
    package = host.package(pkg)

    assert package.is_installed

テストはpytest.mark.parametrize decoratorで始まります。これにより、テストの引数をパラメーター化できます。 この最初のテストでは、httpdおよびfirewalldパッケージの存在をテストするためのパラメーターとしてtest_pkgを取ります。

次のテストでは、httpdfirewalldが実行され、有効になっているかどうかを確認します。 パラメータとしてtest_svcを取ります:

~/ansible-apache/molecule/default/tests/test_default.py

...

@pytest.mark.parametrize('svc', [
  'httpd',
  'firewalld'
])
def test_svc(host, svc):
    service = host.service(svc)

    assert service.is_running
    assert service.is_enabled

最後のテストでは、parametrize()に渡されたファイルとコンテンツが存在することを確認します。 ファイルが自分の役割によって作成されておらず、コンテンツが適切に設定されていない場合、assertFalseを返します。

~/ansible-apache/molecule/default/tests/test_default.py

...

@pytest.mark.parametrize('file, content', [
  ("/etc/firewalld/zones/public.xml", ""),
  ("/var/www/html/index.html", "Managed by Ansible")
])
def test_files(host, file, content):
    file = host.file(file)

    assert file.exists
    assert file.contains(content)

各テストで、assertは、テスト結果に応じてTrueまたはFalseを返します。

完成したファイルは次のようになります。

~/ansible-apache/molecule/default/tests/test_default.py

import os
import pytest

import testinfra.utils.ansible_runner

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')


@pytest.mark.parametrize('pkg', [
  'httpd',
  'firewalld'
])
def test_pkg(host, pkg):
    package = host.package(pkg)

    assert package.is_installed


@pytest.mark.parametrize('svc', [
  'httpd',
  'firewalld'
])
def test_svc(host, svc):
    service = host.service(svc)

    assert service.is_running
    assert service.is_enabled


@pytest.mark.parametrize('file, content', [
  ("/etc/firewalld/zones/public.xml", ""),
  ("/var/www/html/index.html", "Managed by Ansible")
])
def test_files(host, file, content):
    file = host.file(file)

    assert file.exists
    assert file.contains(content)

テストケースを指定したので、役割をテストしましょう。

[[ステップ-6 ---分子を使用した役割のテスト]] ==ステップ6—分子を使用した役割のテスト

テストを開始すると、Moleculeはシナリオで定義したアクションを実行します。 ここで、デフォルトのmoleculeシナリオを再度実行し、それぞれを詳しく調べながら、デフォルトのテストシーケンスでアクションを実行してみましょう。

デフォルトのシナリオのテストを再度実行します。

molecule test

これにより、テスト実行が開始されます。 初期出力では、デフォルトのテストマトリックスが出力されます。

Output--> Validating schema /home/sammy/ansible-apache/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix

└── default
    ├── lint
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    └── destroy

リンティングから始めて、各テストアクションと期待される出力を見ていきましょう。

lintingアクションは、yamllintflake8、およびansible-lintを実行します。

  • yamllint:このリンターは、ロールディレクトリに存在するすべてのYAMLファイルで実行されます。

  • flake8:このPythonコードリンターは、Testinfra用に作成されたテストをチェックします。

  • ansible-lint:Ansibleプレイブック用のこのリンターは、すべてのシナリオで実行されます。

Output...
--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/sammy/ansible-apache/...
Lint completed successfully.
--> Executing Flake8 on files found in /home/sammy/ansible-apache/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/sammy/ansible-apache/molecule/default/playbook.yml...
Lint completed successfully.

次のアクションdestroyは、destroy.ymlファイルを使用して実行されます。 これは、新しく作成されたコンテナでロールをテストするために行われます。

デフォルトでは、destroyは2回呼び出されます。テスト実行の開始時に、既存のコンテナを削除し、最後に、新しく作成されたコンテナを削除します。

Output...
--> 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] *******************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Delete docker network(s)] ************************************************
    skipping: [localhost]

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=1    unreachable=0    failed=0

破棄アクションが完了すると、テストはdependencyに進みます。 このアクションにより、ロールで依存関係が必要な場合に、ansible-galaxyから依存関係を引き出すことができます。 この場合、私たちの役割は何もしません:

Output...
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.

次のテストアクションはsyntaxチェックで、これはデフォルトのplaybook.ymlプレイブックで実行されます。 これは、コマンドansible-playbook --syntax-check playbook.yml--syntax-checkフラグと同じように機能します。

Output...
--> Scenario: 'default'
--> Action: 'syntax'

    playbook: /home/sammy/ansible-apache/molecule/default/playbook.yml

次に、テストはcreateアクションに進みます。 これは、ロールのMoleculeディレクトリにあるcreate.ymlファイルを使用して、指定したDockerコンテナを作成します。

Output...

--> Scenario: 'default'
--> Action: 'create'

    PLAY [Create] ******************************************************************

    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None)
    skipping: [localhost]

    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Build an Ansible compatible image] ***************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Create docker network(s)] ************************************************
    skipping: [localhost]

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) creation to complete] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=4    unreachable=0    failed=0

作成後、テストはprepareアクションに進みます。 このアクションは準備プレイブックを実行し、収束を実行する前にホストを特定の状態にします。 これは、ロールを実行する前にシステムの事前設定が必要な場合に役立ちます。 繰り返しますが、これは私たちの役割には適用されません。

Output...
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.

準備後、convergeアクションは、playbook.ymlプレイブックを実行することにより、コンテナーでロールを実行します。 molecule.ymlファイルで複数のプラットフォームが構成されている場合、Moleculeはこれらすべてに収束します。

Output...
--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [centos7]

    TASK [ansible-apache : Ensure required packages are present] *******************
    changed: [centos7]

    TASK [ansible-apache : Ensure latest index.html is present] ********************
    changed: [centos7]

    TASK [ansible-apache : Ensure httpd service is started and enabled] ************
    changed: [centos7] => (item=httpd)
    changed: [centos7] => (item=firewalld)

    TASK [ansible-apache : Whitelist http in firewalld] ****************************
    changed: [centos7]

    PLAY RECAP *********************************************************************
    centos7                    : ok=5    changed=4    unreachable=0    failed=0

カバーした後、テストはidempotenceに進みます。 このアクションは、多重呼び出しのプレイブックをテストして、複数の実行で予期しない変更が行われないことを確認します。

Output...
--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.

次のテストアクションはside-effectアクションです。 これにより、HAフェールオーバーなど、より多くのものをテストできる状況を作り出すことができます。 デフォルトでは、Moleculeは副作用プレイブックを設定せず、タスクはスキップされます。

Output...
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.

次に、Moleculeは、デフォルトのベリファイアであるTestinfraを使用してverifierアクションを実行します。 このアクションは、前にtest_default.pyで作成したテストを実行します。 すべてのテストに合格すると、成功メッセージが表示され、Moleculeは次のステップに進みます。

Output...
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/sammy/ansible-apache/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux -- Python 3.6.5, pytest-3.7.3, py-1.5.4, pluggy-0.7.1
    rootdir: /home/sammy/ansible-apache/molecule/default, inifile:
    plugins: testinfra-1.14.1
collected 6 items

    tests/test_default.py ......                                             [100%]

    ========================== 6 passed in 41.05 seconds ===========================
Verifier completed successfully.

最後に、Moleculedestroysは、テスト中に完了したインスタンスをdestroysし、それらのインスタンスに割り当てられたネットワークを削除します。

Output...
--> 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] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Delete docker network(s)] ************************************************
    skipping: [localhost]

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0

これでテストアクションが完了し、ロールが意図したとおりに機能したことを確認します。

結論

この記事では、Apacheとfirewalldをインストールおよび構成するためのAnsibleロールを作成しました。 次に、Moleculeが役割が正常に実行されたことを断定するために使用するTestinfraで単体テストを作成しました。

非常に複雑な役割にも同じ基本的な方法を使用でき、CIパイプラインを使用したテストも自動化できます。 Moleculeは、Dockerだけでなく、Ansibleがサポートするプロバイダーでロールをテストするために使用できる高度に構成可能なツールです。 また、独自のインフラストラクチャに対するテストを自動化して、ロールが常に最新かつ機能していることを確認することもできます。 How To Implement Continuous Testing of Ansible Roles Using Molecule and Travis CI on Ubuntu 18.04チュートリアルでMoleculeとTravis CIを使用して、継続的テストをワークフローに統合できます。

公式のMolecule documentationは、Moleculeの使用方法を学ぶための最良のリソースです。