Почему секреты в коде - это бомба замедленного действия
Терраформ работает с инфраструктурой как код (infrastructure as code, iac), и сама идея iac в том, чтобы всё лежало в репозитории и версионировалось. Беда в том, что секреты в эту картину вписываются плохо. Если ты пишешь пароль прямо в HCL или в terraform.tfvars, ты получаешь три проблемы сразу.
- Утечка через git. История коммитов хранит всё. Один невнимательный push в публичный репозиторий - и боты находят твой ключ за минуты.
- Утечка через state. Terraform state - это JSON, где значения ресурсов лежат открытым текстом. Пароль, который ты передал в ресурс, окажется в state как есть. Помечать переменную как sensitive = true помогает только скрыть её в выводе plan, но в state она всё равно ляжет в чистом виде.
- Невозможность ротации. Поменял пароль в облаке - и забыл обновить в десяти местах. Жёстко вшитые секреты гниют.

Два подхода: файлы против централизованного сервиса
Все инструменты управления секретами делятся на два больших лагеря, и понимание этой развилки важнее, чем знание конкретных команд.
Файловый подход - секреты лежат рядом с кодом, но в зашифрованном виде. Зашифрованный файл можно спокойно коммитить в git: без приватного ключа он бесполезен. Расшифровка происходит локально на машине того, у кого есть ключ. Сюда относится SOPS вместе с age. Плюсы: ничего не нужно поднимать, всё в одном репозитории, отлично для маленьких команд и pet проектов. Минусы: нет централизованной ротации, нет тонкого аудита кто что прочитал, ключи расшифровки надо как то раздать людям.
Централизованный сервис - секреты хранятся в отдельной системе с API, доступ выдаётся по ролям, есть журнал обращений и версии. Сюда относятся Yandex Lockbox, HashiCorp Vault, AWS Secrets Manager. Плюсы: единая точка правды, гранулярный доступ, ротация, аудит. Минусы: это ещё один сервис, за который надо платить или который надо администрировать.
Yandex Lockbox: создаём и читаем секретГрубое правило: один человек и пара секретов - бери SOPS. Команда и облачная инфраструктура - бери сервис. В Yandex Cloud этот сервис - Lockbox, и он родной для платформы, так что начнём с него.
Lockbox - это управляемое хранилище секретов в Yandex Cloud. Доступ к нему регулируется через IAM, шифрование - через KMS, а провайдер terraform yandex cloud даёт для работы с ним два ресурса и один data source.
Ключевая модель такая: есть секрет (yandex_lockbox_secret) - это контейнер с именем и метаданными, и есть версия секрета (yandex_lockbox_secret_version) - конкретный набор пар ключ значение. Версии - это важно: каждый раз, когда ты меняешь содержимое, создаётся новая версия, а старые остаются. Это и есть встроенная история, удобная для отката.
Сначала заведём секрет и положим в него версию с парой записей:
Код: Выделить всё
resource "yandex_lockbox_secret" "db" {
name = "prod-db-credentials"
description = "Логин и пароль для прод базы"
labels = {
env = "prod"
}
deletion_protection = true
}
resource "yandex_lockbox_secret_version" "db_v1" {
secret_id = yandex_lockbox_secret.db.id
entries {
key = "username"
text_value = "app_user"
}
entries {
key = "password"
text_value = var.db_password
}
}
Lockbox умеет и сам генерировать пароль, чтобы тебе вообще не пришлось его придумывать. Для этого у секрета есть блок password_payload_specification, и тогда версия создаётся пустой - без entries:
Код: Выделить всё
resource "yandex_lockbox_secret" "generated" {
name = "service-token"
password_payload_specification {
password_key = "token"
length = 24
include_punctuation = false
}
}
resource "yandex_lockbox_secret_version" "generated_v1" {
secret_id = yandex_lockbox_secret.generated.id
}
Код: Выделить всё
data "yandex_lockbox_secret_version" "db" {
secret_id = yandex_lockbox_secret.db.id
version_id = yandex_lockbox_secret_version.db_v1.id
}
locals {
db_password = [
for e in data.yandex_lockbox_secret_version.db.entries :
e.text_value if e.key == "password"
][0]
}
HashiCorp Vault и SOPS + age - когда они уместнееВажная деталь из документации: если секрет создаётся в том же проекте, всегда указывай version_id явно. Иначе data source может зацепить не ту версию - например, самую первую, ещё пустую, и ты будешь долго гадать, почему пароль приходит пустым.
HashiCorp Vault - это тяжёлая артиллерия мира секретов. Он умеет не только хранить статические значения, но и выдавать динамические: например, по запросу создавать временную пару логин пароль для базы, которая сама протухнет через час. Есть свой terraform провайдер vault с ресурсами вроде vault_kv_secret_v2 и data source vault_generic_secret. Если у тебя мультиоблако, строгие требования compliance или нужны короткоживущие учётки - Vault мощнее любого облачного менеджера. Цена за это - его надо развернуть, настроить unseal, бэкапить и обслуживать. Для проекта целиком внутри Yandex Cloud это чаще оверкилл, и Lockbox закрывает 90 процентов задач без головной боли.
SOPS + age - элегантный файловый вариант. SOPS шифрует только значения в YAML или JSON, оставляя ключи и структуру читаемыми, поэтому в git diff видно, какие поля поменялись, но не сами секреты. age - это современный простой инструмент шифрования с короткими ключами, пришедший на смену громоздкому GPG. Зашифрованный secrets.enc.yaml коммитится в репозиторий, а расшифровать его может любой, у кого есть приватный age ключ. В Terraform такие файлы обычно подтягивают через провайдер carlpett/sops и его data source sops_file. Это идеальный выбор, когда нет желания поднимать сервис, а секретов немного.
Кстати, всё сказанное про state и переменные одинаково справедливо и для opentofu - открытого форка Terraform. Lockbox, Vault и SOPS работают с ним точно так же, синтаксис HCL общий.
Типичные грабли
- Секрет всё равно утёк в state. Помни: когда ты читаешь Lockbox через data source, расшифрованное значение ложится в terraform state. Lockbox защищает хранилище, но не твой state файл. Держи state в зашифрованном backend (Object Storage с KMS) и ограничивай к нему доступ.
- Забыл про IAM. Сервисный аккаунт, под которым крутится Terraform, должен иметь роль lockbox.payloadViewer, чтобы читать содержимое версий. Без неё apply упадёт с access denied, хотя ресурс существует.
- Перепутал секрет и версию. yandex_lockbox_secret сам по себе пустой - это просто контейнер. Реальные данные живут в yandex_lockbox_secret_version. Новичок часто пишет entries прямо в секрет и удивляется ошибке.
- Не указал version_id и поймал пустой результат - см. цитату выше.
- Положил password_payload_specification и одновременно свои entries в версию. Так нельзя: для секрета со спецификацией пароля версия должна быть без entries.
Повтори руками, чтобы отложилось:
- Заведи yandex_lockbox_secret с именем lab-secret и одну версию с двумя записями: api_key и api_url. Пароль прокинь через TF_VAR.
- Сделай terraform plan и terraform apply, проверь секрет в консоли Yandex Cloud.
- Добавь второй yandex_lockbox_secret_version с изменённым значением - убедись, что в Lockbox появилось две версии.
- Через data source yandex_lockbox_secret_version прочитай конкретную версию по version_id и выведи нужное значение в output (с пометкой sensitive = true).
- Чем отличается ресурс yandex_lockbox_secret от yandex_lockbox_secret_version и где реально лежат данные?
- Почему при чтении секрета через data source важно указывать version_id?
- В каких случаях ты выберешь SOPS + age, а в каких - централизованный сервис вроде Lockbox или Vault?
- Защищает ли Lockbox твой Terraform state от утечки прочитанного секрета? Что с этим делать?
Секретам не место в коде и в чистом виде в state. Файловый подход (SOPS + age) хорош для соло и малых команд, централизованный сервис - для командной облачной инфраструктуры. В Yandex Cloud родной выбор - Lockbox: секрет это контейнер (yandex_lockbox_secret), данные это версия (yandex_lockbox_secret_version) с блоками entries, чтение через data source с обязательным version_id, доступ по IAM. Vault бери, когда нужны динамические короткоживущие секреты и ты готов его обслуживать. И всегда помни про защиту самого state - менеджер секретов прикрывает хранилище, а не твой terraform plan apply.