Production Checklist
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”setup Terraform และ Terragrunt ที่พร้อมใช้งานจริงบน Azure ไม่ใช่ feature เดียว แต่เป็นผลรวมของทุก practice ที่ course นี้พูดถึงมา ทั้ง remote state, version ที่ pin ไว้, plan ที่ถูก review, secret ที่ป้องกันไว้, static analysis กับ policy gate, unit ที่เรียงลำดับกันตาม dependency, และ test จริง เอามาผูกกันเป็น repository เดียวที่ขับเคลื่อนด้วย CI
Checklist หนึ่งเหตุผลต่อหนึ่งข้อ
หัวข้อที่มีชื่อว่า “Checklist หนึ่งเหตุผลต่อหนึ่งข้อ”แต่ละข้อด้านล่างย้อนกลับไปหา failure mode จริงที่ lesson ใด lesson หนึ่งของ course นี้ถูกสร้างขึ้นมาเพื่อแก้
- remote
azurermbackend พร้อม blob-lease locking ตั้งแต่วันแรก local state แปลว่า disk เครื่องเดียวคือสำเนาความจริงหนึ่งเดียวของทั้ง Azure footprint และถ้า engineer สองคนรันapplyพร้อมกันก็ทำให้ state corrupt ได้ นี่คือเหตุผลทั้งหมดที่ module State Management มีอยู่ - version ของ provider ที่ pin ผ่าน
.terraform.lock.hcl, version ของ module ที่ pin ไว้กับ tag หรือ version constraint provider หรือ module ที่ไม่ pin อาจ resolve ไปเป็น major version ใหม่แบบเงียบ ๆ ในครั้งถัดไปที่initรัน เปลี่ยน behavior ที่ไม่มีใครขอกลางคันของ change ที่ไม่เกี่ยวข้องกันเลย planถูก review ใน CI ก่อนทุกapplyโดยจับตาดู resource replacement ที่ไม่คาดคิดโดยเฉพาะ-/+ใน plan แปลว่าทำลายแล้วสร้างใหม่บน resource จริงของ Azure ที่มี state การ review plan แบบ code diff ธรรมดาพลาดบรรทัดที่สำคัญที่สุดไปเลย- ค่า sensitive เก็บนอก Terraform variable และ state ในจุดที่มี secrets-manager data source ให้ใช้ได้ และ Azure RBAC จำกัดคนที่อ่าน state container ได้อย่างเข้มงวด
sensitive = trueแค่ redact CLI output เท่านั้น การป้องกันจริงคือจำกัด read access ให้ backend และลดสิ่งที่ต้องพิมพ์ลง variable ตั้งแต่แรก tflintกับ security scanner และ ideally policy as code ต่อเข้ากับ CI static analysis จับ HCL ที่รู้กันว่าไม่ดีในไม่กี่วินาทีก่อนplanจะรันด้วยซ้ำ ส่วน policy as code (Sentinel หรือ OPA) evaluate plan ที่ compute จริงเทียบกับ rule ขององค์กรที่มีความหมายก็ต่อเมื่อมี change จริงเกิดขึ้นแล้ว- Terragrunt
dependencyblock ขับเคลื่อนลำดับ rollout ของหลาย unit การแยก infrastructure เป็น unit อิสระ (vnet,vm,database) จะปลอดภัยก็ต่อเมื่อ unit ที่ต้องใช้ output ของอีก unit เช่น subnet ID, ชื่อ resource group ประกาศ dependency นั้นไว้ตรง ๆ เพื่อให้ Terragrunt apply ตามลำดับที่ถูกต้อง แทนที่จะพึ่งให้คนจำเอง terraform testและ.tftest.hclcoverage สำหรับ module ที่ทำอะไรไม่ trivialvalidatecheck syntax และplancheck ว่า Azure ยอมรับ request แต่ทั้งคู่ไม่ assert เลยว่า behavior จริงของ module ถูกต้องหรือเปล่า นี่คือหน้าที่ของterraform test
Dependency block คือชิ้นที่เรียงลำดับ rollout ของหลาย unit
หัวข้อที่มีชื่อว่า “Dependency block คือชิ้นที่เรียงลำดับ rollout ของหลาย unit”Terragrunt dependency block คือตัวที่ให้ unit หนึ่งใช้ output จริงของอีก unit ได้ โดยที่ unit ทั้งสองไม่ต้องรู้ implementation ภายในของกันและกันเลย unit vm ที่ต้องใช้ subnet ที่ unit vnet สร้างไว้ ประกาศ dependency นั้นตรง ๆ ใน terragrunt.hcl ของตัวเอง
include "root" { path = find_in_parent_folders("root.hcl")}
dependency "vnet" { config_path = "../vnet"
mock_outputs = { subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/mock-subnet" } mock_outputs_allowed_terraform_commands = ["plan"]}
terraform { source = "git::https://github.com/acme-corp/terraform-modules.git//vm?ref=v1.4.0"}
inputs = { subnet_id = dependency.vnet.outputs.subnet_id}Terragrunt อ่าน dependency block นี้แล้วหาลำดับ apply ที่ถูกต้องเอง terragrunt run --all apply จะ apply vnet ก่อน อ่าน output subnet_id จริงเมื่อมีค่าแล้ว แล้วค่อยรัน vm ด้วยค่าจริงนั้นเป็น input ไม่มีใครต้อง hardcode ลำดับหรือต้องจำเองว่าจะ apply unit ไหนก่อนหลัง block mock_outputs สำคัญเฉพาะกับ plan เพราะทำให้ plan ของ vm รันผ่านได้สะอาดแม้ก่อนที่ vnet จะเคยถูก apply เลย โดยใช้ค่า placeholder แทนที่จะ fail เพราะ output จริงยังไม่มีอยู่
ภาพรวมแบบ end-to-end
หัวข้อที่มีชื่อว่า “ภาพรวมแบบ end-to-end”เอาทุกข้อมารวมกัน production Terragrunt repository บน Azure จะหน้าตาแบบนี้ root.hcl ตัวเดียวอยู่บนสุด ประกาศ shared remote_state backend กับ provider generate block, unit directory จำนวนหนึ่งอยู่ข้างล่าง vnet/, vm/ (หรือ aks/), และ sql/ เป็น set ขั้นต่ำที่ใช้งานจริงได้ แต่ละตัวมี terragrunt.hcl ของตัวเองที่ wire dependency block ที่ตัวเองต้องใช้จริง ๆ และ CI pipeline ที่ authenticate ผ่าน OIDC federated credential รัน terragrunt run --all plan ทุก pull request และ terragrunt run --all apply เฉพาะหลัง merge เข้า main เท่านั้น
name: terraform
on: pull_request: paths: - 'infra/**' push: branches: - main paths: - 'infra/**'
permissions: id-token: write contents: read pull-requests: write
jobs: static-analysis: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: terraform-linters/setup-tflint@v4 - run: tflint --init && tflint --recursive - name: Run checkov uses: bridgecrewio/checkov-action@master with: directory: infra
plan: needs: static-analysis if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - uses: hashicorp/setup-terraform@v3 - run: terragrunt run --all plan -no-color | tee plan.txt - run: gh pr comment ${{ github.event.pull_request.number }} --body-file plan.txt env: GH_TOKEN: ${{ github.token }}
apply: needs: static-analysis if: github.event_name == 'push' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - uses: hashicorp/setup-terraform@v3 - run: terragrunt run --all apply --non-interactiveไม่มีอะไรใน pipeline นี้ที่ใหม่แล้วมาถึงจุดนี้ของ course ทุก stage คือ lesson ที่ผ่านมาแล้ว เอามาประกอบเป็น repository เดียว vnet, vm/aks, และ sql เป็น unit อิสระอยู่ใต้ root.hcl เดียว dependency block ให้ Terragrunt รู้ลำดับจริงระหว่างกัน tflint กับ checkov fail เร็วก่อนที่จะพยายาม plan ด้วยซ้ำ OIDC แทนที่ client secret ที่เก็บไว้ plan ที่ review แล้ว gate ทุก apply และ apply เองก็เก็บไว้เฉพาะหลัง merge หลัง approval gate รันโดย pipeline ไม่ใช่ laptop
flowchart TB root["root.hcl (remote_state + provider generate)"] --> vnet["vnet unit"] root --> vm["vm / aks unit"] root --> sql["sql unit"] vnet -->|dependency block| vm vnet -->|dependency block| sql pr["Pull request"] -->|OIDC auth| planj["terragrunt run --all plan"] planj -->|reviewed and merged| applyj["terragrunt run --all apply"] vnet -.-> planj vm -.-> planj sql -.-> planj