Azure Key Vault, Terraform, and the Control Plane/Data Plane Difference

Posted by Andrew Wilson on Monday, September 14, 2026

The other day I ran into one of those Azure deployment problems that looks reasonable right up until you understand which service is actually making the call.

I had provisioned an Azure Key Vault with enabled_for_template_deployment set to true. I was familiar with this setting from working with Bicep and ARM templates, where it allows the vault to be used for secret retrieval during a deployment.

Note The deployment identity still needs the required ARM deployment permission.

The Key Vault deployed successfully. I then used Terraform to create a secret in it and expected the setting to cover that deployment as well. It did not.

That was the point where I had to stop thinking about the setting and look at what was actually making each request.

variable "secret_value" {
	type      = string
	sensitive = true
}

resource "azurerm_key_vault" "example" {
	name                          = "example-key-vault"
	location                      = azurerm_resource_group.example.location
	resource_group_name           = azurerm_resource_group.example.name
	tenant_id                     = data.azurerm_client_config.current.tenant_id
	sku_name                      = "standard"
	enable_rbac_authorization     = true
	enabled_for_template_deployment = true
}

resource "azurerm_key_vault_secret" "example" {
	name         = "example-secret"
	value        = var.secret_value
	key_vault_id = azurerm_key_vault.example.id
}

What I had missed

The Key Vault itself is an Azure Resource Manager resource. Configuring it with azurerm_key_vault is a control plane operation. That part matched what I expected.

Creating a secret is different. azurerm_key_vault_secret uses the Key Vault service API, which is the data plane. The Terraform identity therefore needs an appropriate Azure RBAC role to set secrets, and the machine running Terraform must be able to reach the Key Vault endpoint.

That meant checking two separate things:

  • Did the Terraform identity have permission to write secrets?
  • Could the Terraform runner reach the vault through its networking configuration?

Having successfully deployed the vault through ARM did not answer either question. The vault request and the secret request were taking different paths.

Where AzAPI fits

Looking at AzAPI helped clarify the difference between the two providers.

AzureRM gives us typed Terraform resources. Many of those resources manage Azure Resource Manager resources through the control plane, but some resources, such as Key Vault secrets, use the service’s data-plane API.

AzAPI works directly with Azure Resource Manager REST APIs. It is useful when AzureRM has not yet exposed a control-plane resource, API version, or property that is available in Azure.

That makes AzAPI useful for an AzureRM capability gap, but it is not a general replacement for AzureRM data-plane resources. It is a different route to the Azure Resource Manager API, not a switch that makes every AzureRM resource use ARM.

There is a useful detail here. Azure also exposes secrets as the ARM resource Microsoft.KeyVault/vaults/secrets, which AzAPI can target. That provides an ARM-based way to create a secret and may avoid the Terraform runner needing direct access to the Key Vault data-plane endpoint. It still requires the appropriate ARM permissions, and the ARM resource is intended for deployment scenarios; for normal secret interaction, Azure recommends the data-plane API.

The thing worth remembering

enabled_for_template_deployment enables secret retrieval during an ARM or Bicep template deployment (The deployment identity still needs the required ARM deployment permission). The setting does not grant the deployment identity permission to create/read secrets.

When a deployment behaves unexpectedly, it is worth asking which identity is making the request and which endpoint it is calling. In this case, the vault was deployed through ARM, but the AzureRM secret resource was created through the Key Vault data plane. AzAPI can use a different ARM-based resource path, but that comes with different permissions and lifecycle considerations.

That distinction is easy to miss, particularly when moving between Bicep, ARM, and Terraform, so I am noting it here for the next time I run into it.

Useful references: