Drift Detection and Import
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”configuration drift คือสิ่งที่เกิดขึ้นเมื่อของจริงเบี่ยงเบนไปจาก Terraform config โดยที่ Terraform ไม่รู้ตัว และ terraform import (หรือ import block แบบ declarative) คือวิธีเอา resource ที่มีอยู่แล้วเข้ามาอยู่ใต้การจัดการของ Terraform เป็นครั้งแรก
Configuration drift
หัวข้อที่มีชื่อว่า “Configuration drift”drift เกิดขึ้นทุกครั้งที่มีใครหรืออะไรก็ตามแก้ resource นอก Terraform เคสที่พบบ่อยบน Google Cloud คือวิศวกรเปิด Console ตอนรีบ ๆ แล้วแก้ label ของ Compute Engine instance ตรง ๆ หรือเปลี่ยน machine type โดยไม่แตะ Terraform configuration ที่ควรจัดการ instance นั้นอยู่ state ที่ Terraform บันทึกไว้ตอนนี้ไม่ตรงกับ object จริงแล้ว
ไม่มีอะไรแจ้งเตือนเรื่องนี้อัตโนมัติ ครั้งถัดไปที่ใครก็ตามรัน
terraform planTerraform จะ refresh มุมมองของ resource จริง เทียบกับทั้ง configuration และ state ที่บันทึกไว้ล่าสุด แล้วแสดงความต่างออกมาเป็น plan — โดยทั่วไปคือ “in-place update” ที่จะย้อนการแก้มือกลับไปเป็นสิ่งที่ configuration บอกไว้ เพราะ Terraform ถือว่า configuration คือ source of truth
บางครั้งคุณอยากได้ผลตรงข้าม คือยอมรับการแก้มือแล้วแค่ทำให้ state ของ Terraform ตรงกับของจริง โดยไม่แตะ infrastructure จริงเลย นี่คือหน้าที่ของโหมด -refresh-only
terraform apply -refresh-onlyการนี้ update แค่ attribute ที่บันทึกไว้ใน state file ให้ตรงกับสิ่งที่รันอยู่จริงใน GCP ตอนนี้ ไม่สร้าง แก้ไข หรือลบ resource จริงเลย เป็นแค่การ reconcile ความจำของ Terraform กับโลกจริงเท่านั้น
output จริงของ -refresh-only พิสูจน์โดยไม่มี cloud credential เลย — resource local_file ที่ถูกแก้นอก Terraform (แก้ content ด้วยคำสั่ง shell ธรรมดา ไม่ใช่ Terraform) แล้ว refresh
$ echo "listen=9090" > generated/app.conf$ terraform plan -refresh-only
Note: Objects have changed outside of Terraform
Terraform detected the following changes made outside of Terraform since thelast "terraform apply" which may have affected this plan:
# local_file.app_config has been deleted - resource "local_file" "app_config" { - content = <<-EOT listen=8080 EOT -> null - id = "5d54098470458257525a27f6f0b6de89e278569d" -> null }
This is a refresh-only plan, so Terraform will not take any actions to undothese. If you were expecting these changes then you can apply this plan torecord the updated values in the Terraform state without changing any remoteobjects.สังเกตว่า provider local_file report ว่า resource นี้ถูก ลบ ไม่ใช่แก้ไข — id ของมันคือ content hash ดังนั้นแก้ byte ในไฟล์นอก Terraform หมายความว่า object ที่ id นั้นระบุไว้ไม่มีอยู่จริงอีกต่อไปในมุมมองของ read logic ของ provider แม้จะยังมีไฟล์อยู่ที่ path นั้นก็ตาม นี่คือ nuance เฉพาะ provider ที่ควรรู้ตอนอ่าน output ของ -refresh-only — “deleted” ไม่ได้แปลว่า “หายไปจาก disk” เสมอไป อาจแปลว่า “version เฉพาะที่ resource นี้แทนไม่ตรงกันแล้ว” ก็ได้
-detailed-exitcode เปลี่ยน drift ให้เป็น signal ที่ script อ่านได้
หัวข้อที่มีชื่อว่า “-detailed-exitcode เปลี่ยน drift ให้เป็น signal ที่ script อ่านได้”terraform plan -detailed-exitcode คือสิ่งที่ CI pipeline ใช้แยก “ไม่มีอะไรต้องทำ” กับ “มี plan ให้ review” กับ “มีอะไรพังจริง ๆ” แทนที่จะ parse text ของ plan output exit code จริงสามแบบ พิสูจน์แล้ว
$ terraform plan -detailed-exitcodeTerraform has compared your real infrastructure against your configurationand found no differences, so no changes are needed.$ echo $?0$ terraform plan -detailed-exitcodePlan: 1 to add, 0 to change, 0 to destroy.$ echo $?2$ terraform plan -detailed-exitcodeError: Reference to undeclared input variable
An input variable with the name "undefined_var" has not been declared.$ echo $?10 แปลว่า “ไม่มีการเปลี่ยนแปลง config ตรงกับความจริง” 2 แปลว่า “มี plan จริงที่มีการเปลี่ยนแปลง” (ไม่ใช่ error — CI ควรมองว่า “ต้อง review/apply” ไม่ใช่ “fail”) และ 1 แปลว่าเกิด error จริง เช่น configuration ผิดพลาด pipeline ที่เช็คแค่ “exit code ไม่ใช่ศูนย์ = fail” แยกไม่ออกระหว่าง change ที่ถูกต้อง (2) กับ configuration ที่พัง (1) — ปฏิบัติแบบเดียวกันจะบล็อก plan ปกติทุกอันหรือไม่ก็เมิน error จริงไปแบบเงียบ ๆ
moved เปลี่ยนชื่อ resource โดยไม่ต้องลบแล้วสร้างใหม่
หัวข้อที่มีชื่อว่า “moved เปลี่ยนชื่อ resource โดยไม่ต้องลบแล้วสร้างใหม่”การเปลี่ยนชื่อ resource ในไฟล์ .tf — local_file.config เป็น local_file.app_config หรือย้ายเข้าไปอยู่ใน module — สำหรับ Terraform แล้วดูเหมือน “address เก่าหายไป มี address ใหม่โผล่มา” เว้นแต่คุณจะบอกมันเป็นอย่างอื่น ถ้าปล่อยไว้เฉย ๆ มันจะอ่านเป็น destroy-แล้ว-create moved block บอก Terraform ว่านี่คือ object จริงตัวเดียวกันแค่เปลี่ยน address
moved { from = local_file.config to = local_file.app_config}
resource "local_file" "app_config" { filename = "${path.module}/generated/app.conf" content = "listen=8080\n"}output จริงของ plan พิสูจน์ว่าไม่มี destroy/recreate เกิดขึ้น กับ resource ที่มีอยู่แล้วใน state ภายใต้ชื่อเก่า
$ terraform planlocal_file.app_config: Refreshing state... [id=5d54098470458257525a27f6f0b6de89e278569d]
Terraform will perform the following actions:
# local_file.config has moved to local_file.app_config resource "local_file" "app_config" { id = "5d54098470458257525a27f6f0b6de89e278569d" # (10 unchanged attributes hidden) }
Plan: 0 to add, 0 to change, 0 to destroy.# local_file.config has moved to local_file.app_config พร้อม Plan: 0 to add, 0 to change, 0 to destroy คือหลักฐาน object จริงไม่เคยถูกแตะเลย มีแค่ address ใน state ที่เปลี่ยน moved block ถูกเพิ่มเข้ามาใน Terraform 1.1 เพื่อให้ refactor แบบนี้ปลอดภัยและ review ได้ เหมือน import กับ removed moved block ก็คือ configuration — มันไปอยู่ใน pull request, apply ได้เหมือนกันทุกที่ และ (ควร) ลบทิ้งได้เมื่อทุก environment apply ไปแล้วอย่างน้อยหนึ่งครั้ง สำหรับ google_compute_instance.web ที่เปลี่ยนชื่อเป็น google_compute_instance.app_server กลไกเดียวกันนี้หมายความว่า instance จริงยังคง disk, internal IP และ uptime เดิมไว้ — ถ้าไม่มี moved block VM จริงจะถูกลบแล้วสร้างตัวใหม่ขึ้นมาแทน พร้อม downtime ทั้งหมดที่ตามมา
การเอา resource ที่มีอยู่แล้วเข้ามาอยู่ใต้การจัดการด้วย terraform import
หัวข้อที่มีชื่อว่า “การเอา resource ที่มีอยู่แล้วเข้ามาอยู่ใต้การจัดการด้วย terraform import”resource บน GCP จำนวนมากถูกสร้างขึ้นก่อนที่ project จะมี Terraform เลยด้วยซ้ำ — Compute Engine instance ที่มีใครคลิกสร้างผ่าน Console หรือ Cloud Storage bucket ที่สร้างจาก script เก่า ๆ คำสั่ง CLI terraform import เชื่อม resource จริงที่มีอยู่แล้วตัวนั้นเข้ากับ resource address ที่เขียนไว้ใน configuration ของคุณ
terraform import google_compute_instance.web projects/my-project-id/zones/us-central1-a/instances/web-vmจุดที่พลาดง่ายคือ terraform import แค่เติม state entry เท่านั้น ไม่เขียน HCL ให้เลย คุณยังต้องเขียน resource "google_compute_instance" "web" { ... } block เองให้ attribute ใกล้เคียงของจริงมากพอ จนกว่า plan ครั้งถัดไปจะไม่แสดง diff ใหญ่ เขียน config ผิดแล้ว apply ครั้งถัดไปอาจพยายาม “แก้” resource ให้ตรงกับ config ที่ไม่เคยอธิบาย resource นั้นจริง ๆ
import block แบบ declarative
หัวข้อที่มีชื่อว่า “import block แบบ declarative”Terraform 1.5 เพิ่มวิธี import แบบที่สองที่ทีมส่วนใหญ่ตอนนี้ชอบใช้กับอะไรที่ควรค่าแก่การ review: import block เขียนตรงลงในไฟล์ .tf เคียงข้าง configuration อื่น
import { to = google_compute_instance.web id = "projects/my-project-id/zones/us-central1-a/instances/web-vm"}ข้อดีเหนือ CLI command คือเรื่อง review ล้วน ๆ การรัน CLI terraform import คือ side effect ที่เกิดทันที — มันเขียนลง state ทันทีที่คุณรัน ไม่มี preview เลย ส่วน import block เป็นแค่ configuration เพิ่มเติมที่โผล่ใน output ของ terraform plan เป็น planned import เหมือน planned create หรือ update ทำให้เพื่อนร่วมทีม review ได้ใน pull request ก่อนใครจะรัน apply Terraform ยัง generate resource block เริ่มต้นให้คุณได้จาก attribute จริงของ object ด้วย terraform plan -generate-config-out=generated.tf ซึ่งคุณค่อย review และเก็บงานต่อ แทนที่จะเขียนเองจากศูนย์ ตัวอย่าง GCP ข้างบนคือ HCL ที่ terraform validate ผ่านสะอาด — พิสูจน์ plan จริงสำหรับ google_compute_instance ต้องการ instance ID จริงกับ GCP project ที่คอร์สนี้ไม่มี สิ่งที่ตามมาคือกลไกเดียวกันที่พิสูจน์ end-to-end โดยไม่มี cloud credential เลย ใช้ built-in resource terraform_data ที่ตั้ง id เป็นอะไรก็ได้ตอน import
import { to = terraform_data.existing id = "my-existing-id-123"}
resource "terraform_data" "existing" { input = "v1"}output จริงของ plan
$ terraform planterraform_data.existing: Preparing import... [id=my-existing-id-123]terraform_data.existing: Refreshing state... [id=my-existing-id-123]
Terraform will perform the following actions:
# terraform_data.existing will be updated in-place # (imported from "my-existing-id-123") ~ resource "terraform_data" "existing" { id = "my-existing-id-123" + input = "v1" + output = (known after apply) }
Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.Plan: 1 to import, ... 1 to change คือ signal จริงและเป๊ะที่ import block สร้างขึ้น — 1 to change ตรงนี้เพราะ configuration ที่ input = "v1" ไม่มีอยู่ก่อนแล้วบน object ที่กำลัง import (terraform_data ไม่มี “read” จากภายนอกจริง ๆ ตัวอย่างนี้เลยแสดง change เสมอ ส่วน google_compute_instance จริงที่ import กับ config ที่ตรงกับ attribute จริงของ instance อยู่แล้วจะแสดง 0 to change)
จัดระเบียบ state โดยไม่แตะ infrastructure
หัวข้อที่มีชื่อว่า “จัดระเบียบ state โดยไม่แตะ infrastructure”มีอีกสองคำสั่ง terraform state ที่เติมเต็มบทนี้ และทั้งคู่คือภาพสะท้อนกัน
# Rename or move a resource's address in state, e.g. after refactoring into a moduleterraform state mv google_compute_instance.web module.app.google_compute_instance.web
# Stop tracking a resource in state, without destroying the real objectterraform state rm google_compute_instance.webterraform state mv แค่เปลี่ยน address ที่ Terraform ใช้อ้างถึง resource ใน state — มีประโยชน์เวลาปรับโครงสร้าง configuration เป็น module และไม่อยากให้ Terraform คิดว่าต้องลบ address เก่าแล้วสร้างใหม่ output จริง เปลี่ยนชื่อ terraform_data.a เป็น terraform_data.b หลังจากไฟล์ .tf ถูกแก้ไปแล้ว
$ terraform state mv terraform_data.a terraform_data.bMove "terraform_data.a" to "terraform_data.b"Successfully moved 1 object(s).$ terraform planNo changes. Your infrastructure matches the configuration.terraform state rm คือทิศตรงข้ามของ import — โดยลบ entry ของ resource ออกจาก state ทำให้ Terraform ไม่จัดการมันอีกต่อไป แต่ไม่มีผลกับ GCP object จริงเลย ซึ่งยังทำงานต่อเหมือนเดิมทุกอย่าง ตอนนี้แค่ไม่มีใครจัดการเท่านั้น
ทั้งสองคำสั่งมีข้อเสียเดียวกับที่คำสั่ง import แบบ CLI มี คือเกิดขึ้นทันที บนเครื่องเดียว และไม่มีอะไรใน repository บันทึกไว้ว่าเคยเกิดขึ้น state mv เทียบกับ moved block ที่พิสูจน์ไปก่อนหน้านี้คือ trade-off เดียวกันเป๊ะ ๆ state mv เป็น imperative และเกิดทันที — คุณพิมพ์คำสั่งครั้งเดียวบนเครื่องตัวเอง แล้วถ้าเพื่อนร่วมทีมไม่ได้รันคำสั่งเดียวกันเป๊ะ ๆ บนเครื่องของเขา plan ของเขาจะเห็น destroy-แล้ว-create ครั้งถัดไปที่ refactor แบบเดียวกัน moved เป็น declarative และอยู่ในไฟล์ .tf เอง ดังนั้นทุก clone ของ repository ได้ rename เดียวกันอัตโนมัติใน plan ครั้งถัดไป ไม่ต้องมีใครจำคำสั่งแยกต่างหากมารัน
เวอร์ชัน declarative: removed block
หัวข้อที่มีชื่อว่า “เวอร์ชัน declarative: removed block”terraform state rm มีข้อเสียเดียวกับคำสั่ง terraform import แบบเปล่า ๆ คือเป็น CLI side effect ที่เกิดทันทีโดยไม่มี plan step อยู่ข้างหน้าเลย Terraform 1.7 เพิ่มคู่แบบ declarative เข้ามา คือ removed block ซึ่งเป็นคู่ของ state rm เหมือนที่ import block เป็นคู่ของ terraform import
removed { from = google_compute_instance.web
lifecycle { destroy = false }}ลบ resource block ออกจาก configuration แล้วใส่ removed block นี้แทนที่ terraform plan ครั้งถัดไปจะแสดงการ remove ให้ review ก่อนอะไรจะเกิดขึ้น argument lifecycle คือส่วนที่สำคัญ destroy = false บอก Terraform ให้ลืม resource นั้นแต่ปล่อย instance จริงทำงานต่อ เหมือน state rm ค่า default คือ destroy = true ซึ่งลืม resource และ ลบมันด้วย — ถ้าลืมใส่ lifecycle block จะเงียบ ๆ เปลี่ยนการส่งต่อให้กลายเป็นการลบทิ้ง พอ apply ผ่านแล้วก็ลบ removed block ทิ้งได้เช่นกัน มันทำหน้าที่เสร็จแล้ว output จริงพิสูจน์ทาง destroy = false — resource terraform_data.existing ที่เคยถูกจัดการอยู่ ถูกเอาออกจาก config ด้วย removed block แทนที่จะลบตรง ๆ
$ terraform planPlan: 0 to add, 0 to change, 0 to destroy.
Warning: Some objects will no longer be managed by Terraform
If you apply this plan, Terraform will discard its tracking information forthe following objects, but it will not delete them: - terraform_data.existing
After applying this plan, Terraform will no longer manage these objects. Youwill need to import them into Terraform to manage them again.Plan: 0 to add, 0 to change, 0 to destroy คู่กับ warning นั้นคือหลักฐาน ไม่มีอะไรถูกทำลาย Terraform แค่หยุด track object นั้น ตรงตามที่ destroy = false สัญญาไว้เป๊ะ
flowchart LR manual["Instance label edited in Console"] --> plan["terraform plan"] plan -->|"diff shown"| drift["Drift detected"] existing["Existing untracked GCP resource"] --> importBlock["import block or terraform import"] importBlock -->|"state entry created"| state["terraform.tfstate"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุก block ในบทนี้มีไว้ให้ Terraform เปลี่ยน bookkeeping เกี่ยวกับ resource ได้โดยไม่ต้องเปลี่ยน ตัว resource เอง — เปลี่ยนชื่อ (moved), รับมาดูแล (import), ปล่อยออก (removed), หรือแก้ไขให้ตรง (-refresh-only) เส้นด้ายร่วมคือ state คือคำกล่าวอ้างเกี่ยวกับความจริง และเครื่องมือพวกนี้มีไว้แก้คำกล่าวอ้างตอนที่คำกล่าวอ้างนั่นแหละที่ผิด ไม่ใช่ความจริง -detailed-exitcode มีอยู่เพราะ “มี plan” กับ “มีอะไรพัง” เป็นข้อเท็จจริงคนละอย่างที่ script ต้องแยกออก และการ parse text ของ plan output ที่ทำมาให้คนอ่านคือวิธีที่ผิด
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
moved block แทน terraform state mv | เป็น declarative อยู่ใน repo apply เหมือนกันทุก clone (พิสูจน์ข้างบน Plan: 0 to add, 0 to change, 0 to destroy) | มี block เพิ่มที่ต้องจำไปลบทิ้งหลังทุก environment apply แล้ว |
import block แทน CLI terraform import | plan ได้และ review ได้ก่อนอะไรเกิดขึ้นจริง (พิสูจน์ข้างบน output Plan: 1 to import, ... จริง) และ -generate-config-out ช่วยร่าง HCL ให้ | ยังต้อง review (และมักต้องเก็บงานต่อ) resource block ที่ generate มา — ทั้งสองรูปแบบไม่ใช่ตัวแทนของการเข้าใจ resource นั้นจริง ๆ |
removed { lifecycle { destroy = false } } แทน terraform state rm | review ได้ใน PR apply เหมือนกันทุกที่ (พิสูจน์ข้างบน warning จริง + 0 to destroy) | มี block type เพิ่มให้จำตอน review — เข้าใจผิดง่ายว่า resource กำลังถูกทำลายถ้าไม่รู้ syntax |
-refresh-only ก่อนตัดสินใจว่าจะทำอะไรกับ drift | update ความเข้าใจและ state ที่บันทึกไว้ โดยไม่แตะ infrastructure จริงเลย (พิสูจน์ข้างบน) | ไม่แก้อะไรด้วยตัวเอง — เป็น diagnostic step ไม่ใช่ remediation |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “เปลี่ยนชื่อ resource ใน .tf แล้ว apply ใหม่ปลอดภัย ตราบใดที่ attribute เหมือนเดิม” พิสูจน์ข้างบน ถ้าไม่มี
movedblock Terraform ไม่มีทางรู้ว่า address ใหม่คือ object เดียวกัน — มันอ่านเป็น destroy-แล้ว-create เว้นแต่จะบอกมันเป็นอย่างอื่นgoogle_projectไปไกลกว่านั้นอีก argumentdeletion_policyของมัน default เป็น"PREVENT"ตั้งแต่ provider 6.0 ดังนั้นการ destroy โดยไม่ตั้งใจบนgoogle_projectที่ refactor พลาดจะไม่ถึงขั้นสำเร็จเป็น recreate ด้วยซ้ำ —applyfail ตรง ๆ ด้วย “Cannot destroy project as deletion_policy is set to PREVENT” - “
terraform import(หรือ import block) เขียน HCL ของ resource ให้เอง”importblock เดี่ยว ๆ ไม่ทำแบบนั้น — มันแค่เติม state entry-generate-config-outใกล้เคียงกว่า (ร่างresourceblock จาก attribute จริงของ object) แต่ผลลัพธ์ตั้งใจให้ review และเก็บงานต่อ ไม่ใช่ apply แบบไม่ดูอะไรเลย - “plan ที่ exit code ไม่ใช่ศูนย์ แปลว่ามีอะไรพังเสมอ” พิสูจน์ข้างบน
-detailed-exitcodereturn2สำหรับ change ที่ถูกต้อง แยกจาก1สำหรับ error จริง — CI script ที่มองทุก nonzero exit เป็น fail แยกไม่ออกระหว่าง “review plan นี้” กับ “นี่มันพัง” - “drift จาก
-refresh-onlyแสดงเป็น attribute ที่แก้ไขเสมอ” พิสูจน์ข้างบนด้วยตัวอย่างlocal_fileจริง content ที่แก้นอก Terraform แสดงเป็น resource “has been deleted” เพราะidของ provider คือ content hash และ version เฉพาะที่ hash นั้นแทนไม่มีอยู่แล้ว — ไม่ใช่ทุก drift ที่เป็น diff แบบ in-place สวย ๆ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review โค้ด drift/import/refactor ที่ generate มา เช็คสิ่งเหล่านี้
- resource ที่เปลี่ยนชื่อใน
.tfโดยไม่มีmovedblock คู่กัน — พิสูจน์ข้างบน สิ่งนี้อ่านเป็น destroy-แล้ว-create ถ้าไม่มี block นั้น สำหรับgoogle_projectโดยเฉพาะ ค่า defaultdeletion_policy = "PREVENT"(provider 6.0 ขึ้นไป) จะเปลี่ยนเรื่องนี้ให้เป็น apply fail ตรง ๆ แทนที่จะ recreate เงียบ ๆ terraform importหรือterraform state mv/rmถูกให้เป็น CLI instruction แบบ one-off ใน runbook โดยไม่พูดถึงว่ายังต้องมีmoved/import/removedblock ให้เพื่อนร่วมทีมกับ CI เห็นผลเดียวกันอัตโนมัติด้วยimportblock หรือ CLI import command ที่ไม่มีresourceblock คู่กัน หรือ comment ที่บอกเป็นนัยว่า import “setup configuration ให้” — พิสูจน์ข้างบน มีแค่ state entry ที่ถูกเติม-generate-config-outร่าง HCL ให้ก็จริง แต่ผู้เขียนยังต้อง review อยู่ดี- CI script หรือ runbook ที่เช็ค exit code ของ
terraform planด้วยif [ $? -ne 0 ]แทนที่จะใช้-detailed-exitcodeและจัดการ0/1/2แยกกันชัดเจน — พิสูจน์ข้างบน การเช็ค nonzero-exit เฉย ๆ แยก error จริงกับ plan ที่ถูกต้องไม่ออก removedblock ที่ขาดlifecycle { destroy = false }(หรือใส่destroy = trueทั้งที่ตั้งใจ “ปล่อยไว้เฉย ๆ” ชัดเจน) — argumentlifecycleคือสิ่งที่ต้องใส่และคือหัวใจทั้งหมดของ block นี้ ใส่กลับด้านจะลบ infrastructure จริงแทนที่จะปล่อยมันไป
🐛 หา bug ในโค้ดนี้
# Refactor: renamed the resource, forgot the moved blockresource "google_compute_instance" "app_server" {name = "app-server-vm"machine_type = "e2-medium"zone = "us-central1-a"
boot_disk { initialize_params { image = "debian-cloud/debian-12" }}}# (previously this block was named "web")เฉลย
พิสูจน์ข้างบนด้วย plan จริง (local_file.config -> local_file.app_config) การเปลี่ยนชื่อ resource block โดยไม่มี moved { from = ... to = ... } คู่กันเลย ไม่ได้อ่านเป็น rename สำหรับ Terraform เลย — มันอ่านว่า address เก่าหายไปแล้วมี address ใหม่โผล่มา ซึ่ง plan ออกมาเป็น ลบ object เก่า สร้างตัวใหม่ สำหรับ google_compute_instance.web -> google_compute_instance.app_server นั่นคือ Compute Engine instance จริงที่ถูกลบแล้วสร้างตัวใหม่ พร้อม downtime, instance ID ใหม่ และอาจมี internal/external IP ใหม่ด้วย ทั้งที่ผู้เขียนตั้งใจแค่จะ rename วิธีแก้คือใส่ moved block ใน commit เดียวกับที่ rename ซึ่งพิสูจน์ข้างบนแล้วว่าให้ Plan: 0 to add, 0 to change, 0 to destroy พร้อม note has moved to แทน