Ранее в этой главе вы создали несколько пользователей 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.give_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-data.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, будет создан источник data.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, поэтому вы не можете напрямую обратиться по заданному индексу (например, data.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