Policy as Code and Static Analysis
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”static analysis tool จับ pattern ที่รู้อยู่แล้วว่าแย่ใน HCL ก่อนที่ plan จะรันด้วยซ้ำ ส่วน policy-as-code tool ประเมิน plan ที่คำนวณออกมาจริงเทียบกับกฎขององค์กร แล้วบล็อกไม่ให้ apply ได้แบบอัตโนมัติถ้าละเมิดกฎ
Static analysis ก่อน plan จะรันด้วยซ้ำ
หัวข้อที่มีชื่อว่า “Static analysis ก่อน plan จะรันด้วยซ้ำ”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 ที่ sensitivetfsecเคยครอบคลุมเรื่องคล้าย ๆ กัน แล้ว check เหล่านั้นก็ถูกรวมเข้าไปใน Trivy ตั้งแต่นั้นมา ส่วนcheckovยังเป็นตัวเลือกที่ใช้กันแพร่หลายและ maintain อยู่ต่อเนื่องสำหรับ layer นี้
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 --initInstalling "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:latest2 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 --quietCheck: 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.sshoutput จริงจาก 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 ด้วยซ้ำ
Policy-as-code ตอน plan time: Sentinel กับ OPA คู่กับ conftest
หัวข้อที่มีชื่อว่า “Policy-as-code ตอน plan time: Sentinel กับ OPA คู่กับ conftest”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) ที่ใช้ผ่าน
conftestCLI คือทางเลือกโอเพนซอร์สที่ไม่ผูกกับ provider ไหนเป็นพิเศษ ทำงานกับ CI system ไหนก็ได้ ไม่ว่าจะใช้ HCP Terraform อยู่หรือไม่ ด้วยการประเมิน JSON document ที่คุณส่งเข้าไป
ไอเดียร่วมของทั้งสองแบบคือเหมือนกัน มีคนเขียน policy ไว้ครั้งเดียว — “ห้าม Cloud Storage bucket ไหนอนุญาต public access”, “ทุก resource ต้องมี label cost-center” — แล้วจะประเมินอัตโนมัติกับทุก plan ตั้งแต่นั้นเป็นต้นไป บล็อก plan ที่ละเมิดไม่ให้ apply ได้เลย นี่คือเวอร์ชันเชิงกลไกที่บังคับใช้เสมอของสิ่งที่ human reviewer ต้องคอยจำไปเช็คด้วยตาทุกครั้งไม่พลาดสักครั้ง
terraform plan -out=plan.tfplanterraform show -json plan.tfplan > plan.jsondocker run --rm -v "$PWD:/project" -w /project openpolicyagent/conftest:latest test plan.json -p policypackage 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 policyFAIL - plan.json - main - google_storage_bucket_iam_binding.public grants public access via allUsers
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptionsterraform 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 bodypolicy/old-syntax-test.rego:3: rego_parse_error: `contains` keyword is required for partial set rulesconftest อ่าน 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"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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 แทน Sentinel | open-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 เดียวกัน —tflintflag machine type ที่ invalid ส่วนcheckovflag 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 foundrule จริงสำหรับค่า invalid ของ resource นี้คือgoogle_compute_instance_invalid_machine_type - “Rego snippet ที่ copy จาก tutorial เก่า ๆ รันกับ
conftestเวอร์ชันปัจจุบันได้เหมือนเดิม” พิสูจน์ข้างบน: ruledeny[msg] { ... }เดียวกันเป๊ะ (ไม่มีif/contains) load ไม่ขึ้นเลยกับ OPA 1.20.2 ที่conftestbundle มา — Rego v1 ต้องมีif/containsบนทุก rule body
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลา review AI-generated policy/static-analysis tooling สำหรับ Terraform บน GCP ให้เช็ค:
.tflint.hclที่ไม่เคยประกาศplugin "google" { ... }—tflintbinary เปล่า ๆ แทบไม่จับอะไรที่เฉพาะ GCP เลย พิสูจน์ข้างบน (มีแค่ warningrequired_versionทั่วไปตอนไม่ config plugin)- ชื่อ tflint rule ที่เดาเอาแทนที่จะ verify จริง — พิสูจน์ข้างบน
google_compute_instance_invalid_zoneไม่มีอยู่จริงในtflint-ruleset-googlerule จริงสำหรับ 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:googleproceed ได้ด้วย credential ปลอมแต่ shape ถูกสำหรับการสร้าง resource ล้วน ๆ ส่วนazurermทำไม่ได้
🐛 หา bug ในโค้ดนี้
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