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

  enable_new_user_data = true

}

В промышленной среде можно оставить старую версию скрипта, установив параметру enable_new_user_data в файле live/prod/services/webserver-cluster/ma­in.tf значение false:

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"

  instance_type        = "m4.large"

  min_size             = 2

  max_size             = 10

  enable_autoscaling   = true

  enable_new_user_data = false

  custom_tags = {

    Owner      = "team-foo"

    DeployedBy = "terraform"

  }

}

Применение параметра count и встроенных функций для симуляции выражения if-else сродни грязному трюку, но этот подход работает достаточно хорошо. Как вы можете видеть в приведенном здесь коде, он позволяет скрыть от пользователей излишнюю сложность, чтобы они имели дело с простым и понятным API.


Условная логика с использованием выражений for_each и for

Теперь вы знаете, как применять к ресурсам условную логику с помощью параметра count, и догадываетесь, что похожая стратегия возможна и при использовании выражения for_each. Если передать for_each пустую коллекцию, получится ноль ресурсов и ноль вложенных блоков. Но если коллекция непустая, будет создан один или несколько ресурсов или вложенных блоков. Вопрос только в том, как определить, должна коллекция быть пустой или нет?

В качестве ответа можно объединить выражения for_each и for. Например, вспомните, как модуль webserver-cluster в файле modules/services/webserver-cluster/main.tf устанавливает теги:

dynamic "tag" {

  for_each = var.custom_tags

  content {

    key                 = tag.key

    value               = tag.value

    propagate_at_launch = true

  }

}

Если список var.custom_tags пустой, выражению for_each нечего перебирать, поэтому не будет задано ни одного тега. Иными словами, здесь у нас уже есть какая-то условная логика. Но мы можем пойти дальше и добавить к for_each выражение for:

dynamic "tag" {

  for_each = {

    for key, value in var.custom_tags:

    key => upper(value)

    if key != "Name"

  }

  content {

    key                 = tag.key

    value               = tag.value

    propagate_at_launch = true

  }

}

Вложенное выражение for циклически перебирает var.custom_tags, переводя каждое значение в верхний регистр (например, для однородности), и использует условную логику, чтобы отфильтровать любой параметр key, равный Name, поскольку модуль устанавливает свой собственный тег Name. Фильтрация значений в выражении for позволяет реализовать произвольную условную логику.

Выражение for_each почти всегда более предпочтительно для создания множественных копий ресурса по сравнению с параметром count, однако следует отметить, что с точки зрения условной логики присваивание count значений 0 или 1 обычно оказывается более простым, чем назначение for_each пустой/непустой коллекции. В связи с этим параметр count следует добавлять для условного создания ресурсов, тогда как for_each лучше подходит для любых других видов циклов и условных выражений.


Условные выражения с использованием строковой директивы if

Ранее в этой главе мы указывали строковую директиву для выполнения циклов внутри строк. Теперь рассмотрим еще одну:

%{ if }%{ endif }

CONDITION — это любое выражение, возвращающее булево значение, а TRUEVAL — выражение, которое нужно вывести, если CONDITION равно true. При желании можно также добавить блок else:

%{ if }%{ else }%{ endif }

FALSEVAL — это выражение, которое выводится, если CONDITION равно false. Например:

variable "name" {

  description = "A name to render"

  type        = string

}

output "if_else_directive" {

  value = "Hello, %{ if var.name != "" }${var.name}%{ else }(unnamed)%{ endif }"

}

Если выполнить команду terraformapply, присвоив World переменной name, получится следующее:

$ terraform apply -var name="World"

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

if_else_directive = Hello, World

Если выполнить команду terraformapply, присвоив переменной name пустую строку, результат будет таким:

$ terraform apply -var name=""

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

if_else_directive = Hello, (unnamed)


Развертывание с нулевым временем простоя

Итак, у ваших модулей есть простой и понятный API для развертывания кластера веб-серверов. Теперь возникает важный вопрос: как вы будете обновлять этот кластер? То есть как вы развернете в нем новый образ AMI (Amazon Machine Image) после внесения изменений в свой код? И как это сделать таким образом, чтобы пользователи не ощутили перебои в работе?

Для начала образ AMI нужно сделать доступным в виде входной переменной в файле modules/services/webser-vercluster/variables.tf. В реальных сценариях использования этого было бы достаточно, поскольку код самого веб-сервера находился бы в AMI. Но в наших упрощенных примерах весь код веб-сервера размещен в скрипте пользовательских данных, а в качестве AMI применяется стандартный образ Ubuntu. Переход на другую версию Ubuntu будет не очень хорошей демонстрацией, поэтому, помимо новой входной переменной с AMI, можно также добавить входную переменную для изменения текста, который скрипт пользовательских данных возвращает из своего однострочного HTTP-сервера:

variable "ami" {

  description = "The AMI to run in the cluster"

  default     = "ami-0c55b159cbfafe1f0"

  type        = string

}

variable "server_text" {

  description = "The text the web server should return"

  default     = "Hello, World"

  type        = string

}

Когда мы упражнялись с выражениями if-else, вы создали два скрипта пользовательских данных. Вновь их объединим, чтобы не усложнять нашу задачу. Во-первых, удалите входную переменную enable_new_user_data из файла modules/services/webserver-cluster/variables.tf. Во-вторых, удалите из файла modules/services/webserver-cluster/main.tf ресурс template_file под названием user_data_new. В-третьих, оставаясь в том же файле, обновите другой ресурс template_file с именем user_data, чтобы больше не использовать входную переменную enable_new_user_data, и добавьте в его блок vars новую входную переменную server_text:

data "template_file" "user_data" {

  template = file("${path.module}/user-data.sh")

  vars = {

    server_port = var.server_port

    db_address  = data.terraform_remote_state.db.outputs.address

    db_port     = data.terraform_remote_state.db.outputs.port

    server_text = var.server_text

  }

}

Теперь нужно сделать так, чтобы bash-скрипт modules/services/webserver-cluster/user-data.sh использовал эту переменную server_text в теге

, который он возвращает:

#!/bin/bash

cat > index.html <

${server_text}

DB address: ${db_address}

DB port: ${db_port}

EOF

nohup busybox httpd -f -p ${server_port} &

Наконец, найдите конфигурацию запуска в файле modules/services/webserver-cluster/main.tf, присвойте параметру user_data оставшийся источник template_file (тот, что с именем user_data) и установите новую входную переменную ami параметру image_id:

resource "aws_launch_configuration" "example" {

  image_id        = var.ami

  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

  }

}

Теперь в тестовой среде (live/stage/services/webserver-cluster/main.tf) можно установить новые параметры, ami и server_text, и удалить enable_new_user_data:

module "webserver_cluster" {

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

  ami         = "ami-0c55b159cbfafe1f0"

  server_text = "New server text"

  cluster_name           = "webservers-stage"

  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"

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

  instance_type      = "t2.micro"

  min_size           = 2

  max_size           = 2

  enable_autoscaling = false

}

В этом коде используется тот же AMI-образ Ubuntu, но у server_text теперь новое значение. Если выполнить команду plan, должен получиться такой результат:

Terraform will perform the following actions:

  # module.webserver_cluster.aws_autoscaling_group.ex will be updated in-place

  ~ resource "aws_autoscaling_group" "example" {

        id                   = "webservers-stage-terraform-20190516"

      ~ launch_configuration = "terraform-20190516" -> (known after apply)

        (...)

    }

  # module.webserver_cluster.aws_launch_configuration.ex must be replaced

+/- resource "aws_launch_configuration" "example" {

  ~ id                       = "terraform-20190516" -> (known after apply)

    image_id                 = "ami-0c55b159cbfafe1f0"

    instance_type            = "t2.micro"

  ~ name                     = "terraform-20190516" -> (known after apply)

  ~ user_data                = "bd7c0a6" -> "4919a13" # forces replacement

    (...)

  }

Plan: 1 to add, 1 to change, 1 to destroy.