В 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“.
А в самому пул-реквесті навіть можна глянути як воно реалізоване.
