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

Secrets and Credential Management

Azure credential และ application secret ไม่ควร hardcode ลงใน .tf file หรือ .tfvars file ที่ commit เข้า git เลย provider authentication ควรมาจาก Azure CLI session local, managed identity, หรือ OIDC ส่วน application secret ควร pull จาก secrets manager จริงตอน apply แทนการพิมพ์ลงใน Terraform variable

provider "azurerm" block รับ argument client_id, client_secret, tenant_id, และ subscription_id ได้ตรง ๆ และถูกกฎในแง่ HCL syntax ถ้าจะพิมพ์ client secret จริงลงไปในนั้น แต่นี่คือ mistake เดียวกับที่ course นี้เตือนซ้ำ ๆ มาตลอด ค่าที่อยู่ใน .tf file คือค่าที่ห่างจาก git add . โดยไม่ตั้งใจแค่ครั้งเดียวจากการไปอยู่ใน public GitHub history ตลอดไป ไม่ว่าใครจะ review pull request หลังจากนั้นละเอียดแค่ไหนก็ตาม

# do not do this — a real client secret should never appear in a .tf file
provider "azurerm" {
features {}
client_id = "11111111-2222-3333-4444-555555555555"
client_secret = "this-is-a-real-secret-and-now-it-is-in-git-forever"
tenant_id = "66666666-7777-8888-9999-000000000000"
subscription_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}

วิธี authenticate azurerm provider ที่ถูกต้องทุกแบบ หลีกเลี่ยงการพิมพ์ secret ลงใน config เลยทั้งหมด

  • local development — engineer รัน az login ครั้งเดียว แล้ว azurerm provider จะดึง Azure CLI session นั้นมาใช้เองอัตโนมัติ ไม่ต้องมีอะไรใน provider block เกินกว่า features {}
  • managed identity — system- หรือ user-assigned managed identity ที่ attach ไว้กับ compute ที่รัน Terraform (Azure VM, Azure Container Instance, AKS pod) ให้ provider authenticate เป็น identity นั้นได้โดยไม่มี credential material อยู่ที่ไหนเลย เพราะ Azure เองเป็นคน broker token ให้
  • OIDC ผ่าน federated credential — ที่พูดถึงใน lesson CI/CD แล้ว azure/login แลก identity token ของ GitHub Actions แบบ short-lived ที่ sign มาแล้ว เป็น Azure credential ชั่วคราวผ่าน federated credential บน Azure AD App Registration
# provider block for all three approaches — identical HCL, different actual auth mechanism
provider "azurerm" {
features {}
}

สังเกตว่า provider block เองแทบไม่เปลี่ยนเลยระหว่าง local development, managed identity, และ CI ความต่างอยู่ที่ identity ไหนมีอยู่ใน environment ที่ Terraform รันอยู่ ล้วน ๆ ไม่ใช่ที่ argument client_secret ที่อยู่ในไฟล์

application secret เป็นปัญหาคนละแบบกับ provider authentication เช่น database administrator password, API key ที่ resource ต้องใช้ตอนสร้าง, private key ของ TLS certificate module State Management ได้พูดไปแล้วว่าการตั้ง sensitive = true บน Terraform variable แค่ redact ค่านั้นจาก CLI output เท่านั้น ค่ายังไปอยู่ใน terraform.tfstate แบบ plaintext เต็ม ๆ อยู่ดี เมื่อเป็นแบบนั้น การ define secret เป็น variable เลยแปลว่ายังต้องมีคนหาทางเอาค่าจริงเข้า Terraform ให้ได้อยู่ดี ปกติก็คือ .tfvars file ที่เป็นไฟล์ประเภทที่ commit เข้าไปโดยไม่ตั้งใจได้ง่ายที่สุด

pattern ที่ดีกว่าคือ pull secret เข้ามาจาก secrets manager จริงผ่าน data source ตอน apply ทำให้ค่าจริงของ secret ไม่ต้องถูกพิมพ์ลงใน .tf หรือ .tfvars file เลยตั้งแต่แรก

data "azurerm_key_vault" "main" {
name = "app-keyvault"
resource_group_name = "app-shared-rg"
}
data "azurerm_key_vault_secret" "db_password" {
name = "app-db-password"
key_vault_id = data.azurerm_key_vault.main.id
}
resource "azurerm_postgresql_flexible_server" "main" {
name = "app-db"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "15"
sku_name = "GP_Standard_D2s_v3"
administrator_login = "appadmin"
administrator_password = data.azurerm_key_vault_secret.db_password.value
}

ต้องชัดเจนว่าวิธีนี้แก้อะไรได้และแก้อะไรไม่ได้ นี่ไม่ใช่วิธีกัน secret ออกจาก state ได้ทั้งหมด ผลของ data source ก็ถูก cache ไว้ใน state เหมือน attribute ของ resource เป๊ะ ๆ ดังนั้น data.azurerm_key_vault_secret.db_password.value กับ administrator_password ที่ป้อนให้ ก็จบลงใน terraform.tfstate แบบ plain เหมือน variable ที่ตั้ง sensitive = true อยู่ดี การป้องกัน state backend ด้วย Azure RBAC ตามที่ module State Management พูดไว้ ยังคงเป็นส่วนที่ non-negotiable ไม่ว่าจะเลือกวิธีไหน

สิ่งที่ data source แก้ได้จริง ๆ คือ secret ค่าจริงมาจากไหนและ rotate ยังไง ถ้าใช้ variable ค่าต้องมีคนพิมพ์ไว้ที่ไหนสักที่ก่อนที่ Terraform จะรันเสมอ ไม่ว่าจะเป็น .tfvars file, CI secret, หรือค่าที่ paste เข้า prompt ถ้าใช้ data source อ่านจาก Key Vault ค่าจะถูกตั้งและ rotate อยู่ที่เดียวเท่านั้นคือใน Key Vault เอง แล้วทุก terraform plan หรือ apply ก็ดึงค่าปัจจุบันมาใช้เองอัตโนมัติ ไม่มีใครต้องแตะ Terraform configuration เพื่อ rotate เลย

Terraform 1.10 กับ 1.11 ปิดช่องที่หัวข้อที่แล้วยอมรับว่าแก้ไม่ได้ไปเรียบร้อย ephemeral resource (1.10) จะถูกอ่านใหม่ทุกครั้งในแต่ละ phase ของ operation และไม่เคยถูกเขียนลง state หรือ plan file เลย ส่วน write-only argument (1.11) ทำให้ resource รับค่าที่ provider ใช้ครั้งเดียวแล้วทิ้ง แทนที่จะเก็บไว้เป็น attribute

write-only argument สังเกตได้จาก suffix _wo และแต่ละตัวจะมาคู่กับ argument _wo_version เสมอ เพราะ Terraform ไม่เคยเห็นค่าจริง มันเลยไม่มีทางรู้ว่าค่าเปลี่ยนไปแล้ว การเพิ่มเลข version คือสิ่งที่บอกให้มันส่งค่าใหม่ไปอีกครั้ง

data "azurerm_key_vault" "main" {
name = "app-keyvault"
resource_group_name = "app-shared-rg"
}
# read fresh on every operation, never persisted to state or the plan
ephemeral "azurerm_key_vault_secret" "db_password" {
name = "app-db-password"
key_vault_id = data.azurerm_key_vault.main.id
}
resource "azurerm_postgresql_flexible_server" "main" {
name = "app-db"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "15"
sku_name = "GP_Standard_D2s_v3"
administrator_login = "appadmin"
administrator_password_wo = ephemeral.azurerm_key_vault_secret.db_password.value
administrator_password_wo_version = 1
}

ลองเทียบกับเวอร์ชัน data source ข้างบนทีละบรรทัด data.azurerm_key_vault_secret cache secret ไว้ใน state แต่ ephemeral.azurerm_key_vault_secret ไม่ทำ administrator_password ถูกบันทึกเป็น resource attribute แต่ administrator_password_wo ไม่ถูกบันทึก การ rotate secret ใน Key Vault แล้วเพิ่มค่า administrator_password_wo_version จะ push ค่าใหม่ไปที่ Azure โดยที่ไม่มีค่าไหนตกลงไปอยู่ใน terraform.tfstate เลยสักค่า

ทั้งสองส่วนเป็นแบบ opt-in รายตัว resource ฉะนั้นเช็ค provider documentation ก่อนเสมอ อย่าเพิ่งเดาว่ามี เพราะ provider ต้องทำทั้ง ephemeral resource และ write-only argument ออกมาให้ ตอนนี้บน azurerm ตัว azurerm_key_vault_secret รับ value_wo กับ value_wo_version สำหรับเขียน secret ด้วยวิธีเดียวกัน ส่วน resource ไหนที่ยังไม่ได้ทำ pattern แบบ data source ข้างบนก็ยังเป็นทางสำรองอยู่ และการป้องกัน state backend ด้วย Azure RBAC ก็ยังเป็นเรื่องที่ต้องทำอยู่ดีไม่ว่าจะเลือกวิธีไหน

พิสูจน์ว่า write-only argument ทั้งสองตัวมีอยู่จริง เทียบกับ schema จริงของ azurerm 5.7.0

หัวข้อที่มีชื่อว่า “พิสูจน์ว่า write-only argument ทั้งสองตัวมีอยู่จริง เทียบกับ schema จริงของ azurerm 5.7.0”

administrator_password_wo/administrator_password_wo_version บน azurerm_postgresql_flexible_server และ value_wo/value_wo_version บน azurerm_key_vault_secret มีอยู่จริงทั้งคู่ — พิสูจน์ด้วย terraform validate จริงกับ azurerm 5.7.0:

variables.tf
variable "db_password" {
type = string
ephemeral = true
sensitive = true
}
variable "api_key_value" {
type = string
ephemeral = true
sensitive = true
}
# terraform.tfvars — harness fixture only, never committed in a real repo (see the gitignore checklist below)
db_password = "mock-secret-value"
api_key_value = "mock-api-key-value"
main.tf
resource "azurerm_resource_group" "main" {
name = "app-rg"
location = "West Europe"
}
resource "azurerm_key_vault" "main" {
name = "app-keyvault"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tenant_id = "66666666-7777-8888-9999-000000000000"
sku_name = "standard"
rbac_authorization_enabled = true
}
resource "azurerm_key_vault_secret" "api_key" {
name = "app-api-key"
key_vault_id = azurerm_key_vault.main.id
value_wo = var.api_key_value
value_wo_version = 1
}
resource "azurerm_postgresql_flexible_server" "main" {
name = "app-db"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "15"
sku_name = "GP_Standard_D2s_v3"
administrator_login = "appadmin"
administrator_password_wo = var.db_password
administrator_password_wo_version = 1
}
$ terraform validate
Success! The configuration is valid.

terraform validate กับ config นี้ยังเจอ schema change จริงที่ไม่เกี่ยวข้องกันเลยของ azurerm 5.x ที่ควรพูดถึง: azurerm_key_vault ตอนนี้ต้องมี rbac_authorization_enabled — ถ้าไม่ใส่จะ fail ด้วย The argument "rbac_authorization_enabled" is required, but no definition was found เป็น breaking-change detail จริงจาก azurerm 4→5 major version ไม่ใช่สิ่งที่เดาเอา

terraform plan จริงกับ azurerm resource จริงต้องมี Azure session จริง — พิสูจน์ไปแล้วสองครั้งในคอร์สนี้ (ci-cd-for-infrastructure.mdx: could not configure AzureCli Authorizer ตอนไม่ config auth เลย; policy-as-code-and-static-analysis.mdx: AADSTS90002 tenant-not-found จริงตอนใส่ service-principal ปลอม) error เดียวกันเกิดซ้ำที่นี่ ยืนยันว่า provider ของ azurerm authenticate ตอน Configure เสมอไม่ว่าจะ plan resource ไหน — ต่างจาก google (พิสูจน์ในคอร์ส GCP พี่น้องว่าคำนวณ plan แบบ offline เต็มรูปแบบได้ด้วย credential ปลอมแต่ shape ถูก) เพราะแบบนี้ คอร์สนี้เลยจับคู่ schema proof ข้างบน (validate จริงกับ azurerm 5.7.0 ยืนยันว่า _wo argument ทั้งสองมีอยู่จริง) เข้ากับ proof terraform show -json ของบท secrets ฝั่ง AWS (change.after.password_wo = null, after_sensitive.password_wo = true) แทนที่จะพยายามทำ plan-JSON proof แยกต่างหากสำหรับ azurerm ที่ environment นี้ทำไม่ได้จริง — กลไก write-only เองเป็นของ Terraform-core และไม่ผูกกับ provider (document ไว้เหมือนกันทุก provider ที่ implement มัน) เลย schema proof บวกกับ plan-JSON proof ข้าม provider หนึ่งครั้งคือเพดานที่ซื่อสัตย์ที่สุดที่พิสูจน์ได้ที่นี่

Key Vault มักอยู่คนละ subscription ที่คุมเข้มกว่าจาก resource ที่อ่านมัน — เป็น subscription “security” ที่แชร์กัน ดูแลโดยทีมอื่น การอ่าน secret จากมันต้องมี azurerm provider ตัวที่สอง ที่ alias ไว้ ชี้ไปที่ subscription นั้น และ Key Vault data source หรือ ephemeral resource ทุกตัวต้อง reference มันตรง ๆ ด้วย provider = azurerm.<alias>:

provider "azurerm" {
features {}
subscription_id = "00000000-0000-0000-0000-000000000000" # app subscription
}
provider "azurerm" {
alias = "keyvault"
features {}
subscription_id = "11111111-1111-1111-1111-111111111111" # shared security subscription
}
data "azurerm_key_vault" "shared" {
provider = azurerm.keyvault
name = "shared-keyvault"
resource_group_name = "shared-security-rg"
}
ephemeral "azurerm_key_vault_secret" "db_password" {
provider = azurerm.keyvault
name = "app-db-password"
key_vault_id = data.azurerm_key_vault.shared.id
}

terraform validate จริงกับ azurerm 5.7.0 ยืนยันว่า compile ผ่าน รวมถึง ephemeral block ที่ reference provider ที่ alias ไว้ — provider = azurerm.keyvault ใช้ได้เหมือนกันทั้งบน data, resource, และ ephemeral block เพราะ provider aliasing เป็นกลไกของ Terraform-core ที่ไม่เปลี่ยนไปตามประเภท resource/data/ephemeral ถ้าไม่มี alias Terraform จะพยายามอ่าน Key Vault ผ่าน subscription ของ provider default แทน แล้วได้ error “not found” จริง ๆ ทันทีที่ Key Vault อยู่ที่อื่นจริง ๆ — alias ตรงนี้ไม่ใช่ความสะดวกที่เลือกได้ มันคือสิ่งที่ทำให้อ่านถูก subscription ตั้งแต่แรก

ไม่ว่าจะ handle secret ใน config ได้ระมัดระวังแค่ไหน Terraform หรือ Terragrunt repository ก็มีไฟล์อยู่หลายประเภทที่ไม่ควรเข้า git ตั้งแต่แรกเลย เพราะแต่ละอย่างอาจมี resource attribute value, configuration ที่ดูเหมือน sensitive, หรือ provider credential ใน local filesystem ได้

Terminal window
# .gitignore — Terraform / Terragrunt
# state files can contain plaintext secrets and full resource attributes
*.tfstate
*.tfstate.backup
# provider binaries and working files downloaded by terraform init
.terraform/
# variable files often carry real values for local runs — audit case by case,
# a checked-in *.tfvars with only non-sensitive defaults can be fine
*.tfvars
# Terragrunt's per-unit cache of generated and downloaded Terraform files
.terragrunt-cache/

*.tfstate กับ *.tfstate.backup สำคัญด้วยเหตุผลเดียวกับที่ module State Management พูดวนกลับมาตลอด state คือ record แบบ plaintext ของทุก attribute ที่ Terraform เคยตั้งค่า ไม่ว่าจะ sensitive หรือไม่ .terraform/ เป็น output ที่ derive มาล้วน ๆ จาก init มีทั้ง provider, module, และ lock metadata ไม่มีอะไรควร commit และทุกอย่าง regenerate ใหม่ได้ใน CI *.tfvars เป็นเรื่องที่ต้องใช้วิจารณญาณมากกว่าจะ block อัตโนมัติ dev.tfvars ที่มีแค่ instance size กับชื่อ region ไม่มีปัญหาถ้า commit แต่ .tfvars ตัวไหนก็ตามที่มี credential จริงหรือ connection string ต้องไม่เข้า git เด็ดขาด ทีมส่วนใหญ่เลย exclude pattern นี้ไปเลยตรง ๆ หรือไม่ก็ใช้ naming convention อย่าง *.auto.tfvars สำหรับไฟล์ที่ปลอดภัย แล้วแยกชัดเจนสำหรับไฟล์ที่มีอะไร sensitive

flowchart LR
  hardcoded["Hardcoded client_secret in .tf file"] -->|committed| git["git history forever"]
  kv["Azure Key Vault secret"] -->|data source at apply time| tf["Terraform resource attribute"]
  tf -->|recorded| state["terraform.tfstate"]
  kv -->|rotated in one place| tf
Azure Key Vault เป็นแหล่งความจริงของ credential อ่านเข้า Terraform ผ่าน data source ตอน apply เทียบกับ credential ที่ hardcode ไว้ในไฟล์ที่ commit แล้ว

ทุกเทคนิคในบทนี้ตอบคำถามเดียวกันด้วยความแรงที่เพิ่มขึ้นเรื่อย ๆ: “secret value นี้อยู่ที่ไหน นานแค่ไหน” client_secret ที่ hardcode ไว้อยู่ตลอดไป ใน git history data source แคบลงแค่ ค่ามาจากไหน (Key Vault ไม่ใช่ค่าที่พิมพ์เอง) แต่ไม่แคบ ไปจบที่ไหน — พิสูจน์ข้างบนว่ายังลงไปอยู่ใน state ephemeral + write-only คือคำตอบที่แรงที่สุด พิสูจน์จริงข้างบนกับ schema ของ azurerm 5.7.0 เอง provider alias เป็นเรื่องแยกต่างหาก ไม่เกี่ยวกัน — มันตอบว่า “subscription ไหน” ที่ Key Vault read เล็งไป ไม่ใช่ “ค่าอยู่นานแค่ไหน” — บทนี้พิสูจน์ทั้งสองเรื่องคู่กันเพราะ Key Vault จริง ๆ มักอยู่คนละ subscription จาก resource ที่อ่านมันบ่อยมาก

ทางเลือกได้อะไรเสียอะไร
Azure CLI / managed identity / OIDC แทน client_secretไม่มี credential ค้างอยู่ให้หลุดจากไฟล์ .tf/.tfvarsแต่ละกลไกต้อง setup environment ของตัวเอง (CLI session, identity assignment, federated credential) ถึงจะใช้งานได้
data source แทน variable สำหรับ secretรวม lifecycle การ rotate ไว้ที่ Key Vault ที่เดียวพิสูจน์ข้างบน: ค่าที่ resolve แล้วยังลงไปอยู่ใน state — ต้องปกป้อง backend ด้วย Azure RBAC อยู่ดี
ephemeral + write-only (administrator_password_wo, value_wo)พิสูจน์ข้างบนจริงกับ azurerm 5.7.0: argument ทั้งสองมีอยู่จริงบน resource เหล่านี้พิสูจน์ซ้ำแบบ independent ด้วย live azurerm plan.json ในคอร์สนี้ไม่ได้ (azurerm ต้องมี Azure auth จริงแม้แค่จะ plan) — บทนี้ระบุชัดว่า proof ตรงนี้อยู่ระดับ schema จับคู่กับ plan-JSON proof ข้าม provider ที่พิสูจน์ไว้แล้วฝั่ง AWS
azurerm provider ตัวที่สองที่ alias ไว้ สำหรับอ่าน Key Vault ข้าม subscriptionพิสูจน์ข้างบน: provider = azurerm.keyvault validate ผ่านทั้งบน data, resource, และ ephemeral blockKey Vault reference ทุกจุดในทั้ง configuration ต้องจำใส่ provider = ชัดเจน ลืมแล้วจะเงียบ ๆ ไปเล็ง subscription default ผิดตัว แทนที่จะ error ดัง ๆ
  • “mark variable เป็น sensitive = true กันมันออกจาก state ได้” เนื้อหา baseline ของบทนี้เองกับ module State Management บอกไว้แล้ว: sensitive แค่ redact CLI output ค่ายังคงลงไปอยู่ใน terraform.tfstate แบบ plaintext
  • “data source ที่ดึงจาก Key Vault แทนที่ความจำเป็นของ ephemeral/write-only ได้เต็มร้อย” พิสูจน์ข้างบนจาก design: ผลลัพธ์ของ data source ถูก cache ใน state เหมือน attribute ของ resource เป๊ะ — ephemeral มีไว้เพื่อปิด gap นี้โดยเฉพาะเพราะ data แก้ไม่ได้
  • “write-only argument ใช้ได้กับ Azure resource ไหนก็ได้ เพราะ feature นี้มีอยู่ใน provider แล้ว” พิสูจน์ข้างบนด้วยการ validate schema ตรง ๆ: administrator_password_wo มีอยู่บน azurerm_postgresql_flexible_server และ value_wo มีอยู่บน azurerm_key_vault_secret โดยเฉพาะ เพราะ schema ของแต่ละ resource ต้องเพิ่มมันเข้าไปเอง feature ที่มีอยู่ใน Terraform core ไม่ได้แปลว่าทุก argument ของทุก resource จะมีคู่ _wo
  • “data \"azurerm_key_vault_secret\" block ที่ไม่มี provider = ชัดเจน จะ fail ดัง ๆ ถ้า Key Vault อยู่คนละ subscription” มัน fail จริง แต่ไม่จำเป็นต้องดังหรือชัดเจน — พิสูจน์ข้างบน ถ้าไม่มี alias Terraform ก็แค่ query subscription ของ provider default แล้วได้ “not found” จริง ๆ กลับมาสำหรับ Key Vault ที่อยู่คนละที่จริง ๆ ซึ่ง diagnose ผิดเป็นปัญหาเรื่องชื่อหรือ permission ได้ง่ายมาก แทนที่จะรู้ว่าจริง ๆ คือลืม provider =

เวลา review AI-generated Terraform ที่จัดการ Azure secret ให้เช็ค:

  • client_secret ที่พิมพ์ตรง ๆ ลงใน provider "azurerm" block หรืออ่านจาก environment variable ธรรมดา (ไม่ใช่ federated) ที่ไม่มี rotation story — สัญญาณของ credential แบบ long-lived ที่ควรเป็น CLI/managed-identity/OIDC แทน
  • argument แบบ _wo ที่ใช้โดยไม่เช็ค schema ของ resource นั้นจริง ๆ — พิสูจน์ข้างบนผ่าน terraform validate argument ต้องมีอยู่จริงบน resource ตัวนั้นเป๊ะ ๆ ชื่อ _wo ที่ฟังดูสมเหตุสมผลแต่ไม่มีจริงจะ fail validate ทันที
  • Key Vault data/ephemeral block ที่ไม่มี provider = ใน configuration ที่ประกาศ azurerm provider แบบ alias ไว้ด้วย — พิสูจน์ข้างบน ลืม alias จะเงียบ ๆ ไปเล็ง subscription ผิดตัว แทนที่จะ error ตรงจุดที่พลาด
  • value_wo/administrator_password_wo ที่ไม่มี _wo_version ขยับตามใน PR ที่ควรจะ rotate credential — เพราะตัวค่าเองไม่ได้ track ใน state เลข version ที่ไม่เปลี่ยนแปลว่า provider ยังใช้ค่าเดิมแบบเงียบ ๆ
  • โค้ดที่อ่าน secret ผ่าน Key Vault data source (ไม่ใช่ ephemeral) แล้ว treat เหมือนว่าเทียบเท่ากับการจัดการแบบ write-only — พิสูจน์ข้างบน ผลลัพธ์ของ data ถูก cache ใน state มีแค่คู่ ephemeral/write-only เท่านั้นที่หลีกเลี่ยงได้

🐛 หา bug ในโค้ดนี้

provider "azurerm" {
features {}
subscription_id = "00000000-0000-0000-0000-000000000000"
}
provider "azurerm" {
alias = "keyvault"
features {}
subscription_id = "11111111-1111-1111-1111-111111111111"
}
data "azurerm_key_vault" "shared" {
name = "shared-keyvault"
resource_group_name = "shared-security-rg"
}
เฉลย

data "azurerm_key_vault" "shared" block นี้ไม่มี provider = azurerm.keyvault — พิสูจน์ข้างบน นั่นแปลว่ามันเงียบ ๆ ไป query subscription ของ provider default (00000000-...) ไม่ใช่ shared security subscription (11111111-...) ที่ Key Vault อยู่จริง ผลลัพธ์คือ error “Key Vault not found” จริง ๆ ที่ดูเหมือนพิมพ์ชื่อผิดหรือ permission มีปัญหา ทั้งที่ bug จริงคือขาดบรรทัด provider = บน data source ที่มี aliased provider อยู่เหนือมันในไฟล์เดียวกันเลย ทางแก้ พิสูจน์ข้างบน คือใส่ provider = azurerm.keyvault ชัดเจนบนทุก block ที่ต้องอ่านจาก subscription นั้น

best practice ปัจจุบันสำหรับ authenticate azurerm provider ใน CI pipeline ที่รันบน Azure คืออะไร
ทำไม pull secret เข้า Terraform ผ่าน data source จาก Azure Key Vault โดยทั่วไปดีกว่า define เป็น Terraform variable
อะไรต่อไปนี้ควร exclude จาก gitignore ของ Terraform หรือ Terragrunt repository เสมอ
provider "azurerm" block มีแค่ features {} ไม่มี argument client_id หรือ client_secret เลย นี่แปลว่าอะไร
การจับคู่ write-only argument อย่าง administrator_password_wo กับ administrator_password_wo_version ได้อะไรขึ้นมาจริง ๆ
Key Vault อยู่คนละ subscription "shared-security" จาก resource ที่อ่านมัน การ read ต้องมีอะไร พิสูจน์ข้างบนกับ validate จริง?