Terraform Provider for Azure Key Vault を作った 〜Azure の Key Vault secret を Terraform で安全に管理したい〜
Azure の Key Vault secret (AWS でいう paramete store) を Terraform で管理できると次のようなメリットがあります。
- secret の作成・変更を pull request のレビューフローに乗せることができる
- ただし secret value は apply 時に指定したり、Terraform では仮の値を指定して別途本当の値で更新したりする想定
- CI 等で terraform plan/apply するようにしていれば secret の操作に関する権限がない人でも作成・変更できる
- secret value の更新に関しては同上
- Terraform file を見るだけでどのような secret があるか確認できる
- 謎に作成された secret の存在に気付ける
- 権限のスコープを設定する際に特定の secret に対する read 権限を手軽に付与できる
- resource ID を手軽に取得できる
ところが、Terraform で Key Vault secret を普通に管理すると、state file に secret value が保存されるという問題があります。
Terraform の state file に一部の attribute を保存しないようにしたいという話は 10 年以上も前から話題になっています。1
その対策として、Terraform 1.11 では write-only arguments が導入され、Terraform Provider for Azure の azurerm_key_vault_secret
リソースでも write-only argument である value_wo
が導入されました。2
ここで、write-only arguments を使わなくても ignore_changes
を使えば良いじゃないかと思った方もいるでしょうが、ignore_changes
だと state file に実際の値が保存されます。詳細は [Terraform] 誤解されがちなignore_changesの動き・機密情報はstateに保持されるのか? | DevelopersIO を読んでみてください。
azurerm_key_vault_secret
で value_wo
を使えば問題ないかというとそうでもなく、value_wo
を使ったとしても Microsoft.KeyVault/vaults/secrets/getSecret/action
の権限が必要で、terraform plan
を実行できる権限を付与するには secret value を取得する権限を付与することになり使い勝手が悪いです。
というわけで、value_wo を使っている場合に secret value の取得を不要にする PR を 3 ヶ月前に出したんですが、いつまで経ってもレビューされる気配はなく、半年以上前に出した簡単な bugfix PR も同様で、Slack も閑散としていて、レビューしてくれとたまにメッセージを投稿する人がいてもスルーという状態で、マージされるにはまだまだ時間がかかりそうです。
そんなわけで、Microsoft.KeyVault/vaults/secrets/getSecret/action
の権限なしで secret を管理できる terraform-provider-azurekv を作りました。
使い方
基本的な使い方はドキュメントを参照してください。
基本的に azurerm_key_vault_secret
と同じ使い方ができますが、soft delete 済みの secret を復活させたり、purge させる機能はサポートしていません。
azurerm_key_vault_secret からのマイグレーション
例えば次のようなリソースが定義されていたとします。
resource "azurerm_key_vault_secret" "example" {
name = "example"
key_vault_id = var.key_vault_id
value_wo = "This value is manged outside of Terraform"
value_wo_version = 1
}
この場合、まずは azurerm_key_vault_secret
の内容をコピペして、コピーした内容の azurerm_key_vault_secret
を azurekv_secret
に変更し、import
ブロックでインポートします。
resource "azurerm_key_vault_secret" "example" {
name = "example"
key_vault_id = var.key_vault_id
value_wo = "This value is manged outside of Terraform"
value_wo_version = 1
}
resource "azurekv_secret" "example" {
name = "example"
key_vault_id = var.key_vault_id
value_wo = "This value is manged outside of Terraform"
value_wo_version = 1
}
import {
id = azurerm_key_vault_secret.example.id
to = azurekv_secret.example
}
これで一度 terraform apply
を実行し、次に azurerm_key_vault_secret.example
を削除します。
resource "azurekv_secret" "example" {
name = "example"
key_vault_id = var.key_vault_id
value_wo = "This value is manged outside of Terraform"
value_wo_version = 1
}
removed {
from = azurerm_key_vault_secret.example
lifecycle {
destroy = false
}
}
最後に不要になった removed
ブロックを削除すれば移行完了です。
その他の選択肢
Azure provider
Microsoft.KeyVault/vaults/secrets/getSecret/action
の権限が必要な時点で選択肢にはないんですが、ignore_changes
の意味がないことを確認します。
terraform {
required_version = ">=1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=4.41.0"
}
}
}
provider "azurerm" {
features {}
}
resource "azurerm_key_vault_secret" "example" {
name = "example"
key_vault_id = var.key_vault_id
value = "This value is manged outside of Terraform"
lifecycle {
ignore_changes = [value]
}
}
terraform apply
後の状態は次のとおりです。
$ jq -r '.resources[] | select(.name == "example").instances[].attributes.value' terraform.tfstate
This value is manged outside of Terraform
secret value を更新します。
az keyvault secret set \
--vault-name $KEY_VAULT_NAME \
--name example \
--value 'This is the actual secret!'
terraform refresh
の後に state file を確認してみると、Terraform 外で更新した値が保存されていることがわかります。
$ jq -r '.resources[] | select(.name == "example").instances[].attributes.value' terraform.tfstate
This is the actual secret!
AzAPI provider
AzAPI provider を使うと、secret value が state file に保存されても Terraform 外で更新された値には追従しないので、ignore_changes
を使った時のような問題は起きません。
terraform {
required_version = ">=1.0"
required_providers {
azapi = {
source = "azure/azapi"
version = "=2.6.1"
}
}
}
resource "azapi_resource" "example" {
name = "example"
type = "Microsoft.KeyVault/vaults/secrets@2024-11-01"
parent_id = "/subscriptions/f48afe64-0e0a-4ce8-b0bd-21eecb6b9ad1/resourceGroups/terraform-provider-azurekv/providers/Microsoft.KeyVault/vaults/avnjctgtwrxesljlzowkbdlr"
body = {
properties = {
value = "This value is manged outside of Terraform"
}
}
}
ただ、次のようにタグを追加するだけでも新しいバージョンの secret が作成され、value も Terraform file で指定しているものに戻ってしまうので使い勝手が悪いです。
resource "azapi_resource" "example" {
name = "example"
type = "Microsoft.KeyVault/vaults/secrets@2024-11-01"
parent_id = "/subscriptions/f48afe64-0e0a-4ce8-b0bd-21eecb6b9ad1/resourceGroups/terraform-provider-azurekv/providers/Microsoft.KeyVault/vaults/avnjctgtwrxesljlzowkbdlr"
body = {
properties = {
value = "This value is manged outside of Terraform"
}
tags = {
Environment = "Production"
}
}
}
おまけ 〜動作確認用 service principal の作成方法〜
開発方法についてはだいたい https://github.com/abicky/terraform-provider-azurekv の README に書いてあるんですが、動作確認用の service principal の作成について紹介します。
テスト用 service principal
make testacc
の実行に使うやつです。
まず、次のような JSON を作成します。
{
"Name": "Terraform Provider for Azure Key Vault Tester",
"Actions": [
"Microsoft.Resources/subscriptions/resourceGroups/delete",
"Microsoft.Resources/subscriptions/resourceGroups/read",
"Microsoft.Resources/subscriptions/resourceGroups/write",
"Microsoft.KeyVault/locations/deletedVaults/purge/action",
"Microsoft.KeyVault/locations/operationResults/read",
"Microsoft.KeyVault/vaults/delete",
"Microsoft.KeyVault/vaults/read",
"Microsoft.KeyVault/vaults/write"
],
"DataActions": [
"Microsoft.KeyVault/vaults/secrets/delete",
"Microsoft.KeyVault/vaults/secrets/getSecret/action",
"Microsoft.KeyVault/vaults/secrets/purge/action",
"Microsoft.KeyVault/vaults/secrets/readMetadata/action",
"Microsoft.KeyVault/vaults/secrets/setSecret/action"
],
"AssignableScopes": [
"/subscriptions/YOUR_SUBSCRIPTION_ID"
]
}
上記の JSON を使って custom role を作成します。
az role definition create --role-definition terraform-provider-for-azure-key-vault-tester.json
作成された custom role を付与しつつ service principal を作成します。
az ad sp create-for-rbac \
--name terraform-provider-azurekv-tester \
--role 'Terraform Provider for Azure Key Vault Tester' \
--scopes /subscriptions/$YOUR_SUBSCRIPTION_ID
出力された appId
が client ID、password
が client secret になります。secret の期限はデフォルトで 1 年間です。
なお、CI では managed identity を作成し、同じ role を付与した上で OIDC を利用しています。
terraform plan 用 service principal
次のような JSON を用意します。あとはテスト用 service principal と同様です。
{
"Name": "Terraform Provider for Azure Key Vault Planner",
"Actions": [
"Microsoft.KeyVault/vaults/read"
],
"DataActions": [
"Microsoft.KeyVault/vaults/secrets/readMetadata/action"
],
"AssignableScopes": [
"/subscriptions/YOUR_SUBSCRIPTION_ID"
]
}
terraform plan 用 service principal
次のような JSON を用意します。あとはテスト用 service principal と同様です。
{
"Name": "Terraform Provider for Azure Key Vault Applier",
"Actions": [
"Microsoft.KeyVault/vaults/read"
],
"DataActions": [
"Microsoft.KeyVault/vaults/secrets/delete",
"Microsoft.KeyVault/vaults/secrets/readMetadata/action",
"Microsoft.KeyVault/vaults/secrets/setSecret/action"
],
"AssignableScopes": [
"/subscriptions/YOUR_SUBSCRIPTION_ID"
]
}