Terraform: використання Ephemeral resources та Write-only attributes

Автор |  03/09/2025
 

В 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 Manager
  • resource "aws_secretsmanager_secret_version": записуємо в цей Secret значення із resource "random_password"
  • data "aws_secretsmanager_secret_version": отримуємо значення з AWS Secrets Manager
  • output "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" ми тепер посилаємось на пароль через ephemeralephemeral.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“.

А в самому пул-реквесті навіть можна глянути як воно реалізоване.

Корисні посилання