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

How Terraform Works

ทุกโมดูลก่อนหน้านี้ในคอร์สสอนให้คุณ ใช้ Terraform กับ Terragrunt: เขียน HCL, รัน init/plan/apply, เก็บ state แบบ remote, ต่อ variable กับ module, สร้าง live tree แบบ multi-environment, และ ship ผ่าน CI พร้อม policy check ไม่มีข้อไหนเลยที่ต้องรู้ว่า terraform plan จริง ๆ แล้ว สร้าง อะไรก่อนจะ print diff ออกมา, terraform.tfstate จริง ๆ แล้ว คือ อะไรในระดับ byte, -out=tfplan เขียนอะไรลงดิสก์จริง ๆ, hashicorp/google (หรือ aws, หรือ azurerm) คุยกับ terraform ในฐานะ process อย่างไร, หรือ terragrunt run --all ตัดสินใจอย่างไรว่า unit ไหนรันก่อนไหน

ช่องว่างนี้แหละคือจุดที่ Terraform tooling หรือ review ที่ AI เขียนพังแบบที่ดูดีบนผิวหน้า: script “policy check” ที่ grep ข้อความ output ของ terraform plan แทนที่จะอ่าน array resource_changes[].change.actions ที่มีโครงสร้างจริงจาก terraform show -json; CI step ที่สมมติว่า terraform apply -parallelism=50 เร็วกว่าเสมอโดยไม่รู้ว่า flag นี้จำกัดอะไร; เพื่อนร่วมทีมที่ “แก้” state lock ที่ค้างด้วยการแก้ field serial ใน terraform.tfstate มือ โดยไม่รู้ว่า state push จะปฏิเสธค่าที่ stale ตรง ๆ; block dev_overrides ที่ copy-paste มาจาก blog แล้วทำให้ init พังเงียบ ๆ โดยไม่มีใครอธิบายได้จาก error text อย่างเดียว ไม่มีข้อไหนเลยที่เป็น HCL syntax error ไม่มีข้อไหนเลยที่ terraform validate จับได้ ทุกข้อคือข้อเท็จจริงเกี่ยวกับ runtime ที่โมดูลนี้พิสูจน์ให้ดูจริง ๆ

โมดูลนี้มีหน้าที่เดียว: ให้ความรู้เรื่อง runtime 5 ชิ้นที่ทำให้คุณมอง output ของ Terraform หรือ Terragrunt แบบไหนก็ได้ — graph, state file, plan -out, provider crash, run --all log — แล้วรู้ทันทีว่าอะไรสร้างมันขึ้นมาและทำไม

  1. terraform plan/apply จริง ๆ walk อะไร และตามลำดับไหน? (The Graph and the Walk — terraform graph จริง ทั้ง DOT และ Mermaid, -parallelism, depends_on เทียบกับ implicit reference)
  2. terraform.tfstate จริง ๆ คืออะไรในระดับ schema? (State File Anatomy — JSON จริงจาก state pull, การบังคับใช้ serial/lineage พิสูจน์ด้วยการถูกปฏิเสธจริง, state replace-provider, show -json เทียบกับ state ดิบ)
  3. plan file ที่ save ไว้คืออะไร และ CI/policy tooling อ่านอะไรจากมันจริง ๆ? (Plan File and JSON Output — -out คือ zip จริง พิสูจน์ด้วย unzip -l; matrix เต็มของ resource_changes[].change.actions; -detailed-exitcode)
  4. Terraform คุยกับ provider จริง ๆ ได้อย่างไร? (Provider Plugin Protocol — go-plugin บน gRPC, handshake จริงจาก TF_LOG=TRACE กับ call GetProviderSchema, ทำไม init ต้องโหลด binary ต่อ OS/arch, provider crash ที่ทำให้เกิดขึ้นจริง, dev_overrides)
  5. Terragrunt ตัดสินใจว่าจะรันอะไรอย่างไร และตามลำดับไหน? (Terragrunt Discovery and DAG — terragrunt find/list --dag จริง, log ของ run --all --parallelism 1 ที่เรียงลำดับจริง, .terragrunt-cache, mock_outputs)

ทุก claim ในโมดูลนี้มีหลักฐานจากหนึ่งใน 3 อย่าง: การรันจริงบนเครื่อง ทำซ้ำได้ กับ Terraform 1.16.4 และ Terragrunt 1.1.0 (เวอร์ชันที่คอร์สนี้ pin ไว้) — วาง output ไว้ใต้ claim นั้น ตัด path เฉพาะเครื่องออกแต่ไม่เคยแต่งขึ้นเอง; เอกสารทางการที่ developer.hashicorp.com หรือ terragrunt.gruntwork.io; หรือถ้าทำทั้งสองอย่างแบบ offline ไม่ได้ ก็บอกตรง ๆ ว่าทำไม่ได้

บทเรียนตอบคำถามอะไร
The Graph and the WalkTerraform สร้างอะไรจากไฟล์ .tf ของคุณก่อนจะทำอะไรเลย และ -parallelism เปลี่ยนสิ่งที่เกิดขึ้นตอน apply อย่างไร?
State File Anatomyข้างใน terraform.tfstate มีอะไรบ้างจริง ๆ และอะไรที่หยุดไม่ให้สองคนเขียนทับ state กันเงียบ ๆ?
Plan File and JSON Outputplan file ที่ -out สร้างประกอบด้วยอะไรบ้าง และ policy engine หรือเครื่องมือคำนวณ cost อ่านอะไรจาก show -json จริง ๆ?
Provider Plugin Protocolระหว่าง terraform init กับ provider พร้อม plan เกิดอะไรขึ้นจริง ๆ และ provider crash หน้าตาเป็นอย่างไร?
Terragrunt Discovery and DAGTerragrunt หา unit ของคุณเจอได้อย่างไร สร้างลำดับ dependency ของตัวเองอย่างไร และตัดสินใจว่า run --all ทำอะไรก่อนอย่างไร?

The State File แนะนำ state ไปแล้วว่าเป็น “ไฟล์ JSON ที่แมป configuration เข้ากับ infrastructure จริง” โมดูลนี้ไปไกลกว่านั้น เข้าสู่ schema จริง, อัลกอริทึม graph จริง, byte จริงบนดิสก์, และขอบเขต process จริงระหว่าง Terraform Core กับ provider

flowchart TD
  hcl["Your .tf files"] --> graphbuild["Build the dependency graph\n(The Graph and the Walk)"]
  graphbuild --> walk["Walk the graph, up to -parallelism nodes concurrently\n(default 10)"]
  walk --> rpc["Per node: gRPC call to the provider plugin\n(Provider Plugin Protocol)"]
  rpc --> plan["Plan result assembled"]
  plan --> outfile["terraform plan -out=tfplan\n(a real zip -- Plan File and JSON Output)"]
  outfile --> apply["terraform apply tfplan"]
  apply --> state["Written to state\n(State File Anatomy)"]
  state --> tg["Terragrunt units: their own DAG over many state files\n(Terragrunt Discovery and DAG)"]
หนึ่ง terraform apply จาก HCL ถึง provider RPC

ทุกอย่างในโมดูลนี้คือเรื่องเดียวกัน เล่าจาก 5 มุมของ pipeline เดียวกัน: Terraform ไม่เคย execute configuration ของคุณจากบนลงล่าง — มัน compile มันเป็น graph แล้วทุกคำสั่ง (plan, apply, destroy, แม้แต่ graph เอง) คือการ walk บน graph นั้น ส่งแต่ละ node ไปให้ provider plugin ที่เป็นเจ้าของมันผ่าน gRPC แล้วบันทึกผลลงเอกสาร state ที่มีเวอร์ชันเดียว Terragrunt อยู่อีกชั้นเหนือขึ้นไปและทำแบบเดียวกันแต่หยาบกว่า: แทนที่จะเป็น resource, node ของมันคือทั้ง unit Terraform (directory), edge มาจาก dependency/dependencies block แทน resource reference, และ --parallelism ของมันเองก็จำกัดว่า unit กี่ตัวรันพร้อมกัน — แยกขาดจาก -parallelism ที่ terraform apply แต่ละตัวข้างใน unit ใช้โดยสิ้นเชิง พอคุณวางข้อเท็จจริงไหนก็ตามในโมดูลนี้ลงบน “graph, walk, gRPC call, state write” (แล้วอีกชั้น “graph ของ Terragrunt เอง, walk, การรัน unit”) การอ่าน diff ของ plan ที่ไม่คุ้น, state file, CI policy step, หรือ log ของ run --all ก็จะไม่ใช่ปริศนา 5 อย่างแยกกันอีกต่อไป

  • “terraform apply รัน resource ตามลำดับที่เขียนในไฟล์” The Graph and the Walk พิสูจน์ตรงข้ามด้วย terraform graph จริง: ลำดับมาจาก dependency graph ทั้งหมด (implicit reference กับ depends_on ที่ชัดเจน) และ resource ที่ไม่เกี่ยวกันจะถูกส่งพร้อมกัน สูงสุดตาม -parallelism (default 10) — พิสูจน์ด้วย wall-clock จริงที่ operation อิสระ 2 วินาที 5 ตัวเสร็จใน ~2 วินาทีตาม default และ ~10 วินาทีที่ -parallelism=1
  • “state เป็นแค่ cache แก้มือได้ถ้าคำสั่ง terraform state ไม่สะดวก” State File Anatomy พิสูจน์ว่า state มีเวอร์ชันและถูกบังคับใช้จริง: terraform state push จริงที่มี serial เก่าหรือ lineage ไม่ตรงกันจะถูกปฏิเสธตรง ๆ ด้วย error text จริง ไม่ merge เงียบ ๆ
  • “plan file ที่ save ไว้ก็คือ output ที่มนุษย์อ่านได้ของ plan แค่ cache ไว้” Plan File and JSON Output พิสูจน์ว่า -out สร้าง zip archive จริง (unzip -l แสดงสด ๆ) ที่มี plan, state ก่อนและหลัง refresh, และ config snapshot เต็ม — พอให้ apply ทำงานได้โดยไม่ต้อง re-evaluate ไฟล์ .tf ของคุณเลย
  • “Terraform ‘รู้เอง’ วิธีคุยกับ AWS, Azure, GCP built-in” Provider Plugin Protocol พิสูจน์ว่า provider ทุกตัวคือ subprocess แยกที่ Terraform สั่งรันแล้วคุยผ่าน gRPC บน Unix socket พร้อม call GetProviderSchema จริงที่จับได้ด้วย TF_LOG=TRACE
  • “terragrunt run --all walk unit ตามลำดับที่ find แสดง” Terragrunt Discovery and DAG พิสูจน์ว่าลำดับมาจาก dependency/dependencies block ที่สร้าง DAG ของ Terragrunt เอง แยกขาดจากลำดับตัวอักษรหรือลำดับ discovery — พิสูจน์ด้วย log จริงที่ --parallelism 1 แสดง plan ของ dependency unit เสร็จก่อนที่ dependent ทั้งสองตัวจะเริ่มด้วยซ้ำ

ประเด็นทั้งหมดของโมดูลนี้คือ section นี้ 5 รอบ — บทละรอบ ตัวอย่างระดับภาพรวมก่อน: script “policy gate” ของ CI ที่ดูสมเหตุสมผลแต่จริง ๆ ไม่ได้เช็คอะไรเลยเงียบ ๆ

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

#!/usr/bin/env bash
# A teammate's "policy gate" before terraform apply in CI
terraform plan -out=tfplan > plan_output.txt
if grep -q "Plan: 0 to add, 0 to change, 0 to destroy" plan_output.txt; then
echo "no changes, skipping approval"
else
echo "changes detected, needs approval"
fi
terraform apply tfplan
เฉลย

script นี้ grep บรรทัดสรุปที่มนุษย์อ่าน ซึ่งเป็น string ที่เปราะบางต่อ localization และ formatting ไม่ใช่ interface ที่มั่นคง — Terraform ไม่เคย document ข้อความนั้นเป๊ะ ๆ ว่าเป็น API Plan File and JSON Output พิสูจน์ว่า interface ที่มั่นคงและเป็นทางการคือ terraform show -json tfplan ที่ expose resource_changes[].change.actions เป็น array จริง (["no-op"], ["create"], ["update"], ["delete","create"], ["forget"], …) ออกแบบมาเพื่อ use case นี้เป๊ะ ๆ — และ terraform plan -detailed-exitcode (พิสูจน์ในบทเดียวกัน: exit code 2 สำหรับ “มีการเปลี่ยนแปลง”, 0 สำหรับ “ไม่มีการเปลี่ยนแปลง”, 1 สำหรับ error จริง) แทนที่ grep ทั้งหมดด้วย exit code ที่ document ไว้ให้ script branch ได้ตรง ๆ โดยไม่ต้อง parse text เลย

Terraform สร้างอะไรจากไฟล์ .tf ของคุณจริง ๆ ก่อนจะรัน plan หรือ apply?
ลองรัน terraform state push จริงด้วย lineage ที่ไม่ตรงกับ state ปัจจุบันของ backend จะเกิดอะไรขึ้น พิสูจน์ในโมดูลนี้?
terraform plan -out=tfplan สร้างอะไรลงดิสก์จริง ๆ?
Terraform Core คุยกับ provider อย่าง hashicorp/google ระหว่าง plan จริง ๆ ได้อย่างไร?
ใน live tree ที่มี vpc, compute (depends on vpc), และ db (depends on vpc) log จริงของ terragrunt run --all --parallelism 1 -- plan พิสูจน์อะไรเรื่องลำดับการรัน?