Remote Backends and Locking
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”remote backend เก็บ state ไว้ที่จุดกลางอย่าง S3 แทนที่จะอยู่บน laptop ของคนใดคนหนึ่ง ส่วน locking กันไม่ให้สองคนเขียนทับ state ที่ share กันนั้นพร้อมกัน
ทำไม local state ถึงใช้กับทีมไม่ได้
หัวข้อที่มีชื่อว่า “ทำไม local state ถึงใช้กับทีมไม่ได้”โดย default terraform.tfstate จะอยู่บนเครื่องที่รัน apply ซึ่งโอเคสำหรับทดลองคนเดียว แต่พังทันทีเมื่อมีคนที่สองเข้ามาร่วม
- เพื่อนร่วมทีม A รัน
applyแล้วสร้าง VPC local state file บน laptop ของเขารู้เรื่องนี้แล้ว - เพื่อนร่วมทีม B บน laptop คนละเครื่อง ไม่เคยเห็น state file นั้นเลย เขารัน
planเทียบกับ local state ที่เก่าหรือว่างเปล่าของตัวเอง แล้วอาจพยายามสร้าง VPC ซ้ำ หรือได้ plan ที่ไม่สมเหตุสมผลเมื่อเทียบกับของจริงที่มีอยู่แล้ว - CI/CD pipeline เจอปัญหาเดียวกันแต่หนักกว่า pipeline runner คือ environment ใหม่ที่ใช้แล้วทิ้ง ไม่มี local state file ติดตัวมาเลย จึงต้องการ state ฉบับกลางที่อ่านและเขียนได้ทุกครั้งที่รัน ไม่ใช่ copy ที่บังเอิญไปอยู่บน laptop ของใครสักคน
ทางแก้คือย้าย state ออกจากเครื่องใครเครื่องคนหนึ่ง ไปไว้บน backend กลางที่ทั้งทีมและทุก CI run ชี้ไปที่เดียวกัน
Config S3 backend พร้อม native locking
หัวข้อที่มีชื่อว่า “Config S3 backend พร้อม native locking”วิธีที่แนะนำในปัจจุบันสำหรับเก็บ Terraform state บน AWS คือ S3 bucket ที่ config เป็น backend พร้อมเปิด native S3 state locking
terraform { backend "s3" { bucket = "my-company-terraform-state" key = "networking/terraform.tfstate" region = "us-east-1" use_lockfile = true }}bucket กับ key รวมกันระบุว่า object ไหนใน S3 ที่เก็บ state ของ configuration นี้ — key คือ path ภายใน bucket ดังนั้น configuration ต่างกัน (หรือ Terragrunt unit ต่างกัน) มักใช้ key คนละอันใน bucket เดียวกัน use_lockfile = true คือบรรทัดสำคัญ Terraform ใช้ conditional-write lock file ที่เก็บอยู่คู่กับ state object ตรงใน S3 เลย เพื่อจัดการ locking ไม่ต้องมี infrastructure สำหรับ locking แยกต่างหาก แค่ S3 bucket ก็พอ
คุณจะยังเจอ pattern เก่าใน codebase ที่มีอยู่แล้ว ซึ่งใช้ DynamoDB table แยกสำหรับ locking
# Legacy locking pattern — dynamodb_table is deprecated and scheduled for removal.# Prefer use_lockfile = true (shown above) in new configurations.terraform { backend "s3" { bucket = "my-company-terraform-state" key = "networking/terraform.tfstate" region = "us-east-1" dynamodb_table = "terraform-state-lock" }}pattern นั้นยังทำงานได้ และไม่ต้องตกใจถ้าคุณรับ codebase ที่ใช้แบบนี้มา แต่ dynamodb_table ตอนนี้เป็น argument ที่ deprecated แล้วบน S3 backend และมีแผนจะลบออกในเวอร์ชันถัดไปของ Terraform configuration ใหม่ควรใช้ use_lockfile = true แทน เพราะทำหน้าที่เดียวกันโดยไม่ต้องสร้าง AWS resource ตัวที่สองมาดูแลและจ่ายเงินเพิ่ม
Migrate backend init -migrate-state กับ init -reconfigure ต่างกันยังไง
หัวข้อที่มีชื่อว่า “Migrate backend init -migrate-state กับ init -reconfigure ต่างกันยังไง”การเปลี่ยน backend block — ย้ายไป bucket อื่น เปลี่ยน key หรือย้ายจาก local ไป S3 ครั้งแรก — ต้องรัน terraform init ใหม่ และ Terraform มี flag สองตัวให้เลือกต่างกันตามว่าอยากให้ state เก่าตามมาด้วยหรือไม่ คอร์สนี้ไม่มี cloud credential เลย พิสูจน์ข้างล่างจึงเปลี่ยน path ของ local backend แทนการ migrate ไป S3 จริง — prompt และกลไกเดียวกันเป๊ะ ๆ ใช้ได้กับการย้าย local→S3 จริง ต่างกันแค่ชนิด backend
terraform init -migrate-state จะถามว่าจะ copy state เดิมไปที่ backend ใหม่ไหม output จริง ตอนเปลี่ยน path = "state/old.tfstate" เป็น path = "state/new.tfstate" ใน backend "local" block
$ terraform init -migrate-stateInitializing the backend...Backend configuration changed!
Terraform has detected that the configuration specified for the backendhas changed. Terraform will now check for existing state in the backends.Do you want to copy existing state to the new backend? Pre-existing state was found while migrating the previous "local" backend to the newly configured "local" backend. No existing state was found in the newly configured "local" backend. Do you want to copy this state to the new "local" backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
Successfully configured the backend "local"! Terraform will automaticallyuse this backend unless the backend configuration changes.ตอบ yes แล้ว resource ถูก copy ไปที่ location ใหม่จริง ๆ — state/new.tfstate มี resource local_file ตัวเดียวกับที่ path เก่ามี terraform init -reconfigure ข้าม prompt นั้นไปเลยและ ไม่ copy state มาด้วย — พิสูจน์ เปลี่ยน path อีกครั้งแล้วรัน -reconfigure แทน
$ terraform init -reconfigureInitializing the backend...
Successfully configured the backend "local"! Terraform will automaticallyuse this backend unless the backend configuration changes.ไม่มีคำถาม migration เลย plan ครั้งถัดไปพิสูจน์ความต่าง — Terraform เชื่อว่า resource ไม่มีอยู่แล้ว เพราะ backend location ใหม่ไม่เคยถูกเติมข้อมูล
$ terraform plan # local_file.config will be created + resource "local_file" "config" { + content = "listen=8080\n" + filename = "./generated/app.conf" }
Plan: 1 to add, 0 to change, 0 to destroy.-reconfigure เหมาะกับ “เริ่มสะอาดที่ location ใหม่นี้ ไม่เอา state เก่า” (หรือ backend เก่าหายไป/เข้าไม่ถึงแล้ว) ส่วน -migrate-state เหมาะกับ “นี่คือ infrastructure เดิม แค่ย้ายที่เก็บ state record” เลือกผิดตัวกับ S3 bucket จริง จะทำให้ state เก่าถูกทิ้งไว้ไม่ได้ migrate หรือแย่กว่านั้น ทำให้ Terraform คิดว่า VPC ที่รันอยู่ต้องสร้างใหม่ตั้งแต่ต้น
Partial backend configuration ด้วย -backend-config
หัวข้อที่มีชื่อว่า “Partial backend configuration ด้วย -backend-config”backend block ไม่จำเป็นต้อง hardcode ทุก argument — ปล่อยว่างไว้แล้วใส่ค่าตอน init (จากไฟล์ หรือ -backend-config="key=value" ทีละตัว) ทำให้ secret กับค่าที่ต่างกันตาม environment ไม่ต้องอยู่ในไฟล์ .tf ที่ commit
terraform { backend "s3" {}}# backend.hcl (not committed, or generated by CI per environment)bucket = "my-company-terraform-state"key = "networking/terraform.tfstate"region = "us-east-1"terraform init -backend-config=backend.hcloutput จริงกับ local backend พิสูจน์ว่า flag ถูกยอมรับและ merge เข้าไป (path ถูกส่งแบบเดียวกับที่ bucket/key/region จะถูกส่งให้ S3)
$ terraform init -backend-config=backend.hclInitializing the backend...
Successfully configured the backend "local"! Terraform will automaticallyuse this backend unless the backend configuration changes.นี่คือกลไกเดียวกับที่ CI pipeline ใช้ชี้ configuration เดียวกันไปที่ S3 key คนละอันต่อ environment (dev.hcl, prod.hcl) โดยไม่ต้อง duplicate ไฟล์ .tf
terraform force-unlock ทางออกแบบ manual
หัวข้อที่มีชื่อว่า “terraform force-unlock ทางออกแบบ manual”ถ้า apply ถูก kill กลางคัน (laptop sleep, CI job ถูก cancel) lock ที่มันถือไว้อาจค้างอยู่โดยไม่มีอะไรมาปล่อยมันแล้ว terraform force-unlock <LOCK_ID> ลบ lock นั้นด้วยมือ output จริงจาก -help ของ Terraform 1.16.4
$ terraform force-unlock -helpUsage: terraform [global options] force-unlock LOCK_ID
Manually unlock the state for the defined configuration.
This will not modify your infrastructure. This command removes the lock on the state for the current workspace. The behavior of this lock is dependent on the backend being used. Local state files cannot be unlocked by another process.
Options:
-force Don't ask for input for unlock confirmation.LOCK_ID ไม่ใช่อะไรที่คุณคิดขึ้นเอง — ตอน lock ถูกถืออยู่แล้ว run ถัดไป ที่ acquire lock ไม่ได้จะ print lock ID นั้น (พร้อมว่าใครถืออยู่ และตอนไหน) ใน error message ของตัวเอง แล้ว ID นั้นแหละที่เอาไปวางใน force-unlock lock ที่ค้างจริง ๆ ไม่สามารถจำลองแบบ offline ได้ในคอร์สนี้ — local backend ที่ harness ใช้ไม่มี lock ให้ค้างแบบที่ .tflock object ของ S3 หรือ blob lease ที่ถืออยู่ทำได้ (-help text ข้างบนบอกตรง ๆ ว่า “Local state files cannot be unlocked by another process”) ปฏิบัติกับ force-unlock เป็นทางเลือกสุดท้ายสำหรับ lock ที่คุณ verify แล้วว่าค้างจริง (process ที่ถือมันหายไปจริง ๆ — เช็คก่อน เช่นว่า CI run นั้นถูก cancel จริงไหม) ไม่ใช่ reflex สำหรับ “apply ของฉันช้าไปหน่อย”
Terragrunt remote_state generate backend.tf ที่ไม่ต้องเขียนมือ
หัวข้อที่มีชื่อว่า “Terragrunt remote_state generate backend.tf ที่ไม่ต้องเขียนมือ”remote_state block ของ Terragrunt ใน root.hcl generate backend block แบบข้างบนให้ทุก unit ที่ include มัน โดย resolve key ต่อ unit ด้วย path_relative_to_include() อัตโนมัติ
remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite" } config = { bucket = "my-company-terraform-state" key = "${path_relative_to_include()}/terraform.tfstate" region = "us-east-1" encrypt = true use_lockfile = true }}include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "."}รัน terragrunt run -- init -backend=false ใน unit vpc generate backend.tf จริง — ทุก key จาก config ไปอยู่ตรง ๆ และ key resolve เป็น vpc/terraform.tfstate
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEaterraform { backend "s3" { bucket = "my-company-terraform-state" encrypt = true key = "vpc/terraform.tfstate" region = "us-east-1" use_lockfile = true }}ทุก argument ใน config map ถูก emit ตรง ๆ — ไม่มีขั้น auto-create แบบที่ GCP ที่ project/location trigger การสร้าง bucket (คู่เทียบฝั่ง GCP ใน module-versioning-and-registries.mdx พิสูจน์ความต่างนี้ตรง ๆ) นี่คือกลไกที่ทำให้ไม่ต้องเขียน backend "s3" {} block มือทีละ environment — remote_state block เดียวใน root config แล้วทุก unit ข้างล่างได้ backend ที่ถูกต้องและ scope เฉพาะตัวเองมาให้เอง
locking ป้องกันอะไรจริง ๆ
หัวข้อที่มีชื่อว่า “locking ป้องกันอะไรจริง ๆ”ลองนึกภาพว่าไม่มี locking เลย สองคน หรือคนกับ CI run รัน terraform apply เทียบกับ state บน S3 เดียวกันในเวลาที่ใกล้กันมาก ทั้งคู่อ่าน state ปัจจุบัน ทั้งคู่คำนวณ plan และทั้งคู่เริ่มเขียนผลกลับไปที่ state object เดียวกัน ขึ้นอยู่กับ timing การเขียนครั้งที่สองอาจเขียนทับครั้งแรก — ทำให้การเปลี่ยนแปลงที่ record ไว้จาก run แรกหายไปเงียบ ๆ ทั้งที่ infrastructure ของ run แรกถูกเปลี่ยนจริงไปแล้ว หรือ state file อาจ corrupt ระหว่างเขียน
lock เปลี่ยน race นี้ให้กลายเป็นคิว apply ตัวแรกที่เริ่มจะได้ lock ไป apply ตัวที่สองจะรอให้ lock ถูกปล่อย หรือ fail ทันทีพร้อม error ที่ชัดเจน ขึ้นอยู่กับ flag ที่ใช้ ไม่ว่าทางไหน run ตัวที่สองก็ไม่มีทางเขียนทับ state ของ run แรกในระหว่างที่ run แรกยังทำงานอยู่
# A run that finds the state already locked fails fast with a clear message,# rather than corrupting the state fileterraform apply# Error: Error acquiring the state lockflowchart LR eng1["Engineer A: terraform apply"] -->|acquires lock| s3["S3 state + lock file"] eng2["CI run: terraform apply"] -->|blocked, waits or fails| s3 s3 -->|lock released after apply| eng2
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกกลไกในบทนี้มีไว้ตอบคำถามเดียวอย่างตรงไปตรงมา “ใครชนะตอนเขียน และผู้แพ้รู้ตัวไหม” remote backend ตอบว่า “อะไรคือฉบับจริงหนึ่งเดียว” ไม่ให้ laptop ใครเป็นฉบับนั้น locking ตอบว่า “เกิดอะไรขึ้นตอนสองการเขียนชนกัน” โดยเปลี่ยน race ให้เป็นคิวแทนที่จะเป็นการสุ่ม -migrate-state กับ -reconfigure ตอบว่า “record เก่าตามมาด้วยไหม” ตอน backend location เปลี่ยน force-unlock มีอยู่เพราะกลไกคิวเองก็ติดขัดได้ (lock holder ที่จะไม่มีวันปล่อยแล้ว) — มันคือการ override คิวด้วยมือ ไม่ใช่ตัวแทนของคิว ซึ่งเป็นเหตุผลที่มันต้องการ lock ID จริง ไม่ใช่ flag “unlock ทุกอย่างเลย”
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
use_lockfile = true แทน dynamodb_table | AWS resource น้อยลงหนึ่งตัวที่ต้องสร้าง จ่ายเงิน และดูแลให้ sync กับ state bucket (พิสูจน์ข้างบน locking อยู่ใน S3 เอง) | config dynamodb_table แบบเก่าต้อง migrate อย่างตั้งใจ ปล่อยทิ้งไว้เฉย ๆ ไม่ได้ |
-migrate-state แทน -reconfigure ตอน backend location เปลี่ยน | state เก่าตามมาด้วย — พิสูจน์ข้างบน resource ยังถูก track อยู่หลังจากนั้น | ต้องตอบ prompt แบบ interactive (หรือ -input=false บวกความระมัดระวัง) — พลาดกดตอบ “no” แล้วเสียการเชื่อมกับ infrastructure ที่มีอยู่ได้ง่าย |
Partial backend config (-backend-config) | ค่าเฉพาะ environment กับ secret ไม่ต้องอยู่ในไฟล์ .tf ที่ commit | backend block เพียว ๆ อ่านแยกไม่บอกอีกต่อไปว่า state จริงอยู่ bucket/key ไหน — ต้องไปหาไฟล์ -backend-config หรือ CI step ด้วย |
Terragrunt remote_state generate backend.tf | block เดียวใน root.hcl scope state path ให้ทุก unit ถูกต้องอัตโนมัติ (พิสูจน์ข้างบนด้วย output จริงที่ generate) | ไฟล์ที่ generate ไม่ควรแก้มือ — แก้แล้วจะถูกทับใน run ถัดไป (if_exists = "overwrite") |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
terraform init -reconfigureเป็นแค่ชื่อที่ปลอดภัยกว่าของ-migrate-state” พิสูจน์ข้างบน-reconfigureไม่ถามและไม่ copy state เลยplanครั้งถัดไปแสดง resource ที่ track ไว้เป็น “to create” อีกครั้ง เพราะ backend location ใหม่ไม่เคยถูกเติมข้อมูล - “lock ค้างเป็นเรื่องปกติ และ
force-unlockเป็นคำสั่งประจำวัน” พิสูจน์ข้างบนจาก-helptext จริง มันคือทางออกฉุกเฉินสำหรับ lock ที่ verify แล้วว่า holder หายไปจริง ไม่ใช่ default response สำหรับ apply ที่ช้าหรือกำลังทำงานอยู่ - “
dynamodb_tableยังต้อง config เพื่อให้ S3 locking ทำงานในปี 2026” พิสูจน์ข้างบนuse_lockfile = trueที่เพิ่มมาสำหรับ S3 native locking ใน Terraform 1.10.0 ตาม CHANGELOG ไม่ต้องมี DynamoDB table เลย —dynamodb_tableคือทางที่ deprecated แล้ว - “
backend.tfที่ Terragrunt generate เป็นแค่ copy-paste convenience — เขียนไฟล์เดียวกันด้วยมือก็ถูกต้องเหมือนกัน” พิสูจน์ข้างบน ไฟล์ที่ generate resolvepath_relative_to_include()ต่อ unit อัตโนมัติ เขียนมือหมายถึงต้อง derive และพิมพ์keyที่ถูกต้องไม่ซ้ำกันเองทุก unit โดยไม่มี check ว่าสอง unit ชน key กันหรือไม่
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review backend configuration หรือ migration script ที่ generate มา เช็คสิ่งเหล่านี้
- ใส่
dynamodb_tableเข้าไปใน S3 backend block ใหม่ “เพราะ S3 locking ทำงานแบบนี้” — พิสูจน์ข้างบน argument นั้น deprecated แล้วuse_lockfile = trueคือกลไกปัจจุบันที่ไม่ต้องมี AWS resource ตัวที่สอง terraform init -reconfigureถูกใช้ใน runbook step ที่ตั้งใจจะรักษา state เดิมไว้ — พิสูจน์ข้างบน-reconfigureไม่ migrate state เลย flag ที่ถูกต้องสำหรับ intent นั้นคือ-migrate-state- CI script ที่ pipe
yesเข้าterraform initทุกครั้งแบบไม่มีเงื่อนไขเพื่อเลี่ยง interactive prompt รวมถึง prompt “copy existing state?” ของ-migrate-state— คำตอบนั้นต้องเป็นyesที่ตั้งใจ (environment นี้เป็นการเปลี่ยน location จริง) ไม่งั้น script เสี่ยง copy state ไปผิดที่แบบเงียบ ๆ force-unlockถูกเรียกด้วย lock ID ที่คิดขึ้นเองหรือ copy มาจาก tutorial แทนที่จะเป็น ID ที่ print ออกมาจาก error “state is locked” จริงของ run นั้นbucket/key/regionhardcode ไว้ใน module ที่ตั้งใจ reuse ข้าม environment แทนที่จะเป็นbackend "s3" {}เปล่า ที่เติมด้วย-backend-configต่อ environment (หรือใน Terragrunt คือremote_stateblock เดียวที่ generate ให้ถูกต้องต่อ unit)
🐛 หา bug ในโค้ดนี้
# CI "cleanup" step run before every apply, to avoid ever seeing a locked-state errorterraform force-unlock -force $(cat last-known-lock-id.txt)terraform apply -auto-approveเฉลย
บรรทัดนี้รัน force-unlock แบบไม่มีเงื่อนไขก่อน apply ทุกครั้ง โดยใช้ lock ID ที่ cache มาจาก run ก่อนหน้า ไม่ใช่ ID ของ lock ที่มีอยู่จริงตอนนี้ พิสูจน์ข้างบน force-unlock “will not modify your infrastructure” ด้วยตัวมันเอง แต่การปล่อย lock ที่กำลังถูกถืออยู่จริง — โดย teammate ที่ apply อยู่ หรือ CI run ที่รันพร้อมกัน — แบบไม่ดูอะไร คือการเอาการป้องกันเดียวที่ state locking ให้ไว้ออกไป บรรทัดถัดมา apply จะแข่งตรง ๆ กับสิ่งที่ถือ lock นั้นอยู่ ซึ่งคือ scenario การเขียนพังที่ locking มีไว้ป้องกันเป๊ะ ๆ การใช้ force-unlock ที่ถูกต้องคือ reactive และทำด้วยมือ อ่าน error “state is locked” จริงที่ระบุ ID มา verify แยกว่า holder หายไปจริง แล้วค่อย unlock ID นั้นตัวเดียว — ไม่ใช่ step ล่วงหน้าใน script ที่รันไม่ว่าจะมีอะไร lock อยู่จริงหรือไม่