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
พิสูจน์: terragrunt render resolve find_in_parent_folders แบบจริง
หัวข้อที่มีชื่อว่า “พิสูจน์: terragrunt render resolve find_in_parent_folders แบบจริง”สิ่งที่พูดไว้ข้างบนไม่ได้เดาเอาเอง — มาจาก Terragrunt 1.1.0 จริงกับ unit tree ในสคแรตช์ root.hcl บนสุดของ tree:
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" }}
generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOFprovider "azurerm" { features {} subscription_id = "00000000-0000-0000-0000-000000000000"}EOF}และ vnet/terragrunt.hcl ที่ใช้ find_in_parent_folders("root.hcl") — Terragrunt เดินขึ้นไปจาก unit directory ปัจจุบัน หาไฟล์ที่ชื่อตรงนี้เป๊ะ ๆ ทำให้ vnet/ ไม่ต้อง hardcode ว่าต้องเดินขึ้นกี่ directory ถึงจะถึง root:
include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "."}
inputs = { address_space = ["10.0.0.0/16"]}รัน terragrunt render --format json จากข้างใน vnet/ (ไม่มี apply ไม่มี network — render แค่ parse กับ evaluate HCL เฉย ๆ) จะพิมพ์ config ที่ resolve ครบแล้วที่ Terragrunt กำลังจะส่งให้ Terraform ผลจริง ตัดบางส่วนออก:
$ terragrunt render --format json{ "remote_state": { "backend": "azurerm", "config": { "container_name": "tfstate", "key": "vnet/terraform.tfstate", "resource_group_name": "rg-terraform-state", "storage_account_name": "acmetfstate", "subscription_id": "00000000-0000-0000-0000-000000000000" }, "generate": { "if_exists": "overwrite_terragrunt", "path": "backend.tf" } }, "generate": { "provider": { "contents": "provider \"azurerm\" {\n features {}\n subscription_id = \"00000000-0000-0000-0000-000000000000\"\n}\n", "if_exists": "overwrite_terragrunt", "path": "provider.tf" } }, "inputs": { "address_space": ["10.0.0.0/16"] }, "terraform": { "source": "." }}หลักฐานอยู่ที่ "key": "vnet/terraform.tfstate" — path_relative_to_include() evaluate ออกมาเป็น string "vnet" เพราะ unit นี้อยู่ตรงนั้นเทียบกับ root.hcl ที่มัน include เข้ามา แล้วถูก interpolate เข้าไปใน key โดย unit ฝั่งนี้ไม่ต้อง hardcode อะไรเลย unit vm/terragrunt.hcl ที่มี body สามบรรทัดแบบเดียวกันเป๊ะ ก็จะ resolve ออกมาเป็น "key": "vm/terraform.tfstate" แทน — root.hcl เดียวกัน key ต่างกัน มาจากตำแหน่ง directory ล้วน ๆ
ตัว module เองก็จริงด้วย — azurerm_resource_group + azurerm_virtual_network พร้อม address_space = var.address_space validate ด้วย terraform init -backend=false && terraform validate จริงกับ hashicorp/azurerm ~> 5.0:
$ terraform validateSuccess! The configuration is valid.backend.tf จริงที่ generate เขียน
หัวข้อที่มีชื่อว่า “backend.tf จริงที่ generate เขียน”generate ไม่ได้เรียก Azure มันแค่เขียนไฟล์ .tf ธรรมดาเข้าไปใน working copy ของ unit ก่อนที่ Terraform จะรัน เนื้อหาจริง capture มาจาก working directory .terragrunt-cache ของ Terragrunt เอง (ไม่เคย init กับ storage account จริงเลย — คอร์สนี้ไม่มี Azure credentials และบทนี้พิสูจน์ตัว ไฟล์ ไม่ใช่ backend ที่ใช้งานจริง):
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEaterraform { backend "azurerm" { container_name = "tfstate" key = "vnet/terraform.tfstate" resource_group_name = "rg-terraform-state" storage_account_name = "acmetfstate" subscription_id = "00000000-0000-0000-0000-000000000000" }}comment # Generated by Terragrunt กับบรรทัด signature นั้นเป็นของจริง — Terragrunt เขียนไว้เพื่อให้ run ครั้งถัดไปรู้ว่าไฟล์นี้เป็นของตัวเอง แล้ว if_exists = "overwrite_terragrunt" ก็ overwrite ได้อย่างปลอดภัย โดยไม่ไปแตะ backend.tf ที่คนเขียนเองด้วยมือภายใต้ signature คนละแบบ ลองรันจริง (terragrunt plan) จะไปเจอกำแพงจริง กำแพงที่คาดไว้: unable to build authorizer for Resource Manager API: could not configure AzureCli Authorizer: could not parse Azure CLI version: launching Azure CLI: exec: "az": executable file not found in $PATH — auth failure ของ Azure จริง ไม่ใช่ error ปลอม พิสูจน์ว่า generate ส่ง backend ที่ shape ถูกต้องให้ Terraform จริง ๆ ก่อนจะหยุดที่กำแพงเดียวที่คอร์สนี้ข้ามไม่ได้ (ไม่มี Azure credentials ไม่มี az CLI ติดตั้ง)
remote_state auto-create บน Azure — และเส้นแบ่ง version ที่สำคัญตรงนี้
หัวข้อที่มีชื่อว่า “remote_state auto-create บน Azure — และเส้นแบ่ง version ที่สำคัญตรงนี้”remote_state ทำมากกว่าแค่ generate backend file — สำหรับ backend บางกลุ่ม Terragrunt จะสร้าง storage resource ให้เองด้วยตอนรันครั้งแรก ถ้ายังไม่มีอยู่ นี่คือข้อเท็จจริงเรื่อง version ที่สำคัญที่สุดสำหรับคอร์สนี้ ควรพูดให้แม่นยำ ไม่ใช่พูดกว้าง ๆ quote ตรงจาก Terragrunt experiments registry:
“Terragrunt manages lifecycle operations for the Azure Storage (
azurerm) remote state backend, matching what it already does for S3 buckets and GCS buckets: Bootstrap: creates the resource group (when allowed), storage account, and blob container backing the state, and converges blob versioning and optional soft delete on both new and pre-existing accounts. Delete: removes state blobs or entire containers, with confirmation prompts. Migrate: moves state blobs within the same storage account.”
แต่บน Terragrunt 1.1.0 ที่ติดตั้งในคอร์สนี้ lifecycle bootstrap/delete/migrate ตัวนี้ยังถูก gate อยู่ ตาม docs.terragrunt.com/features/state-backend: “azurerm backend … <Before version="v1.2.0"> (experimental; bootstrap, delete, and migrate require the azure-backend experiment)” มัน graduate เป็น on-by-default ใน Terragrunt 1.2.0 เท่านั้น พิสูจน์ข้างบน generate+init ธรรมดากับ azurerm ทำงานได้โดยไม่ต้องมี flag เพิ่มบน 1.1.0 ที่ติดตั้งอยู่นี้ (run ไปถึง error auth จริงของ Azure ไม่ใช่ error แบบ “experiment ไม่ได้เปิด”) — แต่การ auto-create resource group/storage account/container โดยเฉพาะต้องมี --experiment azure-backend ตรง ๆ จนกว่าจะถึง 1.2.0 เทียบให้เห็นภาพ เพราะต่างกันจริงตาม cloud: S3 กับ GCS auto-create bucket โดยไม่ต้องมี flag เพิ่มเลย (GCS ต้องมี key project/location เพิ่มเพื่อรู้ว่าจะสร้างที่ไหน) — azurerm คือ backend เดียวในสามตัวที่ lifecycle bootstrap เองยังอยู่หลัง experiment flag บน version ที่ติดตั้งอยู่นี้
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ตอนรีวิว root.hcl/unit config ที่ AI สร้าง ให้เช็คสิ่งเหล่านี้:
keyในremote_state.configที่ hardcode แทนที่จะเป็น"${path_relative_to_include()}/terraform.tfstate"— พิสูจน์ข้างล่าง ทำให้ทุก unit ที่ include root เดียวกันแอบ share state blob เดียวกัน แล้วapplyของ unit ที่สองอาจทำลาย resource ของ unit แรกได้โดยไม่มี error ใด ๆ เตือนเลยgenerateblock ที่ไม่มีif_exists = "overwrite_terragrunt"— ค่า default คือ"error"ดังนั้น run ครั้งที่สองของ checkout ใหม่จะ fail ทันทีด้วย “file already exists” ตอนที่ run ก่อนหน้าเขียนbackend.tfไปแล้วazurermremote_stateblock ที่สมมติว่า storage account/resource group ถูกสร้างอัตโนมัติโดยไม่ต้องมี flag เพิ่มบน Terragrunt 1.1.0 — พิสูจน์ข้างบน bootstrap ต้องมี--experiment azure-backendบน version ที่ติดตั้งในคอร์สนี้provider "azurerm" {}ที่ generate มาขาดfeatures {}block เปล่า ๆ — azurerm provider ปฏิเสธไม่รันถ้าไม่มี แม้จะไม่มีอะไรต้อง configure ข้างในก็ตามterraform { source = "..." }ที่ไม่ pin?ref=บน Git URL — ทุก unit resolve ตาม default branch ปัจจุบัน ทำให้applyสองครั้งห่างกันแค่ชั่วโมงเดียวอาจดึง module โค้ดคนละเวอร์ชันจริง ๆ โดยไม่มี diff ให้เห็นในterragrunt.hclของ unit เลย
🐛 หา bug ในโค้ดนี้
# root.hcl — shared by every unitremote_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 = "terraform.tfstate" subscription_id = "00000000-0000-0000-0000-000000000000"}}เฉลย
บั๊กคือ key = "terraform.tfstate" — literal string ธรรมดา ไม่ใช่ "${path_relative_to_include()}/terraform.tfstate" พิสูจน์ข้างบนแล้วว่า path_relative_to_include() คือตัวที่ทำให้ unit vnet resolve เป็น "vnet/terraform.tfstate" และ unit vm resolve เป็น "vm/terraform.tfstate" ถ้า hardcode literal ทั้งสอง unit จะ resolve ไปที่ blob key เดียวกันเป๊ะภายใน container เดียวกัน Terragrunt ไม่เตือนเรื่องนี้เลย ไม่มีการเช็คว่า container+key คู่นี้ “เป็น state ของ unit อื่นอยู่แล้ว” อาการแรกที่มักเจอคือ apply ของ vm รายงานว่าอยากจะ destroy resource ที่ไม่มีใครจำได้ว่าสร้างไว้ — จริง ๆ นั่นคือ resource ของ vnet เพราะทั้งสอง unit อ่านและเขียนทับ state blob เดียวกันมาตลอด
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”root.hcl ไม่ใช่ inheritance แบบ magic — มันคือไฟล์เดียวที่ include copy เข้าไปใน config ของทุก unit ที่ evaluate แล้ว เหมือน shared header ที่ถูก #include เข้าไปใน C source หลายไฟล์ ไม่มีอะไร share กันตอน runtime จริง ๆ แต่ละ unit ก็ยังรัน terraform init/plan/apply ของตัวเองแยกอิสระ กับ backend.tf ที่ generate ออกมาของตัวเอง ความ unique ของ key (ผ่าน path_relative_to_include()) คือสิ่งเดียวที่กั้นระหว่าง “unit อิสระที่มี state อิสระ” กับ “unit ที่แอบทำลาย state กันเอง” — พิสูจน์ตรง ๆ ข้างบนแล้ว ให้มอง Terragrunt unit เหมือน Terraform root module เป๊ะ ๆ เพราะมันคือแบบนั้นจริง ๆ terragrunt.hcl กับ include แค่ automate การเขียน backend กับ provider block ที่ root module ที่รันด้วยมือต้องพิมพ์เองทุกครั้ง
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
| แบ่ง infrastructure เป็น unit (state แยกต่อ directory) | ความผิดพลาดหรือ lock ใน unit หนึ่งแตะ unit อื่นไม่ได้ plan เล็กลง | reference ข้าม unit ต้องใช้ dependency block (บทถัดไป) แทนการ reference resource ใน state เดียวกัน |
remote_state + generate รวม backend ไว้ที่เดียว | ที่เดียวที่ต้องแก้ storage account/container ของทุก unit ไม่มี backend block copy-paste เลย | ไฟล์ที่ generate มองไม่เห็นจนกว่าจะไปหาใน .terragrunt-cache — รัน terraform init ตรง ๆ ใน unit directory (ข้าม Terragrunt) จะไม่เห็นมัน |
Azure auto-create ถูก gate ไว้หลัง --experiment azure-backend บน 1.1.0 | opt-in ชัดเจนก่อนที่ Terragrunt จะได้รับอนุญาตให้สร้าง resource ที่มีผลต่อ billing จริงของ Azure (storage account) แทนคุณ | พิสูจน์ข้างบนว่าทำผิดจะดูเหมือน auth fail จริงของ Azure ไม่ใช่ flag หาย เว้นแต่จะรู้ล่วงหน้าว่าต้องเช็ค experiment gate |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “root.hcl กับ terragrunt.hcl เป็นไฟล์ format คนละแบบ” เป็น HCL dialect เดียวกัน — พิสูจน์ข้างบน
root.hclของคอร์สนี้ใช้remote_state/generateblock เดียวกันเป๊ะกับที่terragrunt.hclของ unit ใช้ได้ ชื่อเป็นแค่ convention ไม่มีอะไรบังคับ - “generate เขียนตรงเข้า unit directory ของฉันเลย ต้องเห็น backend.tf อยู่ตรงนั้น” พิสูจน์ข้างบน
backend.tfจริงที่บทนี้ capture มาอยู่ใน.terragrunt-cache/HASH/HASH/backend.tfซึ่งเป็น working copy ของ Terragrunt — ไม่ใช่ source directory ที่คุณแก้ไข - “cloud backend ทุกตัวที่ Terragrunt รองรับ auto-create storage แบบเดียวกันหมด” พิสูจน์ข้างบนด้วย quote จากเอกสารและ run จริง: S3 กับ GCS auto-create โดยไม่ต้องมี flag เพิ่ม (GCS ต้องมี
project/location)azurermบน Terragrunt 1.1.0 ที่ติดตั้งในคอร์สนี้ต้องมี--experiment azure-backendสำหรับ lifecycle bootstrap/delete/migrate โดยเฉพาะ กลายเป็น on-by-default ใน 1.2.0 เท่านั้น - “azurerm backend ต้องมี lock resource แยก เหมือน backend ของ cloud อื่นบางเจ้า” ไม่จริง — Azure Blob Storage มี lease mechanism ของตัวเองที่ทำ locking ให้เลย ไม่มี lock-table เทียบเท่าที่ไหนเลยใน
remote_state.configที่โชว์ข้างบน