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

Всім привіт!

Я працюю DevOps-інженером у сервісі бронювання готелів Ostrovok.ru. У цій статті я хочу розповісти про наш досвід тестування ansible-ролей.

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

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

Молекула

Молекула - Фреймворк для допомоги у тестуванні ansible-ролей.

Спрощений опис: Молекула створює інстанс на вказаній платформі (хмара, віртуалка, контейнер; докладніше див. розділ Водій), проганяє на ньому вашу роль, потім запускає тести та видаляє інстанс. У разі виникнення невдачі на одному з кроків Молекула повідомить вас про це.

Тепер докладніше.

Трохи теорії

Розглянемо дві ключові сутності Молекули: Scenario та Driver.

сценарій

Сценарій містить опис того, що, де, як і в якій послідовності буде виконано. В однієї ролі може бути кілька сценаріїв, і кожен — це директорія на шляху <role>/molecule/<scenario>, Що містить у собі опис необхідних для тесту дій. Обов'язково має бути сценарій default, який буде автоматично створений, якщо ви ініціалізуватимете роль за допомогою Молекули. Імена наступних сценаріїв вибираються на вашу думку.

Послідовність дій тестування у сценарії називається матриця, і за умовчанням вона така:

(Кроки, позначені ?, за замовчуванням пропускаються, якщо не описані користувачем)

  • 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. Очікуватиме ці файли Молекула буде в папці сценарію.

Водій

Драйвер – це суть, де створюються інстанси для тестів.
Список стандартних драйверів, для яких у Молекули готові шаблони, є: 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

залежність

Ця секція визначає джерело залежностей.

Можливі варіанти: галактика, позолота, shell.

Shell – це командна оболонка, яка використовується у випадку, якщо galaxy і gilt не покривають ваших потреб.

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

водій

Назва драйвера. В нас це docker.

стрічка

Як лінтер використовується yamllint.

Корисні налаштування в цій частині конфіга - це можливість вказати файл конфігурації для yamllint, прокинути змінні оточення або відключити лінтер:

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

Платформи

Визначає конфігурацію інстансів.
У випадку з докером у ролі драйвера, Молекула ітерується по цій секції, і кожен елемент списку доступний Dockerfile.j2 як змінна item.

У випадку з драйвером, у якому обов'язкові create.yml и destroy.yml, секція доступна в них як molecule_yml.platforms, А ітерації по ній описані вже в цих файлах.

Оскільки Молекула надає управління інстансами ansible-модулям, то список можливих налаштувань треба шукати там. Для докера, наприклад, використовується модуль docker_container_module. Які модулі використовуються в інших драйверах, можна знайти в документації.

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

Замінимо тут centos:7 на Ubuntu.

провізор

"Постачальник" - сутність, що управляє інстансами. У випадку Молекули це ansible, підтримка інших не планується, тому цю секцію можна із застереженням назвати розширеною конфігурацією ansible.
Тут можна вказати багато всього, виокремлю основні, на мій погляд, моменти:

  • підручники: Ви можете вказати, які плейбуки повинні використовуватися на певних стадіях.

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'"

  • опції: параметри Ansible та змінні оточення

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

сценарій

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

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

верифікатор

Налаштування фреймворку для тестів та лінтера до нього. За замовчуванням як лінтер використовується 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-ролей, і він не пов'язаний з Молекулою – розкажіть про нього у коментарях!

Джерело: habr.com

Додати коментар або відгук