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

Remote Backends and Locking

remote backend เก็บ state ไว้ในที่ที่แชร์กันได้อย่าง Azure Storage แทนที่จะอยู่บน laptop ของคนเดียว และ locking ก็ป้องกันไม่ให้สองคนเขียน state ที่แชร์กันนั้นพร้อมกัน

โดย default terraform.tfstate อยู่บนเครื่องที่คุณรัน apply นั่นแหละ ซึ่งใช้ได้ดีตอนทำคนเดียว แต่พังทันทีที่มีคนที่สองเข้ามา

  • เพื่อนร่วมทีม A รัน apply แล้วสร้าง virtual network ไฟล์ state บน laptop ของเขาก็รู้เรื่องนี้แล้ว
  • เพื่อนร่วมทีม B บน laptop คนละเครื่อง ไม่เคยเห็นไฟล์ state นั้นเลย เขารัน plan เทียบกับ local state ของตัวเองที่เก่าหรือว่างเปล่า แล้วก็จะพยายามสร้าง virtual network ซ้ำ หรือไม่ก็ได้ plan ที่ไม่สมเหตุสมผลเทียบกับของจริงที่มีอยู่
  • CI/CD pipeline เจอปัญหาเดียวกันแต่หนักกว่า — pipeline runner คือ environment ใหม่ที่ใช้แล้วทิ้ง ไม่มี local state file อยู่เลย จึงต้องการ state ชุดเดียวที่เป็น canonical ให้อ่านและเขียนทุกครั้งที่รัน ไม่ใช่สำเนาที่บังเอิญอยู่บน laptop ใครสักคน

ทางแก้คือย้าย state ออกจากเครื่องใดเครื่องหนึ่ง ไปไว้บน shared backend ที่ทั้งทีมและทุก CI run ชี้ไปที่เดียวกัน

วิธีมาตรฐานในการเก็บ Terraform state บน Azure คือใช้ Azure Storage container เป็น backend

terraform {
backend "azurerm" {
resource_group_name = "tfstate-rg"
storage_account_name = "acmetfstate001"
container_name = "tfstate"
key = "networking.terraform.tfstate"
}
}

resource_group_name กับ storage_account_name ระบุว่า storage account ไหนเก็บ state ไว้ container_name คือ blob container ใน account นั้น และ key คือชื่อ blob ภายใน container — configuration ที่ต่างกัน (หรือ Terragrunt unit ที่ต่างกัน) มักใช้ key คนละตัวใน container เดียวกัน ทั้งสี่ argument นี้จำเป็นทั้งหมด เพื่อให้ backend รู้ว่า blob ตัวไหนเก็บ state ของ configuration นี้อยู่

นี่คือจุดที่ต่างจาก cloud อื่นจริง ๆ state locking ของ azurerm backend ติดมากับวิธีทำงานของ Azure Blob Storage เอง ไม่ต้อง config อะไรเพิ่มเลย ตอน Terraform เขียน state ผ่าน backend นี้ จะขอ blob lease บน state blob ก่อนเขียน — เป็นฟีเจอร์ native ของ Azure Blob Storage ที่ให้สิทธิ์เขียนแบบ exclusive กับคนที่ถือ lease อยู่ พอ apply ตัวที่สองพยายามขอ lease ตัวเดียวกันตอนที่ lease ยังถูกถืออยู่ ก็ขอไม่ได้ เลยต้องรอหรือ fail แบบ clean แทนที่จะไปเขียนทับ run แรก

ไม่มี lock table แยกที่ต้องสร้าง ไม่มี argument เพิ่มที่ต้องตั้ง และไม่มี Azure resource เพิ่มที่ต้อง provision แค่เพื่อ locking เลย argument สี่ตัวใน block ข้างบนคือทั้งหมดที่ azurerm backend ต้องการ locking มาฟรีทันทีที่ state ถูกเก็บใน Azure Storage เรื่องนี้ควรพูดให้ชัดเพราะเรียบง่ายกว่า backend ของ cloud อื่นบางเจ้าที่ผ่านมา ซึ่ง locking เคยต้องมี resource แยกไว้เก็บ lock record ต่างหาก

ลองนึกภาพว่าไม่มี locking คนสองคน หรือคนหนึ่งกับ CI run หนึ่ง รัน terraform apply เข้าไปที่ state เดียวกันบน Azure Storage ในเวลาใกล้เคียงกัน ทั้งคู่อ่าน state ปัจจุบัน ทั้งคู่คำนวณ plan และทั้งคู่เริ่มเขียนผลลัพธ์กลับไปที่ state blob เดียวกัน แล้วแต่จังหวะเวลา การเขียนครั้งที่สองอาจเขียนทับครั้งแรก — ทำให้การเปลี่ยนแปลงที่ run แรกบันทึกไว้หายไปเงียบ ๆ ทั้งที่ infrastructure ของ run แรกถูกเปลี่ยนไปจริง ๆ แล้ว หรือไม่ก็ state file อาจเสียหายกลางคัน

lock เปลี่ยน race แบบนั้นให้กลายเป็นคิว apply ตัวแรกที่เริ่มจะได้ blob lease ไป ส่วน apply ตัวที่สองก็ต้องรอให้ lease ถูกปล่อย หรือไม่ก็ fail ทันทีด้วย error ที่ชัดเจน แล้วแต่ flag ที่ใช้ ไม่ว่าทางไหน run ที่สองก็ไม่มีทางเขียนทับ state ของ run แรกในระหว่างที่ run แรกยังทำงานอยู่

Terminal window
# A run that finds the state already locked fails fast with a clear message,
# rather than corrupting the state file
terraform apply
# Error: Error acquiring the state lock
flowchart LR
  eng1["Engineer A: terraform apply"] -->|acquires blob lease| storage["Azure Storage: state blob"]
  eng2["CI run: terraform apply"] -->|blocked, waits or fails| storage
  storage -->|lease released after apply| eng2
Two applies race for the same Azure-Storage-backed state; the built-in blob lease lets one through and makes the other wait
ทำไม local terraform.tfstate ถึงพังทันทีที่เพื่อนร่วมทีมคนที่สองเริ่มรัน Terraform
ต้อง config resource หรือ argument เพิ่มอะไรใน azurerm backend เพื่อเปิดใช้ state locking
state locking ป้องกันอะไรจริง ๆ