Worked review 3: CI pipeline และ policy check
Feature ที่ต้องทำ
หัวข้อที่มีชื่อว่า “Feature ที่ต้องทำ”Prompt คือ: “เขียน GitHub Actions pipeline ที่รัน Terraform plan บน pull request และ apply บน merge เข้า main โดยใช้ Workload Identity Federation แทน GCP key ที่เก็บไว้ พร้อม policy check” สิ่งที่ตามมาคือ draft แรกแบบที่ AI assistant ส่งกลับมาให้กับ prompt แบบนี้พอดี — YAML ที่ดูสมเหตุสมผล, action google-github-actions/auth จริง, Rego policy ที่อ่านแล้วเหมือน OPA tutorial ทุกอัน มี bug จริง 10 ข้อ; ดู AI bug catalog สำหรับ pattern ทั่วไปเบื้องหลังหลายข้อ (entry 4, 18, 19, 20, 22) เก้าในสิบข้อนี้เป็น CI/policy mechanics เดียวกันเป๊ะที่ไม่ขึ้นกับ provider เหมือนกับ worked review 3 ของ review ทั้งสองฝั่งก่อนหน้า — พิสูจน์สดใหม่ที่นี่ ใน repo นี้ — บวกอีกหนึ่ง bug ที่เฉพาะ Workload Identity Federation ของ GCP จริงๆ บทนี้เสริม — และลิงก์ไปหา — CI/CD for Infrastructure ที่แสดง pipeline ที่แก้แล้วเต็มรูปแบบ ทุกข้อที่กล่าวถึงด้านล่างพิสูจน์ด้วย actionlint, tflint, checkov, และ conftest จริง (ผ่าน docker run --rm กับ image rhysd/actionlint, ghcr.io/terraform-linters/tflint, bridgecrew/checkov, และ openpolicyagent/conftest ที่ pin ไว้) บวก exit code จริงของ Terraform และ terragrunt run --all plan --out-dir จริง
โค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “โค้ดที่ AI สร้าง”# .github/workflows/terraform.yml, as generated -- fragment, not a collected fencename: terraformon: pull_request: push: branches: [main]
jobs: plan: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@master - uses: hashicorp/setup-terraform@v3 - name: Authenticate to GCP via Workload Identity Federation uses: google-github-actions/auth@v3 with: workload_identity_provider: projects/111122223333/locations/global/workloadIdentityPools/github-pool/providers/github-provider service_account: github-actions-terraform@acme-prod-123456.iam.gserviceaccount.com - run: terraform init - name: plan run: | echo "TF_VAR_db_password=$TF_VAR_db_password" terraform plan -detailed-exitcode -out=$PLAN_FILE env: PLAN_FILE: tfplan TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
apply: needs: plan if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@master - uses: hashicorp/setup-terraform@v3 - run: terraform init - run: terraform apply -auto-approve# ci/gitignore, as generated -- fragment, not a collected fence.terraform/.terraform.lock.hcl*.tfstate*.tfstate.backup# terraform.tfvars, as generated -- fragment, committed alongside the workflow abovedb_password = "Sup3rSecret!2024"api_token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz"# policy/deny_no_tags.rego, as generated -- fragment, not a collected fencepackage main
deny[msg] { resource := input.resource_changes[_] resource.type == "google_compute_instance" msg := sprintf("%s must set labels", [resource.address])}# .tflint.hcl, as generated -- fragment, not a collected fenceplugin "google" { enabled = true version = "0.30.0" source = "github.com/terraform-linters/tflint-ruleset-google"}เดินรีวิวทีละ bug
หัวข้อที่มีชื่อว่า “เดินรีวิวทีละ bug”Workflow: actions/checkout@master — branch ที่เปลี่ยนแปลงได้ ถูกใช้ราวกับ pin ไว้แล้ว
หัวข้อที่มีชื่อว่า “Workflow: actions/checkout@master — branch ที่เปลี่ยนแปลงได้ ถูกใช้ราวกับ pin ไว้แล้ว”อาการ — actionlint จริง, rhysd/actionlint, container แบบ one-shot:
$ docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest -color(no output -- 0 exit code)🐛 หา bug ในโค้ดนี้
- uses: actions/checkout@masterเฉลย
bug เดียวกับ AI bug catalog entry 19 และ mechanics เดียวกับ entry 1 ของ review ทั้งสองฝั่งก่อนหน้า — พิสูจน์สดใหม่ที่นี่ actionlint ต้องการแค่มี token @ref อยู่จริงตาม syntax เท่านั้น การลบมันออกไปเลย (uses: actions/checkout ไม่มี @ เลย) คือ error จริงที่มันจับได้ — ยืนยันแยกต่างหาก: specifying action "actions/checkout" in invalid format because ref is missing แต่ @master ให้ ref ที่ valid ตาม syntax actionlint เลยยอมรับเหมือนกับ commit SHA ที่ pin ไว้ วิธีแก้: uses: actions/checkout@<40-char-sha> # v5.0.0
Workflow: ปัญหา shellcheck ที่ actionlint จับได้จริง
หัวข้อที่มีชื่อว่า “Workflow: ปัญหา shellcheck ที่ actionlint จับได้จริง”อาการ — actionlint จริง กับ run: terraform plan -detailed-exitcode -out=$PLAN_FILE:
$ docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest -color.github/workflows/terraform.yml:16:9: shellcheck reported issue in this script: SC2086:info:1:40: Double quote to prevent globbing and word splitting [shellcheck] |16 | run: | | ^~~~🐛 หา bug ในโค้ดนี้
run: |terraform plan -detailed-exitcode -out=$PLAN_FILEเฉลย
actionlint ฝัง shellcheck เข้าไปกับทุก multi-line run: script $PLAN_FILE ที่ไม่ quote เจอ word-splitting และ glob expansion จาก shell ได้ วิธีแก้: -out="$PLAN_FILE"
Workflow: env: TF_VAR_db_password ถูก echo ในบรรทัด debug
หัวข้อที่มีชื่อว่า “Workflow: env: TF_VAR_db_password ถูก echo ในบรรทัด debug”อาการ — จริง env ทำซ้ำสิ่งที่ environment ของ CI runner หน้าตาเป็นเมื่อ env: ตั้งตัวแปรที่มาจาก secret:
$ export TF_VAR_db_password="SuperSecret123"$ env | grep TF_VAR_db_passwordTF_VAR_db_password=SuperSecret123🐛 หา bug ในโค้ดนี้
- name: planrun: | echo "TF_VAR_db_password=$TF_VAR_db_password" terraform plan -detailed-exitcode -out=$PLAN_FILEenv: TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}เฉลย
GitHub Actions มาสก์ ค่า secret ที่รู้จักแล้ว ใน raw log output ก็จริง แต่พิสูจน์แล้วด้านบน ข้อเท็จจริงทาง shell ที่แท้จริงคือ TF_VAR_db_password เป็น environment variable ธรรมดาสามัญทันทีที่ env: ตั้งมัน วิธีแก้: อย่า echo ตัวแปรที่มาจาก secret เลย แม้จะเพื่อ debug ก็ตาม
apply: ไม่มี plan ที่บันทึกไว้ — terraform apply -auto-approve plan ใหม่ตั้งแต่ต้น
หัวข้อที่มีชื่อว่า “apply: ไม่มี plan ที่บันทึกไว้ — terraform apply -auto-approve plan ใหม่ตั้งแต่ต้น”อาการ — จริง เทียบกันตรงๆ ก่อนอื่น apply ที่ไม่มี plan file ที่บันทึกไว้:
$ terraform apply -auto-approveterraform_data.x: Refreshing state...Plan: 1 to add, 0 to change, 0 to destroy.Apply complete! Resources: 1 added, 0 changed, 0 destroyed.แล้ว plan ที่บันทึกจริงถูก apply ผ่าน path ของไฟล์แทน:
$ terraform plan -out=tfplan$ terraform apply tfplanApply complete! Resources: 1 added, 0 changed, 0 destroyed.🐛 หา bug ในโค้ดนี้
- run: terraform init- run: terraform apply -auto-approveเฉลย
job apply ไม่เคย download plan ที่ job plan คำนวณไว้เลย พิสูจน์แล้วด้านบน: apply -auto-approve ที่ไม่มี plan file พิมพ์ Refreshing state... / Plan: ของตัวเองสดใหม่ก่อน apply การ apply plan file จริงที่บันทึกไว้แทนไม่พิมพ์บรรทัดไหนเลย วิธีแก้: terraform plan -out=tfplan เป็น build artifact ใน job plan download แล้ว apply ตรงตัว (terraform apply tfplan) ใน job apply
plan: คำนวณ -detailed-exitcode แต่ exit code สามแบบไม่เคยถูกอ่านจริง
หัวข้อที่มีชื่อว่า “plan: คำนวณ -detailed-exitcode แต่ exit code สามแบบไม่เคยถูกอ่านจริง”อาการ — จริง command เดียวกันเป๊ะ exit code จริงต่างกันในสองสถานะ:
$ terraform plan -detailed-exitcode # a resource actually needs to change$ echo $?2
$ terraform plan -detailed-exitcode # re-run immediately after, nothing left to change$ echo $?0🐛 หา bug ในโค้ดนี้
terraform plan -detailed-exitcode -out=$PLAN_FILE# (workflow never checks $? here at all)เฉลย
-detailed-exitcode มีไว้เจาะจงเพื่อให้ script แยกได้ว่า “มี change จริงพร้อมให้รีวิว” (2) กับ “เกิด error จริง” (1) กับ “ไม่มีอะไรต้องทำ” (0) — พิสูจน์แล้วด้านบน วิธีแก้: จับ $? ชัดเจน แยกตามมัน แบบที่บท CI/CD for Infrastructure ของคอร์สนี้ทำใน pipeline ของมัน
.gitignore: .terraform.lock.hcl ถูก gitignore ไปพร้อมกับ .terraform/
หัวข้อที่มีชื่อว่า “.gitignore: .terraform.lock.hcl ถูก gitignore ไปพร้อมกับ .terraform/”# .gitignore, as generated.terraform/.terraform.lock.hcl*.tfstate*.tfstate.backupbug เดียวกับ AI bug catalog entry 4 วิธีแก้:
# .gitignore, fixed.terraform/*.tfstate*.tfstate.backupterraform.tfvars: secret จริง commit ไปพร้อมกับ workflow
หัวข้อที่มีชื่อว่า “terraform.tfvars: secret จริง commit ไปพร้อมกับ workflow”# terraform.tfvars, as generateddb_password = "Sup3rSecret!2024"api_token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz"bug เดียวกับ AI bug catalog entry 22 — แย่กว่าเดิมในบริบท CI เจาะจง เพราะ .tfvars ที่ commit แล้วไปถึงทุก checkout, ทุก pull request จาก fork, และทุก CI log ที่บังเอิญ cat ไฟล์เพื่อ debug วิธีแก้: ลบไฟล์ออกจาก working tree และจาก git history; ดึง db_password/api_token จาก environment variable แบบ TF_VAR_* ที่มาจาก CI secret เท่านั้น
policy/deny_no_tags.rego: syntax Rego ก่อน v1 load ไม่ผ่าน
หัวข้อที่มีชื่อว่า “policy/deny_no_tags.rego: syntax Rego ก่อน v1 load ไม่ผ่าน”# policy/deny_no_tags.rego, as generated -- @expect-error rego_parse_error: `if` keyword is required before rule bodypackage main
deny[msg] { resource := input.resource_changes[_] resource.type == "google_compute_instance" msg := sprintf("%s must set labels", [resource.address])}อาการ — conftest test จริง, openpolicyagent/conftest เวอร์ชันปัจจุบัน, กับ terraform show -json plan จริง:
$ docker run --rm -v "$(pwd)":/project openpolicyagent/conftest test --policy policy plan.jsonError: running test: load: loading policies: load: 2 errors occurred during loading:policy/deny_no_tags.rego:3: rego_parse_error: `if` keyword is required before rule bodypolicy/deny_no_tags.rego:3: rego_parse_error: `contains` keyword is required for partial set rules🐛 หา bug ในโค้ดนี้
deny[msg] {resource := input.resource_changes[_]resource.type == "google_compute_instance"msg := sprintf("%s must set labels", [resource.address])}เฉลย
bug เดียวกับ AI bug catalog entry 20 วิธีแก้:
# policy/deny_no_tags.rego, fixedpackage main
import rego.v1
deny contains msg if { resource := input.resource_changes[_] resource.type == "google_compute_instance" msg := sprintf("%s must set labels", [resource.address])}จริง กับ plan เดียวกันเป๊ะ:
$ docker run --rm -v "$(pwd)":/project openpolicyagent/conftest test --policy policy plan.jsonFAIL - plan.json - main - google_compute_instance.app must set labels
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions.tflint.hcl/checkov: ใช้ได้จริง ทำงานจริง แต่ไม่เคยต่อเข้า CI เลย
หัวข้อที่มีชื่อว่า “.tflint.hcl/checkov: ใช้ได้จริง ทำงานจริง แต่ไม่เคยต่อเข้า CI เลย”อาการ — tflint กับ checkov จริง, container แบบ one-shot, กับ main.tf ของ pipeline เอง (google_compute_instance ที่ใช้ network แบบ default ไม่มี Shielded VM ไม่มีการบล็อก project-wide SSH key ไม่มี CSEK encryption):
$ docker run --rm -v "$(pwd)":/data -w /data ghcr.io/terraform-linters/tflint1 issue(s) found:
Warning: terraform "required_version" attribute is required (terraform_required_version)
on main.tf line 1: 1: terraform {$ docker run --rm -v "$(pwd)":/tf bridgecrew/checkov -d /tf --compact --quietPassed checks: 5, Failed checks: 3, Skipped checks: 0
Check: CKV_GCP_38: "Ensure VM disks for critical VMs are encrypted with Customer Supplied Encryption Keys (CSEK)"FAILED for resource: google_compute_instance.appCheck: CKV_GCP_32: "Ensure 'Block Project-wide SSH keys' is enabled for VM instances"FAILED for resource: google_compute_instance.appCheck: CKV_GCP_39: "Ensure Compute instances are launched with Shielded VM enabled"FAILED for resource: google_compute_instance.apprun --all plan ไม่มี --out-dir ที่ชั้น Terragrunt
หัวข้อที่มีชื่อว่า “run --all plan ไม่มี --out-dir ที่ชั้น Terragrunt”อาการ — terragrunt run --all plan --out-dir จริง plan file จริงต่อ unit สำหรับทุก unit ใน tree และ command ถัดไปที่ต้องรันพิมพ์ออกมาตรงๆ:
$ terragrunt run --all --non-interactive --no-color --out-dir /tmp/tg-plans -- plan[live/prod/app] terraform apply "/tmp/tg-plans/live/prod/app/tfplan.tfplan"
❯❯ Run Summary 4 units Succeeded 4$ find /tmp/tg-plans -maxdepth 3/tmp/tg-plans/live/dev/network/tmp/tg-plans/live/dev/app/tmp/tg-plans/live/prod/network/tmp/tg-plans/live/prod/app🐛 หา bug ในโค้ดนี้
terragrunt run --all -- plan# ... later, in a separate job/step ...terragrunt run --all -- apply -auto-approveเฉลย
bug เดียวกันเป๊ะกับ entry “ไม่มี -out” ของ plain Terraform ด้านบน แค่ขึ้นไปอีกชั้น วิธีแก้: run --all plan --out-dir <dir> ใน job plan, run --all apply --out-dir <dir> (directory เดียวกัน) ใน job apply
เงื่อนไข attribute ของ Workload Identity Federation scope กว้างเกินไป
หัวข้อที่มีชื่อว่า “เงื่อนไข attribute ของ Workload Identity Federation scope กว้างเกินไป”workflow ที่ generate มามี step google-github-actions/auth ที่มี workload_identity_provider/service_account ที่จริงและรูปทรงถูกต้อง แต่ไม่มีอะไรในโค้ดที่ generate มาของบทนี้แสดง attribute condition ของ provider เองเลย — ส่วนที่ตัดสินใจจริงว่า identity ของ GitHub ตัวไหน impersonate service account นั้นได้ Workload Identity Pool provider ที่ไม่มี attribute condition เลย หรือเช็คแค่ repository โดยไม่มี branch/ref เปิดให้ branch ไหนก็ได้ — รวมถึง pull request จาก fork — impersonate service account ผ่าน pool นี้ได้ วิธีแก้: attribute condition ที่ scope assertion.ref ไปที่ branch ที่รัน apply จริงเจาะจง แล้วใช้ service account แยกที่ scope แคบกว่า (หรือไม่ใช้เลย) สำหรับ job plan ที่รันบน pull request จาก fork ที่ไม่น่าเชื่อถือ
อะไรเปลี่ยนและทำไม
หัวข้อที่มีชื่อว่า “อะไรเปลี่ยนและทำไม”| # | Bug | วิธีแก้ |
|---|---|---|
| 1 | actions/checkout@master — ref เปลี่ยนแปลงได้ actionlint ยอมรับเหมือน SHA | Pin เป็น commit SHA เต็ม |
| 2 | -out=$PLAN_FILE ไม่ quote ใน run: script | -out="$PLAN_FILE" — actionlint/shellcheck SC2086 จริง |
| 3 | TF_VAR_db_password ที่มาจาก secret ถูก echo ในบรรทัด debug | อย่า echo เลย — env output จริงพิสูจน์ว่ามันเป็นตัวแปรธรรมดาที่อ่านได้ทันทีที่ตั้งแล้ว |
| 4 | apply -auto-approve ไม่มี plan file บันทึกไว้ | plan -out=tfplan เป็น artifact, apply tfplan ใน apply job |
| 5 | คำนวณ -detailed-exitcode แล้วไม่เคยแยก exit code 3 แบบ | จับ $? แยกตาม 0/1/2 ชัดเจน |
| 6 | .terraform.lock.hcl ถูก gitignore ไปพร้อม .terraform/ | ignore แค่ .terraform/ |
| 7 | terraform.tfvars มี secret จริง commit ไว้ | ลบทิ้ง; secret มาจาก TF_VAR_* CI secret เท่านั้น |
| 8 | Rego ก่อน v1 (deny[msg] { ... }) | import rego.v1 + deny contains msg if { ... } |
| 9 | มี config .tflint.hcl/checkov แต่ไม่มี CI step ไหนรันเลยสักตัว | เพิ่ม step tflint/checkov จริง — ทั้งสองเครื่องมือพิสูจน์แล้วว่าเจอ issue จริง |
| 10 | ไม่มี --out-dir บน terragrunt run --all plan | --out-dir <dir> ใน plan, dir เดียวกันใน apply |
| — | attribute condition ของ Workload Identity Pool provider หายไปหรือกว้างเกินไป | Scope assertion.ref ไปที่ branch ที่แน่นอน — อ้างอิงจาก docs ไม่มี GCP project ในคอร์สนี้ |
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”เก้าใน bug สิบข้อนี้เป็น CI/policy mechanics เดียวกันเป๊ะที่ไม่ขึ้นกับ provider เหมือน review ทั้งสองฝั่งก่อนหน้า — พฤติกรรม actionlint เดียวกัน ช่องว่าง -out/-detailed-exitcode เดียวกัน linter config ที่แยกขาดจาก CI แบบเดียวกัน วิธีแก้ terragrunt --out-dir เดียวกัน bug ข้อเดียวที่เฉพาะ GCP จริงๆ คือขอบเขตความไว้ใจ: AWS scope เงื่อนไข sub ของ trust policy ของ IAM role, azurerm scope subject ของ federated credential ของ App Registration, GCP scope attribute condition ของ Workload Identity Pool provider บน assertion.ref — สาม mechanism ต่างกัน แต่รูปแบบความล้มเหลวเหมือนกันเป๊ะเมื่อข้อไหนก็ตามกว้างเกินไป
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ถ้ามีไฟล์
.tflint.hcl/checkov config อยู่ใน repo แปลว่ามันถูก enforce” พิสูจน์แล้วด้านบน: ทั้งสองเครื่องมือ รันตรงกับmain.tfของ pipeline เอง เจอ issue จริงที่ CI run ซึ่งอิงแค่ว่ามี config file อยู่จะไม่เห็นเลย - “
actionlintจับ third-party action ที่ไม่ pin ได้” พิสูจน์แล้วด้านบน: มันต้องการแค่มี@refอยู่ตาม syntax แต่ไม่แยกแยะ branch name เปลี่ยนแปลงได้แบบ@masterจาก commit SHA แบบ immutable - “การมาสก์ secret ของ GitHub แปลว่า secret แบบ
TF_VAR_*รั่วผ่าน CI log ไม่ได้” พิสูจน์แล้วด้านบน: ทันทีที่ secret กลายเป็น environment variable ธรรมดา มันอ่านได้ด้วยเครื่องมือไหนก็ตามที่ dump environment ของมัน
flowchart TD Y["workflow YAML"] --> AL["actionlint"] AL -->|"1: @master accepted, ref exists"| AL AL -->|"2: unquoted $PLAN_FILE, real SC2086"| AL Y --> R["terraform plan -detailed-exitcode"] R -->|"3: TF_VAR_* echoed"| R R -->|"5: exit code 0/1/2 never branched"| R R --> A["terraform apply"] A -->|"4: no -out, re-plans from scratch"| A G[".gitignore / terraform.tfvars"] -->|"6: lockfile ignored"| G G -->|"7: secret committed"| G P["policy/*.rego"] --> CF["conftest"] CF -->|"8: pre-v1 Rego, load error"| CF L[".tflint.hcl / checkov config"] -->|"9: never wired into CI"| L TG["terragrunt run --all plan"] -->|"10: no --out-dir"| TG WIF["Workload Identity Pool provider"] -->|"attribute condition too broad -- docs"| WIF