| live | ||
| modules | ||
| scripts | ||
| .gitignore | ||
| .gitlab-ci.yml | ||
| .sops.yaml | ||
| Dockerfile | ||
| infrastructure-secrets.yaml | ||
| infrastructure.yaml | ||
| README.md | ||
Terraform Infrastructure (stage)
Infrastructure as Code для управления ресурсами Yandex Cloud и Kubernetes окружения stage.
Всё описывается декларативно в одном файле infrastructure.yaml; секретные значения — в зашифрованном infrastructure-secrets.yaml (sops). Terragrunt читает конфиг и раскладывает по модулям.
TL;DR для «мне нужно задеплоить X»: см. раздел Рецепты.
Содержание
- Структура репозитория
- Как это работает
- Сущности
infrastructure.yaml- namespaces · buckets (S3) · yc_service_accounts · databases · kafka · rabbitmq · secrets
- Секреты подробно
- Секретные значения (sops)
- Рецепты
- Запуск
- CI/CD
- Бэкап и откат
- Траблшутинг
- Требования
Структура репозитория
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).
Как это работает
- Ты правишь
infrastructure.yaml(и при необходимостиinfrastructure-secrets.yamlчерез sops). - Terragrunt в нужном юните читает конфиг:
yamldecode(file(".../infrastructure.yaml")).environments.stage. - Передаёт срез в соответствующий модуль как
inputs. 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
- Добавь запись в соответствующий раздел
infrastructure.yaml(см. примеры выше). plan→applyсоответствующего юнита (или через CI).
Новый kafka/rabbitmq пользователь (пароль генерит terraform)
- Добавь в
kafka.users/rabbitmq.usersбезpasswordSource→ terraform создастrandom_password. - Заведи секрет с этим юзером, чтобы приложение получило креды (см. следующий рецепт).
Секрет с динамически сгенерированными паролями
- 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:sops infrastructure-secrets.yaml- добавь
environments.stage.secret_values.<resource_key>.<key>: <base64>(printf '%s' 'значение' | base64) - заведи сам секрет в
infrastructure.yaml(type: opaque, тот жеresource_key/name).
Нюанс:
secret_valuesперекрывает всюdataсекрета. Смешать «часть из outputs + одно явное секретное поле» однимsecret_valuesнельзя — нужен либоcustom_keys(если не секрет), либо доработка модуля (merge-режим).
Взять существующий секрет под terraform (adopt 1:1)
- Захвати текущую
.dataиз кластера (base64 берётся как есть):kubectl --context yc-sarex-stage -n <ns> get secret <name> -o jsonpath='{.data}' - Впиши значения в
infrastructure-secrets.yamlподsecret_values.<resource_key>иsops -e -i. - В
infrastructure.yamlдобавь секрет сadopt: true(генерация import-блоков — вlive/stage/secrets/terragrunt.hcl). 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(родительский): jobgenerate-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).