Почему мы уверены в том, что развернули
Часто бывает, когда что-то не работает. И никто не хочет, чтобы что-то не работало по его вине. В контексте больших инфраструктур и распределенных приложений ошибка конфигурации может быть фатальной.
В статье я покажу как правильно тестировать окружение для приложения, какие инструменты использовать, приведу примеры удачного и целесообразного тестирования.
Статья будет интересна командам, которые практикуют DevOps или SRE, ответственным Dev, и прочим хорошим людям.
Следует сказать, что реализовывать концепцию Infrastructure as Code можно и без тестирования. И даже будет работать. Но разве есть предел совершенству?
Тестирование к нам пришло из мира разработки, тестирование — это целая огромная эпопея, которая включает в себя такие варианты реализаций:
- Юнит тесты (тестируют код) — есть функция, которая принимает на вход один параметр, проводит вычисления и возвращает результат. Юниты — проверка того, что результат соответствует ожидаемому, грубо говоря, что 1 == 1.
- Интеграционные тесты (тестируют взаимосвязь компонентов) — есть приложение, которому необходима база для корректной работы, и в ней табличка `users`. Этот тест проверит что есть база, что есть табличка, что все готово к работе.
- Системные тесты (тестируют приложение в целом) — есть приложение, оно должно конвертировать файл. Тест проверяет, что приложение конвертирует, и результат нас устраивает.
- Дымовые тесты (имитируют поведение пользователя) — тестирует, что пользователь может открыть в браузере сайт, сделать log in, загрузить изображение, и сконвертировать его.
И тут может прийти лживое осознание, что в нашем мире Ansible/Chef/Puppet/Salt все очень примитивно, все очень просто, и в экосистеме наших тулзовин нету реализации чего-то подобного.
А вот как-бы не так.
Все это есть, все это используют уже давно и успешно.
Чем тестировать
Уточню сразу, что мы не будем рассматривать высокоуровневое программирование для Configuration management. Например, если вы написали свой Ansible модуль, или Chef LWRP — ваших знаний достаточно, чтобы понять как правильно тестировать ваше творение.
По сути все рассматриваемые фреймворки для тестирования очень похожи, отличаются набором готовых тестовых кейсов и синтаксисом.
Везде будем решать одну задачу для примера: мы должны убедиться в том, что пакет 'apache2' у нас установлен, и сервис 'apache2' запущен и работает. В конце проверяем, что порт 80 кто-то слушает.
ServerSpec
Первый фреймворк для тестирования, который мы рассмотрим — Serverspec.
Пример использования:
require 'spec_helper'
describe package('apache2') do
it { should be_installed }
end
describe service('apache2') do
it { should be_enabled }
it { should be_running }
end
describe port(80) do
it { should be_listening }
end
InSpec
Второй фреймворк — InSpec.
Синтаксис идентичный Serverspec.
Testinfra
Третий фреймворк — Testinfra.
def test_apache_is_installed(Package):
apache = Package("apache2")
assert apache.is_installed
def test_apache_running_and_enabled(Service):
apache = Service("apache2")
assert apache.is_running
assert apache.is_enabled
def test_apache_port(Socket):
sock = Socket("tcp://80")
assert sock.is_listening
Goss
Четвертый фреймворк — Goss.
package:
apache2:
installed: true
service:
apache2:
enabled: true
running: true
port:
tcp:80:
listening: true
Serverspec/InSpec/Testinfra/Goss?
Как видно по синтаксису — есть из чего выбрать. Кому что больше нравится, то лучше всего и использовать. Например, если у вас Chef — отлично ляжет InSpec или Serverspec. Если Ansible или Salt — круто подойдет Testinfra или Goss.
Все отличия — это наличие или отсутствие готовых модулей, первым делом необходимо подготовить список необходимых требований для фреймворка и искать тот, в котором есть все что нужно (или почти все).
Окей, выбрали. Что же именно тестировать?
Что тестировать
Несколько недель назад мы общались в нашем DevOps Community по поводу того, что именно нужно тестировать, нужно ли вообще что-то тестировать, сколько лет лететь до Trappist-1 и так далее.
Например, ctrlok считает что тестировать декларативные штуки не правильно. И я с ним согласен.
Оговорюсь, что точно не нужно тестировать:
- Разные декларативные штуки (наличие пакета, версию пакета, присутствие файла) — обо всем этом позаботится ваш Configuration management.
Что нужно тестировать:
- Вещи, которые когда-то/недавно уже ломались
- Вещи, которые потенциально могут поломаться
- Вещи, которые кто-то (коллега) может поломать из-за незнания
- Вещи, без которых ничего точно работать не будет
У нас есть очень много приложений в контексте одного продукта, и все они требуют тонкой настройки окружения для нормальной работы. Одним из самых тревожных и узких мест было приложение, которое занимается конвертацией различных форматов документов. Не трудно догадаться, что в нем используются десятки пакетов и сотни библиотек, и это все может внезапно перестать работать, если обновляется какой-нибудь пакет, от которого оно зависит.
Для того, чтобы правильно оценить приложение и подготовить его для тестирования — нужно думать как тестировщик. Или, например, проконсультироваться у команды QA.
Во время общения необходимо продумать основные тест-кейсы и попытаться покрыть все из них. Практический пример (на нашем приложении):
- Конвертация из pdf в html
- Конвертация из html в pdf
- Конвертация из doc/docx в pdf
В нашем случае отдельные пункты — это отдельные модули приложения, именно так мы их и будем тестировать.
Давайте тестировать!
Покажу небольшой пример интеграционно-системных тестов первого модуля приложения. Уточню, что у нас есть роль, которая должна правильно установить консольную утилиту pdf2htmlex, где четко описаны все зависимости и необходимые штуки, т.е. мы имеем ввиду что процесс развертывания окружения прошел успешно, и наступил этап тестирования инфраструктуры.
Для начала, предлагаю убедиться что бинарник доступен, его можно запустить, и что-то получить в результат:
# Validate binary exists and working
describe bash('pdf2htmlEX --version') do
its('stderr') { should match /pdf2htmlEX version 0.14.6/ }
its('exit_status') { should eq 0 }
end
Теперь давайте протестируем, что этот бинарник реально умеет конвертировать тестовый документ, и результат нас устраивает:
# Try to convert and validate result html
describe bash('pdf2htmlEX /opt/test/pdf-sample.pdf /tmp/pdf-sample.html') do
its('stderr') { should match /Working/ }
its('exit_status') { should eq 0 }
end
describe file('/tmp/pdf-sample.html') do
it { should exist }
its('content') { should match //}
its('size') { should eq 267189 }
end
В данном случае, если тест пройдет успешно — мы можем четко декларировать, что модуль приложения, который отвечает за конвертацию из PDF в HTML работает. Мало того, он работает корректно, нас устраивает результат его работы.
Сложно и долго делать такой тест? Не думаю.
Сколько это сэкономит денег бизнесу? У каждого своя цена ошибки.
Benefits
Стоит сказать, что тестировать окружения мы начали не сразу. Зачем тратить время на такие вещи — это же очевидно, что все мы супермены, и пишем код для Configuration Management сразу без ошибок?
И такой подход работает, когда DevOps команда состоит из 1–2 человек. В таком случае каждый инженер знает как должно работать приложение, знает как готовить для него окружение, и как его «потыкать» — посмотреть, что оно будет работать на площадке, которую он только что подготовил.
Все заканчивается тогда, когда в production среде все должно работать, но не работает. Начинается debug процесс руками на проде или откаты-роллбеки, а каждая минута очень дорого стоит для бизнеса.
Именно поэтому тестирование инфраструктуры — это инвестиция в будущее.
Теперь, когда что-то ломается — мы абсолютно уверены, что наша часть работы была сделана на 100%, и с нашей стороны все вопросы закрыты.
Поэтому мы и уверены в том, что только что развернули.
Выводы
Все мы работаем с приложениями, которые очень часто меняются в очень динамичном мире благодаря нашим же правильно настроенным процессам — CI и CD. Мы все очень серьезно относимся к работе самих приложений, тестируя со всех сторон одно и тоже. И в это же время халатно относиться к инфраструктуре, на которой это приложение работает — не очень разумно. Тем более, это чревато большими последствиями.
Для себя мы решили, что будем тестировать в таких случаях:
- если в модуле системы есть сложная логика (комплексность)
- если модуль уже сейчас часто ломается (доступность)
- если нужно передать специфичные знания (коммуникация)
- если ошибка будет стоить нам больше Х денег (бизнес-ценность)
Основным посылом было донести всю важность тестирования инфраструктуры и побудить использовать приведенные инструменты если не прямо сейчас, то в ближайшем будущем.
А сколько процентов вашей инфраструктуры покрыто тестами?