Secrets and Credential Management
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”provider authentication ควรมาจาก Application Default Credentials หรือกลไก identity แทนที่ key ที่ดาวน์โหลดมาแล้วพิมพ์ใส่ไฟล์ และแม้แต่การดึง application secret จาก secrets manager จริงตอน apply time ก็ยังไม่ได้กันค่าไม่ให้อยู่ใน Terraform state ได้เต็มร้อย
อย่า hardcode credential ในบล็อก provider
หัวข้อที่มีชื่อว่า “อย่า hardcode credential ในบล็อก provider”เป็นไปได้ที่จะ authenticate google provider ด้วย service account key JSON file ที่ดาวน์โหลดมา แล้ว reference ตรง ๆ จากบล็อก provider "google" และใช้งานได้ในความหมายแคบ ๆ ว่า terraform plan รันผ่าน แต่ก็ยังคงผิดอยู่ดี
# Anti-pattern — never do thisprovider "google" { project = "acme-prod-123456" region = "us-central1" credentials = file("service-account-key.json")}key file ที่ reference แบบนี้จะถูก commit เข้า git ทันทีที่มีคนลืมว่าอยู่ตรงนั้น แล้วไฟล์นั้นจะยังอยู่ต่อไปในทุก clone และทุก commit history ตั้งแต่จุดนั้นเป็นต้นไป ไม่ว่าจะถูกลบออกจาก revision ปัจจุบันทีหลังหรือไม่ก็ตาม ทางแก้คือไม่ให้ provider มี credential เลยใน configuration แล้วปล่อยให้ resolve เอาจาก environment ที่รันอยู่แทน
# The provider block declares no credentials.# Authentication comes from Application Default Credentials:# a local gcloud auth, an attached GCE/CI service account, or# Workload Identity Federation in CI.provider "google" { project = "acme-prod-123456" region = "us-central1"}ในเครื่อง local ปกติแปลว่า Application Default Credentials ที่ตั้งค่าไว้ผ่าน gcloud auth application-default login บน GCE instance หรือ managed compute ส่วนใหญ่แปลว่า service account ที่ attach ไว้กับ runtime อยู่แล้ว ถูก discover อัตโนมัติ ใน CI แปลว่า Workload Identity Federation ที่ครอบคลุมไว้ในบทเรียน CI/CD — google-github-actions/auth เติม Application Default Credentials ให้ก่อนที่ Terraform จะรันด้วยซ้ำ provider block เลยเจอ credential ได้โดยไม่มี key file ให้เขียนไว้ที่ไหนเลย
ดึง secret เข้ามาผ่าน data source ไม่ใช่ variable
หัวข้อที่มีชื่อว่า “ดึง secret เข้ามาผ่าน data source ไม่ใช่ variable”คำถามอีกข้อที่แยกจาก provider authentication คือ application secret เช่น database password, third-party API key ที่ resource ต้องใช้เป็น argument การกำหนด secret นั้นเป็น Terraform variable ทำให้ variable declaration กลายเป็นสิ่งที่ต้องถือว่าเป็น source of truth ซึ่งแปลว่ายังต้องมีคนพิมพ์ค่าจริงใส่ไฟล์ tfvars, CI secret หรือ prompt ตอน apply อยู่ดี
pattern ที่ดีกว่าคืออ่านค่าปัจจุบันตรง ๆ จาก secrets manager จริงผ่าน data source ตอน apply time
data "google_secret_manager_secret_version" "api_key" { secret = "third-party-api-key"}
resource "google_cloud_run_v2_service" "webhook_handler" { name = "webhook-handler" location = "us-central1"
template { containers { image = "us-central1-docker.pkg.dev/acme-prod-123456/app/webhook-handler:latest"
env { name = "API_KEY" value = data.google_secret_manager_secret_version.api_key.secret_data } } }}นี่ไม่ใช่ทางออกฟรี ๆ ที่ไม่มีข้อแม้ และควรพูดให้ชัดว่าทำไม module State Management ได้ครอบคลุมไปแล้วว่าตั้ง sensitive = true บน variable ทำได้แค่ redact ออกจาก CLI output ค่าจริงยังลงไปอยู่ใน terraform.tfstate แบบ plaintext อยู่ดี การอ่าน secret ผ่าน data source ก็ไม่ได้หลีกเลี่ยงสิ่งนี้ได้เต็มที่เหมือนกัน ผลลัพธ์ของ data source ก็ถูก cache ไว้ใน state เหมือนกัน ค่า secret_data ที่ resolve แล้วเลยยังจบลงไปอยู่ใน state เหมือนกับ sensitive variable สิ่งที่ pattern แบบ data source ปรับปรุงจริง ๆ คือ lifecycle ของ secret ในส่วนอื่นทั้งหมด ค่าจริงไม่ต้องถูกพิมพ์ใส่ variable เลย ไม่ต้องอยู่ในไฟล์ .tfvars ที่ใครสักคนอาจ commit พลาดไป และการหมุนเวียนใน Secret Manager ไม่ต้องแก้ Terraform เลยสักบรรทัด apply ครั้งถัดไปก็แค่อ่านค่าที่เป็นปัจจุบันเข้ามา Secret Manager คือ source of truth Terraform แค่อ่านค่ามา การป้องกัน state backend ที่ครอบคลุมไว้ใน module State Management ยังคงเป็นส่วนที่ข้ามไม่ได้อยู่ดี
checklist สำหรับ .gitignore ของ repo Terraform/Terragrunt
หัวข้อที่มีชื่อว่า “checklist สำหรับ .gitignore ของ repo Terraform/Terragrunt”.gitignore ที่สั้นและตั้งใจเขียนไว้จะกันไม่ให้อุบัติเหตุข้างบนส่วนใหญ่ไปถึง git ได้ตั้งแต่แรก
# Terraform / Terragrunt .gitignore
# Never commit state — it can contain plaintext secrets and is the# canonical source of truth; committing it invites drift and leaks.*.tfstate*.tfstate.backup
# Local provider and module cache, regenerated by `terraform init`..terraform/
# May contain sensitive values depending on the file — judge case by# case rather than blanket-ignoring every *.tfvars in the repo.*.tfvars
# Terragrunt's local working-directory cache, regenerated automatically..terragrunt-cache/
# Downloaded service account key JSON files — should not exist in this# repo at all if Workload Identity Federation and ADC are used correctly.*-key.json*-sa.jsonบางตัวในนี้ควรมีเหตุผลสั้น ๆ กำกับไว้ทีละตัว
*.tfstateกับ*.tfstate.backup— ห้าม commit state เด็ดขาด นอกจากปัญหาเรื่องขนาดไฟล์กับ merge conflict แล้ว state ยังมี secret แบบ plaintext ได้ตามที่ครอบคลุมไปข้างบน และซ้ำกับงานที่ remote backend ทำอยู่แล้วอย่างถูกต้อง.terraform/— นี่คือ local cache ของ provider กับ module ที่ดาวน์โหลดมาโดยinitfolder นี้ใหญ่, เฉพาะเครื่อง, และ reproduce ได้เต็มที่จาก.terraform.lock.hclเลยไม่มีอะไรที่ควร version ตรงนี้เลย*.tfvars— พิจารณาเป็นเคส ๆ ไปแทนที่จะ ignore ทั้งหมด บางทีมตั้งใจ commitenvironment.tfvarsที่ไม่ sensitive ซึ่งแค่กำหนด machine type หรือชื่อ region กฎคือ “ไฟล์นี้มี secret จริงไหม” ไม่ใช่ “ไฟล์.tfvarsทุกไฟล์คือ secret เสมอ”.terragrunt-cache/— เทียบเท่ากับ.terraform/ของ Terragrunt หนึ่งอันต่อ unit regenerate ทุกครั้งที่รัน ไม่ต้อง commit เลย และถ้า commit ไปโดยไม่ตั้งใจมักทำให้ repo บวมเร็วมาก- service account key JSON file ที่ดาวน์โหลดมา — repo ที่ทำตามคำแนะนำในบทก่อนหน้าไม่ควรมีไฟล์แบบนี้อยู่เลยตั้งแต่แรก แต่กฎแบบ name-pattern ก็เป็นประกันราคาถูกกันไว้เผื่อมีคนดาวน์โหลดมาทดสอบ local เร็ว ๆ แล้วลืมลบทิ้ง
flowchart LR bad["Downloaded key in a .tf / .tfvars file"] -->|committed by accident| leak["Leaked credential in git history"] sm["Secret Manager: source of truth"] -->|data source at apply time| attr["Resource attribute"] attr -->|still recorded| state["terraform.tfstate"]