++ adopt prod

This commit is contained in:
Kochetkov S 2026-07-07 17:10:06 +03:00
parent 84600ee61b
commit 8d12527af4
12 changed files with 1924 additions and 442 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ override.tf.json
*_override.tf.json
.terraformrc
terraform.rc
.claude/
# Terragrunt
.terragrunt-cache/

542
README.md
View File

@ -1,141 +1,125 @@
# Terraform Infrastructure
# Terraform Infrastructure (stage)
Infrastructure as Code для управления ресурсами Yandex Cloud и Kubernetes.
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 # Конфигурация всех ресурсов (единый источник правды)
├── live/ # Terragrunt конфигурации по окружениям
│ ├── terragrunt.hcl # Общие настройки (backend, providers)
│ └── stage/ # Stage окружение
│ ├── env.hcl
│ ├── namespace/
│ ├── database/
│ ├── s3/
│ └── secrets/
└── modules/ # Terraform модули
├── k8s-namespace/ # Создание Kubernetes namespaces
├── k8s-secret/ # Создание Kubernetes secrets (с зависимостями)
├── k8s-secrets/ # Простое создание secrets
├── yc-database/ # PostgreSQL users и databases в Yandex Cloud
├── yc-valkey-user/ # Valkey/Redis users в Yandex Cloud
├── 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`).
### infrastructure.yaml
---
Единый файл конфигурации для всех ресурсов. Структура:
## Как это работает
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
environments:
stage: # Имя окружения
namespaces: # Kubernetes namespaces
namespaces:
- name: pulse
labels: {}
annotations: {}
labels: { environment: stage }
annotations: { managed-by: terraform }
```
buckets: # S3 бакеты
### 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 # опционально
versioning:
enabled: false
role: storage.uploader # storage.uploader(def) | storage.viewer | storage.editor
versioning: { enabled: false }
cors:
enabled: true
allowed_methods: [GET, PUT]
databases: # PostgreSQL databases
- cluster_id: "xxx"
database:
name: mydb
extensions: [pg_stat_statements]
user:
name: myuser
password_length: 32
conn_limit: 10
valkey_users: # Valkey/Redis users
- cluster_id: "xxx"
host: rc1a-example.mdb.yandexcloud.net
user:
name: myuser
permissions:
patterns: allkeys
pubSubChannels: allchannels
secrets: # Kubernetes secrets
- name: my-secret
namespace: pulse
type: database # database | database_url | valkey | s3 | dockerconfigjson | opaque
dependencies:
cluster: "xxx"
db: mydb
user: myuser
yc_service_accounts: # доп. YC SA (не привязанные к бакету), опционально
- name: app-external-sa
description: External integration SA
folder_roles: [storage.viewer]
create_static_access_key: true
```
### Terragrunt
### yc_service_accounts
Дополнительные YC Service Accounts с folder-ролями и (опц.) статическим ключом. Создаются в юните `s3`,
их данные доступны секретам типа `yc_sa`.
Terragrunt читает `infrastructure.yaml` и передаёт данные в модули:
```hcl
locals {
infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml"))
env_config = local.infra_config.environments[local.env_name]
}
inputs = {
buckets = local.env_config.buckets
}
```
### Порядок применения
Модули имеют зависимости и применяются в порядке:
```
namespace → database / s3 / valkey-users → secrets
```
## Модули
### yc-s3
Создание S3 бакетов с изолированным доступом.
**Особенности:**
- Каждый бакет получает отдельный Service Account
- SA не имеет глобальных IAM ролей на storage на уровне folder
- Доступ к бакету через IAM binding (`yandex_storage_bucket_iam_binding`)
- SA может работать только со своим бакетом
**Роли (Yandex Cloud IAM):**
- `storage.uploader` (default) — загрузка объектов
- `storage.viewer` — чтение объектов
- `storage.editor` — полный доступ
**Пример:**
```yaml
buckets:
- name: my-bucket
acl: private
role: storage.uploader # опционально
```
### yc-database
Создание PostgreSQL users и databases в существующем кластере.
**Особенности:**
- Пароль генерируется автоматически
- Поддержка extensions
- Поддержка permissions на другие БД
- `ignore_changes` на password
**Пример:**
### databases
PostgreSQL users и databases в существующем кластере YC (юнит `database`, модуль `yc-database`).
Пароль генерируется автоматически, на password стоит `ignore_changes`.
```yaml
databases:
- cluster_id: "c9qa2coo5ukgcg93fldm"
@ -146,129 +130,271 @@ databases:
name: myuser
password_length: 32
conn_limit: 10
permissions: [other_db]
permissions: [other_db] # доступ к другим БД, опционально
```
### yc-valkey-user
### kafka
YC Managed Kafka: **topics, users, permissions** (юнит `kafka-topics`, модуль `kafka-topics-yc`).
Кластеры описаны в `kafka_cluster_refs` (id/host/port/sasl/…); в сущностях ссылаешься по `clusterRef`.
Создание Valkey/Redis users в существующем кластере Yandex Cloud.
**Особенности:**
- Пароль генерируется автоматически
- Поддержка ACL permissions
- `ignore_changes` на password
**Пример:**
```yaml
valkey_users:
- cluster_id: "c9qa2coo5ukgcg93fldm"
host: rc1a-example.mdb.yandexcloud.net
port: "6380"
user:
name: myuser
password_length: 32
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:
patterns: allkeys
pubSubChannels: allchannels
categories: "+@read +@write"
commands: "+GET -FLUSHALL"
sanitizePayload: sanitize-payload
- 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 } }
```
### k8s-secret
### secrets
Kubernetes secrets (юнит `secrets`, модуль `k8s-secret`). Подробно — ниже.
Создание Kubernetes secrets с поддержкой зависимостей.
---
**Типы секретов:**
- `database` — host, port, database, username, password, ca.crt
- `database_url` — database_url в формате postgresql+asyncpg
- `valkey` — cert, host, login, password, port, url
- `s3` — access_key, secret_key, bucket, endpoint
- `dockerconfigjson` — credentials для container registry
- `opaque` — произвольные данные
## Секреты подробно
**Особенности:**
- Автоматическое получение данных из database/s3 модулей
- Поддержка `random_keys` для генерации паролей
- Поддержка `custom_keys` для статических значений
- `ignore_changes` для предотвращения перезаписи
Секрет описывается в `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
secrets:
- name: my-db-secret
namespace: pulse
type: database
dependencies:
cluster: "c9qa2coo5ukgcg93fldm"
db: mydb
user: myuser
- 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:
<resource_key>:
<data_key>: <base64(raw_value)> # 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:
session_key:
length: 32
special: false
lifecycle:
ignore_changes: true
password: { length: 32, special: false }
token: { length: 48 }
```
`adopt` не нужен. Значения сгенерируются и сохранятся в state.
### k8s-namespace
Создание Kubernetes namespaces.
**Пример:**
### Секрет из инфраструктуры (kafka/rabbitmq/db/s3)
```yaml
namespaces:
- name: pulse
labels:
environment: stage
annotations:
managed-by: terraform
- 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 берётся как есть):
```bash
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.
---
## Запуск
### Локально
```bash
cd terraform/live/stage/s3
# окружение (см. также «Траблшутинг» про формат .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`).
```bash
cd terraform/live/stage
terragrunt run-all plan
terragrunt run-all apply
```
### Переменные окружения
```bash
export YC_TOKEN="..." # IAM токен (или YC_SERVICE_ACCOUNT_KEY_FILE)
export YC_CLOUD_ID="..."
export YC_FOLDER_ID="..." # Default folder
export YC_STAGE_FOLDER_ID="..." # Stage folder (опционально)
export KUBECONFIG="~/.kube/config"
export KUBE_CONTEXT="stage"
export S3_ACCESS_KEY="..." # Для terraform state
export S3_SECRET_KEY="..."
export DOCKER_REGISTRY_USERNAME="..." # Для dockerconfigjson secrets
export DOCKER_REGISTRY_PASSWORD="..."
```
---
## CI/CD
Pipeline автоматически:
1. Читает изменённые файлы
2. Определяет затронутые модули
3. Запускает `terragrunt plan` / `terragrunt apply`
- `.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.0
- Terragrunt >= 0.45
- Yandex Cloud провайдер ~> 0.100
- IAM токен с правами `storage.admin` для создания бакетов и bucket policy
- 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).

240
infrastructure-secrets.yaml Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -35,10 +35,13 @@ inputs = {
vhosts = try(local.rabbitmq_config.vhosts, [])
users = try(local.rabbitmq_config.users, [])
permissions = try(local.rabbitmq_config.permissions, [])
topic_permissions = try(local.rabbitmq_config.topic_permissions, [])
exchanges = try(local.rabbitmq_config.exchanges, [])
queues = try(local.rabbitmq_config.queues, [])
bindings = try(local.rabbitmq_config.bindings, [])
policies = try(local.rabbitmq_config.policies, [])
unmanaged_output_users = try(local.rabbitmq_config.unmanaged_output_users, [])
unmanaged_output_permissions = try(local.rabbitmq_config.unmanaged_output_permissions, [])
yc_token = get_env("YC_TOKEN", "")
yc_service_account_key_file = get_env("YC_SERVICE_ACCOUNT_KEY_FILE", "")

View File

@ -49,6 +49,15 @@ dependency "rabbitmq" {
skip_outputs = get_env("TG_SKIP_DEPENDENCY_OUTPUTS", "false") == "true"
}
dependency "kafka_topics" {
config_path = "../kafka-topics"
mock_outputs = {
kafka_outputs_map = {}
}
mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"]
skip_outputs = get_env("TG_SKIP_DEPENDENCY_OUTPUTS", "false") == "true"
}
dependency "valkey_users" {
config_path = "../valkey-users"
mock_outputs = {
@ -136,6 +145,51 @@ EOT
terraform {
source = "${get_terragrunt_dir()}/../../../modules//k8s-secret"
extra_arguments "infrastructure_tfvars" {
commands = get_terraform_commands_that_need_vars()
arguments = ["-var-file=terragrunt.auto.tfvars.json"]
}
}
generate "import_existing" {
path = "import_existing.tf"
if_exists = "overwrite"
contents = join("\n\n", [
for secret in local.env_config.secrets : <<-EOF
import {
to = kubernetes_secret.without_ignore["${try(secret.resource_key, "") != "" ? secret.resource_key : secret.name}"]
id = "${secret.namespace}/${secret.name}"
}
EOF
if try(secret.adopt, false)
])
}
generate "tfvars" {
path = "terragrunt.auto.tfvars.json"
if_exists = "overwrite"
disable_signature = true
contents = jsonencode({
secrets = [
for secret in local.env_config.secrets : {
resource_key = try(secret.resource_key, "")
name = secret.name
namespace = secret.namespace
secret_type = secret.type
registry_url = try(secret.registry_url, "")
dependencies = try(secret.dependencies, {})
yc_sa_key_names = try(secret.yc_sa_key_names, {})
custom_keys = try(secret.custom_keys, {})
constant_keys = try(secret.constant_keys, {})
random_keys = try(secret.random_keys, {})
credential_keys = try(secret.credential_keys, {})
labels = try(secret.labels, {})
annotations = try(secret.annotations, {})
ignore_changes = try(secret.lifecycle.ignore_changes, false)
}
]
})
}
inputs = {
@ -172,12 +226,14 @@ inputs = {
database_outputs_map = dependency.database.outputs.database_outputs_map
rabbitmq_outputs_map = try(dependency.rabbitmq.outputs.rabbitmq_outputs_map, {})
kafka_outputs_map = try(dependency.kafka_topics.outputs.kafka_outputs_map, {})
valkey_outputs_map = dependency.valkey_users.outputs.valkey_outputs_map
constants = {
s3_endpoint = "https://storage.yandexcloud.net"
postgres_port = "6432"
postgres_ca = local.yc_postgresql_ca_cert
kafka_ca = local.yc_postgresql_ca_cert
valkey_port = "6380"
valkey_ca = local.yc_postgresql_ca_cert
}

View File

@ -287,7 +287,7 @@ resource "kubernetes_secret" "with_ignore" {
lifecycle {
# Игнорируем data, type и metadata чтобы не было phantom updates
ignore_changes = [data, type, metadata]
ignore_changes = [data, type, metadata, wait_for_service_account_token]
}
depends_on = [random_password.secrets]
@ -312,7 +312,7 @@ resource "kubernetes_secret" "without_ignore" {
lifecycle {
# type и metadata игнорируем чтобы не было phantom updates
ignore_changes = [type, metadata]
ignore_changes = [type, metadata, wait_for_service_account_token]
}
depends_on = [random_password.secrets]

View File

@ -7,3 +7,22 @@ output "user_names" {
description = "Kafka users managed by this service manifest state."
value = [for user in yandex_mdb_kafka_user.this : user.name]
}
output "kafka_outputs_map" {
description = "Map for k8s-secret module. Key format: clusterRef:user."
value = {
for key, item in local.user_clusters : key => {
host = try(var.kafka_cluster_refs[item.cluster_ref].host, "")
hostname = try(format("%s:%s", var.kafka_cluster_refs[item.cluster_ref].host, tostring(var.kafka_cluster_refs[item.cluster_ref].port)), "")
port = tostring(try(var.kafka_cluster_refs[item.cluster_ref].port, 9091))
username = item.user
password = try(random_password.kafka_user[key].result, "")
sasl_mechanism = try(var.kafka_cluster_refs[item.cluster_ref].sasl_mechanism, "SCRAM-SHA-512")
security_protocol = try(var.kafka_cluster_refs[item.cluster_ref].security_protocol, "SASL_SSL")
bootstrap_server = try(format("%s:%s", var.kafka_cluster_refs[item.cluster_ref].host, tostring(var.kafka_cluster_refs[item.cluster_ref].port)), "")
bootstrap_servers = try(format("%s:%s", var.kafka_cluster_refs[item.cluster_ref].host, tostring(var.kafka_cluster_refs[item.cluster_ref].port)), "")
bootstrap_servers_json = try(jsonencode([format("%s:%s", var.kafka_cluster_refs[item.cluster_ref].host, tostring(var.kafka_cluster_refs[item.cluster_ref].port))]), "[]")
}
}
sensitive = true
}

View File

@ -37,6 +37,10 @@ variable "kafka_cluster_refs" {
description = "Environment clusterRef mapping from infrastructure.yaml."
type = map(object({
cluster_id = string
host = optional(string, "")
port = optional(number, 9091)
sasl_mechanism = optional(string, "SCRAM-SHA-512")
security_protocol = optional(string, "SASL_SSL")
default_partitions = optional(number, 3)
default_replication_factor = optional(number, 1)
max_replication_factor = optional(number, 1)

View File

@ -1,5 +1,5 @@
locals {
rabbitmq_enabled = length(var.vhosts) + length(var.users) + length(var.permissions) + length(var.exchanges) + length(var.queues) + length(var.bindings) + length(var.policies) > 0
rabbitmq_enabled = length(var.vhosts) + length(var.users) + length(var.permissions) + length(var.topic_permissions) + length(var.exchanges) + length(var.queues) + length(var.bindings) + length(var.policies) > 0
management_endpoint = local.rabbitmq_enabled ? var.management_endpoint : "http://127.0.0.1:15672"
management_username = local.rabbitmq_enabled ? var.management_username : "noop"
@ -8,6 +8,8 @@ locals {
vhosts = {
for vhost in var.vhosts : vhost.name => {
name = vhost.name
description = try(vhost.description, null)
tracing = try(vhost.tracing, false)
}
}
@ -30,6 +32,14 @@ locals {
}
}
topic_permissions = {
for permission in var.topic_permissions : "${permission.vhost}:${permission.user}" => {
vhost = permission.vhost
user = permission.user
permissions = try(permission.permissions, [])
}
}
exchanges = {
for exchange in var.exchanges : "${exchange.vhost}:${exchange.name}" => {
name = exchange.name
@ -52,7 +62,7 @@ locals {
}
bindings = {
for idx, binding in var.bindings : "${binding.vhost}:${binding.source}:${binding.destination_type}:${binding.destination}:${try(binding.routing_key, "")}:${idx}" => {
for binding in var.bindings : "${binding.vhost}:${binding.source}:${binding.destination_type}:${binding.destination}:${try(binding.properties_key, format("%s:%s", try(binding.routing_key, ""), sha1(jsonencode(try(binding.arguments, {})))))}" => {
vhost = binding.vhost
source = binding.source
destination = binding.destination
@ -93,6 +103,8 @@ resource "rabbitmq_vhost" "this" {
for_each = local.vhosts
name = each.value.name
description = each.value.description
tracing = each.value.tracing
lifecycle {
prevent_destroy = true
@ -139,6 +151,26 @@ resource "rabbitmq_permissions" "this" {
}
}
resource "rabbitmq_topic_permissions" "this" {
for_each = local.topic_permissions
user = rabbitmq_user.this[each.value.user].name
vhost = rabbitmq_vhost.this[each.value.vhost].name
dynamic "permissions" {
for_each = each.value.permissions
content {
exchange = permissions.value.exchange
write = tostring(try(permissions.value.write, ""))
read = tostring(try(permissions.value.read, ""))
}
}
lifecycle {
prevent_destroy = true
}
}
resource "rabbitmq_exchange" "this" {
for_each = local.exchanges
@ -177,7 +209,7 @@ resource "rabbitmq_queue" "this" {
resource "rabbitmq_binding" "this" {
for_each = local.bindings
source = rabbitmq_exchange.this["${each.value.vhost}:${each.value.source}"].name
source = each.value.source
vhost = rabbitmq_vhost.this[each.value.vhost].name
destination = each.value.destination
destination_type = each.value.destination_type
@ -187,6 +219,8 @@ resource "rabbitmq_binding" "this" {
lifecycle {
prevent_destroy = true
}
depends_on = [rabbitmq_exchange.this, rabbitmq_queue.this]
}
resource "rabbitmq_policy" "this" {

View File

@ -5,19 +5,24 @@ output "vhosts" {
output "users" {
description = "RabbitMQ users managed by this component."
value = keys(rabbitmq_user.this)
value = sort(concat(keys(rabbitmq_user.this), tolist(var.unmanaged_output_users)))
}
output "rabbitmq_outputs_map" {
description = "Map for k8s-secret module. Key format: default:vhost:user."
value = {
for key, item in local.user_vhosts : "default:${item.vhost}:${item.user}" => {
for key, item in merge(local.user_vhosts, {
for item in var.unmanaged_output_permissions : "${item.vhost}:${item.user}" => {
vhost = item.vhost
user = item.user
}
}) : "default:${item.vhost}:${item.user}" => {
host = var.amqp_host
port = tostring(var.amqp_port)
vhost = item.vhost
username = rabbitmq_user.this[item.user].name
password = random_password.user[item.user].result
uri = "amqp://${rabbitmq_user.this[item.user].name}:${urlencode(random_password.user[item.user].result)}@${var.amqp_host}:${var.amqp_port}/${urlencode(item.vhost)}"
username = try(rabbitmq_user.this[item.user].name, item.user)
password = try(random_password.user[item.user].result, "")
uri = try("amqp://${rabbitmq_user.this[item.user].name}:${urlencode(random_password.user[item.user].result)}@${var.amqp_host}:${var.amqp_port}/${urlencode(item.vhost)}", "")
management_endpoint = var.management_endpoint
}
}

View File

@ -43,43 +43,61 @@ variable "rabbitmq_policy" {
variable "vhosts" {
description = "RabbitMQ vhosts declared in infrastructure.yaml."
type = list(any)
type = any
default = []
}
variable "users" {
description = "RabbitMQ users declared in infrastructure.yaml."
type = list(any)
type = any
default = []
}
variable "permissions" {
description = "RabbitMQ permissions declared in infrastructure.yaml."
type = list(any)
type = any
default = []
}
variable "topic_permissions" {
description = "RabbitMQ topic permissions declared in infrastructure.yaml."
type = any
default = []
}
variable "exchanges" {
description = "RabbitMQ exchanges declared in infrastructure.yaml."
type = list(any)
type = any
default = []
}
variable "queues" {
description = "RabbitMQ queues declared in infrastructure.yaml."
type = list(any)
type = any
default = []
}
variable "bindings" {
description = "RabbitMQ bindings declared in infrastructure.yaml."
type = list(any)
type = any
default = []
}
variable "policies" {
description = "RabbitMQ policies declared in infrastructure.yaml."
type = list(any)
type = any
default = []
}
variable "unmanaged_output_users" {
description = "Existing RabbitMQ users intentionally not managed, kept only for output compatibility."
type = any
default = []
}
variable "unmanaged_output_permissions" {
description = "Existing RabbitMQ user/vhost pairs intentionally not managed, kept only for output compatibility."
type = any
default = []
}