# Terraform Infrastructure (stage) Infrastructure as Code для управления ресурсами **Yandex Cloud** и **Kubernetes** окружения `stage`. Всё описывается декларативно в одном файле `infrastructure.yaml`; секретные значения — в зашифрованном `infrastructure-secrets.yaml` (sops). Terragrunt читает конфиг и раскладывает по модулям. > TL;DR для «мне нужно задеплоить X»: см. раздел [Рецепты](#рецепты--как-завести-новую-сущность). --- ## Содержание - [Структура репозитория](#структура-репозитория) - [Как это работает](#как-это-работает) - [Сущности `infrastructure.yaml`](#сущности-infrastructureyaml) - [namespaces](#namespaces) · [buckets (S3)](#buckets-s3) · [yc_service_accounts](#yc_service_accounts) · [databases](#databases) · [kafka](#kafka) · [rabbitmq](#rabbitmq) · [secrets](#secrets) - [Секреты подробно](#секреты-подробно) - [Секретные значения (sops)](#секретные-значения-sops) - [Рецепты](#рецепты--как-завести-новую-сущность) - [Запуск](#запуск) - [CI/CD](#cicd) - [Бэкап и откат](#бэкап-и-откат) - [Траблшутинг](#траблшутинг) - [Требования](#требования) --- ## Структура репозитория ``` 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//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`). ```yaml 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`). ```yaml 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`. ```yaml 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`. ```yaml 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-раннера). ```yaml 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):** ```yaml - 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) и не меняется. См. [Рецепты](#взять-существующий-секрет-под-terraform-adopt-11). --- ## Секретные значения (sops) Секретные значения **не хранятся** в `infrastructure.yaml`. Они лежат в зашифрованном `infrastructure-secrets.yaml`: ```yaml environments: stage: secret_values: : : # base64 = как в k8s .data ``` - Шифрование: `.sops.yaml` → age-recipient (тот же, что в `terraform-contour`). Приватный ключ — в CI-переменной `SOPS_AGE_KEY` (в репу не коммитится). Подробности и команды — в [SOPS.md](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. `plan` → `apply` соответствующего юнита (или через CI). ### Новый kafka/rabbitmq пользователь (пароль генерит terraform) 1. Добавь в `kafka.users` / `rabbitmq.users` **без** `passwordSource` → terraform создаст `random_password`. 2. Заведи секрет с этим юзером, чтобы приложение получило креды (см. следующий рецепт). ### Секрет с динамически сгенерированными паролями ```yaml - name: my-secret namespace: myns type: opaque random_keys: password: { length: 32, special: false } token: { length: 48 } ``` `adopt` не нужен. Значения сгенерируются и сохранятся в state. ### Секрет из инфраструктуры (kafka/rabbitmq/db/s3) ```yaml - 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..: ` (`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 берётся как есть): ```bash kubectl --context yc-sarex-stage -n get secret -o jsonpath='{.data}' ``` 2. Впиши значения в `infrastructure-secrets.yaml` под `secret_values.` и `sops -e -i`. 3. В `infrastructure.yaml` добавь секрет с `adopt: true` (генерация import-блоков — в `live/stage/secrets/terragrunt.hcl`). 4. `plan` → ожидаемо `import, 0 change` по data. --- ## Запуск ### Локально ```bash # окружение (см. также «Траблшутинг» про формат .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/ terragrunt init terragrunt plan terragrunt apply ``` > `rabbitmq` и `secrets` требуют сетевого доступа к кластеру/RabbitMQ Management API — с локальной > машины план этих юнитов может не подняться (нет доступа к in-cluster RabbitMQ). Прогоняй в CI. ### Провайдеры/бэкенд - State: S3 `tfstate-terragrunt-stage`, ключ `stage//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 -o yaml` → файл (для отката `kubectl apply -f`). - **state**: `aws --endpoint-url=https://storage.yandexcloud.net s3 cp s3://tfstate-terragrunt-stage/stage//terraform.tfstate ./backup/`. - **versioning бакета включён** → предыдущую версию state можно достать через `aws s3api list-object-versions … / get-object --version-id …`. Откат данных секрета: `kubectl --context yc-sarex-stage apply -f .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).