# App Configuration

Source: /azure/services/app-configuration/

## Introduction

Azure App Configuration is a managed service for centralizing application settings and feature flags.
It stores configuration as key-values that can be filtered by label, versioned through revisions, frozen into point-in-time snapshots, and referenced against secrets held in Azure Key Vault.
For more information, see [What is Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview).

LocalStack for Azure provides a local environment for building and testing applications that make use of Azure App Configuration.
The supported APIs are available on our [API Coverage section](#api-coverage), which provides information on the extent of App Configuration's integration with LocalStack.

## Getting started

This guide is designed for users new to App Configuration and assumes basic knowledge of the Azure CLI and our `lstk az` proxy. The [Observe RBAC enforcement](#observe-rbac-enforcement) section additionally requires [`jq`](https://jqlang.github.io/jq/) to read a claim out of an access token.

Launch LocalStack using your preferred method. For more information, see [Introduction to LocalStack for Azure](/azure/getting-started/). Once the container is running, enable Azure CLI interception by running:

```bash
lstk az start-interception
```

This command points the `az` CLI away from the public Azure management REST API and toward the LocalStack for Azure emulator API.
To revert this configuration, run:

```bash
lstk az stop-interception
```

This reconfigures the `az` CLI to send commands to the official Azure management REST API.

### Create a resource group

Create a resource group to hold all resources created in this guide:

```bash
az group create \
  --name rg-appconfig-demo \
  --location westeurope
```

```bash title="Output"
{
  "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-appconfig-demo",
  "location": "westeurope",
  "name": "rg-appconfig-demo",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "type": "Microsoft.Resources/resourceGroups"
}
```

### Create an App Configuration store

Create a configuration store. The store name is a DNS label: alphanumerics and hyphens only, between 5 and 50 characters, and globally unique:

```bash
az appconfig create \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --location westeurope \
  --sku Standard \
  --retention-days 7 \
  --tags environment=demo
```

```bash title="Output"
{
  "creationDate": "2026-09-16T14:11:29.455779+00:00",
  "defaultKeyValueRevisionRetentionPeriodInSeconds": 2592000,
  "disableLocalAuth": false,
  "enablePurgeProtection": false,
  "endpoint": "https://appconfig-demo-localstack.azure.localhost.localstack.cloud:4566",
  "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-appconfig-demo/providers/Microsoft.AppConfiguration/configurationStores/appconfig-demo-localstack",
  "location": "westeurope",
  "name": "appconfig-demo-localstack",
  "provisioningState": "Succeeded",
  "resourceGroup": "rg-appconfig-demo",
  "sku": {
    "name": "standard"
  },
  "softDeleteRetentionInDays": 7,
  "tags": {
    "environment": "demo"
  },
  "type": "Microsoft.AppConfiguration/configurationStores"
  ...
}
```

The `endpoint` property is the data-plane address that SDKs and configuration providers use verbatim.
Clients should read it back from the service rather than build it from the store name.

:::note
The SKU determines whether the store can be restored after deletion. `Free` and `Developer` stores are deleted permanently, while `Standard` and `Premium` stores are soft-deleted and can be recovered. This guide uses `Standard` so that the [soft-delete lifecycle](#delete-and-verify) works as documented.
:::

### Show and list stores

Retrieve a single store:

```bash
az appconfig show \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --query "{Name:name, Sku:sku.name, Endpoint:endpoint, Retention:softDeleteRetentionInDays}"
```

```bash title="Output"
{
  "Endpoint": "https://appconfig-demo-localstack.azure.localhost.localstack.cloud:4566",
  "Name": "appconfig-demo-localstack",
  "Retention": 7,
  "Sku": "standard"
}
```

List every store in a resource group:

```bash
az appconfig list \
  --resource-group rg-appconfig-demo \
  --query "[].{Name:name, Location:location, Sku:sku.name, State:provisioningState}" \
  --output table
```

```bash title="Output"
Name                       Location    Sku       State
-------------------------  ----------  --------  ---------
appconfig-demo-localstack  westeurope  standard  Succeeded
```

### Update a store

Update the store to change its tags:

```bash
az appconfig update \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --tags environment=demo owner=platform \
  --query "tags"
```

```bash title="Output"
{
  "environment": "demo",
  "owner": "platform"
}
```

:::note
`az appconfig update --tags` replaces the entire tag collection rather than merging into it. Any tag omitted from the command is removed from the store.
:::

### Manage access keys

Every store is created with four access keys: a primary and secondary pair with read and write access, and a read-only pair.
List them:

```bash
az appconfig credential list \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --query "[].{Name:name, Id:id, ReadOnly:readOnly}" \
  --output table
```

```bash title="Output"
Name                 Id                      ReadOnly
-------------------  ----------------------  ----------
Primary              MujyyVBRNGLykfLG3RkB1A  False
Secondary            DfbAmyIZEVAx9iE6KnBt9A  False
Primary Read Only    wxWGWZrgcR7VP8oyubuQ8Q  True
Secondary Read Only  LarYNCfPTx0XPIFzNsj_8g  True
```

Each key carries a ready-to-use connection string in the form `Endpoint=<endpoint>;Id=<id>;Secret=<secret>`.
Capture the secondary key's identifier and regenerate it:

```bash
SECONDARY_KEY_ID=$(az appconfig credential list \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --query "[?name=='Secondary'].id | [0]" \
  --output tsv)

az appconfig credential regenerate \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --id="$SECONDARY_KEY_ID" \
  --query "{Name:name, Id:id, ReadOnly:readOnly}"
```

```bash title="Output"
{
  "Id": "hSS0LdDUJPo7oR8BXT3BdA",
  "Name": "Secondary",
  "ReadOnly": false
}
```

Regenerating a key replaces the whole credential for that slot: both the identifier and the secret change, while the name and the read-only flag are preserved. Azure behaves the same way, so the identifier in the output above differs from the one that was listed. The other three keys are unaffected, so a running application using the primary key keeps working.

:::note
Access key identifiers are base64url encoded, so an identifier can begin with a hyphen. Pass it with the equals form, `--id="$SECONDARY_KEY_ID"`, or the Azure CLI may parse a leading hyphen as the start of another argument and fail with `argument --id: expected one argument`.
:::

### Set and read key-values

A key-value is identified by its key and an optional label. The same key can carry a different value under each label, which is how per-environment configuration is modelled:

```bash
az appconfig kv set \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --value blue \
  --content-type "text/plain" \
  --tags tier=frontend \
  --yes
```

```bash title="Output"
{
  "contentType": "text/plain",
  "etag": "fba4697ee4724387aa9b15f061d9b433",
  "key": "app/settings/color",
  "label": "production",
  "lastModified": "2026-09-16T14:11:33.819984+00:00",
  "locked": false,
  "tags": {
    "tier": "frontend"
  },
  "value": "blue"
}
```

Read a single key-value back:

```bash
az appconfig kv show \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production
```

```bash title="Output"
{
  "contentType": "text/plain",
  "etag": "fba4697ee4724387aa9b15f061d9b433",
  "key": "app/settings/color",
  "label": "production",
  "lastModified": "2026-09-16T14:11:33.819984+00:00",
  "locked": false,
  "tags": {
    "tier": "frontend"
  },
  "value": "blue"
}
```

Add a second key-value so the listing below has more than one entry:

```bash
az appconfig kv set \
  --name appconfig-demo-localstack \
  --key "app/settings/size" \
  --label production \
  --value large \
  --yes
```

```bash title="Output"
{
  "contentType": "",
  "etag": "75505fc1d5004e009e85563ec9b32f0c",
  "key": "app/settings/size",
  "label": "production",
  "lastModified": "2026-09-16T14:11:34.703800+00:00",
  "locked": false,
  "tags": {},
  "value": "large"
}
```

List key-values, filtering by a key prefix. Both `--key` and `--label` accept a star as a wildcard, and `--label` also accepts a comma-separated list:

```bash
az appconfig kv list \
  --name appconfig-demo-localstack \
  --key "app/settings/*" \
  --label production \
  --all \
  --output table
```

```bash title="Output"
CONTENT TYPE    KEY                 VALUE    LAST MODIFIED         TAGS                  LABEL       LOCKED
--------------  ------------------  -------  --------------------  --------------------  ----------  --------
text/plain      app/settings/color  blue     2026-09-16T14:11:33Z  {'tier': 'frontend'}  production  False
                app/settings/size   large    2026-09-16T14:11:34Z  {}                    production  False
```

Every write creates a new revision. Update `app/settings/color`:

```bash
az appconfig kv set \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --value red \
  --yes \
  --query "value"
```

```bash title="Output"
"red"
```

Both versions are now visible, newest first:

```bash
az appconfig revision list \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --all \
  --output table
```

```bash title="Output"
CONTENT TYPE    KEY                 VALUE    LAST MODIFIED         TAGS                  LABEL       LOCKED
--------------  ------------------  -------  --------------------  --------------------  ----------  --------
text/plain      app/settings/color  red      2026-09-16T14:11:37Z  {'tier': 'frontend'}  production  False
text/plain      app/settings/color  blue     2026-09-16T14:11:33Z  {'tier': 'frontend'}  production  False
```

### Lock and unlock a key-value

Locking a key-value makes it read-only, which protects a setting from accidental change:

```bash
az appconfig kv lock \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --yes \
  --query "locked"
```

```bash title="Output"
true
```

A write to a locked key-value is rejected:

```bash
az appconfig kv set \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --value green \
  --yes
```

```bash title="Output"
ERROR: Failed to update read only key-value. Unlock the key-value before updating it.
```

Unlock it to allow writes again:

```bash
az appconfig kv unlock \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --yes \
  --query "locked"
```

```bash title="Output"
false
```

### Read key-values as of a past instant

Because every write is retained as a revision, the store can be read as it stood at an earlier moment.
Capture the current key-value's timestamp and trim it to whole seconds, which is the only precision `--datetime` accepts:

```bash
LAST_MODIFIED=$(az appconfig kv show \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --query lastModified \
  --output tsv)

CUTOFF="${LAST_MODIFIED%%.*}"
CUTOFF="${CUTOFF%%+*}Z"
echo "$CUTOFF"
```

```bash title="Output"
2026-09-16T14:11:37Z
```

Wait for the next second before writing again. A whole-second cutoff covers the whole of that second, so a write made inside it would fall within the window being queried and would be returned instead of the earlier value:

```bash
sleep 1
```

Change the value, so there is something to look back past:

```bash
az appconfig kv set \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --value amber \
  --yes \
  --query "value"
```

```bash title="Output"
"amber"
```

Pass the captured instant to `--datetime` to read the store as it stood before that change:

```bash
az appconfig kv list \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --datetime "$CUTOFF" \
  --output table
```

```bash title="Output"
CONTENT TYPE    KEY                 VALUE    LAST MODIFIED         TAGS                  LABEL       LOCKED
--------------  ------------------  -------  --------------------  --------------------  ----------  --------
text/plain      app/settings/color  red      2026-09-16T14:11:37Z  {'tier': 'frontend'}  production  False
```

The same listing without `--datetime` returns the current value:

```bash
az appconfig kv list \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --output table
```

```bash title="Output"
CONTENT TYPE    KEY                 VALUE    LAST MODIFIED         TAGS                  LABEL       LOCKED
--------------  ------------------  -------  --------------------  --------------------  ----------  --------
text/plain      app/settings/color  amber    2026-09-16T14:11:42Z  {'tier': 'frontend'}  production  False
```

:::note
`--datetime` accepts only whole seconds, in the format `YYYY-MM-DDThh:mm:ss` with an optional `Z` or timezone offset. A whole-second value covers the whole of that second, so a cutoff copied from a key-value's own `lastModified` returns that write rather than the one before it.
:::

### Create and manage snapshots

A snapshot freezes the key-values matching a filter at the moment it is created, giving an application a configuration set that cannot shift underneath it:

```bash
az appconfig snapshot create \
  --name appconfig-demo-localstack \
  --snapshot-name baseline \
  --filters '{"key":"app/settings/*","label":"production"}'
```

```bash title="Output"
{
  "compositionType": "key",
  "created": "2026-09-16T14:11:43.730861+00:00",
  "etag": "49c270f7426ed076c6387c4bdc45afaa",
  "expires": null,
  "filters": [
    {
      "key": "app/settings/*",
      "label": "production"
    }
  ],
  "itemsCount": 2,
  "name": "baseline",
  "retentionPeriod": 2592000,
  "size": 1000,
  "status": "ready",
  "tags": {}
}
```

:::note
The label in a snapshot filter is matched exactly, and omitting it selects the *null* label rather than every label. This is the opposite of `az appconfig kv list`, where an omitted label means any label. A wildcard label is rejected with `Unexpected label which could match multiple values for a key`. A filter that matches nothing still produces a `ready` snapshot, so check `itemsCount` to confirm the filter was correct.
:::

List the snapshots on a store:

```bash
az appconfig snapshot list \
  --name appconfig-demo-localstack \
  --query "[].{Name:name, Status:status, Items:itemsCount, Created:created}" \
  --output table
```

```bash title="Output"
Name      Status    Items    Created
--------  --------  -------  --------------------------------
baseline  ready     2        2026-09-16T14:11:43.730861+00:00
```

A snapshot is materialized once, when it is created, so changing a key-value afterwards does not affect it.
Change `app/settings/color` again:

```bash
az appconfig kv set \
  --name appconfig-demo-localstack \
  --key "app/settings/color" \
  --label production \
  --value violet \
  --yes \
  --query "value"
```

```bash title="Output"
"violet"
```

The store now holds `violet`, but listing through the snapshot still returns the value captured when it was created:

```bash
az appconfig kv list \
  --name appconfig-demo-localstack \
  --snapshot baseline \
  --output table
```

```bash title="Output"
CONTENT TYPE    KEY                 VALUE    LAST MODIFIED         TAGS                  LABEL       LOCKED
--------------  ------------------  -------  --------------------  --------------------  ----------  --------
text/plain      app/settings/color  amber    2026-09-16T14:11:42Z  {'tier': 'frontend'}  production  False
                app/settings/size   large    2026-09-16T14:11:34Z  {}                    production  False
```

Archive a snapshot to mark it for expiry, and recover it to cancel that:

```bash
az appconfig snapshot archive \
  --name appconfig-demo-localstack \
  --snapshot-name baseline \
  --query "{Status:status, Expires:expires}"
```

```bash title="Output"
{
  "Expires": "2026-10-16T14:11:53.597151+00:00",
  "Status": "archived"
}
```

```bash
az appconfig snapshot recover \
  --name appconfig-demo-localstack \
  --snapshot-name baseline \
  --query "{Status:status, Expires:expires}"
```

```bash title="Output"
{
  "Expires": null,
  "Status": "ready"
}
```

An archived snapshot still serves its contents. Archiving starts the retention clock; recovering stops it.

### Reference a Key Vault secret

Rather than storing a secret in App Configuration, store a reference to it in Key Vault.
Create a vault that uses Azure RBAC, which is the permission model the `Key Vault Secrets User` role requires:

```bash
az keyvault create \
  --name kv-appconfig-demo \
  --resource-group rg-appconfig-demo \
  --location westeurope \
  --enable-rbac-authorization true \
  --retention-days 7 \
  --query "{Name:name, Uri:properties.vaultUri, Rbac:properties.enableRbacAuthorization}"
```

```bash title="Output"
{
  "Name": "kv-appconfig-demo",
  "Rbac": true,
  "Uri": "https://kv-appconfig-demo.vault.azure.localhost.localstack.cloud:4566"
}
```

Writing a secret to an RBAC-enabled vault requires the `Key Vault Secrets Officer` role on the vault.
Resolve the object ID of the principal you are signed in as, then grant it that role:

```bash
KEY_VAULT_ID=$(az keyvault show \
  --name kv-appconfig-demo \
  --resource-group rg-appconfig-demo \
  --query id \
  --output tsv)

CALLER_OBJECT_ID=$(az ad signed-in-user show --query id --output tsv 2>/dev/null \
  || az ad sp show --id "$(az account show --query user.name --output tsv)" --query id --output tsv)

az role assignment create \
  --assignee-object-id "$CALLER_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets Officer" \
  --scope "$KEY_VAULT_ID" \
  --query "roleDefinitionName" \
  --output tsv
```

```bash title="Output"
Key Vault Secrets Officer
```

:::note
Use `--assignee-principal-type User` instead when you are signed in as a user rather than as a service principal. On real Azure the assignment takes a few moments to propagate, so a `secret set` that fails with a `403` immediately afterwards usually succeeds on a retry.
:::

Write the secret and capture the identifier it returns:

```bash
SECRET_ID=$(az keyvault secret set \
  --vault-name kv-appconfig-demo \
  --name db-password \
  --value "P@ssw0rd-from-key-vault" \
  --query "id" \
  --output tsv)

echo "$SECRET_ID"
```

```bash title="Output"
https://kv-appconfig-demo.vault.azure.localhost.localstack.cloud:4566/secrets/db-password/587b6bfb287d49a9bc498eca45f61cd0
```

Create the reference using that secret identifier:

```bash
az appconfig kv set-keyvault \
  --name appconfig-demo-localstack \
  --key "app/secrets/db-password" \
  --label production \
  --secret-identifier "$SECRET_ID" \
  --yes
```

```bash title="Output"
{
  "contentType": "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8",
  "etag": "2c01f41547944aecad34f963c5477324",
  "key": "app/secrets/db-password",
  "label": "production",
  "lastModified": "2026-09-16T14:11:57.108598+00:00",
  "locked": false,
  "tags": {},
  "value": "{\"uri\": \"https://kv-appconfig-demo.vault.azure.localhost.localstack.cloud:4566/secrets/db-password/587b6bfb287d49a9bc498eca45f61cd0\"}"
}
```

A Key Vault reference is an ordinary key-value whose content type is `application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8` and whose value is a JSON document holding the secret identifier.
Reading it back plainly returns that identifier, not the secret.
Pass `--resolve-keyvault` to have the CLI fetch the secret from Key Vault with your credentials:

```bash
az appconfig kv list \
  --name appconfig-demo-localstack \
  --key "app/secrets/db-password" \
  --label production \
  --resolve-keyvault \
  --all \
  --output table
```

```bash title="Output"
CONTENT TYPE                                                        KEY                      VALUE                    LAST MODIFIED         TAGS    LABEL       LOCKED
------------------------------------------------------------------  -----------------------  -----------------------  --------------------  ------  ----------  --------
application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8  app/secrets/db-password  P@ssw0rd-from-key-vault  2026-09-16T14:11:57Z  {}      production  False
```

:::note
Resolution happens in the client, not in the service, so the caller needs read access to the secret in Key Vault as well as to the store. Do not combine `--resolve-keyvault` with `--fields`: `--fields` makes the CLI request only the named fields from the service, and it then cannot build a reference whose value was not returned. Use `--query` to shape the output instead. A secret identifier that includes a version pins the reference to that version, while one without a version always follows the latest.
:::

### Control data-plane access with RBAC

An application should read configuration with its own identity rather than an access key.
Create a user-assigned managed identity and capture its principal ID, which is what role assignments are evaluated against:

```bash
az identity create \
  --name id-appconfig-demo \
  --resource-group rg-appconfig-demo \
  --location westeurope \
  --query "{Name:name, ClientId:clientId, PrincipalId:principalId}"
```

```bash title="Output"
{
  "ClientId": "5886b11c-c8aa-41c7-b5e4-28a74405fd3e",
  "Name": "id-appconfig-demo",
  "PrincipalId": "6828983e-8850-42cc-904a-c1e04dce057f"
}
```

```bash
IDENTITY_PRINCIPAL_ID=$(az identity show \
  --name id-appconfig-demo \
  --resource-group rg-appconfig-demo \
  --query principalId \
  --output tsv)

STORE_ID=$(az appconfig show \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --query id \
  --output tsv)
```

Grant the identity the `App Configuration Data Reader` role at the scope of the store.
The assignment targets the principal ID, not the client ID:

```bash
az role assignment create \
  --assignee-object-id "$IDENTITY_PRINCIPAL_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "App Configuration Data Reader" \
  --scope "$STORE_ID" \
  --query "roleDefinitionName" \
  --output tsv
```

```bash title="Output"
App Configuration Data Reader
```

Verify the assignment:

```bash
az role assignment list \
  --assignee "$IDENTITY_PRINCIPAL_ID" \
  --scope "$STORE_ID" \
  --query "[].{Role:roleDefinitionName, PrincipalType:principalType}" \
  --output table
```

```bash title="Output"
Role                           PrincipalType
-----------------------------  ----------------
App Configuration Data Reader  ServicePrincipal
```

An application running in Azure obtains a token for this identity from the instance metadata service, and that token carries the principal ID above.

### Observe RBAC enforcement

By default LocalStack stores role assignments without enforcing them, so every data-plane request succeeds.
Enforcement is enabled by an environment variable read once at startup, so it requires restarting the emulator, which clears the in-memory state created so far.
The sequence below is therefore self-contained: it starts from a fresh emulator and creates its own resource group and store.

:::note
Everything created in the preceding sections is lost when the emulator restarts. Run this section on its own, or re-create those resources afterwards.
:::

Restart the emulator with enforcement enabled, then re-enable interception:

```bash
localstack stop
IMAGE_NAME=localstack/localstack-azure localstack start -d -e LS_AZURE_ENFORCE_RBAC=1
lstk az start-interception
```

Create a store and seed a key-value using an access key, which is never subject to RBAC:

```bash
az group create --name rg-appconfig-rbac-demo --location westeurope --output none

az appconfig create \
  --name appconfig-rbac-localstack \
  --resource-group rg-appconfig-rbac-demo \
  --location westeurope \
  --sku Standard \
  --output none

az appconfig kv set \
  --name appconfig-rbac-localstack \
  --key "app/settings/color" \
  --value blue \
  --auth-mode key \
  --yes \
  --query "value"
```

```bash title="Output"
"blue"
```

Capture the store's identifiers, then sign in as a second service principal that holds no administrative role.
The Azure CLI cannot acquire a managed identity token outside Azure compute, so a plain service principal stands in for the workload here:

```bash
STORE_ID=$(az appconfig show \
  --name appconfig-rbac-localstack \
  --resource-group rg-appconfig-rbac-demo \
  --query id \
  --output tsv)

STORE_ENDPOINT=$(az appconfig show \
  --name appconfig-rbac-localstack \
  --resource-group rg-appconfig-rbac-demo \
  --query endpoint \
  --output tsv)

az login --service-principal -u appcs-workload -p any-pass --tenant any-tenant --output none
```

Read a key-value through the data-plane endpoint with `--auth-mode login`. A caller holding no data role is refused:

```bash
az appconfig kv show \
  --endpoint "$STORE_ENDPOINT" \
  --key "app/settings/color" \
  --auth-mode login
```

```bash title="Output"
ERROR: Failed to retrieve key-values from config store. Operation returned an invalid status 'FORBIDDEN'
Content: {"type": "https://azconfig.io/errors/forbidden", "title": "Forbidden.", "detail": "The principal 'b70f3654-c056-4bfa-8c6d-620e27ff4a92' does not have the required permission 'Microsoft.AppConfiguration/configurationStores/keyValues/read' on '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-appconfig-rbac-demo/providers/Microsoft.AppConfiguration/configurationStores/appconfig-rbac-localstack'.", "status": 403}
```

The denial names the principal, the permission it lacked, and the scope it was evaluated at.
Capture that principal's object ID from the `oid` claim of its own access token, which is what a role assignment must target:

```bash
WORKLOAD_OBJECT_ID=$(az account get-access-token --query accessToken --output tsv \
  | jq -Rr 'split(".")[1] | @base64d | fromjson | .oid')

echo "$WORKLOAD_OBJECT_ID"
```

```bash title="Output"
b70f3654-c056-4bfa-8c6d-620e27ff4a92
```

Sign back in as the administrative principal and grant the workload the `App Configuration Data Reader` role:

```bash
az login --service-principal -u any-app -p any-pass --tenant any-tenant --output none

az role assignment create \
  --assignee-object-id "$WORKLOAD_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "App Configuration Data Reader" \
  --scope "$STORE_ID" \
  --query "roleDefinitionName" \
  --output tsv
```

```bash title="Output"
App Configuration Data Reader
```

Sign back in as the workload. The read now succeeds:

```bash
az login --service-principal -u appcs-workload -p any-pass --tenant any-tenant --output none

az appconfig kv show \
  --endpoint "$STORE_ENDPOINT" \
  --key "app/settings/color" \
  --query "value" \
  --output tsv \
  --auth-mode login
```

```bash title="Output"
blue
```

A write is still refused, because `App Configuration Data Reader` grants no write permission:

```bash
az appconfig kv set \
  --endpoint "$STORE_ENDPOINT" \
  --key "app/settings/color" \
  --value red \
  --auth-mode login \
  --yes
```

```bash title="Output"
ERROR: Failed to set the key-value due to an exception: Operation returned an invalid status 'FORBIDDEN'
Content: {"type": "https://azconfig.io/errors/forbidden", "title": "Forbidden.", "detail": "The principal 'b70f3654-c056-4bfa-8c6d-620e27ff4a92' does not have the required permission 'Microsoft.AppConfiguration/configurationStores/keyValues/write' on '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-appconfig-rbac-demo/providers/Microsoft.AppConfiguration/configurationStores/appconfig-rbac-localstack'.", "status": 403}
```

Assigning `App Configuration Data Owner` instead allows both reads and writes:

```bash
az login --service-principal -u any-app -p any-pass --tenant any-tenant --output none

az role assignment create \
  --assignee-object-id "$WORKLOAD_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "App Configuration Data Owner" \
  --scope "$STORE_ID" \
  --query "roleDefinitionName" \
  --output tsv

az login --service-principal -u appcs-workload -p any-pass --tenant any-tenant --output none

az appconfig kv set \
  --endpoint "$STORE_ENDPOINT" \
  --key "app/settings/color" \
  --value red \
  --auth-mode login \
  --yes \
  --query "value"
```

```bash title="Output"
"red"
```

Access keys are not affected by role assignments. A client authenticating with a connection string is authenticated by the key it signed with and is never evaluated against RBAC, exactly as in Azure.

Sign back in as the administrative principal before cleaning up, because the workload principal is not authorized to delete these resources:

```bash
az login --service-principal -u any-app -p any-pass --tenant any-tenant --output none

az appconfig delete \
  --name appconfig-rbac-localstack \
  --resource-group rg-appconfig-rbac-demo \
  --yes

az appconfig purge --name appconfig-rbac-localstack --yes

az group delete --name rg-appconfig-rbac-demo --yes
```

:::note
Address the data plane with `--endpoint` rather than `--name` when using `--auth-mode login`. With `--name`, the CLI first resolves the store through Azure Resource Manager, which requires the separate control-plane permission `Microsoft.AppConfiguration/configurationStores/read`. Azure behaves the same way. Note also that App Configuration has no `enableRbacAuthorization` switch: Microsoft Entra data-plane RBAC is always active on a store, and only enforcement inside the emulator is opt-in. For more information, see [Role Assignment: Enabling RBAC enforcement](/azure/services/role-assignment/#enabling-rbac-enforcement).
:::

### Delete and verify

Delete an individual key-value:

```bash
az appconfig kv delete \
  --name appconfig-demo-localstack \
  --key "app/settings/size" \
  --label production \
  --yes \
  --query "[].key"
```

```bash title="Output"
[
  "app/settings/size"
]
```

Delete the store. On the `Standard` tier this is a soft delete:

```bash
az appconfig delete \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --yes
```

The store no longer appears in `az appconfig list`, but it is retained until its scheduled purge date:

```bash
az appconfig show-deleted --name appconfig-demo-localstack
```

```bash title="Output"
{
  "configurationStoreId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-appconfig-demo/providers/Microsoft.AppConfiguration/configurationStores/appconfig-demo-localstack",
  "deletionDate": "2026-09-16T14:12:02.287739+00:00",
  "id": "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.AppConfiguration/locations/westeurope/deletedConfigurationStores/appconfig-demo-localstack",
  "location": "westeurope",
  "name": "appconfig-demo-localstack",
  "purgeProtectionEnabled": false,
  "scheduledPurgeDate": "2026-09-23T14:12:02.287739+00:00",
  "systemData": null,
  "tags": {
    "environment": "demo",
    "owner": "platform"
  },
  "type": "Microsoft.AppConfiguration/deletedConfigurationStores"
}
```

Restore it with `az appconfig recover`, which takes no resource group because a deleted store is addressed at subscription scope:

```bash
az appconfig recover --name appconfig-demo-localstack --yes
```

To remove a store permanently, delete it and then purge it. A soft-deleted store keeps its globally unique name reserved, so purging is what allows the same name to be reused:

```bash
az appconfig delete \
  --name appconfig-demo-localstack \
  --resource-group rg-appconfig-demo \
  --yes

az appconfig purge --name appconfig-demo-localstack --yes
```

Finally, remove the resource group and confirm it is gone:

```bash
az group delete --name rg-appconfig-demo --yes

az group exists --name rg-appconfig-demo
```

```bash title="Output"
false
```

## Features

The App Configuration emulator supports the following features:

- **Configuration stores**: Create, get, list by subscription and by resource group, update, and delete stores, across the `Free`, `Developer`, `Standard`, and `Premium` SKUs.
- **Soft delete and purge**: The full lifecycle of deleting, listing and showing deleted stores, recovering them, and purging them, including purge protection and a configurable retention period.
- **Access keys**: List the four access keys of a store, regenerate any of them individually, and use the generated connection strings.
- **Key-values**: Set, show, list, and delete key-values, with content types, tags, and labels.
- **Filtering**: Filter listings by key and label, using exact values, star wildcards, or comma-separated lists, and project fields with `--fields`.
- **Revisions**: Every write is retained as a revision, listed newest first and trimmed according to the store's retention period.
- **Point-in-time reads**: Read key-values, keys, labels, and revisions as they stood at an earlier instant with `--datetime`.
- **Snapshots**: Create snapshots over a key and label filter, show and list them, archive and recover them, and list a snapshot's frozen contents.
- **Locks**: Lock a key-value to make it read-only and unlock it to restore writes.
- **Feature flags**: Set, show, list, enable, and disable feature flags, including percentage and other filters, stored as key-values under the `.appconfig.featureflag/` prefix.
- **Key Vault references**: Store references to Key Vault secrets with the standard reference content type, resolved client-side by the CLI or a configuration provider.
- **Import and export**: Move key-values between a store and a file with `az appconfig kv import` and `az appconfig kv export`.
- **Data-plane RBAC**: Evaluate `App Configuration Data Reader` and `App Configuration Data Owner` against bearer callers, including managed identities, when enforcement is enabled.
- **Access key authentication**: Requests signed with an access key are accepted, and a read-only key is refused write operations.
- **Replicas**: Create, get, list, and delete replicas. A replica endpoint serves the contents of its parent store.
- **Private endpoints**: The full Azure Resource Manager lifecycle of private endpoint connections, driven by `Microsoft.Network`, including auto-approval, rejection, and cascading deletion.
- **Pagination and conditional requests**: Cursor-based pagination over large listings, ETag preconditions on individual key-values, conditional list requests, and `Range` requests over revisions.

## Limitations

- **Data-plane RBAC is not enforced by default**: Role assignments are stored but every request succeeds. Set `LS_AZURE_ENFORCE_RBAC` when starting the emulator to enforce them. See [Role Assignment: Enabling RBAC enforcement](/azure/services/role-assignment/#enabling-rbac-enforcement).
- **Access key signatures are not verified**: The credential identifier in a request is resolved and checked, but the signature itself, the date skew, and the content hash are not. The emulator accepts requests that Azure would reject, never the reverse.
- **Private endpoints do not isolate traffic**: Connections are created, approved, rejected, and reported on the store, but no request is blocked and `publicNetworkAccess` is left as the client set it. The per-tier limit on the number of connections is not enforced.
- **Replicas do not replicate**: A replica endpoint serves the same single store, so there is no replication lag, no regional failover, and no region-specific outage.
- **Snapshot behavior differs in several details**: Azure Resource Manager exposes only snapshot creation and retrieval, so listing, archiving, and recovering are available on the data plane alone. A snapshot filter is returned without the empty `tags` field that Azure includes. Snapshot creation is synchronous, so a snapshot is already `ready` in the response rather than reaching that state asynchronously, and the reported `size` is approximate.
- **Customer-managed keys are cosmetic**: The `encryption.keyVaultProperties` settings are stored and returned, but no data is encrypted with them.
- **Not implemented**: Event Grid filters, private endpoint connection proxies, network security perimeter configurations, and request throttling.
- **The `api-version` parameter is required**: Data-plane requests that omit it are rejected, while Azure serves them. This makes the emulator stricter than the service it emulates.
- **Terraform cannot manage key-values**: The `azurerm_app_configuration_key` and `azurerm_app_configuration_feature` resources require an App Configuration domain suffix that the Azure Resource Manager metadata document does not carry, so the provider cannot resolve it for any non-public environment. Use the Azure CLI or Bicep to manage key-values. The `azurerm_app_configuration` store resource itself works.
- **No data persistence across restarts**: Store, key-value, revision, and snapshot data is held in memory and is lost when the LocalStack emulator is stopped or restarted.

## Samples

The following samples demonstrate how to use Azure App Configuration with LocalStack for Azure:

- [Web App, App Configuration, and Key Vault (Python)](https://github.com/localstack/localstack-azure-samples/blob/main/samples/web-app-app-configuration/python/README.md)
- [Web App, App Configuration, and Key Vault (.NET)](https://github.com/localstack/localstack-azure-samples/blob/main/samples/web-app-app-configuration/dotnet/README.md)
- [Azure Kubernetes Service, App Configuration, and Key Vault (Python)](https://github.com/localstack-samples/aks-samples/blob/main/samples/web-app-app-configuration/python/README.md)
- [Azure Kubernetes Service, App Configuration, and Key Vault (.NET)](https://github.com/localstack-samples/aks-samples/blob/main/samples/web-app-app-configuration/dotnet/README.md)

## API Coverage


### App Configuration API coverage

Source service: `app-configuration`. 51 of 51 tracked operations are implemented.

Service documentation: /azure/services/app-configuration/

| Operation | Status |
| --- | --- |
| ConfigurationStores.Create | Implemented |
| ConfigurationStores.Delete | Implemented |
| ConfigurationStores.Get | Implemented |
| ConfigurationStores.GetDeleted | Implemented |
| ConfigurationStores.List | Implemented |
| ConfigurationStores.ListByResourceGroup | Implemented |
| ConfigurationStores.ListDeleted | Implemented |
| ConfigurationStores.ListKeyValue | Implemented |
| ConfigurationStores.ListKeys | Implemented |
| ConfigurationStores.PurgeDeleted | Implemented |
| ConfigurationStores.RegenerateKey | Implemented |
| ConfigurationStores.Update | Implemented |
| KeyValues.CreateOrUpdate | Implemented |
| KeyValues.Delete | Implemented |
| KeyValues.Get | Implemented |
| Microsoft.AppConfiguration.CheckKeyValue | Implemented |
| Microsoft.AppConfiguration.CheckKeyValues | Implemented |
| Microsoft.AppConfiguration.CheckKeys | Implemented |
| Microsoft.AppConfiguration.CheckLabels | Implemented |
| Microsoft.AppConfiguration.CheckRevisions | Implemented |
| Microsoft.AppConfiguration.CheckSnapshot | Implemented |
| Microsoft.AppConfiguration.CheckSnapshots | Implemented |
| Microsoft.AppConfiguration.CreateSnapshot | Implemented |
| Microsoft.AppConfiguration.DeleteKeyValue | Implemented |
| Microsoft.AppConfiguration.DeleteLock | Implemented |
| Microsoft.AppConfiguration.GetKeyValue | Implemented |
| Microsoft.AppConfiguration.GetKeyValues | Implemented |
| Microsoft.AppConfiguration.GetKeys | Implemented |
| Microsoft.AppConfiguration.GetLabels | Implemented |
| Microsoft.AppConfiguration.GetOperationDetails | Implemented |
| Microsoft.AppConfiguration.GetRevisions | Implemented |
| Microsoft.AppConfiguration.GetSnapshot | Implemented |
| Microsoft.AppConfiguration.GetSnapshots | Implemented |
| Microsoft.AppConfiguration.PutKeyValue | Implemented |
| Microsoft.AppConfiguration.PutLock | Implemented |
| Microsoft.AppConfiguration.UpdateSnapshot | Implemented |
| Operations.CheckNameAvailability | Implemented |
| Operations.List | Implemented |
| Operations.RegionalCheckNameAvailability | Implemented |
| PrivateEndpointConnections.CreateOrUpdate | Implemented |
| PrivateEndpointConnections.Delete | Implemented |
| PrivateEndpointConnections.Get | Implemented |
| PrivateEndpointConnections.ListByConfigurationStore | Implemented |
| PrivateLinkResources.Get | Implemented |
| PrivateLinkResources.ListByConfigurationStore | Implemented |
| Replicas.Create | Implemented |
| Replicas.Delete | Implemented |
| Replicas.Get | Implemented |
| Replicas.ListByConfigurationStore | Implemented |
| Snapshots.Create | Implemented |
| Snapshots.Get | Implemented |
