Skip to content

Repository files navigation

Terraform Azure App Configuration

Creates Azure App Configuration stores with their geo-replicas, configuration keys, and feature flags.

CI Release Terraform Registry License


Overview

App Configuration stores keyed by name, covering the full azurerm_app_configuration surface (sku, identity, customer-managed-key encryption, geo-replicas, soft delete and purge protection, ARM data plane proxy) plus the data plane: configuration keys (azurerm_app_configuration_key) and feature flags (azurerm_app_configuration_feature, with targeting, timewindow, custom, and percentage filters). Secure by default: access keys disabled (local_auth_enabled = false, the data plane is Entra ID only, which is also how this module writes keys and features). The store has no IP firewall, so public_network_access defaults to Enabled; set it to Disabled and reach the store through the private-endpoint module for network isolation. The resource group is passed by id.

Keys and features target a store created in the same call (by its map key) or an existing store (by configuration_store_id). The split matters because writing the data plane needs the App Configuration Data Owner role on the store, and a fresh grant is eventually consistent: a consumer typically creates the store in one call, grants the role (role-assignment module plus a time_sleep), then writes keys and features in a second, data-only call against the store id (which needs no resource_group_id or location).

A kv value lands in the store and in Terraform state in plain text (the provider has no write-only variant for App Configuration keys). Put secrets in Key Vault and reference them with type = "vault" and vault_key_reference; a check block warns when a kv key name looks like a credential.

Usage

module "app_configuration" {
  source  = "libre-devops/app-configuration/azurerm"
  version = "~> 4.0"

  resource_group_id = module.rg.ids["rg-ldo-uks-prd-001"]
  location          = "uksouth"
  tags              = module.tags.tags

  app_configurations = {
    "appcs-ldo-uks-prd-001" = {
      replicas = {
        "ukwest" = { location = "ukwest" }
      }
    }
  }

  app_configuration_keys = {
    "app/greeting" = {
      app_configuration_key = "appcs-ldo-uks-prd-001"
      value                 = "hello"
    }
  }
}

The inline app_configuration_keys form above only works when the applier already holds a data-plane role on new stores' scope (for example an inherited subscription-level grant); otherwise use the two-call pattern shown in examples/complete.

Examples

  • examples/minimal - a single store with the secure defaults.
  • examples/complete - a store with a geo-replica and a system-assigned identity, the App Configuration Data Owner grant with an RBAC settle, and a data-only second call writing keys (plain, JSON, locked, and optionally vault-referenced) and feature flags exercising every filter type.

Developing

Local work needs PowerShell 7+ and just, because the recipes wrap the LibreDevOpsHelpers PowerShell module (the same engine the libre-devops/terraform-azure action runs in CI). Install just with brew install just, or uv tool add rust-just then uv run just <recipe>.

Run just to list recipes: just update-ldo-pwsh (install or force-update LibreDevOpsHelpers from PSGallery), just validate, just scan (Trivy only), just pwsh-analyze (PSScriptAnalyzer only), just plan, just apply, just destroy, just e2e, just test, and just docs (the plan/apply/destroy recipes mirror the action, including the storage firewall dance; just e2e applies an example then always destroys it, defaulting to minimal, so nothing is left running). Releasing is also just: just increment-release [patch|minor|major] bumps, tags, and publishes a GitHub release, and the Terraform Registry picks up the tag.

Security scan exceptions

This module is scanned with Trivy; HIGH and CRITICAL findings fail the build. Any waiver is a deliberate, reviewed decision, never a way to quiet a finding that should be fixed. Waivers live in .trivyignore.yaml (the machine-applied source of truth, passed to Trivy with --ignorefile) and are mirrored in the table below so the reason is auditable.

Trivy ID Resource Finding Justification
None

To add an exception: add an entry to .trivyignore.yaml (id, optional paths to scope it, and a statement recording why), then add a matching row here. Where the finding is out of this module's scope, point the justification at the Libre DevOps module that does address it (for example the private-endpoint module). Both the file and this table are reviewed in the pull request.

Reference

The Requirements, Providers, Inputs, Outputs, and Resources below are generated by terraform-docs.

Requirements

Name Version
terraform >= 1.9.0, < 2.0.0
azurerm >= 4.0.0, < 5.0.0

Providers

Name Version
azurerm >= 4.0.0, < 5.0.0

Modules

No modules.

Resources

Name Type
azurerm_app_configuration.this resource
azurerm_app_configuration_feature.this resource
azurerm_app_configuration_key.this resource

Inputs

Name Description Type Default Required
app_configuration_features Feature flags to write, keyed by a friendly name (the flag name defaults to the map key).
Each entry targets exactly one store: app_configuration_key references a store created in this
call by its map key, configuration_store_id an existing store by id. Filters (targeting,
timewindow, custom, percentage) gate when the flag evaluates true. Writing features needs the
App Configuration Data Owner role on the store; a fresh grant is eventually consistent, so
settle it (time_sleep) before the first write.
map(object({
app_configuration_key = optional(string)
configuration_store_id = optional(string)

name = optional(string)
description = optional(string)
enabled = optional(bool, false)
key = optional(string)
label = optional(string)
locked = optional(bool, false)
percentage_filter_value = optional(number)
targeting_filters = optional(list(object({
default_rollout_percentage = number
users = optional(list(string), [])
groups = optional(list(object({
name = string
rollout_percentage = number
})), [])
})), [])
timewindow_filters = optional(list(object({
start = optional(string)
end = optional(string)
})), [])
custom_filters = optional(list(object({
name = string
parameters = optional(map(string), {})
})), [])
tags = optional(map(string), {})
}))
{} no
app_configuration_keys Configuration keys to write, keyed by a friendly name (the configuration key defaults to the map
key). Each entry targets exactly one store: app_configuration_key references a store created in
this call by its map key, configuration_store_id an existing store by id. type kv stores value
directly (it lands in the store AND in Terraform state, so put secrets in Key Vault and use type
vault with vault_key_reference instead). Writing keys needs the App Configuration Data Owner role
on the store; a fresh grant is eventually consistent, so settle it (time_sleep) before the first
write.
map(object({
app_configuration_key = optional(string)
configuration_store_id = optional(string)

key = optional(string)
label = optional(string)
type = optional(string, "kv")
value = optional(string)
vault_key_reference = optional(string)
content_type = optional(string)
locked = optional(bool, false)
tags = optional(map(string), {})
}))
{} no
app_configurations App Configuration stores to create, keyed by store name. Full azurerm_app_configuration surface:
sku (free, developer, standard, premium), identity, customer-managed-key encryption, replicas
(geo-replication, keyed by replica name), soft delete and purge protection, and the ARM data
plane proxy settings.

Secure defaults: local_auth_enabled false (access keys disabled, data plane is Entra ID only,
which is also how this module writes keys and features). public_network_access defaults to
Enabled because the store has no IP firewall: to lock it down set it to Disabled and reach the
store through a private endpoint (private-endpoint module). Purge protection follows the
provider default (off); soft delete (7 days) still protects deleted stores, and disposable
stores stay reusable by name.
map(object({
sku = optional(string, "standard")
local_auth_enabled = optional(bool, false)
public_network_access = optional(string, "Enabled")
purge_protection_enabled = optional(bool, false)
soft_delete_retention_days = optional(number, 7)

data_plane_proxy_authentication_mode = optional(string)
data_plane_proxy_private_link_delegation_enabled = optional(bool)

identity = optional(object({
type = string
identity_ids = optional(list(string), [])
}))

encryption = optional(object({
key_vault_key_identifier = optional(string)
identity_client_id = optional(string)
}))

replicas = optional(map(object({
location = string
})), {})
}))
{} no
location Azure region for the stores. Only needed when app_configurations is non-empty; a data-only call (keys and features against existing stores) can omit it. string null no
resource_group_id Resource id of the resource group to create the stores in. The name and subscription are parsed from it (pass the rg module's ids output). Only needed when app_configurations is non-empty; a data-only call (keys and features against existing stores) can omit it. string null no
tags Tags applied to the stores. map(string) {} no

Outputs

Name Description
app_configuration_ids_zipmap Map of store name to an object of its name and resource id, for easy composition.
app_configurations Map of store name to its useful (non-sensitive) attributes.
endpoints Map of store name to its data-plane endpoint URL.
feature_ids Map of feature entry key to its resource id.
identities Map of store name to its managed identity (principal_id, tenant_id), null when the store has none.
ids Map of store name to resource id.
key_ids Map of key entry key to its resource id.
primary_read_keys Map of store name to its primary read access key (id, secret, connection_string). Empty objects when local auth is disabled.
primary_write_keys Map of store name to its primary write access key (id, secret, connection_string). Empty objects when local auth is disabled.
replica_endpoints Map of store name to a map of replica name to replica endpoint URL.
resource_group_name Resource group name parsed from resource_group_id, null on a data-only call.
secondary_read_keys Map of store name to its secondary read access key (id, secret, connection_string). Empty objects when local auth is disabled.
secondary_write_keys Map of store name to its secondary write access key (id, secret, connection_string). Empty objects when local auth is disabled.
subscription_id Subscription id parsed from resource_group_id, null on a data-only call.

About

🧩 Terraform module for Azure App Configuration: stores, geo-replicas, keys, and feature flags on the Entra ID data plane

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages