module "asg" {
source = "../../cluster/asg-rolling-deploy"
cluster_name = "hello-world-${var.environment}"
ami = var.ami
user_data = data.template_file.user_data.rendered
instance_type = var.instance_type
min_size = var.min_size
max_size = var.max_size
enable_autoscaling = var.enable_autoscaling
subnet_ids = local.subnet_ids
target_group_arns = [aws_lb_target_group.asg.arn]
health_check_type = "ELB"
custom_tags = var.custom_tags
}
module "alb" {
source = "../../networking/alb"
alb_name = "hello-world-${var.environment}"
subnet_ids = local.subnet_ids
}
Обновите переменные db_address и db_port внутри user_data так, чтобы они использовали local.mysql_config:
data "template_file" "user_data" {
template = file("${path.module}/user-data.sh")
vars = {
server_port = var.server_port
db_address = local.mysql_config.address
db_port = local.mysql_config.port
server_text = var.server_text
}
}
И присвойте параметру vpc_id ресурса aws_lb_target_group значение local.vpc_id:
resource "aws_lb_target_group" "asg" {
name = "hello-world-${var.environment}"
port = var.server_port
protocol = "HTTP"
vpc_id = local.vpc_id
health_check {
path = "/"
protocol = "HTTP"
matcher = "200"
interval = 15
timeout = 3
healthy_threshold = 2
unhealthy_threshold = 2
}
}
Благодаря этим изменениям вы теперь можете при желании внедрить VPC ID, идентификаторы подсетей и/или конфигурационные параметры MySQL в модуль hello-world-app. Но, если их опустить, модуль возьмет подходящий источник данных и сам извлечет эти значения. Обновим пример Hello World, позволив внедрять конфигурацию MySQL, но при этом опустим VPC ID и идентификаторы подсетей, так как VPC по умолчанию вполне подходит для тестирования. Добавьте в файл examples/hello-world-app/variables.tf новую входную переменную:
variable "mysql_config" {
description = "The config for the MySQL DB"
type = object({
address = string
port = number
})
default = {
address = "mock-mysql-address"
port = 12345
}
}
Передайте эту переменную модулю hello-world-app в файле examples/hello-world-app/main.tf:
module "hello_world_app" {
source = "../../../modules/services/hello-world-app"
server_text = "Hello, World"
environment = "example"
mysql_config = var.mysql_config
instance_type = "t2.micro"
min_size = 2
max_size = 2
enable_autoscaling = false
}
Теперь в модульном тесте переменной mysql_config можно присвоить любое значение на ваш выбор. Создайте модульный тест test/hello_world_app_example_test.go следующего содержания:
func TestHelloWorldAppExample(t *testing.T) {
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь вел к папке
// с примерами для hello-world-app!
TerraformDir: "../examples/hello-world-app/standalone",
}
// Очищаем все ресурсы в конце теста
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
albDnsName := terraform.OutputRequired(t, opts, "alb_dns_name")
url := fmt.Sprintf("http://%s", albDnsName)
expectedStatus := 200
expectedBody := "Hello, World"
maxRetries := 10
timeBetweenRetries := 10 * time.Second
http_helper.HttpGetWithRetry(
t,
url,
&tls.Config{},
expectedStatus,
expectedBody,
maxRetries,
timeBetweenRetries,
)
}
Этот модульный тест почти идентичен тому, который мы создали для примера с alb. Единственное отличие — параметр TerraformDir указывает на пример hello-world-app (не забудьте обновить путь в соответствии со своей файловой системой), а ожидаемый ответ от ALB имеет код состояния 200OK и тело HelloWorld. В этот тест нужно добавить всего один новый элемент — переменную mysql_config:
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь вел к папке
// с примерами для hello-world-app!
TerraformDir: "../examples/hello-world-app/standalone",
Vars: map[string]interface{}{
"mysql_config": map[string]interface{}{
"address": "mock-value-for-test",
"port": 3306,
},
},
}
Параметр Vars в terraform.Options позволяет устанавливать переменные в коде Terraform. Этот код передает некоторые фиктивные данные для переменной mysql_config. Вы можете использовать для этого любое значение, например IP-адрес небольшой базы данных, полностью размещенной в оперативной памяти, которая запускается на время тестирования.
Выполните команду gotest с аргументом -run, чтобы запустить только этот новый тест (по умолчанию Go запускает все тесты в текущей папке, включая тот, который вы создали для примера с ALB):
$ go test -v -timeout 30m -run TestHelloWorldAppExample
(...)
PASS
ok terraform-up-and-running 204.113s
Если все пройдет хорошо, тест выполнит terraformapply и начнет слать HTTP-запросы к балансировщику нагрузки. Как только придет ответ, он выполнит команду terraformdestroy, чтобы очистить все ресурсы. В общей сложности все это должно занять всего пару минут. Теперь у вас есть неплохой модульный тест для приложения Hello, World.
Параллельное выполнение тестов
В предыдущем пункте вы запускали тесты по одному, используя команду gotest с аргументом -run. Если опустить последний, Go выполнит все ваши тесты — по очереди. Четыре-пять минут на один тест — не так уж плохо для проверки инфраструктурного кода, но, если таких тестов десятки и все они запускаются последовательно, это может занять несколько часов. Чтобы ускорить обратную связь, тесты по возможности лучше распараллеливать.
Для параллельного выполнения в начало каждого теста нужно добавить t.Parallel(). Вот как это выглядит на примере test/hello_world_app_example_test.go:
func TestHelloWorldAppExample(t *testing.T) {
t.Parallel()
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь вел
// к папке с примерами для hello-world-app!
TerraformDir: "../examples/hello-world-app/standalone",
Vars: map[string]interface{}{
"mysql_config": map[string]interface{}{
"address": "mock-value-for-test",
"port": 3306,
},
},
}
// (...)
}
И аналогично в файле test/alb_example_test.go:
func TestAlbExample(t *testing.T) {
t.Parallel()
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь вел
// к папке с примерами для alb!
TerraformDir: "../examples/alb",
}
// (...)
}
Теперь, если выполнить gotest, эти тесты запустятся параллельно. Но есть одна загвоздка: некоторые ресурсы, создаваемые этими тестами (например, ASG, группа безопасности и ALB), имеют одинаковые имена. В результате из-за конфликта имен тесты будут провалены. Но даже без t.Parallel(), если одни и те же тесты выполняются разными членами команды или запускаются внутри среды CI, подобные конфликты неизбежны.
Из этого следует ключевой вывод о тестировании № 4: все ваши ресурсы должны быть разделены по пространствам имен.
Иными словами, пишите свои модули и примеры таким образом, чтобы имя каждого ресурса можно было при желании сконфигурировать. В случае с примером для alb это означает, что конфигурируемым должно быть имя балансировщика. Добавьте в файл examples/alb/variables.tf новую входную переменную с разумным значением по умолчанию:
variable "alb_name" {
description = "The name of the ALB and all its resources"
type = string
default = "terraform-up-and-running"
}
Затем передайте это значение модулю alb в файле examples/alb/main.tf:
module "alb" {
source = "../../modules/networking/alb"
alb_name = var.alb_name
subnet_ids = data.aws_subnet_ids.default.ids
}
Теперь откройте файл test/alb_example_test.go и присвойте этой переменной уникальное значение:
package test
import (
"fmt"
"crypto/tls"
"github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"testing"
"time"
)
func TestAlbExample(t *testing.T) {
t.Parallel()
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь вел
// к папке с примерами для alb!
TerraformDir: "../examples/alb",
Vars: map[string]interface{}{
"alb_name": fmt.Sprintf("test-%s", random.UniqueId()),
},