Тестуємо проект на SaltStack c допомогою KitchenCI

Введення

У мене є pet project, яким я займаюся у вільний час. Цей проект повністю присвячений інфраструктурним експериментів.
Для управління конфігурацією я використовую SaltStack. SaltStack — це централізована система управління інфраструктурою. Це значить, є майстер-сервер, який налаштовує підлеглі сервери.
За час життя проекту я наступив на невеликий набір граблів, але в результаті прийшов до дуже зручному підходу роботи з ним. Загалом про це і стаття — як воно все починалося і до чого прийшов.
Коли дерева були великими
Весь проект був монолітним, в ньому було все:
  • стану (states) — інструкції, описи як і що налаштовувати;
  • структури даних (pillars) — дані, які використовуються в станах. Наприклад:
    • список системних пакетів під якусь задачу;
    • логін/пароль від Docker hub'a, які використовуються в станах з розгортання Docker контейнерів;
      списки серверів і призначені ним стану і дані.

Весь проект лежав в одному git репозиторії, який був підключений до майстер-сервера через gitfs. Це дуже зручно — не треба піклуватися про актуалізацію даних на майстер-сервері. SaltStack сам збирає всі з репозиторію.
Я міг би підняти тестову копію своєї інфраструктури і тестувати всі через неї, використовуючи окрему гілку в git-репозиторії. Але підняти копію інфраструктури для тестів дорого:
  • по грошах, якщо це хмари;
  • за часом, в будь-якому разі — треба взяти і зробити, і підтримувати в робочому стані.
З іншого боку, "бою" і так один суцільний "тест" і нічого страшного, якщо поламаю (ну як "не страшно", прикро буває). А раз не страшно, то кожна зміна, в тому числі і проміжне, я деплоил через push в репозиторій. Commit-лог став виглядати моторошно, м'яко кажучи:
  • спробуємо вирішити проблему по установці пакету...;
  • ще одна спроба виправити помилку...;
  • магія...;
  • ну тепер точно все;
  • ну тепер точно все №2;
  • якісь зміни, що забув в минулий раз;
насправді не все було так погано, але в цілому картинку приклад передає правильну %)
Далі стало ще гірше — з часом я почав забувати, яке стан що саме робить і як воно це робить. Все-таки це особистий проект і працюю над ним я не завжди, а час від часу. README файл вже погано вирішував цю проблему.
Також була ще й пов'язаність станів через дані. Різні стани використовували одні й ті ж дані, якщо змінити структуру даних для одного стану — інші гарантовано ламалися. З-за цього в якийсь момент часу "бою" конфігурація знаходиться в стані "кишки назовні". Я можу чинити баг протягом декількох днів, а значить, в цей час якийсь із станів могло залишатися неробочим. В цілому це свідчило про погану продуманості архітектури проекту.
Але всі ці мінуси мене мало хвилювали. В моєму режимі я готовий був з ними жити. Я Не міг миритися тільки з одним — змінюючи щось у структурі даних, я забував перевірити всі стани. Ловити потім такі "відстрочені" помилки довго і стрьомно.
Рішення — писати тести
Я зрозумів, що якщо я напишу тести, то у мене буде гарантія, що якщо я щось змінив, то автотесты перевірять результат роботи всіх станів. Ура! Все цілком просто. Задача зрозуміла: хочу перевіряти результат роботи станів у проекті.
Отже, що у нас є для тестування? Результат роботи SaltStack'a — це конфігураційні файли, сервіси, Docker контейнери, налаштування файерволу, SElinux і так далі. Ось це все чудово тестується за допомогою Serverspec тестів.
Я почав згадувати конференції, де був, згадувати статті, які зустрічав на цю тему. Загалом, російською з актуального і хорошого в голові крутився тільки один автор — Ігор Курочкін IgorITL, кого я слухав наживо на DevConf'e 2015. Можна подивитися його доповідь "Тестуємо інфраструктуру код":
Ще я знайшов непогану статтю для розуміння проблеми "Agile DevOps: Test-driven infrastructure".

Після прочитання всіх матеріалів я зрозумів, що для мого завдання підходить інструмент KitchenCI, так як він:
  • працює з SaltStack;
  • запускає інфраструктурний код де завгодно — Vagrant, Docker, lxc і купа різних хмар;
  • підтримує тестові фреймворки: bats, RSpec, Serverspec та інші.
Я порахував, що, здається, тепер я все знаю. Є теорія, в голові все вклалося — тепер-то вже точно можна почати писати тести, чи не так?
Перший млинець грудкою
я Подивився на проект і побачив, що якось теорія в моїй голові ну взагалі ніяк не вкладається на мою реальність. Як підійти до мого проекту? Куди класти тести? Як їх запускати?
У пошуках відповіді я знову поліз уважно читати документацію KitchenCI. На жаль, в ній сильно відволікає спеціалізація цього інструменту на Chef та його особливості. Приклади знову ж таки все для нього.
Давайте подивимося на KitchenCI трохи уважніше. У цьому інструменті ми оперуємо такими об'єктами:
  • драйвера   плагіни, з допомогою яких KitchenCI запускає віртуальні машини. Наприклад vagrant, docker, digitlocean і т. д. За замовчуванням використовується vagrant і мене це повністю влаштовує — хочу тести ганяти локально;
  • движок (provisioner), на якому описана наша конфігурація. За замовчуванням це Chef в режимі masterless;
  • платформа   ім'я образу, який буде використаний як база для нашої тестової віртуальної машини;
  • набори тестів для запуску (suite). Якщо нічого не змінювати, то KitchenCI буде намагатися знайти тести в директорії default, саме таке ім'я набору;
У мене ж використовується SaltStack. Гугл підказує нам, що є сторонній проект 'kitchen-salt', який реалізує provisioner salt_solo для SaltStack. Там же є докладний урок і приклад, як це використовувати.
Прочитавши документацію по KitchenCI і kitchen-salt, я виніс головне — тестуються окремі рецепти (в термінології Chef'a), а не вся конфігурація цілком. У SaltStack'е аналогом Chef'івських рецептів є формули — незалежні стану, винесені в самостійний проект. Ці формули використовуються для повторного використання коду в інших проектах. Наприклад, ціла пачка таких формул доступна на GitHub.
У цьому і полягає основна причина, чому мій проект "не підходить" для KitchenCI — він монолітний. В голові закрутилися слова "рефакторинг", "зв'язаність коду", "модульний підхід" тощо. Я засумував. Як я не програміст і слів-то таких знати не повинен.
Рефакторинг проекту
Challenge accepted! Наскільки я пам'ятаю перше правило рефакторінгу, у нього повинна бути ясна, досяжна і вимірна мета. Зазвичай це розгорнуту відповідь на питання "Для чого ми вносимо зміни?". У моєму випадку це було сформульовано наступним чином:
  • всі стану основного проекту повинні бути винесені в окремі дочірні проекти-формули;
  • кожна формула повинна мати README файл із описом;
  • кожна формула повинна супроводжуватися
    pillar.example
    файлом, прикладом структури зберігання даних, яку очікує даний стан;
  • кожна формула повинна бути оформлена у відповідності з вимогами і рекомендаціями, які можна знайти в офіційній документации.
Склавши список завдань, я знову засумував, попив кави і пішов робити. Стан за станом перетворювались в окремі формули. Виносячи стан з основного проекту, я вносив в конфігурацію майстер-сервера посилання на нову формулу. Таким чином, працездатність проекту особливо не страждала протягом всієї переробки.
З-за прив'язаності частини станів довелося переглянути структуру зберігання даних — я зробив її більш незалежною, розбив на які не перетинаються частини для різних формул і при цьому намагався уникнути дублювання даних. Це було, мабуть, найскладнішим. З-за цього частина логіки деяких станів була перенесена в інші.
Підсумком роботи стало майже два десятка окремих формул, прості і зрозумілі, з прикладами використовуваних даних і мінімальної документацією. Підсумкова структура даних стала помітно простіше. Навіть на цьому етапі я відчув позитивний результат — я тепер знав, що мої формули незалежні, і можна сміливо вносити до них зміни.
Тестування
Як тільки я почав розглядати окрему формулу як об'єкт тестування, у мене відразу ж склалася картинка в голові про те, як застосовувати KitchenCI. Давайте розберемо процес тестування на прикладі найпростішої формули "Common packages". Ця формула встановлює системні пакунки, які я очікую зустріти на будь-якому зі своїх серверів. Це просто звичні для мене утиліти.
NB! Далі за текстом, всі команди виконуються в корені проекту формули.
Ось так виглядає початкова файлова структура формули:
.git
common-packages/init.sls
pillar.example
README.md

Стан
init.sls
:
packages:
pkg.latest:
- pkgs:
{%- if pillar['packages'] is defined %}
{%- for package in pillar['packages'] %}
- {{ package }}
{% endfor %}
{% endif %}

Приклад даних,
pillar.example
:
packages:
- bind-utils
- whois
- git
- psmisc
- mlocate
- openssl
- bash-completion
- net-tools

Для роботи KitchenCI нам потрібно встановлені Vagrant і ruby (і gem bundler, звичайно). Створимо
Gemfile
зі списком необхідних ruby gems в корені проекту нашої формули:
source "https://rubygems.org"

gem "test-kitchen"
gem "kitchen-salt"
gem "kitchen-vagrant"

Встановлюємо перераховані залежності:
$ bundle install

Попросимо KitchenCI створити нам структуру і файли заглушки для тестів:
$ sudo kitchen init -P salt_solo

У нас з'явилися:
  • директорія для інтеграційних тестів набору:
    test/integration/default
  • файл
    chefignore
    , який ми сміливо можемо видалити, це "спадщина" тісної інтеграції KitchenCI і Chef'a
  • файл
    .gitignore
    (якщо він не був створений вами раніше), куди додалися рядки:
    .kitchen/
    .kitchen.local.yml

  • і самий головний файл
    .kitchen.yml
    з наступним вмістом
---
driver:
name: vagrant

provisioner:
name: salt_solo

platforms:
- name: ubuntu-14.04
- name: centos-7.2

suites:
- name: default
run_list:
attributes:

Вносимо в
.kitchen.yml
опис нашої формули:
---
driver:
name: vagrant

provisioner:
name: salt_solo
formula: common-packages # <- ім'я нашої формули
pillars-from-files:
packages.sls: pillar.example # <- використовуємо pillar.example, щоб бути впевненим за працездатність прикладу
pillars: # <- сюди ми вкладаємо структуру даних (pillar'и), повторюючи як файлову структуру так і вміст файлів!
top.sls:
base:
'*':
- packages

state_top: # <- вміст state.sls де ми призначаємо нашу формулу
base:
'*':
- common-packages

platforms:
- name: centos-7.2 # <--- у мене все під CentOS 7, тому я прибрав зайві платформи

suites:
- name: default
run_list:
attributes:

загалом, все готово. Давайте створимо віртуальну машину, налаштуємо її і проженемо в ній формулу:
$ kitchen converge centos-7.2


Так, KitchenCI виконав для нас наступні дії:
  1. створив віртуальну машину на базі CentOS 7;
  2. встановив і налаштував SaltStack в masterless режимі усередині цієї машини;
  3. застосував формулу;
  4. видав детальні логи про всіх перерахованих вище кроків.
Хо-хо! Я тепер можу розробляти формули і фиксить в них баги без необхідності коммитить проміжні зміни у майстер і викладати їх на "бою". "Бою" інфраструктура буде помітно стабільніше і, здається, мій commit-лог тепер буде не соромно показати, якщо раптом доведеться.
Можна подивитися руками результат роботи формули, зайшовши всередину машинки:
$ kitchen login centos-7.2

Я навчився за допомогою KitchenCI запускати формули і перевіряти їх працездатність. Перевіряти руками — це здорово. Але де ж автотесты? Давайте все-таки перевіряти результат роботи формули автотестами.
Для цього виконаємо наступні кроки:
  • Створюємо теку
    ./test/integration/default/serverspec
  • І в неї розміщуємо файл packages_spec.rb
packages_spec.rbУвага! Суфікс _spec обов'язковий. Почитати про це та інші нюанси і в цілому познайомитися з Serverspec можна на офіційному сайті: http://serverspec.org/.
require 'serverspec'

# Required by serverspec
set :backend, :exec

describe package('bind-utils') do
it { should be_installed }
end

describe package('whois') do
it { should be_installed }
end

describe package('git') do
it { should be_installed }
end

describe package('psmisc') do
it { should be_installed }
end

describe package('mlocate') do
it { should be_installed }
end

describe package('openssl') do
it { should be_installed }
end

describe package('bash-completion') do
it { should be_installed }
end

describe package('net-tools') do
it { should be_installed }
end

Щоб заощадити час і не чекати знову, поки машинка буде створена і налаштується з нуля, давайте просто попросимо KitchenCI прогнати тести:
$ kitchen verify centos-7.2


Ось і вся магія.
KitchenCI дозволяє зробити всі перераховані вище кроки однією командою: kitchen test. Буде створена віртуальна машина, прогонятся формула і тести, потім машинка буде знищена.
Функціональне тестування
kitchen-salt може тестувати не тільки окремі формули, але і їх набори. Тобто, ви цілком можете тестувати підсумковий результат роботи кількох формул. Така перевірка покаже, чи можуть ваші формули працювати спільно і вони дають очікуваний результат. Все це можливо завдяки різним комбінаціям опцій provisioner'a: https://github.com/simonmcc/kitchen-salt/blob/master/provisioner_options.md. А це значить, що я цілком міг і до початкового вигляду мого проекту прив'язати KitchenCI і тести, але як мені здається в підсумку вийшло значно краще.
Висновки
Тепер я потихеньку покриваю тестами свої старі формули і пишу нові, причому пишу набагато швидше, ніж раніше. І я в будь-який момент впевнений в працездатності своїх формул, як нових, так і старих. Так, незважаючи на тимчасові витрати на рефакторинг і написання тестів, я отримав явний приріст у роботі зі своїм кишеньковим проектом. Тепер немає побоювань, що відклавши проект на тривалий період часу, я не зможу продовжити його з-за складності самого проекту або незрозуміло чому не працюють формул. Так, рефакторинг зжер кілька днів мого особистого часу. Так, тести писати нудно, але вони дають відчуття впевненості у проекті. Класне таке почуття.
Буду радий відповісти на запитання, вислухати зауваження та поради на майбутнє :)
Посилання
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.