Policy as Code and Static Analysis
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”static analysis tool จับ pattern ที่รู้อยู่แล้วว่าไม่ดีใน HCL ก่อนที่ plan จะรันด้วยซ้ำ ส่วน policy-as-code tool ประเมิน plan ที่คำนวณจริงเทียบกับ rule ขององค์กร แล้วบล็อกไม่ให้ apply ได้แบบอัตโนมัติถ้า plan นั้นผิด rule
Static analysis ก่อน plan รันด้วยซ้ำ
หัวข้อที่มีชื่อว่า “Static analysis ก่อน plan รันด้วยซ้ำ”terraform validate เช็คแค่ว่า HCL syntax ถูกต้องและ consistent กันภายใน — type ตรงกัน argument ที่ required มีครบ reference resolve ได้ แต่ไม่รู้หรอกว่า instance type t2.micro เป็นตัวเลือกที่สมเหตุสมผลไหม หรือ S3 bucket ควรเปิดให้ public read จริง ๆ หรือเปล่า tool สองประเภทนี้เข้ามาเติมช่องว่างนั้น และทั้งคู่รันเร็ว ก่อนที่จะมีการ plan หรือ apply อะไรเลย
tflintprovider-aware มาพร้อม plugin rule set (plugin ของ AWS, ของ Azure และอื่น ๆ) ที่จับ mistake ที่validateมองไม่เห็น เพราะต้องรู้ว่า AWS API จริง ๆ รับอะไรได้บ้าง เช่น instance type ที่ deprecated ไปแล้ว, ARN pattern ที่ผิด, argument ที่ provider เลิก support ไปเงียบ ๆ- security และ misconfiguration scanner ในตระกูล
checkovpattern-match ตัว HCL เองเทียบกับ configuration ที่รู้อยู่แล้วว่าไม่ดี เช่น S3 bucket ที่เปิด public read access, security group ที่เปิด0.0.0.0/0บน port ที่ sensitive, EBS volume ที่ไม่ได้ encrypttfsecเคยครอบคลุมเรื่องคล้าย ๆ กันนี้และ check เหล่านั้นถูกรวมเข้าไปใน Trivy แล้ว ส่วนcheckovยังเป็นตัวเลือกที่ใช้กันแพร่หลายและ maintain อยู่ต่อเนื่องสำหรับ layer นี้
รันจริง ทั้งสอง tool ผ่าน docker run --rm กับ config เล็ก ๆ ที่ตั้งใจทำให้แย่สามจุด (instance type รุ่นเก่า, S3 bucket แบบ public-read, security group เปิด port 22 ให้ทั้งโลก):
$ docker run --rm -v "$PWD:/data" -w /data ghcr.io/terraform-linters/tflint:latest --initInstalling "aws" plugin...Installed "aws" (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.31.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 {
Warning: "t1.micro" is previous generation instance type. (aws_instance_previous_type)
on main.tf line 22: 22: instance_type = "t1.micro"image tflint เริ่มต้นไม่มี rule เฉพาะ cloud เลยสักตัว — AWS ruleset เป็น plugin แยกต่างหาก (tflint-ruleset-aws) ที่ต้องประกาศใน .tflint.hcl แล้วดึงมาด้วย --init นี่คือเหตุผลที่ step นี้ต้องรันก่อน สังเกต rule text จริง: aws_instance_previous_type, “previous generation instance type” — ไม่ใช่ “invalid value” แบบที่ตัวอย่างที่ไม่ได้ verify อาจเดาไว้
$ docker run --rm -v "$PWD:/data" bridgecrew/checkov:latest -d /data --compact --quietCheck: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22" FAILED for resource: aws_security_group_rule.ssh File: /main.tf:38-45
Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access." FAILED for resource: aws_s3_bucket.reports File: /main.tf:25-27
Passed checks: 16, Failed checks: 17, Skipped checks: 0output จริงจาก checkov (ตัดให้เหลือแค่สอง check ที่ตัวอย่างบทนี้ตั้งใจให้ trigger จาก 17 การ fail จริงบน config สองสามตัวที่ไม่ได้ harden เลย — checkov เช็คมากกว่าที่ config เล็ก ๆ แบบนี้จะครอบคลุมได้ทั้งหมด เช่น EBS encryption, IMDSv2, detailed monitoring, S3 versioning) tool ทั้งสองรันกับ configuration ดิบ ๆ เลย ไม่ต้องมี AWS credential ไม่ต้อง plan ไม่ต้องมี state (ยกเว้น step --init ของ ghcr.io/terraform-linters/tflint ที่ต้องดาวน์โหลด ruleset plugin ผ่าน network ครั้งเดียว cache ไว้ใน working directory ที่ mount สำหรับรันครั้งต่อไป) นี่แหละคือเหตุผลที่ควรเป็น gate แรกสุดใน pipeline เพราะเร็ว ถูก และจับ mistake ทั้ง class ได้ก่อนที่ Terraform จะคุยกับ AWS ด้วยซ้ำ
Policy-as-code ตอน plan time คือ Sentinel กับ OPA พร้อม conftest
หัวข้อที่มีชื่อว่า “Policy-as-code ตอน plan time คือ Sentinel กับ OPA พร้อม conftest”static analysis หยุดอยู่แค่ที่ code จึงตอบคำถามแบบ “plan นี้สร้าง m5.xlarge instance เกินห้าตัวใน production account ไหม” ไม่ได้ เพราะตัวเลขนั้นยังไม่มีอยู่จนกว่าจะคำนวณ plan จริง policy-as-code tool ปิด gap ตรงนี้ด้วยการประเมิน machine-readable output ของ plan ไม่ใช่ตัว HCL
- Sentinel เป็น policy engine ที่ built-in มากับ HCP Terraform integrate แน่นกับ HCP Terraform และ Terraform Enterprise run — policy ผูกไว้กับ workspace หรือ organization แล้วถูกประเมินอัตโนมัติเป็นส่วนหนึ่งของทุก run ที่นั่น
- Open Policy Agent (OPA) ใช้ผ่าน CLI ชื่อ
conftestเป็น alternative แบบ open-source ที่ provider-agnostic กว่า ใช้ได้กับ CI system ไหนก็ได้ ไม่ว่าจะมี HCP Terraform หรือไม่ ด้วยการประเมิน JSON document ที่คุณส่งเข้าไป
ไอเดียหลักของทั้งสองตัวเหมือนกัน คือมีคนเขียน policy ไว้ครั้งเดียว — “security group ห้ามเปิด ingress จาก 0.0.0.0/0 บน port 22” หรือ “ทุก resource ต้องมี tag cost-center” — แล้วจะประเมินอัตโนมัติกับทุก plan ตั้งแต่นั้นมา บล็อก plan ที่ผิด rule ไม่ให้ apply ได้เลย เป็น version แบบ mechanical และบังคับใช้เสมอของสิ่งที่ 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 == "aws_security_group_rule" resource.change.after.cidr_blocks[_] == "0.0.0.0/0" resource.change.after.from_port <= 22 resource.change.after.to_port >= 22 msg := sprintf("%s allows ingress from 0.0.0.0/0 on port 22", [resource.address])}output จริงกับ plan.json ของ config สามตัวเดียวกัน:
Conftest: v0.70.1OPA: 1.20.2FAIL - plan.json - main - aws_security_group_rule.ssh allows ingress from 0.0.0.0/0 on port 22
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptionssyntax deny contains msg if { ... } สำคัญ: openpolicyagent/conftest:latest bundle OPA 1.20.2 ที่ default เป็น Rego v1 ต้องมี keyword if/contains บนทุก rule — รูปแบบเก่า deny[msg] { ... } (ไม่มี if ไม่มี contains) ที่ tutorial conftest ก่อนปี 2025 หลายที่ยังใช้กัน load ไม่ขึ้นเลย:
$ docker run --rm -v "$PWD:/project" -w /project openpolicyagent/conftest:latest test plan.json -p policyError: running test: load: loading policies: load: 2 errors occurred during loading:policy/ssh.rego:3: rego_parse_error: `if` keyword is required before rule bodypolicy/ssh.rego:3: rego_parse_error: `contains` keyword is required for partial set rulesทั้งคู่คือรันจริงกับ rule deny ชื่อเดียวกันเป๊ะ ๆ ต่างกันแค่ syntax conftest อ่าน plan.json ตัวเดียวกับที่คนอ่านเองก็ได้ แล้ว reject run ทันทีที่ deny ให้ message ออกมา — ไม่มี plan ตัวไหนที่ตรง rule นี้ไปถึง apply ได้เลย
ประเมินต้นทุน: Infracost (มาจาก docs เท่านั้น — ไม่ได้รันข้างล่างนี้)
หัวข้อที่มีชื่อว่า “ประเมินต้นทุน: Infracost (มาจาก docs เท่านั้น — ไม่ได้รันข้างล่างนี้)”static analysis กับ policy-as-code ทั้งคู่ตอบคำถาม “ปลอดภัย/compliant ไหม” ไม่มีตัวไหนตอบ “ราคาเท่าไหร่” Infracost เติม gap ตรงนี้โดยเฉพาะ และในที่นี้มาจาก docs ไม่ได้รัน — มันต้องการ pricing API/cloud pricing catalog ที่ environment แบบ offline นี้เข้าไม่ถึง อ้างอิงจาก 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 output ของ 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 รูปแบบตั้งใจให้คุ้น ๆ กัน: input terraform show -json ตัวเดียวกับที่บทนี้พิสูจน์ output จริงไปแล้ว เพียงแต่เป็นอีกมุมหนึ่ง (ต้นทุน) บน artifact เดียวกับที่ policy-as-code เช็ค compliance
สอง layer ที่เสริมกัน ไม่ได้ทำซ้ำกัน
หัวข้อที่มีชื่อว่า “สอง layer ที่เสริมกัน ไม่ได้ทำซ้ำกัน”ชวนให้คิดว่า tflint/checkov กับ Sentinel/OPA ทำงานเดียวกันซ้ำสองรอบ แต่จริง ๆ ทั้งสองเช็คคนละเรื่องกันเลย
- static analysis เช็ค ตัว code เอง ไม่ขึ้นกับว่า change จริง ๆ คืออะไร จึง flag S3 bucket ที่ public เหมือนกันหมด ไม่ว่า run นี้จะสร้าง bucket เดียวหรือร้อยตัว
- policy-as-code เช็ค change ที่ plan ไว้จริง ๆ และ reference ค่าที่คำนวณจริงได้ ซึ่งมีอยู่ก็ต่อเมื่อ plan ถูกสร้างขึ้นแล้ว เช่น instance count จริง, ค่า tag จริง, CIDR block จริงที่ resolve ได้หลังจาก variable interpolation กับ module composition รันเสร็จ rule แบบ “บล็อก plan ที่จะเหลือ instance หลัง load balancer น้อยกว่าสองตัว” ไม่มี equivalent ที่ระดับ HCL ดิบเลย เพราะไม่มีทางรู้ instance count ได้โดยไม่คำนวณ 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 และลำดับของ pipeline ก็ตามมาจากว่าคำถามนั้นตอบได้เร็วสุดเมื่อไหร่ “HCL syntax ถูกไหม” (validate) ตอบได้โดยไม่ต้องมี context เลย “HCL ใช้ pattern ที่รู้อยู่แล้วว่าไม่ดีไหม” (tflint/checkov) ต้องมี rule catalog ของ provider แต่ไม่ต้องมี plan “change ที่คำนวณจริงนี้ผิด policy ไหม” (Sentinel/OPA) ตอบไม่ได้ก่อนมี plan เพราะค่าที่กำลังเช็ค (CIDR จริง, instance count จริง) ยังไม่มีอยู่ “ราคาเท่าไหร่” (Infracost) เป็นมุมที่สี่บน plan ที่คำนวณแล้วตัวเดียวกัน รันผิดลำดับก็เสีย plan ไปกับ code ที่รู้อยู่แล้วว่าแย่ตั้งแต่แรก หรือไม่ก็ดอง static check ที่ใช้เวลาห้าวินาทีไว้จนหลัง plan ที่แพงกว่ามากรันเสร็จ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
tflint พร้อม provider ruleset plugin | จับ mistake เฉพาะ provider (type ที่ deprecated, ARN ผิด) ที่ validate มองไม่เห็น | ต้อง --init ดึง plugin ผ่าน network ต่อ provider และ image เปล่า ๆ (ไม่มี plugin) แทบไม่จับอะไรที่เฉพาะ cloud เลย |
rule catalog กว้าง ๆ ของ checkov | รันครั้งเดียวเจอ misconfiguration จริงได้หลายสิบข้อ (พิสูจน์ข้างบน: 17 check fail บน config 3 resource) | noise สูงตอนรันครั้งแรกกับ infrastructure ที่มีอยู่แล้ว — ส่วนใหญ่ต้อง baseline/suppress ก่อนจะ enforce ใน CI |
OPA/conftest กับ show -json แทน Sentinel | open-source ใช้ได้กับ CI system ไหนก็ได้ ไม่ต้องพึ่ง HCP Terraform | ต้องเขียน policy เองเป็น Rego และตามให้ทันทั้ง schema ของ terraform show -json และ version ของ Rego ที่ OPA ใช้ (พิสูจน์ข้างบน: Rego v1 ทำให้ rule deny[msg] แบบเก่าพัง) |
| Infracost จาก docs ไม่ได้พิสูจน์ที่นี่ | ตอบคำถามที่ทั้ง static analysis และ policy-as-code ตอบไม่ได้ (ต้นทุน) | environment นี้เข้าไม่ถึง cloud pricing catalog — claim ข้างบนมาจาก docs ระบุไว้ชัดเจนว่าเป็นแบบนั้น |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
tflintจับ security misconfiguration แบบเดียวกับที่checkovจับ” พิสูจน์ข้างบน: ทั้งคู่เจอคนละอย่างบน config เดียวกัน —tflintflag instance type ที่ deprecated กับrequired_versionที่หายไป ส่วนcheckovflag S3 ACL แบบ public กับ security group ที่เปิด เป็น rule catalog ที่เสริมกัน ไม่ใช่ทับกัน - “ถ้า
tflint/checkovผ่าน แปลว่า plan ปลอดภัยพอจะ apply” ทั้งคู่เห็นแค่ HCL ดิบ — พิสูจน์ข้างบน ไม่มี tool ไหนแตะ plan หรือ state เลย policy แบบ “ห้ามมีm5.xlargeเกินห้าตัวใน production” ไม่มี equivalent ที่ระดับ HCL มีแค่conftestกับshow -jsonเท่านั้นที่ประเมินแบบนี้ได้ - “Rego snippet ที่ copy จาก tutorial เก่า ๆ รันกับ
conftestเวอร์ชันปัจจุบันได้เหมือนเดิม” พิสูจน์ข้างบน: ruledeny[msg] { ... }เดียวกันเป๊ะ (ไม่มีif/contains) ที่ guide เก่าหลายที่ยังโชว์อยู่ load ไม่ขึ้นเลยกับ OPA 1.20.2 ที่conftestbundle มา — Rego v1 ต้องมีif/containsบนทุก rule body - “ต้องรัน Infracost ถึงจะรู้ว่ามันคืออะไร ทำงานยังไง” บทนี้ตั้งใจทำตรงข้าม: section Infracost ข้างบนมาจาก docs ล้วน ๆ ระบุชัดว่าไม่ได้รัน เพราะไม่มี pricing catalog ให้เข้าถึงใน environment นี้ — นี่คือวิธีที่ถูกต้องในการเขียนถึง tool ที่พิสูจน์ในเครื่องไม่ได้ ไม่ใช่แต่งค่า output ขึ้นมาเอง
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลา review AI-generated policy/static-analysis tooling สำหรับ Terraform ให้เช็ค:
.tflint.hclที่ไม่เคยประกาศ cloud provider plugin เลย (plugin "aws" { ... }) —tflintbinary เปล่า ๆ แทบไม่จับอะไรที่เฉพาะ cloud เลย พิสูจน์ข้างบน (มีแค่ warningrequired_versionทั่วไปตอนไม่ config plugin)- Rego policy ที่ใช้
deny[msg] { ... }โดยไม่มีif/containsกับconftest/OPA image เวอร์ชันปัจจุบัน — พิสูจน์ข้างบน นี่คือ parse error จริงภายใต้ default Rego v1 ของ OPA 1.20.2 ไม่ใช่แค่ความกังวลเรื่อง version-skew ทางทฤษฎี - CI step ที่รัน
checkov/tflintแต่ไม่เคยเช็ค exit code (continue-on-error: trueแล้วไม่มีอะไรอ่านผลต่อ) — scan รันจริง เจอ issue จริง แต่ไม่บล็อกอะไรเลย - policy-as-code rule ที่เขียนเทียบกับ
terraform.tfstateที่ parse เอง แทนที่จะเป็นresource_changes[].change.actions/.afterของterraform show -json— state อธิบายสิ่งที่ เป็นอยู่ ไม่ใช่สิ่งที่ plan ที่รอ apply กำลังจะทำ เลย check ที่อิง state บล็อก plan แย่ ๆ ก่อน apply ไม่ได้ - Rego policy ไฟล์เดียวใหญ่ ๆ ที่ปน security rule (“ห้ามเปิด SSH”) กับ cost/tagging rule ไว้ด้วยกัน — test และคิดตามยากกว่าแยก policy package ต่อเรื่อง และ skip เฉพาะบางประเภทตอนต้องการ exception ก็ยากกว่า
🐛 หา bug ในโค้ดนี้
plugin "aws" {enabled = true}เฉลย
plugin block นี้ขาด argument สองตัวที่ tflint --init ต้องใช้จริง ๆ ถึงจะดึงอะไรได้: version กับ source พิสูจน์ข้างบน tflint --init กับ config ที่ไม่มี version/source จะ fail ตรง ๆ หรือไม่ก็ resolve ไปที่ default ของ plugin manager แบบเงียบ ๆ ซึ่งไม่แน่นอน — การรันจริงในบทนี้ pin ทั้งคู่ (version = "0.31.0", source = "github.com/terraform-linters/tflint-ruleset-aws") ซึ่งเป็นสิ่งที่ทำให้ CI run reproducible แทนที่จะได้ ruleset version ต่างกันทุกครั้งที่ runner ใหม่