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=${azurerm_postgresql_flexible_server.main.fqdn};Password=${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 เลื่อนผ่านหน้าจอใน shared CI log หรือ terminal ที่กำลัง screen-share อยู่โดยไม่ตั้งใจ
ประเด็นที่ควรพูดให้ชัดเจนไปเลยคือ sensitive = true เป็นฟีเจอร์ด้าน display ไม่ใช่ฟีเจอร์ด้านการเข้ารหัส ค่าถูกคำนวณ เก็บ และ diff โดย Terraform เหมือนกันทุกประการไม่ว่าคุณจะตั้ง sensitive หรือไม่ — ค่ายังไปอยู่ใน terraform.tfstate แบบ plaintext เต็ม ๆ ทั้ง tag ด้วย ใครก็ตามที่อ่านไฟล์ state ดิบได้ ไม่ว่าจากใน Azure Storage container, .tfstate บนเครื่อง local หรือจาก terraform state pull ก็เห็น password ตัวนั้นนั่งอยู่ใน JSON ตรง ๆ ไม่ว่าจะมี flag sensitive หรือไม่ก็ตาม
terraform state pull | grep -A3 db_connect_string# "db_connect_string": {# "value": "Server=app-db.postgres.database.azure.com;Password=S3cretPassword!",# "type": "string"ปกป้อง state ที่ rest
หัวข้อที่มีชื่อว่า “ปกป้อง state ที่ rest”เพราะการตั้งค่าเป็น sensitive ไม่ได้เอาค่านั้นออกจาก state การป้องกันจริง ๆ ต้องเกิดขึ้นที่ storage layer ให้ปฏิบัติกับ terraform.tfstate เหมือนเป็น secret ตัวหนึ่ง เหมือนที่คุณปฏิบัติกับไฟล์ .env ที่เต็มไปด้วย production credential
- Encryption at rest เปิดอยู่โดย default อยู่แล้ว Azure Storage เข้ารหัสทุก object ที่ rest โดยอัตโนมัติ รวมถึงทุก version ของ state blob ของคุณ ไม่ว่าใครจะเรียก storage account API ก็ตาม คุณไม่ต้องเปิดอะไรเพิ่มเอง — นี่คือ baseline ไม่ใช่คำตอบทั้งหมด
- จำกัดว่าใครอ่าน container นี้ได้ Azure RBAC คือสิ่งที่หยุดคนที่ไม่มีสิทธิ์ไม่ให้เข้าถึง plaintext ได้จริง ๆ ไม่ว่า encryption at rest จะมีอยู่หรือไม่ก็ตาม — encryption at rest ป้องกันคนที่ไปแตะ disk ดิบโดยตรง ไม่ใช่ป้องกันคนที่มี Azure identity ถูกต้องแล้วเรียก storage API จำกัด access ให้แคบด้วย role อย่าง
Storage Blob Data Readerที่ assign แค่ระดับ resource group หรือ storage account ที่เก็บ state เท่านั้น ไม่ใช่ทั้ง subscription
resource "azurerm_role_assignment" "state_reader" { scope = azurerm_storage_account.tfstate.id role_definition_name = "Storage Blob Data Reader" principal_id = data.azuread_group.platform_team.object_id}ถ้าเป็นไปได้ วิธีที่ดีกว่าคือหลีกเลี่ยงปัญหานี้ไปเลย อย่าใส่ secret ที่อยู่ยาวนานลงใน Terraform variable ตั้งแต่แรก ถ้าคุณดึงค่าตอน apply ได้แทน data source ที่อ่านจาก Azure Key Vault จะดึงค่าปัจจุบันตอนที่ Terraform รัน โดยที่คุณไม่ต้องพิมพ์ secret ลงไฟล์ .tf หรือ .tfvars เลย
data "azurerm_key_vault_secret" "db_password" { name = "app-db-password" key_vault_id = data.azurerm_key_vault.main.id}
resource "azurerm_postgresql_flexible_server" "main" { name = "app-db" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location version = "15" sku_name = "GP_Standard_D2s_v3" administrator_login = "appadmin" administrator_password = data.azurerm_key_vault_secret.db_password.value}ต้องมองให้ชัดว่าวิธีนี้แก้อะไรได้และแก้อะไรไม่ได้ ค่า secret ที่ resolve แล้วยังไปจบที่ state entry ของ azurerm_postgresql_flexible_server.main อยู่ดี เพราะ Terraform ต้องบันทึก attribute ที่ตั้งไว้ เหมือนความเสี่ยง plaintext จากบทแรกของโมดูลนี้เป๊ะ สิ่งที่แก้ได้จริง ๆ คือ lifecycle ของ secret ตัวนั้นในที่อื่นทั้งหมด — ไม่เคยถูกพิมพ์ลง variable ไม่เคยอยู่ในไฟล์ .tfvars ที่อาจถูก commit เข้าไปโดยไม่ตั้งใจ และการ rotate ใน Key Vault ก็ไม่ต้องแตะ Terraform configuration เลย การปกป้อง state backend เองยังคงเป็นส่วนที่ต่อรองไม่ได้อยู่ดี
จุดที่ shared state เริ่มไม่พอ
หัวข้อที่มีชื่อว่า “จุดที่ shared state เริ่มไม่พอ”บทก่อนหน้าตั้ง azurerm backend ที่มี locking ติดมาในตัว ซึ่งแก้ปัญหา “ทุกคนอ่านและเขียน state เดียวกันได้อย่างปลอดภัย” ได้แล้ว นั่นจำเป็น แต่ไม่พอเมื่อทีมโตเกินกว่าวิศวกรสองสามคน ถ้าไม่มีอะไรเพิ่มเติมนอกจากนั้น “ทุกคนรัน terraform apply จาก laptop ของตัวเองเมื่อไหร่ก็ได้ที่อยากรัน” ก็ยังมีช่องโหว่จริงอยู่
applyที่รันพร้อมกันจะไม่ทำให้อะไรเสียหายอีกต่อไปเพราะมี locking แล้ว แต่ก็ยังต้องต่อคิวกัน — run ของวิศวกรคนที่สองก็แค่รอเฉย ๆ โดยไม่รู้เลยว่า run แรกกำลังทำอะไรอยู่ หรือจะใช้เวลานานแค่ไหน- ไม่มีจุดไหนที่คนที่สอง review การเปลี่ยนแปลงก่อนขึ้น production จริง locking ป้องกันแค่ไม่ให้สอง apply แข่งกันเขียนพร้อมกัน แต่ไม่ได้หยุดวิศวกรคนหนึ่งจาก apply การเปลี่ยนแปลงที่ไม่มีใครดูมาก่อนเลย
- วิศวกรแต่ละคนต้องมี Azure credential ที่ valid และมีสิทธิ์พอจะรัน
applyจากเครื่องตัวเองได้ นั่นเป็น blast radius ที่กว้างกว่าที่ทีมส่วนใหญ่อยากแจกให้ทุก laptop
ทั้งหมดนี้ไม่ได้แปลว่า shared backend จากบทก่อนหน้าผิด — เป็นสิ่งที่ต้องมีก่อน ไม่ใช่ความผิดพลาด แค่ยังไม่ใช่เรื่องทั้งหมด การปิดช่องว่างนี้คือหน้าที่ของ CI/CD สำหรับ infrastructure โดยตรง — pipeline รัน plan กับทุกการเปลี่ยนแปลงที่เสนอมาเพื่อให้ 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"] kv["Azure Key Vault"] -->|data source at apply time| res["Resource attribute"] res -->|attribute recorded| state