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

Policy as Code and Static Analysis

static analysis tool จับ pattern ที่รู้อยู่แล้วว่าแย่ใน HCL ก่อนที่ plan จะรันด้วยซ้ำ ส่วน policy-as-code tool ประเมิน plan ที่คำนวณออกมาจริงเทียบกับกฎขององค์กร แล้วบล็อกไม่ให้ apply ได้แบบอัตโนมัติถ้าละเมิดกฎ

terraform validate เช็คแค่ว่า HCL ของคุณ syntax ถูกต้องและสอดคล้องกันภายใน — type ตรงกัน, required argument ครบ, reference resolve ได้ แต่ไม่รู้เลยว่า machine type แบบ f1-micro เป็นตัวเลือกที่สมเหตุสมผลไหม หรือ Cloud Storage bucket ควร public read ได้จริงเหรอ tool สองประเภทนี้มาเติมช่องว่างตรงนั้น และทั้งคู่รันเร็ว ก่อนที่จะมีการ plan หรือ apply อะไรเลย

  • tflint รู้จัก provider มี plugin rule set ให้ใช้ รวมถึง tflint-ruleset-google ที่จับ mistake ที่ validate มองไม่เห็น เพราะต้องรู้ว่า Google Cloud API จริง ๆ รับอะไรได้บ้าง เช่น machine type ที่ deprecated ไปแล้ว, ชื่อ region ที่ไม่ถูกต้อง, argument ที่ provider เลิกรองรับไปเงียบ ๆ
  • security และ misconfiguration scanner ในตระกูล checkov/ผู้สืบทอดของ tfsec จะ pattern-match ตัว HCL เทียบกับ configuration แย่ ๆ ที่รู้จักอยู่แล้ว เช่น Cloud Storage bucket ที่เปิด public read access, persistent disk ที่ไม่เข้ารหัส, firewall rule ที่เปิดให้ 0.0.0.0/0 เข้าถึง port ที่ sensitive tfsec เคยครอบคลุมเรื่องคล้าย ๆ กัน แล้ว check เหล่านั้นก็ถูกรวมเข้าไปใน Trivy ตั้งแต่นั้นมา ส่วน checkov ยังเป็นตัวเลือกที่ใช้กันแพร่หลายและ maintain อยู่ต่อเนื่องสำหรับ layer นี้
.tflint.hcl
plugin "google" {
enabled = true
version = "0.29.0"
source = "github.com/terraform-linters/tflint-ruleset-google"
}

รันจริง ทั้งสอง tool ผ่าน docker run --rm กับ config เล็ก ๆ ที่ตั้งใจทำให้แย่: machine type ที่ invalid, IAM binding แบบ public-read บน bucket, และ firewall rule เปิด port 22 ให้ทั้งโลก:

$ docker run --rm -v "$PWD:/data" -w /data ghcr.io/terraform-linters/tflint:latest --init
Installing "google" plugin...
Installed "google" (source: github.com/terraform-linters/tflint-ruleset-google, version: 0.29.0)
$ docker run --rm -v "$PWD:/data" -w /data ghcr.io/terraform-linters/tflint:latest
2 issue(s) found:
Warning: terraform "required_version" attribute is required (terraform_required_version)
on main.tf line 1:
1: terraform {
Error: "n2-standard-999" is an invalid as machine type (google_compute_instance_invalid_machine_type)
on main.tf line 17:
17: machine_type = "n2-standard-999"

ไม่มี rule จริงชื่อ google_compute_instance_invalid_zone อยู่ใน tflint-ruleset-google — รัน tflint --only google_compute_instance_invalid_zone กับ plugin จริง ได้ error Failed to check rule config; Rule not found: google_compute_instance_invalid_zone rule จริงที่จับค่า GCP ที่ invalid บน resource นี้คือ google_compute_instance_invalid_machine_type และเป็น Error severity จริง พิสูจน์ข้างบนกับ n2-standard-999 ที่ตั้งใจทำให้แย่

$ docker run --rm -v "$PWD:/data" bridgecrew/checkov:latest -d /data --compact --quiet
Check: CKV_GCP_28: "Ensure that Cloud Storage bucket is not anonymously or publicly accessible"
FAILED for resource: google_storage_bucket_iam_binding.public
Check: CKV_GCP_2: "Ensure Google compute firewall ingress does not allow unrestricted ssh access"
FAILED for resource: google_compute_firewall.ssh

output จริงจาก checkov (ตัดให้สั้น) CKV_GCP_28 ยืนยันว่าจริงและตรงตามที่ claim ไว้เป๊ะ ๆ fire บน resource google_storage_bucket_iam_binding ที่ grant allUsers — checkov มองที่ IAM binding ถูกต้อง ไม่ใช่ตัว bucket resource เอง เพราะ public access บน GCS มาจาก IAM ไม่ใช่ ACL flag ระดับ bucket แบบที่ AWS S3 ใช้ tool ทั้งสองตัวรันกับ raw configuration เท่านั้น ไม่ต้องมี GCP credential, ไม่ต้อง plan, ไม่ต้อง state นี่แหละคือเหตุผลที่ควรอยู่เป็น gate แรกสุดใน pipeline เพราะถูก, เร็ว, และจับ mistake ทั้ง class ได้ก่อนที่ Terraform จะคุยกับ Google Cloud ด้วยซ้ำ

static analysis หยุดอยู่แค่ที่ตัวโค้ด จึงตอบคำถามแบบ “plan นี้สร้าง instance n2-standard-4 เกินห้าตัวใน production project ไหม” ไม่ได้ เพราะตัวเลขนั้นยังไม่มีอยู่จนกว่าจะคำนวณ plan ออกมาจริง ๆ policy-as-code tool ปิดช่องว่างตรงนี้ด้วยการประเมิน machine-readable output ของ plan แทนที่จะดูที่ HCL

  • Sentinel คือ policy engine ในตัวของ HCP Terraform ผูกแน่นกับ HCP Terraform และ Terraform Enterprise run — policy ถูกผูกไว้กับ workspace หรือ organization แล้วถูกประเมินอัตโนมัติเป็นส่วนหนึ่งของทุก run ที่นั่น
  • Open Policy Agent (OPA) ที่ใช้ผ่าน conftest CLI คือทางเลือกโอเพนซอร์สที่ไม่ผูกกับ provider ไหนเป็นพิเศษ ทำงานกับ CI system ไหนก็ได้ ไม่ว่าจะใช้ HCP Terraform อยู่หรือไม่ ด้วยการประเมิน JSON document ที่คุณส่งเข้าไป

ไอเดียร่วมของทั้งสองแบบคือเหมือนกัน มีคนเขียน policy ไว้ครั้งเดียว — “ห้าม Cloud Storage bucket ไหนอนุญาต public access”, “ทุก resource ต้องมี label cost-center” — แล้วจะประเมินอัตโนมัติกับทุก plan ตั้งแต่นั้นเป็นต้นไป บล็อก plan ที่ละเมิดไม่ให้ apply ได้เลย นี่คือเวอร์ชันเชิงกลไกที่บังคับใช้เสมอของสิ่งที่ human reviewer ต้องคอยจำไปเช็คด้วยตาทุกครั้งไม่พลาดสักครั้ง

Terminal window
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
docker run --rm -v "$PWD:/project" -w /project openpolicyagent/conftest:latest test plan.json -p policy
policy/public-bucket.rego
package main
deny contains msg if {
resource := input.resource_changes[_]
resource.type == "google_storage_bucket_iam_binding"
resource.change.after.members[_] == "allUsers"
msg := sprintf("%s grants public access via allUsers", [resource.address])
}

ต่างจาก azurerm (พิสูจน์ในบทนี้ของคอร์ส Azure พี่น้องว่าต้องมี Azure session จริงแม้แค่จะคำนวณ plan) provider google plan การสร้าง resource ล้วน ๆ ได้ด้วย service-account JSON ที่ปลอม — มัน validate แค่ รูปร่าง ของ credential file ไม่ได้เช็คว่า key จริงหรือเปล่า แล้ว defer การเรียก API จริงไปตอน apply แทน นี่ทำให้พิสูจน์แบบ end-to-end จริงเต็มรูปแบบได้ในบทนี้:

$ terraform plan -input=false -out=plan.tfplan -detailed-exitcode
...
Plan: 4 to add, 0 to change, 0 to destroy.
$ echo $?
2
$ docker run --rm -v "$PWD:/project" -w /project openpolicyagent/conftest:latest test plan.json -p policy
FAIL - plan.json - main - google_storage_bucket_iam_binding.public grants public access via allUsers
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions

terraform plan จริง (service-account credential ปลอมแต่ shape ถูก — credentials = file("fakecreds/key.json") ใน provider block), terraform show -json จริง และ conftest reject จริง ทั้งหมดกับข้อมูล google_storage_bucket_iam_binding.public จริงที่มี members: ["allUsers"] syntax deny contains msg if { ... } สำคัญที่นี่เหมือนกัน: openpolicyagent/conftest:latest bundle OPA 1.20.2 default เป็น Rego v1 ต้องมี if/contains บนทุก rule — รูปแบบเก่า deny[msg] { ... } load ไม่ขึ้น:

Error: running test: load: loading policies: load: 2 errors occurred during loading:
policy/old-syntax-test.rego:3: rego_parse_error: `if` keyword is required before rule body
policy/old-syntax-test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules

conftest อ่าน plan.json ตัวเดียวกับที่คนอ่านเองด้วยมือได้ แล้วปฏิเสธ run ทันทีที่ deny สร้าง message ออกมา ไม่มี plan ที่เข้าเงื่อนไขนั้นไปถึง apply เลยสักตัว

ประเมินต้นทุน: Infracost (มาจาก docs เท่านั้น — ไม่ได้รันข้างล่างนี้)

หัวข้อที่มีชื่อว่า “ประเมินต้นทุน: Infracost (มาจาก docs เท่านั้น — ไม่ได้รันข้างล่างนี้)”

static analysis กับ policy-as-code ทั้งคู่ตอบคำถาม “ปลอดภัย/compliant ไหม” ไม่มีตัวไหนตอบ “ราคาเท่าไหร่” Infracost เติม gap นี้ และในที่นี้มาจาก docs ไม่ได้รัน — มันต้องการ pricing catalog แบบ live ที่ environment แบบ offline นี้เข้าไม่ถึง (credential ปลอมที่ shape ถูกต้องพอจะทำให้ google plan รันได้ข้างบน แต่ pricing lookup จริงของ Infracost คือ external API call แยกต่างหาก ที่ key ปลอมไม่พอจะผ่านได้) อ้างอิงจาก docs ของ Infracost เอง: “Infracost shifts cloud costs left — into your engineering workflow. It helps you write cost-aware infrastructure (Terraform, CloudFormation, AWS CDK) across AWS, Azure, and Google Cloud.” และเฉพาะสำหรับ CI: “Add Infracost to your pull requests to give your team a safety net — every PR gets an automatic comment showing the cost diff, FinOps policy violations, and tagging issues introduced by the change, so cost impact gets discussed alongside the code review.” เหมือน conftest มันอ่าน JSON ของ plan ได้โดยตรงเช่นกัน: “Infracost can also estimate costs from a Terraform plan JSON file (the output of terraform show -json), which is useful with HCP Terraform / Enterprise, Bitbucket Pipelines, and costs that only resolve at plan time.” — www.infracost.io/docs

น่าจะดูเหมือน tflint/checkov กับ Sentinel/OPA ทำงานเดียวกันซ้ำสองรอบ แต่จริง ๆ แล้วเช็คคนละอย่างกันโดยพื้นฐาน

  • static analysis เช็ค ตัวโค้ดเอง ไม่ขึ้นกับว่าตอนนี้กำลังเปลี่ยนแปลงอะไรอยู่ จึง flag Cloud Storage bucket ที่ public เหมือนกันไม่ว่า run นี้จะสร้าง bucket เดียวหรือร้อยตัว
  • policy-as-code เช็ค การเปลี่ยนแปลงที่ plan ไว้จริง ๆ และ reference ค่าที่คำนวณจริงซึ่งมีอยู่แค่ตอนที่ plan มีแล้วเท่านั้น เช่น instance count จริง, label value จริง, member list จริงที่ resolve ได้หลังจาก variable interpolation กับ module composition รันไปแล้ว กฎแบบ “บล็อก plan ไหนที่จะสร้าง instance n2-standard-4 เกินห้าตัวใน production project” ไม่มีทางเทียบเท่าได้ที่ระดับ raw-HCL เลย เพราะไม่มีทางรู้จำนวน instance ได้โดยไม่คำนวณ plan ก่อน

ทั้งสอง stage นี้อยู่ใน pipeline เดียวกัน เรียงต่อกัน static check ที่ถูกและเร็วมาก่อน จับ mistake ทั้ง class ได้ทันที policy evaluation กับ plan ที่คำนวณแล้วมาทีหลัง จับ violation เฉพาะสถานการณ์ที่โผล่มาได้แค่ตอนที่ Terraform รู้แน่ชัดแล้วว่ากำลังจะทำอะไร

flowchart LR
  hcl["HCL code"] --> lint["tflint / checkov: static checks"]
  lint -->|passes| plan["terraform plan"]
  plan --> policy["Sentinel / OPA + conftest: policy check"]
  policy -->|passes| apply["apply"]
  policy -->|violates a rule| blocked["Blocked, never applied"]
HCL ผ่าน static analysis แล้ว plan ถูกคำนวณและเช็คกับ policy ก่อนจะ apply ได้

tool ทุกตัวในบทนี้ตอบคำถามคนละข้อในจุดคนละจุดของ pipeline “HCL syntax ถูกไหม” (validate) ไม่ต้องมี context เลย “HCL ใช้ pattern ที่รู้อยู่แล้วว่าไม่ดีไหม” (tflint/checkov) ต้องมี provider rule catalog แต่ไม่ต้องมี plan “change ที่คำนวณจริงนี้ผิด policy ไหม” (Sentinel/OPA) ตอบก่อนมี plan ไม่ได้ “ราคาเท่าไหร่” (Infracost) เป็นมุมที่สี่บน plan ที่คำนวณแล้วตัวเดียวกัน บทนี้ยังเผย fact เฉพาะ provider ที่ควรจำไว้: provider google validate แค่ รูปร่าง ของ service-account credential ปลอมแล้ว proceed คำนวณ plan จริงสำหรับการสร้าง resource ล้วน ๆ ในขณะที่ azurerm (พิสูจน์ในคอร์สพี่น้อง) fail ตั้งแต่ configure provider โดยไม่มี credential เลย ไม่มีพฤติกรรมไหน “ถูกกว่า” — มันแค่เป็นเส้นทาง authentication จริงที่ต่างกัน และ pipeline ที่สมมติว่า offline-plan behavior ของ cloud หนึ่ง generalize ไปอีก cloud ได้ ก็คือการสมมติที่ยังไม่พิสูจน์

ทางเลือกได้อะไรเสียอะไร
tflint พร้อม google ruleset pluginจับ mistake เฉพาะ provider (machine type ที่ invalid) ที่ validate มองไม่เห็นต้อง --init ดึง plugin ผ่าน network และ image เปล่า ๆ แทบไม่จับอะไรที่เฉพาะ GCP เลย
rule catalog กว้าง ๆ ของ checkovรันครั้งเดียวเจอ misconfiguration จริงได้หลายข้อ (พิสูจน์ข้างบน: public IAM binding, firewall SSH ที่เปิด)finding ตกอยู่ที่ resource ที่ grant access จริง (IAM binding ไม่ใช่ bucket) — reviewer ที่คาดว่า bucket resource เองจะถูก flag อาจพลาดได้
OPA/conftest กับ show -json แทน Sentinelopen-source ใช้ได้กับ CI system ไหนก็ได้ ไม่ต้องพึ่ง HCP Terraform พิสูจน์ที่นี่ว่าเป็น run จริงแบบ end-to-end เต็มรูปแบบ (ต่างจากบท Azure พี่น้องที่หา live plan จริงแบบ offline ไม่ได้)ต้องเขียน policy เองเป็น Rego และตามให้ทันทั้ง schema ของ terraform show -json และ version ของ Rego ที่ OPA ใช้ (พิสูจน์ข้างบน: Rego v1 ทำให้ rule deny[msg] แบบเก่าพัง)
Infracost จาก docs ไม่ได้พิสูจน์ที่นี่ตอบคำถามที่ทั้ง static analysis และ policy-as-code ตอบไม่ได้ (ต้นทุน)environment นี้เข้าไม่ถึง pricing catalog แบบ live — credential ปลอมพอทำให้ google plan รันได้ข้างบน แต่ไม่พอสำหรับ pricing API call จริง
  • “tflint จับ security misconfiguration แบบเดียวกับที่ checkov จับ” พิสูจน์ข้างบน: ทั้งคู่เจอคนละอย่างบน config เดียวกัน — tflint flag machine type ที่ invalid ส่วน checkov flag IAM binding แบบ public กับ firewall rule ที่เปิด SSH เป็น rule catalog ที่เสริมกัน ไม่ใช่ทับกัน
  • “provider ของทุก cloud ใน Terraform ทำงานเหมือนกันตอน offline” พิสูจน์ข้างบนด้วยพฤติกรรมจริงสองแบบที่ต่างกันในคอร์สคู่นี้เอง: google คำนวณ plan จริงเต็มรูปแบบด้วย credential file ที่ปลอมแต่ shape ถูก ส่วน azurerm (คอร์สพี่น้อง) ปฏิเสธตั้งแต่ configure provider เลยถ้าไม่มี session จริง การสมมติว่าอันหนึ่ง generalize ไปอีกอันได้ คือ claim ที่ยังไม่พิสูจน์แบบที่กฎ “พิสูจน์ทุกอย่าง” ของคอร์สนี้มีไว้จับ
  • “rule name อย่าง google_compute_instance_invalid_zone ฟังดูสมเหตุสมผล น่าจะมีอยู่จริง” พิสูจน์ข้างบน: รัน tflint --only google_compute_instance_invalid_zone กับ plugin จริง ได้ Rule not found rule จริงสำหรับค่า invalid ของ resource นี้คือ google_compute_instance_invalid_machine_type
  • “Rego snippet ที่ copy จาก tutorial เก่า ๆ รันกับ conftest เวอร์ชันปัจจุบันได้เหมือนเดิม” พิสูจน์ข้างบน: rule deny[msg] { ... } เดียวกันเป๊ะ (ไม่มี if/contains) load ไม่ขึ้นเลยกับ OPA 1.20.2 ที่ conftest bundle มา — Rego v1 ต้องมี if/contains บนทุก rule body

เวลา review AI-generated policy/static-analysis tooling สำหรับ Terraform บน GCP ให้เช็ค:

  • .tflint.hcl ที่ไม่เคยประกาศ plugin "google" { ... } — tflint binary เปล่า ๆ แทบไม่จับอะไรที่เฉพาะ GCP เลย พิสูจน์ข้างบน (มีแค่ warning required_version ทั่วไปตอนไม่ config plugin)
  • ชื่อ tflint rule ที่เดาเอาแทนที่จะ verify จริง — พิสูจน์ข้างบน google_compute_instance_invalid_zone ไม่มีอยู่จริงใน tflint-ruleset-google rule จริงสำหรับ machine type ที่ invalid คือ google_compute_instance_invalid_machine_type
  • Rego policy ที่ใช้ deny[msg] { ... } โดยไม่มี if/contains กับ conftest/OPA image เวอร์ชันปัจจุบัน — พิสูจน์ข้างบน นี่คือ parse error จริงภายใต้ default Rego v1 ของ OPA 1.20.2
  • policy check ที่เขียนเทียบกับ resource block ของ google_storage_bucket เองแทนที่จะเป็น google_storage_bucket_iam_binding/google_storage_bucket_iam_member ที่ grant access จริง — พิสูจน์ข้างบน finding จริง CKV_GCP_28 ของ checkov ตกอยู่ที่ IAM binding เพราะ public access บน GCS คือ IAM grant ไม่ใช่ ACL flag ระดับ bucket แบบที่ S3 ใช้
  • claim ที่บอกว่า terraform plan แบบ offline “ทำงานไม่ได้เลยถ้าไม่มี cloud credential จริง” แบบเป็น fact สากล — พิสูจน์ข้างบนว่าเป็นเรื่องเฉพาะ provider: google proceed ได้ด้วย credential ปลอมแต่ shape ถูกสำหรับการสร้าง resource ล้วน ๆ ส่วน azurerm ทำไม่ได้

🐛 หา bug ในโค้ดนี้

.tflint.hcl
plugin "google" {
enabled = true
}
เฉลย

plugin block นี้ขาด version กับ source สองตัวที่ tflint --init ต้องใช้จริง ๆ ถึงจะดึงอะไรได้ พิสูจน์ข้างบน การ init จริงในบทนี้ pin ทั้งคู่ (version = "0.29.0", source = "github.com/terraform-linters/tflint-ruleset-google") — ถ้าไม่มี CI run จะ fail ตอน install plugin หรือไม่ก็ได้ version ที่ plugin manager เลือก default ให้แบบเงียบ ๆ ซึ่งไม่ reproducible ข้าม runner

tflint กับ checkov จับอะไรได้ที่ terraform validate ธรรมดาจับไม่ได้
ความต่างสำคัญระหว่างสิ่งที่ static analysis tool เช็คกับสิ่งที่ policy-as-code tool อย่าง Sentinel หรือ OPA เช็คคืออะไร
policy แบบไหนที่บังคับใช้ได้เฉพาะกับ plan ที่คำนวณแล้วเท่านั้น ไม่ใช่กับ raw HCL อย่างเดียว
Sentinel ต่างจาก Open Policy Agent คู่กับ conftest ยังไง
บทนี้พิสูจน์ fact เฉพาะ provider อะไรเกี่ยวกับพฤติกรรม terraform plan แบบ offline ระหว่าง google กับ azurerm?
ทำไม section Infracost ในบทนี้ถึงระบุชัดว่ามาจาก docs แทนที่จะพิสูจน์ด้วยการรันจริง?