ข้ามไปยังเนื้อหา

Drift Detection and Import

configuration drift คือสิ่งที่เกิดขึ้นเมื่อของจริงเบี่ยงเบนไปจาก Terraform config โดยที่ Terraform ไม่รู้ตัว และ terraform import (หรือ import block แบบ declarative) คือวิธีเอา resource ที่มีอยู่แล้วเข้ามาอยู่ใต้การจัดการของ Terraform เป็นครั้งแรก

drift เกิดขึ้นทุกครั้งที่มีใครหรืออะไรก็ตามแก้ resource นอก Terraform เคสที่พบบ่อยบน Google Cloud คือวิศวกรเปิด Console ตอนรีบ ๆ แล้วแก้ label ของ Compute Engine instance ตรง ๆ หรือเปลี่ยน machine type โดยไม่แตะ Terraform configuration ที่ควรจัดการ instance นั้นอยู่ state ที่ Terraform บันทึกไว้ตอนนี้ไม่ตรงกับ object จริงแล้ว

ไม่มีอะไรแจ้งเตือนเรื่องนี้อัตโนมัติ ครั้งถัดไปที่ใครก็ตามรัน

Terminal window
terraform plan

Terraform จะ refresh มุมมองของ resource จริง เทียบกับทั้ง configuration และ state ที่บันทึกไว้ล่าสุด แล้วแสดงความต่างออกมาเป็น plan — โดยทั่วไปคือ “in-place update” ที่จะย้อนการแก้มือกลับไปเป็นสิ่งที่ configuration บอกไว้ เพราะ Terraform ถือว่า configuration คือ source of truth

บางครั้งคุณอยากได้ผลตรงข้าม คือยอมรับการแก้มือแล้วแค่ทำให้ state ของ Terraform ตรงกับของจริง โดยไม่แตะ infrastructure จริงเลย นี่คือหน้าที่ของโหมด -refresh-only

Terminal window
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 the
last "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 undo
these. If you were expecting these changes then you can apply this plan to
record the updated values in the Terraform state without changing any remote
objects.

สังเกตว่า 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 นี้แทนไม่ตรงกันแล้ว” ก็ได้

terraform plan -detailed-exitcode คือสิ่งที่ CI pipeline ใช้แยก “ไม่มีอะไรต้องทำ” กับ “มี plan ให้ review” กับ “มีอะไรพังจริง ๆ” แทนที่จะ parse text ของ plan output exit code จริงสามแบบ พิสูจน์แล้ว

$ terraform plan -detailed-exitcode
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
$ echo $?
0
$ terraform plan -detailed-exitcode
Plan: 1 to add, 0 to change, 0 to destroy.
$ echo $?
2
$ terraform plan -detailed-exitcode
Error: Reference to undeclared input variable
An input variable with the name "undefined_var" has not been declared.
$ echo $?
1

0 แปลว่า “ไม่มีการเปลี่ยนแปลง 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 plan
local_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 ของคุณ

Terminal window
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 นั้นจริง ๆ

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 plan
terraform_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)

มีอีกสองคำสั่ง terraform state ที่เติมเต็มบทนี้ และทั้งคู่คือภาพสะท้อนกัน

Terminal window
# Rename or move a resource's address in state, e.g. after refactoring into a module
terraform state mv google_compute_instance.web module.app.google_compute_instance.web
# Stop tracking a resource in state, without destroying the real object
terraform state rm google_compute_instance.web

terraform 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.b
Move "terraform_data.a" to "terraform_data.b"
Successfully moved 1 object(s).
$ terraform plan
No 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 ครั้งถัดไป ไม่ต้องมีใครจำคำสั่งแยกต่างหากมารัน

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 plan
Plan: 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 for
the following objects, but it will not delete them:
- terraform_data.existing
After applying this plan, Terraform will no longer manage these objects. You
will 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"]
การแก้มือถูกจับเป็น drift โดย plan ส่วน resource ที่มีอยู่แล้วแต่ยังไม่ถูก track ถูกนำเข้ามาผ่าน import

ทุก 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 importplan ได้และ review ได้ก่อนอะไรเกิดขึ้นจริง (พิสูจน์ข้างบน output Plan: 1 to import, ... จริง) และ -generate-config-out ช่วยร่าง HCL ให้ยังต้อง review (และมักต้องเก็บงานต่อ) resource block ที่ generate มา — ทั้งสองรูปแบบไม่ใช่ตัวแทนของการเข้าใจ resource นั้นจริง ๆ
removed { lifecycle { destroy = false } } แทน terraform state rmreview ได้ใน PR apply เหมือนกันทุกที่ (พิสูจน์ข้างบน warning จริง + 0 to destroy)มี block type เพิ่มให้จำตอน review — เข้าใจผิดง่ายว่า resource กำลังถูกทำลายถ้าไม่รู้ syntax
-refresh-only ก่อนตัดสินใจว่าจะทำอะไรกับ driftupdate ความเข้าใจและ state ที่บันทึกไว้ โดยไม่แตะ infrastructure จริงเลย (พิสูจน์ข้างบน)ไม่แก้อะไรด้วยตัวเอง — เป็น diagnostic step ไม่ใช่ remediation
  • “เปลี่ยนชื่อ resource ใน .tf แล้ว apply ใหม่ปลอดภัย ตราบใดที่ attribute เหมือนเดิม” พิสูจน์ข้างบน ถ้าไม่มี moved block Terraform ไม่มีทางรู้ว่า address ใหม่คือ object เดียวกัน — มันอ่านเป็น destroy-แล้ว-create เว้นแต่จะบอกมันเป็นอย่างอื่น google_project ไปไกลกว่านั้นอีก argument deletion_policy ของมัน default เป็น "PREVENT" ตั้งแต่ provider 6.0 ดังนั้นการ destroy โดยไม่ตั้งใจบน google_project ที่ refactor พลาดจะไม่ถึงขั้นสำเร็จเป็น recreate ด้วยซ้ำ — apply fail ตรง ๆ ด้วย “Cannot destroy project as deletion_policy is set to PREVENT”
  • “terraform import (หรือ import block) เขียน HCL ของ resource ให้เอง” import block เดี่ยว ๆ ไม่ทำแบบนั้น — มันแค่เติม state entry -generate-config-out ใกล้เคียงกว่า (ร่าง resource block จาก attribute จริงของ object) แต่ผลลัพธ์ตั้งใจให้ review และเก็บงานต่อ ไม่ใช่ apply แบบไม่ดูอะไรเลย
  • “plan ที่ exit code ไม่ใช่ศูนย์ แปลว่ามีอะไรพังเสมอ” พิสูจน์ข้างบน -detailed-exitcode return 2 สำหรับ 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 สวย ๆ

ตอน review โค้ด drift/import/refactor ที่ generate มา เช็คสิ่งเหล่านี้

  • resource ที่เปลี่ยนชื่อใน .tf โดยไม่มี moved block คู่กัน — พิสูจน์ข้างบน สิ่งนี้อ่านเป็น destroy-แล้ว-create ถ้าไม่มี block นั้น สำหรับ google_project โดยเฉพาะ ค่า default deletion_policy = "PREVENT" (provider 6.0 ขึ้นไป) จะเปลี่ยนเรื่องนี้ให้เป็น apply fail ตรง ๆ แทนที่จะ recreate เงียบ ๆ
  • terraform import หรือ terraform state mv/rm ถูกให้เป็น CLI instruction แบบ one-off ใน runbook โดยไม่พูดถึงว่ายังต้องมี moved/import/removed block ให้เพื่อนร่วมทีมกับ CI เห็นผลเดียวกันอัตโนมัติด้วย
  • import block หรือ CLI import command ที่ไม่มี resource block คู่กัน หรือ 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 ที่ถูกต้องไม่ออก
  • removed block ที่ขาด lifecycle { destroy = false } (หรือใส่ destroy = true ทั้งที่ตั้งใจ “ปล่อยไว้เฉย ๆ” ชัดเจน) — argument lifecycle คือสิ่งที่ต้องใส่และคือหัวใจทั้งหมดของ block นี้ ใส่กลับด้านจะลบ infrastructure จริงแทนที่จะปล่อยมันไป

🐛 หา bug ในโค้ดนี้

# Refactor: renamed the resource, forgot the moved block
resource "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 แทน

เพื่อนร่วมทีมแก้ Compute Engine instance ตรง ๆ ใน GCP Console มือ Terraform เปิดโปง configuration drift นี้ยังไงในครั้งถัดไปที่มีคนรัน terraform plan
ทำไมการแก้ content ของไฟล์นอก Terraform ถึงแสดงเป็น "has been deleted" แทนที่จะเป็น "has been modified"
$ echo "listen=9090" > generated/app.conf
$ terraform plan -refresh-only
...
  # local_file.app_config has been deleted
  - resource "local_file" "app_config" {
      - content = <<-EOT
            listen=8080
        EOT -> null
exit code 2 ตรงนี้แปลว่าอะไร และ CI script ควรจัดการมันต่างจาก exit code 1 ยังไง
$ terraform plan -detailed-exitcode
Plan: 1 to add, 0 to change, 0 to destroy.
$ echo $?
2
HCL block จริงตัวไหนที่ทำให้เกิด output แบบนี้เป๊ะ ๆ ตอน resource ถูกเปลี่ยนชื่อ
$ terraform plan
local_file.app_config: Refreshing state...
  # 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.
ความต่างสำคัญระหว่างคำสั่ง CLI terraform import กับ import block แบบ declarative คืออะไร
HCL จริงตัวไหนที่ทำให้เกิด output นี้ หลังจาก terraform_data.existing ถูกเอาออกจาก resource block
$ terraform plan
Plan: 0 to add, 0 to change, 0 to destroy.

Warning: Some objects will no longer be managed by Terraform
...
 - terraform_data.existing