Введение



У меня есть 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 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


Да, 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 и тесты, но как мне кажется в итоге получилось значительно лучше.


Выводы


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


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


Ссылки


Поделиться с друзьями
-->

Комментарии (5)


  1. kix
    30.05.2016 16:59

    Хм, у вас на КДПВ слегка перерисованная картинка с затертым копирайтом, и

    источник

    image


    1. kix
      30.05.2016 17:03

      Пользуясь случаем, передам привет gerasimovich из IT-People, которым картинки рисовала alice_sleeping


    1. sugdyzhekov
      30.05.2016 17:19

      Искренне прошу прощения! Да фотошопили, но не копирайт. Исходная картинка у нас была без копирайта. Сейчас обновлю статью)


  1. rawsik
    31.05.2016 11:19

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

    При более менее сложной структуре разворота через salt через, в том числе его оркестратор, получая на выходе несколько БД ( мастер\слэйв), приложений, балансировщика и тп.
    Тестирование может заключаться в развороте всего этого через CI на VM ( тот же openstack) с пересозданием окружения и прогоном в обязательном порядке сначала smoke а потом полных тестов. Т.к. на выходе интересна полная рабочая система, что всё именно и в том порядке установилось ( чем иногда страдает salt ), корректно поднялось и между собой завелось.

    Соответственно при любой ошибке во время установке ( не получилось применить один из стейтов), подробно рапортует сам salt, плюс эти данные можно ловить из его event'ов


    1. sugdyzhekov
      31.05.2016 20:39

      Да, KitchenCI гоняет формулы и тесты в рамках одного сервера. Для автоматического тестирования мультисерверной инфраструктуры нужен другой инструмент для создания и прогона тестов. Сами тесты могут быть на том же Serverspec'e, например. То что вы описали, это вполне возможно хоть на том же Jenkins'e, правда не понятно на сколько удобно будет разбирать результаты.

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

      Я со своим проектом до такой проблемы еще не дорос :) И, к сожалению, специализированного инструмента не знаю. Может кто еще подскажет)