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

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()),

                },