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 ชื่อ network, unit ชื่อ gke, และ unit ชื่อ cloud-sql คือ unit อิสระสามตัว มี directory สามอัน มี terragrunt.hcl สามไฟล์ มี state สามไฟล์ ทั้งที่ถ้าอยู่ในโลกของ Terraform โปรเจกต์เดียว คุณอาจเผลอจัดการ resource ทั้งสามตัวจาก root module เดียวและ state เดียวไปเลย
การแบ่ง infrastructure เป็น unit แบบนี้คือ trade-off ที่ตั้งใจทำ state ที่เล็กลงและ apply แยกอิสระจากกันแปลว่าความผิดพลาดใน unit หนึ่งไม่ทำให้ state ของอีก unit เสียหรือถูก lock ไปด้วย และ plan ของ gke ก็ไม่ต้อง evaluate ทุก resource ใน network กับ cloud-sql แค่เพื่อบอกว่าอะไรเปลี่ยนไป ต้นทุนที่ต้องแลกคือ unit เหล่านี้ต้องมีวิธี share config ที่ยังใช้ร่วมกันอยู่ ซึ่งนั่นคือหน้าที่ของ root.hcl พอดี
root.hcl: config ที่ทุก unit ยืนอยู่บน
หัวข้อที่มีชื่อว่า “root.hcl: config ที่ทุก unit ยืนอยู่บน”ลองนึกภาพ directory layout ที่มี config ไฟล์เดียวอยู่บนสุดของ project หรือ 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 บน GCP เกือบทั้งหมดคือ Google Cloud Storage (GCS) bucket สำหรับเก็บ state file การประกาศ backend ด้วยมือในทุก ๆ unit คือความซ้ำซ้อนแบบที่ Terragrunt เกิดมาเพื่อกำจัดพอดี แทนที่จะทำแบบนั้น root.hcl ประกาศไว้ครั้งเดียวด้วย remote_state block และ generate sub-block ข้างในบอก Terragrunt ให้เขียนไฟล์ backend.tf ลงไปในทุก unit ก่อนที่ Terraform จะรันที่นั่น
remote_state { backend = "gcs"
generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" }
config = { bucket = "acme-terraform-state" prefix = "${path_relative_to_include()}/terraform.tfstate" project = "acme-prod-123456" location = "US" }}path_relative_to_include() ให้ prefix ที่ unique เฉพาะของแต่ละ unit ภายใน bucket เดียวกัน โดยอิงจากตำแหน่งของ unit นั้นเทียบกับไฟล์ที่ include เข้ามา ทำให้ unit network กับ unit gke ไปลง state path คนละที่กันโดยอัตโนมัติ โดยไม่มี unit ไหนต้องเขียน path นั้นตรง ๆ เอง if_exists = "overwrite_terragrunt" บอก Terragrunt ว่า overwrite backend.tf ที่ generate ไว้จาก run ก่อนหน้าได้อย่างปลอดภัย ต่างจาก backend ของ cloud อื่นบางเจ้า GCS ไม่ต้องมี lock-table argument แยกต่างหากเลย เพราะ GCS จัดการ state locking ให้เองอยู่แล้ว จึงไม่มี setting แบบ dynamodb_table ให้ต้องตั้งตรงนี้
top-level generate block ก็ทำงานแบบเดียวกันกับ provider configuration มักวางไว้ข้าง ๆ remote_state ใน root.hcl นี่แหละ
generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOFprovider "google" { project = "acme-prod-123456" region = "us-central1"}EOF}ทั้งสอง block ผลิตไฟล์ .tf ธรรมดา คือ backend.tf กับ provider.tf วางอยู่ข้าง ๆ โค้ด Terraform จริงที่ Terragrunt รัน Terraform เองไม่รู้เลยว่าไฟล์พวกนี้ถูก generate มา จากมุมมองของ Terraform คือ config ที่บังเอิญมีอยู่แล้วเฉย ๆ
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 = { cidr_block = "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 สามบรรทัดนี้ได้ GCS backend กับ Google provider ที่ใช้งานได้จริงมาฟรี โดยไม่ต้องเขียนซ้ำแม้แต่บรรทัดเดียว
flowchart TD root["root.hcl (remote_state + generate)"] root --> network["network/terragrunt.hcl"] root --> gke["gke/terragrunt.hcl"] root --> sql["cloud-sql/terragrunt.hcl"] network --> networkmod["terraform.source: modules//network"] gke --> gkemod["terraform.source: modules//gke"] sql --> sqlmod["terraform.source: modules//cloud-sql"] network -.->|inherits generated backend.tf + provider.tf| root gke -.->|inherits generated backend.tf + provider.tf| root sql -.->|inherits generated backend.tf + provider.tf| root