The State File
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”state ของ Terraform คือไฟล์ JSON ที่แมป resource แต่ละตัวใน configuration ของคุณเข้ากับ object จริงที่สร้างขึ้น และเป็นสิ่งเดียวที่ทำให้ Terraform รู้ว่าตัวเองจัดการอะไรอยู่
ทำไม Terraform ถึงต้องมี state
หัวข้อที่มีชื่อว่า “ทำไม Terraform ถึงต้องมี state”ตอนคุณรัน terraform apply Terraform สร้าง resource ผ่าน Azure Resource Manager API — virtual machine, storage account, resource group สักตัว Azure API ไม่มีแนวคิดเรื่อง “Terraform” เลย ถ้า subscription ของคุณมี virtual machine 500 ตัว Azure API ก็บอกไม่ได้ว่า VM ตัวไหนคือตัวที่ Terraform สร้างให้กับ resource "azurerm_linux_virtual_machine" "web" block นี้ ตัวไหนที่เพื่อนร่วมทีมสร้างมือใน Azure Portal และตัวไหนเป็นของ Terraform configuration อื่นไปเลย
Terraform แก้ปัญหานี้ด้วยการเก็บ record ของตัวเอง ทุกครั้งที่ apply Terraform จะเขียน mapping ระหว่าง resource address ใน configuration กับ ID จริงของ infrastructure ลงไฟล์ state ที่ชื่อ default คือ terraform.tfstate
resource "azurerm_linux_virtual_machine" "web" { name = "web-vm" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location size = "Standard_B2s" admin_username = "azureuser"
network_interface_ids = [ azurerm_network_interface.web.id, ]}หลัง apply ไฟล์ state จะบันทึกประมาณว่า azurerm_linux_virtual_machine.web -> /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-resources/providers/Microsoft.Compute/virtualMachines/web-vm path ยาว ๆ นั้นแหละคือหน้าตาจริงของ “real-world object” บน Azure — ทุก resource ถูกระบุด้วย resource ID แบบเต็มที่เข้ารหัส subscription, resource group, provider namespace และชื่อ resource ไว้ในสตริงเดียว นอกจาก ID แล้ว state ยัง cache ทุก attribute ที่ Azure ส่งกลับมาสำหรับ VM ตัวนั้นด้วย — private IP, OS disk ID, network interface ที่ผูกอยู่ และอื่น ๆ ข้อมูลที่ cache ไว้นี้ทำให้ Terraform ไม่ต้องเรียก Azure API แล้ว describe resource ทุกตัวใหม่แค่เพื่อคำนวณ plan เพราะเทียบ configuration กับ state ล่าสุดก่อนได้เลย นี่คือทั้งวิธีตรวจจับว่าอะไรเปลี่ยน และเป็นข้อดีด้าน performance จริง ๆ กับ configuration ขนาดใหญ่ state ยังเป็นที่เก็บ dependency metadata ด้วย ทำให้ Terraform รู้ลำดับที่ต้องสร้าง แก้ไข หรือลบ resource
ความเสี่ยงของ plaintext ที่ซ่อนอยู่ใน state file
หัวข้อที่มีชื่อว่า “ความเสี่ยงของ plaintext ที่ซ่อนอยู่ใน state file”state file ไม่ได้ผ่านการ sanitize attribute value ใด ๆ ที่ Terraform ได้รับจาก provider — หรือที่คุณตั้งตรงใน configuration — จะไปอยู่ใน terraform.tfstate แบบ plaintext รวมถึงค่าที่เป็น secret ด้วย ตัวอย่างที่เจอบ่อย
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 = "S3cretPassword!"}คุณไม่เคยรัน terraform output กับ password ตัวนั้น และไม่เคย print ใน log แต่ค่านั้นก็ยังไปอยู่ใน terraform.tfstate แบบ plaintext อยู่ดี เพราะ Terraform ต้องจำ attribute ทุกตัวที่ตั้งไว้ เพื่อตรวจ drift ตอน plan ครั้งถัดไป ใครก็ตามที่อ่านไฟล์นั้นได้ก็อ่าน password ได้ด้วย
นี่คือเหตุผลที่ terraform.tfstate ห้าม commit เข้า version control เด็ดขาด ใส่ลง .gitignore ตั้งแต่ commit แรกของโปรเจกต์ใหม่
terraform.tfstateterraform.tfstate.backup*.tfstate*.tfstate.*บทถัดไปจะพูดถึงการย้าย state ออกจาก laptop ไปไว้บน remote backend ที่ควบคุม access ได้จริง นั่นคือวิธีแก้ความเสี่ยงนี้ที่แท้จริง — .gitignore แค่กันอุบัติเหตุที่เห็นชัดที่สุดเท่านั้น
การดู state
หัวข้อที่มีชื่อว่า “การดู state”Terraform มีคำสั่งสามตัวสำหรับดูข้างใน state โดยไม่ต้องเปิดไฟล์ JSON เอง
# Human-readable dump of the entire current stateterraform show
# List every resource address currently tracked in stateterraform state list
# Show full attribute detail for one specific resourceterraform state show azurerm_linux_virtual_machine.webterraform state list คือวิธีเร็วที่สุดในการตอบคำถามว่า “configuration นี้จัดการอะไรอยู่ตอนนี้” คำสั่งนี้ print address อย่าง azurerm_linux_virtual_machine.web หรือ module.network.azurerm_virtual_network.main โดยไม่มีรายละเอียดอื่น terraform state show <address> จะลงลึกไป address ใดตัวหนึ่ง แล้ว print attribute ทุกตัวที่ Terraform บันทึกไว้ ซึ่งบ่อยครั้งเร็วกว่าการไปเปิด Azure Portal เวลาต้องเช็คค่าที่ Terraform track อยู่
กายวิภาคของ state file จริง
หัวข้อที่มีชื่อว่า “กายวิภาคของ state file จริง”ตัวอย่างข้างล่างนี้ไม่ได้เขียนจากความจำ — มาจาก terraform apply จริงกับ resource สองตัวที่ไม่ต้องใช้ cloud credential เลย: terraform_data (ตัวแทน null_resource ที่ deprecated ไปแล้วของ built-in provider) และ local_file (hashicorp/local) นี่คือ JSON structure ชุดเดียวกับที่ state ของ Azure ใช้ — format ไม่ขึ้นกับ provider ต่างกันแค่สตริง provider กับ type/attributes ของ resource ถ้าเป็น azurerm_* จริง configuration:
terraform { required_providers { local = { source = "hashicorp/local" version = "~> 2.5" } }}
resource "terraform_data" "trigger" { input = "v1"}
resource "local_file" "config" { filename = "${path.module}/generated/app.conf" content = "listen=8080\ntrigger_id=${terraform_data.trigger.output}\n"}หลัง terraform init -backend=false && terraform apply -auto-approve ไฟล์ terraform.tfstate จริงบน disk (ตัด machine path ออกแล้ว)
{ "version": 4, "terraform_version": "1.16.4", "serial": 6, "lineage": "91789eb2-a206-8e38-2571-62323b581164", "outputs": {}, "resources": [ { "mode": "managed", "type": "local_file", "name": "config", "provider": "provider[\"registry.terraform.io/hashicorp/local\"]", "instances": [ { "schema_version": 0, "attributes": { "content": "listen=8080\ntrigger_id=v1\n", "content_sha1": "7551ae2164513c74b5c0665a6b28b6a40399686f", "filename": "./generated/app.conf", "id": "7551ae2164513c74b5c0665a6b28b6a40399686f" }, "dependencies": ["terraform_data.trigger"], "sensitive_attributes": [[{ "type": "get_attr", "value": "sensitive_content" }]] } ] }, { "mode": "managed", "type": "terraform_data", "name": "trigger", "provider": "provider[\"terraform.io/builtin/terraform\"]", "instances": [ { "schema_version": 0, "attributes": { "id": "7e48645a-6b7e-14c9-d8b3-99d042610145", "input": { "value": "v1", "type": "string" }, "output": { "value": "v1", "type": "string" } }, "sensitive_attributes": [] } ] } ], "check_results": null}field ระดับบนสุดแต่ละตัวมีความหมายเฉพาะ
version— เวอร์ชันของ format ไฟล์ state (เป็น 4 มาตั้งแต่ Terraform 0.12 — ไม่เกี่ยวกับเวอร์ชันของตัว Terraform binary)terraform_version— เวอร์ชัน CLI จริงที่เขียนไฟล์นี้ครั้งล่าสุดserial— ตัวนับที่เพิ่มขึ้นทุกครั้งที่เขียน state ไฟล์ state สองไฟล์ที่มีlineageเดียวกันจะถูกเทียบกันด้วยserialเพื่อดูว่าไฟล์ไหนใหม่กว่าlineage— UUID ที่ generate ครั้งเดียวตอนสร้าง state ให้ working directory ครั้งแรก มันไม่เปลี่ยนตลอดอายุของ state นั้น และเป็นวิธีที่ Terraform แยก “state เดียวกันแต่เป็นก็อปปี้เก่ากว่า” ออกจาก “state คนละไฟล์กันที่หน้าตาคล้ายกันเฉย ๆ”resources[]— หนึ่ง entry ต่อ resource block (ไม่ใช่ต่อ instance —count/for_eachใช้ entry เดียวกันแต่มีหลายinstances[]) แต่ละตัวมีmode,type,name,provider, และinstancesถ้าเป็นazurerm_linux_virtual_machineจริงtypeจะเป็น"azurerm_linux_virtual_machine"และproviderจะเป็นprovider["registry.terraform.io/hashicorp/azurerm"]instances[].attributes— attribute ทุกตัวที่ provider ส่งกลับมาหลัง create/read ข้อมูลชุดเดียวกับที่terraform state showprint ออกมาinstances[].dependencies— resource address ที่ configuration ของ instance นี้อ้างถึง (พิสูจน์ข้างบน:local_file.configdepend onterraform_data.triggerเพราะcontentของมัน interpolateterraform_data.trigger.output) นี่คือ dependency metadata ที่พูดถึงข้างบนเป๊ะ ๆ — เป็นเหตุผลที่destroyรื้อ resource ตามลำดับที่ถูกต้องได้โดยไม่ต้องคำนวณ dependency ใหม่ตั้งแต่ต้น (VM ต้องถูกลบก่อน network interface ที่มันพึ่งพา เป็นต้น)
terraform show -json ข้อมูลชุดเดียวกัน แต่ provider-agnostic และเสถียร
หัวข้อที่มีชื่อว่า “terraform show -json ข้อมูลชุดเดียวกัน แต่ provider-agnostic และเสถียร”terraform state show ข้างบนเป็น format ที่อ่านง่ายสำหรับคน terraform show -json คือ machine-readable contract ที่ tool อื่น (Sentinel, OPA/conftest, custom CI check) ต้อง parse — มัน document ไว้ชัดเจน มี version (format_version) และไม่เปลี่ยนรูปแบบระหว่างเวอร์ชัน provider เหมือน internal ของ raw state บางครั้งเป็น output จริง (ตัดทอน) จาก apply เดียวกัน
{ "format_version": "1.0", "terraform_version": "1.16.4", "values": { "root_module": { "resources": [ { "address": "local_file.config", "mode": "managed", "type": "local_file", "name": "config", "provider_name": "registry.terraform.io/hashicorp/local", "values": { "content": "listen=8080\ntrigger_id=v1\n", "id": "7551ae2164513c74b5c0665a6b28b6a40399686f" }, "sensitive_values": { "sensitive_content": true }, "depends_on": ["terraform_data.trigger"] } ] } }}drift-detection-and-import.mdx และ sensitive-data-and-team-workflows.mdx ต่างก็กลับมาใช้ show -json เพราะ proof ของ -detailed-exitcode และ sensitive_values map ทั้งคู่อยู่ใน structure นี้เป๊ะ ๆ
ทำไมการแก้ state ด้วยมือถึงพัง lineage กับ serial ถูกบังคับใช้จริง ไม่ใช่แค่ตกแต่ง
หัวข้อที่มีชื่อว่า “ทำไมการแก้ state ด้วยมือถึงพัง lineage กับ serial ถูกบังคับใช้จริง ไม่ใช่แค่ตกแต่ง”terraform state push <file> อัพโหลด state file ในเครื่องไปแทนที่สิ่งที่ backend ถืออยู่ตอนนี้ — และมันบังคับตรวจ lineage กับ serial ก่อนจะยอมทำแบบนั้น เพื่อกันไม่ให้คุณเอา state เก่าหรือไม่เกี่ยวข้องไปทับ state ที่ใหม่กว่า พิสูจน์ เอา state จริงข้างบน เปลี่ยน lineage เป็น UUID สุ่ม แล้ว push กลับ
$ terraform state push tampered.tfstateFailed to write state: cannot import state with lineage "00000000-0000-0000-0000-000000000000" over unrelated state with lineage "91789eb2-a206-8e38-2571-62323b581164"state เดียวกัน แต่ตั้ง serial ให้ย้อนกลับแทน
$ terraform state push tampered2.tfstateFailed to write state: cannot import state with serial 1 over newer state with serial 6ทั้งสองคือ error จริงจาก Terraform 1.16.4 ไม่ได้จำลอง นี่คือเหตุผลที่การแก้ terraform.tfstate ด้วย text editor ตรง ๆ อันตราย แม้จะตั้งใจแก้แค่ field เดียว เว้นแต่คุณจะเพิ่ม serial ให้ถูกต้องด้วย (และไม่แตะ lineage เลย) consistency check ของ Terraform เองจะปฏิเสธการเขียนทันทีตอนคุณพยายาม push กลับ — และถ้าคุณข้าม check นี้ไปด้วยการเขียนทับ backend object ตรง ๆ (ข้าม state push ไปเลย เช่น copy ไฟล์ทับ Azure Storage blob ด้วยมือ) ก็ไม่มีอะไรหยุดคุณได้ ซึ่งนี่คือ failure mode ตัวจริง state เสียหายแบบเงียบ ไม่มี error เลย จะรู้ตัวอีกทีก็ตอน plan ครั้งถัดไป ที่ความเข้าใจของ Terraform ไม่ตรงกับ Azure resource ที่ deploy จริงอีกต่อไป terraform state mv, import, และ rm ถูกสร้างมาเพื่อไม่ให้คุณต้องเปิด JSON ด้วยมือเลย — มันอัพเดต serial ให้ถูกต้องในตัวเป็นส่วนหนึ่งของ operation เดียวกัน
Azure backend เก็บอะไรจริง ๆ
หัวข้อที่มีชื่อว่า “Azure backend เก็บอะไรจริง ๆ”คอร์สนี้ใช้ local backend สำหรับ proof ทุกอันข้างบน (ไม่ต้องใช้ credential) แต่ production state บน Azure อยู่บน azurerm backend ตาม documentation ของ azurerm backend เอง
“Stores the state as a Blob with the given Key within the Blob Container within the Blob Storage Account.” — developer.hashicorp.com/terraform/language/backend/azurerm
ดังนั้น state JSON ที่แสดงข้างบนคือสิ่งที่ไปอยู่ใน blob นั้นเป๊ะ ๆ — shape เดียวกันของ version/serial/lineage/resources ไม่มีอะไรเฉพาะ Azure เพิ่มเข้ามา การ lock ไม่ใช่ object ที่สองแบบที่ S3 native locking เป็น “This backend supports state locking and consistency checking with Azure Blob Storage native capabilities” — azurerm backend ใช้ blob lease บน state blob เองตลอดช่วงเวลาที่ operation ทำงาน แทนที่จะเขียน lock file หรือ lock record แยกต่างหาก remote-backends-and-locking.mdx พูดถึง backend block เต็ม ๆ (use_azuread_auth, use_oidc), migration, และ force-unlock
flowchart LR cfg["Terraform config (.tf files)"] --> apply["terraform apply"] apply --> azure["Real Azure resources"] azure -->|attributes written back| state["terraform.tfstate"] cfg -->|next run| plan["terraform plan"] state -->|read for comparison| plan
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”state คือความจำเดียวของ Terraform เกี่ยวกับความจริง — ไม่ใช่ cache ที่ทิ้งได้ปลอดภัย และไม่ใช่ log ที่ append เพิ่มด้วยมือได้ปลอดภัย ทุกอย่างในบทนี้มาจากข้อเท็จจริงเดียว Azure Resource Manager API ไม่รู้จัก Terraform เลย ทุกบิตของ “สร้างอะไรไปแล้ว หน้าตาเป็นยังไง” ต้องอยู่ที่ที่ Terraform ควบคุมได้เต็มที่ lineage กับ serial ไม่ใช่ metadata ให้คนอ่านเฉย ๆ — มันคือกลไกที่ทำให้ state push เป็น operation ที่ปลอดภัยและถูกตรวจสอบ แทนที่จะเป็นการเขียนทับแบบไม่ดูอะไร ปฏิบัติกับ state file เหมือนกับ transaction log ของ database คุณอ่านมันด้วย tool ที่สร้างมาเพื่ออ่าน (state show, show -json, state pull) และแก้มันด้วยคำสั่งที่สร้างมาเพื่อแก้ (state mv, import, rm) — ไม่ใช่ text editor เด็ดขาด
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
| cache attribute ทุกตัวไว้ใน state | plan เทียบกับข้อมูล cache แทนที่จะ describe resource ทุกตัวใหม่จาก Azure API — ได้ performance จริงเมื่อ scale ใหญ่ | attribute ทุกตัว รวม secret อยู่แบบ plaintext บน disk หรือใน blob (พิสูจน์ข้างบน) |
consistency check lineage/serial บน state push | state เก่าหรือไม่เกี่ยวข้องเขียนทับ state ปัจจุบันแบบเงียบ ๆ ไม่ได้ (พิสูจน์ข้างบนสองครั้ง) | ป้องกันแค่ทาง state push — การเขียนทับ blob ตรง ๆ ข้าม check นี้ได้เลย ไม่มี error |
terraform show -json เป็น machine format ที่เสถียร | tool (policy check, CI, custom script) พึ่ง schema ที่ document ไว้ มี version แทนที่จะ parse output ที่อ่านง่ายสำหรับคน | มี representation ของข้อมูลชุดเดียวกันสองแบบให้จำ raw terraform.tfstate กับ show -json ไม่ใช่ shape เดียวกัน |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “state เก็บแค่สิ่งที่ใส่ใน
outputblock” พิสูจน์ข้างบน attribute เต็มชุดของlocal_file.config— รวมค่าที่มาจากการคำนวณที่ไม่เคย output เลย — ถูกเขียนลงresources[].instances[].attributesไม่ว่าจะมีoutputblock หรือไม่ - “แก้
terraform.tfstateด้วยมือได้ปลอดภัย ถ้าระวังพอ” พิสูจน์ข้างบนstate pushปฏิเสธการเปลี่ยนlineageและserialที่ย้อนกลับด้วย error จริง ความอันตรายจริง ๆ อยู่ที่กรณีที่ error นั้นไม่ครอบคลุม — การเขียนทับ Azure Storage blob ตรง ๆ ที่ไม่มี error หรือ warning ใด ๆ - “
serialคือจำนวน resource หรือ version number ที่เราตั้งเองได้” มันคือตัวนับการเขียนภายในที่ Terraform เพิ่มเอง คุณไม่เคยตั้งมันเอง และการตั้งเป็นค่าที่ไม่ใช่ “สูงกว่าค่าปัจจุบัน” คือสิ่งที่state pushปฏิเสธในพิสูจน์ข้างบน - “การ lock state บน Azure ต้องใช้ lock file หรือ table แยกต่างหาก เหมือน S3 ที่ใช้ DynamoDB หรือ
.tflockobject” พิสูจน์จาก docs ของ azurerm backend เองข้างบน มัน lock ด้วยการเอา blob lease บน state blob ตรง ๆ ไม่มี lock resource แยกให้ provision
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review Terraform ที่ generate มาแล้วแตะ state ตรง ๆ เช็คสิ่งเหล่านี้
- script ที่เปิด
terraform.tfstateด้วยsed/jq -i/text editor เพื่อ “แก้” ค่าเดียว ณ ที่นั้น แล้วหวังให้applyครั้งถัดไป pick up ไป — พิสูจน์ข้างบน อย่างดีก็โดนstate pushปฏิเสธด้วย checklineage/serialอย่างแย่ blob เสียหายแบบเงียบ ๆ ถ้า check ถูกข้ามไป .gitignoreไม่มี*.tfstate*เลย หรือมีแค่terraform.tfstateแต่ขาดterraform.tfstate.backupกับ*.tfstate.*(workspace variant)- โค้ดหรือ runbook ที่คิดว่า
serialเป็นเลขที่ตั้งเองได้ปลอดภัย (“ตั้ง serial เป็น 1 เพื่อ reset”) — มันคือตัวนับการเขียนแบบ monotonic ที่ backend บังคับใช้ ไม่ใช่ field ให้เลือกเลขเอง - custom drift-checking script ที่อ่าน
terraform.tfstateตรง ๆ ด้วย local path ที่ hardcode ไว้ แทนที่จะใช้terraform state pullซึ่งพังทันทีที่โปรเจกต์เปลี่ยนไปใช้ remoteazurermbackend - คิดว่าการ lock state บน Azure ต้องมี lock table แบบ DynamoDB แยกต่างหาก — พิสูจน์ข้างบน azurerm backend ใช้ blob lease บน state blob เอง ไม่มี resource แยกให้ provision
🐛 หา bug ในโค้ดนี้
#!/usr/bin/env bash# "quick fix" for a wrong VM size recorded in statesed -i '' 's/"Standard_B2s"/"Standard_B4ms"/' terraform.tfstateterraform state push terraform.tfstateเฉลย
มีปัญหาจริงสองอย่าง พิสูจน์ข้างบน อย่างแรก state push บังคับตรวจ lineage/serial — แต่ sed -i ตรงนี้ไม่ได้เปลี่ยนทั้งสอง field เลย push อาจสำเร็จได้จริง ๆ ซึ่งคือกับดัก Terraform เชื่อว่า VM มี size เป็น Standard_B4ms ไปแล้ว ทั้งที่ Azure ยังรัน VM แบบ Standard_B2s อยู่ ไม่มีการเรียก Azure API มา verify เลย plan ครั้งถัดไปจะไม่แสดงการเปลี่ยนแปลงใด ๆ (state ตอนนี้ “เห็นด้วย” กับค่าที่แก้ไปแบบผิด ๆ แล้ว) drift ระหว่าง state ที่แก้กับ VM จริงจะไม่ถูกจับได้เลยจนกว่าอย่างอื่นจะทำให้เห็น — บิลที่ไม่ตรง หรือมีคนเช็คใน portal วิธีแก้ที่ถูกต้องเมื่อ “state ไม่ตรงกับความจริง” ไม่ใช่การแก้มือเด็ดขาด — คือ terraform apply (ถ้า config ควรชนะ), terraform plan -refresh-only (เพื่อดูว่าอะไรเปลี่ยน), หรือ terraform import/state rm (พูดถึงใน drift-detection-and-import.mdx) ทั้งหมดนี้ผ่านกลไก consistency ของ Terraform เอง ไม่ใช่การข้ามมันไป