Тестируем проект на SaltStack c помощью KitchenCI

Введение


872706a97db04c01b983a2308af8e645.png

У меня есть pet project, которым я занимаюсь в свободное время. Этот проект полностью посвящён инфраструктурным экспериментам. b22a0adc14ec4a1b8377e33671965636.png
Для управления конфигурацией я использую 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».


f538b1c50ab04563a238bfb198c1e91d.png

После прочтения всех материалов я понял, что для моей задачи подходит инструмент 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 cо списком требуемых 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

1ab20e5c95f11c62b0405bfbf141913d.png

Да, 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

c8cd471ba618e86b605ff9a64305b922.png

Вот и вся магия.

KitchenCI позволяет сделать все вышеперечисленные шаги одной командой: kitchen test. Будет создана виртуальная машина, прогонятся формула и тесты, затем машинка будет уничтожена.


Функциональное тестирование

kitchen-salt может тестировать не только отдельные формулы, но и их наборы. То есть, вы вполне можете тестировать итоговый результат работы нескольких формул. Такая проверка покажет, могут ли ваши формулы работать совместно и дают ли они ожидаемый результат. Все это возможно благодаря различным комбинациям опций provisioner«a: https://github.com/simonmcc/kitchen-salt/blob/master/provisioner_options.md. А это значит, что я вполне мог и к исходному виду моего проекта привязать KitchenCI и тесты, но как мне кажется в итоге получилось значительно лучше.


Выводы

Теперь я потихоньку покрываю тестами свои старые формулы и пишу новые, причём пишу намного быстрее, чем раньше. И я в любой момент уверен в работоспособности своих формул, как новых, так и старых. Да, несмотря на временные затраты на рефакторинг и написание тестов, я получил явный прирост в работе со своим карманным проектом. Теперь нет опасений, что отложив проект на продолжительный период времени, я не смогу продолжить его из-за сложности самого проекта или непонятно почему не работающих формул. Да, рефакторинг сожрал несколько дней моего личного времени. Да, тесты писать скучно, но они дают чувство уверенности в проекте. Классное такое чувство.

Буду рад ответить на вопросы, выслушать замечания и советы на будущее:)


Ссылки


© Habrahabr.ru