Instructions: how to test ansible roles and find out about problems before production

Hi all!

I work as a DevOps engineer in a hotel booking service. Ostrovok.ru. In this article, I want to talk about our experience in testing ansible roles.

At Ostrovok.ru, we use ansible as a configuration manager. Recently, we came to the need to test roles, but as it turned out, there are not so many tools for this - the most popular, perhaps, is the Molecule framework, so we decided to use it. But it turned out that his documentation is silent about many pitfalls. We could not find a sufficiently detailed manual in Russian, so we decided to write this article.

Instructions: how to test ansible roles and find out about problems before production

Molecule

Molecule - a framework to help test ansible roles.

Simplified description: The molecule creates an instance on the platform you specify (cloud, virtual machine, container; for more details, see the section driver), runs your role on it, then runs tests and deletes the instance. In case of failure on one of the steps, the Molecule will inform you about it.

Now the details.

Some theory

Consider two key entities of the Molecule: Scenario and Driver.

Screenwriting

The script contains a description of what, where, how and in what sequence will be performed. One role can have several scripts, and each is a directory along the path <role>/molecule/<scenario>, which contains descriptions of the actions required for the test. Script must be included default, which will be automatically created if you initialize the role with a Molecule. The names of the following scripts are up to you.

The sequence of testing actions in a script is called matrix, and by default it is:

(Steps labeled ?, skipped by default if not specified by the user)

  • lint - running linters. By default are used yamllint и flake8,
  • destroy - deleting instances from the last launch of the Molecule (if any),
  • dependency? — installation of the ansible dependency of the tested role,
  • syntax - checking the syntax of the role using ansible-playbook --syntax-check,
  • create - creating an instance,
  • prepare? — preparation of the instance; e.g. check/install python2
  • converge — launch of the playbook being tested,
  • idempotence - restarting the playbook for the idempotency test,
  • side_effect? - actions not directly related to the role, but necessary for tests,
  • verify - running tests of the resulting configuration using testinfra(default) /goss/inspec,
  • cleanup? - (in new versions) - roughly speaking, "cleaning" the external infrastructure affected by the Molecule,
  • destroy - Deleting an instance.

This sequence covers most cases, but can be changed if necessary.

Each of the above steps can be run separately with molecule <command>. But it should be understood that for each such cli-command there may be its own sequence of actions, which you can find out by executing molecule matrix <command>. For example, when running the command converge (running the playbook under test), the following actions will be performed:

$ molecule matrix converge
...
└── default         # название сценария
    ├── dependency  # установка зависимостей
    ├── create      # создание инстанса
    ├── prepare     # преднастройка инстанса
    └── converge    # прогон плейбука

The sequence of these actions can be edited. If something from the list is already done, it will be skipped. The current state, as well as the config of the instances, the Molecule stores in the directory $TMPDIR/molecule/<role>/<scenario>.

Add steps with ? you can describe the desired actions in the ansible-playbook format, and make the file name according to the step: prepare.yml/side_effect.yml. Expect these files The molecule will be in the script folder.

driver

A driver is an entity where test instances are created.
The list of standard drivers for which Molecule has templates ready is as follows: Azure, Docker, EC2, GCE, LXC, LXD, OpenStack, Vagrant, Delegated.

In most cases, templates are files create.yml и destroy.yml in the script folder that describe the creation and deletion of an instance, respectively.
The exceptions are Docker and Vagrant, as interactions with their modules can occur without the aforementioned files.

It is worth highlighting the Delegated driver, since if it is used in the files for creating and deleting an instance, only work with the configuration of instances is described, the rest should be described by the engineer.

The default driver is Docker.

Now let's move on to practice and consider further features there.

Beginning of work

As a "hello world", let's test a simple nginx installation role. We will choose docker as the driver - I think most of you have it installed (and remember that docker is the default driver).

Prepare virtualenv and install in it molecule:

> pip install virtualenv
> virtualenv -p `which python2` venv
> source venv/bin/activate
> pip install molecule docker  # molecule установит ansible как зависимость; docker для драйвера

The next step is to initialize the new role.
Initialization of a new role, as well as a new script, is performed using the command molecule init <params>:

> molecule init role -r nginx
--> Initializing new role nginx...
Initialized role in <path>/nginx successfully.
> cd nginx
> tree -L 1
.
├── README.md
├── defaults
├── handlers
├── meta
├── molecule
├── tasks
└── vars

6 directories, 1 file

It turned out a typical ansible-role. Further, all interactions with CLI Molecules are made from the root of the role.

Let's see what is in the role directory:

> tree molecule/default/
molecule/default/
├── Dockerfile.j2  # Jinja-шаблон для Dockerfile
├── INSTALL.rst.   # Немного информации об установке зависимостей сценария
├── molecule.yml   # Файл конфигурации
├── playbook.yml   # Плейбук запуска роли
└── tests          # Директория с тестами стадии verify
    └── test_default.py

1 directory, 6 files

Let's analyze the config molecule/default/molecule.yml (replace only docker image):

---
dependency:
  name: galaxy
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: instance
    image: centos:7
provisioner:
  name: ansible
  lint:
    name: ansible-lint
scenario:
  name: default
verifier:
  name: testinfra
  lint:
    name: flake8

dependency

This section describes the source of dependencies.

Possible options: galaxy, applies,shell.

Shell is just a command shell that is used in case galaxy and gilt don't cover your needs.

I will not dwell here for a long time, it is enough described in documentation.

driver

The name of the driver. Ours is docker.

ribbon

The linter is yamllint.

Useful options in this part of the config are the ability to specify a configuration file for yamllint, forward environment variables, or disable the linter:

lint:
  name: yamllint
  options:
    config-file: foo/bar
  env:
    FOO: bar
  enabled: False

platforms

Describes the configuration of the instances.
In the case of docker as a driver, the Molecule is iterated over this section, and each element of the list is available in Dockerfile.j2 as a variable item.

In the case of a driver that requires create.yml и destroy.yml, the section is available in them as molecule_yml.platforms, and iterations over it are already described in these files.

Since the Molecule provides control of instances to ansible modules, the list of possible settings should also be looked for there. For docker, for example, the module is used docker_container_module. Which modules are used in other drivers can be found in documentation.

As well as examples of the use of various drivers can be found in the tests of the Molecule itself.

Replace here centos:7 on ubuntu.

provisioner

"Supplier" - an entity that manages instances. In the case of Molecule, this is ansible, support for others is not planned, so this section can be called ansible extended configuration with a caveat.
Here you can specify a lot of things, I will highlight the main points, in my opinion:

  • playbooks: you can specify which playbooks should be used at certain stages.

provisioner:
  name: ansible
  playbooks:
    create: create.yml
    destroy: ../default/destroy.yml
    converge: playbook.yml
    side_effect: side_effect.yml
    cleanup: cleanup.yml

provisioner:
  name: ansible
  config_options:
    defaults:
      fact_caching: jsonfile
    ssh_connection:
      scp_if_ssh: True

provisioner:
  name: ansible  
  connection_options:
    ansible_ssh_common_args: "-o 'UserKnownHostsFile=/dev/null' -o 'ForwardAgent=yes'"

  • options: Ansible options and environment variables

provisioner:
  name: ansible  
  options:
    vvv: true
    diff: true
  env:
    FOO: BAR

scenario

Name and description of script sequences.
You can change the default action matrix of any command by adding the key <command>_sequence and as a value for it by defining the list of steps we need.
Let's say we want to change the sequence of actions when running the playbook run command: molecule converge

# изначально:
# - dependency
# - create
# - prepare
# - converge
scenario:
  name: default
  converge_sequence:
    - create
    - converge

verifier

Setting up a framework for tests and a linter to it. The default linter is testinfra и flake8. The possible options are the same as above:

verifier:
  name: testinfra
  additional_files_or_dirs:
    - ../path/to/test_1.py
    - ../path/to/test_2.py
    - ../path/to/directory/*
  options:
    n: 1
  enabled: False
  env:
    FOO: bar
  lint:
    name: flake8
    options:
      benchmark: True
    enabled: False
    env:
      FOO: bar

Let's return to our role. Let's edit the file tasks/main.yml to this kind:

---
- name: Install nginx
  apt:
    name: nginx
    state: present

- name: Start nginx
  service:
    name: nginx
    state: started

And add tests to molecule/default/tests/test_default.py

def test_nginx_is_installed(host):
    nginx = host.package("nginx")
    assert nginx.is_installed

def test_nginx_running_and_enabled(host):
    nginx = host.service("nginx")
    assert nginx.is_running
    assert nginx.is_enabled

def test_nginx_config(host):
    host.run("nginx -t")

Done, it remains only to run (from the root of the role, let me remind you):

> molecule test

Long exhaust under the spoiler:

--> Validating schema <path>/nginx/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix

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

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in <path>/nginx/...
Lint completed successfully.
--> Executing Flake8 on files found in <path>/nginx/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on <path>/nginx/molecule/default/playbook.yml...
Lint completed successfully.
--> 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)] ************************************************

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

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

    playbook: <path>/nginx/molecule/default/playbook.yml

--> Scenario: 'default'
--> 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 [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)] ************************************************

    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

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

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

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

    TASK [nginx : Install nginx] ***************************************************
    changed: [instance]

    TASK [nginx : Start nginx] *****************************************************
    changed: [instance]

    PLAY RECAP *********************************************************************
    instance                   : ok=3    changed=2    unreachable=0    failed=0

--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in <path>/nginx/molecule/default/tests/...
    ============================= test session starts ==============================
    platform darwin -- Python 2.7.15, pytest-4.3.0, py-1.8.0, pluggy-0.9.0
    rootdir: <path>/nginx/molecule/default, inifile:
    plugins: testinfra-1.16.0
collected 4 items

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

    ========================== 4 passed in 27.23 seconds ===========================
Verifier completed successfully.
--> 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)] ************************************************

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

Our simple role was tested without problems.
It is worth remembering that if there are problems during the work molecule test, then if you didn't change the default sequence, the Molecule will delete the instance.

The following commands are useful for debugging:

> molecule --debug <command> # debug info. При обычном запуске Молекула скрывает логи.
> molecule converge          # Оставляет инстанс после прогона тестируемой роли.
> molecule login             # Зайти в созданный инстанс.
> molecule --help            # Полный список команд.

Existing Role

Adding a new script to an existing role is from the role directory with the following commands:

# полный список доступных параметров
> molecule init scenarion --help
# создание нового сценария
> molecule init scenario -r <role_name> -s <scenario_name>

In case this is the first scenario in the role, then the parameter -s can be omitted as it will create a script default.

Conclusion

As you can see, the Molecule is not very complex, and by using your own templates, deploying a new script can be reduced to editing variables in the instance creation and deletion playbooks. The molecule integrates seamlessly with CI systems, which allows you to increase the speed of development by reducing the time for manual testing of playbooks.

Thank you for your attention. If you have experience in testing ansible roles, and it is not related to the Molecule, tell us about it in the comments!

Source: habr.com

Add a comment