enable_new_user_data = true
}
В промышленной среде можно оставить старую версию скрипта, установив параметру enable_new_user_data в файле live/prod/services/webserver-cluster/main.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
CONDITION — это любое выражение, возвращающее булево значение, а TRUEVAL — выражение, которое нужно вывести, если CONDITION равно true. При желании можно также добавить блок else:
%{ if
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.