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

Drift Detection and Import

configuration drift เกิดขึ้นเมื่อของจริงเปลี่ยนไปโดยไม่ผ่าน Terraform ส่วน import คือวิธีเอา resource จริงที่มีอยู่แล้วเข้ามาไว้ใน state ของ Terraform โดยไม่ต้องสร้างใหม่

state เป็นแค่ snapshot ที่บันทึกไว้ ณ ตอน apply ครั้งล่าสุดเท่านั้น ไม่มีอะไรกันไม่ให้ใครเปิด AWS Console แล้วแก้ resource ตรง ๆ เช่นแก้ tag ของ EC2 instance เปลี่ยนขนาด RDS instance หรือเปิด port บน security group ด้วยมือ Terraform ไม่มีทางรู้เรื่องนี้จนกว่าจะไปดูใหม่อีกครั้ง

resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}

สมมติเพื่อนร่วมทีมแก้ tag Name ของ instance นั้นใน console เป็น "web-server-old" โดยไม่ผ่าน Terraform run ใด ๆ เลย ครั้งถัดไปที่ใครก็ตามรัน terraform plan Terraform จะ refresh มุมมองของ resource จริง สังเกตว่า tag ไม่ตรงกับ configuration แล้ว แล้วแสดง diff ออกมา — โดยเสนอให้เปลี่ยน tag กลับเป็น "web-server" เพราะสำหรับ Terraform แล้ว configuration คือ source of truth และการแก้ผ่าน console คือ drift ที่ต้องแก้ไขให้ตรงกัน

Terminal window
terraform plan
# ~ resource "aws_instance" "web" {
# ~ tags = {
# ~ "Name" = "web-server-old" -> "web-server"
# }
# }

นั่นคือวิธีปกติที่ plan เปิดโปง drift ออกมา — เป็น change ที่เสนอไว้ ซึ่งจะเขียนทับของจริงด้วย configuration ในครั้งถัดไปที่คุณ apply บางครั้งคุณอยากได้ผลตรงข้าม คืออยากให้ Terraform แค่ update record ของตัวเองให้ตรงกับของจริง โดยไม่แตะ infrastructure จริงเลย นี่คือหน้าที่ของ -refresh-only

Terminal window
terraform apply -refresh-only

โหมดนี้ refresh state จาก AWS resource จริง แล้วแสดงให้ดูว่าอะไรเปลี่ยนไป จากนั้นถามให้ยืนยันก่อนเขียนการเปลี่ยนแปลงนั้นลง terraform.tfstate — แต่ไม่แก้ AWS จริงเด็ดขาด นี่คือเครื่องมือที่ถูกต้องเวลา drift เกิดขึ้นโดยตั้งใจ และคุณตัดสินใจแล้วว่าจะให้การแก้ผ่าน console ชนะ หรือเวลาที่คุณต้อง update ความเข้าใจว่า state ตอนนี้เก็บอะไรอยู่ ก่อนจะตัดสินใจว่าจะแก้ configuration หรือยอมรับของจริง

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 ไปแล้วอย่างน้อยหนึ่งครั้ง

การเอา resource ที่มีอยู่แล้วเข้ามาอยู่ใต้การจัดการ

หัวข้อที่มีชื่อว่า “การเอา resource ที่มีอยู่แล้วเข้ามาอยู่ใต้การจัดการ”

drift สมมติว่า Terraform จัดการ resource นั้นอยู่แล้ว แต่มีอีกปัญหาที่เกี่ยวกันคือ resource ที่มีอยู่จริงบน AWS แต่ไม่เคยถูก Terraform สร้างเลย — อาจมีมาก่อนที่ทีมจะเริ่มใช้ Terraform หรือมีใครคลิกสร้างผ่าน console ตอนเกิด outage terraform import คือคำสั่ง CLI ที่เชื่อม resource จริงตัวนั้นเข้ากับ resource address ใน configuration ของคุณ

Terminal window
terraform import aws_instance.web i-0123456789abcdef0

การรัน import แค่เขียน state entry เท่านั้น — โดยแมป aws_instance.web เข้ากับ instance ID จริง แล้วดึง attribute ปัจจุบันทั้งหมดของ instance นั้นเข้ามา คำสั่งนี้ ไม่ เขียน resource "aws_instance" "web" { ... } block ให้คุณ คุณยังต้องเขียน HCL เองให้ attribute ตรงกับของจริงมากพอ จนกว่า plan ครั้งถัดไปจะออกมาสะอาด ถ้า configuration ของคุณไม่ตรงกับของจริงหลัง import แล้ว plan จะแสดง diff แบบเดียวกับที่ drift ทำ และคุณต้องแก้ HCL — ไม่ใช่แก้ resource จริง — จนทั้งสองตรงกัน

Terraform 1.5 เพิ่มวิธี import แบบที่สอง เขียนตรงลงในไฟล์ .tf เป็น block ระดับ first-class แทนที่จะเป็นคำสั่ง CLI ที่รันครั้งเดียวจบ

import {
to = aws_instance.web
id = "i-0123456789abcdef0"
}

ข้างใต้ทำหน้าที่เดียวกับ terraform import แต่ต่างกันตรง workflow ที่สำคัญ คำสั่ง CLI รันทันทีและแก้ state ตั้งแต่วินาทีที่คุณสั่ง — ไม่มี preview และไม่มีอะไรให้ review ก่อนเกิดขึ้น ส่วน import block เป็นแค่ configuration ที่จะโผล่เป็น action ที่วางแผนไว้ในครั้งถัดไปที่คุณรัน terraform plan เคียงข้างกับ change อื่น ๆ ก่อนที่อะไรจะเกิดขึ้นจริง

Terminal window
terraform plan
# Terraform will perform the following actions:
#
# # aws_instance.web will be imported
# resource "aws_instance" "web" {
# id = "i-0123456789abcdef0"
# instance_type = "t3.micro"
# ...
# }

นั่นทำให้ import block เป็นวิธีที่แนะนำเมื่อใช้ได้ — เพราะ plan ได้ review ได้ใน pull request เหมือน configuration change อื่น ๆ และทำซ้ำข้าม environment ได้ แทนที่จะเป็น side effect ที่เกิดทันทีซึ่งต้องมีใครจำไว้คอยรันมือบนทุกเครื่องที่ต้องการ คุณยังต้องเขียน resource block ที่ตรงกันเองอยู่ดีไม่ว่าจะใช้วิธีไหน — block นี้แค่บอก Terraform ว่าจะเชื่อม object จริงตัวไหนเข้ากับ address ไหน ตัวอย่าง AWS ข้างบนคือ HCL ที่ terraform validate ผ่านสะอาด — พิสูจน์ plan จริงสำหรับ aws_instance ต้องการ instance ID จริงกับ AWS credential ที่คอร์สนี้ไม่มี สิ่งที่ตามมาคือกลไกเดียวกันที่พิสูจน์ 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 เสมอ ส่วน aws_instance จริงที่ import กับ config ที่ตรงกับ attribute จริงของ instance อยู่แล้วจะแสดง 0 to change)

มีอีกสองคำสั่งที่เติมเต็มชุดเครื่องมือแก้ state ทั้งคู่แก้แค่ bookkeeping ของ Terraform และไม่แตะ AWS เลย

Terminal window
# Move a resource to a new address in state, e.g. after refactoring into a module
terraform state mv aws_instance.web module.compute.aws_instance.web
# Stop tracking a resource in state, without destroying the real object
terraform state rm aws_instance.web

terraform state mv คือคำสั่งที่ใช้เวลาปรับโครงสร้าง configuration ใหม่ — เช่นย้าย resource เข้าไปอยู่ใน module — แล้วไม่อยากให้ Terraform มองว่าเป็น “ลบตัวเก่า สร้างตัวใหม่” แค่ update address ใน state ให้ตรงกับตำแหน่งใหม่ใน HCL และ resource จริงบน AWS ไม่ถูกแตะเลย 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 ลืมไปเลย แต่ object จริงบน AWS ยังทำงานต่อเหมือนเดิมทุกอย่าง มีประโยชน์เวลาอยากส่งต่อ resource ให้ Terraform configuration อื่นดูแล หรือตั้งใจหยุดจัดการอะไรบางอย่างโดยไม่ลบทิ้ง

ทั้งสองคำสั่งมีข้อเสียเดียวกับที่คำสั่ง import แบบ CLI มี คือเกิดขึ้นทันที บนเครื่องเดียว และไม่มีอะไรใน repository บันทึกไว้ว่าเคยเกิดขึ้น state mv เทียบกับ moved block ที่พิสูจน์ไปก่อนหน้านี้คือ trade-off เดียวกันเป๊ะ ๆ state mv เป็น imperative และเกิดทันที — คุณพิมพ์คำสั่งครั้งเดียวบนเครื่องตัวเอง แล้วถ้าเพื่อนร่วมทีมไม่ได้รันคำสั่งเดียวกันเป๊ะ ๆ บนเครื่องของเขา plan ของเขาจะเห็น destroy-แล้ว-create ครั้งถัดไปที่ refactor แบบเดียวกัน moved เป็น declarative และอยู่ในไฟล์ .tf เอง ดังนั้นทุก clone ของ repository ได้ rename เดียวกันอัตโนมัติใน plan ครั้งถัดไป ไม่ต้องมีใครจำคำสั่งแยกต่างหากมารัน removed block คือคู่แบบ declarative ของ terraform state rm เหมือนที่ import block เป็นคู่ของ terraform import

# Stop managing this resource, but leave the real object running in AWS
removed {
from = aws_instance.web
lifecycle {
destroy = false
}
}

ลบ resource block ออกแล้วใส่อันนี้แทน argument lifecycle เป็นสิ่งที่ต้องใส่และคือหัวใจทั้งหมด destroy = false แปลว่า “ลืมมันไป แต่อย่าไปแตะ” ส่วน destroy = true แปลว่า “ลืมมันไปและลบ object จริงทิ้ง” 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 สัญญาไว้เป๊ะ เพราะมันเป็น configuration การเปลี่ยนแปลงนี้จึงโผล่ใน terraform plan ถูก review ใน pull request และ apply ได้เหมือนกันทุกเครื่องทุก CI run แทนที่จะต้องหวังให้แต่ละคนจำคำสั่ง CLI ที่ถูกต้องได้ พอ apply ผ่านครบทุกที่แล้ว removed block ก็ทำงานเสร็จหน้าที่และลบทิ้งได้

flowchart LR
  console["Manual change in AWS Console"] --> drift["Resource now disagrees with config"]
  drift -->|terraform plan| diff["Diff shown: drift detected"]
  existing["Existing untracked AWS resource"] -->|terraform import or import block| state["Resource added to state"]
  state -->|next plan| clean["plan shows no diff, if config matches"]
การแก้ผ่าน console ด้วยมือถูกจับเป็น 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, ... จริง)ยังต้องเขียน resource block ที่ตรงกันเอง — ทั้งสองรูปแบบไม่ generate HCL ให้
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 เว้นแต่จะบอกมันเป็นอย่างอื่น
  • “terraform import (หรือ import block) เขียน HCL ของ resource ให้เอง” พิสูจน์ข้างบน ทั้งสองรูปแบบแค่เติม state entry (หรือวางแผนจะเติม) resource block ที่ตรงกันยังต้องเขียนมือ ถ้าไม่ตรงกับ attribute จริงของ object plan ครั้งถัดไปจะแสดง diff แบบเดียวกับที่ drift ทำ
  • “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 นั้น
  • 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 ที่ถูกเติม HCL ยังเป็นหน้าที่ของผู้เขียนอยู่ดี
  • 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 "aws_instance" "app_server" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
# (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 เก่า สร้างตัวใหม่ สำหรับ aws_instance.web -> aws_instance.app_server นั่นคือ EC2 instance จริงที่ถูก terminate แล้ว launch ตัวใหม่ พร้อม downtime, instance ID ใหม่ และอาจมี IP ใหม่ด้วย ทั้งที่ผู้เขียนตั้งใจแค่จะ rename วิธีแก้คือใส่ moved block ใน commit เดียวกับที่ rename ซึ่งพิสูจน์ข้างบนแล้วว่าให้ Plan: 0 to add, 0 to change, 0 to destroy พร้อม note has moved to แทน

เพื่อนร่วมทีมแก้ tag ของ EC2 instance ตรง ๆ ใน AWS Console จะเกิดอะไรขึ้นครั้งถัดไปที่มีคนรัน 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.
ทำไม import block แบบ declarative มักถูกเลือกใช้มากกว่าการรันคำสั่ง CLI terraform import
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