A composite module that provisions a hardened Azure App Configuration store together with its configuration keys and feature flags, targeting
hashicorp/azurerm ~> 4.0.
This module manages an Azure App Configuration store and the child data-plane objects that share its lifecycle:
- ποΈ The store (
azurerm_app_configuration.this) β a centralized, versioned configuration service reachable over Microsoft Entra ID. - π Configuration keys (
azurerm_app_configuration_key) β key/value entries, or references to Key Vault secrets, as a keyedfor_eachmap. - π© Feature flags (
azurerm_app_configuration_feature) β feature toggles with percentage, targeting, custom, and time-window filters, as a keyedfor_eachmap. - π‘οΈ A hardened default posture β private network access, no access keys, purge protection, and a system-assigned managed identity, all on the empty call.
- π Customer-managed key wiring β an
encryptionblock ready to consume a Key Vault key, off until a key is supplied. - π Geo-replicas β optional regional replicas as a keyed map.
π‘ Why it matters: application configuration and feature state are cross-cutting and security-sensitive. Centralizing them in a private, Entra-authenticated store β with secrets kept in Key Vault and referenced rather than embedded β removes configuration sprawl and keeps sensitive material out of application code and state.
If this module saves you time, please consider supporting its continued development:
- β Star the repository on GitHub.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
rg["terraform-azurerm-resource-group"]
kv["terraform-azurerm-key-vault"]
ra["terraform-azurerm-role-assignments"]
pe["terraform-azurerm-private-endpoint"]
appcfg["terraform-azurerm-app-configuration"]
store["azurerm_app_configuration.this"]
app["Consuming apps / services"]
rg -->|"resource_group_name + location"| appcfg
kv -->|"CMK key id for encryption"| appcfg
appcfg -->|"creates"| store
ra -->|"App Configuration Data Reader at store id"| store
pe -->|"private endpoint to store id"| store
app -->|"read config via Entra RBAC"| store
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef keystone fill:#004578,stroke:#002d4d,color:#ffffff;
classDef sib fill:#eef2f7,stroke:#c5ccd6,color:#1b1f24;
class appcfg me;
class store keystone;
class rg,kv,ra,pe,app sib;
flowchart TB
subgraph inputs["Inputs"]
v1["name / resource_group_name / location"]
v2["sku / public_network_access / local_auth_enabled / purge_protection"]
v3["identity / encryption / replicas"]
v4["keys{} / features{}"]
end
store["azurerm_app_configuration.this"]
key["azurerm_app_configuration_key (for_each keys)"]
feat["azurerm_app_configuration_feature (for_each features)"]
out["Outputs: id / name / endpoint / identity_principal_id / child id maps"]
v1 -->|"configure"| store
v2 -->|"configure"| store
v3 -->|"nested blocks"| store
v4 -->|"child maps"| store
store -->|"configuration_store_id"| key
store -->|"configuration_store_id"| feat
store -->|"emit"| out
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef keystone fill:#004578,stroke:#002d4d,color:#ffffff;
classDef sib fill:#eef2f7,stroke:#c5ccd6,color:#1b1f24;
class key,feat me;
class store keystone;
class v1,v2,v3,v4,out sib;
Resource inventory
| Resource | Cardinality | Role |
|---|---|---|
azurerm_app_configuration.this |
single (keystone) | The App Configuration store. |
azurerm_app_configuration_key.this |
for_each over var.keys |
Key/value entries or Key Vault references. |
azurerm_app_configuration_feature.this |
for_each over var.features |
Feature flags with optional filters. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| azurerm provider | ~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, auth, and subscription. |
Schema notes that bite (verified against the live provider schema):
name,resource_group_name, andlocationare force-new β changing any replaces the store.soft_delete_retention_daysaccepts only 1β7 days, applies to thestandardandpremiumSKUs, and is force-new β and Microsoft states the retention period "can only be set at the creation of store and once set, it can't be changed", which is why. See the replacement warning below: on a purge-protected store that force-new cannot complete.purge_protection_enabledapplies tostandardonly and is irreversible once enabled; deleting a protected store schedules (not immediate) deletion.skucannot be downgraded exceptpremiumβstandard; a downgrade forces replacement.public_network_accessbecomesAutomaticwhen unset in the provider and cannot return toAutomaticafter a concrete value is applied. This module always sets a concrete value (defaultDisabled).data_plane_proxy_private_link_delegation_enabledcannot betruewhiledata_plane_proxy_authentication_modeisLocal.valueandvault_key_referenceon a key are mutually exclusive;content_type/valueapply only whentype = "kv".
Least-privilege, at the smallest scope that works:
- Control plane (create/update the store, identity, encryption, replicas):
App Configuration Contributoron the target resource group, or a custom role withMicrosoft.AppConfiguration/configurationStores/*write actions scoped to the resource group. - Data plane (create the keys and feature flags this module owns):
App Configuration Data Owneron the store or a parent scope. Keys and features are provisioned through the data-plane API, soContributoralone is not sufficient. - Customer-managed key (only when
encryptionis set): the store's managed identity needsKey Vault Crypto Service Encryption Useron the referenced key. - Consumers (runtime):
App Configuration Data Readeron the store.
- An existing resource group in a supported US Azure region.
- The
Microsoft.AppConfigurationresource provider registered on the subscription. - For customer-managed key encryption: an existing Key Vault key and a managed identity granted access to it.
- The
App Configuration Data Ownerrole in effect for the caller beforeapply(data-plane RBAC can take a short time to propagate). - The caller configures the
provider "azurerm" { features {} }block, authentication, and subscription; the module declares none of these.
terraform-azurerm-app-configuration/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
βββ variables.tf # deeply-typed object() schemas; secure defaults; tags + timeouts tail
βββ main.tf # keystone `this` + for_each keys/features; dynamic blocks + try()
βββ outputs.tf # id first, then name, endpoint, identity, child id maps
βββ README.md # this document
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT, Copyright (c) 2026 Casey Wood
βββ .gitignore # canonical library ignore set
The smallest real call β an empty configuration produces a hardened, private store:
provider "azurerm" {
features {}
}
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-platform-prod"
resource_group_name = "rg-platform-prod"
location = "eastus"
}βΉοΈ The caller owns provider configuration: authentication (Azure CLI, managed identity, or OIDC), the target subscription, and the mandatory
features {}block. This module never declares aproviderblock.
Consumes
| Input | Type | Source |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group (name) |
location |
string |
caller / resource group location |
encryption.key_vault_key_identifier |
string |
terraform-azurerm-key-vault (key ID) |
encryption.identity_client_id |
string |
terraform-azurerm-user-assigned-identity (client_id) |
Emits
| Output | Description |
|---|---|
id |
App Configuration store Resource ID (emitted first). |
name |
Store name. |
endpoint |
Data-plane endpoint URL. |
identity_principal_id |
Principal ID of the system-assigned identity (null if none). |
identity_tenant_id |
Tenant ID of the store's identity. |
replica_endpoints |
Map: replica name β endpoint URL. |
app_configuration_key_ids |
Map: key β key resource ID. |
app_configuration_feature_ids |
Map: key β feature resource ID. |
The examples read the current tenant from the provider rather than hard-coding it.
data "azurerm_client_config" "current" {}1 Β· Minimal, hardened store
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-platform-prod"
resource_group_name = "rg-platform-prod"
location = "eastus"
}π The empty call yields
standardSKU,public_network_access = "Disabled",local_auth_enabled = false,purge_protection_enabled = true,soft_delete_retention_days = 7, and a system-assigned managed identity.
2 Β· A handful of configuration keys
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-web-prod"
resource_group_name = "rg-web-prod"
location = "eastus2"
keys = {
api_base_url = {
key = "Web:ApiBaseUrl"
value = "https://api.internal.example.com"
}
page_size = {
key = "Web:PageSize"
value = "50"
content_type = "text/plain"
}
}
}π‘ Map keys (
api_base_url,page_size) are stablefor_eachidentifiers; renaming an entry's map key never re-creates the others.
3 Β· Labeled keys for multiple environments
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-shared"
resource_group_name = "rg-shared"
location = "centralus"
keys = {
timeout_dev = {
key = "Service:TimeoutSeconds"
label = "dev"
value = "30"
}
timeout_prod = {
key = "Service:TimeoutSeconds"
label = "prod"
value = "5"
}
}
}βΉοΈ
labelpartitions the same key across environments.keyandlabelare both immutable β changing either replaces that entry.
4 Β· Key Vault reference (no plaintext secrets)
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-secretsafe"
resource_group_name = "rg-platform-prod"
location = "eastus"
keys = {
db_connection = {
key = "Data:ConnectionString"
type = "vault"
vault_key_reference = "https://kv-platform-prod.vault.azure.net/secrets/db-connection"
}
}
}π For secret material use
type = "vault"withvault_key_referenceβ the value stays in Key Vault.valueandvault_key_referenceare mutually exclusive.
5 Β· A simple feature flag (off by default)
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-features"
resource_group_name = "rg-web-prod"
location = "eastus"
features = {
new_checkout = {
name = "NewCheckout"
description = "Enables the redesigned checkout flow."
enabled = false
}
}
}
β οΈ Feature flags default toenabled = falseso a newly declared flag never silently activates behavior. Flip it explicitly when you are ready.
6 Β· Percentage rollout filter
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-rollout"
resource_group_name = "rg-web-prod"
location = "eastus2"
features = {
beta_search = {
name = "BetaSearch"
enabled = true
percentage_filter_value = 25
}
}
}π‘ A percentage filter enables the flag for the given percentage of evaluations β here 25%.
7 Β· Targeting filter (users and groups)
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-targeting"
resource_group_name = "rg-web-prod"
location = "eastus"
features = {
early_access = {
name = "EarlyAccess"
enabled = true
targeting_filter = [
{
default_rollout_percentage = 0
users = ["alice@example.com", "bob@example.com"]
groups = [
{ name = "beta-testers", rollout_percentage = 100 },
{ name = "internal", rollout_percentage = 50 },
]
}
]
}
}
}βΉοΈ Targeting filters roll a feature out to named users and groups independently of the default percentage.
8 Β· Time-window and custom filters
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-filters"
resource_group_name = "rg-web-prod"
location = "eastus2"
features = {
holiday_banner = {
name = "HolidayBanner"
enabled = true
timewindow_filter = [
{ start = "2026-12-01T00:00:00+00:00", end = "2026-12-26T00:00:00+00:00" }
]
custom_filter = [
{ name = "Microsoft.Targeting", parameters = { Audience = "all" } }
]
}
}
}
β οΈ Time-windowstart/endmust be RFC3339 timestamps. Custom filters pass provider-specific parameters straight through.
9 Β· Public access opt-in (explicit)
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-public"
resource_group_name = "rg-sandbox"
location = "westus2"
public_network_access = "Enabled"
}
β οΈ public_network_access = "Enabled"exposes the data-plane endpoint to the internet. Prefer a private endpoint (see the composition example) for production workloads.
10 Β· Customer-managed key (CMK) encryption
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-cmk"
resource_group_name = "rg-platform-prod"
location = "eastus"
sku = "standard"
identity = {
type = "UserAssigned"
identity_ids = ["/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform-prod/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-appcfg"]
}
encryption = {
key_vault_key_identifier = "https://kv-platform-prod.vault.azure.net/keys/appcfg-cmk/abc123"
identity_client_id = "11111111-1111-1111-1111-111111111111"
}
}π CMK requires a managed identity with
Key Vault Crypto Service Encryption Useron the key. When using a user-assigned identity for the key, setencryption.identity_client_id.
11 Β· Geo-replicas
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-geo"
resource_group_name = "rg-platform-prod"
location = "eastus"
replicas = {
west = { name = "westus2replica", location = "westus2" }
cen = { name = "centralusreplica", location = "centralus" }
}
}βΉοΈ Replicas require the
standardorpremiumSKU and a region different from the store's. Their endpoints are emitted inreplica_endpoints.
12 Β· Free SKU (purge protection disabled)
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-dev"
resource_group_name = "rg-dev"
location = "eastus2"
sku = "free"
purge_protection_enabled = false
}
β οΈ Purge protection and soft-delete retention apply to thestandardandpremiumSKUs β Microsoft: "All App Configuration stores in the Standard and Premium tiers have automatically enabled the soft-delete feature." Selectingfreeordeveloperrequires settingpurge_protection_enabled = false.
13 Β· Data-plane proxy (pass-through)
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-proxy"
resource_group_name = "rg-platform-prod"
location = "eastus"
data_plane_proxy_authentication_mode = "Pass-through"
data_plane_proxy_private_link_delegation_enabled = true
}βΉοΈ
data_plane_proxy_private_link_delegation_enabled = truerequiresdata_plane_proxy_authentication_mode = "Pass-through"; it is rejected while the mode isLocal.
14 Β· Keys and features at scale via for_each
locals {
settings = {
"App:Title" = "Platform Console"
"App:Theme" = "dark"
"App:MaxUploadMb" = "25"
"App:Region" = "us"
}
}
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-scale"
resource_group_name = "rg-platform-prod"
location = "eastus"
keys = {
for k, v in local.settings : replace(lower(k), ":", "_") => {
key = k
value = v
}
}
tags = {
environment = "prod"
owner = "platform-team"
}
}π‘ Module-level
tagspropagate to the store and merge into each child key/feature that supports tags; per-childtagsoverride on conflict.
15 Β· ποΈ End-to-end composition
Wire a resource group, a Key Vault key for CMK, this store, and a data-plane role assignment for a consuming application's identity.
provider "azurerm" {
features {}
}
module "resource_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-platform-prod"
location = "eastus"
}
module "key_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault.git?ref=v1.0.0"
tenant_id = data.azurerm_client_config.current.tenant_id
name = "kv-platform-prod"
resource_group_name = module.resource_group.name
location = module.resource_group.location
keys = {
appcfg_cmk = { name = "appcfg-cmk", key_type = "RSA", key_size = 2048, key_opts = ["get", "unwrapKey", "wrapKey"] }
}
}
module "app_configuration" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-configuration.git?ref=v1.0.0"
name = "appcfg-platform-prod"
resource_group_name = module.resource_group.name
location = module.resource_group.location
sku = "standard"
encryption = {
key_vault_key_identifier = module.key_vault.key_ids["appcfg_cmk"]
}
keys = {
api_base_url = { key = "Web:ApiBaseUrl", value = "https://api.internal.example.com" }
}
features = {
new_checkout = { name = "NewCheckout", enabled = false }
}
}
# Grant a consuming application's identity least-privilege data-plane read access.
module "app_config_reader" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.app_configuration.id
role_assignments = {
app_reader = {
scope = module.app_configuration.id
role_definition_name = "App Configuration Data Reader"
principal_id = var.consuming_app_principal_id
}
}
}π Consumers read configuration over Entra ID with
App Configuration Data Readerβ no access keys are issued. The store's own identity is granted access to the Key Vault key out of band (viarole-assignments) so CMK can wrap/unwrap.
Grouped summary
- Identity & placement (required):
name,resource_group_name,location. - Store posture (secure defaults):
sku(standard),local_auth_enabled(false),public_network_access(Disabled),purge_protection_enabled(true),soft_delete_retention_days(7),data_plane_proxy_authentication_mode(Local),data_plane_proxy_private_link_delegation_enabled(false). - Identity & encryption:
identity(system-assigned by default),encryption(CMK, off by default). - Collections:
replicas,keys,features. - Universal tail:
tags,timeouts.
Full object() schemas
variable "name" { type = string } # immutable
variable "resource_group_name" { type = string } # immutable
variable "location" { type = string } # immutable
variable "sku" { type = string, default = "standard" } # free|developer|standard|premium
variable "local_auth_enabled" { type = bool, default = false }
variable "public_network_access" { type = string, default = "Disabled" } # Enabled|Disabled
variable "purge_protection_enabled" { type = bool, default = true }
variable "soft_delete_retention_days" { type = number, default = 7 } # 1..7
variable "data_plane_proxy_authentication_mode" { type = string, default = "Local" } # Local|Pass-through
variable "data_plane_proxy_private_link_delegation_enabled" { type = bool, default = false }
variable "identity" {
type = object({
type = string # SystemAssigned | UserAssigned | "SystemAssigned, UserAssigned"
identity_ids = optional(set(string))
})
default = { type = "SystemAssigned" }
}
variable "encryption" {
type = object({
key_vault_key_identifier = optional(string)
identity_client_id = optional(string)
})
default = null
}
variable "replicas" {
type = map(object({
name = string
location = string
}))
default = {}
}
variable "keys" {
type = map(object({
key = string
label = optional(string)
content_type = optional(string)
value = optional(string)
type = optional(string, "kv") # kv | vault
vault_key_reference = optional(string)
locked = optional(bool)
tags = optional(map(string), {})
}))
default = {}
}
variable "features" {
type = map(object({
name = string
key = optional(string)
label = optional(string)
description = optional(string)
enabled = optional(bool, false)
locked = optional(bool)
percentage_filter_value = optional(number)
custom_filter = optional(list(object({
name = string
parameters = optional(map(string), {})
})), [])
targeting_filter = optional(list(object({
default_rollout_percentage = number
users = optional(list(string), [])
groups = optional(list(object({
name = string
rollout_percentage = number
})), [])
})), [])
timewindow_filter = optional(list(object({
start = optional(string)
end = optional(string)
})), [])
tags = optional(map(string), {})
}))
default = {}
}
variable "tags" { type = map(string), default = {} }
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Notes |
|---|---|---|
id |
App Configuration store Resource ID. | Emitted first. |
name |
Store name. | |
location |
Azure region, in the canonical form Azure uses. | Read from the resource, not var.location. |
endpoint |
Data-plane endpoint URL. | |
identity_principal_id |
Principal ID of the system-assigned identity. | null when no system-assigned identity. |
identity_tenant_id |
Tenant ID of the store's identity. | null when no identity. |
replica_endpoints |
Map: replica name β endpoint URL. | Empty when no replicas. |
app_configuration_key_ids |
Map: key β key resource ID. | Empty when no keys. |
app_configuration_feature_ids |
Map: key β feature resource ID. | Empty when no features. |
π Access keys and connection strings are deliberately not emitted. Authenticate consumers with Microsoft Entra ID.
- Force-new identity fields.
name,resource_group_name, andlocationreplace the store when changed. Treat them as immutable; a rename is a re-create. - π΄ A FORCED REPLACEMENT MAY NOT BE ABLE TO COMPLETE, and the secure default is what makes that so. Microsoft: "When a Standard or Premium tier App Configuration store is deleted, its name is reserved for the retention period. To recreate a store with the same name before the retention period expires, you need to purge the soft-deleted store first, provided the store doesn't have purge protection enabled. If the purge protection is enabled, you must wait for the retention period to elapse." So with
purge_protection_enabled = true, any force-new change (name,resource_group_name,location,soft_delete_retention_days, or askudowngrade) plans a destroy-then-create in which the destroy succeeds and the create cannot β the store is gone and the name is locked for up tosoft_delete_retention_days. Changing the retention window from 7 to 3 is enough to trigger it. Treat every force-new field here as permanent, and reach for a new name rather than a forced replacement. Seerecreating_this_store_may_be_impossible_while_purge_protection_is_on. - Purge protection is a one-way door.
purge_protection_enabled = true(the default) cannot be undone β Microsoft is explicit that no administrative role overrides it β and a protected store is soft-deleted (scheduled) rather than removed. Use thefree/developerSKU withpurge_protection_enabled = falsefor throwaway environments. - Soft-delete retention is capped at 7 days. Unlike Key Vault, this resource permits only 1β7 days; the module defaults to the maximum (7) and validates the range at parse time.
- SKU defaults to
standard, notfree. This is a deliberate divergence from the provider so that purge protection and soft-delete retention apply out of the box. - Keys and features are data-plane objects. They are created through App Configuration's data-plane API, so the identity running Terraform needs
App Configuration Data Ownerin addition to control-plane rights; otherwise the store creates but the children fail. for_eachkey stability. Both children iterate over map keys you choose. Keep those keys stable; changing a map key destroys and recreates that child (andkey/labelare themselves force-new).- Mutually exclusive value paths. A
kv-type key usesvalue/content_type; avault-type key usesvault_key_reference. Do not set both. features {}dependence. The provider will not initialize without the caller'sprovider "azurerm" { features {} }block; that is expected and belongs to the root module.
The secure-by-default posture (empty call) and the caller opt-out for each:
| Concern | Secure default | Opt-out (caller types it) |
|---|---|---|
| Public network access | public_network_access = "Disabled" |
"Enabled" |
| Local (access-key) auth | local_auth_enabled = false (Entra ID) |
true |
| Purge protection | purge_protection_enabled = true |
false (required on free/developer, which have no soft delete) |
| Soft-delete retention | soft_delete_retention_days = 7 (max permitted) |
1β6 |
| Managed identity | system-assigned identity created | identity = null |
| Encryption | Microsoft-managed key | supply encryption for CMK |
| Feature flags | enabled = false per flag |
enabled = true |
| Secret material in keys | type = "vault" reference |
inline value with type = "kv" |
# Offline validation β no cloud calls, no backend:
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module by tag:
?ref=v1.0.0. Never track a branch. - This module is plan-only during authoring; a human runs
terraform planandapplyfrom CI against real credentials. - The caller supplies
provider "azurerm" { features {} }, authentication, and the subscription.
The offline proof gate exercises everything that does not require Azure:
terraform validateβ proves the configuration is internally consistent and type-correct against the pinned provider schema; the deeply-typedobject()inputs surface malformed calls at parse time.terraform fmt -checkβ enforces canonical formatting.- What only
terraform plan(run by a human, from CI, with credentials) can exercise: data-plane RBAC propagation for keys/features, CMK key access, SKU/region constraints, and replica placement. None of these run during authoring.
Outputs:
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform-prod/providers/Microsoft.AppConfiguration/configurationStores/appcfg-platform-prod"
name = "appcfg-platform-prod"
endpoint = "https://appcfg-platform-prod.azconfig.io"
identity_principal_id = "7c9e6679-7425-40de-944b-e07fc1f90ae7"
identity_tenant_id = "72f988bf-86f1-41af-91ab-2d7cd011db47"
replica_endpoints = {}
app_configuration_key_ids = {
"api_base_url" = "https://appcfg-platform-prod.azconfig.io/kv/Web:ApiBaseUrl"
}
app_configuration_feature_ids = {
"new_checkout" = "https://appcfg-platform-prod.azconfig.io/kv/.appconfig.featureflag/NewCheckout"
}
| Symptom | Cause | Fix |
|---|---|---|
AuthorizationPermissionMismatch creating keys/features |
Caller lacks the data-plane role | Grant App Configuration Data Owner on the store (or a parent scope) before apply; allow time for RBAC propagation. |
Error setting purge_protection_enabled on free SKU |
Soft delete, which purge protection protects, exists only on standard and premium |
Use sku = "standard" or "premium", or set purge_protection_enabled = false. |
Store destroyed, then create fails: the name "is not available" |
The store was soft-deleted by a forced replacement and its name is reserved; purge protection forbids purging it | Nothing recovers this before the retention window elapses. Recreate under a different name, or wait up to soft_delete_retention_days. Prevent it by treating every force-new field as permanent β see recreating_this_store_may_be_impossible_while_purge_protection_is_on. |
soft_delete_retention_days rejected |
Value outside 1β7 | Choose a value in 1β7; the module validates this at parse time. |
| Plan shows the store being replaced | A force-new field changed (name/resource_group_name/location/soft_delete_retention_days) |
Confirm the change is intended; these fields are immutable. |
data_plane_proxy_private_link_delegation_enabled rejected |
It is true while auth mode is Local |
Set data_plane_proxy_authentication_mode = "Pass-through". |
| CMK enablement fails | Store identity lacks key access | Grant the identity Key Vault Crypto Service Encryption User on the key; set encryption.identity_client_id for a user-assigned identity. |
Both value and vault_key_reference set on a key |
They are mutually exclusive | Use value with type = "kv", or vault_key_reference with type = "vault". |
| Provider fails to initialize | Missing features {} in the caller |
Add provider "azurerm" { features {} } to the root module. |
- azurerm_app_configuration
- azurerm_app_configuration_key
- azurerm_app_configuration_feature
- Azure App Configuration β RBAC and Entra ID
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-key-vault,terraform-azurerm-role-assignments,terraform-azurerm-private-endpoint,terraform-azurerm-monitor-diagnostic-setting. - This module's
SCOPE.mdβ the cross-module contract.
π "Infrastructure as Code should be standardized, consistent, and secure."