Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure App Configuration Terraform Module

A composite module that provisions a hardened Azure App Configuration store together with its configuration keys and feature flags, targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Version Type Resources


🧩 Overview

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 keyed for_each map.
  • 🚩 Feature flags (azurerm_app_configuration_feature) β€” feature toggles with percentage, targeting, custom, and time-window filters, as a keyed for_each map.
  • πŸ›‘οΈ 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 encryption block 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.

❀️ Support this project

If this module saves you time, please consider supporting its continued development:

πŸ—ΊοΈ Where this fits in the family

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;
Loading

🧬 What this module builds

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;
Loading

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.

βœ… Provider / Versions

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, and location are force-new β€” changing any replaces the store.
  • soft_delete_retention_days accepts only 1–7 days, applies to the standard and premium SKUs, 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_enabled applies to standard only and is irreversible once enabled; deleting a protected store schedules (not immediate) deletion.
  • sku cannot be downgraded except premium β†’ standard; a downgrade forces replacement.
  • public_network_access becomes Automatic when unset in the provider and cannot return to Automatic after a concrete value is applied. This module always sets a concrete value (default Disabled).
  • data_plane_proxy_private_link_delegation_enabled cannot be true while data_plane_proxy_authentication_mode is Local.
  • value and vault_key_reference on a key are mutually exclusive; content_type/value apply only when type = "kv".

πŸ”‘ Required Azure RBAC Roles / Permissions

Least-privilege, at the smallest scope that works:

  • Control plane (create/update the store, identity, encryption, replicas): App Configuration Contributor on the target resource group, or a custom role with Microsoft.AppConfiguration/configurationStores/* write actions scoped to the resource group.
  • Data plane (create the keys and feature flags this module owns): App Configuration Data Owner on the store or a parent scope. Keys and features are provisioned through the data-plane API, so Contributor alone is not sufficient.
  • Customer-managed key (only when encryption is set): the store's managed identity needs Key Vault Crypto Service Encryption User on the referenced key.
  • Consumers (runtime): App Configuration Data Reader on the store.

Azure Prerequisites

  • An existing resource group in a supported US Azure region.
  • The Microsoft.AppConfiguration resource 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 Owner role in effect for the caller before apply (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.

πŸ“ Module Structure

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

βš™οΈ Quick Start

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 a provider block.

πŸ”Œ Cross-Module Contract

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.

πŸ“š Example Library

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 standard SKU, 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 stable for_each identifiers; 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"
    }
  }
}

ℹ️ label partitions the same key across environments. key and label are 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" with vault_key_reference β€” the value stays in Key Vault. value and vault_key_reference are 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 to enabled = false so 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-window start/end must 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 User on the key. When using a user-assigned identity for the key, set encryption.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 standard or premium SKU and a region different from the store's. Their endpoints are emitted in replica_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 the standard and premium SKUs β€” Microsoft: "All App Configuration stores in the Standard and Premium tiers have automatically enabled the soft-delete feature." Selecting free or developer requires setting purge_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 = true requires data_plane_proxy_authentication_mode = "Pass-through"; it is rejected while the mode is Local.

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 tags propagate to the store and merge into each child key/feature that supports tags; per-child tags override 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 (via role-assignments) so CMK can wrap/unwrap.

πŸ“₯ Inputs

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
}

🧾 Outputs

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.

🧠 Architecture Notes

  • Force-new identity fields. name, resource_group_name, and location replace 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 a sku downgrade) 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 to soft_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. See recreating_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 the free/developer SKU with purge_protection_enabled = false for 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, not free. 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 Owner in addition to control-plane rights; otherwise the store creates but the children fail.
  • for_each key stability. Both children iterate over map keys you choose. Keep those keys stable; changing a map key destroys and recreates that child (and key/label are themselves force-new).
  • Mutually exclusive value paths. A kv-type key uses value/content_type; a vault-type key uses vault_key_reference. Do not set both.
  • features {} dependence. The provider will not initialize without the caller's provider "azurerm" { features {} } block; that is expected and belongs to the root module.

🧱 Design Principles

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"

πŸš€ Runbook

# 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 plan and apply from CI against real credentials.
  • The caller supplies provider "azurerm" { features {} }, authentication, and the subscription.

πŸ§ͺ Testing

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-typed object() 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.

πŸ’¬ Example Output

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"
}

πŸ” Troubleshooting

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.

πŸ”— Related Docs

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."