The Terraform Workflow
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”สี่คำสั่งครอบคลุมงานที่คุณทำเกือบทุกวัน terraform init เตรียม working directory ให้พร้อม terraform plan แสดง dry-run diff ให้ดู terraform apply ลงมือทำจริง และ terraform destroy รื้อทุกอย่างที่ configuration จัดการอยู่ทิ้ง
init, plan, apply
หัวข้อที่มีชื่อว่า “init, plan, apply”terraform init คือคำสั่งแรกที่คุณรันในทุกไดเรกทอรี และรันอีกครั้งหลังเพิ่ม provider หรือ module ใหม่ คำสั่งนี้อ่าน required_providers block, ดาวน์โหลด provider plugin ที่ตรงกัน แล้วเตรียม backend ที่เก็บ state ให้พร้อม
terraform initterraform plan คือ dry run โดยอ่าน real GCP state, เทียบกับ configuration ของคุณ แล้วพิมพ์ diff ออกมา คือ resource ไหนจะถูกสร้าง อัปเดต in-place หรือลบ โดยยังไม่เปลี่ยนแปลงอะไรจริง
terraform plan# example outputTerraform will perform the following actions:
# google_storage_bucket.reports will be created + resource "google_storage_bucket" "reports" { + name = "acme-monthly-reports" + location = "US" + id = (known after apply) }
Plan: 1 to add, 0 to change, 0 to destroy.terraform apply รัน plan จริง โดยคำนวณ diff ใหม่อีกครั้ง (ยกเว้นคุณส่ง saved plan file เข้าไปตามที่จะพูดถึงด้านล่าง) แสดงให้คุณดู ถามยืนยัน แล้วค่อยเรียก GCP API จริงเพื่อสร้าง อัปเดต หรือลบ resource
terraform applyterraform destroy คือด้านตรงข้ามของ apply โดยคำนวณ plan เพื่อลบทุก resource ที่ configuration นี้จัดการอยู่ และหลังยืนยันแล้วก็ลบทิ้ง
terraform destroyflowchart LR
init["terraform init"] --> plan["terraform plan"]
plan --> review{"Review the diff"}
review --> apply["terraform apply"]
apply --> real["Real infrastructure updated"]
init -.-> destroy["terraform destroy"]
destroy -.-> gone["Managed resources removed"] Save plan ก่อน apply
หัวข้อที่มีชื่อว่า “Save plan ก่อน apply”terraform apply เฉย ๆ จะคำนวณ plan ของตัวเองใหม่ทันทีก่อนรันจริง ระหว่างช่วงเวลาที่คุณ review output ของ plan กับตอนที่มีคนรัน apply โลกจริงอาจเปลี่ยนไปแล้ว คนอื่น apply การเปลี่ยนแปลงที่ขัดกัน quota ของ GCP เปลี่ยน หรืออะไรก็ได้ plan ที่ apply คำนวณใหม่ตอนนั้นอาจไม่ใช่อันเดียวกับที่คุณ review ไปแล้ว
การ save plan ลงไฟล์ปิดช่องว่างนี้ คุณสั่ง apply ให้รันตรงไฟล์นั้นได้เลย โดยไม่คำนวณใหม่
terraform plan -out=tfplanterraform apply tfplanรูปแบบนี้สำคัญที่สุดใน CI/CD pipeline สามารถรัน plan -out=tfplan, โพสต์ diff ให้คน review และรัน apply tfplan หลังได้รับ approve เท่านั้น ทำให้มั่นใจได้ว่าสิ่งที่ apply ไปคือสิ่งที่คนอนุมัติเป๊ะ ๆ
พิสูจน์แบบ end-to-end จริง ด้วย resource local_file ที่ไม่ผูกกับ provider ไหน (กลไกเหมือนกันเป๊ะกับ google_storage_bucket แค่ GCP API call ตอน apply เข้าถึงแบบ offline ไม่ได้):
$ terraform plan -out=tfplanPlan: 1 to add, 0 to change, 0 to destroy.
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply: terraform apply "tfplan"
$ terraform apply tfplanlocal_file.config: Creating...local_file.config: Creation complete after 0s [id=5a6df720540c20d95d530d3fd6885511223d5d20]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.apply tfplan รันโดยไม่มี prompt ยืนยันเลย — พิสูจน์ว่ามันไม่ได้คำนวณใหม่แล้วถามอีกรอบ แต่รัน plan ที่ review และ save ไว้แล้วตรง ๆ
-detailed-exitcode: แยก “ไม่มีอะไรเปลี่ยน” กับ “มีการเปลี่ยนแปลงรออยู่” ด้วย exit code ล้วน ๆ
หัวข้อที่มีชื่อว่า “-detailed-exitcode: แยก “ไม่มีอะไรเปลี่ยน” กับ “มีการเปลี่ยนแปลงรออยู่” ด้วย exit code ล้วน ๆ”โดย default exit code ของ terraform plan เป็น 0 ไม่ว่าจะมีการเปลี่ยนแปลงหรือไม่ — ใช้ไม่ได้เลยสำหรับ CI script ที่ต้องตัดสินใจว่าจะเปิด pull request หรือส่ง notification ไหม -detailed-exitcode แก้ปัญหานี้ด้วย exit code ที่แยกชัดเจนสามแบบตามเอกสาร: 0 = สำเร็จ ไม่มีการเปลี่ยนแปลง; 1 = error; 2 = สำเร็จ มีการเปลี่ยนแปลงรออยู่
พิสูจน์จริง ไดเรกทอรีเดียวกัน สองสถานะ:
$ terraform plan -out=tfplan -detailed-exitcodePlan: 1 to add, 0 to change, 1 to destroy.Saved the plan to: tfplan$ echo $?2
$ terraform apply tfplan # (applies the change)
$ terraform plan -detailed-exitcodeNo changes. Your infrastructure matches the configuration.$ echo $?0นี่คือกลไกที่ CI pipeline เช็คจริง ๆ — if [ $? -eq 2 ]; then echo "changes pending"; fi — แทนที่จะ parse ข้อความ plan output ซึ่งมีไว้ให้มนุษย์อ่าน ไม่ใช่ format ที่คงที่สำหรับโปรแกรม
terraform show -json: plan ในรูปแบบ structured data ไม่ใช่ text
หัวข้อที่มีชื่อว่า “terraform show -json: plan ในรูปแบบ structured data ไม่ใช่ text”terraform show -json <planfile> render plan ที่ save ไว้เป็น JSON แทนที่ text ที่อ่านง่ายที่ plan ปกติพิมพ์ — format ที่มีเอกสารรองรับและคงที่ ที่เครื่องมือ policy (OPA/Conftest, Sentinel, CI script เอง) parse จริง ๆ แทนที่จะขูด terminal text ที่มีสี
$ terraform show -json tfplan | python3 -m json.toolOutput จริง (ตัดให้สั้น) สำหรับ plan ของ local_file.config เดียวกันเป๊ะข้างบน:
{ "resource_changes": [ { "address": "local_file.config", "mode": "managed", "type": "local_file", "name": "config", "provider_name": "registry.terraform.io/hashicorp/local", "change": { "actions": ["create"], "before": null, "after": { "content": "v1", "filename": "./config.txt" } } } ]}resource_changes[].change.actions คือ field ที่ policy tooling ของ CI อ่านจริง ๆ — list อย่าง ["create"], ["update"], ["delete"], ["delete", "create"] (replace) หรือ ["no-op"] JSON shape เป๊ะ ๆ นี้ และวิธีที่ policy engine ใช้มัน อธิบายลึกเต็ม ๆ ใน Plan File and JSON Output ในโมดูล Terraform Internals
Provider dependency lock file
หัวข้อที่มีชื่อว่า “Provider dependency lock file”ตอนที่ terraform init resolve version constraint ใน required_providers Terraform จะเขียนเวอร์ชันที่เลือกและ cryptographic checksum ลงใน .terraform.lock.hcl ในไดเรกทอรีเดียวกับ configuration ของคุณ
# .terraform.lock.hcl (excerpt — Terraform manages this file's contents)provider "registry.terraform.io/hashicorp/google" { version = "8.2.0" constraints = "~> 8.0" hashes = [ "h1:examplehashvaluegeneratedbyterraforminit=", ]}ไฟล์นี้ควรถูก commit เข้า version control เพราะเป็น provider-level analogue ของ package-lock.json หรือ Gemfile.lock ถ้าไม่มีไฟล์นี้ การรัน terraform init แต่ละครั้ง — โน้ตบุ๊กคุณวันนี้ โน้ตบุ๊กเพื่อนร่วมทีมพรุ่งนี้ CI runner สัปดาห์หน้า — อาจ resolve ~> 8.0 ไปคนละ patch release กัน และความต่างเล็ก ๆ ของ behavior ใน provider ก็อาจทำให้เครื่องคนอื่นได้ plan ที่ต่างออกไปโดยไม่มีเหตุผลที่มองเห็น เมื่อ commit lock file ไว้ ทุกครั้งที่ init จะติดตั้งเวอร์ชันเดิมเป๊ะ ๆ จนกว่าจะมีคนตั้งใจรัน terraform init -upgrade เพื่อขยับ constraint ไปข้างหน้า
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกคำสั่งในบทนี้ตอบคำถามหนึ่งในสองข้อ: “จะเปลี่ยนอะไร” (plan, show -json, -detailed-exitcode) หรือ “ลงมือทำจริง” (apply, destroy) การ save plan ลงไฟล์ (-out) คือสิ่งที่เปลี่ยนคำตอบของคำถามแรกให้กลายเป็น artifact ตายตัวที่คำสั่งของคำถามที่สองรันตามได้เป๊ะ ปิดช่องว่างระหว่าง “สิ่งที่มนุษย์ review” กับ “สิ่งที่รันจริง” — เหตุผลหลักที่ CI/CD pipeline ใช้รูปแบบนี้แทนที่จะเรียก plan กับ apply แยกกันสองครั้ง
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ต้นทุน |
|---|---|---|
apply โดยไม่ save plan | ขั้นตอน/ไฟล์น้อยกว่าสำหรับการเปลี่ยนแปลงเร็ว ๆ แบบ interactive คนเดียว | คำนวณ plan ใหม่ก่อนรันจริง — plan ที่ review กับ plan ที่ apply อาจต่างกันได้ถ้ามีอะไรเปลี่ยนระหว่างนั้น |
plan -out=tfplan แล้ว apply tfplan | plan ที่ review ไว้คือสิ่งที่รันจริงเป๊ะ พิสูจน์ข้างบนด้วย zero prompt ตอน apply | มีไฟล์เพิ่มต้องจัดการและส่งต่อระหว่าง pipeline stage; plan file ที่เก่าอาจล้าสมัยถ้าไม่รีบ apply |
-detailed-exitcode | exit code ที่เครื่องเช็คได้ (0/1/2) พิสูจน์ข้างบน แทนการ parse text output ที่มีไว้ให้มนุษย์อ่าน | ต้องจำ flag เพิ่มอีกตัว; exit code default (0 เสมอตอนสำเร็จ) มักถูกเข้าใจผิดว่าคือ “ไม่มีการเปลี่ยนแปลง” ทั้งที่ไม่ใช่ |
terraform show -json | JSON schema ที่มีเอกสารรองรับและคงที่สำหรับ policy tool พิสูจน์ข้างบนด้วย resource_changes[].change.actions จริง | ยาวและไม่เหมาะให้มนุษย์อ่านตรง ๆ — text plan output ยังดีกว่าสำหรับคน review |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
terraform apply tfplanยังแสดง prompt ยืนยัน แค่ยืนยันกับ plan ที่ save ไว้” พิสูจน์ข้างบนแล้วว่าไม่ prompt เลย — นั่นคือประเด็นสำคัญทั้งหมดของ saved plan file และเป็นเหตุผลจริงที่ควรระวังว่าใครรันapplyกับไฟล์นั้นได้บ้าง - “exit code ของ
terraform planบอกได้ว่ามีการเปลี่ยนแปลงหรือไม่” พิสูจน์ข้างบนแล้วว่า exit code default เป็น0เสมอไม่ว่าจะมีการเปลี่ยนแปลงหรือไม่ — มีแค่-detailed-exitcodeเท่านั้นที่แยก0(ไม่มีเปลี่ยนแปลง) จาก2(มีเปลี่ยนแปลง) จาก1(error) - “
terraform show -jsonกับ textplanoutput สีสัน มีข้อมูลเดียวกันแค่คนละรูปแบบ” เวอร์ชัน JSON คือตัวที่มี schema เอกสารรองรับและคงที่สำหรับโปรแกรม ส่วนเวอร์ชัน text มีไว้ให้มนุษย์อ่าน และ format ที่แน่นอนของมันไม่ใช่ contract ที่คงที่สำหรับ parse - “saved plan file (
tfplan) เป็นแค่ cache — review text output ครั้งเดียวก็พอแล้ว ตัวไฟล์ไม่สำคัญ” พิสูจน์ข้างบนแล้วว่าapply tfplanรันเนื้อหาของไฟล์นั้นเป๊ะ ๆ โดยไม่คำนวณใหม่ — ตัวไฟล์เองคือสิ่งที่การันตี ไม่ใช่ text ที่มนุษย์เหลือบดู
อ่านโค้ดที่ AI สร้างให้
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้างให้”- CI script ที่รัน
terraform planแล้วterraform applyเป็นสองคำสั่งแยกกันไม่เชื่อมโยงกัน (ไม่มี-out/planfile) แล้วเรียกว่า “reviewed แล้ว” — พิสูจน์ข้างบนแล้วว่านี่เปิดช่องว่างเดียวกับที่ saved plan ปิดไว้: plan ที่ apply อาจต่างจากที่มนุษย์ดูไป - script ที่เช็ค
if [ $? -ne 0 ]หลังรันterraform planเฉย ๆ เพื่อตัดสินว่ามีการเปลี่ยนแปลงไหม — พิสูจน์ข้างบนแล้วว่า exit code ของplanเฉย ๆ เป็น0เสมอตอนสำเร็จ มีแค่-detailed-exitcodeเท่านั้นที่ให้สัญญาณที่เช็คได้จริง ลืม flag นี้ทำให้ check ใช้ไม่ได้แบบเงียบ ๆ - โค้ดที่ parse terminal text ของ
terraform planที่มีสี (regex จับบรรทัด+/~/-) เพื่อหาว่าอะไรเปลี่ยน ทั้งที่terraform show -jsonมีข้อมูลเดียวกันอยู่แล้วเป็น JSON ที่มีเอกสารรองรับและคงที่ - คำสั่ง
apply tfplanที่ generate มาพร้อม-auto-approveflag แถมไปด้วยกับ planfile argument — ซ้ำซ้อนและสับสน:apply <planfile>ไม่ prompt อยู่แล้วตั้งแต่แรก พิสูจน์ข้างบนแล้ว ดังนั้น-auto-approveไม่มีความหมายตรงนั้นเลย (มันมีผลแค่กับรูปแบบที่ไม่มี planfile)
🐛 หา bug ในโค้ดนี้
terraform plan -out=tfplan# ... post plan output to Slack for review ...sleep 3600 # wait an hour for a human to approve in Slackterraform apply -auto-approveเฉลย
saved plan file tfplan ถูกคำนวณแล้วไม่เคยถูกใช้เลย — pipeline รัน terraform plan -out=tfplan (save plan ไว้ให้ review ถูกต้องแล้ว) แต่ apply ด้วย terraform apply -auto-approve เปล่า ๆ ซึ่ง — พิสูจน์ข้างบนแล้วด้วยการรัน apply tfplan จริงที่ไม่มี prompt เลยเฉพาะตอนส่ง planfile เข้าไปเท่านั้น — คำนวณ plan ใหม่ทั้งหมดกับ infrastructure จริง ณ เวลาที่ apply ไม่ใช่ plan ที่มนุษย์ review ไปเมื่อชั่วโมงก่อนใน Slack อะไรก็ตามที่เปลี่ยนบน GCP ในชั่วโมงนั้น (การเปลี่ยนแปลงของคนอื่น, resource ที่ drift) จะถูกพับรวมเข้าไปใน apply แบบเงียบ ๆ ทางแก้คือ terraform apply -input=false tfplan (ไฟล์ที่ save ไว้ ไม่คำนวณใหม่ และไม่ต้องมี -auto-approve เพราะ planfile ไม่ prompt อยู่แล้ว) แทนที่จะเป็น plan สด ๆ ที่ใช้ -auto-approve