This directory contains the Bicep template 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
- Visual Studio Code + Bicep extension
- Docker
- Azure CLI + lstk CLI
- Python 3.12+
- PostgreSQL client (
psql) jq
brew install localstack/tap/lstk # or: npm install -g @localstack/lstkThe deploy.sh script creates the resource group while the Bicep modules create:
- Azure Virtual Network (
virtual-network.bicep) with two subnets:- app-subnet: delegated to
Microsoft.Web/serverFarmsfor the Web App's regional VNet integration. - pe-subnet: hosts the three Private Endpoints.
- app-subnet: delegated to
- User-assigned managed identity (
managed-identity.bicep)<prefix>-identity-<suffix>. - Azure Database for PostgreSQL flexible server (
postgresql-flexible-server.bicep): public-access mode, BurstableStandard_B1ms, version 16, 32 GiB, HA disabled, the permissive firewall rule for the post-deploy psql bootstrap and thePlannerDBdatabase (UTF8 /en_US.utf8). - Azure Key Vault (
key-vault.bicep)<prefix>-keyvault-<suffix>withenableRbacAuthorization: true, soft delete (7 days) and no purge protection; the twoMicrosoft.KeyVault/vaults/secretschildrenpg-userandpg-passwordcreated from thepgAppUserand@secure() pgAppPasswordparameters; the Key Vault Secrets User role assignment for the identity; an optional Key Vault Secrets Officer assignment for the deploying principal (deployerPrincipalId); and its diagnostic settings. The module outputs the versionless secret identifiers, assembled from the vault URI plussecrets/<name>the way the Azure CLI variant does (the emulator does not return thesecretUriproperty of the secret resource today, see the module comments). - Azure App Configuration store (
app-configuration.bicep)<prefix>-appconfig-<suffix>, Standard SKU, withdisableLocalAuth: falseand public network access enabled; the fiveMicrosoft.AppConfiguration/configurationStores/keyValueschildren, created in a loop over thekeyValuesarraymain.bicepassembles from the PostgreSQL and Key Vault module outputs:PG_HOST,PG_PORTandPG_DATABASEas plain values andPG_USERandPG_PASSWORDwith the content typeapplication/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8and the value{"uri":"<secret identifier>"}; the App Configuration Data Reader role assignment for the identity; and its diagnostic settings. - Three Azure Private DNS Zones (
private-dns-zone.bicep),privatelink.postgres.database.azure.com,privatelink.azconfig.ioandprivatelink.vaultcore.azure.net, each linked to the VNet aslink-to-vnet. - Three Azure Private Endpoints (
private-endpoint.bicep), groupspostgresqlServer,configurationStoresandvault, each with adefaultDNS zone group. - Azure NAT Gateway and one Network Security Group per subnet (inside
virtual-network.bicep). - Azure Log Analytics Workspace (
log-analytics.bicep). - Azure App Service Plan (
app-service-plan.bicep). - Azure Web App (
web-app.bicep) with regional VNet integration into app-subnet, the user-assigned identity, and the app settingsEndpoints__AppConfiguration(the store endpoint) andAZURE_CLIENT_ID(the identity client id) next toLOGIN_NAME,WEBSITES_PORTand the Oryx build flags. The template sets noPG_*app setting: the app loads the five settings from the store.
Every configurable value of a module is a parameter with a default (SKUs, network access, soft-delete retention, role definition ids, principal types, diagnostic categories, Private DNS zone link names, zone group names), and every value main.bicep passes is one of its own parameters with a default (secret names, key names, the Key Vault reference content type, the default PostgreSQL port, Private DNS zone names, private endpoint group ids, the subnet delegation). No resource definition carries a literal, so a deployment can be adapted by overriding parameters in main.bicepparam instead of editing the modules.
Two remarks about the store's key-values:
- The
keyValueschildren are written through Azure Resource Manager, which authorizes them with the store's Azure Resource Manager authentication mode. The store keeps the defaultLocalmode and access keys enabled, so the deploying principal only needs Contributor. If you disable access keys, switch the store toPass-throughand grant the deploying principal App Configuration Data Owner, otherwise the key-value writes fail on Azure. - The Key Vault reference values are built with string interpolation (
'{"uri":"${uri}"}') rather thanstring({ uri: uri }): the result is the same JSON on Azure, while the LocalStack emulator's template engine rendersstring()of an object as a Python dictionary today.
Update main.bicepparam before deploying. The defaults are:
using 'main.bicep'
param prefix = 'local'
param suffix = 'test'
param runtimeName = 'python'
param runtimeVersion = '3.13'
param databaseName = 'PlannerDB'
param username = 'paolo'
param pgAdminLogin = 'pgadmin'
param pgAdminPassword = readEnvironmentVariable('PG_ADMIN_PASSWORD', '')
param pgVersion = '16'
param pgSkuTier = 'Burstable'
param pgSkuName = 'Standard_B1ms'
param pgStorageSizeGB = 32
param pgBackupRetentionDays = 7
param pgAppUser = 'testuser'
param pgAppPassword = readEnvironmentVariable('PG_APP_PASSWORD', '')
param appConfigurationSku = 'Standard'
param keyVaultSkuName = 'standard'pgAdminPassword and pgAppPassword are the secure parameters that carry the two passwords; deploy.sh reads them from the PG_ADMIN_PASSWORD and PG_APP_PASSWORD env vars (with development defaults) and passes them on the command line together with prefix, suffix, location, pgAdminLogin, pgAppUser, deployerPrincipalId and deployerPrincipalType. Override for non-dev deployments. The store and vault names are globally unique on Azure, so export your own SUFFIX.
# default values
bash deploy.sh
# your own suffix and secrets
SUFFIX='<unique>' \
PG_ADMIN_PASSWORD='<your-admin-password>' \
PG_APP_PASSWORD='<your-app-password>' \
bash deploy.sh
# preview the changes of a re-deployment instead of validating the template
USE_WHAT_IF=1 bash deploy.shThe script will:
- Ensure the resource group exists.
- Recover a soft-deleted App Configuration store or Key Vault of the same name, so a re-deployment after a cleanup does not fail on a reserved name.
- Resolve the object id of the deploying principal (skipped with a warning when Microsoft Graph cannot resolve it).
- Validate
main.bicep(or run a what-if withUSE_WHAT_IF=1). - Deploy the template, passing the parameters above.
- Check that the store holds the five expected key-values.
- Wait for the
PlannerDBdatabase to accept connections, then usepsql(connected via the public endpoint and the firewall rule) to create thetestuserrole, theactivitiestable and the demo rows. The role name and password are the values already stored in Key Vault; nothing is written to the Web App's app settings. - Print the key-values with their content types and the Web App's app settings.
- Zip the application source under
../srcand deploy it.
deploy.sh accepts the same environment overrides as the Azure CLI variant: PREFIX, SUFFIX, LOCATION, PG_ADMIN_USER, PG_ADMIN_PASSWORD, PG_APP_USER, PG_APP_PASSWORD, DEPLOY_APP, plus VALIDATE_TEMPLATE and USE_WHAT_IF.
The template creates the key-values itself, which keeps the whole topology in one deployment. If the keyValues children ever prove unreliable on one of the targets (for example because access keys are disabled and the store's Azure Resource Manager authentication mode is still Local), the same key-values can be seeded from deploy.sh after the deployment, the way the Azure CLI variant does:
- Pass an empty array as the
keyValuesparameter of theappConfigurationmodule inmain.bicep(and drop theappConfigurationKeyValuesvariable). Never keep both: two owners of the same key-value drift on every deployment. - After the deployment, read the outputs
appConfigurationName,keyVaultUri,postgresFqdnanddatabaseNameand 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 template's 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 template path works on the emulator and on Azure.
PGPASSWORD='TestP@ssw0rd123' psql -h <fqdn> -p <port> -U testuser -d PlannerDB \
-c "SELECT id, username, activity, created_at FROM activities;"
az appconfig kv list --name local-appconfig-test \
--query "[].{Key:key,ContentType:contentType,Label:label}" --output table
az keyvault secret list --vault-name local-keyvault-test --query "[].name" --output tsv
az webapp config appsettings list --name local-webapp-test --resource-group local-rg \
--query "[?starts_with(name, 'PG_')].name" --output tsv
bash ../scripts/validate.sh<port> is 5432 in real Azure, or the port suffix of the server's FQDN in LocalStack:
az postgres flexible-server show \
--resource-group local-rg --name local-pgflex-test \
--query fullyQualifiedDomainName --output tsv