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

Чтобы быстро проверить, верно ли сконфигурировано ваше окружение, создайте в своей новой папке файл go_sanity_test.go следующего содержания:

package test

import (

        "fmt"

        "testing"

)

func TestGoIsWorking(t *testing.T)  {

        fmt.Println()

        fmt.Println("If you see this text, it's working!")

        fmt.Println()

}

Выполните этот тест с помощью команды gotest и убедитесь, что она возвращает следующий вывод:

$ go test -v

If you see this text, it's working!

PASS

ok   terraform-up-and-running   0.004s

Флаг -v означает verbose («подробно»). Он делает так, чтобы тест показывал весь журнальный вывод.

Если все работает, можете удалить go_sanity_test.go и приступить к написанию модульного теста для модуля alb. Создайте в папке test файл alb_example_test.go со следующим каркасом своего теста:

package test

import (

        "testing"

)

func TestAlbExample(t *testing.T) {

}

Для начала вы должны указать Terratest, где находится ваш код Terraform. Используйте для этого тип terraform.Options:

package test

import (

        "github.com/gruntwork-io/terratest/modules/terraform"

        "testing"

)

func TestAlbExample(t *testing.T) {

        opts := &terraform.Options{

                // Сделайте так, чтобы этот относительный путь

                // вел к папке с примерами для alb!

                TerraformDir: "../examples/alb",

        }

}

Следует отметить, что для проверки модуля alb вам действительно нужно протестировать код примера в папке examples (обновите относительный путь в TerraformDir, чтобы он вел к нужной папке). Это означает, что демонстрационный код теперь имеет тройное назначение: он служит исполняемой документацией, средством ручного тестирования и инструментом для выполнения автоматических тестов ваших модулей.

Обратите также внимание на новую инструкцию импорта для библиотеки Terratest вверху файла. Чтобы загрузить эту зависимость на ваш компьютер, выполните depensure:

$ dep ensure

Команда depensure просканирует ваш код на Go, найдет все новые инструкции импорта, автоматически загрузит их вместе со всеми зависимостями в папку vendor и пропишет их в файл Gopkg.lock. Если для вас в этом слишком много магии, можете использовать команду depensure-add, чтобы перечислить все нужные вам зависимости вручную:

$ dep ensure -add github.com/gruntwork-io/terratest/modules/terraform

Следующий этап автоматического тестирования — выполнение команд terraforminit и terraformapply, которые развернут ваш код. У Terratest для этого есть вспомогательные средства:

func TestAlbExample(t *testing.T) {

        opts := &terraform.Options{

                // Сделайте так, чтобы этот относительный путь

                // вел к папке с примерами для alb!

                TerraformDir: "../examples/alb",

        }

        terraform.Init(t, opts)

        terraform.Apply(t, opts)

}

Выполнение команд init и apply в Terratest — настолько рутинная операция, что для этого предусмотрен удобный вспомогательный метод, который делает все одной командой:

func TestAlbExample(t *testing.T) {

        opts := &terraform.Options{

                // Сделайте так, чтобы этот относительный путь

                // вел к папке с примерами для alb!

                TerraformDir: "../examples/alb",

        }

        // Развертываем пример

        terraform.InitAndApply(t, opts)

}

Код выше уже представляет собой довольно функциональный модульный тест: он выполняет terraforminit и terraformapply и проваливает тест, если эти команды­ не завершаются успешно (например, из-за проблем в вашем коде Terraform). Но вы можете пойти еще дальше и выполнить HTTP-запросы к развернутому балансировщику нагрузки, чтобы убедиться, что он возвращает нужные вам данные. Для этого вам надо как-то получить доменное имя развернутого балансировщика. К счастью, пример alb возвращает его в виде выходной переменной:

output "alb_dns_name" {

  value       = module.alb.alb_dns_name

  description = "The domain name of the load balancer"

}

У Terratest есть встроенные вспомогательные средства для чтения вывода из кода Terraform:

func TestAlbExample(t *testing.T) {

        opts := &terraform.Options{

                // Сделайте так, чтобы этот относительный путь

                // вел к папке с примерами для alb!

                TerraformDir: "../examples/alb",

        }

        // Развертываем пример

        terraform.InitAndApply(t, opts)

        // Получаем URL-адрес ALB

        albDnsName := terraform.OutputRequired(t, opts, "alb_dns_name")

        url := fmt.Sprintf("http://%s", albDnsName)

}

Функция OutputRequired возвращает вывод с заданным именем или проваливает тест, если этот вывод пустой или не существует. Предыдущий листинг формирует на основе этого вывода URL-адрес, используя встроенную в Go функцию ­fmt.Sprintf (не забудьте импортировать пакет fmt). Следующим шагом будет выполнение HTTP-запросов по этому URL-адресу:

package test

import (

        "fmt"

        "crypto/tls"

        "github.com/gruntwork-io/terratest/modules/http-helper"

        "github.com/gruntwork-io/terratest/modules/terraform"

        "testing"

)

func TestAlbExample(t *testing.T) {

        opts := &terraform.Options{

                // Сделайте так, чтобы этот относительный путь

                // вел к папке с примерами для alb!

                TerraformDir: "../examples/alb",

        }

        // Развертываем пример

        terraform.InitAndApply(t, opts)

        // Получаем URL-адрес ALB

        albDnsName := terraform.OutputRequired(t, opts, "alb_dns_name")

        url := fmt.Sprintf("http://%s", albDnsName)

        // Проверяем в ALB действие по умолчанию, которое должно вернуть 404

        expectedStatus := 404

        expectedBody := "404: page not found"

http_helper.HttpGetWithValidation(t, url, expectedStatus, expectedBody)

}

Этот код импортирует из Terratest новый пакет, http_helper. Чтобы его загрузить, нужно еще раз выполнить rundep. Метод http_helper.HttpGetWithValidation сделает HTTP-запрос типа GET по переданному вами URL-адресу и провалит тест, если код состояния и тело ответа не совпадают с теми, которые вы указали.

У этого кода есть одна проблема: между завершением команды terraformapply и тем, когда доменное имя балансировщика нагрузки становится доступным (то есть распространилось по системе), проходит время. Если вызвать http_helper.HttpGetWithValidation незамедлительно, он, вполне вероятно, окажется неудачным, хотя через 30 секунд или минуту ALB заработает в нормальном режиме. Как уже обсуждалось в подразделе «Отложенная согласованность согласуется… с отлагательством» на с. 211, такого рода асинхронное поведение с отложенной согласованностью является для AWS (а точнее, для большинства распределенных систем) нормальным и решением будет повторное выполнение вызовов. У Terratest есть вспомогательный метод и для этого:

func TestAlbExample(t *testing.T) {

        opts := &terraform.Options{

                // Сделайте так, чтобы этот относительный путь

                // вел к папке с примерами для alb!

                TerraformDir: "../examples/alb",

        }

        // Развертываем пример

        terraform.InitAndApply(t, opts)

        // Получаем URL-адрес ALB

        albDnsName := terraform.OutputRequired(t, opts, "alb_dns_name")

        url := fmt.Sprintf("http://%s", albDnsName)

        // Проверяем в ALB действие по умолчанию, которое должно вернуть 404

        expectedStatus := 404

        expectedBody := "404: page not found"

        maxRetries := 10

        timeBetweenRetries := 10 * time.Second

http_helper.HttpGetWithRetry(

                t,

                url,

&tls.Config{},

                expectedStatus,

                expectedBody,

                maxRetries,

                timeBetweenRetries,

        )

}

Метод http_helper.HttpGetWithRetry почти не отличается от метода http_helper.HttpGetWithValidation, только при отсутствии ответа с нужными кодом состояния или телом он повторит попытку. Максимальное количество повторений (десять) и длину интервалов между ними (десять секунд) можно настроить. Если в какой-то момент он получит ожидаемый ответ, тест будет пройден; если после истечения максимального количества попыток ожидаемый ответ так и не пришел, тест считается проваленным.

В конце теста нужно выполнить команду terraformdestroy, чтобы очистить ресурсы. Для этого у Terratest есть вспомогательный метод: terraform.Destroy. Но, если вызывать его в самом конце, из-за любой программной ошибки выше по коду (например, HttpGetWithRetry даст сбой из-за неправильной конфигурации ALB) тест может завершиться, не доходя до него, в результате чего развернутая инфраструктура не будет удалена.

Таким образом, вам нужно убедиться в том, что terraform.Destroy выполняется всегда, даже если тест проваливается. Во многих языках программирования для этого предусмотрены конструкции try/finally или try/ensure. Но в Go это делается с помощью выражения defer, которое гарантирует, что переданный в него код будет выполнен при завершении окружающей его функции (каким бы оно ни было):

func TestAlbExample(t *testing.T) {

        opts := &terraform.Options{

                // Сделайте так, чтобы этот относительный путь

                // вел к папке с примерами для alb!