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

  def do_GET(request, response)

    web_service = WebService.new("http://www.example.org")

    handlers = Handlers.new(web_service)

    status_code, content_type, body = handlers.handle(request.path)

    response.status = status_code

    response['Content-Type'] = content_type

    response.body = body

  end

end

Вы можете создать в коде своего теста mock-объект класса WebService, который позволяет указать фиктивный ответ:

class MockWebService

  def initialize(response)

    @response = response

  end

  def proxy

    @response

  end

end

Теперь вы можете создать экземпляр этого класса MockWebService и внедрить его в класс Handlers в своих модульных тестах:

def test_unit_web_service

  expected_status = 200

  expected_content_type = 'text/html'

  expected_body = 'mock example.org'

  mock_response = [expected_status, expected_content_type, expected_body]

  mock_web_service = MockWebService.new(mock_response)

  handlers = Handlers.new(mock_web_service)

  status_code, content_type, body = handlers.handle("/web-service")

  assert_equal(expected_status, status_code)

  assert_equal(expected_content_type, content_type)

 assert_equal(expected_body, body)

end

Выполните тесты еще раз, чтобы убедиться в том, что они по-прежнему работают:

$ ruby web-server-test.rb

Loaded suite web-server-test

Started

...

Finished in 0.000645 seconds.

--------------------------------------------

4 tests, 12 assertions, 0 failures, 0 errors

100% passed

--------------------------------------------

Превосходно! Этот подход минимизирует внешние зависимости, позволяя писать быстрые и надежные тесты, а также проверять всевозможные крайние случаи. И, поскольку три тестовых случая, которые вы добавили ранее, по-прежнему успешно выполняются, вы можете быть уверены, что ваш рефакторинг ничего не сломал.

Теперь вернемся к Terraform и посмотрим, как внедрение зависимостей будет выглядеть для наших модулей. Начнем с hello-world-app. Сперва следует создать простой в развертывании пример в папке examples (если вы этого еще не сделали):

provider "aws" {

  region = "us-east-2"

  # Разрешаем любую версию провайдера AWS вида 2.x

  version = "~> 2.0"

}

module "hello_world_app" {

  source = "../../../modules/services/hello-world-app"

  server_text = "Hello, World"

  environment = "example"

  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"

  db_remote_state_key    = "examples/terraform.tfstate"

  instance_type      = "t2.micro"

  min_size           = 2

  max_size           = 2

  enable_autoscaling = false

}

Проблема с зависимостями сразу бросается в глаза: hello-world-app предполагает, что вы уже развернули модуль mysql, и требует, чтобы вы предоставили информацию о бакете S3, в котором mysql хранит свое состояние. Для этого предусмотрены аргументы db_remote_state_bucket и db_remote_state_key. Наша цель — создать для hello-world-app модульный тест и по возможности минимизировать число внешних зависимостей, хотя свести его к нулю в Terraform не получится.

Один из первых шагов по минимизации зависимостей состоит в прояснении того, от чего зависит ваш модуль. Для этого все источники данных и ресурсы, представляющие внешние зависимости, можно вынести в отдельный файл dependencies.tf.­ Так будет выглядеть файл modules/services/hello-world-app/dependencies.tf:

data "terraform_remote_state" "db" {

  backend = "s3"

  config = {

    bucket = var.db_remote_state_bucket

    key    = var.db_remote_state_key

    region = "us-east-2"

  }

}

data "aws_vpc" "default" {

  default = true

}

data "aws_subnet_ids" "default" {

  vpc_id = data.aws_vpc.default.id

}

Это помогает пользователям вашего кода понять с первого взгляда, на какие внешние компоненты он полагается. В случае с модулем hello-world-app можно сразу же увидеть, что он зависит от базы данных, VPC и подсетей. Но как внедрить зависимости снаружи модуля, чтобы их можно было заменить во время тестирования? Вы уже знаете ответ: входные переменные.

Для каждой из этих зависимостей в файле modules/services/hello-world-app/variables.tf нужно добавить входную переменную:

variable "vpc_id" {

  description = "The ID of the VPC to deploy into"

  type        = string

  default     = null

}

variable "subnet_ids" {

  description = "The IDs of the subnets to deploy into"

  type        = list(string)

  default     = null

}

variable "mysql_config" {

  description = "The config for the MySQL DB"

  type        = object({

    address = string

    port    = number

  })

  default     = null

}

Теперь у нас есть по одной входной переменной для VPC ID, идентификаторов подсетей и конфигурации MySQL. У каждой есть поле default, поэтому все они опциональные: пользователь может указать для них собственные значения или оставить default. Значение по умолчанию, которое используется для этих переменных, вам еще не встречалось: null. Если бы вместо этого в качестве default было указано пустое значение (например, пустая строка для vpc_id или пустой список для subnet_ids), вы бы не смогли отличить его от такого же значения, сознательно установленного пользователем. В таких случаях может пригодиться значение null, так как оно сигнализирует о том, что переменная не задана и что пользователь полагается на поведение по умолчанию.

Обратите внимание: переменная mysql_config имеет конструктор типа object для создания вложенного типа с ключами address и port. Этот тип специально предназначен для того, чтобы соответствовать выходным типам модуля mysql:

output "address" {

  value       = aws_db_instance.example.address

  description = "Connect to the database at this endpoint"

}

output "port" {

  value       = aws_db_instance.example.port

  description = "The port the database is listening on"

}

Одно из преимуществ этого состоит в том, что по завершении рефакторинга у вас появится возможность совместного использования модулей hello-world-app и mysql следующим образом:

module "hello_world_app" {

  source = "../../../modules/services/hello-world-app"

  server_text            = "Hello, World"

  environment            = "example"

  # Все выходные переменные из модуля mysql передаются напрямую!

  mysql_config = module.mysql

  instance_type      = "t2.micro"

  min_size           = 2

  max_size           = 2

  enable_autoscaling = false

}

module "mysql" {

  source = "../../../modules/data-stores/mysql"

  db_name     = var.db_name

  db_username = var.db_username

  db_password = var.db_password

}

Поскольку типы mysql_config и выходных переменных модуля mysql совпадают, вы можете передавать их напрямую одной строчкой. И если типы когда-нибудь поменяются и перестанут совпадать, Terraform сразу же выдаст вам ошибку, чтобы вы знали, что их нужно обновить. Это пример не простой, а типобезопасной композиции функций.

Но, чтобы это заработало, нужно закончить рефакторинг кода. Поскольку конфигурацию MySQL можно передавать в качестве ввода, значит, переменные db_remote_state_bucket и db_remote_state_key должны теперь быть опциональными, поэтому присвойте им значение null по умолчанию:

variable "db_remote_state_bucket" {

  description = "The name of the S3 bucket for the DB's Terraform state"

  type        = string

  default     = null

}

variable "db_remote_state_key" {

  description = "The path in the S3 bucket for the DB's Terraform state"

  type        = string

  default     = null

}

Дальше используйте параметр count, чтобы опционально создать в файле modules/services/hello-world-app/dependencies.tf три источника данных в зависимости от того, установлено ли значение null соответствующей входной переменной:

data "terraform_remote_state" "db" {

  count = var.mysql_config == null ? 1 : 0

  backend = "s3"

  config = {

    bucket = var.db_remote_state_bucket

    key    = var.db_remote_state_key

    region = "us-east-2"

  }

}

data "aws_vpc" "default" {

  count   = var.vpc_id == null ? 1 : 0

  default = true

}

data "aws_subnet_ids" "default" {

  count  = var.subnet_ids == null ? 1 : 0

  vpc_id = data.aws_vpc.default.id

}

Теперь нужно обновить все ссылки на эти источники данных так, чтобы пользователь мог выбирать между источником и входной переменной. Оформим это в виде локальных значений:

locals {

  mysql_config = (

    var.mysql_config == null

      ? data.terraform_remote_state.db[0].outputs

      : var.mysql_config

  )

  vpc_id = (

    var.vpc_id == null

      ? data.aws_vpc.default[0].id

      : var.vpc_id

  )

  subnet_ids = (

    var.subnet_ids == null

      ? data.aws_subnet_ids.default[0].ids

      : var.subnet_ids

  )

}

Заметьте, что источники данных теперь являются массивами, так как они используют параметр count, поэтому каждый раз при ссылке на них нужно применять синтаксис поиска по массиву (например, [0]). Пройдитесь по коду и вместо ссылок на эти источники подставьте ссылки на соответствующие локальные значения. Сначала сделайте так, чтобы источник aws_subnet_ids применял local.vpc_id:

data "aws_subnet_ids" "default" {

  count = var.subnet_ids == null ? 1 : 0

  vpc_id = local.vpc_id

}

Затем присвойте local.subnet_ids параметрам subnet_ids в модулях asg и alb: