В Terraform ephemeral resources та write-only arguments з’явились давно, ще у версії 1.10, але не було нагоди про них написати детальніше.
Основна ідея їх – не залишати “слідів” в state-файлі, що особливо корисно для паролів або токенів, бо дані існують тільки під час виконання apply самого Terraform в його пам’яті.
Втім, для їх використання є певні обмеження – далі на них глянемо, але спочатку подивимось на все в дії.
Зміст
Приклад без ephemeral values та write-only arguments
Почнемо зі старої схеми, без використання ephemeral resources та write-only arguments – створимо рандомний пароль, ресурс aws_secretsmanager_secret
, в ньому збережемо цей пароль, і отримаємо його з data
:
provider "aws" { region = "us-east-1" default_tags { tags = { component = "devops" created-by = "terraform" environment = "test" } } } ### RESOURCES ### # generate a random password resource "random_password" "test_random_password" { length = 8 special = false } # create an AWS Secret resource resource "aws_secretsmanager_secret" "test_aws_secret" { name = "db_password" description = "database passsword" recovery_window_in_days = 0 } # create an AWS Secret value resource "aws_secretsmanager_secret_version" "test_aws_secret_version" { secret_id = aws_secretsmanager_secret.test_aws_secret.id secret_string = random_password.test_random_password.result } ### DATA SOURCES ### # retrieve the AWS Secret value data "aws_secretsmanager_secret_version" "test_aws_secret_data" { secret_id = aws_secretsmanager_secret.test_aws_secret.id depends_on = [aws_secretsmanager_secret_version.test_aws_secret_version] } ### OUTPUTS ### # get the random password value output test_random_password { value = random_password.test_random_password.result sensitive = true } # get the AWS Secret value output "test_aws_secret" { value = data.aws_secretsmanager_secret_version.test_aws_secret_data.secret_string sensitive = true }
Тут ми:
resource "random_password"
: генеруємо сам парольresource "aws_secretsmanager_secret"
: створюємо новий запис в AWS Secrets Managerresource "aws_secretsmanager_secret_version"
: записуємо в цей Secret значення ізresource "random_password"
data "aws_secretsmanager_secret_version"
: отримуємо значення з AWS Secrets Manageroutput "test_random_password"
: виводимо значення ізresource "random_password"
output "test_aws_secret"
: виводимо значення, отримане з AWS Secrets Manager
Виконуємо terraform init
та terraform apply
:
... Apply complete! Resources: 3 added, 0 changed, 0 destroyed. Outputs: test_aws_secret = <sensitive> test_random_password = <sensitive>
Виглядає ОК – в outputs
у нас завдяки sensitive = true
нічого не відобразилось.
Але пароль є в state file:
$ cat terraform.tfstate { ... "outputs": { "test_aws_secret": { "value": "1atcZYGR", "type": "string", "sensitive": true }, "test_random_password": { "value": "1atcZYGR", "type": "string", "sensitive": true } }, ... "resources": [ { "mode": "data", "type": "aws_secretsmanager_secret_version", "name": "test_aws_secret_data", ... "secret_string": "1atcZYGR", ... { "mode": "managed", "type": "aws_secretsmanager_secret_version", "name": "test_aws_secret_version", ... "secret_string": "1atcZYGR", ... { "mode": "managed", "type": "random_password", "name": "test_random_password", ... "result": "1atcZYGR",
Тепер почнемо ховати ці дані зі стейту.
Використання Write-Only Attributes
Атрибути ресурсів, які мають суфікс _wo
є “write-only” даними, тобто Terraform їх тримає в пам’яті під час виконання операцій, але ніде в себе не зберігає.
Втім, таки атрибути підтримуються далеко не всіма ресурсами. Наприклад, в AWS RDS через ресурс aws_db_instance
можна передати пароль через атрибут password_wo
, а в aws_opensearch_domain
і його master_user_password
для створення root-юзера в internal user database – (поки що) ні.
Офіційна документація – Use write-only arguments.
aws_secretsmanager_secret_version
теж підтримує write-only attributes – secret_string_wo
замість secret_string
, і secret_string_wo_version
замість secret_string_version
.
Використання secret_string_wo_version
обов’язкове при secret_string_wo
, бо так як Terraform не зберігає інформацію про пароль – то він не буде знати, коли його треба оновити. Для цього задаємо версію, яку інкрементимо кожен раз, коли хочемо оновити пароль.
Редагуємо наш код, тільки resource "aws_secretsmanager_secret_version"
– задаємо secret_string_wo
і secret_string_wo_version
, решту залишаємо без змін:
... # create an AWS Secret value resource "aws_secretsmanager_secret_version" "test_aws_secret_version" { secret_id = aws_secretsmanager_secret.test_aws_secret.id #secret_string = random_password.test_random_password.result secret_string_wo = random_password.test_random_password.result secret_string_wo_version = 1 } ...
Виконуємо terraform apply
, і перевіряємо стейт тепер:
$ cat terraform.tfstate { ... "outputs": { "test_aws_secret": { "value": "1atcZYGR", "type": "string", "sensitive": true }, "test_random_password": { "value": "1atcZYGR", "type": "string", "sensitive": true } }, ... "resources": [ { "mode": "data", "type": "aws_secretsmanager_secret_version", "name": "test_aws_secret_data", ... "secret_string": "1atcZYGR", ... { "mode": "managed", "type": "aws_secretsmanager_secret_version", "name": "test_aws_secret_version", ... "secret_string": "", "secret_string_wo": null, "secret_string_wo_version": 1, ... { "mode": "managed", "type": "random_password", "name": "test_random_password", ... "result": "1atcZYGR",
Тепер у нас в managed.aws_secretsmanager_secret_version.test_aws_secret_version
немає значень для secret_string
та secret_string_wo
.
Використання Ephemeral resources
Ідея “ефемерних” ресурсів така ж, як і з write-only arguments – ці ресурси існують тільки в пам’яті Terraform під час виконання terraform apply
і не зберігаються в state file.
Але використання таких ресурсів обмежене:
- можна посилатись на них у write-only arguments
- в інших ефемерних ресурсах
- в
locals
- в ephemeral variables
- в providers, provisioner та connection
Документація – Ephemeral block reference.
Редагуємо наш код і міняємо resource "random_password"
на ephemeral "random_password"
, resource "aws_secretsmanager_secret_version"
залишаємо – він пароль запише в AWS Secrets Manager, але не зберігає значення в state, і додаємо новий ресурс – ephemeral "aws_secretsmanager_secret_version"
, через який ми цей пароль отримаємо назад в Terraform.
При цьому в secret_string_wo
і в output "test_random_password"
ми тепер посилаємось на пароль через ephemeral – ephemeral.random_password.test_random_password.result
.
І в output "test_aws_secret"
теж використовуємо ephemeral.aws_secretsmanager_secret_version.test_aws_secret_data.secret_string
.
data "aws_secretsmanager_secret_version"
можемо прибирати, бо пароль ми тепер отримаємо саме з ephemeral "aws_secretsmanager_secret_version"
:
... ### RESOURCES ### # generate a random password ephemeral "random_password" "test_random_password" { length = 8 special = false } # create an AWS Secret resource resource "aws_secretsmanager_secret" "test_aws_secret" { name = "db_password" description = "database passsword" recovery_window_in_days = 0 } # create an AWS Secret value resource "aws_secretsmanager_secret_version" "test_aws_secret_version" { secret_id = aws_secretsmanager_secret.test_aws_secret.id #secret_string = random_password.test_random_password.result secret_string_wo = ephemeral.random_password.test_random_password.result secret_string_wo_version = 1 } ### DATA SOURCES ### # Retrieve the password from Secrets Manager (ephemeral) ephemeral "aws_secretsmanager_secret_version" "test_aws_secret_version_ephemeral" { secret_id = aws_secretsmanager_secret.test_aws_secret.id } # retrieve the AWS Secret value # data "aws_secretsmanager_secret_version" "test_aws_secret_data" { # secret_id = aws_secretsmanager_secret.test_aws_secret.id # depends_on = [aws_secretsmanager_secret_version.test_aws_secret_version] # } ### OUTPUTS ### # get the random password value output test_random_password { value = ephemeral.random_password.test_random_password.result sensitive = true } # get the AWS Secret value output "test_aws_secret" { value = ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string sensitive = true }
Помилка “This output value is not declared as returning an ephemeral value”
Виконуємо terraform apply
, і ловимо першу помилку:
... │ Error: Ephemeral value not allowed │ │ on main.tf line 53, in output "test_random_password": │ 53: value = ephemeral.random_password.test_random_password.result │ │ This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value. ╵ ╷ │ Error: Ephemeral value not allowed │ │ on main.tf line 59, in output "test_aws_secret": │ 59: value = ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string │ │ This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value.
Але навіть якщо ми додамо параметр ephemeral = true
:
... ### OUTPUTS ### # get the random password value output test_random_password { value = ephemeral.random_password.test_random_password.result sensitive = true ephemeral = true } # get the AWS Secret value output "test_aws_secret" { value = ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string sensitive = true ephemeral = true }
То це все одно працювати не буде.
Помилка “Ephemeral outputs are not allowed in context of a root module”
Тепер помилка буде виглядати так:
... ╷ │ Error: Ephemeral output not allowed │ │ on main.tf line 52: │ 52: output test_random_password { │ │ Ephemeral outputs are not allowed in context of a root module ╵ ╷ │ Error: Ephemeral output not allowed │ │ on main.tf line 59: │ 59: output "test_aws_secret" { │ │ Ephemeral outputs are not allowed in context of a root module
Бо використання Ephemeral outputs можливе тільки в модулях – далі глянемо, як саме.
ОК – поки просто приберемо Outputs
, і тепер terraform apply
проходить без проблем:
$ terraform apply ... random_password.test_random_password: Refreshing state... [id=none] ephemeral.random_password.test_random_password: Opening... ephemeral.random_password.test_random_password: Opening complete after 0s ... ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Opening... ... ephemeral.random_password.test_random_password: Closing... ephemeral.random_password.test_random_password: Closing complete after 0s ...
Зверніть уваги, що для ephemeral
ресурсів Terraform тепер виконує операції не Reading та Refreshing state – а Opening та Closing.
Тобто, він просто створює об’єкт в пам’яті, зчитує в нього ресурс, а потім “закриває” і видаляє з пам’яті.
Перевіряємо state file тепер:
... { "mode": "managed", "type": "aws_secretsmanager_secret_version", "name": "test_aws_secret_version", ... "secret_string": "", "secret_string_wo": null, "secret_string_wo_version": 1, ...
Тепер у нас:
- ресурсів
ephemeral "random_password"
таephemeral "aws_secretsmanager_secret_version"
в стейті нема взагалі - а
managed.aws_secretsmanager_secret_version.test_aws_secret_version
все ще має пусте поле вsecret_string_wo
– бо ми його ще раніше зробили write-only
ОК – а як тепер використати пароль? Бо data "aws_secretsmanager_secret_version"
ми ж прибрали.
Використання значень з Ephemeral resources
Ми вже бачили приклад посилання на Ephemeral resources вище, коли робили secret_string_wo = ephemeral.random_password.test_random_password.result
.
Аналогічно можемо використати і ephemeral.aws_secretsmanager_secret_version.db_password_wo_ephemeral.secret_string
.
Як писав вище – можемо це робити не всюди, але в providers
це допускається.
Для перевірки – запустимо PostgreSQL з нашим паролем (візьмемо його напряму з AWS Console > AWS Secrets Manager):
Запускаємо контейнер, в який передаємо змінну POSTGRES_PASSWORD="1atcZYGR"
:
$ docker run --rm --name some-postgres -e POSTGRES_PASSWORD="1atcZYGR" -p 5432:5432 postgres
В наш код додаємо провайдера, і в з ним підключимось до контейнера, де створимо тестову базу.
В полі password
провайдера як раз і використаємо ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string
:
... ### PostgreSQL Configuration terraform { required_providers { postgresql = { source = "cyrilgdn/postgresql" version = "~> 1.20" } } } provider "postgresql" { host = "localhost" port = 5432 username = "postgres" password = ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string sslmode = "disable" } resource "postgresql_database" "demo_db" { name = "demo_db" template = "template0" connection_limit = -1 allow_connections = true }
Робимо terraform init
та terraform apply
:
$ terraform init && terraform apply ... ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Opening... ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Opening complete after 1s postgresql_database.demo_db: Creating... postgresql_database.demo_db: Creation complete after 0s [id=demo_db] ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Closing... ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Closing complete after 0s Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Перевіряємо базу:
$ export PGPASSWORD="1atcZYGR" $ psql -h localhost -U postgres -c "\l" List of databases Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges -----------+----------+----------+-----------------+------------+------------+--------+-----------+----------------------- demo_db | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | ...
Таким жеж чином ми могли б використати ефемерний ресурс через locals
:
... locals { db_password_local = ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string } provider "postgresql" { host = "localhost" port = 5432 username = "postgres" password = local.db_password_local #password = ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string sslmode = "disable" } resource "postgresql_database" "demo_db" { name = "demo_db_via_local" template = "template0" connection_limit = -1 allow_connections = true }
Перевіряємо:
$ terraform apply ... # postgresql_database.demo_db will be updated in-place ~ resource "postgresql_database" "demo_db" { id = "demo_db" ~ name = "demo_db" -> "demo_db_via_local" # (10 unchanged attributes hidden) } ... Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
І в state-файлі у нас ідже пароль не світиться:
$ cat terraform.tfstate | grep 1atcZYGR | echo $? 127
Використання Ephemeral Outputs
Вище ми пробували використати output "test_aws_secret"
з ephemeral = true
, але отримали помилку “Ephemeral outputs are not allowed in context of a root module“.
Спробуємо використати у власному модулі.
Документація – ephemeral – Avoid storing values in state or plan files.
Створимо модуль modules/secret_ephemeral
, в який винесемо генерацію паролю і його збереження в AWS Secrets Manager, і додамо Ephemeral Output.
А в рутовому модулі – використаємо outputs
цього модулю для отримання через ephemeral "aws_secretsmanager_secret_version"
, як це робили вище.
Пишемо файл modules/secret_ephemeral/secret.tf
:
### RESOURCES ### # generate a random password ephemeral "random_password" "test_random_password" { length = 8 special = false } # create an AWS Secret resource resource "aws_secretsmanager_secret" "test_aws_secret" { name = "db_password_via_module" description = "database passsword" recovery_window_in_days = 0 } # create an AWS Secret value resource "aws_secretsmanager_secret_version" "test_aws_secret_version" { secret_id = aws_secretsmanager_secret.test_aws_secret.id #secret_string = random_password.test_random_password.result secret_string_wo = ephemeral.random_password.test_random_password.result secret_string_wo_version = 1 } # Retrieve the password from Secrets Manager (ephemeral) ephemeral "aws_secretsmanager_secret_version" "test_aws_secret_version_ephemeral" { secret_id = aws_secretsmanager_secret.test_aws_secret.id } output "password_ephemeral" { value = ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string ephemeral = true }
В головному файлі main.tf
– прибираємо все, пов’язане з паролем, додаємо виклик модуля, і в locals
використовуємо його output
:
... ### PostgreSQL Configuration terraform { required_providers { postgresql = { source = "cyrilgdn/postgresql" version = "~> 1.20" } } } module "secret_ephemeral" { source = "./modules/secret_ephemeral" } locals { db_password_local = module.secret_ephemeral.password_ephemeral } provider "postgresql" { host = "localhost" port = 5432 username = "postgres" password = local.db_password_local #password = ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral.secret_string sslmode = "disable" } resource "postgresql_database" "demo_db" { name = "demo_db_via" template = "template0" connection_limit = -1 allow_connections = true }
Тільки спочатку треба створити пароль – запустити terraform apply
без resource "postgresql_database"
, і оновити запуск контейнера з новим паролем:
$ docker run --rm --name some-postgres -e POSTGRES_PASSWORD="PHsfzcIx" -p 5432:5432 postgres
Тепер наш провайдер використовує пароль з Ephemeral Output модуля modules/secret_ephemeral
:
... module.secret_ephemeral.ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Opening... module.secret_ephemeral.ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Opening complete after 1s postgresql_database.demo_db: Creating... postgresql_database.demo_db: Creation complete after 0s [id=demo_db_via] module.secret_ephemeral.ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Closing... module.secret_ephemeral.ephemeral.aws_secretsmanager_secret_version.test_aws_secret_version_ephemeral: Closing complete after 0s Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
В стейті у нас все так жеж ніякого паролю нема:
$ cat terraform.tfstate | grep PHsfzcIx | echo $? 127
Власне, на цьому і все.
Дуже жаль, що aws_opensearch_domain
не підтримує write-only. Хотів його використати для рутового паролю 🙁
Але в GitHub вже є на це issue Support ephemeral “write-only” argument for aws_opensearch_domain, і навіть з коментом “I have started working on this issue, and will submit a PR shortly“.
А в самому пул-реквесті навіть можна глянути як воно реалізоване.
Корисні посилання
- Securely storing credentials in Terraform with ‘Ephemeral Blocks’ and ‘Write-Only’ attributes
- Understanding ephemerality in Terraform
- Ensuring Terraform State Security with Ephemeral Values and Write-Only Outputs
- Terraform 1.10: Secure Secrets with Ephemeral Values