terraform-contour-mirror/README.md
2026-07-07 17:10:06 +03:00

401 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<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`).
```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:
<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:
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.<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
# окружение (см. также «Траблшутинг» про формат .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).