$ SKIP_deploy_db=true \
SKIP_deploy_app=true \
SKIP_validate_app=true \
go test -timeout 30m -run 'TestHelloWorldAppStageWithStages'
(...)
The 'SKIP_deploy_db' environment variable is set, so skipping stage 'deploy_db'.
(...)
The 'SKIP_deploy_app' environment variable is set, so skipping stage 'deploy_app'.
(...)
The 'SKIP_validate_app' environment variable is set, so skipping stage 'validate_app'.
(...)
The 'SKIP_teardown_app' environment variable is not set, so executing stage 'teardown_app'.
(...)
The 'SKIP_teardown_db' environment variable is not set, so executing stage 'teardown_db'.
(...)
PASS
ok terraform-up-and-running 340.02s
Использование стадий тестирования позволяет быстро получать обратную связь от автоматических тестов, что существенно ускоряет и повышает качество итеративной разработки. Это не повлияет на скорость выполнения тестов в среде CI, однако воздействие на среду разработки будет огромным.
Повторение попыток
Начав регулярно выполнять автоматические тесты для своего инфраструктурного кода, вы столкнетесь с их непредсказуемостью. Иногда они будут проваливаться по причинам временного характера: например, сервер EC2 может не запуститься, в Terraform может проявиться ошибка отложенной согласованности или вам не удастся установить TLS-соединение с S3. Мир инфраструктуры полон беспорядка, поэтому вы должны быть готовы к периодическим сбоям в своих тестах, обрабатывая их соответствующим образом.
Чтобы сделать тесты чуть более устойчивыми, для известных ошибок можно предусмотреть повторение попыток. Например, в процессе написания этой книги я время от времени получаю такого рода ошибку (особенно при параллельном запуске множества тестов):
* error loading the remote state: RequestError: send request failed
Post https://xxx.amazonaws.com/: dial tcp xx.xx.xx.xx:443:
connect: connection refused
Чтобы ваши тесты лучше справлялись с подобным, вы можете включить в Terratest повторные попытки, используя аргументы MaxRetries, TimeBetweenRetries и RetryableTerraformErrors метода terraform.Options:
func createHelloOpts(
dbOpts *terraform.Options,
terraformDir string) *terraform.Options {
return &terraform.Options{
TerraformDir: terraformDir,
Vars: map[string]interface{}{
"db_remote_state_bucket": dbOpts.BackendConfig["bucket"],
"db_remote_state_key": dbOpts.BackendConfig["key"],
"environment": dbOpts.Vars["db_name"],
},
// Повторяем не более трех раз с интервалом пять секунд
// между попытками, для известных ошибок
MaxRetries: 3,
TimeBetweenRetries: 5 * time.Second,
RetryableTerraformErrors: map[string]string{
"RequestError: send request failed": "Throttling issue?",
},
}
}
Аргументу RetryableTerraformErrors можно передать ассоциативный массив с известными ошибками, которые требуют повторения попытки. В качестве ключей выступают сообщения об ошибках, которые нужно искать в журнальных записях (здесь можно использовать регулярные выражения), а значениями служат дополнительные сведения, которые записываются в журнал, когда Terratest находит одну из ошибок и инициирует повторную попытку. Теперь, когда код теста сталкивается с указанной вами ошибкой, в журнале должно появляться сообщение и по прошествии TimeBetweenRetries ваша команда выполнится еще раз:
$ go test -v -timeout 30m
(...)
Running command terraform with args [apply -input=false -lock=false -auto-approve]
(...)
* error loading the remote state: RequestError: send request failed
Post https://s3.amazonaws.com/: dial tcp 11.22.33.44:443:
connect: connection refused
(...)
'terraform [apply]' failed with the error 'exit status code 1'
but this error was expected and warrants a retry. Further details:
Intermittent error, possibly due to throttling?
(...)
Running command terraform with args [apply -input=false -lock=false -auto-approve]
Сквозные тесты
Разобравшись с модульными и интеграционными тестами, можно приступить к тестированию последнего типа, которым вы можете воспользоваться, — сквозному. Если вернуться к нашему примеру с Ruby, сквозные тесты могут предполагать развертывание веб-сервера вместе с любыми хранилищами данных, которые ему нужны, и проверку его работы из веб-браузера с помощью такого инструмента, как Selenium. Похоже выглядят и сквозные тесты для инфраструктуры Terraform: сначала мы развертываем весь код в среду, которая симулирует промышленные условия, а затем проверяем его с точки зрения конечного пользователя.
Для написания сквозных тестов можно использовать ту же стратегию, что и для интеграционных, — сначала создается несколько десятков стадий для выполнения terraformapply, проводятся некоторые проверки, а затем все очищается с помощью terraformdestroy. Но на практике такой подход применяют редко. Это связано с пирамидой тестирования, которую вы можете видеть на рис. 7.1.
Рис. 7.1. Пирамида тестирования
Суть пирамиды тестирования в том, что в общем случае у вас должно быть много модульных тестов (основание пирамиды), меньше интеграционных (середина пирамиды) и еще меньше сквозных (вершина пирамиды). Это вызвано тем, что при движении вверх по пирамиде возрастают сложность, хрупкость и время выполнения тестов.
Из этого следует ключевой вывод о тестировании № 5: чем меньше модули, тем легче и быстрее их тестировать.
В предыдущих разделах вы уже видели, что даже для тестирования относительно простого модуля hello-world-app требуется довольно много усилий, связанных с пространствами имен, внедрением зависимостей, повторными попытками, обработкой ошибок и разделением на стадии. С развитием инфраструктуры этот процесс только усложняется. Поэтому как можно большую часть тестирования следует выполнять максимально близко к основанию пирамиды, поскольку на этом уровне можно получить самую быструю и надежную обратную связь.
К моменту, когда вы добираетесь до вершины пирамиды, выполнение тестов для развертывания сложной инфраструктуры с нуля теряет всякий смысл. Этому есть две причины.
•Слишком медленно. Развертывание целой инфраструктуры с нуля с последующим ее удалением занимает очень много времени: порядка нескольких часов. Тестовые наборы, которые выполняются так долго, приносят относительно мало пользы просто потому, что обратная связь слишком замедляется. Такие тесты обычно запускаются только на ночь. Это означает, что утром вы получите отчет о проваленном тесте, потратите какое-то время на исследование проблемы, внесете исправление и узнаете о результате только на следующий день. Таким образом, вы можете сделать примерно одно исправление в сутки. В подобного рода ситуациях разработчики часто начинают винить в проваленных тестах кого-то другого, убеждать руководство в том, что код нужно развернуть, несмотря на выявленные проблемы, и, в конце концов, вовсе игнорировать непройденные тесты.
• Высокая хрупкость. Как уже упоминалось в предыдущих разделах, мир инфраструктуры беспорядочен. Чем больше ресурсов вы развертываете, тем выше вероятность возникновения нерегулярных и непредсказуемых проблем. Представьте, к примеру, что у какого-то ресурса (скажем, сервера EC2) есть один шанс из тысячи (0,1 %) выйти из строя из-за нерегулярной ошибки (в мире DevOps этот показатель будет выше). Это означает, что вероятность развертывания тестом этого ресурса без каких-либо нерегулярных ошибок равна 99,9 %. Но что насчет теста, который развертывает два ресурса? Для его успешного прохождения оба ресурса должны быть развернуты без нерегулярных ошибок. Чтобы просчитать эти шансы, вероятности нужно умножить: 99,9 × 99,9 = 99,8 %. В случае с тремя ресурсами шансы равны 99,9 × 99,9 × 99,9 = 99,7 %. С N ресурсами формула выглядит как 99,9 %N.
Теперь рассмотрим разные виды автоматического тестирования. Если ваш модульный тест, предназначенный для одного модуля, развертывает 20 ресурсов, шансы на успех равны 99,9 %20 = 98,0 %. Значит, два теста из 100 провалятся; обычно их можно сделать более надежными, если добавить несколько повторных попыток. Но представьте, что ваш интеграционный тест с тремя модулями развертывает 60 ресурсов. Теперь шансы на успех равны 99,9 %60 = 94,1 %. Опять же, добавив достаточное количество повторных попыток, вы можете сделать такой тест вполне стабильным для того, чтобы он приносил какую-то пользу. Но если у вас есть сквозной тест из 30 модулей или примерно 600 ресурсов? Шансы на успех падают до 99,9 %600 = 54,9 %. Это означает, что почти каждая вторая проверка будет проваливаться по временным причинам!
С некоторыми из этих ошибок можно справиться с помощью повторных попыток, но это быстро превратится в вечную игру в догонялки. Вы добавляете повторную попытку на случай истечения времени ожидания TLS-соединения и тут же сталкиваетесь с простоем в работе APT-репозитория в своем шаблоне для Packer. Вы добавляете повторные попытки для сборки Packer, и тут ваша сборка прерывается из-за ошибки отложенной согласованности в Terraform. Кое-как справившись с этой проблемой, вы видите, что сборка «падает» в результате перебоев в работе GitHub. И, поскольку сквозные тесты выполняются очень долго, на исправления у вас есть лишь 1–2 попытки в день.
В реальности очень немногие компании со сложной инфраструктурой выполняют сквозные тесты, которые развертывают все с нуля. Более распространенная стратегия сквозного тестирования выглядит так.
1. Вы развертываете окружение под названием test, максимально приближенное к промышленным условиям, и делаете это единожды. Оно остается на постоянной основе.