Это не редкость, а почти неизбежность, если работать наивно. Инфраструктура как код (iac) - штука прекрасная, но у Terraform есть одна неприятная особенность: он любит запоминать всё. Любое значение, которое ты передал в ресурс, с высокой вероятностью осядет в state и в плане. В этом уроке разберёмся, как передавать секреты в ресурсы без следов в коде, почему state - это главная дыра, и что подарила нам свежая фича Terraform 1.10/1.11 - ephemeral-значения и write-only атрибуты. Будем гонять на примере пароля для MDB PostgreSQL.
Откуда вообще берутся утечки секретов
Сначала надо понять врага в лицо. У Terraform есть три места, где секрет может засветиться:
- Сам код (.tf файлы). Если ты пишешь password = "qwerty123" прямо в ресурсе - это самый грубый вариант. Код уходит в репозиторий, и привет.
- План (terraform plan). Когда ты сохраняешь план в файл через -out, секреты из конфигурации попадают и туда.
- Состояние (terraform state). Вот это самое коварное. Даже если ты вынес пароль в переменную и нигде его не хардкодишь, после apply финальное значение всё равно записывается в state как факт о созданном ресурсе.
- через переменные окружения - Terraform читает TF_VAR_db_password из окружения, и в коде остаётся только объявление variable;
- через data source из хранилища секретов (в Yandex Cloud это Lockbox) - пароль лежит в защищённом сервисе, а Terraform забирает его на лету;
- через зашифрованные файлы (sops, ansible-vault и подобные) - на диске лежит шифротекст, расшифровка происходит в момент прогона.

Почему state - это бомба замедленного действия
Terraform state - это JSON, в котором записано текущее состояние всей твоей инфраструктуры. Зачем он нужен: чтобы Terraform понимал, что уже создано, и не пытался каждый раз пересоздавать ресурсы. Terraform plan и terraform apply сравнивают желаемое состояние из кода с фактическим из state - именно поэтому state обязан хранить реальные значения атрибутов. В том числе пароли.
То есть даже идеально написанный код с переменными окружения и Lockbox не спасает: пароль пользователя кластера всё равно лежит в state открытым текстом. Пометка sensitive = true только прячет значение из вывода в терминал, но в файле оно записано как есть. Это надо твёрдо усвоить: sensitive - это про вывод на экран, а не про шифрование.
Что с этим делать на уровне state (минимальная гигиена, которая нужна всегда):
- Шифрование бэкенда. Храни state не на ноутбуке, а в удалённом бэкенде с шифрованием. В Yandex Cloud это Object Storage (S3-совместимый бакет) с включённым шифрованием через KMS. Тогда даже сам файл на диске у провайдера лежит зашифрованным.
- Ограничение доступа. Бакет со state закрывается на отдельную сервисную учётку и узкий круг людей. State - это карта всей инфраструктуры, доступ к нему = доступ ко всем секретам.
- Никогда не коммить state в git. Звучит банально, но это причина половины инцидентов.
Ephemeral-значения и write-only атрибуты (Terraform 1.10/1.11)
В Terraform 1.10 появилась концепция ephemeral - временных значений, которые принципиально не пишутся ни в state, ни в план. Они живут только в оперативной памяти во время прогона и исчезают, как только apply закончился. В 1.11 эту историю докрутили до управляемых ресурсов через write-only аргументы. То же самое доехало и до OpenTofu - открытого форка Terraform, так что фича не привязана к одному вендору.
Из чего это состоит:
- Ephemeral input variables и outputs. Объявляешь переменную с ephemeral = true, и Terraform гарантирует, что её значение нигде не сохранится.
- Ephemeral resources. Новый вид блока ephemeral. Вместо привычных Read/Refresh у такого ресурса операции Open и Close: Terraform открывает доступ к секрету, использует его и закрывает. В state не остаётся ничего.
- Write-only атрибуты. Это аргументы обычных ресурсов, которые можно только записать, но нельзя прочитать обратно. Идеально для паролей: ты передал значение в ресурс, оно ушло в API провайдера - и всё, в state хранится только версия, а не сам пароль.
Код: Выделить всё
variable "pg_user_password" {
type = string
ephemeral = true
sensitive = true
}
# в шелле перед прогоном:
# export TF_VAR_pg_user_password='S3cretFromVault!'
Код: Выделить всё
resource "yandex_lockbox_secret" "pg" {
name = "pg-app-password"
password_payload_specification {
password_key = "password"
length = 24
}
}
resource "yandex_lockbox_secret_version" "pg" {
secret_id = yandex_lockbox_secret.pg.id
}
Теперь собираем сам кластер MDB PostgreSQL. Имена ресурсов и атрибутов - ровно из доки провайдера yandex (yandex_mdb_postgresql_cluster):
Код: Выделить всё
resource "yandex_mdb_postgresql_cluster" "app" {
name = "app-pg"
environment = "PRODUCTION"
network_id = yandex_vpc_network.net.id
config {
version = 17
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 16
}
}
host {
zone = "ru-central1-d"
subnet_id = yandex_vpc_subnet.subnet.id
}
}
Код: Выделить всё
resource "yandex_mdb_postgresql_user" "app" {
cluster_id = yandex_mdb_postgresql_cluster.app.id
name = "app"
password = var.pg_user_password
conn_limit = 50
}
Типичные грабли
- Путать sensitive и ephemeral. sensitive прячет значение из логов, но пишет его в state. ephemeral - не пишет вообще. Это разные вещи, не взаимозаменяемые.
- Думать, что Lockbox data source решает всё. Обычный (не ephemeral) data source кладёт прочитанное значение в state. То есть подтянул пароль из Lockbox обычным способом - и он снова в state. Спасает именно ephemeral-вариант.
- Старая версия Terraform. Ephemeral resources требуют 1.10+, write-only аргументы - 1.11+. На 1.5 ничего этого нет, как и в старом OpenTofu. Проверь terraform version перед тем, как закладываться на фичу.
- Забыть про версионирование write-only. Раз значение нельзя прочитать из state, Terraform не понимает, поменялось ли оно. Чтобы протолкнуть новый пароль, надо менять сопутствующий version-аргумент - иначе обновление просто не триггернётся.
- Расслабиться по поводу бэкенда. Даже с ephemeral шифрование бэкенда и ограничение доступа к state никто не отменял. Там по-прежнему лежит карта инфраструктуры, пусть и без паролей.
Повтори руками, без этого не уляжется:
- Создай кластер yandex_mdb_postgresql_cluster и пользователя yandex_mdb_postgresql_user с паролем из обычной переменной. Сделай apply.
- Открой terraform.tfstate и найди в нём свой пароль глазами. Прочувствуй масштаб проблемы.
- Переделай переменную на ephemeral = true, пересоздай пользователя, снова загляни в state - пароля там уже быть не должно.
- Заведи yandex_lockbox_secret с password_payload_specification и убедись, что в коде ты пароль не писал ни разу.
- Настрой удалённый бэкенд в Object Storage с шифрованием и убери локальный state совсем.
- Чем отличается sensitive = true от ephemeral = true? Где после apply окажется пароль в каждом случае?
- Почему обычный data source из Lockbox не спасает пароль от попадания в state, а ephemeral - спасает?
- Какие минимальные версии Terraform нужны для ephemeral resources и для write-only аргументов?
- Зачем write-only атрибутам нужен отдельный version-аргумент и что будет, если его не менять?
Terraform по умолчанию пишет всё в state, и пароль от MDB PostgreSQL там оказывается открытым текстом, даже если ты аккуратно тянул его через переменные окружения или Lockbox. Базовая защита state - шифрование бэкенда и жёсткое ограничение доступа - обязательна всегда. Но с Terraform 1.10/1.11 (и в OpenTofu) появился способ не пускать секрет в state в принципе: ephemeral input variables, ephemeral resources и write-only атрибуты. Пароль проходит сквозь конфигурацию и уходит в API, не оставляя следов в terraform state. Запомни главное правило: sensitive - это про экран, ephemeral - про то, что значения в state не будет вовсе.