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

Secrets and Credential Management

provider authentication ควรมาจาก role หรือกลไก identity แทนที่จะเป็น key ตัวจริงที่พิมพ์ลงในไฟล์ และถึงจะดึง application secret จาก secrets manager จริง ๆ ตอน apply time ก็ยังไม่ได้กันค่าไม่ให้ไปอยู่ใน Terraform state แบบเต็มร้อย

authenticate AWS provider ด้วย access_key กับ secret_key ตัวจริงที่เขียนตรง ๆ ใน provider "aws" block หรือในไฟล์ .tfvars ทำได้จริง และใช้งานได้ในความหมายแคบ ๆ คือ terraform plan รันผ่าน แต่ก็ยังผิดอยู่ดี

# Anti-pattern — never do this
provider "aws" {
region = "us-east-1"
access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

key ที่เขียนไว้ในไฟล์ .tf จะโดน commit เข้า git ทันทีที่มีคนลืมว่าอยู่ตรงนั้น แล้วค่านั้นจะอยู่ต่อไปในทุก clone และทุก commit ใน history ตั้งแต่จุดนั้นเป็นต้นไป ไม่ว่าทีหลังจะลบออกจาก revision ปัจจุบันหรือเปล่าก็ตาม ทางแก้คือไม่ให้ provider มี credential เลยใน configuration แล้วปล่อยให้ resolve จาก environment ที่รันอยู่แทน

# The provider block declares no credentials.
# Authentication comes from the environment: local env vars,
# an EC2/CI instance role, or OIDC federation in CI.
provider "aws" {
region = "us-east-1"
}

ตอนรัน local ปกติแล้วนี่หมายถึง environment variable หรือ named profile จาก aws configure ส่วนใน CI หมายถึงการ assume role แบบ OIDC-federated ที่พูดถึงในบท CI/CD — aws-actions/configure-aws-credentials เติม environment variable มาตรฐานของ AWS ให้ก่อนที่ Terraform จะรันด้วยซ้ำ เลยทำให้ provider block หา credential เจอได้โดยไม่ต้องมี key สักตัวถูกเขียนไว้ที่ไหนเลย

คำถามที่แยกออกไปจาก provider authentication คือ application secret — password ของ database, API key ของ third-party — ที่ resource ตัวหนึ่งต้องใช้เป็น argument นิยาม secret นั้นเป็น Terraform variable ทำให้ตัว variable declaration กลายเป็นสิ่งที่ต้องปฏิบัติเหมือน source of truth ซึ่งแปลว่าต้องมีคนพิมพ์ค่าจริงลงในไฟล์ tfvars, CI secret หรือตอน prompt ที่ apply time อยู่ดี

pattern ที่ดีกว่าคืออ่านค่าปัจจุบันตรงจาก secrets manager จริง ๆ ผ่าน data source ตอน apply time

data "aws_secretsmanager_secret_version" "api_key" {
secret_id = "prod/third-party/api-key"
}
resource "aws_lambda_function" "webhook_handler" {
function_name = "webhook-handler"
runtime = "nodejs20.x"
handler = "index.handler"
environment {
variables = {
API_KEY = data.aws_secretsmanager_secret_version.api_key.secret_string
}
}
}

นี่ไม่ใช่บัตรผ่านฟรี และควรพูดให้ชัดว่าทำไม module State Management เคยพูดไว้แล้วว่าการ set sensitive = true บน variable แค่ redact ออกจาก CLI output — ค่าจริงยังไปอยู่ใน terraform.tfstate เป็น plaintext อยู่ดี การอ่าน secret ผ่าน data source ก็ไม่ได้กันตรงนี้แบบเต็มร้อยเช่นกัน ผลลัพธ์ของ data source ก็ถูก cache ไว้ใน state เหมือนกัน ดังนั้น secret_string ที่ resolve ได้ก็ยังไปอยู่ใน state เหมือนกับที่ sensitive variable จะเป็น สิ่งที่ pattern แบบ data source ปรับปรุงจริง ๆ คือ lifecycle ของ secret ในที่อื่น ๆ — ค่าจริงไม่เคยถูกพิมพ์ลงใน variable เลย ไม่เคยไปนั่งอยู่ใน .tfvars ที่ใครสัก commit พลาด และ rotate ใน Secrets Manager ก็ไม่ต้องแก้ Terraform เลยสักบรรทัด — apply รอบถัดไปก็แค่อ่านค่าปัจจุบันที่มีอยู่ Secrets Manager คือ source of truth ส่วน Terraform แค่อ่านค่ามา การปกป้อง state backend เอง ที่พูดไว้ใน module State Management ยังเป็นส่วนที่ข้ามไม่ได้อยู่ดี

.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/

บางบรรทัดในนี้ควรมีเหตุผลกำกับสั้น ๆ

  • *.tfstate กับ *.tfstate.backup ห้าม commit state เด็ดขาด นอกจากปัญหาเรื่องขนาดไฟล์กับ merge conflict แล้ว state ยังมี plaintext secret แฝงอยู่ได้อย่างที่พูดไปข้างบน และซ้ำกับงานที่ remote backend ทำอยู่แล้วอย่างถูกต้อง
  • .terraform/ นี่คือ local cache ของ provider กับ module ที่ init ดาวน์โหลดมา folder นี้ใหญ่ ผูกกับเครื่องนั้น ๆ และ reproduce ได้เต็มร้อยจาก .terraform.lock.hcl เลยไม่มีอะไรใน folder นี้ที่คุ้มค่าจะ version
  • *.tfvars ให้ตัดสินเป็นเคส ๆ ไป ไม่ใช่ ignore ทุกไฟล์แบบเหมารวม บางทีมตั้งใจ commit environment.tfvars ที่ไม่ sensitive ซึ่งแค่ set instance size หรือชื่อ region — rule คือ “ไฟล์นี้มี secret หรือเปล่า” ไม่ใช่ “ไฟล์ .tfvars ทุกไฟล์คือ secret เสมอ”
  • .terragrunt-cache/ เป็น equivalent ของ .terraform/ ฝั่ง Terragrunt มีหนึ่งตัวต่อ unit regenerate ทุกครั้งที่รัน ไม่ต้อง commit เลย และถ้า commit พลาดมักทำให้ repo บวมเร็วมาก
flowchart LR
  bad["Hardcoded credential in a .tf / .tfvars file"] -->|committed by accident| leak["Leaked secret in git history"]
  sm["Secrets Manager: source of truth"] -->|data source at apply time| attr["Resource attribute"]
  attr -->|still recorded| state["terraform.tfstate"]
credential ที่ hardcode ไว้ในไฟล์ที่ commit เทียบกับ secrets manager ที่เป็น source of truth จริง อ่านตอน apply time
practice ปัจจุบันสำหรับ authenticate AWS provider ใน CI pipeline คืออะไร
ทำไมดึง secret จาก secrets manager ผ่าน data source ถึงมักดีกว่านิยามเป็น Terraform variable
nuance อะไรที่ยังใช้ได้อยู่ แม้จะเปลี่ยน secret จาก Terraform variable มาเป็น data source ของ secrets manager แล้ว
อะไรควรอยู่ใน gitignore ของ repo Terraform/Terragrunt และเพราะอะไร