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

Terragrunt HCL and the Root Config

Terragrunt unit คือ infrastructure ที่ deploy ได้หนึ่งชิ้น มี terragrunt.hcl ของตัวเอง มี Terraform state ของตัวเอง ส่วนไฟล์ root.hcl ที่อยู่ชั้นบนสุดเก็บ config ที่ทุก unit ต้องใช้ร่วมกัน โดยไม่มี unit ไหนต้องเขียนซ้ำ

ใน module ก่อนหน้านี้ (Modules) คุณเห็นแล้วว่า Terraform root module ก็คือ directory ที่คุณรัน terraform apply ตรง ๆ Terragrunt เอาไอเดียเดียวกันนี้มาใช้ แล้วตั้งชื่อใหม่ว่า unit unit คือ directory ที่มี terragrunt.hcl ของตัวเอง ห่อ Terraform root module หนึ่งตัวไว้ข้างใน และมี state file แยกของตัวเอง unit ชื่อ vpc, unit ชื่อ ec2, และ unit ชื่อ rds คือ unit อิสระสามตัว มี directory สามอัน มี terragrunt.hcl สามไฟล์ มี state สามไฟล์ ทั้งที่ถ้าอยู่ในโลกของ Terraform โปรเจกต์เดียว คุณอาจเผลอจัดการ resource ทั้งสามตัวจาก root module เดียวและ state เดียวไปเลย

การแบ่ง infrastructure เป็น unit แบบนี้คือ trade-off ที่ตั้งใจทำ state ที่เล็กลงและ apply แยกอิสระจากกันแปลว่าความผิดพลาดใน unit หนึ่งไม่ทำให้ state ของอีก unit เสียหรือถูก lock ไปด้วย และ plan ของ ec2 ก็ไม่ต้อง evaluate ทุก resource ใน vpc กับ rds แค่เพื่อบอกว่าอะไรเปลี่ยนไป ต้นทุนที่ต้องแลกคือ unit เหล่านี้ต้องมีวิธี share config ที่ยังใช้ร่วมกันอยู่ ซึ่งนั่นคือหน้าที่ของ root.hcl พอดี

ลองนึกภาพ directory layout ที่มี config ไฟล์เดียวอยู่บนสุดของ account หรือ 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 ตลอดทั้งคอร์ส

ทุก unit ต้องการ Terraform backend ในงาน AWS เกือบทั้งหมดคือ S3 bucket สำหรับเก็บ state file การประกาศ backend ด้วยมือในทุก ๆ unit คือความซ้ำซ้อนแบบที่ Terragrunt เกิดมาเพื่อกำจัดพอดี แทนที่จะทำแบบนั้น root.hcl ประกาศไว้ครั้งเดียวด้วย remote_state block และ generate sub-block ข้างในบอก Terragrunt ให้เขียนไฟล์ backend.tf ลงไปในทุก unit ก่อนที่ Terraform จะรันที่นั่น

root.hcl
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "acme-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "acme-terraform-locks"
}
}

path_relative_to_include() ให้ key ที่ unique เฉพาะของแต่ละ unit ภายใน bucket เดียวกัน โดยอิงจากตำแหน่งของ unit นั้นเทียบกับไฟล์ที่ include เข้ามา ทำให้ unit vpc กับ unit ec2 ไปลง state path คนละที่กันโดยอัตโนมัติ โดยไม่มี unit ไหนต้องเขียน path นั้นตรง ๆ เอง if_exists = "overwrite_terragrunt" บอก Terragrunt ว่า overwrite backend.tf ที่ generate ไว้จาก run ก่อนหน้าได้อย่างปลอดภัย

top-level generate block ก็ทำงานแบบเดียวกันกับ provider configuration มักวางไว้ข้าง ๆ remote_state ใน root.hcl นี่แหละ

generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}

ทั้งสอง block ผลิตไฟล์ .tf ธรรมดา คือ backend.tf กับ provider.tf วางอยู่ข้าง ๆ โค้ด Terraform จริงที่ Terragrunt รัน Terraform เองไม่รู้เลยว่าไฟล์พวกนี้ถูก generate มา จากมุมมองของ Terraform คือ config ที่บังเอิญมีอยู่แล้วเฉย ๆ

remote_state กับ provider generate block อยู่ใน root.hcl เพราะทุก unit ใช้ร่วมกัน สิ่งที่ต่างกันในแต่ละ unit คือ module Terraform ตัวไหนที่ unit นั้นรันจริง ๆ ซึ่งอยู่ใน terragrunt.hcl ของ unit นั้นเอง ข้างใน terraform block

vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "git::[email protected]:acme/infrastructure-modules.git//vpc?ref=v1.4.0"
}
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 สามบรรทัดนี้ได้ S3 backend กับ AWS provider ที่ใช้งานได้จริงมาฟรี โดยไม่ต้องเขียนซ้ำแม้แต่บรรทัดเดียว

flowchart TD
  root["root.hcl (remote_state + generate)"]
  root --> vpc["vpc/terragrunt.hcl"]
  root --> ec2["ec2/terragrunt.hcl"]
  root --> rds["rds/terragrunt.hcl"]
  vpc --> vpcmod["terraform.source: modules//vpc"]
  ec2 --> ec2mod["terraform.source: modules//ec2"]
  rds --> rdsmod["terraform.source: modules//rds"]
  vpc -.->|inherits generated backend.tf + provider.tf| root
  ec2 -.->|inherits generated backend.tf + provider.tf| root
  rds -.->|inherits generated backend.tf + provider.tf| root
root.hcl หนึ่งไฟล์ป้อนหลาย unit แต่ละอันชี้ไปที่ module ของตัวเอง
Terragrunt unit คืออะไร
ทำไม shared parent config file ถึงตั้งชื่อตาม convention ว่า root.hcl แทนที่จะเป็น terragrunt.hcl
remote_state block ที่มี generate sub-block ผลิตอะไรออกมาจริง ๆ
terraform source attribute ที่ชี้ไปที่โค้ด module จริง ควรอยู่ตรงไหน