Go to file
2026-07-07 17:10:06 +03:00
live ++ adopt prod 2026-07-07 17:10:06 +03:00
modules ++ adopt prod 2026-07-07 17:10:06 +03:00
scripts ++ port sops secret-values adopt mechanism to prod 2026-07-06 17:10:46 +03:00
.gitignore ++ adopt prod 2026-07-07 17:10:06 +03:00
.gitlab-ci.yml add prod kafka topics flow 2026-05-05 16:22:04 +03:00
.sops.yaml ++ port sops secret-values adopt mechanism to prod 2026-07-06 17:10:46 +03:00
Dockerfile ++ port sops secret-values adopt mechanism to prod 2026-07-06 17:10:46 +03:00
infrastructure-secrets.yaml ++ adopt prod 2026-07-07 17:10:06 +03:00
infrastructure.yaml ++ adopt prod 2026-07-07 17:10:06 +03:00
README.md ++ adopt prod 2026-07-07 17:10:06 +03:00

Terraform Infrastructure (stage)

Infrastructure as Code для управления ресурсами Yandex Cloud и Kubernetes окружения stage. Всё описывается декларативно в одном файле infrastructure.yaml; секретные значения — в зашифрованном infrastructure-secrets.yaml (sops). Terragrunt читает конфиг и раскладывает по модулям.

TL;DR для «мне нужно задеплоить X»: см. раздел Рецепты.


Содержание


Структура репозитория

terraform/
├── infrastructure.yaml         # ЕДИНЫЙ источник правды: все несекретные описания сущностей
├── infrastructure-secrets.yaml # sops-зашифрованные секретные значения (secret_values)
├── .sops.yaml                  # правило шифрования (age-recipient)
├── SOPS.md                     # как работать с sops-ключом и secret-values
├── Dockerfile                  # образ раннера: terraform + terragrunt + sops
├── .gitlab-ci.yml              # родительский pipeline (генерит дочерний и триггерит)
├── scripts/generate-pipeline.sh# генератор дочернего pipeline по юнитам live/
├── live/
│   ├── terragrunt.hcl          # root: S3 backend + генерация provider'ов
│   ├── provider.tf, backend.tf  # сгенерированные (не редактировать руками)
│   └── stage/
│       ├── env.hcl             # параметры окружения stage
│       ├── namespace/          # юнит: k8s namespaces
│       ├── s3/                 # юнит: S3 бакеты + YC service accounts
│       ├── database/           # юнит: PostgreSQL users/databases
│       ├── kafka-topics/       # юнит: Kafka topics/users/permissions
│       ├── rabbitmq/           # юнит: RabbitMQ vhosts/users/permissions/routing
│       └── secrets/            # юнит: k8s secrets (зависит от всех выше)
└── modules/
    ├── k8s-namespace/          # Kubernetes namespaces
    ├── k8s-secret/             # Kubernetes secrets (основной, из outputs/random/явных значений)
    ├── kafka-topics-yc/        # Kafka topics/users/permissions в YC Managed Kafka
    ├── rabbitmq/               # RabbitMQ сущности через Management API
    ├── yc-database/            # PostgreSQL users/databases
    └── yc-s3/                  # S3 бакеты с изолированным доступом

Один «юнит» = одна папка в live/stage/ со своим terragrunt.hcl и своим terraform-state (s3://tfstate-terragrunt-stage/stage/<unit>/terraform.tfstate).


Как это работает

  1. Ты правишь infrastructure.yaml (и при необходимости infrastructure-secrets.yaml через sops).
  2. Terragrunt в нужном юните читает конфиг: yamldecode(file(".../infrastructure.yaml")).environments.stage.
  3. Передаёт срез в соответствующий модуль как inputs.
  4. terragrunt plan/apply создаёт/меняет ресурсы. State — в S3 (versioning включён).

Порядок применения (зависимости)

namespace ──▶ s3 ─┐
              database ─┤
              kafka-topics ─┼──▶ secrets
              rabbitmq ─┘

secrets зависит от outputs всех инфраструктурных юнитов (берёт оттуда хосты/пароли/ключи). CI сам выстраивает needs; при ручном прогоне соблюдай порядок.


Сущности infrastructure.yaml

Всё живёт под environments.stage.*. Ниже — по одному разделу на тип.

namespaces

Kubernetes namespaces (юнит namespace).

namespaces:
  - name: pulse
    labels: { environment: stage }
    annotations: { managed-by: terraform }

buckets (S3)

S3-бакеты Yandex Object Storage с изолированным доступом (юнит s3, модуль yc-s3). Каждый бакет получает отдельный SA с доступом только к нему (через yandex_storage_bucket_iam_binding).

buckets:
  - name: pulse-stage
    acl: private
    role: storage.uploader        # storage.uploader(def) | storage.viewer | storage.editor
    versioning: { enabled: false }
    cors:
      enabled: true
      allowed_methods: [GET, PUT]

yc_service_accounts:              # доп. YC SA (не привязанные к бакету), опционально
  - name: app-external-sa
    description: External integration SA
    folder_roles: [storage.viewer]
    create_static_access_key: true

yc_service_accounts

Дополнительные YC Service Accounts с folder-ролями и (опц.) статическим ключом. Создаются в юните s3, их данные доступны секретам типа yc_sa.

databases

PostgreSQL users и databases в существующем кластере YC (юнит database, модуль yc-database). Пароль генерируется автоматически, на password стоит ignore_changes.

databases:
  - cluster_id: "c9qa2coo5ukgcg93fldm"
    database:
      name: mydb
      extensions: [pg_stat_statements]
    user:
      name: myuser
      password_length: 32
      conn_limit: 10
      permissions: [other_db]      # доступ к другим БД, опционально

kafka

YC Managed Kafka: topics, users, permissions (юнит kafka-topics, модуль kafka-topics-yc). Кластеры описаны в kafka_cluster_refs (id/host/port/sasl/…); в сущностях ссылаешься по clusterRef.

kafka_cluster_refs:
  stage:
    cluster_id: c9qo2dr244cohj4amaia
    host: rc1a-....mdb.yandexcloud.net
    port: 9091
    sasl_mechanism: SCRAM-SHA-512
    security_protocol: SASL_SSL
    default_partitions: 3
    default_replication_factor: 1

kafka:
  topics:
    - name: sarex.stage.myservice.events.v1
      owner: myservice          # kafka-user, которому принадлежит топик
      clusterRef: stage
      partitions: 1
      replicationFactor: 1
      config: { cleanup.policy: delete, retention.ms: "604800000", min.insync.replicas: "1" }
      deletionPolicy: orphan     # orphan = не удалять топик при удалении из конфига
  users:
    # (A) НОВЫЙ юзер — пароль генерирует terraform (random_password):
    - name: myservice
      clusterRef: stage
      permissions:
        - topic: sarex.stage.myservice.events.v1
          roles: ["ACCESS_ROLE_PRODUCER"]     # PRODUCER | CONSUMER | ADMIN
    # (B) юзер, чей пароль берётся из существующего k8s-секрета (adopt/переезд):
    - name: checklists
      clusterRef: stage
      passwordSource: { namespace: proc, secret: checklists-kafka-secret, key: password }
      permissions: []

На kafka-юзере стоит prevent_destroy и ignore_changes = [password] (реальный пароль в кластере не перезаписывается сгенерированным).

rabbitmq

RabbitMQ через провайдер cyrilgdn/rabbitmq (юнит rabbitmq, модуль rabbitmq): vhosts, users, permissions, topic_permissions, exchanges, queues, bindings, policies. Требует доступ к Management API кластера (обычно только с CI-раннера).

rabbitmq:
  policy: { allow_delete: false }
  vhosts:  [ { name: app_stage } ]
  users:   [ { name: app, tags: [], password_length: 32 } ]
  permissions:
    - { vhost: app_stage, user: app, configure: ".*", write: ".*", read: ".*" }
  exchanges: [ { vhost: app_stage, name: app.events, type: topic, durable: true } ]
  queues:    [ { vhost: app_stage, name: app.events.q, durable: true } ]
  bindings:
    - { vhost: app_stage, source: app.events, destination: app.events.q, destination_type: queue, routing_key: "#" }
  policies:
    - { vhost: app_stage, name: ttl, pattern: ".*", apply_to: queues, priority: 0, definition: { message-ttl: 86400000 } }

secrets

Kubernetes secrets (юнит secrets, модуль k8s-secret). Подробно — ниже.


Секреты подробно

Секрет описывается в secrets: и собирает data из одного или нескольких источников. Ключ секрета для оверрайдов/импорта = resource_key (если задан) иначе name.

Типы (type) и их «дефолтные» поля (когда credential_keys не заданы — берутся все):

type откуда data
database host, port, database, username, password, ca.crt (из database-модуля)
rabbitmq host, hostname, port, vhost, user, username, password, uri, management_endpoint
kafka host, hostname, port, username, user, password, sasl_mechanism, security_protocol, bootstrap_server(s), bootstrap_servers_json
s3 access_key, secret_key, bucket, endpoint
yc_sa access_key, secret_key, service_account_id, endpoint
dockerconfigjson .dockerconfigjson из DOCKER_REGISTRY_USERNAME/PASSWORD
opaque только из custom/constant/random/secret_values

Способы наполнить data (можно комбинировать, если не используется secret_values):

  • dependencies + (опц.) credential_keys — тянуть значения из outputs kafka/rabbitmq/database/s3. credential_keys = { имя_ключа_в_секрете: имя_поля_источника }, позволяет переименовать/выбрать подмножество.
  • random_keys — сгенерировать значения (random_password), напр. токены/сессии.
  • custom_keysнесекретные статические значения прямо в yaml (host, url, флаги).
  • constant_keys — значения из общих constants (kafka_ca, postgres_ca, postgres_port, s3_endpoint).
  • secret_values (sops) — секретные явные значения 1:1 (см. ниже). Оверрайд на весь секрет.

Пример (kafka-секрет с выбором и переименованием полей + CA из constants):

- name: cde-api-kafka-secret
  namespace: documentations
  type: kafka
  dependencies: { kafka_ref: stage, kafka_user: pdm-outbox-relay }
  credential_keys:
    host: host
    port: port
    username: username
    password: password
    sasl_mechanism: sasl_mechanism
    security_protocol: security_protocol
  constant_keys: { ssl_cafile: kafka_ca }

lifecycle.ignore_changes: true — терраформ управляет ресурсом, но не сверяет data (используется для секретов, которые наполняет кто-то ещё, напр. helm).

adopt: true — существующий, созданный руками секрет заводится под управление terraform 1:1 как есть: его текущая .data берётся из infrastructure-secrets.yaml (secret_values) и не меняется. См. Рецепты.


Секретные значения (sops)

Секретные значения не хранятся в infrastructure.yaml. Они лежат в зашифрованном infrastructure-secrets.yaml:

environments:
  stage:
    secret_values:
      <resource_key>:
        <data_key>: <base64(raw_value)>   # base64 = как в k8s .data
  • Шифрование: .sops.yaml → age-recipient (тот же, что в terraform-contour). Приватный ключ — в CI-переменной SOPS_AGE_KEY (в репу не коммитится). Подробности и команды — в SOPS.md.
  • В CI job'ы *-secrets делают sops --decrypt во временный файл и передают путь через INFRA_SECRET_VALUES_FILE. Локально terragrunt расшифровывает сам, если доступен SOPS_AGE_KEY.
  • Если для resource_key есть запись в secret_values — вся data секрета берётся оттуда (computed/random по этому секрету игнорируется). Это и «adopt как есть», и «полностью явный секрет».

Редактировать: sops infrastructure-secrets.yaml. Зашифровать заново после ручной правки: sops -e -i infrastructure-secrets.yaml. Расшифрованный файл никогда не коммитить.gitignore).


Рецепты — как завести новую сущность

Новый namespace / bucket / database / kafka-topic / rabbitmq-vhost

  1. Добавь запись в соответствующий раздел infrastructure.yaml (см. примеры выше).
  2. planapply соответствующего юнита (или через CI).

Новый kafka/rabbitmq пользователь (пароль генерит terraform)

  1. Добавь в kafka.users / rabbitmq.users без passwordSource → terraform создаст random_password.
  2. Заведи секрет с этим юзером, чтобы приложение получило креды (см. следующий рецепт).

Секрет с динамически сгенерированными паролями

- name: my-secret
  namespace: myns
  type: opaque
  random_keys:
    password: { length: 32, special: false }
    token:    { length: 48 }

adopt не нужен. Значения сгенерируются и сохранятся в state.

Секрет из инфраструктуры (kafka/rabbitmq/db/s3)

- name: my-kafka-secret
  namespace: myns
  type: kafka
  dependencies: { kafka_ref: stage, kafka_user: myservice }
  # credential_keys опционально — если нужно подмножество/переименование

Секрет с чётко указанными полями

  • Несекретные поля → custom_keys в infrastructure.yaml.
  • Секретные поля → в infrastructure-secrets.yaml:
    1. sops infrastructure-secrets.yaml
    2. добавь environments.stage.secret_values.<resource_key>.<key>: <base64> (printf '%s' 'значение' | base64)
    3. заведи сам секрет в infrastructure.yaml (type: opaque, тот же resource_key/name).

Нюанс: secret_values перекрывает всю data секрета. Смешать «часть из outputs + одно явное секретное поле» одним secret_values нельзя — нужен либо custom_keys (если не секрет), либо доработка модуля (merge-режим).

Взять существующий секрет под terraform (adopt 1:1)

  1. Захвати текущую .data из кластера (base64 берётся как есть):
    kubectl --context yc-sarex-stage -n <ns> get secret <name> -o jsonpath='{.data}'
    
  2. Впиши значения в infrastructure-secrets.yaml под secret_values.<resource_key> и sops -e -i.
  3. В infrastructure.yaml добавь секрет с adopt: true (генерация import-блоков — в live/stage/secrets/terragrunt.hcl).
  4. plan → ожидаемо import, 0 change по data.

Запуск

Локально

# окружение (см. также «Траблшутинг» про формат .env со встроенным SA-ключом)
export S3_ACCESS_KEY=... S3_SECRET_KEY=...
export YC_SERVICE_ACCOUNT_KEY_FILE=/path/to/sa-key.json   # ключ SA (файл или JSON-содержимое)
export YC_STAGE_FOLDER_ID=...
export KUBECONFIG=$HOME/.kube/config
export KUBE_CONTEXT=yc-sarex-stage          # ВАЖНО: иначе возьмётся текущий контекст (часто prod!)
export SOPS_AGE_KEY='AGE-SECRET-KEY-...'    # для расшифровки secret_values

cd live/stage/<unit>
terragrunt init
terragrunt plan
terragrunt apply

rabbitmq и secrets требуют сетевого доступа к кластеру/RabbitMQ Management API — с локальной машины план этих юнитов может не подняться (нет доступа к in-cluster RabbitMQ). Прогоняй в CI.

Провайдеры/бэкенд

  • State: S3 tfstate-terragrunt-stage, ключ stage/<unit>/terraform.tfstate (versioning включён).
  • Бакет без SSE и не поддерживает часть AWS-проверок → в live/terragrunt.hcl стоят skip_bucket_ssencryption/skip_bucket_versioning/… (не убирать skip_bucket_ssencryption).

CI/CD

  • .gitlab-ci.yml (родительский): job generate-pipeline вызывает scripts/generate-pipeline.sh, который по папкам live/**/terragrunt.hcl генерит дочерний pipeline (validate → plan → apply на юнит), затем trigger-downstream его запускает.
  • Образ раннера: cr.yandex/.../terraform/terragrunt:v9.11 (terraform 1.5.7 + terragrunt 0.93.11 + sops), собирается из Dockerfile. Сейчас пересобирается вручную (docker build --platform linux/amd64 …
    • docker push); тег задаётся в .gitlab-ci.yml.
  • Секреты в CI (переменные): S3_ACCESS_KEY/SECRET, YC_*_FOLDER_ID, YC_SERVICE_ACCOUNT_KEY_FILE, SOPS_AGE_KEY, DOCKER_REGISTRY_*, SAREX_REGISTRY_KEY (для пуша образа).
  • apply — ручной (manual) в pipeline.

Бэкап и откат

Перед массовым apply (особенно adopt) делай бэкап:

  • k8s-секреты: kubectl ... get secret <name> -o yaml → файл (для отката kubectl apply -f).
  • state: aws --endpoint-url=https://storage.yandexcloud.net s3 cp s3://tfstate-terragrunt-stage/stage/<unit>/terraform.tfstate ./backup/.
  • versioning бакета включён → предыдущую версию state можно достать через aws s3api list-object-versions … / get-object --version-id ….

Откат данных секрета: kubectl --context yc-sarex-stage apply -f <backup>.yaml.


Траблшутинг

  • error checking if SSE is enabled for AWS S3 bucket … 404 — свежий terragrunt проверяет SSE бакета стейта, которого у Object Storage нет. Лечится флагами skip_bucket_ssencryption/ в live/terragrunt.hcl (уже стоят). Не удаляй их.
  • secrets plan показывает N change с ~ data — не подгрузились secret_values (нет SOPS_AGE_KEY/INFRA_SECRET_VALUES_FILE) → модуль ушёл в computed. Проверь ключ sops.
  • + wait_for_service_account_token = true на импортируемых секретах — служебный флаг провайдера, к данным отношения не имеет; в модуле выставлен false, чтобы не шуметь.
  • kubectl берёт не тот кластер — задай KUBE_CONTEXT=yc-sarex-stage (дефолтный контекст часто prod).
  • .env со встроенным SA-ключом — если локальный .env держит JSON SA-ключа инлайном после YC_SERVICE_ACCOUNT_KEY_FILE=, source .env сломается: вынеси JSON в отдельный файл и укажи путь.
  • Дрейф версий образа — версия terragrunt/terraform в Dockerfile должна совпадать с тем, что реально в теге образа; расхождение уже ломало init (SSE-проверка). Держи их синхронно.

Требования

  • Terraform 1.5.7 / Terragrunt 0.93.x (в образе); OpenTofu локально не резолвит провайдер yandex.
  • Провайдеры: yandex-cloud/yandex, hashicorp/kubernetes, hashicorp/random, cyrilgdn/rabbitmq.
  • sops + age-ключ (SOPS_AGE_KEY) для secret_values.
  • Доступ: S3 (state), YC SA-ключ, kubeconfig на yc-sarex-stage, RabbitMQ Management API (для юнита rabbitmq).