Terragrunt HCL and the Root Config
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Terragrunt unit คือ infrastructure ที่ deploy ได้หนึ่งชิ้น มี terragrunt.hcl ของตัวเอง มี Terraform state ของตัวเอง ส่วนไฟล์ root.hcl ที่อยู่ชั้นบนสุดเก็บ config ที่ทุก unit ต้องใช้ร่วมกัน โดยไม่มี unit ไหนต้องเขียนซ้ำ
unit ของ Terragrunt คืออะไรกันแน่
หัวข้อที่มีชื่อว่า “unit ของ Terragrunt คืออะไรกันแน่”ใน module ก่อนหน้านี้ (Modules) คุณเห็นแล้วว่า Terraform root module ก็คือ directory ที่คุณรัน terraform apply ตรง ๆ Terragrunt เอาไอเดียเดียวกันนี้มาใช้ แล้วตั้งชื่อใหม่ว่า unit unit คือ directory ที่มี terragrunt.hcl ของตัวเอง ห่อ Terraform root module หนึ่งตัวไว้ข้างใน และมี state file แยกของตัวเอง unit ชื่อ vnet, unit ชื่อ vm, และ unit ชื่อ database คือ unit อิสระสามตัว มี directory สามอัน มี terragrunt.hcl สามไฟล์ มี state สามไฟล์ ทั้งที่ถ้าอยู่ในโลกของ Terraform โปรเจกต์เดียว คุณอาจเผลอจัดการ resource ทั้งสามตัวจาก root module เดียวและ state เดียวไปเลย
การแบ่ง infrastructure เป็น unit แบบนี้คือ trade-off ที่ตั้งใจทำ state ที่เล็กลงและ apply แยกอิสระจากกันแปลว่าความผิดพลาดใน unit หนึ่งไม่ทำให้ state ของอีก unit เสียหรือถูก lock ไปด้วย และ plan ของ vm ก็ไม่ต้อง evaluate ทุก resource ใน vnet กับ database แค่เพื่อบอกว่าอะไรเปลี่ยนไป ต้นทุนที่ต้องแลกคือ unit เหล่านี้ต้องมีวิธี share config ที่ยังใช้ร่วมกันอยู่ ซึ่งนั่นคือหน้าที่ของ root.hcl พอดี
root.hcl: config ที่ทุก unit ยืนอยู่บน
หัวข้อที่มีชื่อว่า “root.hcl: config ที่ทุก unit ยืนอยู่บน”ลองนึกภาพ directory layout ที่มี config ไฟล์เดียวอยู่บนสุดของ subscription หรือ environment tree แล้วมี terragrunt.hcl อยู่ในทุก unit directory ข้างใต้ ไฟล์บนสุดนี้เมื่อก่อนก็ตั้งชื่อว่า terragrunt.hcl เหมือนกัน เพราะ Terragrunt ไม่เคยบังคับชื่อเฉพาะไว้ การตั้งชื่อแบบนั้นก็ยังใช้ได้อยู่ทุกวันนี้ และ repository กับ tutorial เก่า ๆ จำนวนมากก็ยังใช้แบบนั้น แต่กำกวมทันทีที่มีคนพูดว่า “ไปดู terragrunt.hcl” เพราะไม่รู้ว่าหมายถึงไฟล์ root ที่ share กันหรือ unit เฉพาะที่กำลังยืนอยู่
convention ที่แนะนำตอนนี้เปลี่ยนมาตั้งชื่อไฟล์ parent ที่ share กันนี้ว่า root.hcl แทน เพื่อตัดความกำกวมนั้นออกไปตรง ๆ root.hcl คือไฟล์เดียวที่ share กันบนสุดอย่างไม่ต้องเดา ส่วน terragrunt.hcl คือ config เฉพาะของ unit นั้น ๆ อย่างชัดเจน คอร์สนี้ใช้ root.hcl ตลอดทั้งคอร์ส
remote_state กับ generate: backend เดียว ไม่ต้อง copy-paste ทุก unit
หัวข้อที่มีชื่อว่า “remote_state กับ generate: backend เดียว ไม่ต้อง copy-paste ทุก unit”ทุก unit ต้องการ Terraform backend ในงาน Azure คือ Azure Storage Account container สำหรับเก็บ state blob การประกาศ backend ด้วยมือในทุก ๆ unit คือความซ้ำซ้อนแบบที่ Terragrunt เกิดมาเพื่อกำจัดพอดี แทนที่จะทำแบบนั้น root.hcl ประกาศไว้ครั้งเดียวด้วย remote_state block และ generate sub-block ข้างในบอก Terragrunt ให้เขียนไฟล์ backend.tf ลงไปในทุก unit ก่อนที่ Terraform จะรันที่นั่น
remote_state { backend = "azurerm"
generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" }
config = { resource_group_name = "rg-terraform-state" storage_account_name = "acmetfstate" container_name = "tfstate" key = "${path_relative_to_include()}/terraform.tfstate" subscription_id = "00000000-0000-0000-0000-000000000000" }}path_relative_to_include() ให้ key ที่ unique เฉพาะของแต่ละ unit ภายใน container เดียวกัน โดยอิงจากตำแหน่งของ unit นั้นเทียบกับไฟล์ที่ include เข้ามา ทำให้ unit vnet กับ unit vm ไปลง state path คนละที่กันโดยอัตโนมัติ โดยไม่มี unit ไหนต้องเขียน path นั้นตรง ๆ เอง if_exists = "overwrite_terragrunt" บอก Terragrunt ว่า overwrite backend.tf ที่ generate ไว้จาก run ก่อนหน้าได้อย่างปลอดภัย สังเกตว่า block นี้ไม่มี lock-table argument เลย ต่างจาก backend ของ cloud อื่นบางเจ้า azurerm backend ไม่ต้องมี locking resource แยกต่างหาก เพราะใช้ blob-lease locking ที่ built-in มากับ Azure Blob Storage อยู่แล้ว
top-level generate block ก็ทำงานแบบเดียวกันกับ provider configuration มักวางไว้ข้าง ๆ remote_state ใน root.hcl นี่แหละ
generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOFprovider "azurerm" { features {} subscription_id = "00000000-0000-0000-0000-000000000000"}EOF}ทั้งสอง block ผลิตไฟล์ .tf ธรรมดา คือ backend.tf กับ provider.tf วางอยู่ข้าง ๆ โค้ด Terraform จริงที่ Terragrunt รัน Terraform เองไม่รู้เลยว่าไฟล์พวกนี้ถูก generate มา จากมุมมองของ Terraform คือ config ที่บังเอิญมีอยู่แล้วเฉย ๆ features {} block ที่ว่างเปล่านี้จำเป็นสำหรับ azurerm provider แม้จะไม่มีอะไรต้อง config ข้างในก็ตาม Terraform จะไม่ยอมรันถ้าไม่มี block นี้
source ใน terraform block: ชี้ unit ไปหา module ของตัวเอง
หัวข้อที่มีชื่อว่า “source ใน terraform block: ชี้ unit ไปหา module ของตัวเอง”remote_state กับ provider generate block อยู่ใน root.hcl เพราะทุก unit ใช้ร่วมกัน สิ่งที่ต่างกันในแต่ละ unit คือ module Terraform ตัวไหนที่ unit นั้นรันจริง ๆ ซึ่งอยู่ใน terragrunt.hcl ของ unit นั้นเอง ข้างใน terraform block
include "root" { path = find_in_parent_folders("root.hcl")}
terraform {}
inputs = { address_space = ["10.0.0.0/16"]}source ชี้ไปที่ local relative path ก็ได้ตอนพัฒนา หรือชี้ไปที่ Git URL แบบ pin tag เหมือนตัวอย่างข้างบนก็ได้ เป็นวิธี address module แบบเดียวกับที่คุณรู้จักจาก source argument ใน Terraform module block เพียงแต่ย้ายมาเขียนที่ระดับ unit แทนที่จะอยู่ใน main.tf บรรทัด include คือสิ่งที่ดึงทุกอย่างที่ root.hcl ประกาศไว้เข้ามา กลไกเต็ม ๆ ของ include เป็นหัวข้อของบทถัดไป ตอนนี้แค่สังเกตว่า unit config สามบรรทัดนี้ได้ Azure Storage backend กับ azurerm provider ที่ใช้งานได้จริงมาฟรี โดยไม่ต้องเขียนซ้ำแม้แต่บรรทัดเดียว
flowchart TD root["root.hcl (remote_state + generate)"] root --> vnet["vnet/terragrunt.hcl"] root --> vm["vm/terragrunt.hcl"] root --> database["database/terragrunt.hcl"] vnet --> vnetmod["terraform.source: modules//vnet"] vm --> vmmod["terraform.source: modules//vm"] database --> dbmod["terraform.source: modules//database"] vnet -.->|inherits generated backend.tf + provider.tf| root vm -.->|inherits generated backend.tf + provider.tf| root database -.->|inherits generated backend.tf + provider.tf| root