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

Ранее в этой главе вы создали несколько пользователей 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