Инструкция: как тестировать ansible-роли и узнавать о проблемах до продакшена

    Всем привет!


    Я работаю DevOps-инженером в сервисе бронирования отелей Ostrovok.ru. В этой статье я хочу рассказать о нашем опыте тестирования ansible-ролей.


    В Ostrovok.ru в качестве менеджера конфигураций мы используем ansible. Недавно мы пришли к необходимости тестирования ролей, но, как оказалось, инструментов для этого существует не так много — самым популярным, пожалуй, является фреймворк Molecule, поэтому мы решили использовать его. Но оказалось, что его документация умалчивает о многих подводных камнях. Достаточно подробного руководства на русском нам не удалось найти, поэтому мы решили написать эту статью.



    Molecule


    Молекула — фреймворк для помощи в тестировании ansible-ролей.


    Упрощенное описание: Молекула создаёт инстанс на указанной вами платформе (облако, виртуалка, контейнер; подробнее см. раздел Driver), прогоняет на нём вашу роль, затем запускает тесты и удаляет инстанс. В случае возникновения неудачи на одном из шагов, Молекула сообщит вам об этом.


    Теперь подробнее.


    Немного теории


    Рассмотрим две ключевые сущности Молекулы: Scenario и Driver.


    Scenario


    Сценарий содержит в себе описание того, что, где, как и в какой последовательности будет выполнено. У одной роли может быть несколько сценариев, и каждый — это директория по пути <role>/molecule/<scenario>, содержащая в себе описания необходимых для теста действий. Обязательно должен присутствовать сценарий default, который будет автоматически создан, если вы инициализируете роль с помощью Молекулы. Имена следующих сценариев выбираются на ваше усмотрение.


    Последовательность действий тестирования в сценарии называется matrix, и по умолчанию она такова:


    (Шаги, помеченные ?, по умолчанию пропускаются, если не описаны пользователем)


    • lint — прогон линтеров. По умолчанию используются yamllint и flake8,
    • destroy — удаление инстансов с прошлого запуска Молекулы (если остались),
    • dependency? — установка ansible-зависимости тестируемой роли,
    • syntax — проверка синтаксиса роли с помощью ansible-playbook --syntax-check,
    • create — создание инстанса,
    • prepare? — подготовка инстанса; например, проверка / установка python2
    • converge — запуск тестируемого плейбука,
    • idempotence — повторный запуск плейбука для теста на идемпотентность,
    • side_effect? — действия, не относящиеся непосредственно к роли, но нужные для тестов,
    • verify — запуск тестов получившейся конфигурации с помощью testinfra(по умолчанию) /goss/inspec,
    • cleanup? — (в новых версиях) — грубо говоря, "очистка" внешней инфраструктуры, задетой Молекулой,
    • destroy — удаление инстанса.

    Эта последовательность покрывает большинство случаев, но, при необходимости, её можно изменить.


    Каждый из вышеперечисленных шагов можно запускать отдельно с помощью molecule <command>. Но стоит понимать, что для каждой такой cli-команды может существовать своя последовательность действий, узнать которую можно, выполнив molecule matrix <command>. Например, при запуске команды converge (прогон тестируемого плейбука) будут выполнены следующие действия:


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

    Последовательность этих действий можно редактировать. Если что-то из списка уже выполнено, то оно будет пропущено. Текущее состояние, а также конфиг инстансов, Молекула хранит в директории $TMPDIR/molecule/<role>/<scenario>.


    Добавить шаги с ? можно, описав желаемые действия в формате ansible-плейбука, а имя файла сделать соответственно шагу: prepare.yml/side_effect.yml. Ожидать эти файлы Молекула будет в папке сценария.


    Driver


    Драйвер – это сущность, где создаются инстансы для тестов.
    Список стандартных драйверов, для которых у Молекулы готовы шаблоны, таков: Azure, Docker, EC2, GCE, LXC, LXD, OpenStack, Vagrant, Delegated.


    В большинстве случаев шаблоны – это файлы create.yml и destroy.yml в папке сценария, которые описывают создание и удаление инстанса соответственно.
    Исключения составляют Docker и Vagrant, так как взаимодействия с их модулями может происходить без вышеупомянутых файлов.


    Стоит выделить драйвер Delegated, так как в случае его использования в файлах создания и удаления инстанса описана только работа с конфигурацией инстансов, остальное должен описать инженер.


    Драйвером по умолчанию является Docker.


    Теперь перейдём к практике и дальнейшие особенности рассмотрим там.


    Начало работы


    В качестве "hello world" протестируем простую роль установки nginx. В качестве драйвера выберем докер – думаю, он установлен у большинства из вас (и помним, что докер — драйвер по умолчанию).


    Подготовим virtualenv и установим в него molecule:


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

    Следующим шагом инициализируем новую роль.
    Инициализация новой роли, как и нового сценария, производятся с помощью команды 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

    Получилась типичная ansible-роль. Далее все взаимодействия с CLI Молекулы производятся из корня роли.


    Посмотрим, что находится в директории роли:


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

    Разберём конфиг molecule/default/molecule.yml (заменим только 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


    Эта секция описывает источник зависимостей.


    Возможные варианты: galaxy, gilt, shell.


    Shell – это просто командная оболочка, которая используется в случае, если galaxy и gilt не покрывают ваших потребностей.


    Не буду здесь долго останавливаться, достаточно описано в документации.


    driver


    Название драйвера. У нас это docker.


    lint


    В качестве линтера используется yamllint.


    Полезные опции в данной части конфига — это возможность указать файл конфигурации для yamllint, пробросить переменные окружения либо отключить линтер:


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

    platforms


    Описывает конфигурацию инстансов.
    В случае с докером в роли драйвера, Молекула итерируется по этой секции, и каждый элемент списка доступен в Dockerfile.j2 как переменная item.


    В случае с драйвером, в котором обязательны create.yml и destroy.yml, секция доступна в них как molecule_yml.platforms, а итерации по ней описаны уже в этих файлах.


    Поскольку Молекула предоставляет управление инстансами ansible-модулям, то и список возможных настроек надо искать там. Для докера, например, используется модуль docker_container_module. Какие модули используются в остальных драйверах, можно найти в документации.


    А также примеры использования различных драйверов можно найти в тестах самой Молекулы.


    Заменим здесь centos:7 на ubuntu.


    provisioner


    "Поставщик" — сущность, управляющая инстансами. В случае Молекулы это ansible, поддержка других не планируется, поэтому эту секцию можно с оговоркой назвать расширенной конфигурацией ansible.
    Здесь можно указать много всего, выделю основные, на мой взгляд, моменты:


    • playbooks: можно указывать, какие плейбуки должны использоваться на определённых стадиях.

    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 и переменные окружения

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

    scenario


    Название и описание последовательностей сценария.
    Изменить матрицу действий по умолчанию какой-либо команды можно, добавив ключ <command>_sequence и как значение для него определив нужный нам список шагов.
    Допустим, мы хотим изменить последовательность действий при запуске команды прогона плейбука: molecule converge


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

    verifier


    Настройка фреймворка для тестов и линтера к нему. По умолчанию в качестве линтера используется testinfra и flake8. Возможные опции схожи с вышеизложенными:


    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

    Вернёмся к нашей роли. Отредактируем файл tasks/main.yml до такого вида:


    ---
    - name: Install nginx
      apt:
        name: nginx
        state: present
    
    - name: Start nginx
      service:
        name: nginx
        state: started
    

    И добавим тесты в 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")
    

    Готово, осталось только запустить (из корня роли, напомню):


    > molecule test

    Длинный выхлоп под спойлером:
    --> 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
    

    Наша простая роль протестировалась без проблем.
    Стоит помнить, что если возникли проблемы при работе molecule test, то, если вы не изменяли стандартную последовательность, Молекула удалит инстанс.


    Для дебага полезны следующие команды:


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

    Существующая роль


    Добавление нового сценария к существующей роли происходит из директории роли следующими командами:


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

    В случае, если это первый сценарий в роли, то параметр -s можно опустить, так как будет создан сценарий default.


    Заключение


    Как видите, Молекула не очень сложна, а при использовании собственных шаблонов можно свести развертывание нового сценария к правке переменных в плейбуках создания и удаления инстансов. Молекула без проблем интегрируется с системами CI, что позволяет увеличить скорость разработки за счет сокращения времени на ручное тестирование плейбуков.


    Спасибо за ваше внимание. Если у вас есть опыт тестирования ansible-ролей, и он не связан с Молекулой — расскажите о нем в комментариях!

    Ostrovok.ru
    106,00
    Компания
    Поделиться публикацией

    Комментарии 15

      +1
      спасибо за статью.
        +2
        Если сравнивать со спецназом, то ansible тренируется в здании похожем на захваченное террористами. Docker приезжает на спецоперецию со своим зданием.
          0
          его документация умалчивает о многих подводных камнях

          так о каких камнях она умалчивает?..
            0

            Например, о директории, в которой хранится state, и знание о которой поможет в дебаге с большим количеством инстансов. К сожалению, не могу вспомнить всего списка, т.к. с момента написания статьи прошло некоторое время, а с версии 2.20 документация заметно улучшилась по сравнению с тем, что было в 2.19. Но, возможно, выражение о "многих подводных камнях" в данном случае действительно слишком претензиозное.

              0
              Претензиозность дело такое, на сам текст не осбо влияет, но если открываешь статью именно что б найти «умолчанное» — то это достаточно сложно сделать.
            0
            Вот было бы что-то такое же, но для плейбуков.
              0
              Так вроде можно
              molecule.yml / playbook.yml
              molecule.yml
              ...
              ansible:
                requirements_file: requirements.yml
              ...
              
              playbook.yml
              ...
              - name: Converge
                hosts: all
                any_errors_fatal: true
                roles:
                  - role: common
              ...
              

              молекула загрузит роли из requirements.yml
              хосты и группы можно определить в molecule.yml
              0
              А вы пробовали использовать вместо testinfra что то еще с молекулой?
              Например serverspec/inspec/goss.
              Можно ли расшарить один enviroment молекулы между разными ролями?
              Например я тестирую свои роли для CentOS/Debian/OpenSUSE/Oracle/Scientific/Ubuntu и на выходе у меня получается 11 виртуальных машин (Которые собраны с помощью packer и максимально приближены к чистой инсталяции firewall/selinx включены и т.д.).
              И вот поднимать по 11 инстансов для каждой роли не очень удобно.
                +1
                Нет, нам пока хватает testinfra.
                Не совсем понял второй вопрос, что Вы имеете в виду под environment?
                  0
                  Например есть у меня роль common (настройки timezone/ssh/cron/...) и например elk (elasticsearc/kibana/logstash/filebeat/...) для каждой роли поднимается свой набор виртуалок. У меня получается их много, если бы можно было как то расшарить их между ролями.
                  Да вопрос наверно не совсем корректный, а точнее не имеет смысла.
                  Роли должны все таки тестироваться изолированно.
                  0
                  В playbook.yml можно несколько ролей записать, они последовательно применятся на одной виртуалке.
                    0
                    Мы окружение описываем в матрице CI. Вот так выглядит наш файл для молекулы
                    molecule.yml
                    ---
                    dependency:
                      name: galaxy
                    driver:
                      name: docker
                    lint:
                      name: yamllint
                      options:
                        config-file: molecule/default/.yamllint
                    platforms:
                      - name: ${MOLECULE_PROJECT_NAMESPACE:-infra}-${MOLECULE_JOB_STAGE:-test}-${MOLECULE_PROJECT_NAME:-activemq}-01-${MOLECULE_DISTRO:-centos7}
                        image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible"
                        image_version: latest
                        command: ${MOLECULE_DOCKER_COMMAND:-"/usr/sbin/init"}
                        volumes:
                          - /sys/fs/cgroup:/sys/fs/cgroup:ro
                        privileged: true
                        pre_build_image: true
                    provisioner:
                      name: ansible
                      lint:
                        name: ansible-lint
                      playbooks:
                        converge: ${MOLECULE_PLAYBOOK:-playbook.yml}
                    scenario:
                      name: default
                    verifier:
                      name: testinfra
                      lint:
                        name: flake8


                    0
                    В статье тема раскрыта более шире
                      +1
                      В предложенной Вами статье действительно шире раскрыта глобальная тема тестирования ролей. Наша же статья только о Молекуле
                        0
                        Наша же статья только о Молекуле

                        Тогда к чему было это
                        Если у вас есть опыт тестирования ansible-ролей, и он не связан с Молекулой — расскажите о нем в комментариях!

                        в конце статьи?

                        По сути — действительно, было бы интересно увидеть именно про заявленные «подводные камни».

                        С molecule есть много «приколов», но для тестирования это отличная вещь, особенно, начиная со второй версии.
                        Конечно, и сами Ansible роли нужно тоже создавать с учётом того, как они будут тестироваться — делать их изначально небольшими и не злоупотреблять зависимостями — иначе каждый раз тестируя свою отдельную роль, большая часть времения будет уходить на прогон других ролей.
                        Также, если тестируются роли на docker image, а реально они будут deploy-иться на EC2 из каких-то AMI, то очевидно, что разница между docker image и AMI как базовой системы может в итоге «подложить свинью» при реальном deploy.

                        Кстати, вместо prepare.yml мы используем «pre_tasks» перед «roles» и «tasks» — после. «pre_tasks» позволяет «подшаманить» базовый docker image — создать пользователей, например, установить пакеты типа даже sudo, shadow-utils и даже unzip, что актуально для Oracle Linux docker images (да, это можно и в Dockerfile положить, но мы делаем тем же Ansible, чтобы не морочиться синтаксисом и прочим для разных типов дистрибов Linux). «tasks» после блока «roles» с тестируемой ролью (или нескольких ролей) служит, чтобы стартануть сервисы или сконфигурировать что-то — проверить как поведёт себя то, что устанавливалось ролью после запуска инстанса. В ролях не всегда имеет смысл запускать сервисы немедленно, особенно, если Ansible используется для сборки AMI в provisioner-е packer, например.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое