db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
}
Теперь создадим в файле test/hello_world_integration_test.go каркас интеграционного теста, а детали реализации оставим на потом:
// Подставьте сюда подходящие пути к вашим модулям
const dbDirStage = "../live/stage/data-stores/mysql"
const appDirStage = "../live/stage/services/hello-world-app"
func TestHelloWorldAppStage(t *testing.T) {
t.Parallel()
// Развертываем БД MySQL
dbOpts := createDbOpts(t, dbDirStage)
defer terraform.Destroy(t, dbOpts)
terraform.InitAndApply(t, dbOpts)
// Развертываем hello-world-app
helloOpts := createHelloOpts(dbOpts, appDirStage)
defer terraform.Destroy(t, helloOpts)
terraform.InitAndApply(t, helloOpts)
// Проверяем, работает ли hello-world-app
validateHelloApp(t, helloOpts)
}
У теста следующая структура: развернуть mysql и hello-world-app, проверить приложение, удалить hello-world-app (выполняется в конце благодаря defer) и в завершение удалить mysql (выполняется в конце благодаря defer). Методы createDbOpts, createHelloOpts и validateHelloApp пока не существуют, поэтому реализуем их по очереди. Начнем с метода createDbOpts:
func createDbOpts(t *testing.T, terraformDir string) *terraform.Options {
uniqueId := random.UniqueId()
return &terraform.Options{
TerraformDir: terraformDir,
Vars: map[string]interface{}{
"db_name": fmt.Sprintf("test%s", uniqueId),
"db_password": "password",
},
}
}
Пока ничего нового: код передает методу terraform.Options заданную папку и устанавливает переменные db_name и db_password.
Дальше нужно разобраться с тем, где модуль mysql будет хранить свое состояние. До сих пор конфигурация backend содержала значения, прописанные вручную:
terraform {
backend "s3" {
# Поменяйте на имя своего бакета!
bucket = "terraform-up-and-running-state"
key = "stage/data-stores/mysql/terraform.tfstate"
region = "us-east-2"
# Замените именем своей таблицы DynamoDB!
dynamodb_table = "terraform-up-and-running-locks"
encrypt = true
}
}
Эти значения создают большую проблему, потому что, если оставить их как есть, будет перезаписан реальный файл состояния среды финального тестирования! Одним из вариантов — использование рабочих областей (как обсуждалось в подразделе «Изоляция через рабочие области» на с. 114), но для этого все равно нужен доступ к бакету S3, принадлежащему учетной записи среду финального тестирования, тогда как все ваши тесты должны выполняться от имени совершенно отдельного пользователя AWS. Вместо этого лучше использовать частичную конфигурацию, как было описано в разделе «Ограничения хранилищ Terraform» на с. 110. Вынесите всю конфигурацию backend во внешний файл, такой как backend.hcl:
bucket = "terraform-up-and-running-state"
key = "stage/data-stores/mysql/terraform.tfstate"
region = "us-east-2"
dynamodb_table = "terraform-up-and-running-locks"
encrypt = true
Таким образом, конфигурация backend в файле live/stage/data-stores/mysql/main.tf остается пустой:
terraform {
backend "s3" {
}
}
При развертывании модуля mysql в настоящей среде финального тестирования нужно указать аргумент -backend-config, чтобы Terraform использовал конфигурацию backend из файла backend.hcl:
$ terraform init -backend-config=backend.hcl
При выполнении тестов для модуля mysql вы можете заставить Terratest передать тестовые значения, используя параметр BackendConfig для terraform.Options:
func createDbOpts(t *testing.T, terraformDir string) *terraform.Options {
uniqueId := random.UniqueId()
bucketForTesting := "YOUR_S3_BUCKET_FOR_TESTING"
bucketRegionForTesting := "YOUR_S3_BUCKET_REGION_FOR_TESTING"
dbStateKey := fmt.Sprintf("%s/%s/terraform.tfstate", t.Name(), uniqueId)
return &terraform.Options{
TerraformDir: terraformDir,
Vars: map[string]interface{}{
"db_name": fmt.Sprintf("test%s", uniqueId),
"db_password": "password",
},
BackendConfig: map[string]interface{}{
"bucket": bucketForTesting,
"region": bucketRegionForTesting,
"key": dbStateKey,
"encrypt": true,
},
}
}
Вы должны указать собственные значения для переменных bucketForTesting и bucketRegionForTesting. В качестве хранилища в тестовой учетной записи AWS можно создать один бакет S3, так как параметр key (путь внутри бакета) содержит идентификатор uniqueId, который должен быть достаточно уникальным, чтобы все тесты имели разные значения.
Далее следует внести некоторые изменения в модуль hello-world-app в среде финального тестирования. Откройте файл live/stage/services/hello-world-app/variables.tf и сделайте доступными переменные db_remote_state_bucket, db_remote_state_key и environment:
variable "db_remote_state_bucket" {
description = "The name of the S3 bucket for the database's remote state"
type = string
}
variable "db_remote_state_key" {
description = "The path for the database's remote state in S3"
type = string
}
variable "environment" {
description = "The name of the environment we're deploying to"
type = string
default = "stage"
}
Передайте эти значения модулю hello-world-app в файле live/stage/services/hello-world-app/main.tf:
module "hello_world_app" {
source = "../../../../modules/services/hello-world-app"
server_text = "Hello, World"
environment = var.environment
db_remote_state_bucket = var.db_remote_state_bucket
db_remote_state_key = var.db_remote_state_key
instance_type = "t2.micro"
min_size = 2
max_size = 2
enable_autoscaling = false
}
Теперь вы можете реализовать метод createHelloOpts:
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"],
},
}
}
Обратите внимание, что переменным db_remote_state_bucket и db_remote_state_key присвоены значения из BackendConfig для модуля mysql. Благодаря этому модуль hello-world-app читает именно то состояние, которое только что было записано модулем mysql. Переменная environment равна db_name, чтобы все ресурсы распределялись по пространствам имен одним и тем же образом.
Наконец-то вы можете реализовать метод validateHelloApp:
func validateHelloApp(t *testing.T, helloOpts *terraform.Options) {
albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name")
url := fmt.Sprintf("http://%s", albDnsName)
maxRetries := 10
timeBetweenRetries := 10 * time.Second
http_helper.HttpGetWithRetryWithCustomValidation(
t,
url,
&tls.Config{},
maxRetries,
timeBetweenRetries,
func(status int, body string) bool {
return status == 200 &&
strings.Contains(body, "Hello, World")
},
)
}
Как и наши модульные тесты, этот метод использует пакет http_helper, только на этот раз мы вызываем из него http_helper.HttpGetWithRetryWithCustomValidation, что позволяет нам указать наши собственные правила проверки кода состояния и тела HTTP-ответа. Это необходимо для проверки наличия в HTTP-ответе строки Hello, World, а не для точного сопоставления строк, так как ответ, который возвращает скрипт пользовательских данных внутри модуля hello-world-app, содержит и другой текст.
Теперь запустите интеграционный тест и проверьте, работает ли он:
$ go test -v -timeout 30m -run "TestHelloWorldAppStage"
(...)
PASS
ok terraform-up-and-running 795.63s
Отлично! Теперь у вас есть интеграционный тест, с помощью которого можно убедиться в корректной совместной работе нескольких ваших модулей. Он получился более сложным, чем модульный тест, и его выполнение длится вдвое дольше (10–15 минут вместо 4–5). Сделать его быстрее не так уж просто — узкое место здесь зависит от того, как долго AWS будет развертывать RDS, ASG, ALB и т. д. Но в определенных обстоятельствах работу теста можно сократить с помощью стадий тестирования.
Стадии тестирования
Если взглянуть на код вашего интеграционного теста, можно заметить, что он состоит из нескольких отдельных стадий.
1. Запустить terraformapply для модуля mysql.
2. Запустить terraformapply для модуля hello-world-app.
3. Выполнить проверку и убедиться в том, что все работает.
4. Выполнить terraformdestroy для модуля hello-world-app.