This directory contains PowerShell scripts that automate Azure deployment workflows for the MyToDoApp application. These scripts are executed automatically by azd (Azure Developer CLI) at various stages of the deployment lifecycle.
When you run azd up or other azd commands, these scripts execute in the following order:
- preup.ps1 - Runs before infrastructure provisioning
- postprovision.ps1 - Runs after infrastructure is provisioned
- postdeploy.ps1 - Runs after application deployment
- postup.ps1 - Runs after successful
azd upcompletion - postdown.ps1 - Runs after tearing down infrastructure with
azd down
Execution Phase: Before azd provision (pre-infrastructure deployment)
Purpose: Prepares the Azure environment by creating Azure AD app registrations and discovering available Azure AI Foundry models with quota.
Azure AD App Registrations:
- Creates the web application registration (
MyToDoApp)- Generates OAuth client ID and client secret
- Creates service principal for the app
- Stores credentials in
azdenvironment variables
- Creates the API application registration (
MyToDoApp-Api)- Configures Application ID URI (format:
api://<guid>) - Defines an
Api.Accessapp role for application permissions - Assigns the web app service principal to the API app role
- Enables web-to-API authentication via client credentials flow
- Configures Application ID URI (format:
Azure AI Foundry Model Discovery:
- Enumerates all available models in the Azure AI Services account
- Retrieves quota availability for each model in the deployment region
- Selects the best available chat model using intelligent filtering:
- Prefers "mini" models (e.g., gpt-4o-mini) for cost-effectiveness
- Excludes "nano" models if better options exist
- Falls back to any available model if neither mini nor standard models are available
- Selects the best available embedding model with preference:
- Prefers "small" embedding models for cost optimization
- Falls back to any available embedding model
- Prioritizes models with highest available capacity within each category
- Stores model selection as environment variables for consumption by Bicep templates
- The actual model deployments are created by the
aiservices.bicepmodule duringazd provision - This separation allows the script to discover available models before infrastructure deployment
- The actual model deployments are created by the
Azure AI Services Account Creation:
- Creates Azure AI Services (Cognitive Services) account if it doesn't exist
- Account type:
AIServices(includes Azure AI Foundry and other AI services) - SKU:
S0(Standard tier) - Custom subdomain configured for API access
- Derives unique account name if not specified (format:
todoapp-openai-<hash>)
- Account type:
- Note: The script creates the account but does NOT deploy models
- Model deployments are handled by Bicep infrastructure (see Infrastructure Documentation)
- The script sets environment variables that the Bicep templates use to deploy the selected models
Environment Setup:
- Sets
TENANT_ID,AZURE_SUBSCRIPTION_IDfrom current Azure context - Captures
NAME(user email) andOBJECT_ID(user principal ID) - Creates resource group if it doesn't exist
- Validates or creates Azure AI Services account in target region
Ensure-AppRegistration: Creates or validates web app registration with OAuth secretEnsure-ApiAppRegistration: Creates API app with app roles and assigns permissionsEnsure-OpenAIAccount: Creates Azure AI Services account (type: AIServices, SKU: S0) if missingGet-AccountModelsMultiVersion: Enumerates available models using Azure REST API (API version: 2025-07-01-preview)Get-AoaiModelAvailableQuota: Retrieves real-time quota availability per model/region/SKU
| Variable | Description |
|---|---|
CLIENT_ID |
Web app OAuth client ID |
CLIENT_SECRET |
Web app OAuth client secret |
API_APP_ID |
API app client ID (GUID only) |
API_APP_OBJECT_ID |
API app Azure AD object ID |
API_APP_ROLE_ID |
App role ID for Api.Access permission |
API_APP_ID_URI |
Full API identifier URI (e.g., api://guid) |
chatGptDeploymentVersion |
Selected chat model version |
chatGptSkuName |
Selected chat model SKU |
chatGptModelName |
Selected chat model name |
availableChatGptDeploymentCapacity |
Available quota for chat model |
embeddingDeploymentVersion |
Selected embedding model version |
embeddingDeploymentSkuName |
Selected embedding model SKU |
embeddingDeploymentModelName |
Selected embedding model name |
availableEmbeddingDeploymentCapacity |
Available quota for embedding model |
AZURE_LOCATION |
Deployment region (default: eastus2) |
AZURE_RESOURCE_GROUP |
Resource group name (derived if missing) |
AZURE_OPENAI_ACCOUNT_NAME |
Azure AI Foundry account name |
NAME |
Current user email/account |
OBJECT_ID |
Current user principal ID |
SQL_DATABASE_NAME |
Name of the Database (default: todo) |
The script uses a sophisticated filtering approach to select optimal models:
Chat Model Selection:
- First preference: Models matching "mini" (case-insensitive regex
(?i)mini)- Example:
gpt-4o-mini,gpt-35-turbo-mini - Rationale: Best balance of performance and cost
- Example:
- Second preference: Models NOT matching "nano" (excludes
(?i)nano)- Avoids very small models like
gpt-4-nanounless necessary
- Avoids very small models like
- Fallback: Any available model with highest capacity
- Ensures deployment succeeds even in constrained quota scenarios
Embedding Model Selection:
- First preference: Embedding models matching "small" (case-insensitive regex
(?i)small)- Example:
text-embedding-3-small - Rationale: Cost-effective for most embedding use cases
- Example:
- Fallback: Any available embedding model
- Prioritizes by capacity (highest first) and version (newest first)
- Parallel quota retrieval: Uses PowerShell 7+ parallel execution (throttle limit: 8)
- Idempotent: Safe to run multiple times; skips existing resources
- Quota-aware: Only selects models with available capacity
- Smart filtering: Prefers cost-effective models while ensuring availability
- Fallback logic: Uses sequential execution if parallel not supported
- Validates Azure login context before operations
- Checks for required modules (Az.Accounts, Az.Resources, Az.CognitiveServices)
- Skips model discovery if selections already exist
- Provides detailed warnings for missing quota or API failures
Execution Phase: After azd provision completes (post-infrastructure provisioning)
Purpose: Configures Azure SQL Database permissions for the managed identity and creates the todo table schema immediately after infrastructure deployment.
Module Installation:
- Trusts PSGallery repository to suppress untrusted prompts
- Installs Microsoft.Graph module if not present
- Installs required Az modules:
Az.Resources,Az.ManagedServiceIdentity,Az.Sql
Managed Identity Database Permissions:
- Locates or auto-discovers the user-assigned managed identity in the resource group
- Locates or auto-discovers the Azure SQL Server in the resource group
- Obtains Azure AD access token for SQL Database authentication
- Loads SQL from
assign-database-roles.sqlwith template substitution - Creates external user in the database mapped to the managed identity
- Grants database roles:
db_datareader,db_datawriter,db_ddladmin
Database Schema Creation:
- Loads SQL from
create-tables.sqlfor table creation - Creates the
dbo.todotable if it doesn't exist - Defines schema with columns:
id,name,recommendations_json,notes,priority,completed,due_date,oid - Adds JSON validation constraint for
recommendations_jsoncolumn - Sets default values for
priority(0) andcompleted(false)
CREATE TABLE dbo.todo (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(100) NOT NULL,
recommendations_json NVARCHAR(MAX) NULL,
notes NVARCHAR(100) NULL,
priority INT NOT NULL DEFAULT(0),
completed BIT NOT NULL DEFAULT(0),
due_date NVARCHAR(50) NULL,
oid NVARCHAR(50) NULL,
CONSTRAINT CK_todo_recommendations_json_isjson CHECK (
recommendations_json IS NULL OR ISJSON(recommendations_json)=1
)
);Convert-SecureIfNeededToPlainText: Converts SecureString tokens to plain textGet-AzAccessToken: Retrieves Azure AD token for SQL authentication- ADO.NET execution using
Microsoft.Data.SqlClientwith AccessToken authentication - SQL Template Substitution: Replaces
{{IDENTITY_NAME}}placeholder in SQL files with actual identity name - SQL Script Loading: Loads SQL from external
.sqlfiles for better maintainability
| File | Purpose |
|---|---|
assign-database-roles.sql |
Creates external user and grants database roles to managed identity |
create-tables.sql |
Creates the dbo.todo table schema |
| Variable | Description | Source | Default |
|---|---|---|---|
TENANT_ID |
Azure AD tenant ID | azd environment | None |
AZURE_RESOURCE_GROUP |
Resource group name | azd environment | None |
USER_MANAGED_IDENTITY_NAME |
Managed identity name | azd environment (auto-discovered if missing) | First found in RG |
SQL_SERVER_NAME |
SQL server name | azd environment (auto-discovered if missing) | First found in RG |
SQL_DATABASE_NAME |
SQL database name | azd environment | todo |
Unlike manual troubleshooting scripts, postprovision.ps1 is configured as an azd hook in azure.yaml to run automatically after infrastructure provisioning. This ensures:
- Database is ready for application deployment
- Managed identity has proper permissions before containers start
- Table schema exists before Data API Builder attempts to query it
- Eliminates manual configuration steps
- Safe to run multiple times: All operations check for existence before creation
- Skip existing users: Won't fail if managed identity user already exists
- Skip existing roles: Only grants roles if not already assigned
- Skip existing table: Won't recreate
todotable if it exists
Uses Azure AD token-based authentication instead of SQL username/password:
- Obtains access token via
Get-AzAccessToken -ResourceUrl 'https://database.windows.net/' - Creates
SqlConnectionwith token viaAccessTokenproperty - Executes T-SQL with admin privileges from current user context
- Managed identity receives database roles for application access
- Validates Azure login context before operations
- Auto-discovers resources if environment variables missing
- Sets discovered resource names in azd environment for future runs
- Validates SQL script files exist before attempting to load
- Provides detailed error messages for T-SQL failures
- Exits with error code 1 on critical failures (missing identity, server, or SQL files)
Execution Phase: After azd deploy completes (post-application deployment)
Purpose: Updates the Azure AD web app registration with the correct redirect URIs and logout URL after the container app is deployed.
Redirect URI Configuration:
- Retrieves the deployed web app URL from
azdenvironment (APP_REDIRECT_URI) - Updates the Azure AD app registration with production redirect URI
- Adds local development redirect URI for debugging
- Sets the logout redirect URL to the web app base URL
URIs Configured:
- Production redirect:
https://todoapp-app-xyz.azurecontainerapps.io/getAToken - Local development:
http://localhost:5000/getAToken - Logout redirect:
https://todoapp-app-xyz.azurecontainerapps.io(base URL)
The redirect URIs depend on the deployed container app's URL, which isn't known until after Bicep deployment completes. This script bridges that gap by updating the app registration with the correct URLs after the infrastructure is provisioned.
- User clicks "Sign In" on web app
- Flask redirects to Azure AD with
redirect_uri=https://<app-url>/getAToken - User authenticates with Azure AD
- Azure AD validates redirect URI matches app registration
- Azure AD redirects back to
/getATokenwith authorization code - Flask exchanges code for access token
- Retrieves
MyToDoAppapp registration by display name - Updates
ReplyUrls(redirect URIs) usingSet-AzADApplication - Updates
LogoutUrlusingUpdate-AzADApplication - Validates
APP_REDIRECT_URIenvironment variable exists
- Fails if
APP_REDIRECT_URInot found in environment - Requires existing Azure AD app registration
- Validates Azure login context before operations
Execution Phase: After azd up completes (post-deployment)
Purpose: Generates a .env file at the project root for local development by pulling configuration from the deployed Azure environment.
Environment File Generation:
- Reads the current
azdenvironment variables - Creates or updates
.envfile with local development settings - Preserves existing comments and non-target variables
- Quotes values containing special characters (whitespace,
#,;,:,=)
Local Development Variables:
IS_LOCALHOST=true- Marker for local execution code pathsAPPLICATIONINSIGHTS_CONNECTION_STRING- Telemetry endpointREDIS_CONNECTION_STRING- Redis connection with Entra ID authenticationAZURE_CLIENT_ID- Managed identity client ID for local developmentKEY_VAULT_NAME- Key Vault for secrets retrievalAPI_URL- Backend GraphQL API endpointREDIS_LOCAL_PRINCIPAL_ID- User's principal ID for Redis AAD authentication
Get-AzdValue: Retrieves variable fromazdenvironment with error handlingQuote-EnvValue: Safely quotes values for.envformatParse-EnvFile: Parses existing.envfile preserving structureUpdate-EnvFile: Merges new values with existing file
- Idempotent: Updates existing
.envwithout losing other content - Selective: Only syncs variables that exist in
azdenvironment - Safe: Skips variables with missing values (logs warning)
- POSIX-friendly: Ensures file ends with newline
- Run
azd upto deploy Azure infrastructure postup.ps1automatically generates.envfile- Local Flask app reads
.envon startup - Application uses local Azure CLI credentials for authentication
- Connects to deployed Azure resources (Redis, SQL, OpenAI, Key Vault)
IS_LOCALHOST=true
APPLICATIONINSIGHTS_CONNECTION_STRING='InstrumentationKey=abc...'
REDIS_CONNECTION_STRING='rediss://identity-name@hostname:6380/0'
AZURE_CLIENT_ID=12345678-1234-1234-1234-123456789abc
KEY_VAULT_NAME=todoapp-kv-abc123def456
API_URL=https://todoapp-api-abc123.azurecontainerapps.io/graphql
REDIS_LOCAL_PRINCIPAL_ID=87654321-4321-4321-4321-cba987654321Execution Phase: After azd down (post-infrastructure teardown)
Purpose: Cleans up Azure AD app registrations and local development artifacts after the infrastructure has been deleted.
Azure AD Cleanup:
- Removes the web application registration (
MyToDoApp)- Deletes service principal first (to avoid orphaned SPs)
- Deletes app registration and all associated credentials
- Removes the API application registration (
MyToDoApp-Api)- Deletes app roles and permissions
- Deletes service principal and app registration
Environment Cleanup:
- Unsets all app registration variables from
azdenvironment:CLIENT_ID,CLIENT_SECRETAPI_APP_ID,API_APP_OBJECT_IDAPI_APP_ROLE_ID,API_APP_ID_URI
- Removes generated
.envfile from project root
Remove-AppRegistration: Safely deletes app registration and service principalRemove-AzdValue: Unsets variables fromazdenvironmentConnect-AzContextIfNeeded: Ensures Azure authentication before deletion
- Locate app registration by client ID or display name
- Delete service principal (if exists)
- Delete app registration
- Clear
azdenvironment variables - Remove local
.envfile
- Idempotent: Safe to run multiple times
- Defensive: Continues if resources already deleted
- Logging: Provides detailed feedback on each deletion step
- Fallback: Tries display name if client ID lookup fails
- After
azd down: Automatically cleans up app registrations - Manual cleanup: Run directly to remove orphaned registrations
- Development reset: Clean slate before re-provisioning
This script permanently deletes Azure AD app registrations. Ensure you have backups of any custom configuration or credentials before running. Deleted app registrations can be restored from Azure AD's "Deleted applications" for 30 days.
All scripts follow a consistent pattern for PowerShell module management:
# Trust PSGallery to avoid prompts
function Ensure-PsGalleryTrusted { ... }
# Install and import required modules
function Ensure-Module {
param([string]$Name, [string]$MinVersion)
# Install if missing, import with error handling
}function Ensure-AzLogin {
param([string]$TenantId, [string]$SubscriptionId)
# Connect if not logged in
# Switch subscription if needed
}function Get-AzdValue {
param([string]$Name, [string]$Default='')
# Retrieve from azd env with ANSI color handling
# Detect error patterns and return default
}
function Set-AzdValue {
param([string]$Name, [string]$Value)
azd env set $Name $Value | Out-Null
}If scripts fail with "execution policy" errors:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedEnsure you have the latest Az modules:
Update-Module Az.Accounts, Az.Resources, Az.CognitiveServices -ForceIf quota retrieval is slow, set throttle limit:
$env:AOAI_QUOTA_DOP = 16 # Increase parallel degreeIf app registrations already exist with different configuration:
# Manually delete conflicting apps
Remove-AzADApplication -DisplayName "MyToDoApp"
Remove-AzADApplication -DisplayName "MyToDoApp-Api"
# Re-run preup.ps1
./scripts/preup.ps1If no models have available quota in your region:
# Try a different region
azd env set AZURE_LOCATION "eastus"
azd provisionTo sync additional variables in postup.ps1:
$desiredVariables = @(
# Existing entries...
@{ Target='NEW_VAR'; Candidates=@('SOURCE_VAR_1','SOURCE_VAR_2') }
)To change model selection criteria in preup.ps1:
# Modify the sorting logic (line ~750)
$sorted = $allQuota | Sort-Object -Property @{
Expression={ [int]$_.AvailableCapacity };
Descending=$true
}, @{
Expression={$_.ModelVersion};
Descending=$true
}
# Add custom filters
$chatPick = $sorted | Where-Object { $_.ModelName -like '*gpt-4*' } | Select-Object -First 1To add custom app registration settings in preup.ps1:
# After line ~250 (app creation)
Update-AzADApplication -ObjectId $app.Id -SignInAudience 'AzureADMultipleOrgs'
# Add required resource access, optional claims, etc.- Az.Accounts (>= 2.12.0) - Azure authentication and context
- Az.Resources - Azure AD app registration management
- Az.CognitiveServices - Azure AI Foundry account operations
- azd (Azure Developer CLI) - Environment variable storage and lifecycle hooks
- Azure CLI (optional) - Used by local development for authentication
- PowerShell 5.1+ - Windows PowerShell or PowerShell Core
- PowerShell 7+ - Recommended for parallel quota retrieval
- Client secrets stored in
azdenvironment (encrypted on disk) - Never commit
.envfile to source control (add to.gitignore) - Rotate secrets annually via Azure AD app registration
- Web app SP granted
Api.Accessapp role on API registration - No directory-level permissions required
- Follows principle of least privilege
- Uses Azure CLI or Visual Studio credentials for managed identity
- REDIS_LOCAL_PRINCIPAL_ID enables Redis AAD authentication locally
- No local secrets required (all retrieved from Key Vault)