Читаем Terraform: инфраструктура на уровне кода полностью

  value = <

%{ for name in var.names }

  ${name}

%{ endfor }

EOF

}

Выполнив terraformapply, вы получите следующий вывод:

$ terraform apply

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

Outputs:

for_directive =

  neo

  trinity

  morpheus

Обратите внимание на дополнительные символы перехода на новую строку. В строковой директиве можно указать маркер ~, чтобы удалить все пробельные символы (пробелы и перенос строки) перед ней (если маркер находится в начале директивы) или после нее (если маркер находится в конце директивы):

output "for_directive_strip_marker" {

  value = <

%{~ for name in var.names }

  ${name}

%{~ endfor }

EOF

}

Эта обновленная версия дает такой вывод:

for_directive_strip_marker =

  neo

  trinity

  morpheus

Условные выражения

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

Параметр count для условных ресурсов.

• Выраженияfor_eachиfor для условных ресурсов и их вложенных блоков.

• Строковая директива if для условных выражений внутри строк.

Рассмотрим их все по очереди.

Условные выражения с использованием параметра count

Параметр count, который вы видели ранее, позволяет создавать простые циклы. Но если проявить смекалку, тот же механизм можно использовать и для условных выражений. Мы начнем с рассмотрения конструкций if в пункте «Выражения if с использованием параметра count» ниже, а затем перейдем к выражениям if-else.

Выражения if с использованием параметра count

В главе 4 вы написали модуль Terraform, который можно применить в качестве «чертежа» для развертывания кластеров с веб-серверами. Этот модуль создавал группу автомасштабирования (ASG), балансировщик нагрузки (ALB), группы безопасности и ряд других ресурсов. Чего он не создавал, так это запланированного действия. Поскольку кластер нужно масштабировать только в промышленных условиях, вы определили ресурсы aws_autoscaling_schedule непосредственно в промышленной конфигурации в файле live/prod/services/webserver-cluster/main.tf. Можно ли их определить в модуле webserver-cluster и затем создавать только для определенных пользователей?

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

variable "enable_autoscaling" {

  description = "If set to true, enable auto scaling"

  type        = bool

}

Теперь, если бы вы использовали язык программирования общего назначения, вы бы могли применить эту входную переменную в выражении if:

# Это просто псевдокод. Он не будет работать в Terraform.

if var.enable_autoscaling {

  resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {

    scheduled_action_name  = "${var.cluster_name}-scale-out-during-business-hours"

    min_size               = 2

    max_size               = 10

    desired_capacity       = 10

    recurrence             = "0 9 * * *"

    autoscaling_group_name = aws_autoscaling_group.example.name

  }

  resource "aws_autoscaling_schedule" "scale_in_at_night" {

    scheduled_action_name  = "${var.cluster_name}-scale-in-at-night"

    min_size               = 2

    max_size               = 10

    desired_capacity       = 2

    recurrence             = "0 17 * * *"

    autoscaling_group_name = aws_autoscaling_group.example.name

  }

}

Terraform не поддерживает выражения if, поэтому данный код работать не будет. Но того же результата можно добиться с помощью параметра count и двух особенностей языка.

• Если внутри ресурса параметру count присвоить значение 1, вы получите копию этого ресурса; если указать 0, этот ресурс вообще не будет создан.

• Terraform поддерживает условные выражения в формате ?:. Это тернарный синтаксис, с которым вы можете быть знакомы по другим языкам программирования. Он проверит булеву логику в CONDITION и, если результат равен true, вернет TRUE_VAL; в противном случае возвращается FALSE_VAL.

Объединив эти две идеи, мы можем обновить модуль webserver-cluster следу­ющим образом:

resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {

  count = var.enable_autoscaling ? 1 : 0

  scheduled_action_name  = "${var.cluster_name}-scale-out-during-business-hours"

  min_size               = 2

  max_size               = 10

  desired_capacity       = 10

  recurrence             = "0 9 * * *"

  autoscaling_group_name = aws_autoscaling_group.example.name

}

resource "aws_autoscaling_schedule" "scale_in_at_night" {

  count = var.enable_autoscaling ? 1 : 0

  scheduled_action_name  = "${var.cluster_name}-scale-in-at-night"

  min_size               = 2

  max_size               = 10

  desired_capacity       = 2

  recurrence             = "0 17 * * *"

  autoscaling_group_name = aws_autoscaling_group.example.name

}

Если var.enable_autoscaling равно true, параметру count для каждого из ресурсов aws_autoscaling_schedule будет присвоено значение 1, поэтому оба они будут созданы в единственном экземпляре. Если var.enable_autoscaling равно false, параметру count для каждого из ресурсов aws_autoscaling_schedule будет присвоено значение 0, поэтому ни один из них создан не будет. Это именно та условная логика, которая нам нужна!

Теперь мы можем обновить использование этого модуля в тестовой среде (в файле live/stage/services/webserver-cluster/main.tf): выключим масштабирование, присвоив enable_autoscaling значение false:

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"

  instance_type        = "t2.micro"

  min_size             = 2

  max_size             = 2

  enable_autoscaling   = false

}

Аналогичным образом обновим использование этого модуля в промышленной среде (в файле live/prod/services/webserver-cluster/main.tf). Включим масштабирование, присвоив enable_autoscaling значение true (не забудьте также убрать пользовательские ресурсы aws_autoscaling_schedule, которые остались в промышленной среде после выполнения примеров главы 4):

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

  custom_tags = {

    Owner      = "team-foo"

    DeployedBy = "terraform"

  }

}

Этот подход хорошо работает в случае, если пользователь передает вашему модулю явное булево значение. Но если вместо этого передается результат более сложного сравнения, такого как проверка равенства строк? Рассмотрим более замысловатый пример.

Представьте, что вы хотите создать в рамках модуля webserver-cluster набор оповещений CloudWatch. Оповещение CloudWatch может доставляться с помощью разных механизмов (скажем, в виде электронного письма или текстового сообщения), если достигается заранее заданный порог. Например, ниже мы используем ресурс aws_cloudwatch_metric_alarm в файле modules/services/webserver-cluster/main.tf, чтобы создать оповещение, которое срабатывает, если загруженность процессора превышает 90 % на протяжении пяти минут:

resource "aws_cloudwatch_metric_alarm" "high_cpu_utilization" {

  alarm_name  = "${var.cluster_name}-high-cpu-utilization"

  namespace   = "AWS/EC2"

  metric_name = "CPUUtilization"

  dimensions = {

    AutoScalingGroupName = aws_autoscaling_group.example.name

  }

  comparison_operator = "GreaterThanThreshold"

  evaluation_periods  = 1

  period              = 300

  statistic           = "Average"

  threshold           = 90

  unit                = "Percent"

}

Это хорошо работает для показателя CPUUtilization. Но если нужно добавить еще одно оповещение, которое срабатывает, когда заканчиваются кредиты для процессора?46 Ниже это демонстрируется на примере нашего кластера веб-серверов:

resource "aws_cloudwatch_metric_alarm" "low_cpu_credit_balance" {

  alarm_name  = "${var.cluster_name}-low-cpu-credit-balance"

  namespace   = "AWS/EC2"

  metric_name = "CPUCreditBalance"

  dimensions = {

    AutoScalingGroupName = aws_autoscaling_group.example.name

  }

  comparison_operator = "LessThanThreshold"

  evaluation_periods  = 1

  period              = 300

  statistic           = "Minimum"

  threshold           = 10

  unit                = "Count"

}

Но есть одна загвоздка: кредиты для процессора распространяются только на серверы типа tXXX (как t2.micro, t2.medium и т. д.). Более крупные типы серверов (вроде m4.large) эти кредиты не поддерживают и не отчитываются о показателе CPUCreditBalance. Поэтому, если вы создадите подобное оповещение для таких серверов, оно никогда не выйдет из состояния INSUFFICIENT_DATA. Возможно ли создавать оповещения только в случае, если var.instance_type начинается с буквы t?

Вы могли бы создать новую булеву входную переменную с именем var.is_t2_instance, но тогда бы она дублировала var.instance_type, а вы, скорее всего, забудете обновлять их вместе. Лучшая альтернатива — использование условного выражения:

resource "aws_cloudwatch_metric_alarm" "low_cpu_credit_balance" {

  count = format("%.1s", var.instance_type) == "t" ? 1 : 0

  alarm_name  = "${var.cluster_name}-low-cpu-credit-balance"

  namespace   = "AWS/EC2"

  metric_name = "CPUCreditBalance"

  dimensions = {

    AutoScalingGroupName = aws_autoscaling_group.example.name

  }

  comparison_operator = "LessThanThreshold"

  evaluation_periods  = 1

  period              = 300

  statistic           = "Minimum"

  threshold           = 10

  unit                = "Count"

}

Код оповещения остается прежним, если не считать относительно сложного параметра count:

count = format("%.1s", var.instance_type) == "t" ? 1 : 0

Здесь используется функция format, которая извлекает первый символ из var.in­stance_type. Если это символ t (как в случае с t2.micro), параметру count присваивается значение 1; в противном случае параметр count будет равен 0. Таким образом, оповещение создается только для серверов, у которых действительно есть показатель CPUCreditBalance.

Выражения if-else с использованием параметра count

Теперь вы знаете, как создавать выражения if. Но что насчет if-else?

Ранее в этой главе вы создали несколько пользователей IAM с правом на чтение ресурсов EC2. Представьте, что вы хотите дать одному из них, Neo, еще и доступ к CloudWatch, но будет этот доступ только на чтение или еще и на запись, должен решать тот, кто применяет конфигурацию Terraform. Этот пример немного надуманный, но он позволяет легко продемонстрировать простую разновидность выражения if-else, в которой существенно лишь то, какая из веток, if или else, будет выполнена. В то же время остальному коду Terraform не нужно ничего об этом знать.

Вот правило IAM, которое разрешает доступ на чтение к CloudWatch:

resource "aws_iam_policy" "cloudwatch_read_only" {

  name   = "cloudwatch-read-only"

  policy = data.aws_iam_policy_document.cloudwatch_read_only.json

}

data "aws_iam_policy_document" "cloudwatch_read_only" {

  statement {

    effect    = "Allow"

    actions   = [

      "cloudwatch:Describe*",

      "cloudwatch:Get*",

      "cloudwatch:List*"

    ]

    resources = ["*"]

  }

}

А вот правило IAM, которое выдает полный доступ к CloudWatch (на чтение и запись):

resource "aws_iam_policy" "cloudwatch_full_access" {

  name   = "cloudwatch-full-access"

  policy = data.aws_iam_policy_document.cloudwatch_full_access.json

}

data "aws_iam_policy_document" "cloudwatch_full_access" {

  statement {

    effect    = "Allow"

    actions   = ["cloudwatch:*"]

    resources = ["*"]

  }

}

Наша цель — назначить одно из этих правил IAM пользователю neo с учетом значения новой входной переменной под названием give_neo_cloudwatch_full_access:

variable "give_neo_cloudwatch_full_access" {

  description = "If true, neo gets full access to CloudWatch"

  type        = bool

}

Если бы вы использовали язык программирования общего назначения, выражение if-else можно было бы написать в таком виде:

# Это просто псевдокод. Он не будет работать в Terraform.

if var.give_neo_cloudwatch_full_access {

  resource "aws_iam_user_policy_attachment" "neo_cloudwatch_full_access" {

    user       = aws_iam_user.example[0].name

    policy_arn = aws_iam_policy.cloudwatch_full_access.arn

  }

} else {

  resource "aws_iam_user_policy_attachment" "neo_cloudwatch_read_only" {

    user       = aws_iam_user.example[0].name

    policy_arn = aws_iam_policy.cloudwatch_read_only.arn

  }

}

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

resource "aws_iam_user_policy_attachment" "neo_cloudwatch_full_access" {

  count = var.give_neo_cloudwatch_full_access ? 1 : 0

  user       = aws_iam_user.example[0].name

  policy_arn = aws_iam_policy.cloudwatch_full_access.arn

}

resource "aws_iam_user_policy_attachment" "neo_cloudwatch_read_only" {

  count = var.give_neo_cloudwatch_full_access ? 0 : 1

  user       = aws_iam_user.example[0].name

  policy_arn = aws_iam_policy.cloudwatch_read_only.arn

}

Этот код содержит два ресурса aws_iam_user_policy_attachment. У первого, который выдает полный доступ к CloudWatch, есть условное выражение. Если var.gi­ve_neo_cloudwatch_full_access равно true, оно возвращает 1, если нет — 0 (это частица if). Условное выражение второго ресурса, который выдает доступ на чтение, делает все наоборот: если var.give_neo_cloudwatch_full_access равно true, оно возвращает 0, если нет — 1 (это частица else).

Этот подход хорошо работает в ситуациях, когда вашему коду Terraform не нужно знать о том, какая из веток (if или else) на самом деле выполняется. Но если нужно обратиться к какому-нибудь выходному атрибуту ресурса, который возвращается из if или else? Представьте, к примеру, что вы хотите предложить пользователю на выбор два разных скрипта в разделе user_data модуля webserver-cluster. В настоящее время модуль webserver-cluster загружает скрипт user-da­ta.sh из источника данных template_file:

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

  }

}

Скрипт user-data.sh сейчас выглядит так:

#!/bin/bash

cat > index.html <

Hello, World

DB address: ${db_address}

DB port: ${db_port}

EOF

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

Теперь представьте, что вы хотите позволить некоторым из своих кластеров использовать следующую, более короткую альтернативу под названием user-data-new.sh:

#!/bin/bash

echo "Hello, World, v2" > index.html

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

Для загрузки этого скрипта вам понадобится новый источник данных template_file:

data "template_file" "user_data_new" {

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

  vars = {

    server_port = var.server_port

  }

}

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

variable "enable_new_user_data" {

  description = "If set to true, use the new User Data script"

  type        = bool

}

Если бы вы использовали язык программирования общего назначения, вы могли бы разместить в конфигурации запуска выражение if-else, которое выбирает между двумя вариантами template_file в user_data:

# Это просто псевдокод. Он не будет работать в Terraform.

resource "aws_launch_configuration" "example" {

  image_id        = "ami-0c55b159cbfafe1f0"

  instance_type   = var.instance_type

  security_groups = [aws_security_group.instance.id]

  if var.enable_new_user_data {

    user_data = data.template_file.user_data_new.rendered

  } else {

    user_data = data.template_file.user_data.rendered

  }

}

Чтобы это заработало в настоящем коде Terraform, сначала нужно воспользоваться приемом симуляции выражения if-else, который мы рассмотрели ранее, чтобы в итоге создавался только один источник данных template_file:

data "template_file" "user_data" {

  count = var.enable_new_user_data ? 0 : 1

  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

  }

}

data "template_file" "user_data_new" {

  count = var.enable_new_user_data ? 1 : 0

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

  vars = {

    server_port = var.server_port

  }

}

Если атрибут var.enable_new_user_data равен true, будет создан источник da­ta.template_file.user_data_new, но не data.template_file.user_data. Если он равен false, все будет наоборот. Вам остается лишь присвоить параметру user_data ресурса aws_launch_configuration источник данных template_file, который на самом деле существует. Для этого можно воспользоваться еще одним условным выражением:

resource "aws_launch_configuration" "example" {

  image_id        = "ami-0c55b159cbfafe1f0"

  instance_type   = var.instance_type

  security_groups = [aws_security_group.instance.id]

  user_data = (

    length(data.template_file.user_data[*]) > 0

      ? data.template_file.user_data[0].rendered

      : data.template_file.user_data_new[0].rendered

  )

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

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

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

  lifecycle {

    create_before_destroy = true

  }

}

Разберем по частям большое значение параметра user_data. Вначале взгляните на проверку булева условия:

length(data.template_file.user_data[*]) > 0

Обратите внимание, что оба источника данных используют параметр count и, следовательно, являются массивами, поэтому для работы с ними нужно использовать соответствующий синтаксис. Однако один из них имеет длину 1, а другой — 0, поэтому вы не можете напрямую обратиться по заданному индексу (например, da­ta.template_file.user_data[0]), так как массив может оказаться пустым. В качестве решения можно воспользоваться выражением *, которое всегда возвращает массив (хоть и потенциально пустой), и затем проверить его длину.

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

? data.template_file.user_data[0].rendered

: data.template_file.user_data_new[0].rendered

Terraform выполняет отложенное вычисление условных результатов, поэтому значение true будет получено, только если условие истинно. В противном случае значение равно false. Таким образом, обращение к элементам user_data и user_data_new с индексом 0 будет безопасным, поскольку мы знаем, что вычислению подлежит только выражение с непустым массивом.

Можете теперь попробовать новый скрипт пользовательских данных в тестовой среде. Для этого присвойте параметру enable_new_user_data в файле live/stage/services/webserver-cluster/main.tf значение true:

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"

  instance_type        = "t2.micro"

  min_size             = 2

  max_size             = 2

  enable_autoscaling   = false

  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.

Как видите, Terraform хочет внести два изменения: заменить старую конфигурацию запуска новой с обновленным полем user_data и модифицировать уже имеющуюся группу автомасштабирования так, чтобы она ссылалась на новую конфигурацию запуска. Проблема в том, что во втором случае изменения не вступят в силу, пока ASG не запустит новые серверы EC2. Как же заставить ASG их развернуть?

Как вариант, вы можете уничтожить группу ASG (например, с помощью команды terraformdestroy) и затем создать ее заново (скажем, выполнив terraformapply). Но проблема этого способа в том, что после удаления старого экземпляра ASG и до загрузки нового ваши пользователи будут испытывать перебои в работе. Вместо этого лучше сделать развертывание с нулевым временем простоя. Для этого нужно сначала создать новую группу ASG и затем удалить старую. Оказывается, именно это делает параметр жизненного цикла create_before_destroy, с которым вы впервые столкнулись в главе 2. Рассмотрим, как организовать развертывание с нулевым временем простоя, используя этот параметр жизненного цикла47.

1. Сконфигурируйте параметр name для ASG так, чтобы он напрямую зависел от имени конфигурации запуска. При каждом изменении этой конфигурации (которое происходит в результате обновления AMI или пользовательских данных) будет меняться и ее название, а вместе с ним и имя ASG. Это заставит Terraform заменить группу автомасштабирования.

2. Присвойте true параметру create_before_destroy группы ASG, чтобы каждый раз, когда ее нужно заменить, система Terraform сначала создавала ее замену, и только потом удаляла оригинал.

3. Присвойте параметру min_elb_capacity группы ASG значение min_size, принадлежащее кластеру. Благодаря этому, прежде чем уничтожать оригинал, Terraform будет ждать, пока как минимум min_size серверов из новой группы ASG не пройдут проверку работоспособности в ALB.

Так должен выглядеть обновленный ресурс aws_autoscaling_group в файле modules/services/webserver-cluster/main.tf:

resource "aws_autoscaling_group" "example" {

  # Создаем явную зависимость от имени конфигурации запуска,

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

  name = "${var.cluster_name}-${aws_launch_configuration.example.name}"

  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

  # Ждем, пока проверку работоспособности не пройдет как минимум

  # столько серверов, прежде чем считать завершенным развертывание ASG

  min_elb_capacity = var.min_size

  # При замене этой группы ASG сначала создаем ее новую версию

  # и только потом удаляем старую

  lifecycle {

    create_before_destroy = true

  }

  tag {

    key                 = "Name"

    value               = var.cluster_name

    propagate_at_launch = true

  }

  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

    }

  }

}

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

Terraform will perform the following actions:

  # module.webserver_cluster.aws_autoscaling_group.example must be replaced

+/- resource "aws_autoscaling_group" "example" {

      ~ id     = "example-2019" -> (known after apply)

      ~ name   = "example-2019" -> (known after apply) # forces replacement

        (...)

    }

  # module.webserver_cluster.aws_launch_configuration.example must be replaced

+/- resource "aws_launch_configuration" "example" {

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

        image_id        = "ami-0c55b159cbfafe1f0"

        instance_type   = "t2.micro"

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

      ~ user_data       = "bd7c0a" -> "4919a" # forces replacement

        (...)

    }

    (...)

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

Главное, на что следует обратить внимание, — это строка forcesreplacement напротив параметра name ресурса aws_autoscaling_group. Это означает, что Terraform заменит этот ресурс новой группой ASG с новым образом AMI или новыми пользовательскими данными. Выполните команду apply, чтобы инициировать развертывание, и проследите за тем, как этот процесс работает.

Сначала у нас запущена оригинальная группа ASG, скажем, версии v1 (рис. 5.2).

Вы обновляете некоторые аспекты конфигурации запуска (например, переходите на образ AMI с кодом версии v2) и выполняете команду apply. Это заставляет Terraform начать процесс развертывания нового экземпляра ASG с кодом версии v2 (рис. 5.3).

После 1–2 минут серверы в новой группе ASG завершили загрузку, подключились к базе данных, зарегистрировались в ALB и начали проходить проверку работоспособности. На этом этапе обе версии вашего приложения, v1 и v2, работают параллельно, и то, какую из них видит пользователь, зависит от того, куда ALB направил его запрос (рис. 5.4).

Рис. 5.2. Сначала у вас есть исходная группа ASG, выполняющая код версии v1

Рис. 5.3. Terraform начинает развертывание нового экземпляра ASG с кодом версии v2

Рис. 5.4. Серверы в новой группе ASG загрузились, подключились к БД, зарегистрировались в ALB и начали обслуживать запросы

После того, как min_elb_capacity серверов из кластера ASG версии v2 зарегистрировалось в ALB, Terraform начинает удалять старую группу ASG. Сначала отменяется их регистрация в ALB, а затем они выключаются (рис. 5.5).

Через 1–2 минуты старая группа ASG исчезнет, и у вас останется только версия v2 вашего приложения в новом экземпляре ASG (рис. 5.6).

Весь процесс у вас всегда будут оставаться рабочие серверы, обслуживающие запросы от ALB, поэтому простоя не наблюдается. Открыв URL-адрес ALB в своем браузере, вы должны увидеть что-то похожее на рис. 5.7.

Рис. 5.5. Серверы из старой группы ASG начинают выключаться

Рис. 5.6. Теперь остается только новая группа ASG, которая выполняет код версии v2

Получилось! Сервер с новым сообщением был развернут. Можете провести увлекательный эксперимент: внесите еще одно изменение в параметр server_text (например, поменяйте текст на foobar) и выполните команду apply. Если вы работаете в Linux/Unix/OS X, можете открыть отдельную вкладку терминала и запустить однострочный bash-скрипт, который будет циклически вызывать curl, обращаясь к ALB раз в секунду. Это наглядно продемонстрирует, как происходит развертывание с нулевым временем простоя:

$ while true; do curl http://>; sleep 1; done

Рис. 5.7. Новый код уже развернут

Где-то на протяжении первой минуты ответ должен оставаться прежним: Newservertext. Затем вы заметите чередование Newservertext и foobar. Значит, новые серверы зарегистрировались в ALB и прошли проверку работоспособности. Еще через минуту сообщение Newservertext исчезнет, и вы будете видеть только foobar. Это означает, что старая группа ASG была отключена. Вывод будет выглядеть примерно так (для ясности я вывожу только содержимое тегов

):

New server text

New server text

New server text

New server text

New server text

New server text

foo bar

New server text

foo bar

New server text

foo bar

New server text

foo bar

New server text

foo bar

New server text

foo bar

foo bar

foo bar

foo bar

foo bar

foo bar

У этого подхода есть еще одно преимущество: если во время развертывания что-то пойдет не так, Terraform автоматически откатит все назад. Например, если в версии v2 вашего приложения допущена ошибка, из-за которой оно не может загрузиться, серверы из новой группы ASG не будут зарегистрированы в ALB. Terraform будет ждать регистрации min_elb_capacity серверов из ASG v2 на протяжении отрезка времени длиной wait_for_capacity_timeout (по умолчанию десять минут). После этого развертывание считается неудачным, серверы ASG v2 удаляются, а Terraform завершает работу с ошибкой (тем временем версия v1 вашего приложения продолжает нормально работать в оригинальной группе ASG).

Подводные камни Terraform

После рассмотрения всех этих советов и приемов стоит сделать шаг назад и выделить несколько подводных камней, включая те, что связаны с циклами, выражениями if и методиками развертывания, а также с более общими проблемами, которые касаются Terraform в целом:

• параметры count и for_each имеют ограничения;

• ограничения развертываний с нулевым временем простоя;

• даже хороший план может оказаться неудачным;

• рефакторинг может иметь свои подвохи;

• отложенная согласованность согласуется… с отлагательством.

Параметры count и for_each имеют ограничения

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

• В count и for_each нельзя ссылаться ни на какие выходные переменные ресурса.

•count и for_each нельзя использовать в конфигурации модуля.

Рассмотрим их по очереди.

В count и for_each нельзя ссылаться ни на какие выходные переменные ресурса

Представьте, что нужно развернуть несколько серверов EC2 и по какой-то причине вы не хотите использовать ASG. Ваш код может быть таким:

resource "aws_instance" "example_1" {

  count         = 3

  ami           = "ami-0c55b159cbfafe1f0"

  instance_type = "t2.micro"

}

Поскольку параметру count присвоено статическое значение, этот код заработает без проблем: когда вы выполните команду apply, он создаст три сервера EC2. Но если вам захотелось развернуть по одному серверу в каждой зоне доступности (Availability Zone или AZ) в рамках текущего региона AWS? Вы можете сделать так, чтобы ваш код загрузил список зон из источника данных aws_availability_zones и затем «циклически» прошелся по каждой из них и создал в ней сервер EC2, используя параметр count и доступ к массиву по индексу:

resource "aws_instance" "example_2" {

  count             = length(data.aws_availability_zones.all.names)

  availability_zone = data.aws_availability_zones.all.names[count.index]

  ami               = "ami-0c55b159cbfafe1f0"

  instance_type     = "t2.micro"

}

data "aws_availability_zones" "all" {}

Этот код тоже будет прекрасно работать, поскольку параметр count может без проблем ссылаться на источники данных. Но что произойдет, если количество серверов, которые вам нужно создать, зависит от вывода какого-то ресурса? Чтобы это продемонстрировать, проще всего взять ресурс random_integer, который, как можно догадаться по названию, возвращает случайное целое число:

resource "random_integer" "num_instances" {

  min = 1

  max = 3

}

Этот код генерирует случайное число от 1 до 3. Посмотрим, что случится, если мы попытаемся использовать вывод result этого ресурса в параметре count ресурса aws_instance:

resource "aws_instance" "example_3" {

  count         = random_integer.num_instances.result

  ami           = "ami-0c55b159cbfafe1f0"

  instance_type = "t2.micro"

}

Если выполнить для этого кода terraformplan, получится следующая ошибка:

Error: Invalid count argument

  on main.tf line 30, in resource "aws_instance" "example_3":

  30: count = random_integer.num_instances.result

The "count" value depends on resource attributes that cannot be determined

until apply, so Terraform cannot predict how many instances will be created.

To work around this, use the -target argument to first apply only the

resources that the count depends on.

Terraform требует, чтобы count и for_each вычислялись на этапе планирования, до создания или изменения каких-либо ресурсов. Это означает, что count и for_each могут ссылаться на литералы, переменные, источники данных и даже списки ресурсов (при условии, что их длину можно определить во время планирования), но не на вычисляемые выходные переменные ресурса.

count и for_each нельзя использовать в конфигурации модуля

Когда-нибудь у вас может появиться соблазн добавить параметр count в конфигурации модуля:

module "count_example" {

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

  count = 3

  cluster_name  = "terraform-up-and-running-example"

  server_port   = 8080

  instance_type = "t2.micro"

}

Этот код пытается использовать count внутри модуля, чтобы создать три копии ресурса webserver-cluster. Или, возможно, вам захочется сделать подключение модуля опциональным в зависимости от какого-нибудь булева условия, присвоив его параметру count значение 0. Такой код будет выглядеть вполне разумно, однако в результате выполнения terraformplan вы получите такую ошибку:

Error: Reserved argument name in module block

  on main.tf line 13, in module "count_example":

  13: count = 3

The name "count" is reserved for use in a future version of Terraform.

К сожалению, на момент выхода Terraform 0.12.6 использование count или for_each в ресурсе module не поддерживается. Согласно заметкам о выпуске Terraform 0.12 (http://bit.ly/3257bv4) компания HashiCorp планирует добавить эту возможность в будущем, поэтому, в зависимости от того, когда вы читаете эту книгу, она уже может быть доступна. Чтобы узнать наверняка, почитайте журнал изменений Terraform по адресу https://github.com/hashicorp/terraform/blob/master/CHANGELOG.md.

Ограничения развертываний с нулевым временем простоя

Использование блока create_before_destroy в сочетании с ASG является отличным решением для организации развертываний с нулевым временем простоя, если не считать один нюанс: правила автомасштабирования при этом не поддерживаются. Или, если быть более точным, это сбрасывает размер ASG обратно к min_size при каждом развертывании, что может стать проблемой, если вы использовали правила автомасштабирования для увеличения количества запущенных серверов.

Например, модуль webserver-cluster содержит пару ресурсов aws_autoscaling_schedule, которые в 9 утра увеличивают количество серверов в кластере с двух до десяти. Если выполнить развертывание, скажем, в 11 утра, новая группа ASG загрузится не с десятью, а всего с двумя серверами и будет оставаться в таком состоянии до 9 утра следующего дня.

Это ограничение можно обойти несколькими путями.

• Поменять параметр recurrence в aws_autoscaling_schedule с 09*** («запускать в 9 утра») на что-то вроде 0-599-17*** («запускать каждую минуту с 9 утра до 5 вечера»). Если в ASG уже есть десять серверов, повторное выполнение этого правила автомасштабирования ничего не изменит, что нам и нужно. Но если группа ASG развернута совсем недавно, это правило гарантирует, что максимум через минуту количество ее серверов достигнет десяти. Это не совсем элегантный подход, и большие скачки с десяти до двух серверов и обратно тоже могут вызвать проблемы у пользователей.

• Создать пользовательский скрипт, который применяет API AWS для определения количества активных серверов в ASG, вызвать его с помощью внешнего источника данных (см. пункт «Внешний источник данных» на с. 249) и присвоить параметру desired_capacity группы ASG значение, возвращенное этим скриптом. Таким образом, каждый новый экземпляр ASG всегда будет запускаться с той же емкостью, что и старый. Недостаток в том, что применение пользовательских скриптов ухудшает переносимость вашего кода Terraform и усложняет его обслуживание.

Конечно, в идеале в Terraform должна быть встроенная поддержка развертываний с нулевым временем простоя, но по состоянию на май 2019 года команда HashiCorp не планировала добавлять эту функциональность (подробности — по адресу git­hub.com/hashicorp/terraform/issues/1552).

Корректный план может быть неудачно реализован

Иногда при выполнении команды plan получается вполне корректный план развертывания, однако команда apply возвращает ошибку. Попробуйте, к примеру, добавить ресурс aws_iam_user с тем же именем, которое вы использовали для пользователя IAM, созданного вами ранее в главе 2:

resource "aws_iam_user" "existing_user" {

  # Подставьте сюда имя уже существующего пользователя IAM,

  # чтобы попрактиковаться в использовании команды terraform import

  name = "yevgeniy.brikman"

}

Теперь, если выполнить команду plan, Terraform выведет на первый взгляд вполне разумный план развертывания:

Terraform will perform the following actions:

  # aws_iam_user.existing_user will be created

  + resource "aws_iam_user" "existing_user" {

      + arn           = (known after apply)

      + force_destroy = false

      + id            = (known after apply)

      + name          = "yevgeniy.brikman"

      + path          = "/"

      + unique_id     = (known after apply)

    }

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

Если выполнить команду apply, получится следующая ошибка:

Error: Error creating IAM User yevgeniy.brikman: EntityAlreadyExists:

User with name yevgeniy.brikman already exists.

  on main.tf line 10, in resource "aws_iam_user" "existing_user":

  10: resource "aws_iam_user" "existing_user" {

Проблема, конечно, в том, что пользователь IAM с таким именем уже существует. И это может случиться не только с пользователями IAM, но и почти с любым ресурсом. Возможно, кто-то создал этот ресурс вручную или с помощью командной строки, но, как бы то ни было, совпадение идентификаторов приводит к конфликтам. У этой ошибки существует множество разновидностей, которые часто застают врасплох новичков в Terraform.

Ключевым моментом является то, что команда terraformplan учитывает только те ресурсы, которые указаны в файле состояния Terraform. Если ресурсы созданы каким-то другим способом (например, вручную, щелчком кнопкой мыши на консоли AWS), они не попадут в файл состояния и, следовательно, Terraform не будет их учитывать при выполнении команды plan. В итоге корректный на первый взгляд план окажется неудачным.

Из этого можно извлечь два урока.

Если вы уже начали работать с Terraform, не используйте ничего другого. Если часть вашей инфраструктуры управляется с помощью Terraform, больше нельзя изменять ее вручную. В противном случае вы не только рискуете получить странные ошибки Terraform, но также сводите на нет многие преимущества IaC, так как код больше не будет точным представлением вашей инфраструктуры.

• Если у вас уже есть какая-то инфраструктура, используйте команду import. Если вы начинаете использовать Terraform с уже существующей инфраструктурой, ее можно добавить в файл состояния с помощью команды terraformimport. Так Terraform будет знать, какой инфраструктурой нужно управлять. Команда import принимает два аргумента. Первым служит адрес ресурса в ваших конфигурационных файлах. Здесь тот же синтаксис, что и в ссылках на ресурсы: _. (вроде aws_iam_user.existing_user). Второй аргумент — это идентификатор ресурса, который нужно импортировать. Скажем, в качестве ID ресурса aws_iam_user выступает имя пользователя (например, yevgeniy.brikman), а ID ресурса aws_instance будет идентификатор сервера EC2 (вроде i-190e22e5). То, как импортировать ресурс, обычно указывается в документации внизу его страницы.

Ниже показана команда import, позволяющая синхронизировать ресурс aws_­iam­_user, который вы добавили в свою конфигурацию Terraform вместе с пользователем IAM в главе 2 (естественно, вместо yevgeniy.brikman нужно подставить ваше имя):

$ terraform import aws_iam_user.existing_user yevgeniy.brikman

Terraform обратится к API AWS, чтобы найти вашего пользователя IAM и создать в файле состояния связь между ним и ресурсом aws_iam_user.existing_user в вашей конфигурации Terraform. С этого момента при выполнении команды plan Terraform будет знать, что пользователь IAM уже существует, и не станет пытаться создать его еще раз.

Следует отметить, что, если у вас уже есть много ресурсов, которые вы хотите импортировать в Terraform, ручное написание кода и импорт каждого из них по очереди может оказаться хлопотным занятием. Поэтому стоит обратить внимание на такой инструмент, как Terraforming (http://terraforming.dtan4.net/), который может автоматически импортировать из учетной записи AWS код и состояние.

Рефакторинг может иметь свои подвохи

Рефакторинг — распространенная практика в программировании, когда вы меняете внутреннюю структуру кода, оставляя внешнее поведение без изменения. Это нужно, чтобы сделать код более понятным, опрятным и простым в обслуживании. Рефакторинг — это незаменимая методика, которую следует регулярно применять. Но, когда речь идет о Terraform или любом другом средстве IaC, следует крайне осторожно относиться к тому, что имеется в виду под «внешним поведением» участка кода, иначе возникнут непредвиденные проблемы.

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

К примеру, у модуля webserver-cluster есть входная переменная cluster_name:

variable "cluster_name" {

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

  type        = string

}

Представьте, что вы начали использовать этот модуль для развертывания микросервиса с названием foo. Позже вам захотелось переименовать свой сервис в bar. Это изменение может показаться тривиальным, но в реальности из-за него могут возникнуть перебои в работе.

Дело в том, что модуль webserver-cluster использует переменную cluster_name в целом ряде ресурсов, включая параметр name двух групп безопасности и ALB:

resource "aws_lb" "example" {

  name               = var.cluster_name

  load_balancer_type = "application"

  subnets            = data.aws_subnet_ids.default.ids

  security_groups    = [aws_security_group.alb.id]

}

Если поменять параметр name в каком-то ресурсе, Terraform удалит старую версию этого ресурса и создаст вместо него новую. Но если таким ресурсом является ALB, в период между его удалением и загрузкой новой версии у вас не будет механизма для перенаправления трафика к вашему веб-серверу. Точно так же, если удаляется группа безопасности, ваши серверы начнут отклонять любой сетевой трафик, пока не будет создана новая группа.

Еще одним видом рефакторинга, который вас может заинтересовать, является изменение идентификатора Terraform. Возьмем в качестве примера ресурс aws_security_group в модуле webserver-cluster:

resource "aws_security_group" "instance" {

  # (...)

}

Идентификатор этого ресурса называется instance. Представьте, что во время рефакторинга вы решили поменять его на более понятное (по вашему мнению) имя cluster_instance:

resource "aws_security_group" "cluster_instance" {

  # (...)

}

Что в итоге случится? Правильно: перебой в работе.

Terraform связывает ID каждого ресурса с идентификатором облачного провайдера. Например, iam_user привязывается к идентификатору пользователя IAM в AWS, а aws_instance — к ID сервера AWS EC2. Если изменить идентификатор ресурса (скажем, с instance на cluster_instance, как в случае с aws_security_group), для Terraform это будет выглядеть так, будто вы удалили старый ресурс и добавили новый. Если применить эти изменения, Terraform удалит старую группу безопасности и создаст другую, а между тем ваши серверы начнут отклонять любой сетевой трафик.

Вот четыре основных урока, которые вы должны извлечь из этого обсуждения.

Всегда используйте команду plan. Ею можно выявить все эти загвоздки. Тщательно просматривайте ее вывод и обращайте внимание на ситуации, когда Terraform планирует удалить ресурсы, которые, скорее всего, удалять не стоит.

• Создавайте, прежде чем удалять. Если вы хотите заменить ресурс, хорошенько подумайте, нужно ли создавать замену до удаления оригинала. Если ответ положительный, в этом может помочь create_before_destroy. Того же результата можно добиться вручную, выполнив два шага: сначала добавить в конфигурацию новый ресурс и запустить команду apply, а затем удалить из конфигурации старый ресурс и воспользоваться командой apply еще раз.

• Изменение идентификаторов требует изменения состояния. Если вы хотите поменять идентификатор, связанный с ресурсом (например, переименовать aws_security_group с instance на cluster_instance), избегая при этом удаления ресурса и создания его новой версии, необходимо соответствующим образом обновить файл состояния Terraform. Никогда не делайте этого вручную — используйте вместо этого команду terraformstate. При переименовании идентификаторов следует выполнить команду terraformstatemv, которая имеет следующий синтаксис:

terraform state mv

ORIGINAL_REFERENCE — это выражение, ссылающееся на ресурс в его текущем виде, а NEW_REFERENCE — то место, куда вы хотите его переместить. Например, при переименовании группы aws_security_group с instance на cluster_instance нужно выполнить следующую команду:

$ terraform state mv \

  aws_security_group.instance \

  aws_security_group.cluster_instance

Так вы сообщите Terraform, что состояние, которое ранее относилось к aws_se­­curity_group.instance, теперь должно быть связано с aws_security_gro­up.cluster_instance. Если после переименования и запуска этой команды terraformplan не покажет никаких изменений, значит, вы все сделали правильно.

• Некоторые параметры нельзя изменять. Параметры многих ресурсов неизменяемые. Если попытаться их изменить, Terraform удалит старый ресурс и создаст вместо него новый. На странице каждого ресурса обычно указывается, что происходит при изменении того или иного параметра, поэтому не забывайте сверяться с документацией. Всегда используйте команду plan и рассматривайте целесообразность применения стратегии create_before_destroy.

Отложенная согласованность согласуется… с отлагательством

API некоторых облачных провайдеров, таких как AWS, асинхронные и имеют отложенную согласованность. Асинхронность означает, что интерфейс может сразу же вернуть ответ, не дожидаясь завершения запрошенного действия. Отложенная согласованность значит, что для распространения изменений по всей системе может понадобиться время; пока это происходит, ваши ответы могут быть несогласованными и зависеть от того, какая реплика источника данных отвечает на ваши API-вызовы.

Представьте, к примеру, что вы делаете API-вызов к AWS с просьбой создать сервер EC2. API вернет «успешный» ответ (201Created) практически мгновенно, не дожидаясь создания самого сервера. Если вы сразу же попытаетесь к нему подключиться, почти наверняка ничего не получится, поскольку в этот момент AWS все еще инициализирует ресурсы или, как вариант, сервер еще не загрузился. Более того, если вы сделаете еще один вызов, чтобы получить информацию об этом сервере, может прийти ошибка (404NotFound). Дело в том, что сведения об этом сервере EC2 все еще могут распространяться по AWS, чтобы они стали доступными везде, придется подождать несколько секунд.

При каждом использовании асинхронного API с отложенной согласованностью вы должны периодически повторять свой запрос, пока действие не завершится и не распространится по системе. К сожалению, AWS SDK не предоставляет для этого никаких хороших инструментов, и проект Terraform раньше страдал от множества ошибок вроде 6813 (https://github.com/hashicorp/terraform/issues/6813):

$ terraform apply

aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:

The subnet ID 'subnet-xxxxxxx' does not exist

Иными словами, вы создаете ресурс (например, подсеть) и затем пытаетесь получить о нем какие-то сведения (вроде ID только что созданной подсети), а Terraform не может их найти. Большинство из таких ошибок (включая 6813) уже исправлены, но время от времени они все еще проявляются, особенно когда в Terraform добавляют поддержку нового типа ресурсов. Это раздражает, но в большинстве случаев не несет никакого вреда. При повторном выполнении terraformapply все должно заработать, поскольку к этому моменту информация уже распространится по системе.

Резюме

Несмотря на свою декларативную сущность, Terraform включает целый ряд конструкций, которые делают его на удивление гибким и выразительным. Это, к примеру, касается переменных и модулей (см. главу 4), параметра count, выражений for_each, for и create_before_destroy, а также встроенных функций, которые вы могли видеть в этой главе. Вы познакомились с множеством приемов симуляции условных выражений, поэтому потратьте некоторое время на чтение документации по функциям (https://www.terraform.io/docs/configuration/functions.html) и позвольте разыграться своему «внутреннему хакеру». Главное, не переусердствуйте, так как кто-то все равно должен поддерживать ваш код; просто постарайтесь научиться создавать аккуратные и изящные API для своих модулей.

Теперь перейдем к главе 6, в которой важный акцент делается на готовности к промышленному использованию. Речь пойдет о модулях, на которые ваша компания может положиться.

46 О кредитах для процессора можно почитать на сайте EC2 по адресу amzn.to/2lTuvs5.

47 Автором этой методики является Пол Хинзе (bit.ly/2lksQgv).

Перейти на страницу:

Все книги серии Бестселлеры O'Reilly

Искусство управления IT-проектами
Искусство управления IT-проектами

В отличие от множества трудов, посвященных руководству проектами и командами, в этой книге не проповедуются никакие новые учения и не превозносятся великие теории. Скотт Беркун считает залогом успеха практику и разнообразие подходов. В книге описываются основные сложности и проблемные ситуации, возникающие в работе менеджера проекта, даны рекомендации по выходу из них.Издание предназначено не только для лидеров команд и менеджеров высшего звена, но и для программистов, тестеров и других исполнителей конкретных проектных заданий. Также оно будет полезно студентам, изучающим бизнес-менеджмент, проектирование изделий или программную инженерию.Текст нового издания значительно переработан автором с целью добиться большей ясности, кроме того, книга дополнена новым приложением и более чем 120 практическими упражнениями.

Скотт Беркун

Деловая литература
iOS. Приемы программирования
iOS. Приемы программирования

Книга, которую вы держите в руках, представляет собой новый, полностью переписанный сборник приемов программирования по работе с iOS. Он поможет вам справиться с наболевшими проблемами, с которыми приходится сталкиваться при разработке приложений для iPhone, iPad и iPod Touch. Вы быстро освоите всю информацию, необходимую для начала работы с iOS 7 SDK, в частности познакомитесь с решениями для добавления в ваши приложения реалистичной физики или движений — в этом вам помогут API UIKit Dynamics.Вы изучите новые многочисленные способы хранения и защиты данных, отправки и получения уведомлений, улучшения и анимации графики, управления файлами и каталогами, а также рассмотрите многие другие темы. При описании каждого приема программирования приводятся образцы кода, которые вы можете смело использовать.

Вандад Нахавандипур

Программирование, программы, базы данных / Программирование / Книги по IT

Похожие книги