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

В этой главе я покажу, как создавать и применять модули Terraform. Мы рассмотрим такие темы.

• Основные характеристики модулей.

• Входные параметры модулей.

• Локальные переменные модулей.

• Выходные переменные модулей.

• Подводные камни.

• Управление версиями.


Примеры кода

Напоминаю: все примеры кода для этой книги можно найти по адресу github.com/brikis98/terraform-up-and-running-code.


Что такое модуль

В Terraform любой набор конфигурационных файлов, размещенных в одной папке, считается модулем. Вся конфигурация, которую вы уже написали, формально состоит из модулей, хоть и не очень интересных, так как вы развертывали их напрямую (модуль в текущей рабочей папке называется корневым). Чтобы увидеть всю их мощь, нужно использовать один модуль из другого.

В качестве примера превратим в универсальный модуль код из stage/services/webserver-cluster, который включает в себя Auto Scaling Group (ASG), Application Load Balancer (ALB), группы безопасности и многие другие ресурсы.

Для начала выполните terraformdestroy в stage/services/webserver-cluster, чтобы удалить все ресурсы, созданные вами ранее. Затем создайте новую папку верхнего уровня под названием modules и переместите все файлы из stage/services/webserver-cluster в modules/services/webserver-cluster. В итоге ваша структура папок должна выглядеть так, как на рис. 4.4.

Рис. 4.4. Структура папок с модулем и тестовой средой

Откройте файл main.tf в modules/services/webserver-cluster и уберите из него определение provider. Провайдеры должны настраиваться не самим модулем, а его пользователями.

Теперь можно воспользоваться этим модулем в тестовой среде, используя следующий синтаксис:

module "" {

  source = ""

  [CONFIG ...]

}

NAME — это идентификатор, который можно использовать в коде Terraform для обращения к модулю (вроде web-service), SOURCE — путь к коду модуля (скажем, modules/services/webserver-cluster), а CONFIG состоит из одного/нескольких аргументов, предназначенных специально для этого модуля. Например, вы можете создать новый файл stage/services/webserver-cluster/main.tf и использовать в нем модуль webserver-cluster следующим образом:

provider "aws" {

  region = "us-east-2"

}

module "webserver_cluster" {

  source = "../../../modules/services/

    webserver-cluster"

}

Затем вы можете повторно использовать тот же модуль в промышленной среде, создав новый файл prod/services/webserver-cluster/main.tf следующего содержания:

provider "aws" {

  region = "us-east-2"

}

module "webserver_cluster" {

  source = "../../../modules/services/webserver-cluster"

}

Вот и все: повторное использование кода в разных окружениях с минимальным дублированием. Обратите внимание, что при добавлении модуля в конфигурацию Terraform или изменении параметра модуля source необходимо сначала выполнить команду init, а только потом plan или apply:

$ terraform init

Initializing modules...

- webserver_cluster in ../../../modules/services/webserver-cluster

Initializing the backend...

Initializing provider plugins...

Terraform has been successfully initialized!

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

Прежде чем применять этот код, нужно упомянуть об одном недостатке модуля webserver-cluster: все имена в нем прописаны вручную. Это касается групп безопасности, ALB и других ресурсов. Таким образом, при попытке повторного использования этого модуля вы получите конфликты имен. Прямо в коде прописаны даже параметры для обращения к базе данных, поскольку файл main.tf, который вы скопировали в modules/services/webserver-cluster, берет адрес и порт БД из источника данных terraform_remote_state, а тот написан лишь с расчетом на тестовую среду.

Чтобы исправить эти проблемы, необходимо добавить в модуль webserver-cluster конфигурируемые входные параметры. Это позволит ему менять свое поведение в зависимости от окружения.


Входные параметры модуля

В языке программирования общего назначения, таком как Ruby, функцию можно сделать конфигурируемой, передав ей входные параметры:

def example_function(param1, param2)

  puts "Hello, #{param1} #{param2}"

end

# Другие участки вашего кода

example_function("foo", "bar")

Модули Terraform тоже могут иметь параметры. Для их определения используется уже знакомый вам механизм: входные переменные. Откройте файл modules/services/webserver-cluster/variables.tf и добавьте три новых блока variable:

variable "cluster_name" {

  description = "The name to use for all the cluster resources"

  type        = string

}

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

}

Далее пройдитесь по файлу modules/services/webserver-cluster/main.tf и подставьте var.cluster_name прописанных вручную имен (скажем, "terraform-asgexample"). Например, вот как это сделать в группе безопасности ALB:

resource "aws_security_group" "alb" {

  name = "${var.cluster_name}-alb"

  ingress {

    from_port   = 80

    to_port     = 80

    protocol    = "tcp"

    cidr_blocks = ["0.0.0.0/0"]

  }

  egress {

    from_port   = 0

    to_port     = 0

    protocol    = "-1"

    cidr_blocks = ["0.0.0.0/0"]

  }

}

Обратите внимание на то, что параметру name присваивается "${var.cluster_name}-alb". Аналогичные изменения нужно внести и в другой ресурс aws_security_group (можете назвать его "${var.cluster_name}-instance"), а также в aws_alb и раздел tag ресурса aws_autoscaling_group.

Вы также должны обновить источник данных terraform_remote_state, чтобы он использовал db_remote_state_bucket и db_remote_state_key в качестве параметров bucket и соответственно key. Это позволит ему считывать файл состояния из правильной среды:

data "terraform_remote_state" "db" {

  backend = "s3"

  config = {

    bucket = var.db_remote_state_bucket

    key    = var.db_remote_state_key

    region = "us-east-2"

  }

}

Теперь можете аналогичным образом задать эти новые входные переменные в тестовой среде в файле stage/services/webserver-cluster/main.tf:

module "webserver_cluster" {

  source = "../../../modules/services/webserver-cluster"

  cluster_name           = "webservers-stage"

  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"

  db_remote_state_key    = "stage/data-stores/mysql/terraform.tfstate"

}

То же самое нужно сделать для промышленной среды в файле prod/services/webserver-cluster/main.tf:

module "webserver_cluster" {

  source = "../../../modules/services/webserver-cluster"

  cluster_name           = "webservers-prod"

  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"

  db_remote_state_key    = "prod/data-stores/mysql/terraform.tfstate"

}

Промышленная база данных еще не существует. В качестве упражнения попробуйте добавить ее самостоятельно по аналогии с тестовой.

Как видите, для установки входных переменных модуля и аргументов ресурса используется один и тот же синтаксис. Входящие переменные являются API модуля и определяют то, как он себя ведет в разных окружениях. В этом примере мы задаем разные имена для разных сред, но вы можете сделать конфигурируемыми и другие параметры. Предположим, чтобы сэкономить деньги, в тестовой среде можно запускать небольшой кластер веб-серверов, но в промышленных условиях вам понадобится большой кластер, способный справиться с сильными нагрузками. Для этого в файл modules/services/webserver-cluster/variables.tf можно добавить еще три входные переменные:

variable "instance_type" {

  description = "The type of EC2 Instances to run (e.g. t2.micro)"

  type        = string

}

variable "min_size" {

  description = "The minimum number of EC2 Instances in the ASG"

  type        = number

}

variable "max_size" {

  description = "The maximum number of EC2 Instances in the ASG"

  type        = number

}

Дальше нужно обновить конфигурацию запуска в файле modules/services/webserver-cluster/main.tf, присвоив параметру instance_type новую входную переменную var.instance_type:

resource "aws_launch_configuration" "example" {

  image_id        = "ami-0c55b159cbfafe1f0"

  instance_type   = var.instance_type

  security_groups = [aws_security_group.instance.id]

  user_data       = data.template_file.user_data.rendered

  # Требуется при использовании группы автомасштабирования

  # в конфигурации запуска.

  # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html

  lifecycle {

    create_before_destroy = true

  }

}

Похожим образом следует обновить определение ASG в том же файле. Присвойте параметрам min_size и max_size входные переменные var.min_size и var.max_size соответственно:

resource "aws_autoscaling_group" "example" {

  launch_configuration = aws_launch_configuration.example.name

  vpc_zone_identifier  = data.aws_subnet_ids.default.ids

  target_group_arns    = [aws_lb_target_group.asg.arn]

  health_check_type    = "ELB"

  min_size = var.min_size

  max_size = var.max_size

  tag {

    key                 = "Name"

    value               = var.cluster_name

    propagate_at_launch = true

  }

}

Теперь кластер в тестовой среде (stage/services/webserver-cluster/main.tf) можно сделать поменьше и подешевле, указав "t2.micro" для instance_type и 2 для min_size и max_size: