Sensitive Data and Team Workflows
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”การตั้งค่าเป็น sensitive = true ซ่อนค่านั้นจาก terminal แต่ค่ายังอยู่แบบ plaintext ใน state file เหมือนเดิม ดังนั้นทางแก้ที่แท้จริงคือปกป้อง state backend เอง และไม่เอา secret ระยะยาวเข้ามาไว้ใน Terraform เลยตั้งแต่แรก
sensitive = true ทำอะไรจริง ๆ
หัวข้อที่มีชื่อว่า “sensitive = true ทำอะไรจริง ๆ”ทั้ง variable และ output block รับ argument sensitive ได้
variable "database_password" { type = string sensitive = true}
output "db_connect_string" { value = "Server=${aws_db_instance.main.address};Pwd=${var.database_password}" sensitive = true}พอตั้ง sensitive = true แล้ว Terraform จะซ่อนค่านั้นทุกที่ที่ปกติจะโผล่บน terminal หรือใน CI log — โดย print (sensitive value) ใน output ของ terraform plan และ terraform apply แทนค่าจริง และไม่ยอม print ค่าดิบจาก terraform output เว้นแต่คุณจะขอตรง ๆ ด้วย terraform output db_connect_string นี่คือ guardrail ที่มีประโยชน์จริง เพราะกันไม่ให้ password เลื่อนผ่านหน้าจอไปใน CI log ที่แชร์กัน หรือ terminal ที่ screen-share อยู่โดยไม่ตั้งใจ
สิ่งที่ต้องพูดให้ชัดคือความเข้าใจผิดที่พบบ่อย sensitive = true เป็น feature ด้าน display ไม่ใช่ feature ด้านการเข้ารหัส ค่าถูกคำนวณ เก็บ และ diff โดย Terraform เหมือนเดิมทุกอย่างไม่ว่าจะตั้ง sensitive หรือไม่ — ค่ายังไปอยู่ใน terraform.tfstate แบบ plaintext เต็ม ๆ พร้อม tag ทุกตัว ใครก็ตามที่อ่าน state file ดิบได้ ไม่ว่าจะจาก S3 bucket, .tfstate local หรือ terraform state pull ก็เห็น password ตัวนั้นนอนอยู่ใน JSON ตรง ๆ ไม่ว่าจะมี flag sensitive หรือไม่มีก็ตาม
terraform state pull | grep -A3 db_connect_string# "db_connect_string": {# "value": "Server=mydb.abc123.us-east-1.rds.amazonaws.com;Pwd=S3cretPassword!",# "type": "string"ปกป้อง state ที่ rest
หัวข้อที่มีชื่อว่า “ปกป้อง state ที่ rest”เพราะการตั้งค่าให้เป็น sensitive ไม่ได้เอาค่านั้นออกจาก state การป้องกันจริง ๆ ต้องเกิดที่ layer ของ storage ให้ดูแล terraform.tfstate เองเหมือนที่คุณดูแลไฟล์ .env ที่เต็มไปด้วย credential ของ production
- เข้ารหัส backend ที่ rest สำหรับ S3 backend ที่พูดถึงในบทก่อน หมายถึงเปิด S3 server-side encryption (SSE) บน state bucket เพื่อให้ทุก object — รวมถึงทุก version ของ state file — ถูกเข้ารหัสบน disk ไม่ว่าใครจะเรียก bucket API
- จำกัดว่าใครอ่าน bucket ได้ IAM policy ที่จำกัด access ของ state bucket ให้เหลือแค่ role และ user ที่ต้องรัน Terraform จริง ๆ คือสิ่งที่กันไม่ให้คนที่ไม่ได้รับอนุญาตไปถึง plaintext ได้ ไม่ว่าจะเข้ารหัสที่ rest ไว้หรือไม่ก็ตาม — encryption at rest ป้องกันคนที่เข้าถึง disk ดิบ ไม่ใช่ป้องกันคนที่มี AWS credential ถูกต้องแล้วเรียก
s3:GetObject
resource "aws_s3_bucket" "state" { bucket = "my-company-terraform-state"}
resource "aws_s3_bucket_server_side_encryption_configuration" "state" { bucket = aws_s3_bucket.state.id
rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } }}ถ้าเป็นไปได้ วิธีที่ดีกว่าคือหลีกเลี่ยงปัญหานี้ไปเลย อย่าเอา secret ระยะยาวใส่เข้าไปใน Terraform variable ตั้งแต่แรกถ้าดึงตอน apply ได้ data source ที่อ่านจาก secrets manager จะดึงค่าปัจจุบันตอน Terraform รัน โดยที่คุณไม่ต้องพิมพ์ secret ลงไฟล์ .tf หรือ .tfvars เลย
data "aws_secretsmanager_secret_version" "db_password" { secret_id = "prod/app-db/password"}
resource "aws_db_instance" "main" { identifier = "app-db" engine = "postgres" instance_class = "db.t3.micro" username = "app_admin" password = data.aws_secretsmanager_secret_version.db_password.secret_string}ต้องเข้าใจให้ชัดว่าวิธีนี้แก้อะไรและไม่แก้อะไร ค่า secret ที่ resolve ออกมายังไปจบอยู่ใน state entry ของ aws_db_instance.main เหมือนเดิม เพราะ Terraform ต้อง record attribute ที่ตั้งไว้ เหมือนความเสี่ยง plaintext จากบทแรกของ module นี้ สิ่งที่แก้ได้จริงคือ lifecycle ของ secret ในที่อื่น ๆ ทั้งหมด — ไม่เคยถูกพิมพ์ลง variable ไม่เคยอยู่ในไฟล์ .tfvars ที่อาจโดน commit พลาด และการ rotate ใน Secrets Manager ก็ไม่ต้องแตะ Terraform configuration เลย การปกป้อง state backend เองยังคงเป็นส่วนที่ต่อรองไม่ได้
nonsensitive() ยกเลิกการ redact ค่าอย่างตั้งใจ
หัวข้อที่มีชื่อว่า “nonsensitive() ยกเลิกการ redact ค่าอย่างตั้งใจ”บางครั้ง sensitive = true แพร่กระจายไปไกลกว่าที่คุณต้องการ — expression ใด ๆ ที่แตะค่า sensitive จะกลายเป็น sensitive ไปด้วย แม้ผลลัพธ์จริง ๆ จะไม่ใช่ secret เลยก็ตาม (เช่น boolean ที่คำนวณจากความยาวของ password) ฟังก์ชัน nonsensitive() เอา marking นั้นออกจากค่าเดียวอย่างตั้งใจ สำหรับกรณีแบบนั้นเป๊ะ ๆ พิสูจน์จริงด้วย terraform console แสดงการ redact ก่อน แล้วค่อยเอาออก
$ echo 'var.db_password' | terraform console(sensitive value)$ echo 'nonsensitive(var.db_password)' | terraform console"S3cretPassword!"นั่นคือกลไกทั้งหมด nonsensitive() ไม่ได้เพิ่มการป้องกันอะไร และไม่ได้เอาค่าออกจาก state ไม่ว่ากรณีไหน — มันแค่เปลี่ยนว่าการ redact ของ Terraform จะใช้กับผลลัพธ์ของ expression ตัวนี้โดยเฉพาะต่อจากนี้หรือไม่เท่านั้น ใช้มันเฉพาะตอนที่คุณมั่นใจว่าค่านั้นไม่ได้เป็น secret จริง ๆ ในบริบทนั้น — เรียกมันกับ secret จริงแค่เพื่อปิด warning จะทำลายการป้องกันเดียวที่ sensitive = true มีให้
Ephemeral value ถูกกันออกจาก state และ plan โดยโครงสร้าง
หัวข้อที่มีชื่อว่า “Ephemeral value ถูกกันออกจาก state และ plan โดยโครงสร้าง”Terraform 1.10 เพิ่ม resource, variable และ output แบบ ephemeral — ค่าที่มีอยู่แค่ตลอด operation เดียวของ Terraform และไม่เคยถูกเขียนลง state หรือ saved plan file เลย ไม่ใช่ “เขียนแต่ redact” แบบที่ sensitive = true ทำ ตั้ง variable เป็น ephemeral แบบนี้
variable "session_token" { type = string ephemeral = true}ephemeral block ก็ทำงานแบบเดียวกันสำหรับ resource ที่มีอยู่แค่ชั่วคราว — pattern ที่ built-in มาให้คือ credential อายุสั้นหรือ secret ที่ generate สด ๆ ทุกครั้งที่รัน ไม่เคย persist ไว้ที่ไหนเลย
ephemeral "random_password" "db" { length = 16}เพราะ ephemeral value ไม่เคยถูก persist Terraform จึง enforce ว่า ที่ไหน ที่มันไหลไปได้ ตอน plan time ด้วย error จริง — ไม่ใช่แค่ convention พิสูจน์ ส่ง ephemeral.random_password.db.result เข้า argument ธรรมดา (ไม่ใช่ write-only) ของ resource
$ terraform planError: Invalid use of ephemeral value
with terraform_data.marker, on main.tf line 15, in resource "terraform_data" "marker": 15: input = ephemeral.random_password.db.result
Ephemeral values are not valid for "input", because it is not a write-onlyattribute and must be persisted to state.error นั้นระบุกฎจริงตรง ๆ ค่า ephemeral ไหลไปได้แค่ใน attribute ที่ provider ประกาศเป็น write-only ไว้ชัดเจนเท่านั้น เพราะ write-only attribute คือ argument ชนิดเดียวที่ Terraform รับประกันว่าจะไม่ persist เลย ข้อจำกัดจริงอีกข้อ พิสูจน์แบบเดียวกัน output แบบ ephemeral ใช้ใน root module ไม่ได้เลย —
$ terraform planError: Ephemeral outputs are not allowed in context of a root module— ephemeral output มีไว้ให้ child module ส่งค่า ephemeral กลับขึ้นไปให้ตัวที่เรียกมัน (เพื่อให้ผู้เรียกเอาไปใช้กับ write-only argument ของตัวเองหรือส่งต่อเป็น ephemeral chain) ไม่ใช่ให้ root module print มันออกมา
Write-only argument (_wo) อีกครึ่งหนึ่งของกลไก
หัวข้อที่มีชื่อว่า “Write-only argument (_wo) อีกครึ่งหนึ่งของกลไก”Terraform 1.11 เพิ่ม write-only argument — resource attribute ที่ตามธรรมเนียมลงท้ายด้วย _wo คู่กับ integer _wo_version ที่ provider สัญญาว่าจะไม่เขียนลง state หรือ saved plan เลย ตามตัวอย่างจริงจาก docs
resource "aws_db_instance" "main" { instance_class = "db.t3.micro" # ... password_wo = ephemeral.random_password.db_password.result password_wo_version = 1}password_wo_version คือสิ่งที่บอก Terraform ว่าค่านั้นเปลี่ยนแล้วต้อง apply ใหม่ — เพราะตัวค่าเองไม่เคยถูกเก็บไว้ Terraform เลยไม่มีอะไรจะ diff ในครั้งถัดไปที่ plan การเพิ่มเลข version จึงเป็น signal เดียวที่บอกว่าควรส่งค่า write-only ใหม่ นี่คือกลไกจริงที่ document ไว้ (quote จาก developer.hashicorp.com/terraform/language/manage-sensitive-data) “Providers define which managed resources support these arguments, and Terraform discards the values immediately after the operation is complete”
write-only argument เป็น opt-in ต่อ resource ต่อ provider — ผู้เขียน provider ต้องประกาศ write_only: true บน attribute เจาะจงใน schema ของ resource นั้นเอง ไม่ใช่การเดา terraform providers schema -json dump schema จริงที่ provider hashicorp/aws 6.66.0 ที่ pin ไว้ ship มา และ aws_db_instance ก็ประกาศคู่นี้จริง ๆ
$ terraform providers schema -json | python3 -c "import json,sysd = json.load(sys.stdin)attrs = d['provider_schemas']['registry.terraform.io/hashicorp/aws']['resource_schemas']['aws_db_instance']['block']['attributes']for k, v in attrs.items(): if v.get('write_only') or k.endswith('_wo_version'): print(k, v)"password_wo {'type': 'string', 'description_kind': 'plain', 'optional': True, 'sensitive': True, 'write_only': True}password_wo_version {'type': 'number', 'description_kind': 'plain', 'optional': True}การ dump schema แบบนี้ไม่ต้องมี AWS credential เลย — terraform providers schema -json แค่อ่านสิ่งที่ init ดาวน์โหลดมาแล้ว สิ่งที่ต้องการ credential จริงคือการ apply มันต่างหาก aws_db_instance สร้าง RDS instance จริง ซึ่งคอร์สนี้ทำไม่ได้ คำถามที่ต้องตอบตรง ๆ แทนคือ การรองรับนี้เป็นสากลทุก resource หรือมีแค่บางตัว เช็คกับ random_password ของ hashicorp/random — resource ที่คอร์สนี้ apply ได้เต็มรูปแบบแบบ offline — ด้วย query เดียวกันเป๊ะ
$ terraform providers schema -json | python3 -c "import json,sysd = json.load(sys.stdin)attrs = d['provider_schemas']['registry.terraform.io/hashicorp/random']['resource_schemas']['random_password']['block']['attributes']print([k for k,v in attrs.items() if v.get('write_only')])"[]random_password ไม่มี write-only attribute เลยแม้แต่ตัวเดียว เหมือนกับ built-in terraform_data และ resource ของ hashicorp/local ภาพจริงที่พิสูจน์แล้วทั้งสองด้านข้างบนคือ write-only support ไม่ใช่การรับประกันทั่วทั้ง Terraform ที่ resource ไหนก็ได้ฟรีตั้งแต่ 1.11 — มันคือสิ่งที่ผู้เขียน provider แต่ละคนเพิ่มเข้ามาทีละ resource ทีละ attribute และตามเวอร์ชันที่ pin ไว้นี้ aws_db_instance.password_wo มีจริง ในขณะที่ resource ที่ apply ได้แบบ offline เต็มรูปแบบของคอร์สนี้ไม่มีตัวไหนมีเลย สิ่งที่พิสูจน์ได้จริงแบบ offline ครบวงจร และพิสูจน์ไปแล้วข้างบนคือครึ่งหนึ่งของกลไกที่ทำให้ write-only argument มีประโยชน์ตั้งแต่แรก คือการ enforce ค่า ephemeral ว่าอะไรได้รับอนุญาตให้ไหลเข้า _wo argument บ้าง เมื่อ resource นั้นมี attribute แบบนี้แล้ว
จุดที่ shared state เริ่มไม่พอ
หัวข้อที่มีชื่อว่า “จุดที่ shared state เริ่มไม่พอ”บทก่อนตั้ง S3 backend พร้อม locking ไว้แล้ว ซึ่งแก้ปัญหา “ทุกคนอ่านและเขียน state เดียวกันได้อย่างปลอดภัย” นั่นจำเป็น แต่ไม่พอเมื่อทีมโตเกินสองสามคน ถ้าไม่มีอะไรเพิ่มเติม การที่ “ทุกคนรัน terraform apply จาก laptop ของตัวเองเมื่อไหร่ก็ได้ตามใจ” ยังมีช่องว่างจริง ๆ อยู่
- run
applyที่เกิดพร้อมกันไม่ทำให้ state เสียหายแล้วเพราะมี locking แต่ก็ยังเข้าคิวรอกัน — engineer คนที่สองแค่รอเฉย ๆ โดยไม่รู้ว่า run แรกกำลังทำอะไรอยู่หรือจะใช้เวลานานแค่ไหน - ไม่มีจุดตามธรรมชาติให้คนที่สอง review change ก่อนขึ้นจริง locking กันแค่ apply สองตัวไม่ให้แข่งกัน แต่ไม่ได้กัน engineer คนหนึ่งจาก apply change ที่ไม่มีใครเคยดูมาก่อน
- engineer แต่ละคนต้องมี AWS credential ที่มีสิทธิ์พอจะรัน
applyได้จาก local นั่นเป็น blast radius ที่กว้างกว่าที่ทีมส่วนใหญ่อยากแจกให้ทุก laptop
ทั้งหมดนี้ไม่ได้แปลว่า shared backend จากบทก่อนผิด เพราะเป็นสิ่งที่ต้องมีก่อน ไม่ใช่ความผิดพลาด แค่ยังไม่ใช่เรื่องทั้งหมด การปิดช่องว่างนี้คือสิ่งที่ CI/CD สำหรับ infrastructure มีไว้ทำ — pipeline รัน plan บนทุก change ที่เสนอมาเพื่อ review ก่อนใครจะ apply และ apply รันจาก pipeline identity ที่ควบคุมได้ แทนที่จะรันจาก laptop ของแต่ละคน นี่เป็นหัวข้อของบทหลังในคอร์สนี้ ไม่ใช่สิ่งที่ต้องแก้ตรงนี้ — ประเด็นตอนนี้คือให้รู้จักรูปร่างของช่องว่างนี้ไว้ก่อน จะได้ไม่แปลกใจทีหลัง
flowchart LR sv["variable with sensitive = true"] -->|redacted| cli["CLI output: (sensitive value)"] sv -->|full value written| state["terraform.tfstate: plaintext"] sm["Secrets Manager"] -->|data source at apply time| res["Resource attribute"] res -->|attribute recorded| state
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”สามกลไกในบทนี้ดูคล้ายกันจากภายนอก — sensitive, ephemeral, และ write-only argument — แต่แก้ปัญหาคนละอย่างกันจริง ๆ และความต่างอยู่ตรงที่ อะไรถูกเขียนลง state sensitive = true เขียนค่าลง state เต็ม ๆ แล้ว redact แค่ terminal — พิสูจน์ข้างบน ค่าดิบห่างแค่ state pull | grep เดียว ค่า ephemeral ไม่เคยถูกเขียนลง state เลย และ Terraform enforce เรื่องนี้ตอน plan time ด้วย error จริงทันทีที่คุณพยายามส่งมันเข้า attribute ธรรมดา — พิสูจน์ข้างบน write-only argument คือครึ่งฝั่ง provider ของการรับประกันเดียวกันนั้น _wo attribute คือที่เดียวที่ ephemeral value ได้รับอนุญาตให้ไปถึง เพราะ provider สัญญาว่าจะไม่ persist มันเลย อ่านตามลำดับนี้ — display-only, กันออกจากโครงสร้าง, provider สัญญาว่าจะไม่ persist — ทั้งสามจะไม่ดูเหมือน “secret” สามแบบอีกต่อไป แต่กลายเป็นคำตอบสามแบบสำหรับคำถาม “ค่านี้ไปถึง disk ไหม”
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
sensitive = true | หยุดค่าเลื่อนผ่าน terminal หรือ CI log ที่แชร์กัน (พิสูจน์ข้างบน (sensitive value)) | ไม่มีผลกับ state เลย — พิสูจน์ข้างบน ค่าดิบยังห่างแค่ state pull เดียว |
variable/resource แบบ ephemeral | ไปไม่ถึง state หรือ saved plan ได้เลยโดยโครงสร้าง — enforce ด้วย error จริง ไม่ใช่แค่ convention (พิสูจน์ข้างบน) | ใช้ได้แค่บริบทแคบ ๆ (write-only argument, provider config, ephemeral value อื่น) ephemeral output ใน root module ถูกปฏิเสธตรง ๆ (พิสูจน์ข้างบน) |
write-only (_wo) argument | ค่านั้นไม่แตะ state เลยจริง ๆ แม้จะชั่วคราวก็ตาม (ตามที่ provider สัญญาไว้เอง พิสูจน์ข้างบนว่า aws_db_instance ประกาศ password_wo จริง) | opt-in ต่อ resource ต่อ provider ไม่ใช่การรับประกันทั่ว 1.11 — พิสูจน์ข้างบน random_password ไม่มีเลยสักตัว ใช้จริงกับ aws_db_instance.password_wo ต้องมี RDS instance จริงกับ credential |
nonsensitive() | ให้ค่าที่จริง ๆ ไม่ใช่ secret หลุดจากการ redact ที่ไม่จำเป็น (พิสูจน์ข้างบนผ่าน console) | เอาการป้องกันเดียวที่ sensitive = true มีให้ออกไป ถ้าใช้ผิดกับสิ่งที่เป็น secret จริง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
sensitive = trueกันไม่ให้ค่าไปอยู่ใน state file” พิสูจน์ข้างบนสองครั้ง ทั้งตัวอย่างstate pull | grepเดิม และข้อเท็จจริง baseline — มันเปลี่ยนแค่การแสดงผลบน CLI ไม่เคยเปลี่ยนวิธีเก็บ - “
ephemeral = trueเป็นแค่sensitive = trueเวอร์ชันเข้มกว่า” พิสูจน์ข้างบน คนละกลไกกันเลยsensitiveredact การแสดงผลแต่ยังเขียนลง stateephemeralถูกกันออกจาก state และ plan โดยโครงสร้าง enforce ด้วย error จริง “Ephemeral values are not valid for …” ทันทีที่พยายาม persist - “resource ไหนก็รับ ephemeral value ได้ถ้าอ้างถึงตรง ๆ” พิสูจน์ข้างบน
terraform planปฏิเสธด้วยเหตุผลที่ระบุชัดเจน — attribute เป้าหมาย “ไม่ใช่ write-only attribute และต้อง persist ลง state” มีแค่_woattribute ที่ provider สร้างไว้ให้ชัดเจนเท่านั้นที่ผ่านได้ - “write-only argument เป็น feature ทั่วไปของ Terraform 1.11 ใช้กับ resource ไหนก็ได้” พิสูจน์ข้างบนด้วยการ dump provider schema จริงสองตัว
aws_db_instanceประกาศpassword_wo/password_wo_versionจริง ในขณะที่random_passwordบน toolchain เวอร์ชันเดียวกันเป๊ะไม่มี write-only attribute เลย — มันคือ opt-in ต่อ resource ต่อ provider ไม่ใช่ความสามารถที่ทุก resource ได้ฟรีตั้งแต่ 1.11
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review โค้ดที่ generate มาซึ่งแตะ secret หรือ ephemeral data เช็คสิ่งเหล่านี้
sensitive = trueถูกนำเสนอเป็น “ทางแก้” ของ credential โดยไม่พูดถึงว่า backend ยังเก็บมันแบบ plaintext อยู่ดี — พิสูจน์ข้างบน นี่คือช่องว่างที่บทนี้ปิดพอดี- ephemeral value ถูกอ้างถึงตรง ๆ ใน attribute ธรรมดา (ไม่ใช่
_wo) ของ resource สันนิษฐานว่า “ก็แค่ใช้ได้” เพราะ compile ผ่าน — พิสูจน์ข้างบนterraform planปฏิเสธด้วย error เฉพาะเจาะจง ไม่มีทางไปถึงapplyเลย - root-module
outputที่ตั้งephemeral = trueตั้งใจส่ง secret ที่ generate มาให้สิ่งภายนอก Terraform — พิสูจน์ข้างบน สิ่งนี้ถูกปฏิเสธตรง ๆ ephemeral output มีไว้ให้ child module ส่งค่ากลับขึ้นไปให้ผู้เรียกเท่านั้น nonsensitive()ถูกใช้ห่อค่าที่จริง ๆ เป็น secret แค่เพื่อปิด error “sensitive value in output” แทนที่จะตั้ง output เป็นsensitive = true— สิ่งนี้ทำลายการป้องกันเดียวที่sensitiveมีให้โดยไม่ได้อะไรคืนมาจริง- argument แบบ
password_wo/secret_string_woถูกใช้โดยไม่เพิ่ม_wo_versionคู่กัน แล้วมีรายงานว่า “password ใหม่ไม่เคย apply เลย” — พิสูจน์ข้างบน เลข version คือ signal เดียวที่ Terraform มีว่าต้องส่งค่า write-only ใหม่ เพราะมันไม่เคยเก็บค่าเองไว้ diff
🐛 หา bug ในโค้ดนี้
ephemeral "random_password" "db" {length = 16}
resource "aws_db_instance" "main" {identifier = "app-db"engine = "postgres"instance_class = "db.t3.micro"username = "app_admin"password = ephemeral.random_password.db.result}เฉลย
พิสูจน์ข้างบนด้วย error จริงสำหรับความผิดพลาดแบบเดียวกันบน terraform_data.marker — password บน aws_db_instance เป็น attribute ธรรมดาที่ persist ไม่ใช่ write-only Terraform ปฏิเสธ plan นี้ด้วย “Ephemeral values are not valid for …, because it is not a write-only attribute and must be persisted to state” ทางแก้ไม่ใช่ยอมแพ้เรื่อง generate password แบบ ephemeral แต่คือส่งมันผ่าน write-only pair จริงของ resource แทน password_wo = ephemeral.random_password.db.result คู่กับ password_wo_version ที่เพิ่มขึ้นทุกครั้งที่ควรเปลี่ยนค่า เขียนลง argument password ธรรมดาจะไปไม่ถึง AWS ด้วยซ้ำ — plan เองพังก่อน ซึ่งคือ safety net ที่ทำงานตามที่ออกแบบไว้ ไม่ใช่ bug ที่ต้องหลบด้วย sensitive = true (แบบนั้นจะแค่ redact การแสดงผล ค่า ephemeral ก็ยัง persist ลง attribute ธรรมดาไม่ได้อยู่ดี)