mirror of
https://gitlab.sarex.io/infra/terraform-contour-mirror.git
synced 2026-08-05 18:31:00 +03:00
401 lines
22 KiB
Markdown
401 lines
22 KiB
Markdown
# 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).
|