Terraform: инфраструктура на уровне кода — страница 53 из 65

5. Выполнить terraformdestroy для модуля mysql.

Если вы запускаете эти тесты в среде CI, нужно выполнить каждую стадию, от начала до конца. Но если вы используете их в локальной среде для разработки и вместе с этим итеративно вносите изменения в свой код, все стадии необязательны. Например, если вы редактируете только модуль hello-world-app, повторный запуск всего теста после каждой фиксации влечет за собой развертывание и удаление модуля mysql, хотя ваши изменения его никак не касаются. Это добавляет к времени работы теста 5–10 минут без какой-либо необходимости.

В идеале рабочий процесс должен выглядеть определенным образом.

1. Запустить terraformapply для модуля mysql.

2. Запустить terraformapply для модуля hello-world-app.

3. Переход к итеративной разработке:

а) внести изменение в модуль hello-world-app;

б) повторно выполнить terraformapply для модуля hello-world-app, чтобы развернуть ваши обновления;

в) проверить и убедиться в том, что все работает;

г) если все работает, перейти к следующему шагу, если нет — вернуться к шагу 3«а».

4. Выполнить terraformdestroy для модуля hello-world-app.

5. Выполнить terraformdestroy для модуля mysql.

Возможность быстро выполнить внутренний цикл в пункте 3 — ключ к быстрой итеративной разработке с использованием Terraform. Для этого нужно разбить код своего теста на стадии, после чего вы сможете выбирать, какие из них выполнять, а какие можно пропустить.

Terratest имеет встроенную поддержку этой стратегии в виде пакета test_structure (чтобы его добавить, не забудьте выполнить depensure). Суть в том, что каждая стадия вашего теста заворачивается в функцию с именем; затем вы сможете заставить Terratest пропустить некоторые из этих имен, используя переменные среды. Каждая стадия проверки сохраняет тестовые данные на диск, чтобы их можно было прочитать во время последующих выполнений. Попробуем это на примере test/hello_world_integration_test.go. Сначала набросаем каркас теста, а затем наполним его внутренние методы:

func TestHelloWorldAppStageWithStages(t *testing.T) {

        t.Parallel()

        // Сохраняем функцию в переменную с коротким именем,

        // просто чтобы примеры с кодом было легче уместить

        // на страницах этой книги.

        stage := test_structure.RunTestStage

        // Развертываем БД MySQL

        defer stage(t, "teardown_db", func() { teardownDb(t, dbDirStage) })

        stage(t, "deploy_db", func() { deployDb(t, dbDirStage) })

        // Развертываем приложение hello-world-app

        defer stage(t, "teardown_app", func() { teardownApp(t, appDirStage) })

        stage(t, "deploy_app", func() { deployApp(t, dbDirStage, appDirStage) })

        // Проверяем, работает ли hello-world-app

        stage(t, "validate_app", func() { validateApp(t, appDirStage) })

}

Структура та же, что и прежде: развернуть mysql и hello-world-app, проверить приложение, удалить hello-world-app (выполняется в конце благодаря defer) и mysql (выполняется в конце с помощью defer). Разница лишь в том, что теперь каждая стадия обернута в test_structure.RunTestStage. Метод RunTestStage принимает три аргумента.

•t. Первым аргументом выступает значение t, которое Go передает всем автоматическим тестам. С его помощью можно управлять состоянием теста. Например, вы можете его провалить, вызвав t.Fail().

• Имя стадии. Второй аргумент позволяет задать имя этой стадии тестирования. Вскоре вы увидите пример того, как с помощью этого имени можно пропускать отдельные стадии.

Код для выполнения. Третий аргумент — это код, который нужно выполнить на данной стадии тестирования. Это может быть любая функция.

Теперь реализуем функции для каждой стадии тестирования. Начнем с deployDb:

func deployDb(t *testing.T, dbDir string) {

        dbOpts := createDbOpts(t, dbDir)

        // Сохраняем данные на диск, чтобы в процессе других стадий теста,

        // запущенных позже, можно было их прочитать

        test_structure.SaveTerraformOptions(t, dbDir, dbOpts)

        terraform.InitAndApply(t, dbOpts)

}

Как и прежде, чтобы развернуть mysql, код вызывает createDbOpts и terra­form.InitAndApply. Единственное изменение лишь в том, что теперь между этими двумя шагами находится вызов test_structure.SaveTerraformOptions, который записывает содержимое dbOpts на диск, чтобы позже его могли прочитать другие стадии тестирования. Например, вот реализация функции teardownDb:

func teardownDb(t *testing.T, dbDir string) {

        dbOpts := test_structure.LoadTerraformOptions(t, dbDir)

        defer terraform.Destroy(t, dbOpts)

}

Эта функция вызывает test_structure.LoadTerraformOptions, чтобы загрузить с диска содержимое dbOpts, которое было записано ранее функцией deployDb. Причина, по которой эти данные передаются через диск, а не в оперативной памяти, связана с тем, что каждая стадия может запускаться самостоятельно — то есть в отдельном процессе. Как вы увидите позже в этой главе, при первых нескольких запусках gotest имеет смысл выполнить deployDb, но пропустить teardownDb, а затем, при последующих запусках, сделать наоборот. Чтобы во время всех этих запусков использовалась одна и та же база данных, информацию о ней следует хранить на диске.

Теперь реализуем функцию deployHelloApp:

func deployApp(t *testing.T, dbDir string, helloAppDir string) {

        dbOpts := test_structure.LoadTerraformOptions(t, dbDir)

        helloOpts := createHelloOpts(dbOpts, helloAppDir)

        // Сохраняем данные на диск, чтобы в процессе других стадий теста,

        // запущенных позже, можно было их прочитать

        test_structure.SaveTerraformOptions(t, helloAppDir, helloOpts)

        terraform.InitAndApply(t, helloOpts)

}

Этот код повторно использует ранее определенную функцию createHelloOpts и вызывает для нее terraform.InitAndApply. И снова все поведение заключается в запуске методов test_structure.SaveTerraformOptions и test_structure.SaveTerraformOptions для загрузки с диска dbOpts и сохранения на диск helloOpts соответственно. Вы уже догадываетесь, как будет выглядеть реализация метода teardownApp:

func teardownApp(t *testing.T, helloAppDir string) {

        helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir)

        defer terraform.Destroy(t, helloOpts)

}

А вот реализация метода validateApp:

func validateApp(t *testing.T, helloAppDir string) {

        helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir)

        validateHelloApp(t, helloOpts)

}

Таким образом, данный код идентичен оригинальному интеграционному тесту, только теперь каждая стадия завернута в вызов test_structure.RunTestStage, и еще вам нужно приложить немного усилий для сохранения и чтения данных с диска. Эти простые изменения открывают перед вами важную возможность: заставить Terratest пропустить любую стадию с именем foo, установив переменную среды SKIP_foo=true. Разберем типичный процесс написания кода, чтобы увидеть, как это работает.

Для начала нужно запустить тест, пропустив при этом обе стадии очистки, чтобы по окончании тестирования модули mysql и hello-world-app оставались развернутыми. Поскольку эти стадии называются teardown_db и teardown_app, нужно установить переменные среды SKIP_teardown_db и SKIP_teardown_app соответственно. Так Terratest будет знать, что их нужно пропустить:

$ SKIP_teardown_db=true \

  SKIP_teardown_app=true \

  go test -timeout 30m -run 'TestHelloWorldAppStageWithStages'

(...)

The 'SKIP_deploy_db' environment variable is not set, so executing stage 'deploy_db'.

(...)

The 'deploy_app' environment variable is not set, so executing stage 'deploy_db'.

(...)

The 'validate_app' environment variable is not set,

so executing stage 'deploy_db'.

(...)

The 'teardown_app' environment variable is set, so skipping stage 'deploy_db'.

(...)

The 'teardown_db' environment variable is set, so skipping stage 'deploy_db'.

(...)

PASS

ok  terraform-up-and-running  423.650s

Теперь вы можете приступить к последовательному редактированию модуля hello-world-app, повторно запуская тесты при каждом изменении. Но на этот раз сделайте так, чтобы, помимо очистки, пропускалась также стадия развертывания модуля mysql (так как mysql по-прежнему выполняется). Таким образом, будет выполнена только команда deployapp и проведена проверка модуля hello-world-app:

$ SKIP_teardown_db=true \

  SKIP_teardown_app=true \

  SKIP_deploy_db=true \

  go test -timeout 30m -run 'TestHelloWorldAppStageWithStages'

(...)

The 'SKIP_deploy_db' environment variable is set, so skipping stage 'deploy_db'.

(...)

The 'deploy_app' environment variable is not set, so executing stage 'deploy_db'.

(...)

The 'validate_app' environment variable is not set, so executing stage 'deploy_db'.

(...)

The 'teardown_app' environment variable is set, so skipping stage 'deploy_db'.

(...)

The 'teardown_db' environment variable is set, so skipping stage 'deploy_db'.

(...)

PASS

ok  terraform-up-and-running  13.824s

Обратите внимание на то, как быстро теперь работает каждый из этих тестов: вместо 10–15 минут каждое новое изменение занимает 10–60 секунд (в зависимости от того, что поменялось). Учитывая, что в процессе разработки эти стадии, скорее всего, будут выполняться десятки или даже сотни раз, вы можете сэкономить уйму времени.

Когда после всех изменений модуль hello-world-app начнет работать так, как вы того ожидали, самое время очистить все ресурсы. Запустите тесты еще раз, но теперь пропустите стадии развертывания и проверки, чтобы выполнялась только очистка: