This directory contains Terraform modules and a deployment script for provisioning the sample's Azure resources. For details about the sample application, see Azure Web App with Azure App Configuration and Azure Key Vault.
- LocalStack for Azure
- Terraform (1.5+)
- Docker
- Azure CLI + lstk CLI
- Python 3.12+
- PostgreSQL client (
psql) jq
brew install localstack/tap/lstk # or: npm install -g @localstack/lstkThe Terraform configuration uses three providers: azurerm 5.1.0 for every Azure resource, AzAPI 2.12.0 for the App Configuration key-values only, and time for one propagation wait. It provisions:
- Azure Resource Group.
- Azure Virtual Network with two subnets:
- app-subnet (delegated to
Microsoft.Web/serverFarmsfor the Web App's VNet integration) - pe-subnet (hosts the three Private Endpoints)
- app-subnet (delegated to
- User-assigned managed identity
<prefix>-identity-<suffix>(modulemanaged_identity,azurerm_user_assigned_identity; outputs the client id the Web App receives and the principal id the role assignments are keyed on). - Azure Key Vault
<prefix>-keyvault-<suffix>(modulekey_vault): the vault (azurerm_key_vault, Azure RBAC permission model, soft delete, public network access for the deployment, no purge protection), oneazurerm_key_vault_secretper entry of its sensitivesecretsmap (the root passespg-userandpg-passwordfrom the variablespg_app_userandpg_app_password), and, whendeployer_object_idis set, the Key Vault Secrets Officer assignment for the deploying principal followed by atime_sleepofrbac_propagation_delaybefore the secrets are written:azurerm_key_vault_secretuses the Key Vault data plane, which an RBAC vault authorizes only through such an assignment. The module outputs the versionless identifier of every secret and creates the vault's diagnostic settings. - Azure App Configuration store
<prefix>-appconfig-<suffix>(moduleapp_configuration): the store (azurerm_app_configuration, standard SKU, access keys enabled, public network access for the deployment, soft delete, no purge protection), oneazapi_resourceof typeMicrosoft.AppConfiguration/configurationStores/keyValuesper entry of itskey_valuesmap, and its diagnostic settings. The root passes the five key-values:PG_HOST,PG_PORTandPG_DATABASEas plain values,PG_USERandPG_PASSWORDas Key Vault references (content typeapplication/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8, valuejsonencode({ uri = <versionless secret identifier> })from the Key Vault module outputs). No label. - The identity's role assignments (module
role_assignment, once per grant): App Configuration Data Reader on the store and Key Vault Secrets User on the vault, on the identity's principal id withprincipal_type = "ServicePrincipal". - Every configurable value of the modules is a variable in the module's
variables.tfwith a description and a default, and every value the root passes is a variable of the rootvariables.tf(SKUs, network access, soft-delete retention, secret and key names, role names, Private DNS zone names, private endpoint sub-resources, diagnostic categories); no resource definition carries a literal. - Three Azure Private DNS Zones,
privatelink.postgres.database.azure.com,privatelink.azconfig.ioandprivatelink.vaultcore.azure.net, each linked to the VNet aslink-to-vnet. - Three Azure Private Endpoints, groups
postgresqlServer,configurationStoresandvault, each with adefaultDNS zone group. - Azure NAT Gateway and one Network Security Group per subnet.
- Azure Log Analytics Workspace and the diagnostic settings of every resource, the store and the vault included.
- Azure Database for PostgreSQL flexible server: public-access mode, Burstable
Standard_B1ms, version 16, 32 GiB, HA disabled, the permissiveAllowAllIPsfirewall rule and thePlannerDBdatabase. - Azure App Service Plan.
- Azure Web App with regional VNet integration, the user-assigned identity (
identity { type = "UserAssigned" }), and the app settingsEndpoints__AppConfiguration(the store endpoint) andAZURE_CLIENT_ID(the identity client id) next toLOGIN_NAME,WEBSITES_PORTand the Oryx build flags. NoPG_*app setting is written by Terraform or bydeploy.sh: the app loads the five settings from the store.
Why AzAPI for the key-values: the azurerm resources azurerm_app_configuration_key and azurerm_app_configuration_feature write the key-value through the store's data plane and then need the App Configuration DNS suffix of the cloud environment to read it back, a value that an environment discovered from a metadata endpoint, the way Terraform reaches the LocalStack emulator, never carries, so the apply fails after the write and leaves the resource in state. The ARM child resource Microsoft.AppConfiguration/configurationStores/keyValues is what Bicep uses too, and AzAPI drives it through Azure Resource Manager alone on both targets. See Terraform (AzAPI) form of configurationStores/keyValues and the comments in providers.tf. The ARM path is subject to the store's Azure Resource Manager authentication mode: with the default Local mode and access keys enabled the deploying principal only needs Contributor.
providers.tf hardcodes no endpoint and no subscription. deploy.sh exports the variables the providers read natively, so the same files run unmodified against both targets:
| Variable | Exported when | Value |
|---|---|---|
ARM_SUBSCRIPTION_ID, ARM_TENANT_ID |
always | az account show |
ARM_METADATA_HOSTNAME |
active az cloud is LocalStack |
host of az cloud show --query endpoints.resourceManager |
ARM_RESOURCE_MANAGER_ENDPOINT |
active az cloud is LocalStack |
az cloud show --query endpoints.resourceManager |
ARM_ACTIVE_DIRECTORY_AUTHORITY_HOST |
active az cloud is LocalStack |
az cloud show --query endpoints.activeDirectory |
ARM_RESOURCE_MANAGER_AUDIENCE |
active az cloud is LocalStack |
az cloud show --query endpoints.activeDirectoryResourceId |
ARM_DISABLE_INSTANCE_DISCOVERY, ARM_SKIP_PROVIDER_REGISTRATION |
active az cloud is LocalStack |
true |
azurerm discovers every endpoint from ARM_METADATA_HOSTNAME; AzAPI has no metadata discovery and takes the three ARM_RESOURCE_MANAGER_* and authority variables. Both authenticate with the Azure CLI session (use_cli defaults to true), which lstk az start-interception points at the emulator. Against Azure none of the emulator variables is exported and both providers use their public cloud defaults.
deploy.sh performs:
- Exports the provider variables above for the active
az cloud. - Resolves the object id of the deploying principal (
az ad signed-in-user showfor users,az ad sp show --id <appId>for service principals) and passes it asdeployer_object_id; when Microsoft Graph cannot resolve it, the Key Vault Secrets Officer assignment is skipped with a warning and the role becomes a prerequisite. terraform init -upgradeterraform plan -out=tfplan(passingprefix,suffix,location,pg_admin_login,pg_admin_password,pg_app_user,pg_app_password,deployer_object_id,deployer_principal_typeandrbac_propagation_delay,0son the emulator and60son Azure)terraform apply -auto-approve tfplan- Reads the outputs (
resource_group_name,web_app_name,postgres_server_name,postgres_fqdn,postgres_database_name,app_configuration_name,app_configuration_endpoint,key_vault_name) and checks that the store holds the five expected key-values. - Waits for the
PlannerDBdatabase to accept connections, then connects to the server as the admin via the public endpoint and the firewall rule, creates thetestuserrole, grants schema rights, creates theactivitiestable and seeds the rows. The role credentials are the values already stored in Key Vault; nothing is written to the Web App's app settings. - Prints the key-values with their content types and the Web App's app settings.
- Zips the source under
../srcand deploys it viaaz webapp deploy.
deploy.sh accepts the environment overrides PREFIX, SUFFIX, LOCATION, PG_ADMIN_USER, PG_ADMIN_PASSWORD, PG_APP_USER, PG_APP_PASSWORD, RBAC_PROPAGATION_DELAY and DEPLOY_APP. To prove idempotency, run terraform plan -detailed-exitcode with the same variables and environment after the apply: the key-values, secrets and role assignments show no diff. To re-run the configuration from an empty state after az group delete, purge the soft-deleted Key Vault and App Configuration store first (az keyvault purge and az appconfig purge, see the cleanup notes in the main README): otherwise the azurerm provider recovers the soft-deleted vault together with its secrets (recover_soft_deleted_key_vaults, enabled by default) and azurerm_key_vault_secret refuses to adopt the existing pg-user and pg-password secrets (A resource with the ID ... already exists - to be managed via Terraform this resource needs to be imported into the State). This is the provider's behaviour on Azure and on the emulator alike; the Azure CLI and Bicep variants recover the vault and upsert the secrets, so they can be re-run without a purge. On the emulator a few inherited resources still show read-back drift (NAT gateway zones, App Service plan SKU, some web app and diagnostic-setting attributes), a limitation of the emulator, not of this configuration.
The configuration creates the key-values inside terraform apply, so terraform plan shows their drift and terraform destroy removes them. If the AzAPI path ever proves unreliable on one of the targets, the same key-values can be seeded from deploy.sh after the apply, the way the Azure CLI variant does:
- Pass an empty
key_valuesmap to theapp_configurationmodule inmain.tf(or remove theazapi_resourcefrom the module); theazapiprovider can then leaveproviders.tfand the module'sversions.tftoo. Never keep both: two owners of the same key-value drift on every apply. - After the apply, read
terraform output -raw app_configuration_name,key_vault_uri,postgres_fqdnandpostgres_database_nameand run, once per key,az appconfig kv set --name <store> --key PG_HOST --value <host> --yes(andPG_PORT,PG_DATABASE) andaz appconfig kv set-keyvault --name <store> --key PG_USER --secret-identifier <vault URI>/secrets/pg-user --yes(andPG_PASSWORD), probing each key withaz appconfig kv showfirst so the step stays idempotent. - The seeded key-values must match the AzAPI ones exactly (keys, no label, content types, values), so
validate.shand the app behave the same. The commands use the data plane: they need access keys enabled (the default) or--auth-mode loginwith App Configuration Data Owner.
This alternative was not needed during the development of the sample: the AzAPI path works on the emulator and on Azure.
Override any of the variables in variables.tf by editing terraform.tfvars or passing -var to terraform plan. Notable ones:
| Variable | Default | Description |
|---|---|---|
prefix, suffix |
local, test |
Resource name parts; the store and vault names are globally unique on Azure, use your own suffix |
pg_admin_login |
pgadmin |
Server administrator login |
pg_admin_password |
P@ssw0rd1234! |
Server administrator password (sensitive) |
pg_version |
16 |
PostgreSQL major version |
pg_sku_name |
B_Standard_B1ms |
Compute SKU |
pg_storage_mb |
32768 |
Storage size in MB |
pg_backup_retention_days |
7 |
Backup retention |
pg_database_name |
PlannerDB |
Application database |
pg_app_user |
testuser |
Application role, stored as the pg-user secret |
pg_app_password |
TestP@ssw0rd123 |
Application role password (sensitive), stored as the pg-password secret |
app_configuration_sku |
standard |
App Configuration tier (private endpoints need developer, standard or premium) |
key_vault_sku_name |
standard |
Key Vault SKU |
soft_delete_retention_days |
7 |
Soft-delete retention of the store and the vault |
deployer_object_id |
"" |
Object id of the deploying principal, granted Key Vault Secrets Officer (empty skips it) |
deployer_principal_type |
User |
User, ServicePrincipal or Group |
rbac_propagation_delay |
60s |
Wait between the deployer's role assignment and the secret writes (0s on the emulator) |
pg_user_secret_name, pg_password_secret_name |
pg-user, pg-password |
Names of the two Key Vault secrets |
pg_host_key_name, pg_port_key_name, pg_database_key_name, pg_user_key_name, pg_password_key_name |
PG_HOST, ... |
Keys of the five key-values; the application reads exactly these keys |
key_vault_reference_content_type |
application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8 |
Content type that marks a Key Vault reference |
app_configuration_public_network_access, key_vault_public_network_access_enabled |
Enabled, true |
Public network access of the store and the vault, needed by the deployment |
*_private_dns_zone_name, *_private_endpoint_subresource_name, private_dns_zone_group_name |
Azure defaults | Private DNS zone names, private endpoint sub-resources and zone group name |
For non-dev deployments, set the passwords via env vars: PG_ADMIN_PASSWORD=... PG_APP_PASSWORD=... bash deploy.sh.