Terraform for_each поверх содержимого файла yaml, который является объектом

У меня есть файл yaml, который похож на следующий (к вашему сведению: ssm_secrets может быть пустым массивом):

      rabbitmq:
  repo_name: bitnami
  namespace: rabbitmq
  target_revision: 11.1.1
  path: rabbitmq
  values_file: charts/rabbitmq/values.yaml
  ssm_secrets: []
app_name_1:
  repo_name: repo_name_1
  namespace: namespace_1
  target_revision: target_revision_1
  path: charts/path
  values_file: values.yaml
  ssm_secrets:
    - name: name-dev-1
      key: .env
      ssm_path: ssm_path/dev
name-backend:
  repo_name: repo_name_2
  namespace: namespace_2
  target_revision: target_revision_2
  path: charts/name-backend
  values_file: values.yaml
  ssm_secrets:
    - name: name-backend-app-dev
      ssm_path: name-backend/app/dev
      key: app.ini
    - name: name-backend-abi-dev
      ssm_path: name-backend/abi/dev
      key: contractTokenABI.json
    - name: name-backend-widget-dev
      ssm_path: name-backend/widget/dev
      key: name.ini
    - name: name-abi-dev
      ssm_path: name-abi/dev
      key: name_1.json
    - name: name-website-dev
      ssm_path: name/website/dev
      key: website.ini
    - name: name-name-dev
      ssm_path: name/name/dev
      key: contract.ini
    - name: name-key-dev
      ssm_path: name-key/dev
      key: name.pub

И используя внешние секреты и чертежи EKS, я пытаюсь создать файл yaml, необходимый для создания секретов.

      resource "kubectl_manifest" "secret" {
  for_each   = toset(flatten([for service in var.secrets : service.ssm_secrets[*].ssm_path]))
  yaml_body  = <<YAML
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: ${replace(each.value, "/", "-")}
  namespace: ${split("/", each.value)[0]}
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: ${local.cluster_secretstore_name}
    kind: ClusterSecretStore
  data:
  - secretKey: .env
    remoteRef:
       key: ${each.value}
YAML
  depends_on = [kubectl_manifest.cluster_secretstore, kubernetes_namespace_v1.namespaces]
}

Вышеупомянутое работает нормально, но мне также нужно использовать значение ключа из yaml в secretKey: <key_value from yaml>.

Если я попробую сfor_each = toset(flatten([for service in var.secrets : service.ssm_secrets[*]]))

      resource "kubectl_manifest" "secret" {
  for_each   = toset(flatten([for service in var.secrets : service.ssm_secrets[*]]))
  yaml_body  = <<YAML
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: ${replace(each.value["ssm_path"], "/", "-")}
  namespace: ${split("/", each.value["ssm_path"])[0]}
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: ${local.cluster_secretstore_name}
    kind: ClusterSecretStore
  data:
  - secretKey: .env
    remoteRef:
       key: ${each.value["ssm_path"]}
YAML
  depends_on = [kubectl_manifest.cluster_secretstore, kubernetes_namespace_v1.namespaces]
}

Это просто дает мне следующую ошибку:

Данное значение аргумента «for_each» не подходит: «for_each» поддерживает карты и наборы строк, но вы предоставили набор, содержащий объект типа.

Я попытался преобразовать переменную в карту, использовал поиск, но это не сработало. Любая помощь приветствуется.

Обновление 1:

Согласно предложению @MattSchuchard, изменение for_each наfor_each = toset(flatten([for service in var.secrets : service.ssm_secrets]))

Выдал следующую ошибку:

      Error: Invalid for_each set argument
│ 
│   on ../../modules/02-plugins/external-secrets.tf line 58, in resource "kubectl_manifest" "secret":
│   58:   for_each   = toset(flatten([for service in var.secrets : service.ssm_secrets]))
│     ├────────────────
│     │ var.secrets is object with 14 attributes
│ 
│ The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type object.

Обновление 2:
@mariux предложил идеальное решение, но вот что я придумал. Это не так уж и чище, но определенно работает (PS: сам собираюсь использовать решение Mariux):

      locals {
  my_list = tolist(flatten([for service in var.secrets : service.ssm_secrets[*]]))
}


resource "kubectl_manifest" "secret" {

  count      = length(local.my_list)
  yaml_body  = <<YAML
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: ${replace(local.my_list[count.index]["ssm_path"], "/", "-")}
  namespace: ${split("/", local.my_list[count.index]["ssm_path"])[0]}
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: ${local.cluster_secretstore_name}
    kind: ClusterSecretStore
  data:
  - secretKey: ${local.my_list[count.index]["key"]}
    remoteRef:
       key: ${local.my_list[count.index]["ssm_path"]}
YAML
  depends_on = [kubectl_manifest.cluster_secretstore, kubernetes_namespace_v1.namespaces]
}

2 ответа

Предположения

На основании того, что вы поделились, я делаю следующие предположения:

  • на самом деле эта услуга для вас не важна, так как вы хотите создать внешние секреты,ssm_secrets.*.nameиспользуя данныйkeyиssm_pathатрибуты.
  • каждыйnameявляется глобально уникальным для всех сервисов и никогда не используется повторно.

хаки терраформирования

Основываясь на предположениях, вы можете создать массив ВСЕХ ssm_secrets, используя

      locals {
  ssm_secrets_all = flatten(values(var.secrets)[*].ssm_secrets)
}

и преобразовать ее в карту, которую можно использовать вfor_eachвводя значения с помощью.name:

      locals {
  ssm_secrets_map = { for v in local.ssm_secrets_all : v.name => v }
}

Полный (рабочий) пример

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

  • С использованиемyamldecodeчтобы декодировать исходный ввод вlocal.input
  • С использованиемyamlencodeчтобы упростить чтение манифеста и удалить некоторые интерполяции строк. Это также гарантирует правильность отступа при преобразовании HCL в yaml.

Аterraform init && terraform planпланирует создать следующие ресурсы:

       kubectl_manifest.secret["name-abi-dev"] will be created
 kubectl_manifest.secret["name-backend-abi-dev"] will be created
 kubectl_manifest.secret["name-backend-app-dev"] will be created
 kubectl_manifest.secret["name-backend-widget-dev"] will be created
 kubectl_manifest.secret["name-dev-1"] will be created
 kubectl_manifest.secret["name-key-dev"] will be created
 kubectl_manifest.secret["name-name-dev"] will be created
 kubectl_manifest.secret["name-website-dev"] will be created
      locals {
  # input = var.secrets
  ssm_secrets_all = flatten(values(local.input)[*].ssm_secrets)
  ssm_secrets_map = { for v in local.ssm_secrets_all : v.name => v }

  cluster_secretstore_name = "not provided secretstore name"
}

resource "kubectl_manifest" "secret" {
  for_each = local.ssm_secrets_map

  yaml_body = yamlencode({
    apiVersion = "external-secrets.io/v1beta1"
    kind       = "ExternalSecret"
    metadata = {
      name      = replace(each.value.ssm_path, "/", "-")
      namespace = split("/", each.value.ssm_path)[0]
    }
    spec = {
      refreshInterval = "30m"
      secretStoreRef = {
        name = local.cluster_secretstore_name
        kind = "ClusterSecretStore"
      }
      data = [
        {
          secretKey = ".env"
          remoteRef = {
            key = each.value.key
          }
        }
      ]
    }
  })

  # not included dependencies
  # depends_on = [kubectl_manifest.cluster_secretstore, kubernetes_namespace_v1.namespaces]
}

locals {
  input = yamldecode(<<-EOF
    rabbitmq:
      repo_name: bitnami
      namespace: rabbitmq
      target_revision: 11.1.1
      path: rabbitmq
      values_file: charts/rabbitmq/values.yaml
      ssm_secrets: []
    app_name_1:
      repo_name: repo_name_1
      namespace: namespace_1
      target_revision: target_revision_1
      path: charts/path
      values_file: values.yaml
      ssm_secrets:
        - name: name-dev-1
          key: .env
          ssm_path: ssm_path/dev
    name-backend:
      repo_name: repo_name_2
      namespace: namespace_2
      target_revision: target_revision_2
      path: charts/name-backend
      values_file: values.yaml
      ssm_secrets:
        - name: name-backend-app-dev
          ssm_path: name-backend/app/dev
          key: app.ini
        - name: name-backend-abi-dev
          ssm_path: name-backend/abi/dev
          key: contractTokenABI.json
        - name: name-backend-widget-dev
          ssm_path: name-backend/widget/dev
          key: name.ini
        - name: name-abi-dev
          ssm_path: name-abi/dev
          key: name_1.json
        - name: name-website-dev
          ssm_path: name/website/dev
          key: website.ini
        - name: name-name-dev
          ssm_path: name/name/dev
          key: contract.ini
        - name: name-key-dev
          ssm_path: name-key/dev
          key: name.pub
    EOF
  )
}

terraform {
  required_version = "~> 1.0"

  required_providers {
    kubectl = {
      source  = "gavinbunney/kubectl"
      version = "~> 1.7"
    }
  }
}

подсказка: вы также можете попробовать использоватьkubernetes_manifestресурс вместоkubectl_manifest

PS: Мы создали Terramate , чтобы упростить сложное создание кода Terraform. Но для чистого Terraform это кажется вполне приемлемым.

Если вы изменитеfor_eachметапараметр для:

      for_each = toset(flatten([for service in var.secrets : service.ssm_secrets]))

затем переменная итератора области лямбда/замыкания внутриkubernetes_manifest.secretресурс с именем по умолчаниюeachбудетlist(object)тип, представляющий желаемые значения, аналогичный списку хешей в YAML (список карт в Kubernetes), и можно получить доступssm_pathсeach.value["ssm_path"], иkeyсeach.value["key"].

Другие вопросы по тегам