Secrets and Credential Management
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”provider authentication ควรมาจาก role หรือกลไก identity แทนที่จะเป็น key ตัวจริงที่พิมพ์ลงในไฟล์ และถึงจะดึง application secret จาก secrets manager จริง ๆ ตอน apply time ก็ยังไม่ได้กันค่าไม่ให้ไปอยู่ใน Terraform state แบบเต็มร้อย
อย่า hardcode credential ใน provider block
หัวข้อที่มีชื่อว่า “อย่า hardcode credential ใน provider block”authenticate AWS provider ด้วย access_key กับ secret_key ตัวจริงที่เขียนตรง ๆ ใน provider "aws" block หรือในไฟล์ .tfvars ทำได้จริง และใช้งานได้ในความหมายแคบ ๆ คือ terraform plan รันผ่าน แต่ก็ยังผิดอยู่ดี
# Anti-pattern — never do thisprovider "aws" { region = "us-east-1" access_key = "AKIAIOSFODNN7EXAMPLE" secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}key ที่เขียนไว้ในไฟล์ .tf จะโดน commit เข้า git ทันทีที่มีคนลืมว่าอยู่ตรงนั้น แล้วค่านั้นจะอยู่ต่อไปในทุก clone และทุก commit ใน history ตั้งแต่จุดนั้นเป็นต้นไป ไม่ว่าทีหลังจะลบออกจาก revision ปัจจุบันหรือเปล่าก็ตาม ทางแก้คือไม่ให้ provider มี credential เลยใน configuration แล้วปล่อยให้ resolve จาก environment ที่รันอยู่แทน
# The provider block declares no credentials.# Authentication comes from the environment: local env vars,# an EC2/CI instance role, or OIDC federation in CI.provider "aws" { region = "us-east-1"}ตอนรัน local ปกติแล้วนี่หมายถึง environment variable หรือ named profile จาก aws configure ส่วนใน CI หมายถึงการ assume role แบบ OIDC-federated ที่พูดถึงในบท CI/CD — aws-actions/configure-aws-credentials เติม environment variable มาตรฐานของ AWS ให้ก่อนที่ Terraform จะรันด้วยซ้ำ เลยทำให้ provider block หา credential เจอได้โดยไม่ต้องมี key สักตัวถูกเขียนไว้ที่ไหนเลย
ดึง secret เข้ามาผ่าน data source ไม่ใช่ variable
หัวข้อที่มีชื่อว่า “ดึง secret เข้ามาผ่าน data source ไม่ใช่ variable”คำถามที่แยกออกไปจาก provider authentication คือ application secret — password ของ database, API key ของ third-party — ที่ resource ตัวหนึ่งต้องใช้เป็น argument นิยาม secret นั้นเป็น Terraform variable ทำให้ตัว variable declaration กลายเป็นสิ่งที่ต้องปฏิบัติเหมือน source of truth ซึ่งแปลว่าต้องมีคนพิมพ์ค่าจริงลงในไฟล์ tfvars, CI secret หรือตอน prompt ที่ apply time อยู่ดี
pattern ที่ดีกว่าคืออ่านค่าปัจจุบันตรงจาก secrets manager จริง ๆ ผ่าน data source ตอน apply time
data "aws_secretsmanager_secret_version" "api_key" { secret_id = "prod/third-party/api-key"}
resource "aws_lambda_function" "webhook_handler" { function_name = "webhook-handler" runtime = "nodejs20.x" handler = "index.handler"
environment { variables = { API_KEY = data.aws_secretsmanager_secret_version.api_key.secret_string } }}นี่ไม่ใช่บัตรผ่านฟรี และควรพูดให้ชัดว่าทำไม module State Management เคยพูดไว้แล้วว่าการ set sensitive = true บน variable แค่ redact ออกจาก CLI output — ค่าจริงยังไปอยู่ใน terraform.tfstate เป็น plaintext อยู่ดี การอ่าน secret ผ่าน data source ก็ไม่ได้กันตรงนี้แบบเต็มร้อยเช่นกัน ผลลัพธ์ของ data source ก็ถูก cache ไว้ใน state เหมือนกัน ดังนั้น secret_string ที่ resolve ได้ก็ยังไปอยู่ใน state เหมือนกับที่ sensitive variable จะเป็น สิ่งที่ pattern แบบ data source ปรับปรุงจริง ๆ คือ lifecycle ของ secret ในที่อื่น ๆ — ค่าจริงไม่เคยถูกพิมพ์ลงใน variable เลย ไม่เคยไปนั่งอยู่ใน .tfvars ที่ใครสัก commit พลาด และ rotate ใน Secrets Manager ก็ไม่ต้องแก้ Terraform เลยสักบรรทัด — apply รอบถัดไปก็แค่อ่านค่าปัจจุบันที่มีอยู่ Secrets Manager คือ source of truth ส่วน Terraform แค่อ่านค่ามา การปกป้อง state backend เอง ที่พูดไว้ใน module State Management ยังเป็นส่วนที่ข้ามไม่ได้อยู่ดี
กัน secret ออกจาก state ให้หมดจริง ๆ ด้วย ephemeral กับ write-only
หัวข้อที่มีชื่อว่า “กัน secret ออกจาก state ให้หมดจริง ๆ ด้วย ephemeral กับ write-only”ทุกอย่างข้างบนเป็นจริงมาหลายปี และข้อแม้ที่ว่า “ผลลัพธ์ของ data source ก็ถูก cache ใน state เหมือนกัน” ก็เป็นสิ่งที่ต้องอยู่กับมันไป Terraform 1.10 กับ 1.11 ปิด gap ตรงนี้ด้วยสองฟีเจอร์ที่ทำงานร่วมกัน
ephemeral resource ถูกอ่านใหม่ทุกครั้งที่รัน operation และผลลัพธ์ไม่เคยถูกเขียนลง state หรือ plan file เลย ประกาศด้วย block type ชื่อ ephemeral แทน data
# Read at plan and apply time, never persisted to state or the plan fileephemeral "aws_secretsmanager_secret_version" "db_password" { secret_id = "prod/database/password"}ค่าที่ไม่เคยถูกเก็บก็คือค่าที่ Terraform เอาไปเทียบในรอบถัดไปไม่ได้ ผลลัพธ์จาก ephemeral จึงส่งเข้า argument ปกติของ resource ไม่ได้ ตรงนี้คือหน้าที่ของ write-only argument provider จะ mark argument บางตัวเป็น write-only ตั้งชื่อลงท้ายด้วย _wo และมาคู่กับ argument _wo_version Terraform ส่งค่าให้ provider ตอน apply แล้วก็ลืมมันไป
resource "aws_db_instance" "main" { identifier = "app-prod" engine = "postgres" instance_class = "db.t3.micro" allocated_storage = 20 username = "app"
# The password reaches AWS but is never recorded in state password_wo = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string password_wo_version = 1}เพราะตัวค่าไม่ได้อยู่ใน state Terraform จึงไม่มีทางรู้ว่ามันเปลี่ยนไปแล้ว นั่นคืองานของ password_wo_version เมื่อ rotate secret ก็ขยับเลขจำนวนเต็มนี้ แล้ว provider จะส่งค่าใหม่ให้ในรอบ apply ถัดไป ถ้าไม่ขยับ Terraform ก็ไม่ไปยุ่งกับ password เดิม
มีข้อจำกัดสองข้อที่ควรรู้ก่อนหยิบมาใช้ write-only argument ต้องใช้ Terraform 1.11 ขึ้นไปและ provider ต้องรองรับ — argument ตัวนั้นต้องมีอยู่จริงบน resource type นั้น ๆ ให้เช็ค documentation ของ provider ไม่ใช่เหมาว่า _wo ใช้ได้ทุกที่ และค่า ephemeral ไหลไปได้แค่ในที่จำกัดเท่านั้น คือ write-only argument, ephemeral resource หรือ variable ตัวอื่น, provider configuration, provisioner block และ output ที่ mark เป็น ephemeral เอง การเอาไป assign ให้ argument ปกติหรือ output ปกติจะ error ซึ่งคือภาษาบังคับการรับประกันนี้ให้ ไม่ใช่มาขวางทาง
ในที่ที่ resource รองรับ การใช้คู่นี้ดีกว่า pattern แบบ data source แบบชัดเจน เพราะ secret ถูกอ่านจาก source of truth จริงตอน apply และ ไม่เคยถูกบันทึกลง state เลย ส่วนที่ไหนยังไม่รองรับ pattern แบบ data source บวกกับการปกป้อง backend ก็ยังเป็นคำตอบที่ถูกอยู่
พิสูจน์ว่า write-only argument ทั้งสองตัวมีอยู่จริง เทียบกับ schema จริงของ aws provider 6.66.0
หัวข้อที่มีชื่อว่า “พิสูจน์ว่า write-only argument ทั้งสองตัวมีอยู่จริง เทียบกับ schema จริงของ aws provider 6.66.0”password_wo/password_wo_version บน aws_db_instance และ secret_string_wo/secret_string_wo_version บน aws_secretsmanager_secret_version มีอยู่จริงทั้งคู่ใน schema จริงของ aws provider — พิสูจน์ด้วย terraform validate จริงกับ provider 6.66.0 (mock credentials ไม่ต้องเรียก AWS จริงสำหรับ validate):
variable "db_password" { type = string ephemeral = true sensitive = true}
variable "api_key_value" { type = string ephemeral = true sensitive = true}# terraform.tfvars — harness fixture only, so this lesson's proof can run# offline; a real repo never commits secret values this way (see the# .gitignore checklist below) — supply real values via TF_VAR_* instead,# proven later in this section.db_password = "mock-secret-value"api_key_value = "mock-api-key-value"resource "aws_db_instance" "main" { identifier = "app-prod" engine = "postgres" instance_class = "db.t3.micro" allocated_storage = 20 username = "app" password_wo = var.db_password password_wo_version = 1 skip_final_snapshot = true}
resource "aws_secretsmanager_secret" "api_key" { name = "prod/third-party/api-key"}
resource "aws_secretsmanager_secret_version" "api_key" { secret_id = aws_secretsmanager_secret.api_key.id secret_string_wo = var.api_key_value secret_string_wo_version = 1}$ terraform validateSuccess! The configuration is valid.output จริงของ terraform plan (mock credentials, สร้าง resource ล้วน ๆ) แสดงให้เห็นเลยว่า provider แสดง write-only argument ยังไง — ไม่ถึงกับไปโผล่เป็นค่าใน diff แบบอ่านได้ด้วยซ้ำ:
# aws_secretsmanager_secret_version.api_key will be created + resource "aws_secretsmanager_secret_version" "api_key" { + has_secret_string_wo = (known after apply) + secret_string_wo = (write-only attribute) + secret_string_wo_version = 1 }และใน terraform show -json ของ plan ที่ save ไว้ resource_changes[].change.after.secret_string_wo เป็น null พร้อม after_sensitive.secret_string_wo เป็น true — Terraform ตัดค่าออกจริง ๆ จากรูปแบบ machine-readable ของ plan ด้วย ไม่ใช่แค่ output ของ CLI ที่สวยงาม:
"after": { "secret_string_wo": null, "secret_string_wo_version": 1 },"after_sensitive": { "secret_string_wo": true }has_secret_string_wo (boolean ที่ provider คำนวณจริง) คือวิธีที่ plan รอบถัดไปรู้ได้ว่า “มีการตั้ง write-only value ไว้” โดยไม่ต้องเก็บว่าค่านั้นคืออะไรเลย provider เก็บแค่ bit เดียวนี้ ไม่ใช่ตัว secret
gotcha ที่คอร์สนี้เจอ: default ของ ephemeral variable ยังรั่วอยู่ดี
หัวข้อที่มีชื่อว่า “gotcha ที่คอร์สนี้เจอ: default ของ ephemeral variable ยังรั่วอยู่ดี”การ mark variable เป็น ephemeral = true (Terraform 1.10) กันไม่ให้มันไปโผล่ที่ argument หรือ output ปกติ แต่ ไม่ได้ แปลว่าเขียนค่าเป็น literal default = ใน source .tf จะปลอดภัย — พิสูจน์จริง:
variable "db_password" { type = string ephemeral = true sensitive = true default = "mock-secret-value" # do not do this}$ terraform show -json tfplan | grep -o "mock-secret-value"mock-secret-valueค้นหา literal string นี้ใน plan.json ที่ได้เจอสองที่: .configuration.root_module.variables.db_password.default กับ .variables.db_password.value ไม่ใช่ bug ของ ephemeral/write-only เลยสักนิด — block configuration ของ show -json มี document ไว้ว่าเป็น echo ของ configuration ตามที่เขียนไว้จริง ๆ และ default ที่ hardcode ไว้ก็คือสิ่งที่เขียนไว้จริง ephemeral = true หยุดค่าจาก การถูก persist ลง state หรือไหลเข้า output ปกติเท่านั้น มันไม่ได้พูดอะไรเลยเกี่ยวกับ literal ที่คุณพิมพ์ตรง ๆ ลงใน HCL ที่ version-control ไว้ ซึ่งเป็น rule เดียวกับ “ห้าม hardcode secret ในไฟล์ .tf” จากต้นบทนี้เอง แค่มาโผล่ที่นี่อีกที การเอา default ออกแล้วส่งค่าผ่าน TF_VAR_db_password (environment variable ที่ไม่เคย commit) ตอน plan เท่านั้น ปิด gap นี้ได้สมบูรณ์ — พิสูจน์ด้วยการค้นหาแบบเดียวกันกับ plan JSON ที่ได้ ไม่เจออะไรเลย:
$ TF_VAR_db_password="mock-secret-value" terraform plan -out=tfplan$ terraform show -json tfplan | grep -o "mock-secret-value"# (ไม่มี output เลย — literal ไม่โผล่ใน plan JSON ที่ไหนเลย)checklist สำหรับ .gitignore ของ repo Terraform/Terragrunt
หัวข้อที่มีชื่อว่า “checklist สำหรับ .gitignore ของ repo Terraform/Terragrunt”.gitignore สั้น ๆ ที่ตั้งใจเขียนไว้ กันอุบัติเหตุข้างบนส่วนใหญ่ไม่ให้ไปถึง git ตั้งแต่แรก
# Terraform / Terragrunt .gitignore
# Never commit state — it can contain plaintext secrets and is the# canonical source of truth; committing it invites drift and leaks.*.tfstate*.tfstate.backup
# Local provider and module cache, regenerated by `terraform init`..terraform/
# May contain sensitive values depending on the file — judge case by# case rather than blanket-ignoring every *.tfvars in the repo.*.tfvars
# Terragrunt's local working-directory cache, regenerated automatically..terragrunt-cache/บางบรรทัดในนี้ควรมีเหตุผลกำกับสั้น ๆ
*.tfstateกับ*.tfstate.backupห้าม commit state เด็ดขาด นอกจากปัญหาเรื่องขนาดไฟล์กับ merge conflict แล้ว state ยังมี plaintext secret แฝงอยู่ได้อย่างที่พูดไปข้างบน และซ้ำกับงานที่ remote backend ทำอยู่แล้วอย่างถูกต้อง.terraform/นี่คือ local cache ของ provider กับ module ที่initดาวน์โหลดมา folder นี้ใหญ่ ผูกกับเครื่องนั้น ๆ และ reproduce ได้เต็มร้อยจาก.terraform.lock.hclเลยไม่มีอะไรใน folder นี้ที่คุ้มค่าจะ version*.tfvarsให้ตัดสินเป็นเคส ๆ ไป ไม่ใช่ ignore ทุกไฟล์แบบเหมารวม บางทีมตั้งใจ commitenvironment.tfvarsที่ไม่ sensitive ซึ่งแค่ set instance size หรือชื่อ region — rule คือ “ไฟล์นี้มี secret หรือเปล่า” ไม่ใช่ “ไฟล์.tfvarsทุกไฟล์คือ secret เสมอ”.terragrunt-cache/เป็น equivalent ของ.terraform/ฝั่ง Terragrunt มีหนึ่งตัวต่อ unit regenerate ทุกครั้งที่รัน ไม่ต้อง commit เลย และถ้า commit พลาดมักทำให้ repo บวมเร็วมาก
flowchart LR bad["Hardcoded credential in a .tf / .tfvars file"] -->|committed by accident| leak["Leaked secret in git history"] sm["Secrets Manager: source of truth"] -->|data source at apply time| attr["Resource attribute"] attr -->|still recorded| state["terraform.tfstate"] sm -->|ephemeral block| eph["Ephemeral value, never persisted"] eph -->|write-only argument| provider["Sent to AWS at apply, not stored"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกเทคนิคในบทนี้ตอบคำถามเดียวกันด้วยความแรงที่เพิ่มขึ้นเรื่อย ๆ: “secret value นี้อยู่ที่ไหน นานแค่ไหน” credential ที่ hardcode ไว้อยู่ตลอดไป ใน git history เป็นคำตอบที่อ่อนที่สุด variable อยู่นานเท่าที่สำเนาของมันใน .tfvars/CI secret/state ยังอยู่ data source แคบลงแค่ ค่ามาจากไหน (secrets manager จริง ไม่ใช่ค่าที่พิมพ์เอง) แต่ไม่แคบ ไปจบที่ไหน — พิสูจน์ในเนื้อหา baseline ของบทนี้เองว่ามันยังลงไปอยู่ใน state ephemeral + write-only คือคำตอบที่แรงที่สุด ค่ามีอยู่แค่ใน memory แค่ระหว่าง operation เดียว ส่งตรงให้ provider แล้วไม่เคยถูกบันทึกถาวรที่ไหนที่ Terraform คุมได้เลย — พิสูจน์ข้างบนด้วย null จริงใน change.after กับ boolean has_secret_string_wo จริงที่ยืนแทนมัน gotcha ที่พิสูจน์ข้างบน (default แบบ literal บน ephemeral variable ยังรั่วเข้า show -json) คือบทเรียนเดียวกันแบบย่อ: กลไก อาจสมบูรณ์แบบ แต่ literal ที่คุณพิมพ์เอง ยังเอาชนะมันได้
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
| provider credential ที่ resolve จาก environment (OIDC, instance role) | ไม่มี key ค้างอยู่ให้หลุดจากไฟล์ .tf/.tfvars | ต้อง setup infrastructure ก่อน (OIDC provider, IAM role) ถึงจะใช้งานได้ |
data source แทน variable สำหรับ secret | รวม lifecycle การ rotate ไว้ที่เดียว ค่าจริงไม่เคยอยู่ใน .tfvars | พิสูจน์ในเนื้อหา baseline ของบทนี้: ค่าที่ resolve แล้วยังลงไปอยู่ใน state — ต้องปกป้อง backend อยู่ดี |
ephemeral + write-only (password_wo, secret_string_wo) | พิสูจน์ข้างบน: ค่าไม่มีอยู่จริงทั้งใน state และใน change.after ของ plan | ต้องมี Terraform 1.10/1.11+ และ provider รองรับเป็นราย resource — ไม่ใช่ทุก argument ของทุก resource จะมีคู่ _wo |
ephemeral variable ที่ไม่มี default ส่งผ่าน TF_VAR_* อย่างเดียว | พิสูจน์ข้างบน: ค่าไม่โผล่ใน plan JSON เลยไม่ว่าที่ไหน | เผลอทำพังได้ง่าย ๆ ถ้าเพิ่ม default = literal ไว้เพื่อความสะดวกตอน test local แล้วลืมเอาออก — พิสูจน์ข้างบนว่าแค่นั้นก็ทำให้รั่วกลับมาอีกครั้ง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “mark variable เป็น
sensitive = trueกันมันออกจาก state ได้” เนื้อหา baseline ของบทนี้เองกับ module State Management บอกไว้แล้ว:sensitiveแค่ redact CLI output ค่ายังคงลงไปอยู่ในterraform.tfstateแบบ plaintext - “
datasource ที่ดึงจาก secrets manager แทนที่ความจำเป็นของephemeral/write-only ได้เต็มร้อย” พิสูจน์ข้างบนจาก design ของ pattern data source เอง: ผลลัพธ์ของdatasource ถูก cache ใน state เหมือน attribute ของ resource เป๊ะ —ephemeralมีไว้เพื่อปิด gap นี้โดยเฉพาะเพราะdataแก้ไม่ได้ - “
ephemeral = trueบน variable ทำให้ค่าอะไรก็ตามที่ assign เข้าไปปลอดภัย รวมถึง default ที่ hardcode ไว้” พิสูจน์ข้างบนด้วย string จริงที่ค้นหาเจอในplan.json:defaultแบบ literal บน ephemeral variable ถูก echo เข้า blockconfigurationและvariablesระดับบนสุดของshow -jsonไม่ว่า flagephemeralจะตั้งไว้หรือไม่ephemeralปกป้อง flow ของ state/output ไม่ใช่ literal ที่พิมพ์ตรง ๆ ลงใน HCL ที่ version-control ไว้ — นั่นคือ rule เก่าแยกต่างหากที่บทนี้คุยไปแล้ว - “write-only argument ใช้ได้กับ resource ไหนก็ได้ เพราะ feature นี้มีอยู่ใน provider แล้ว” พิสูจน์ข้างบนด้วยการ validate schema ตรง ๆ:
password_woมีอยู่บนaws_db_instanceและsecret_string_woมีอยู่บนaws_secretsmanager_secret_versionโดยเฉพาะ เพราะ schema ของแต่ละ resource ต้องเพิ่มมันเข้าไปเอง feature ที่มีอยู่ใน Terraform core ไม่ได้แปลว่าทุก argument ของทุก resource จะมีคู่_wo
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลา review AI-generated Terraform ที่จัดการ secret ให้เช็ค:
default =literal บน variable ที่ markephemeral = trueหรือsensitive = true— พิสูจน์ข้างบน สำหรับกรณี ephemeral นี่ทำลายจุดประสงค์ทั้งหมด (รั่วเข้าshow -json) และเป็นกลิ่นที่ต้อง review ไม่ว่ากรณีไหน- argument แบบ
password_wo/_woที่ใช้โดยไม่เช็ค schema ของ resource นั้นจริง ๆ — พิสูจน์ข้างบนผ่านterraform validateargument ต้องมีอยู่จริงบน resource ตัวนั้นเป๊ะ ๆ ชื่อ_woที่ฟังดูสมเหตุสมผลแต่ไม่มีจริงจะ fail validate ทันที เป็นวิธีเร็วที่สุดในการจับชื่อ argument ที่ AI แต่งขึ้นมาเอง password_woที่ไม่มีpassword_wo_versionขยับตามใน PR ที่ควรจะ rotate credential — เพราะตัวค่าเองไม่ได้ track ใน state เลข version ที่ไม่เปลี่ยนแปลว่า provider ยังใช้ password เก่าแบบเงียบ ๆ แม้ ephemeral source จะ return ค่าใหม่แล้วก็ตาม- ephemeral value ที่ assign เข้า argument ปกติ (ไม่ใช่ write-only ไม่ใช่ ephemeral) หรือ output ปกติ — ภาษาปฏิเสธตรง ๆ เลย นี่คือ type error ระดับ Terraform ไม่ใช่การรั่วแบบเงียบ ๆ ตอน runtime แต่ก็ยังควรรู้จักตั้งแต่แรกเห็น ไม่ใช่แปลกใจตอนเจอ error
- โค้ด application ที่อ่าน secret จาก data source ของ
aws_secretsmanager_secret_version(ไม่ใช่ephemeral) แล้ว treat เหมือนว่าเทียบเท่ากับการจัดการแบบ write-only — พิสูจน์ข้างบน ผลลัพธ์ของdataถูก cache ใน state มีแค่คู่ephemeral/write-only เท่านั้นที่หลีกเลี่ยงได้
🐛 หา bug ในโค้ดนี้
variable "db_password" {type = stringephemeral = truesensitive = truedefault = "changeme-in-ci"}
resource "aws_db_instance" "main" {# ...password_wo = var.db_passwordpassword_wo_version = 1}เฉลย
default = "changeme-in-ci" ดูเหมือน placeholder ปลอม ๆ ที่ชัดเจนอยู่แล้ว ซึ่งนั่นแหละคือสิ่งที่ทำให้ bug นี้พลาดตอน review ได้ง่าย — แต่มันก็ยังเป็น string literal ที่ commit เข้า version control อยู่ดี และพิสูจน์ข้างบนว่า ephemeral = true ไม่ได้หยุด default แบบ literal จากการถูก echo เข้า block configuration กับ variables ของ terraform show -json ถ้า “placeholder” นี้ถูกปล่อยไว้เป็น default จริงใน environment ที่ไม่มีใครจำได้ว่าต้อง override ด้วย -var/TF_VAR_db_password database จะถูก provision ด้วย password string ตรง ๆ ที่มองเห็นได้สาธารณะ จาก variable ที่ดูเหมือนจะ ephemeral-safe ทางแก้ พิสูจน์ข้างบน คือไม่ต้องมี default เลย ส่งค่าผ่าน TF_VAR_db_password หรือ -var เท่านั้น ซึ่งกันมันออกจาก plan JSON ได้เต็มร้อย