From 34e0ecd0f66a5e5ceca85afa515bea43609c43ae Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 18 Mar 2026 13:49:24 -0400 Subject: [PATCH 1/7] initial updates --- README.md | 71 +- application/single_app/config.py | 2 +- .../single_app/route_backend_models.py | 33 +- deployers/Initialize-AzureEnvironment.ps1 | 382 ++++- deployers/Initialize-EntraApplication.ps1 | 444 ++++- deployers/azure.yaml | 790 ++++++++- deployers/azurecli/README.md | 237 ++- deployers/azurecli/deploy-simplechat.ps1 | 983 ++++++++++- deployers/azurecli/destroy-simplechat.ps1 | 47 +- deployers/bicep/OneClickDeploy.md | 7 + deployers/bicep/README.md | 182 +- deployers/bicep/cosmosDb-postDeployPerms.sh | 37 +- deployers/bicep/main.bicep | 161 +- deployers/bicep/main.json | 1465 ++++++++++++++--- deployers/bicep/main.parameters.json | 30 + deployers/bicep/modules/appService.bicep | 9 +- .../modules/azureContainerRegistry.bicep | 3 +- deployers/bicep/modules/openAI.bicep | 30 +- deployers/bicep/modules/privateDNS.bicep | 33 +- deployers/bicep/modules/privateDNSLink.bicep | 21 + .../bicep/modules/privateNetworking.bicep | 56 +- .../setPermissions-openAIExternal.bicep | 64 + deployers/bicep/modules/setPermissions.bicep | 28 +- deployers/bicep/postconfig.py | 32 +- deployers/bicep/validate_azd_prerequisites.py | 217 +++ deployers/terraform/.terraform.lock.hcl | 99 +- deployers/terraform/ReadMe.md | 106 +- deployers/terraform/main.tf | 396 +++-- deployers/terraform/private_networking.tf | 329 ++++ .../ACR_BUILD_WORKFLOW_FOR_DEPLOYERS.md | 74 + .../AZURECLI_AOAI_MODEL_DEPLOYMENTS.md | 54 + .../AZURECLI_PRIVATE_NETWORKING_PARITY.md | 148 ++ .../TERRAFORM_PRIVATE_NETWORKING_PARITY.md | 67 + .../v0.237.001/PRIVATE_NETWORKING_SUPPORT.md | 9 + .../AZD_WINDOWS_POSTPROVISION_HOOK_FIX.md | 35 + .../AZD_WINDOWS_RGNAME_RESOLUTION_FIX.md | 36 + .../AZD_WINDOWS_SUBSCRIPTION_TARGETING_FIX.md | 23 + .../POSTCONFIG_AZURECLI_CREDENTIAL_FIX.md | 30 + ...D_POSTPROVISION_COSMOS_KEY_FALLBACK_FIX.md | 33 + ...ISION_COSMOS_FIREWALL_RUNNER_ACCESS_FIX.md | 30 + ...D_POSTPROVISION_MFA_COSMOS_GUIDANCE_FIX.md | 32 + ...D_PRIVATE_NETWORK_RUNNER_IP_PRIMING_FIX.md | 33 + ...D_POSTPROVISION_COSMOS_ACCESS_PROBE_FIX.md | 30 + .../ACR_TASKS_TRUSTED_AZURE_SERVICES_FIX.md | 31 + .../COSMOS_FIREWALL_CIDR_PROPAGATION_FIX.md | 30 + ..._FIREWALL_MFA_FALLBACK_ACCESS_CHECK_FIX.md | 30 + ...R_PREDEPLOY_TEMPORARY_FIREWALL_OPEN_FIX.md | 30 + .../ACR_DEPLOYMENT_TIME_PUBLIC_ACCESS_FIX.md | 31 + .../WINDOWS_ACR_LOG_STREAM_ENCODING_FIX.md | 30 + .../POSTPROVISION_VENV_PIP_USER_FIX.md | 31 + docs/explanation/release_notes.md | 136 ++ docs/how-to/enterprise_networking.md | 18 + .../test_acr_trusted_services_bypass.py | 62 + ...azd_prerequisites_allowed_ip_auto_merge.py | 52 + functional_tests/test_azd_windows_hooks.py | 80 + .../test_azurecli_aoai_model_deployments.py | 83 + .../test_postconfig_azurecli_credential.py | 59 + ...postprovision_python_dependency_install.py | 54 + 58 files changed, 6930 insertions(+), 755 deletions(-) create mode 100644 deployers/bicep/modules/privateDNSLink.bicep create mode 100644 deployers/bicep/modules/setPermissions-openAIExternal.bicep create mode 100644 deployers/bicep/validate_azd_prerequisites.py create mode 100644 deployers/terraform/private_networking.tf create mode 100644 docs/explanation/features/ACR_BUILD_WORKFLOW_FOR_DEPLOYERS.md create mode 100644 docs/explanation/features/AZURECLI_AOAI_MODEL_DEPLOYMENTS.md create mode 100644 docs/explanation/features/AZURECLI_PRIVATE_NETWORKING_PARITY.md create mode 100644 docs/explanation/features/TERRAFORM_PRIVATE_NETWORKING_PARITY.md create mode 100644 docs/explanation/fixes/v0.237.049/AZD_WINDOWS_POSTPROVISION_HOOK_FIX.md create mode 100644 docs/explanation/fixes/v0.237.050/AZD_WINDOWS_RGNAME_RESOLUTION_FIX.md create mode 100644 docs/explanation/fixes/v0.237.051/AZD_WINDOWS_SUBSCRIPTION_TARGETING_FIX.md create mode 100644 docs/explanation/fixes/v0.237.052/POSTCONFIG_AZURECLI_CREDENTIAL_FIX.md create mode 100644 docs/explanation/fixes/v0.237.053/AZD_POSTPROVISION_COSMOS_KEY_FALLBACK_FIX.md create mode 100644 docs/explanation/fixes/v0.237.054/AZD_POSTPROVISION_COSMOS_FIREWALL_RUNNER_ACCESS_FIX.md create mode 100644 docs/explanation/fixes/v0.237.055/AZD_POSTPROVISION_MFA_COSMOS_GUIDANCE_FIX.md create mode 100644 docs/explanation/fixes/v0.237.056/AZD_PRIVATE_NETWORK_RUNNER_IP_PRIMING_FIX.md create mode 100644 docs/explanation/fixes/v0.237.057/AZD_POSTPROVISION_COSMOS_ACCESS_PROBE_FIX.md create mode 100644 docs/explanation/fixes/v0.237.058/ACR_TASKS_TRUSTED_AZURE_SERVICES_FIX.md create mode 100644 docs/explanation/fixes/v0.237.059/COSMOS_FIREWALL_CIDR_PROPAGATION_FIX.md create mode 100644 docs/explanation/fixes/v0.237.060/COSMOS_FIREWALL_MFA_FALLBACK_ACCESS_CHECK_FIX.md create mode 100644 docs/explanation/fixes/v0.237.061/ACR_PREDEPLOY_TEMPORARY_FIREWALL_OPEN_FIX.md create mode 100644 docs/explanation/fixes/v0.237.062/ACR_DEPLOYMENT_TIME_PUBLIC_ACCESS_FIX.md create mode 100644 docs/explanation/fixes/v0.237.063/WINDOWS_ACR_LOG_STREAM_ENCODING_FIX.md create mode 100644 docs/explanation/fixes/v0.237.064/POSTPROVISION_VENV_PIP_USER_FIX.md create mode 100644 functional_tests/test_acr_trusted_services_bypass.py create mode 100644 functional_tests/test_azd_prerequisites_allowed_ip_auto_merge.py create mode 100644 functional_tests/test_azd_windows_hooks.py create mode 100644 functional_tests/test_azurecli_aoai_model_deployments.py create mode 100644 functional_tests/test_postconfig_azurecli_credential.py create mode 100644 functional_tests/test_postprovision_python_dependency_install.py diff --git a/README.md b/README.md index 31ea020ba..8347fa435 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,34 @@ The application utilizes **Azure Cosmos DB** for storing conversations, metadata [Detailed deployment Guide](./deployers/bicep/README.md) +If you prefer a PowerShell and Azure CLI driven deployment without AZD, or you want more script-level control over deployment and recovery steps, see [deployers/azurecli/README.md](./deployers/azurecli/README.md). + +### Prerequisites + +Install these tools before starting the deployment flow: + +1. Azure CLI + Download: https://learn.microsoft.com/cli/azure/install-azure-cli +2. Azure Developer CLI (`azd`) + Download: https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd +3. PowerShell 7 + Download: https://learn.microsoft.com/powershell/scripting/install/installing-powershell +4. Visual Studio Code + Download: https://code.visualstudio.com/download + +Shell guidance: + +- This quick-start section uses PowerShell examples. +- Windows users can run the commands in PowerShell. +- Linux and macOS users can run the PowerShell scripts by using PowerShell 7 with `pwsh`. +- If you prefer bash for the surrounding shell commands, use the shell-specific examples in [deployers/bicep/README.md](./deployers/bicep/README.md). + +Minimum access and setup: + +- An Azure subscription where you have Owner or Contributor access for deployment. +- Permission to create an Entra application registration, or coordination with an Entra admin who can run that step for you. +- Access to Azure Container Registry Tasks so `azd up` can build the application image in ACR. + ### Pre-Configuration: The following procedure must be completed with a user that has permissions to create an application registration in the users Entra tenant. @@ -26,15 +54,16 @@ The following procedure must be completed with a user that has permissions to cr cd ./deployers ``` -Define your application name and your environment: +Define your application name and your environment in PowerShell: -``` -appName = +```powershell +$appName = "simplechat" +$environment = "dev" ``` -``` -environment = -``` +If you type `appName = "simplechat"` in PowerShell, PowerShell treats `appName` as a command name. PowerShell variables must start with `$`. + +This main README uses PowerShell examples consistently. Linux and macOS users should run the same script with `pwsh`. If you prefer bash for the surrounding shell commands, use the shell-specific examples in [deployers/bicep/README.md](./deployers/bicep/README.md). The following script will create an Entra Enterprise Application, with an App Registration named *\*-*\*-ar for the web service called *\*-*\*-app. @@ -47,7 +76,13 @@ The following script will create an Entra Enterprise Application, with an App Re > A different expiration date for the secret which defaults to 180 days with the `-SecretExpirationDays` parameter. ```powershell -.\Initialize-EntraApplication.ps1 -AppName "" -Environment "" -AppRolesJsonPath "./azurecli/appRegistrationRoles.json" +.\Initialize-EntraApplication.ps1 -AppName $appName -Environment $environment -AppRolesJsonPath "./azurecli/appRegistrationRoles.json" +``` + +Linux and macOS example: + +```bash +pwsh ./Initialize-EntraApplication.ps1 -AppName simplechat -Environment dev -AppRolesJsonPath ./azurecli/appRegistrationRoles.json ``` > [!NOTE] @@ -85,7 +120,7 @@ In addition, the script will note additional steps that must be taken for the ap #### Configure AZD Environment -Using the bash terminal in Visual Studio Code +Using PowerShell in Visual Studio Code ```powershell cd ./deployers @@ -103,16 +138,22 @@ This will open a browser window that the user with Owner level permissions to th azd auth login ``` -Use the same value for the \ that was used in the application registration. +Initialize the AZD project in the current folder before creating or selecting the environment. + +```powershell +azd init +``` + +Use the same value for `$environment` that was used in the application registration. ```powershell -azd env new +azd env new $environment ``` Select the new environment ```powershell -azd env select +azd env select $environment ``` This step will begin the deployment process. @@ -121,6 +162,14 @@ This step will begin the deployment process. azd up ``` +Linux and macOS users can run the AZD commands from bash, but should be invoked with `pwsh`. + +```powershell +pwsh azd up +``` + +You will answer questions during the deployment process for more details on those questions please review [deployers/bicep/README.md](./deployers/bicep/README.md). + ## Architecture ![Architecture](./docs/images/architecture.png) diff --git a/application/single_app/config.py b/application/single_app/config.py index d5ba49b6d..7005b564b 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -88,7 +88,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.237.011" +VERSION = "0.237.064" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/route_backend_models.py b/application/single_app/route_backend_models.py index 176d112c8..8aace96aa 100644 --- a/application/single_app/route_backend_models.py +++ b/application/single_app/route_backend_models.py @@ -7,6 +7,15 @@ import re +def _get_configured_models(settings, setting_key): + configured = settings.get(setting_key, {}) or {} + return configured.get('all', []) if isinstance(configured, dict) else [] + + +def _is_foundry_project_endpoint(endpoint): + return 'services.ai.azure.com' in (endpoint or '').lower() + + def register_route_backend_models(app): """ Register backend routes for fetching Azure OpenAI models. @@ -25,10 +34,12 @@ def get_gpt_models(): subscription_id = settings.get('azure_openai_gpt_subscription_id', '') resource_group = settings.get('azure_openai_gpt_resource_group', '') - account_name = settings.get('azure_openai_gpt_endpoint', '').split('.')[0].replace("https://", "") + endpoint = settings.get('azure_openai_gpt_endpoint', '') + account_name = endpoint.split('.')[0].replace("https://", "") + configured_models = _get_configured_models(settings, 'gpt_model') - if not subscription_id or not resource_group or not account_name: - return jsonify({"error": "Azure GPT Model subscription/RG/endpoint not configured"}), 400 + if _is_foundry_project_endpoint(endpoint) or not subscription_id or not resource_group or not account_name: + return jsonify({"models": configured_models}), 200 if AZURE_ENVIRONMENT == "usgovernment" or AZURE_ENVIRONMENT == "custom": @@ -66,8 +77,8 @@ def get_gpt_models(): "modelName": model_name }) - except Exception as e: - return jsonify({"error": str(e)}), 500 + except Exception: + return jsonify({"models": configured_models}), 200 return jsonify({"models": models}) @@ -85,10 +96,12 @@ def get_embedding_models(): subscription_id = settings.get('azure_openai_embedding_subscription_id', '') resource_group = settings.get('azure_openai_embedding_resource_group', '') - account_name = settings.get('azure_openai_embedding_endpoint', '').split('.')[0].replace("https://", "") + endpoint = settings.get('azure_openai_embedding_endpoint', '') + account_name = endpoint.split('.')[0].replace("https://", "") + configured_models = _get_configured_models(settings, 'embedding_model') - if not subscription_id or not resource_group or not account_name: - return jsonify({"error": "Azure Embedding Model subscription/RG/endpoint not configured"}), 400 + if _is_foundry_project_endpoint(endpoint) or not subscription_id or not resource_group or not account_name: + return jsonify({"models": configured_models}), 200 if AZURE_ENVIRONMENT == "usgovernment" or AZURE_ENVIRONMENT == "custom": @@ -124,8 +137,8 @@ def get_embedding_models(): "deploymentName": d.name, "modelName": model_name }) - except Exception as e: - return jsonify({"error": str(e)}), 500 + except Exception: + return jsonify({"models": configured_models}), 200 return jsonify({"models": models}) diff --git a/deployers/Initialize-AzureEnvironment.ps1 b/deployers/Initialize-AzureEnvironment.ps1 index 18180dc3d..456597daa 100644 --- a/deployers/Initialize-AzureEnvironment.ps1 +++ b/deployers/Initialize-AzureEnvironment.ps1 @@ -7,8 +7,8 @@ The `Initialize-AzureEnvironment.ps1` script performs the following tasks: 1. Verifies the user has "Owner" permissions on the current Azure subscription. 2. Creates an Azure resource group if it does not already exist. 3. Deploys an Azure Container Registry (ACR) and retrieves its credentials. -4. Deploys an Azure OpenAI instance. -5. Outputs configuration details required for GitHub Actions secrets. +4. Deploys a new Azure OpenAI instance or reuses an existing one. +5. Outputs configuration details required for deployment scripts, ACR-based image builds, and GitHub Actions secrets. .PARAMETER ResourceGroupName The name of the Azure resource group to create or use. @@ -22,14 +22,37 @@ The name of the Azure Container Registry to create or use. .PARAMETER OpenAiName The name of the Azure OpenAI instance to create or use. +.PARAMETER UseExistingOpenAI +Reuse an existing Azure OpenAI resource or endpoint instead of creating a new Azure OpenAI resource. + +.PARAMETER ExistingOpenAIEndpoint +Optional existing Azure OpenAI endpoint to use. If not provided and `UseExistingOpenAI` is specified, +the script attempts to resolve the endpoint from the existing Azure OpenAI resource metadata. + +.PARAMETER ExistingOpenAIResourceGroup +Optional resource group containing the existing Azure OpenAI resource. Defaults to `ResourceGroupName`. + +.PARAMETER ExistingOpenAISubscriptionId +Optional subscription ID containing the existing Azure OpenAI resource. Defaults to the current Azure subscription. + +.PARAMETER AzureCloudEnvironment +Optional Azure cloud name. You can pass `AzureCloud`, `AzureUSGovernment`, or a custom Azure CLI cloud name. +If omitted during interactive use, the script prompts for the target cloud before login. + .EXAMPLE .\Initialize-AzureEnvironment.ps1 -ResourceGroupName "myResourceGroup" -AzureRegion "eastus" -ACRName "myACR" -OpenAiName "myOpenAI" This command initializes the Azure environment with the specified resource group, region, ACR, and OpenAI instance. +.EXAMPLE +.\Initialize-AzureEnvironment.ps1 -ResourceGroupName "myResourceGroup" -AzureRegion "eastus" -ACRName "myACR" -OpenAiName "sharedOpenAI" -UseExistingOpenAI -ExistingOpenAIResourceGroup "shared-ai-rg" -ExistingOpenAISubscriptionId "00000000-0000-0000-0000-000000000000" + +This command initializes the Azure environment while reusing an existing Azure OpenAI resource. + .NOTES - Ensure that the Azure CLI is installed and available on the `PATH` before running this script. - The script requires the user to have "Owner" permissions on the Azure subscription. +- If you plan to use `az acr build`, the deployment identity must also be able to submit ACR Tasks in the target registry. #> param( @@ -43,19 +66,277 @@ param( [string]$ACRName, [Parameter(Mandatory = $true)] - [string]$OpenAiName + [string]$OpenAiName, + + [Parameter(Mandatory = $false)] + [switch]$UseExistingOpenAI, + + [Parameter(Mandatory = $false)] + [string]$ExistingOpenAIEndpoint, + + [Parameter(Mandatory = $false)] + [string]$ExistingOpenAIResourceGroup, + + [Parameter(Mandatory = $false)] + [string]$ExistingOpenAISubscriptionId, + + [Parameter(Mandatory = $false)] + [string]$AzureCloudEnvironment ) +function Write-InfoMessage { + param([string]$Message) + Write-Host "INFO: $Message" -ForegroundColor Cyan +} + +function Write-SuccessMessage { + param([string]$Message) + Write-Host "SUCCESS: $Message" -ForegroundColor Green +} + +function Write-WarningMessage { + param([string]$Message) + Write-Host "WARNING: $Message" -ForegroundColor Yellow +} + +function Test-AzureCliAuthenticated { + try { + az account show --output none 2>$null + return ($LASTEXITCODE -eq 0) + } + catch { + return $false + } +} + +function Test-AzureCloudExists { + param([string]$CloudName) + + try { + az cloud show --name $CloudName --output none 2>$null + return ($LASTEXITCODE -eq 0) + } + catch { + return $false + } +} + +function Get-CurrentAzureCloudName { + try { + return (az cloud show --query name -o tsv 2>$null) + } + catch { + return $null + } +} + +function Read-AzureCloudMenuSelection { + param([int]$TimeoutSeconds = 10) + + $options = @( + [PSCustomObject]@{ Key = '1'; Label = 'Public Azure'; Value = 'AzureCloud' }, + [PSCustomObject]@{ Key = '2'; Label = 'Azure Government'; Value = 'AzureUSGovernment' }, + [PSCustomObject]@{ Key = '3'; Label = 'Custom Azure CLI cloud'; Value = '__custom__' } + ) + + if (-not [Environment]::UserInteractive) { + return $options[0].Value + } + + try { + $selectedIndex = 0 + $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) + $bufferWidth = 120 + $lastStatusLine = "" + + Write-Host "Select the Azure cloud to use:" -ForegroundColor Cyan + for ($index = 0; $index -lt $options.Count; $index++) { + Write-Host (" [{0}] {1}" -f $options[$index].Key, $options[$index].Label) + } + Write-Host "" + + while ($true) { + $remainingSeconds = [Math]::Max(0, [int][Math]::Ceiling(($deadline - [DateTime]::UtcNow).TotalSeconds)) + + $statusLine = "Current selection: [{0}] {1}. Use Up/Down to change, Enter to confirm, or press 1-3. Default is #1 in {2}s." -f $options[$selectedIndex].Key, $options[$selectedIndex].Label, $remainingSeconds + if ($statusLine -ne $lastStatusLine) { + Write-Host (("`r" + $statusLine).PadRight($bufferWidth)) -NoNewline -ForegroundColor Green + $lastStatusLine = $statusLine + } + + if ($remainingSeconds -le 0) { + Write-Host "" + return $options[$selectedIndex].Value + } + + $pollUntil = [DateTime]::UtcNow.AddMilliseconds(250) + while ([DateTime]::UtcNow -lt $pollUntil) { + if ([Console]::KeyAvailable) { + $key = [Console]::ReadKey($true) + + switch ($key.Key) { + 'UpArrow' { + $selectedIndex = if ($selectedIndex -eq 0) { $options.Count - 1 } else { $selectedIndex - 1 } + break + } + 'DownArrow' { + $selectedIndex = if ($selectedIndex -eq ($options.Count - 1)) { 0 } else { $selectedIndex + 1 } + break + } + 'Enter' { + Write-Host "" + return $options[$selectedIndex].Value + } + 'D1' { Write-Host ""; return $options[0].Value } + 'NumPad1' { Write-Host ""; return $options[0].Value } + 'D2' { Write-Host ""; return $options[1].Value } + 'NumPad2' { Write-Host ""; return $options[1].Value } + 'D3' { Write-Host ""; return $options[2].Value } + 'NumPad3' { Write-Host ""; return $options[2].Value } + } + + switch ($key.KeyChar) { + '1' { + Write-Host "" + return $options[0].Value + } + '2' { + Write-Host "" + return $options[1].Value + } + '3' { + Write-Host "" + return $options[2].Value + } + } + } + + Start-Sleep -Milliseconds 50 + } + } + } + catch { + Write-Host "Select the Azure cloud to use:" -ForegroundColor Cyan + Write-Host "1. Public Azure (AzureCloud)" + Write-Host "2. Azure Government (AzureUSGovernment)" + Write-Host "3. Custom Azure CLI cloud" + $selection = Read-Host "Enter 1, 2, or 3" + + switch ($selection.Trim()) { + '1' { return 'AzureCloud' } + '2' { return 'AzureUSGovernment' } + '3' { return '__custom__' } + default { return $null } + } + } +} + +function Resolve-AzureCloudEnvironment { + param([string]$RequestedCloudName) + + $candidateCloudName = $RequestedCloudName + + while ($true) { + if ([string]::IsNullOrWhiteSpace($candidateCloudName)) { + if (-not [Environment]::UserInteractive) { + return 'AzureCloud' + } + + $menuSelection = Read-AzureCloudMenuSelection + + if ($menuSelection -eq '__custom__') { + $candidateCloudName = Read-Host "Enter the registered Azure CLI custom cloud name" + } elseif (-not [string]::IsNullOrWhiteSpace($menuSelection)) { + $candidateCloudName = $menuSelection + } else { + Write-WarningMessage "Invalid cloud selection." + $candidateCloudName = $null + continue + } + } + + if (Test-AzureCloudExists -CloudName $candidateCloudName) { + return $candidateCloudName + } + + if (-not [Environment]::UserInteractive) { + throw "Azure CLI cloud '$candidateCloudName' is not registered. Register it first with 'az cloud register' or provide AzureCloud/AzureUSGovernment." + } + + Write-WarningMessage "Azure CLI cloud '$candidateCloudName' is not registered on this machine." + Write-InfoMessage "Use 'az cloud list -o table' to see available clouds, or 'az cloud register' for a custom cloud." + $candidateCloudName = $null + } +} + +function Ensure-AzureCliAuthenticated { + param([string]$RequestedCloudName) + + $targetCloudName = Resolve-AzureCloudEnvironment -RequestedCloudName $RequestedCloudName + $currentCloudName = Get-CurrentAzureCloudName + + if ([string]::IsNullOrWhiteSpace($currentCloudName)) { + Write-InfoMessage "Setting Azure CLI cloud to '$targetCloudName'." + az cloud set --name $targetCloudName | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Failed to set Azure CLI cloud to '$targetCloudName'." + } + } elseif ($currentCloudName -ne $targetCloudName) { + Write-InfoMessage "Switching Azure CLI cloud from '$currentCloudName' to '$targetCloudName'." + az cloud set --name $targetCloudName | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Failed to switch Azure CLI cloud to '$targetCloudName'." + } + } + + if (Test-AzureCliAuthenticated) { + Write-SuccessMessage "Azure CLI is authenticated" + return $targetCloudName + } + + if (-not [Environment]::UserInteractive) { + throw "Azure CLI is not authenticated. Run 'az login' first or provide an authenticated non-interactive context." + } + + Write-WarningMessage "Azure CLI is not authenticated for cloud '$targetCloudName'." + Write-InfoMessage "Running 'az login'..." + az login | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Azure CLI login failed for cloud '$targetCloudName'." + } + + if (-not (Test-AzureCliAuthenticated)) { + throw "Azure CLI authentication could not be verified after login." + } + + Write-SuccessMessage "Azure CLI is authenticated" + return $targetCloudName +} + # Ensure Azure CLI is installed if (-not (Get-Command "az" -ErrorAction SilentlyContinue)) { Write-Error "Azure CLI is not installed. Please install it before running this script." exit 1 } + +Write-Host "***************************************************" +Write-Host "Initialize Azure Environment" -ForegroundColor Cyan +Write-Host "- Creates or validates the resource group, ACR, and Azure OpenAI" +Write-Host "- Enables ACR admin access for deployment credential retrieval" +Write-Host "- Produces values used by Azure CLI, AZD/Bicep, Terraform, and GitHub workflows" +Write-Host "***************************************************" + +$AzureCloudEnvironment = Ensure-AzureCliAuthenticated -RequestedCloudName $AzureCloudEnvironment + Write-Host "***************************************************" # Step 1: Verify the user is an owner of the subscription $subscription = az account show --query id -o tsv $userObjectId = az ad signed-in-user show --query id -o tsv +$existingOpenAiResourceGroupResolved = if ($ExistingOpenAIResourceGroup) { $ExistingOpenAIResourceGroup } else { $ResourceGroupName } +$existingOpenAiSubscriptionIdResolved = if ($ExistingOpenAISubscriptionId) { $ExistingOpenAISubscriptionId } else { $subscription } +$resolvedOpenAiEndpoint = $ExistingOpenAIEndpoint + $ownerAssignments = az role assignment list --assignee $userObjectId --subscription $subscription --query "[?roleDefinitionName=='Owner']" -o json if (($ownerAssignments | ConvertFrom-Json).Count -eq 0) { Write-Error "You are not an Owner of the current subscription ($subscription)." @@ -64,6 +345,9 @@ if (($ownerAssignments | ConvertFrom-Json).Count -eq 0) { Write-Host "Verified: You are an Owner on subscription $subscription." } +Write-Host "Current Azure cloud: $AzureCloudEnvironment" +Write-Host "Current subscription: $subscription" + # Check if the ACR name is available $ACRNameAvailable = az acr check-name --name $ACRName -o json | ConvertFrom-Json $acrExists = az acr show --name $ACRName --resource-group $ResourceGroupName --query "name" -o tsv 2>$null @@ -98,46 +382,80 @@ $acrCreds = az acr credential show --name $ACRName --resource-group $ResourceGro $acrUsername = $acrCreds.username $acrPassword = $acrCreds.passwords[0].value $acrLoginServer = az acr show --name $ACRName --resource-group $ResourceGroupName --query "loginServer" -o tsv +$acrResourceId = az acr show --name $ACRName --resource-group $ResourceGroupName --query "id" -o tsv -# Step 4: Deploy Azure OpenAI instance -$openAiExists = az cognitiveservices account show --name $OpenAiName --resource-group $ResourceGroupName --query "name" -o tsv 2>$null -if (-not $openAiExists) { - $cmdOutput = az cognitiveservices account create ` - --name $OpenAiName ` - --resource-group $ResourceGroupName ` - --kind OpenAI ` - --sku S0 ` - --location $AzureRegion ` - --yes 2>&1 - $exitCode = $LASTEXITCODE - $errMsg = $cmdOutput - if ($exitCode -ne 0) { - if ($errMsg -match "deleted"){ - write-warning "Cognitive Services account '$OpenAiName' appears to be soft-deleted. Attempting recovery..." - $recoverOutput = az cognitiveservices account recover --name $OpenAiName --resource-group $ResourceGroupName --location $AzureRegion 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to recover Cognitive Services account '$OpenAiName'. Error: $($recoverOutput | Out-String)" - exit 1 +# Step 4: Deploy or reuse Azure OpenAI instance +if ($UseExistingOpenAI) { + $openAiExists = az cognitiveservices account show --name $OpenAiName --resource-group $existingOpenAiResourceGroupResolved --subscription $existingOpenAiSubscriptionIdResolved --query "name" -o tsv 2>$null + if (-not $openAiExists) { + Write-Error "Existing Azure OpenAI instance '$OpenAiName' was not found in resource group '$existingOpenAiResourceGroupResolved' and subscription '$existingOpenAiSubscriptionIdResolved'." + exit 1 + } + + if (-not $resolvedOpenAiEndpoint) { + $resolvedOpenAiEndpoint = az cognitiveservices account show --name $OpenAiName --resource-group $existingOpenAiResourceGroupResolved --subscription $existingOpenAiSubscriptionIdResolved --query "properties.endpoint" -o tsv 2>$null + if ($LASTEXITCODE -ne 0 -or -not $resolvedOpenAiEndpoint) { + Write-Error "Unable to resolve the endpoint for existing Azure OpenAI instance '$OpenAiName'." + exit 1 + } + } + + Write-Host "Using existing Azure OpenAI instance '$OpenAiName'." + Write-Host "Azure OpenAI endpoint: $resolvedOpenAiEndpoint" +} else { + $openAiExists = az cognitiveservices account show --name $OpenAiName --resource-group $ResourceGroupName --query "name" -o tsv 2>$null + if (-not $openAiExists) { + $cmdOutput = az cognitiveservices account create ` + --name $OpenAiName ` + --resource-group $ResourceGroupName ` + --kind OpenAI ` + --sku S0 ` + --location $AzureRegion ` + --yes 2>&1 + $exitCode = $LASTEXITCODE + $errMsg = $cmdOutput + if ($exitCode -ne 0) { + if ($errMsg -match "deleted"){ + write-warning "Cognitive Services account '$OpenAiName' appears to be soft-deleted. Attempting recovery..." + $recoverOutput = az cognitiveservices account recover --name $OpenAiName --resource-group $ResourceGroupName --location $AzureRegion 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to recover Cognitive Services account '$OpenAiName'. Error: $($recoverOutput | Out-String)" + exit 1 + } else { + write-host "Recovered soft-deleted Cognitive Services account '$OpenAiName'." + } } else { - write-host "Recovered soft-deleted Cognitive Services account '$OpenAiName'." + write-error "Failed to create Cognitive Services account '$OpenAiName'. Error: $($errMsg | Out-String)" + exit 1 } } else { - write-error "Failed to create Cognitive Services account '$OpenAiName'. Error: $($errMsg | Out-String)" + Write-Host "Azure OpenAI instance '$OpenAiName' deployed." } - - } else { - Write-Host "Azure OpenAI instance '$OpenAiName' deployed." - } -} else { - Write-Warning "Azure OpenAI instance '$OpenAiName' already exists." + } else { + Write-Warning "Azure OpenAI instance '$OpenAiName' already exists." + } + + $resolvedOpenAiEndpoint = az cognitiveservices account show --name $OpenAiName --resource-group $ResourceGroupName --query "properties.endpoint" -o tsv 2>$null } Write-Host "***************************************************" -Write-Host "The information provided below must be configured" -Write-Host " as secrets in the target Github Repo." +Write-Host "Deployment bootstrap values" +Write-Host "These values are useful for deployment scripts, ACR builds," +Write-Host "and GitHub Actions secrets." Write-Host "***************************************************" +Write-Host "ACR_NAME: $ACRName" +Write-Host "ACR_RESOURCE_GROUP: $ResourceGroupName" +Write-Host "ACR_RESOURCE_ID: $acrResourceId" Write-Host "ACR_LOGIN_SERVER: $acrLoginServer" Write-Host "ACR_PASSWORD: $acrPassword" Write-Host "ACR_USERNAME: $acrUsername" +Write-Host "AZURE_CLOUD_ENVIRONMENT: $AzureCloudEnvironment" +Write-Host "AZURE_OPENAI_NAME: $OpenAiName" +Write-Host "AZURE_OPENAI_ENDPOINT: $resolvedOpenAiEndpoint" +Write-Host "AZURE_OPENAI_RESOURCE_GROUP: $(if ($UseExistingOpenAI) { $existingOpenAiResourceGroupResolved } else { $ResourceGroupName })" +Write-Host "AZURE_OPENAI_SUBSCRIPTION_ID: $(if ($UseExistingOpenAI) { $existingOpenAiSubscriptionIdResolved } else { $subscription })" +Write-Host "***************************************************" +Write-Host "Example ACR build command from the repository root:" +Write-Host "az acr build --registry $ACRName --file application/single_app/Dockerfile --image simple-chat:latest ." Write-Host "***************************************************" Write-Host "Azure environment initialization complete." diff --git a/deployers/Initialize-EntraApplication.ps1 b/deployers/Initialize-EntraApplication.ps1 index 5a411be00..a581da7f3 100644 --- a/deployers/Initialize-EntraApplication.ps1 +++ b/deployers/Initialize-EntraApplication.ps1 @@ -22,6 +22,10 @@ .PARAMETER AppRolesJsonPath Path to appRegistrationRoles.json file (Optional, default: ./appRegistrationRoles.json) +.PARAMETER AzureCloudEnvironment + Optional Azure cloud name. You can pass AzureCloud, AzureUSGovernment, or a custom Azure CLI cloud name. + If omitted during interactive use, the script prompts for the target cloud before login. + .EXAMPLE .\Initialize-EntraApplication.ps1 -AppName "simplechat" -Environment "dev" @@ -40,29 +44,27 @@ [CmdletBinding()] param( - [Parameter(Mandatory = $true)] - [ValidateLength(3, 12)] # Length between 3 and 12 - [ValidatePattern('^[a-zA-Z0-9]+$')] # Only letters and numbers + [Parameter(Mandatory = $false)] [string]$AppName, - [Parameter(Mandatory = $true)] - [ValidateLength(2, 10)] # Length between 2 and 10 - [ValidatePattern('^[a-zA-Z0-9]+$')] # Only letters and numbers + [Parameter(Mandatory = $false)] [string]$Environment, [Parameter(Mandatory = $false)] - [string]$AppServiceName = "$AppName-$Environment-app", + [string]$AppServiceName, [Parameter(Mandatory = $false)] [int]$SecretExpirationDays = 180, [Parameter(Mandatory = $false)] - [string]$AppRolesJsonPath = "./appRegistrationRoles.json" + [string]$AppRolesJsonPath = "./appRegistrationRoles.json", + + [Parameter(Mandatory = $false)] + [string]$AzureCloudEnvironment ) # Script configuration $ErrorActionPreference = "Stop" -$appRegistrationName = "$AppName-$Environment-ar" #region Helper Functions @@ -86,6 +88,261 @@ function Write-WarningMessage { Write-Host "WARNING: $Message" -ForegroundColor Yellow } +function Get-CommandOutputText { + param($CommandOutput) + + if ($null -eq $CommandOutput) { + return "" + } + + return (($CommandOutput | Out-String).Trim()) +} + +function Test-PermissionGrantRequiredMessage { + param([string]$Message) + + return ($Message -match 'az ad app permission grant' -and $Message -match 'needed to make the change effective') +} + +function Try-GrantAppPermissions { + param( + [string]$AppId, + [string]$ApiId + ) + + $grantOutput = az ad app permission grant --id $AppId --api $ApiId --only-show-errors 2>&1 + $grantExitCode = $LASTEXITCODE + $grantOutputText = Get-CommandOutputText -CommandOutput $grantOutput + + return [PSCustomObject]@{ + Succeeded = ($grantExitCode -eq 0) + OutputText = $grantOutputText + } +} + +function Test-AzureCloudExists { + param([string]$CloudName) + + try { + az cloud show --name $CloudName --output none 2>$null + return ($LASTEXITCODE -eq 0) + } + catch { + return $false + } +} + +function Read-AzureCloudMenuSelection { + param([int]$TimeoutSeconds = 10) + + $options = @( + [PSCustomObject]@{ Key = '1'; Label = 'Public Azure'; Value = 'AzureCloud' }, + [PSCustomObject]@{ Key = '2'; Label = 'Azure Government'; Value = 'AzureUSGovernment' }, + [PSCustomObject]@{ Key = '3'; Label = 'Custom Azure CLI cloud'; Value = '__custom__' } + ) + + if (-not [Environment]::UserInteractive) { + return $options[0].Value + } + + try { + $selectedIndex = 0 + $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) + $bufferWidth = 120 + $lastStatusLine = "" + + Write-Host "Select the Azure cloud to use:" -ForegroundColor Cyan + for ($index = 0; $index -lt $options.Count; $index++) { + Write-Host (" [{0}] {1}" -f $options[$index].Key, $options[$index].Label) + } + Write-Host "" + + while ($true) { + $remainingSeconds = [Math]::Max(0, [int][Math]::Ceiling(($deadline - [DateTime]::UtcNow).TotalSeconds)) + + $statusLine = "Current selection: [{0}] {1}. Use Up/Down to change, Enter to confirm, or press 1-3. Default is #1 in {2}s." -f $options[$selectedIndex].Key, $options[$selectedIndex].Label, $remainingSeconds + if ($statusLine -ne $lastStatusLine) { + Write-Host (("`r" + $statusLine).PadRight($bufferWidth)) -NoNewline -ForegroundColor Green + $lastStatusLine = $statusLine + } + + if ($remainingSeconds -le 0) { + Write-Host "" + return $options[$selectedIndex].Value + } + + $pollUntil = [DateTime]::UtcNow.AddMilliseconds(250) + while ([DateTime]::UtcNow -lt $pollUntil) { + if ([Console]::KeyAvailable) { + $key = [Console]::ReadKey($true) + + switch ($key.Key) { + 'UpArrow' { + $selectedIndex = if ($selectedIndex -eq 0) { $options.Count - 1 } else { $selectedIndex - 1 } + break + } + 'DownArrow' { + $selectedIndex = if ($selectedIndex -eq ($options.Count - 1)) { 0 } else { $selectedIndex + 1 } + break + } + 'Enter' { + Write-Host "" + return $options[$selectedIndex].Value + } + 'D1' { Write-Host ""; return $options[0].Value } + 'NumPad1' { Write-Host ""; return $options[0].Value } + 'D2' { Write-Host ""; return $options[1].Value } + 'NumPad2' { Write-Host ""; return $options[1].Value } + 'D3' { Write-Host ""; return $options[2].Value } + 'NumPad3' { Write-Host ""; return $options[2].Value } + } + + switch ($key.KeyChar) { + '1' { + Write-Host "" + return $options[0].Value + } + '2' { + Write-Host "" + return $options[1].Value + } + '3' { + Write-Host "" + return $options[2].Value + } + } + } + + Start-Sleep -Milliseconds 50 + } + } + } + catch { + Write-Host "Select the Azure cloud to use:" -ForegroundColor Cyan + Write-Host "1. Public Azure (AzureCloud)" + Write-Host "2. Azure Government (AzureUSGovernment)" + Write-Host "3. Custom Azure CLI cloud" + $selection = Read-Host "Enter 1, 2, or 3" + + switch ($selection.Trim()) { + '1' { return 'AzureCloud' } + '2' { return 'AzureUSGovernment' } + '3' { return '__custom__' } + default { return $null } + } + } +} + +function Resolve-AzureCloudEnvironment { + param([string]$RequestedCloudName) + + $candidateCloudName = $RequestedCloudName + + while ($true) { + if ([string]::IsNullOrWhiteSpace($candidateCloudName)) { + if (-not [Environment]::UserInteractive) { + return 'AzureCloud' + } + + $menuSelection = Read-AzureCloudMenuSelection + + if ($menuSelection -eq '__custom__') { + $candidateCloudName = Read-Host "Enter the registered Azure CLI custom cloud name" + } elseif (-not [string]::IsNullOrWhiteSpace($menuSelection)) { + $candidateCloudName = $menuSelection + } else { + Write-WarningMessage "Invalid cloud selection." + $candidateCloudName = $null + continue + } + } + + if (Test-AzureCloudExists -CloudName $candidateCloudName) { + return $candidateCloudName + } + + if (-not [Environment]::UserInteractive) { + throw "Azure CLI cloud '$candidateCloudName' is not registered. Register it first with 'az cloud register' or provide AzureCloud/AzureUSGovernment." + } + + Write-WarningMessage "Azure CLI cloud '$candidateCloudName' is not registered on this machine." + Write-InfoMessage "Use 'az cloud list -o table' to see available clouds, or 'az cloud register' for a custom cloud." + $candidateCloudName = $null + } +} + +function Get-ValidationMessage { + param( + [string]$Value, + [string]$DisplayName, + [int]$MinLength, + [int]$MaxLength, + [string]$AllowedPattern, + [string]$AllowedPatternDescription + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return "$DisplayName is required." + } + + $validationIssues = @() + + if ($Value.Length -lt $MinLength -or $Value.Length -gt $MaxLength) { + $validationIssues += "$DisplayName must be between $MinLength and $MaxLength characters long" + } + + if ($Value -notmatch $AllowedPattern) { + $validationIssues += "$DisplayName must use only $AllowedPatternDescription" + } + + if ($validationIssues.Count -eq 0) { + return $null + } + + return ($validationIssues -join '; ') + '.' +} + +function Resolve-ValidatedInput { + param( + [string]$InitialValue, + [string]$DisplayName, + [int]$MinLength, + [int]$MaxLength, + [string]$AllowedPattern, + [string]$AllowedPatternDescription, + [string]$PromptExample + ) + + $candidateValue = $InitialValue + + while ($true) { + $validationMessage = Get-ValidationMessage ` + -Value $candidateValue ` + -DisplayName $DisplayName ` + -MinLength $MinLength ` + -MaxLength $MaxLength ` + -AllowedPattern $AllowedPattern ` + -AllowedPatternDescription $AllowedPatternDescription + + if (-not $validationMessage) { + return $candidateValue + } + + if (-not [Environment]::UserInteractive) { + throw "$validationMessage Example: $PromptExample" + } + + if (-not [string]::IsNullOrWhiteSpace($candidateValue)) { + Write-WarningMessage "$DisplayName '$candidateValue' is incompatible. $validationMessage" + } else { + Write-WarningMessage $validationMessage + } + + Write-InfoMessage "$DisplayName requirements: $MinLength-$MaxLength characters, $AllowedPatternDescription. Example: $PromptExample" + $candidateValue = Read-Host "Enter $DisplayName" + } +} + function Test-AzureCliInstalled { try { $azVersion = az version 2>&1 | Out-Null @@ -136,6 +393,44 @@ function Get-AppServiceDomain { } } +function Ensure-AzureCliAuthenticated { + param([string]$RequestedCloudName) + + $targetCloudName = Resolve-AzureCloudEnvironment -RequestedCloudName $RequestedCloudName + $currentCloudName = Get-CloudEnvironment + + if ($currentCloudName -ne $targetCloudName) { + Write-InfoMessage "Switching Azure CLI cloud from '$currentCloudName' to '$targetCloudName'..." + az cloud set --name $targetCloudName | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Failed to switch Azure CLI cloud to '$targetCloudName'." + } + } + + if (Test-AzureCliAuthenticated) { + Write-SuccessMessage "Azure CLI is authenticated" + return $targetCloudName + } + + if (-not [Environment]::UserInteractive) { + throw "Azure CLI is not authenticated. Run 'az login' first or provide an authenticated non-interactive context." + } + + Write-WarningMessage "Azure CLI is not authenticated for cloud '$targetCloudName'." + Write-InfoMessage "Running 'az login'..." + az login | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Azure CLI login failed for cloud '$targetCloudName'." + } + + if (-not (Test-AzureCliAuthenticated)) { + throw "Azure CLI authentication could not be verified after login." + } + + Write-SuccessMessage "Azure CLI is authenticated" + return $targetCloudName +} + #endregion #region Main Script @@ -146,6 +441,30 @@ Write-Host "========================================`n" -ForegroundColor Cyan try { #region Dependency Checks + + $AppName = Resolve-ValidatedInput ` + -InitialValue $AppName ` + -DisplayName "AppName" ` + -MinLength 3 ` + -MaxLength 12 ` + -AllowedPattern '^[a-zA-Z0-9]+$' ` + -AllowedPatternDescription "letters and numbers only, with no spaces or hyphens" ` + -PromptExample "simplechat" + + $Environment = Resolve-ValidatedInput ` + -InitialValue $Environment ` + -DisplayName "Environment" ` + -MinLength 2 ` + -MaxLength 10 ` + -AllowedPattern '^[a-zA-Z0-9]+$' ` + -AllowedPatternDescription "letters and numbers only, with no spaces or hyphens" ` + -PromptExample "dev" + + if ([string]::IsNullOrWhiteSpace($AppServiceName)) { + $AppServiceName = "$AppName-$Environment-app" + } + + $appRegistrationName = "$AppName-$Environment-ar" Write-InfoMessage "Performing dependency checks..." @@ -155,11 +474,7 @@ try { } Write-SuccessMessage "Azure CLI is installed" - # Check Azure CLI authentication - if (-not (Test-AzureCliAuthenticated)) { - throw "Azure CLI is not authenticated. Please run 'az login' first" - } - Write-SuccessMessage "Azure CLI is authenticated" + $cloudEnvironment = Ensure-AzureCliAuthenticated -RequestedCloudName $AzureCloudEnvironment # Check app roles JSON file exists if (-not (Test-Path $AppRolesJsonPath)) { @@ -178,8 +493,6 @@ try { $tenantId = $accountInfo.tenantId Write-InfoMessage "Tenant ID: $tenantId" - # Get cloud environment - $cloudEnvironment = Get-CloudEnvironment Write-InfoMessage "Cloud Environment: $cloudEnvironment" # Get Graph URL @@ -285,12 +598,16 @@ try { try { $body = @{ web = @{ logoutUrl = $logoutUrl } } | ConvertTo-Json -Compress - - az rest --method PATCH ` + + $logoutResult = az rest --method PATCH ` --uri "$graphUrl/v1.0/applications/$($appRegistration.id)" ` --headers "Content-Type=application/json" ` - --body $body | Out-Null - + --body ($body -replace '"', '\"') 2>&1 + + if ($LASTEXITCODE -ne 0) { + throw "Failed to configure logout URL. Azure CLI returned: $(Get-CommandOutputText -CommandOutput $logoutResult)" + } + Write-SuccessMessage "Logout URL configured" } catch { @@ -337,30 +654,101 @@ try { ) $permissionErrors = @() + $permissionsRequiringPortalConsent = @() + $permissionGrantAttempted = $false + $permissionGrantSucceeded = $false + $permissionGrantOutput = "" foreach ($permission in $permissions) { try { Write-InfoMessage "Adding permission: $($permission.Name)" - - az ad app permission add ` - --id $appRegistration.id ` + + $permissionResult = az ad app permission add ` + --id $appRegistration.appId ` --api $microsoftGraphId ` - --api-permissions "$($permission.Id)=Scope" 2>&1 | Out-Null - + --only-show-errors ` + --api-permissions "$($permission.Id)=Scope" 2>&1 + + $permissionResultText = Get-CommandOutputText -CommandOutput $permissionResult + + if ($LASTEXITCODE -ne 0) { + if (Test-PermissionGrantRequiredMessage -Message $permissionResultText) { + Write-SuccessMessage "Added: $($permission.Name)" + + if (-not $permissionGrantAttempted) { + Write-InfoMessage "Azure CLI indicates that a permission grant is needed. Attempting 'az ad app permission grant'..." + $grantResult = Try-GrantAppPermissions -AppId $appRegistration.appId -ApiId $microsoftGraphId + $permissionGrantAttempted = $true + $permissionGrantSucceeded = $grantResult.Succeeded + $permissionGrantOutput = $grantResult.OutputText + + if ($permissionGrantSucceeded) { + Write-SuccessMessage "Permission grant command completed successfully." + } else { + Write-WarningMessage "Permission grant command did not complete successfully." + if (-not [string]::IsNullOrWhiteSpace($permissionGrantOutput)) { + Write-WarningMessage $permissionGrantOutput + } + } + } + + if (-not $permissionGrantSucceeded) { + $permissionsRequiringPortalConsent += $permission.Name + Write-InfoMessage "Permission '$($permission.Name)' still needs portal approval or admin consent to become effective." + } + + continue + } + + throw $permissionResultText + } + Write-SuccessMessage "Added: $($permission.Name)" + + if (-not [string]::IsNullOrWhiteSpace($permissionResultText) -and (Test-PermissionGrantRequiredMessage -Message $permissionResultText)) { + if (-not $permissionGrantAttempted) { + Write-InfoMessage "Azure CLI indicates that a permission grant is needed. Attempting 'az ad app permission grant'..." + $grantResult = Try-GrantAppPermissions -AppId $appRegistration.appId -ApiId $microsoftGraphId + $permissionGrantAttempted = $true + $permissionGrantSucceeded = $grantResult.Succeeded + $permissionGrantOutput = $grantResult.OutputText + + if ($permissionGrantSucceeded) { + Write-SuccessMessage "Permission grant command completed successfully." + } else { + Write-WarningMessage "Permission grant command did not complete successfully." + if (-not [string]::IsNullOrWhiteSpace($permissionGrantOutput)) { + Write-WarningMessage $permissionGrantOutput + } + } + } + + if (-not $permissionGrantSucceeded) { + $permissionsRequiringPortalConsent += $permission.Name + Write-InfoMessage "Permission '$($permission.Name)' still needs portal approval or admin consent to become effective." + } + } } catch { - $permissionErrors += "Failed to add $($permission.Name): $_" - Write-WarningMessage "Failed to add permission $($permission.Name)" + $permissionErrorMessage = "Failed to add $($permission.Name): $_" + $permissionErrors += $permissionErrorMessage + Write-WarningMessage $permissionErrorMessage } } + + if ($permissionsRequiringPortalConsent.Count -gt 0) { + $uniquePermissionsRequiringPortalConsent = $permissionsRequiringPortalConsent | Sort-Object -Unique + Write-WarningMessage "Some permissions still require portal approval or admin consent to become effective." + Write-InfoMessage "Permissions awaiting approval: $($uniquePermissionsRequiringPortalConsent -join ', ')" + Write-InfoMessage "Open Azure Portal > Entra ID > App registrations > $appRegistrationName > API permissions, then grant admin consent if required by your tenant." + } if ($permissionErrors.Count -eq 0) { Write-SuccessMessage "All API permissions added successfully" } else { Write-WarningMessage "Some permissions could not be added automatically" - Write-InfoMessage "Missing permissions can be added manually in Azure Portal" + Write-InfoMessage "Review the warnings above, then add or approve any missing permissions in Azure Portal if needed." } #endregion diff --git a/deployers/azure.yaml b/deployers/azure.yaml index 4ab51aa9c..9ccd66979 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -1,4 +1,4 @@ -name: SimpleChat +name: simplechat metadata: template: custom infra: @@ -13,19 +13,37 @@ services: context: ../../ dockerfile: application/single_app/Dockerfile hooks: + preprovision: + # this is run before infrastructure provisioning begins + # primary use is to explain prerequisites and stop early when required networking inputs are missing + posix: + shell: sh + run: | + set -e + echo "========================================" + echo "PRE-PROVISION: Validating prerequisites" + echo "========================================" + python3 ./bicep/validate_azd_prerequisites.py + windows: + shell: pwsh + run: | + Write-Host "========================================" + Write-Host "PRE-PROVISION: Validating prerequisites" + Write-Host "========================================" + python .\bicep\validate_azd_prerequisites.py + postprovision: - # this is run after the infrastructure has been provisioned but before services are deployed. - # primary use is to configure application settings and permissions + # this is run after the infrastructure has been provisioned but before services are deployed. + # primary use is to configure application settings and permissions posix: shell: sh run: | set -e - + echo "========================================" echo "POST-PROVISION: Starting configuration" echo "========================================" - - # Set up variables + export var_acrName=${var_acrname} export var_configureApplication=${var_configureApplication} export var_cosmosDb_uri=${var_cosmosDb_uri} @@ -36,9 +54,10 @@ hooks: export var_keyVaultName=${var_keyVaultName} export var_authenticationType=${var_authenticationType} export var_openAIEndpoint=${var_openAIEndpoint} + export var_openAISubscriptionId=${var_openAISubscriptionId} export var_openAIResourceGroup=${var_openAIResourceGroup} - export var_openAIGPTModel=${var_openAIGPTModel} - export var_openAITextEmbeddingModel=${var_openAITextEmbeddingModel} + export var_openAIGPTModels='${var_openAIGPTModels}' + export var_openAIEmbeddingModels='${var_openAIEmbeddingModels}' export var_blobStorageEndpoint=${var_blobStorageEndpoint} export var_contentSafetyEndpoint=${var_contentSafetyEndpoint} export var_searchServiceEndpoint=${var_searchServiceEndpoint} @@ -49,7 +68,113 @@ hooks: export var_videoIndexerAccountId=${var_videoIndexerAccountId} export var_speechServiceEndpoint=${var_speechServiceEndpoint} - # Execute post-configuration script if enabled + is_mfa_error() { + printf '%s' "$1" | grep -qiE 'AADSTS50076|multi-factor authentication|claims challenge' + } + + print_manual_cosmos_firewall_message() { + runner_public_ip="$1" + echo "✗ ERROR: Azure CLI requires multi-factor authentication before it can update the Cosmos DB firewall." >&2 + echo " Manually add IP ${runner_public_ip} to Azure Portal -> Cosmos DB account '${var_cosmosDb_accountName}' -> Networking -> Firewall in resource group '${var_rgName}'." >&2 + echo " If you already added the IP manually, allow up to 30 minutes for the firewall change to propagate before rerunning 'azd provision' or 'azd up'." >&2 + } + + test_cosmos_runner_access() { + python3 -c "from azure.cosmos import CosmosClient; import os; client = CosmosClient(os.environ['var_cosmosDb_uri'], os.environ['var_cosmosDb_key']); next(client.list_databases(), None); print('ok')" >/dev/null 2>&1 + } + + runner_ip_matches_rules() { + current_rules="$1" + runner_public_ip="$2" + CURRENT_RULES="${current_rules}" RUNNER_PUBLIC_IP="${runner_public_ip}" python3 -c "import ipaddress, os; rules = [rule.strip() for rule in os.environ.get('CURRENT_RULES', '').split(',') if rule.strip()]; ip = ipaddress.ip_address(os.environ['RUNNER_PUBLIC_IP']); matched = any(ip in ipaddress.ip_network(rule, strict=False) if '/' in rule else ip == ipaddress.ip_address(rule) for rule in rules); raise SystemExit(0 if matched else 1)" + } + + wait_for_cosmos_runner_access() { + runner_public_ip="$1" + max_attempts=20 + attempt=1 + + while [ ${attempt} -le ${max_attempts} ]; do + if test_cosmos_runner_access; then + echo "✓ CosmosDB data-plane access is now available for deployment runner IP ${runner_public_ip}" + return 0 + fi + + echo " Waiting for CosmosDB firewall propagation for ${runner_public_ip} (${attempt}/${max_attempts})..." + sleep 30 + attempt=$((attempt + 1)) + done + + echo "✗ ERROR: CosmosDB firewall rule for ${runner_public_ip} is configured but data-plane access is still not available." >&2 + echo " Firewall changes can take up to 30 minutes to propagate. Wait and rerun 'azd provision' or 'azd up'." >&2 + return 1 + } + + python_is_virtual_environment() { + python3 -c "import sys; raise SystemExit(0 if sys.prefix != getattr(sys, 'base_prefix', sys.prefix) else 1)" + } + + install_postprovision_python_dependencies() { + install_output_file=$(mktemp) + + if python_is_virtual_environment; then + python3 -m pip install -r ./bicep/requirements.txt > "${install_output_file}" 2>&1 + else + python3 -m pip install --user -r ./bicep/requirements.txt > "${install_output_file}" 2>&1 + fi + + install_exit_code=$? + if [ ${install_exit_code} -ne 0 ]; then + cat "${install_output_file}" >&2 + rm -f "${install_output_file}" + return ${install_exit_code} + fi + + rm -f "${install_output_file}" + return 0 + } + + ensure_cosmos_runner_access() { + current_rules=$(az cosmosdb show \ + --name ${var_cosmosDb_accountName} \ + --resource-group ${var_rgName} \ + --subscription ${AZURE_SUBSCRIPTION_ID} \ + --query "properties.ipRules[].ipAddressOrRange" \ + -o tsv 2>/dev/null | tr '\n' ',' | sed 's/,$//') + + runner_public_ip=$(python3 -c "import urllib.request; print(urllib.request.urlopen('https://api.ipify.org', timeout=10).read().decode().strip())") + if [ -z "${runner_public_ip}" ]; then + echo "✗ ERROR: Failed to resolve the deployment runner public IP for Cosmos firewall access" >&2 + exit 1 + fi + + if runner_ip_matches_rules "${current_rules}" "${runner_public_ip}"; then + echo "✓ Deployment runner IP ${runner_public_ip} is already configured on the CosmosDB firewall" + wait_for_cosmos_runner_access "${runner_public_ip}" || exit 1 + return + fi + + updated_rules=$(python3 -c "existing = '${current_rules}'.strip(','); runner = '${runner_public_ip}'; rules = [rule.strip() for rule in existing.split(',') if rule.strip()]; rules.append(runner); deduped = []; [deduped.append(rule) for rule in rules if rule not in deduped]; print(','.join(deduped))") + update_output=$(az cosmosdb update \ + --name ${var_cosmosDb_accountName} \ + --resource-group ${var_rgName} \ + --subscription ${AZURE_SUBSCRIPTION_ID} \ + --ip-range-filter "${updated_rules}" 2>&1) + if [ $? -ne 0 ]; then + if is_mfa_error "${update_output}"; then + print_manual_cosmos_firewall_message "${runner_public_ip}" + echo " Checking whether CosmosDB access is already available without an Azure CLI firewall update..." >&2 + wait_for_cosmos_runner_access "${runner_public_ip}" || exit 1 + return + fi + echo "✗ ERROR: Failed to allow deployment runner IP ${runner_public_ip} through the CosmosDB firewall" >&2 + echo "${update_output}" >&2 + exit 1 + fi + echo "✓ Added deployment runner IP ${runner_public_ip} to the CosmosDB firewall" + wait_for_cosmos_runner_access "${runner_public_ip}" || exit 1 + } + if [ "${var_configureApplication}" = "true" ]; then echo "" echo "[1/4] Granting permissions to CosmosDB..." @@ -59,16 +184,33 @@ hooks: echo "✗ ERROR: Failed to grant CosmosDB permissions" >&2 exit 1 fi - + echo "" echo "[2/4] Installing Python dependencies..." - if python3 -m pip install --user -r ./bicep/requirements.txt > /dev/null 2>&1; then + if install_postprovision_python_dependencies; then echo "✓ Dependencies installed successfully" else echo "✗ ERROR: Failed to install Python dependencies" >&2 exit 1 fi - + + export var_cosmosDb_key=$(az cosmosdb keys list \ + --name ${var_cosmosDb_accountName} \ + --resource-group ${var_rgName} \ + --subscription ${AZURE_SUBSCRIPTION_ID} \ + --query primaryMasterKey \ + -o tsv) + if [ -z "${var_cosmosDb_key}" ]; then + echo "✗ ERROR: Failed to resolve Cosmos DB primary key for post-deployment configuration" >&2 + exit 1 + fi + + if test_cosmos_runner_access; then + echo "✓ Deployment runner already has CosmosDB data-plane access" + else + ensure_cosmos_runner_access + fi + echo "" echo "[3/4] Running post-deployment configuration..." if python3 ./bicep/postconfig.py; then @@ -77,7 +219,7 @@ hooks: echo "✗ ERROR: Post-deployment configuration failed" >&2 exit 1 fi - + echo "" echo "[4/4] Restarting web service to apply settings..." if az webapp restart --name ${var_webService} --resource-group ${var_rgName}; then @@ -86,7 +228,7 @@ hooks: echo "✗ ERROR: Failed to restart web service" >&2 exit 1 fi - + echo "" echo "========================================" echo "POST-PROVISION: Completed successfully" @@ -99,16 +241,280 @@ hooks: echo "POST-PROVISION: Completed (skipped)" echo "========================================" fi + windows: + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + function Get-TargetSubscriptionId { + if (-not [string]::IsNullOrWhiteSpace($env:AZURE_SUBSCRIPTION_ID)) { + return $env:AZURE_SUBSCRIPTION_ID + } + + if (-not [string]::IsNullOrWhiteSpace($env:var_subscriptionId)) { + return $env:var_subscriptionId + } + + $resolvedSubscriptionId = az account show --query id -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($resolvedSubscriptionId)) { + throw 'Failed to resolve the target subscription ID for postprovision.' + } + + return $resolvedSubscriptionId + } + + function Resolve-ResourceGroupName { + $subscriptionId = Get-TargetSubscriptionId + + if (-not [string]::IsNullOrWhiteSpace($env:var_rgName)) { + $groupExists = az group exists --name $env:var_rgName --subscription $subscriptionId -o tsv 2>$null + if ($LASTEXITCODE -eq 0 -and $groupExists -eq 'true') { + return $env:var_rgName + } + } + + $cosmosAccountName = $env:var_cosmosDb_accountName + if ([string]::IsNullOrWhiteSpace($cosmosAccountName) -and -not [string]::IsNullOrWhiteSpace($env:var_cosmosDb_uri)) { + $cosmosAccountName = ([System.Uri]$env:var_cosmosDb_uri).Host.Split('.')[0] + } + + $resolvedResourceGroup = $null + if (-not [string]::IsNullOrWhiteSpace($cosmosAccountName)) { + $resolvedResourceGroup = az cosmosdb list --subscription $subscriptionId --query "[?name=='$cosmosAccountName'].resourceGroup | [0]" -o tsv 2>$null + } + + if ([string]::IsNullOrWhiteSpace($resolvedResourceGroup) -and -not [string]::IsNullOrWhiteSpace($env:var_webService)) { + $resolvedResourceGroup = az resource list --subscription $subscriptionId --name "$($env:var_webService)" --resource-type Microsoft.Web/sites --query "[0].resourceGroup" -o tsv 2>$null + } + + if ([string]::IsNullOrWhiteSpace($resolvedResourceGroup)) { + throw 'Unable to resolve resource group from var_rgName, Cosmos DB, or Web App.' + } + + $env:var_rgName = $resolvedResourceGroup + return $resolvedResourceGroup + } + + function Ensure-CosmosRunnerAccess { + param( + [string]$AccountName, + [string]$ResourceGroupName, + [string]$SubscriptionId + ) + + $currentRules = az cosmosdb show --name $AccountName --resource-group $ResourceGroupName --subscription $SubscriptionId --query "properties.ipRules[].ipAddressOrRange" -o tsv 2>$null + $runnerPublicIp = (Invoke-RestMethod -Uri 'https://api.ipify.org' -TimeoutSec 10).Trim() + if ([string]::IsNullOrWhiteSpace($runnerPublicIp)) { + throw 'Failed to resolve the deployment runner public IP for Cosmos DB firewall access.' + } + + $ruleList = @() + if (-not [string]::IsNullOrWhiteSpace($currentRules)) { + $ruleList = $currentRules -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + } + + if (Test-RunnerIpMatchesRules -Rules $ruleList -RunnerPublicIp $runnerPublicIp) { + Write-Host "✓ Deployment runner IP $runnerPublicIp is already configured on the CosmosDB firewall" + Wait-ForCosmosRunnerAccess -RunnerPublicIp $runnerPublicIp + return + } + + $updatedRules = @($ruleList + $runnerPublicIp | Select-Object -Unique) -join ',' + $updateOutput = az cosmosdb update --name $AccountName --resource-group $ResourceGroupName --subscription $SubscriptionId --ip-range-filter $updatedRules 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { + if ($updateOutput -match 'AADSTS50076|multi-factor authentication|claims challenge') { + Write-Warning "Azure CLI requires multi-factor authentication before it can update the Cosmos DB firewall. Manually add IP $runnerPublicIp to Azure Portal -> Cosmos DB account '$AccountName' -> Networking -> Firewall in resource group '$ResourceGroupName'. Firewall changes can take up to 30 minutes to propagate, so wait before rerunning 'azd provision' or 'azd up'." + Write-Host " Checking whether CosmosDB access is already available without an Azure CLI firewall update..." + Wait-ForCosmosRunnerAccess -RunnerPublicIp $runnerPublicIp + return + } + + throw "Failed to allow deployment runner IP $runnerPublicIp through the Cosmos DB firewall.`n$updateOutput" + } + + Write-Host "✓ Added deployment runner IP $runnerPublicIp to the CosmosDB firewall" + Wait-ForCosmosRunnerAccess -RunnerPublicIp $runnerPublicIp + } + + function Test-CosmosRunnerAccess { + $accessCheckOutput = python -c "from azure.cosmos import CosmosClient; import os; client = CosmosClient(os.environ['var_cosmosDb_uri'], os.environ['var_cosmosDb_key']); next(client.list_databases(), None); print('ok')" 2>&1 | Out-String + return $LASTEXITCODE -eq 0 + } + + function Test-RunnerIpMatchesRules { + param( + [string[]]$Rules, + [string]$RunnerPublicIp + ) + + $env:CURRENT_RULES = ($Rules | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join ',' + $env:RUNNER_PUBLIC_IP = $RunnerPublicIp + python -c "import ipaddress, os; rules = [rule.strip() for rule in os.environ.get('CURRENT_RULES', '').split(',') if rule.strip()]; ip = ipaddress.ip_address(os.environ['RUNNER_PUBLIC_IP']); matched = any(ip in ipaddress.ip_network(rule, strict=False) if '/' in rule else ip == ipaddress.ip_address(rule) for rule in rules); raise SystemExit(0 if matched else 1)" 2>$null + return $LASTEXITCODE -eq 0 + } + + function Wait-ForCosmosRunnerAccess { + param( + [string]$RunnerPublicIp + ) + + for ($attempt = 1; $attempt -le 20; $attempt++) { + if (Test-CosmosRunnerAccess) { + Write-Host "✓ CosmosDB data-plane access is now available for deployment runner IP $RunnerPublicIp" + return + } + + Write-Host " Waiting for CosmosDB firewall propagation for $RunnerPublicIp ($attempt/20)..." + Start-Sleep -Seconds 30 + } + + throw "CosmosDB firewall rule for $RunnerPublicIp is configured but data-plane access is still not available. Firewall changes can take up to 30 minutes to propagate. Wait and rerun 'azd provision' or 'azd up'." + } + + function Test-PythonVirtualEnvironment { + python -c "import sys; raise SystemExit(0 if sys.prefix != getattr(sys, 'base_prefix', sys.prefix) else 1)" 2>$null + return $LASTEXITCODE -eq 0 + } + + function Install-PostprovisionPythonDependencies { + $outputFile = [System.IO.Path]::GetTempFileName() + + try { + if (Test-PythonVirtualEnvironment) { + python -m pip install -r .\bicep\requirements.txt *> $outputFile + } else { + python -m pip install --user -r .\bicep\requirements.txt *> $outputFile + } + + if ($LASTEXITCODE -ne 0) { + Get-Content -Path $outputFile -Raw | Write-Error + return $false + } + + return $true + } finally { + Remove-Item -Path $outputFile -ErrorAction SilentlyContinue + } + } + + Write-Host "========================================" + Write-Host "POST-PROVISION: Starting configuration" + Write-Host "========================================" + + if ($env:var_configureApplication -eq 'true') { + Write-Host "" + Write-Host "[1/4] Granting permissions to CosmosDB..." + + $subscriptionId = Get-TargetSubscriptionId + $rgName = Resolve-ResourceGroupName + $cosmosUri = $env:var_cosmosDb_uri + $accountName = ([System.Uri]$cosmosUri).Host.Split('.')[0] + $objectId = az ad signed-in-user show --query id -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($objectId)) { + throw 'Failed to resolve the signed-in user object ID for Cosmos DB role assignment.' + } + + $scope = "/subscriptions/$subscriptionId/resourceGroups/$rgName/providers/Microsoft.DocumentDB/databaseAccounts/$accountName" + $roleId = az role definition list --name Contributor --subscription $subscriptionId --query "[0].id" -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($roleId)) { + throw 'Failed to resolve the Contributor role definition ID.' + } + + $controlPlaneOutput = az role assignment create --assignee-object-id $objectId --assignee-principal-type User --role $roleId --scope $scope 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { + if ($controlPlaneOutput -match 'already exists|RoleAssignmentExists|Conflict') { + Write-Host "Control-plane role already exists." + } elseif ($controlPlaneOutput -match 'AADSTS50076|multi-factor authentication|claims challenge') { + Write-Warning "Azure CLI requires multi-factor authentication before it can create the Cosmos DB control-plane role assignment." + Write-Warning "Continuing with Cosmos DB key-based post-deployment configuration." + } else { + throw "Failed to create the Cosmos DB control-plane role assignment.`n$controlPlaneOutput" + } + } + + $dataPlaneRoleId = az cosmosdb sql role definition list --account-name $accountName --resource-group $rgName --subscription $subscriptionId --query "[?roleName=='Cosmos DB Built-in Data Contributor'].id | [0]" -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($dataPlaneRoleId)) { + throw 'Failed to resolve the Cosmos DB built-in data contributor role definition ID.' + } + + $dataPlaneOutput = az cosmosdb sql role assignment create --account-name $accountName --resource-group $rgName --subscription $subscriptionId --scope / --principal-id $objectId --role-definition-id $dataPlaneRoleId 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { + if ($dataPlaneOutput -match 'already exists|RoleAssignmentExists|Conflict') { + Write-Host "Data-plane role already exists." + } elseif ($dataPlaneOutput -match 'AADSTS50076|multi-factor authentication|claims challenge') { + Write-Warning "Azure CLI requires multi-factor authentication before it can create the Cosmos DB data-plane role assignment." + Write-Warning "Continuing with Cosmos DB key-based post-deployment configuration." + } else { + throw "Failed to create the Cosmos DB data-plane role assignment.`n$dataPlaneOutput" + } + } + + Write-Host "✓ CosmosDB permissions granted successfully" + + Write-Host "" + Write-Host "[2/4] Installing Python dependencies..." + if (-not (Install-PostprovisionPythonDependencies)) { + throw 'Failed to install Python dependencies.' + } + Write-Host "✓ Dependencies installed successfully" + + $cosmosDbKeyOutput = az cosmosdb keys list --name $accountName --resource-group $rgName --subscription $subscriptionId --query primaryMasterKey -o tsv 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { + if ($cosmosDbKeyOutput -match 'AADSTS50076|multi-factor authentication|claims challenge') { + throw "Azure CLI requires multi-factor authentication before it can read the Cosmos DB primary key. Run 'az login --scope https://management.azure.com//.default' and rerun 'azd provision' or 'azd up'." + } + + throw "Failed to resolve the Cosmos DB primary key for post-deployment configuration.`n$cosmosDbKeyOutput" + } + + $env:var_cosmosDb_key = $cosmosDbKeyOutput.Trim() + if ([string]::IsNullOrWhiteSpace($env:var_cosmosDb_key)) { + throw 'Failed to resolve the Cosmos DB primary key for post-deployment configuration.' + } + + if (Test-CosmosRunnerAccess) { + Write-Host "✓ Deployment runner already has CosmosDB data-plane access" + } else { + Ensure-CosmosRunnerAccess -AccountName $accountName -ResourceGroupName $rgName -SubscriptionId $subscriptionId + } + + Write-Host "" + Write-Host "[3/4] Running post-deployment configuration..." + python .\bicep\postconfig.py + if ($LASTEXITCODE -ne 0) { + throw 'Post-deployment configuration failed.' + } + Write-Host "✓ Post-deployment configuration completed" + + Write-Host "" + Write-Host "[4/4] Restarting web service to apply settings..." + az webapp restart --name $env:var_webService --resource-group $rgName --subscription $subscriptionId | Out-Null + if ($LASTEXITCODE -ne 0) { + throw 'Failed to restart web service.' + } + Write-Host "✓ Web service restarted successfully" + + Write-Host "" + Write-Host "========================================" + Write-Host "POST-PROVISION: Completed successfully" + Write-Host "========================================" + } else { + Write-Host "" + Write-Host "Skipping post-deployment configuration (var_configureApplication is not true)" + Write-Host "" + Write-Host "========================================" + Write-Host "POST-PROVISION: Completed (skipped)" + Write-Host "========================================" + } predeploy: - # this is run after infrastructure and postprovisioning but before service deployment - # primary use is to build and push container images + # this is run after infrastructure and postprovisioning but before service deployment + # primary use is to build and push container images posix: shell: sh run: | set -e - - # Error handling function + cleanup_on_error() { local exit_code=$? echo "" @@ -117,84 +523,220 @@ hooks: az webapp start --name ${var_webService} --resource-group ${var_rgName} 2>/dev/null || true exit ${exit_code} } - + echo "========================================" echo "PRE-DEPLOY: Building and pushing image" echo "========================================" - + cd .. timestamp="$(date +"%Y%m%d-%H%M%S")" echo "" echo "Deployment timestamp: ${timestamp}" echo "Image: ${var_containerRegistry}/${var_imageName}:${timestamp}" - + echo "" - echo "[1/6] Stopping web service..." + echo "[1/4] Stopping web service..." if az webapp stop --name ${var_webService} --resource-group ${var_rgName}; then echo "✓ Web service stopped successfully" else echo "✗ ERROR: Failed to stop web service" >&2 exit 1 fi - + echo "" - echo "[2/6] Building Docker image..." + echo "[2/4] Building image in ACR Tasks..." echo "Context: $(pwd)" echo "Dockerfile: application/single_app/Dockerfile" - if docker build -f application/single_app/Dockerfile \ - -t ${var_containerRegistry}/${var_imageName}:${timestamp} . ; then - echo "✓ Docker image built successfully" - else - cleanup_on_error "Docker build" - fi - - echo "" - echo "[3/6] Tagging image as latest..." - if docker tag ${var_containerRegistry}/${var_imageName}:${timestamp} \ - ${var_containerRegistry}/${var_imageName}:latest ; then - echo "✓ Image tagged successfully" - else - cleanup_on_error "Docker tag" - fi - - echo "" - echo "[4/6] Logging in to ACR (${var_acrName})..." - if az acr login --name ${var_acrName}; then - echo "✓ ACR login successful" - else - cleanup_on_error "ACR login" - fi - - echo "" - echo "[5/6] Pushing images to ACR..." - echo " → Pushing latest tag..." - if docker push ${var_containerRegistry}/${var_imageName}:latest; then - echo " ✓ Latest tag pushed successfully" - else - cleanup_on_error "Docker push (latest)" + if [ "${var_enablePrivateNetworking}" = "true" ]; then + echo "ℹ ACR public access remains enabled until POST-UP so Azure Container Registry Tasks can complete the build" fi - - echo " → Pushing timestamped tag..." - if docker push ${var_containerRegistry}/${var_imageName}:${timestamp}; then - echo " ✓ Timestamped tag pushed successfully" + if az acr build \ + --registry ${var_acrName} \ + --file application/single_app/Dockerfile \ + --image ${var_imageName}:${timestamp} \ + --image ${var_imageName}:latest \ + .; then + echo "✓ ACR image build completed successfully" else - cleanup_on_error "Docker push (timestamp)" + cleanup_on_error "ACR build" fi - + echo "" - echo "[6/6] Restarting web service..." + echo "[3/4] Restarting web service..." if az webapp start --name ${var_webService} --resource-group ${var_rgName}; then echo "✓ Web service restarted successfully" else echo "✗ ERROR: Failed to restart web service" >&2 exit 1 fi - + + echo "" + echo "[4/4] Image build and restart complete" + echo " -> Published ${var_imageName}:${timestamp}" + echo " -> Published ${var_imageName}:latest" + echo "" echo "========================================" echo "PRE-DEPLOY: Completed successfully" echo "========================================" - + windows: + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + function Get-TargetSubscriptionId { + if (-not [string]::IsNullOrWhiteSpace($env:AZURE_SUBSCRIPTION_ID)) { + return $env:AZURE_SUBSCRIPTION_ID + } + + if (-not [string]::IsNullOrWhiteSpace($env:var_subscriptionId)) { + return $env:var_subscriptionId + } + + $resolvedSubscriptionId = az account show --query id -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($resolvedSubscriptionId)) { + throw 'Failed to resolve the target subscription ID for predeploy.' + } + + return $resolvedSubscriptionId + } + + function Resolve-ResourceGroupName { + $subscriptionId = Get-TargetSubscriptionId + + if (-not [string]::IsNullOrWhiteSpace($env:var_rgName)) { + $groupExists = az group exists --name $env:var_rgName --subscription $subscriptionId -o tsv 2>$null + if ($LASTEXITCODE -eq 0 -and $groupExists -eq 'true') { + return $env:var_rgName + } + } + + $cosmosAccountName = $env:var_cosmosDb_accountName + if ([string]::IsNullOrWhiteSpace($cosmosAccountName) -and -not [string]::IsNullOrWhiteSpace($env:var_cosmosDb_uri)) { + $cosmosAccountName = ([System.Uri]$env:var_cosmosDb_uri).Host.Split('.')[0] + } + + $resolvedResourceGroup = $null + if (-not [string]::IsNullOrWhiteSpace($cosmosAccountName)) { + $resolvedResourceGroup = az cosmosdb list --subscription $subscriptionId --query "[?name=='$cosmosAccountName'].resourceGroup | [0]" -o tsv 2>$null + } + + if ([string]::IsNullOrWhiteSpace($resolvedResourceGroup) -and -not [string]::IsNullOrWhiteSpace($env:var_webService)) { + $resolvedResourceGroup = az resource list --subscription $subscriptionId --name "$($env:var_webService)" --resource-type Microsoft.Web/sites --query "[0].resourceGroup" -o tsv 2>$null + } + + if ([string]::IsNullOrWhiteSpace($resolvedResourceGroup)) { + throw 'Unable to resolve resource group from var_rgName, Cosmos DB, or Web App.' + } + + $env:var_rgName = $resolvedResourceGroup + return $resolvedResourceGroup + } + + $subscriptionId = Get-TargetSubscriptionId + $rgName = Resolve-ResourceGroupName + + function Cleanup-OnError { + param( + [string]$StepName + ) + + Write-Host "" + Write-Error "Deployment failed at step: $StepName" + Write-Host "Attempting to restart web service..." + az webapp start --name $env:var_webService --resource-group $rgName --subscription $subscriptionId 2>$null | Out-Null + exit 1 + } + + function Wait-ForAcrBuildRun { + param( + [string]$RunId + ) + + for ($attempt = 1; $attempt -le 120; $attempt++) { + $runStatus = az acr task show-run --registry $env:var_acrName --resource-group $rgName --subscription $subscriptionId --run-id $RunId --query status -o tsv 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($runStatus)) { + throw "Failed to query ACR build run status for run ID $RunId." + } + + switch ($runStatus) { + 'Succeeded' { + Write-Host "✓ ACR build run $RunId completed successfully" + return + } + 'Failed' { + throw "ACR build run $RunId failed. Run 'az acr task show-run --registry $($env:var_acrName) --resource-group $rgName --subscription $subscriptionId --run-id $RunId -o json' for details." + } + 'Error' { + throw "ACR build run $RunId errored. Run 'az acr task show-run --registry $($env:var_acrName) --resource-group $rgName --subscription $subscriptionId --run-id $RunId -o json' for details." + } + 'Canceled' { + throw "ACR build run $RunId was canceled." + } + 'Timeout' { + throw "ACR build run $RunId timed out." + } + default { + Write-Host " ACR build run $RunId status: $runStatus ($attempt/120)..." + Start-Sleep -Seconds 10 + } + } + } + + throw "ACR build run $RunId did not complete within the expected time window." + } + + Write-Host "========================================" + Write-Host "PRE-DEPLOY: Building and pushing image" + Write-Host "========================================" + + Set-Location .. + $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' + Write-Host "" + Write-Host "Deployment timestamp: $timestamp" + Write-Host "Image: $($env:var_containerRegistry)/$($env:var_imageName):$timestamp" + + Write-Host "" + Write-Host "[1/4] Stopping web service..." + az webapp stop --name $env:var_webService --resource-group $rgName --subscription $subscriptionId | Out-Null + if ($LASTEXITCODE -ne 0) { + throw 'Failed to stop web service.' + } + Write-Host "✓ Web service stopped successfully" + + Write-Host "" + Write-Host "[2/4] Building image in ACR Tasks..." + Write-Host "Context: $(Get-Location)" + Write-Host "Dockerfile: application/single_app/Dockerfile" + if ($env:var_enablePrivateNetworking -eq 'true') { + Write-Host "ℹ ACR public access remains enabled until POST-UP so Azure Container Registry Tasks can complete the build" + } + $runId = az acr build --registry $env:var_acrName --resource-group $rgName --subscription $subscriptionId --file application/single_app/Dockerfile --image "$($env:var_imageName):$timestamp" --image "$($env:var_imageName):latest" --no-logs --query runId -o tsv . + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($runId)) { + Cleanup-OnError -StepName 'ACR build' + } + Write-Host "Queued ACR build run ID: $runId" + Wait-ForAcrBuildRun -RunId $runId + Write-Host "✓ ACR image build completed successfully" + + Write-Host "" + Write-Host "[3/4] Restarting web service..." + az webapp start --name $env:var_webService --resource-group $rgName --subscription $subscriptionId | Out-Null + if ($LASTEXITCODE -ne 0) { + throw 'Failed to restart web service.' + } + Write-Host "✓ Web service restarted successfully" + + Write-Host "" + Write-Host "[4/4] Image build and restart complete" + Write-Host " -> Published $($env:var_imageName):$timestamp" + Write-Host " -> Published $($env:var_imageName):latest" + + Write-Host "" + Write-Host "========================================" + Write-Host "PRE-DEPLOY: Completed successfully" + Write-Host "========================================" + postup: # this is the final step to run after everything else is done # primary use is disable public network access if private endpoints are used @@ -202,15 +744,15 @@ hooks: shell: sh run: | set -e - + echo "========================================" echo "POST-UP: Final configuration" echo "========================================" - + if [ "${var_enablePrivateNetworking}" = "true" ]; then echo "" echo "Configuring private networking..." - + echo "" echo "[1/4] Disabling public network access for CosmosDB..." if az cosmosdb update --name ${var_cosmosDb_accountName} \ @@ -221,7 +763,7 @@ hooks: echo "✗ ERROR: Failed to disable CosmosDB public access" >&2 exit 1 fi - + echo "" echo "[2/4] Disabling public network access for Key Vault..." if az keyvault update --name ${var_keyVaultName} \ @@ -255,7 +797,7 @@ hooks: echo "✗ ERROR: Failed to disable Web Application public access" >&2 exit 1 fi - + echo "" echo "✓ Private networking configured successfully" else @@ -266,4 +808,112 @@ hooks: echo "" echo "========================================" echo "✓ DEPLOYMENT COMPLETED SUCCESSFULLY" - echo "========================================" \ No newline at end of file + echo "========================================" + windows: + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + function Get-TargetSubscriptionId { + if (-not [string]::IsNullOrWhiteSpace($env:AZURE_SUBSCRIPTION_ID)) { + return $env:AZURE_SUBSCRIPTION_ID + } + + if (-not [string]::IsNullOrWhiteSpace($env:var_subscriptionId)) { + return $env:var_subscriptionId + } + + $resolvedSubscriptionId = az account show --query id -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($resolvedSubscriptionId)) { + throw 'Failed to resolve the target subscription ID for postup.' + } + + return $resolvedSubscriptionId + } + + function Resolve-ResourceGroupName { + $subscriptionId = Get-TargetSubscriptionId + + if (-not [string]::IsNullOrWhiteSpace($env:var_rgName)) { + $groupExists = az group exists --name $env:var_rgName --subscription $subscriptionId -o tsv 2>$null + if ($LASTEXITCODE -eq 0 -and $groupExists -eq 'true') { + return $env:var_rgName + } + } + + $cosmosAccountName = $env:var_cosmosDb_accountName + if ([string]::IsNullOrWhiteSpace($cosmosAccountName) -and -not [string]::IsNullOrWhiteSpace($env:var_cosmosDb_uri)) { + $cosmosAccountName = ([System.Uri]$env:var_cosmosDb_uri).Host.Split('.')[0] + } + + $resolvedResourceGroup = $null + if (-not [string]::IsNullOrWhiteSpace($cosmosAccountName)) { + $resolvedResourceGroup = az cosmosdb list --subscription $subscriptionId --query "[?name=='$cosmosAccountName'].resourceGroup | [0]" -o tsv 2>$null + } + + if ([string]::IsNullOrWhiteSpace($resolvedResourceGroup) -and -not [string]::IsNullOrWhiteSpace($env:var_webService)) { + $resolvedResourceGroup = az resource list --subscription $subscriptionId --name "$($env:var_webService)" --resource-type Microsoft.Web/sites --query "[0].resourceGroup" -o tsv 2>$null + } + + if ([string]::IsNullOrWhiteSpace($resolvedResourceGroup)) { + throw 'Unable to resolve resource group from var_rgName, Cosmos DB, or Web App.' + } + + $env:var_rgName = $resolvedResourceGroup + return $resolvedResourceGroup + } + + $subscriptionId = Get-TargetSubscriptionId + $rgName = Resolve-ResourceGroupName + + Write-Host "========================================" + Write-Host "POST-UP: Final configuration" + Write-Host "========================================" + + if ($env:var_enablePrivateNetworking -eq 'true') { + Write-Host "" + Write-Host "Configuring private networking..." + + Write-Host "" + Write-Host "[1/4] Disabling public network access for CosmosDB..." + az cosmosdb update --name $env:var_cosmosDb_accountName --resource-group $rgName --subscription $subscriptionId --public-network-access Disabled > $null + if ($LASTEXITCODE -ne 0) { + throw 'Failed to disable CosmosDB public access.' + } + Write-Host "✓ CosmosDB public access disabled" + + Write-Host "" + Write-Host "[2/4] Disabling public network access for Key Vault..." + az keyvault update --name $env:var_keyVaultName --resource-group $rgName --subscription $subscriptionId --public-network-access Disabled > $null + if ($LASTEXITCODE -ne 0) { + throw 'Failed to disable Key Vault public access.' + } + Write-Host "✓ Key Vault public access disabled" + + Write-Host "" + Write-Host "[3/4] Disabling public network access for Azure Container Registry..." + az acr update --name $env:var_acrName --resource-group $rgName --subscription $subscriptionId --public-network-enabled false > $null + if ($LASTEXITCODE -ne 0) { + throw 'Failed to disable ACR public access.' + } + Write-Host "✓ ACR public access disabled" + + Write-Host "" + Write-Host "[4/4] Disabling public network access for Web Application..." + az resource update --name $env:var_webService --resource-group $rgName --subscription $subscriptionId --resource-type Microsoft.Web/sites --set properties.publicNetworkAccess=Disabled | Out-Null + if ($LASTEXITCODE -ne 0) { + throw 'Failed to disable Web Application public access.' + } + Write-Host "✓ Web Application public access disabled" + + Write-Host "" + Write-Host "✓ Private networking configured successfully" + } else { + Write-Host "" + Write-Host "Skipping private networking configuration (var_enablePrivateNetworking is not true)" + } + + Write-Host "" + Write-Host "========================================" + Write-Host "✓ DEPLOYMENT COMPLETED SUCCESSFULLY" + Write-Host "========================================" \ No newline at end of file diff --git a/deployers/azurecli/README.md b/deployers/azurecli/README.md index 54688a63f..9a7a6a5a6 100644 --- a/deployers/azurecli/README.md +++ b/deployers/azurecli/README.md @@ -1,3 +1,236 @@ -# Simple Chat - Deployment using AzureCLI + PowerShell +# Simple Chat - Deployment using Azure CLI + PowerShell -[Return to Main](../README.md) \ No newline at end of file +[Return to Main](../README.md) + +## Overview + +This deployment path uses [deploy-simplechat.ps1](deploy-simplechat.ps1) to provision the core Simple Chat resources with Azure CLI and PowerShell. + +This option is useful when you want a script-driven deployment flow without using AZD or Terraform. + +It is also useful when you want more script-level control over deployment sequencing and post-deployment recovery steps than the native AZD workflow provides. + +## What this deployer does + +The Azure CLI deployer provisions the main Simple Chat application resources, including: + +- container image build in Azure Container Registry via `az acr build` when enabled +- Resource group +- App Service plan and web app +- Azure Container Registry integration +- Cosmos DB +- Azure OpenAI account creation or reuse +- Azure OpenAI default GPT and embedding model deployments +- Azure AI Search +- Document Intelligence +- Key Vault +- Application Insights and Log Analytics +- Optional Entra security groups + +## Private networking support + +This deployer now supports the same core private networking flow as the Bicep deployer for the services it provisions. + +It can: + +- create a new VNet with dedicated App Service integration and private endpoint subnets +- reuse an existing VNet by supplying existing subnet resource IDs +- create private endpoints for Key Vault, Cosmos DB, Azure Container Registry, Azure AI Search, Azure OpenAI, Document Intelligence, Storage, and the App Service +- create private DNS zones automatically +- reuse customer-managed private DNS zones by resource ID +- create or skip private DNS VNet links on a per-zone basis + +If you provide only external endpoint information without Azure resource metadata, the script cannot automate Azure OpenAI private endpoint creation for that endpoint. + +## Files in this folder + +- [deploy-simplechat.ps1](deploy-simplechat.ps1) - Main deployment script +- [destroy-simplechat.ps1](destroy-simplechat.ps1) - Cleanup script +- [appRegistrationRoles.json](appRegistrationRoles.json) - App role definition source +- [ai_search-index-group.json](ai_search-index-group.json) - Group search index definition +- [ai_search-index-user.json](ai_search-index-user.json) - User search index definition + +## Prerequisites + +Before running the deployment: + +1. Install Azure CLI +2. Install PowerShell +3. Sign in to the target Azure cloud and subscription +4. Make sure an Azure Container Registry already exists +5. Make sure Azure OpenAI is already available if you plan to reuse an existing instance +6. Make sure you have permission to create resources in the target subscription and tenant +7. If private networking is enabled, make sure you can manage VNets, subnets, private endpoints, private DNS zones, and private DNS VNet links +8. If `param_BuildContainerImageWithAcr = $true`, make sure the target source context contains [application/single_app/Dockerfile](application/single_app/Dockerfile) and the rest of the repository files needed by that Docker build + +For Azure Government: + +```azurecli +az cache purge +az account clear +az cloud set --name AzureUSGovernment +az login --scope https://management.core.usgovcloudapi.net//.default +az login --scope https://graph.microsoft.us//.default +az account set -s "" +``` + +For Azure Commercial: + +```azurecli +az cache purge +az account clear +az cloud set --name AzureCloud +az login --scope https://management.azure.com//.default +az login --scope https://graph.microsoft.com//.default +az account set -s "" +``` + +## Quick start + +1. Open [deploy-simplechat.ps1](deploy-simplechat.ps1) +2. Update the configuration variables near the top of the script +3. Decide whether the script should build the image in ACR by setting `param_BuildContainerImageWithAcr` +4. Run the script from PowerShell + +Example: + +```powershell +cd deployers/azurecli +.\deploy-simplechat.ps1 +``` + +## Configuration areas to review + +The script contains editable configuration variables for: + +- `globalWhichAzurePlatform` +- `paramTenantId` +- `paramLocation` +- `paramEnvironment` +- `paramBaseName` +- `ACR_NAME` +- `IMAGE_NAME` +- `param_BuildContainerImageWithAcr` +- `param_DockerfilePath` +- `param_DockerBuildContextPath` +- `param_PublishLatestImageTag` +- existing Azure OpenAI resource settings +- `param_DeployAzureOpenAiModels` +- `param_AzureOpenAiDeploymentType` +- Azure OpenAI GPT and embedding model names, versions, deployment names, and capacities +- optional Entra security group creation +- `param_EnablePrivateNetworking` +- `param_ExistingVirtualNetworkId` +- `param_ExistingAppServiceSubnetId` +- `param_ExistingPrivateEndpointSubnetId` +- `param_PrivateNetworkAddressPrefixes` +- `param_AppServiceIntegrationSubnetAddressPrefixes` +- `param_PrivateEndpointSubnetAddressPrefixes` +- `param_PrivateDnsZoneConfigs` + +Review those values before running the script. + +## Azure OpenAI deployment types and quota handling + +The Azure CLI deployer can now create the default GPT and embedding model deployments for the Azure OpenAI account it creates or reuses. The Azure OpenAI account still uses the Cognitive Services account SKU, but the model deployments themselves use `Standard`, `DatazoneStandard`, or `GlobalStandard`. + +For Azure Commercial, leave `param_AzureOpenAiDeploymentType` blank to be prompted during deployment, or set it explicitly in the script before you run it. For Azure Government, the script automatically uses `Standard`. + +If a model deployment fails because of quota or regional availability, the script lets you retry that deployment with a different deployment type. If the retry still fails, request additional quota, lower the configured capacity, or set `param_DeployAzureOpenAiModels = $false` and reuse existing Azure OpenAI deployments instead. + +Default model behavior: + +- GPT deployment defaults to `gpt-4o` +- GPT model version defaults to `2024-11-20` in Azure Commercial and `2024-05-13` in Azure Government +- Embedding deployment defaults to `text-embedding-3-small` in Azure Commercial and `text-embedding-ada-002` in `usgovvirginia` +- Deployment capacities default to `100` for GPT and `80` for embeddings + +After the infrastructure deployment completes, review the resulting Azure OpenAI deployments in the Simple Chat UI under `Admin Settings` > `AI Models` > `GPT Configuration` and `Embeddings Configuration`. For the full manual path, see `docs/setup_instructions_manual.md` and `docs/admin_configuration.md`. + +## Container build behavior + +By default, the Azure CLI deployer now builds the container image in ACR instead of assuming you already built and pushed it locally. + +Default behavior: + +- `param_BuildContainerImageWithAcr = $true` +- the script submits the build to ACR Tasks with `az acr build` +- the script uses [application/single_app/Dockerfile](application/single_app/Dockerfile) +- the build context defaults to the repository root via `..\..` + +If you want to skip the image build and deploy an image that already exists in ACR: + +- set `param_BuildContainerImageWithAcr = $false` +- set `IMAGE_NAME` to the repository and tag you want the App Service to pull + +Example: + +```powershell +$IMAGE_NAME = "simplechat:2026-03-11_main_42" +$param_BuildContainerImageWithAcr = $true +$param_DockerfilePath = "application/single_app/Dockerfile" +$param_DockerBuildContextPath = "..\.." +``` + +## Private DNS questions to answer before running + +If `param_EnablePrivateNetworking = $true`, decide the following before you run the script: + +- Will the script create private DNS zones, or will you reuse existing zones? +- If you reuse zones, what are the full resource IDs for those zones? +- Should the script create the VNet links, or are those links already managed by central networking? + +Supported zone keys in `param_PrivateDnsZoneConfigs`: + +- `keyVault` +- `cosmosDb` +- `containerRegistry` +- `aiSearch` +- `blobStorage` +- `cognitiveServices` +- `openAi` +- `webSites` + +Example: + +```powershell +$param_PrivateDnsZoneConfigs = @{ + openAi = @{ + zoneResourceId = "/subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.openai.azure.com" + createVNetLink = $false + } + webSites = @{ + zoneResourceId = "/subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.azurewebsites.net" + } +} +``` + +## Post-deployment tasks + +After deployment, you should still complete the manual steps described in [deploy-simplechat.ps1](deploy-simplechat.ps1), including: + +- App Service authentication provider setup +- Entra admin consent for Graph permissions +- Azure OpenAI model deployment review or existing-endpoint configuration +- Azure AI Search index deployment +- App settings review in the web UI +- Security group membership assignment, if used + +## When to use another deployer + +Choose a different deployer when: + +- You want the broadest deployment coverage and the most mature deployment path → use [../bicep/README.md](../bicep/README.md) +- You want Terraform-managed infrastructure → use [../terraform/ReadMe.md](../terraform/ReadMe.md) +- You want an AZD-first deployment experience → use [../bicep/README.md](../bicep/README.md) + +## Cleanup + +To remove a deployment created with this folder, review and run [destroy-simplechat.ps1](destroy-simplechat.ps1). + +Cleanup behavior: + +- The destroy script deletes the Simple Chat deployment resource group. +- That removes private endpoints, private DNS zones, and the deployment VNet only when those resources were created inside that deployment resource group. +- If the deployment reused an existing VNet, existing subnets, or customer-managed private DNS zones outside the deployment resource group, those shared resources are not deleted. +- The destroy script also removes the Entra application registration and the Simple Chat security groups created by the Azure CLI deployer. \ No newline at end of file diff --git a/deployers/azurecli/deploy-simplechat.ps1 b/deployers/azurecli/deploy-simplechat.ps1 index 55feabbd4..cd9a09299 100644 --- a/deployers/azurecli/deploy-simplechat.ps1 +++ b/deployers/azurecli/deploy-simplechat.ps1 @@ -1,17 +1,28 @@ <# .SYNOPSIS - Deploys the Simple Chat web application to Azure Government. + Deploys the Simple Chat web application with Azure CLI and PowerShell. .DESCRIPTION This PowerShell script uses Azure CLI commands to provision a set of resources. It is designed to be highly configurable. Please review and update the Configuration Variables section before running. + + Private networking scope: + - This script can now create a deployment VNet for Simple Chat private networking, + or reuse an existing VNet and existing subnets. + - This script can create private endpoints for the core Simple Chat dependencies. + - This script can create private DNS zones automatically, or reuse customer-managed + private DNS zones by resource ID. + - Each supported private DNS zone can optionally skip VNet link creation when a + central networking team manages that link separately. .NOTES Author: Microsoft Federal Date: 2025-05-14 - Version: 1.0 + Version: 1.1 Prerequisites: - Azure CLI installed and authenticated. Run `az login` and `az account set --subscription ` - Permissions to create resources in the specified subscription and Entra ID tenant. + - If private networking is enabled, permissions to create or update VNets, subnets, + private endpoints, private DNS zones, and private DNS VNet links. GitHub Url: https://github.com/microsoft/simplechat/ @@ -47,10 +58,61 @@ ============================================ Pre-deployment Notes ============================================ -- Create a new Azure Open AI instance with the needed model deployments. +- Decide whether this script should create or reuse Azure OpenAI and whether it should create the default GPT and embedding model deployments. - Have an Azure Container Registry (ACR) created, admin user enabled, and the name is set in the script. -- Deploy an application image to the ACR. +- Decide whether this script should build the application image in ACR by using `az acr build`. - LOGIN to Azure Cli before running this script. See "LOGIN NOTES" above. +- Review the private networking variables before running if you need private endpoints or VNet integration. +- If you reuse existing networking, supply both the App Service integration subnet ID and the private endpoint subnet ID. + +============================================ +Private networking prerequisites and planning questions +============================================ + +If you enable private networking, answer these questions before you run the script: + +- Will this deployment create a new VNet, or reuse an existing enterprise VNet? +- If you are reusing a VNet, do you already have two separate subnets: + - one subnet for App Service regional VNet integration + - one subnet for private endpoints +- Is the App Service integration subnet delegated to `Microsoft.Web/serverFarms`? +- Will this script create private DNS zones, or will you reuse existing private DNS zones? +- If you reuse private DNS zones, are they already linked to the target VNet, or should this script create the VNet links? +- Are the required Azure resource providers registered in the target subscription, especially `Microsoft.Network`? + +If you intend to place Simple Chat behind private networking, make sure the following prerequisites are already understood and planned: + +- An existing VNet with dedicated subnets for: + - App Service regional VNet integration + - Private endpoints +- The App Service integration subnet must be delegated to `Microsoft.Web/serverFarms`. +- Private DNS zones must exist, or a central networking team must manage them, for the services you expose with private endpoints. +- Each required private DNS zone must be linked to the target VNet unless DNS is managed elsewhere. +- If DNS zones are not linked correctly, services may deploy but private name resolution will fail. + +Common private DNS zones for Simple Chat private endpoint scenarios include: +- `privatelink.azurewebsites.net` +- `privatelink.documents.azure.com` +- `privatelink.blob.core.windows.net` +- `privatelink.search.windows.net` +- `privatelink.openai.azure.com` +- `privatelink.cognitiveservices.azure.com` +- `privatelink.vaultcore.azure.net` +- `privatelink.azurecr.io` + +For Azure Government, use the corresponding sovereign-cloud private DNS zones such as: +- `privatelink.azurewebsites.us` +- `privatelink.documents.azure.us` +- `privatelink.blob.core.usgovcloudapi.net` +- `privatelink.search.azure.us` +- `privatelink.openai.azure.us` +- `privatelink.cognitiveservices.azure.us` +- `privatelink.vaultcore.azure.us` +- `privatelink.azurecr.us` + +For detailed private networking guidance, see: +- `deployers/bicep/README.md` +- `docs/how-to/enterprise_networking.md` ============================================ Manual changes post-deployment. @@ -98,8 +160,9 @@ STEP 6) Cosmos DB STEP 7) Azure OpenAI - - Configure a custom domain on you Azure OpenAI resources. Otherwise, you will not be able to retrieve your OpenAI models - and add your OpenAI endpoint. + - Review the Azure OpenAI model deployments created by this script and adjust deployment type, capacity, or model versions if your environment requires different values. + - If you reuse an existing Azure OpenAI resource, the script can create any missing default GPT and embedding deployments for you. + - Configure a custom domain on your Azure OpenAI resources. Otherwise, you will not be able to retrieve your OpenAI models and add your OpenAI endpoint. STEP 8) Test Web UI fully. #> @@ -118,18 +181,57 @@ $paramResourceOwnerId = "" # used for tagging resources $paramEnvironment = "dev" # Environment identifier (e.g., dev, test, prod, uat) $paramBaseName = "simplechatexp" # A short base name for your organization or project (e.g., contoso1, projectx2) $ACR_NAME = "registrysimplechatexperimental" # Replace with your ACR name (must be globally unique, lowercase alphanumeric) -$IMAGE_NAME = "simplechatexp" # Replace with your image name +$IMAGE_NAME = "simplechatexp:latest" # Repository[:tag] to build and deploy from ACR. +$param_BuildContainerImageWithAcr = $true # When true, run az acr build against the repo instead of requiring a prebuilt image. +$param_DockerfilePath = "application/single_app/Dockerfile" +$param_DockerBuildContextPath = "..\.." # Relative to this script. The default points to the repository root. +$param_PublishLatestImageTag = $true # Adds a latest tag when IMAGE_NAME uses a non-latest tag. $param_UseExisting_OpenAi_Instance = $true $param_Existing_AzureOpenAi_ResourceName = "aoai-global-team" # Azure OpenAI resource name $param_Existing_AzureOpenAi_ResourceGroupName = "RG-AVD-East-Prod" # Azure OpenAI resource group name $param_Existing_AzureOpenAi_SubscriptionId = "9698dd71-9367-49c2-bede-fd0deecfad62" # In case the resource is in another subscription -#$param_Existing_AzureOpenAi_Deployment_Model = "gpt-4o" # Azure OpenAI deployment name -#$param_Existing_AzureOpenAi_Deployment_Embeddings = "text-embedding-ada-002" # Azure OpenAI deployment model name +$param_DeployAzureOpenAiModels = $true # Creates the default GPT and embedding deployments when they do not already exist. +$param_AzureOpenAiDeploymentType = "" # Valid values: Standard, DatazoneStandard, GlobalStandard. Leave blank to be prompted in Azure Commercial. Azure Government always uses Standard. +$param_AzureOpenAiGptDeploymentName = "gpt-4o" +$param_AzureOpenAiGptModelName = "gpt-4o" +$param_AzureOpenAiGptModelVersion = "" # Leave blank to use the default for the selected cloud. +$param_AzureOpenAiGptDeploymentCapacity = 100 +$param_AzureOpenAiEmbeddingDeploymentName = "" +$param_AzureOpenAiEmbeddingModelName = "" +$param_AzureOpenAiEmbeddingModelVersion = "" # Leave blank to use the default for the selected cloud and region. +$param_AzureOpenAiEmbeddingDeploymentCapacity = 80 $paramCreateEntraSecurityGroups = $true # Set to true to create Entra ID security groups $param_Existing_ResourceGroupName = "RG-SimpleChat-Experiemental" # Leave empty if not using an existing resource group name, one will be dynamically generated +# Private networking +$param_EnablePrivateNetworking = $false +$param_ExistingVirtualNetworkId = "" # Optional existing VNet resource ID to reuse. +$param_ExistingAppServiceSubnetId = "" # Required when reusing an existing VNet for App Service regional integration. +$param_ExistingPrivateEndpointSubnetId = "" # Required when reusing an existing VNet for private endpoints. +$param_PrivateNetworkAddressPrefixes = @("10.0.0.0/21") +$param_AppServiceIntegrationSubnetAddressPrefixes = @("10.0.0.0/24") +$param_PrivateEndpointSubnetAddressPrefixes = @("10.0.2.0/24") +$param_AllowedIpAddresses = @() # Optional admin IPs to keep for services that still support IP allow lists. +$param_ShowPrivateNetworkingChecklist = $true +$param_ConfirmPrivateNetworkingPlan = $false + +# Optional per-zone private DNS overrides. +# Supported keys: +# keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites +# Example: +# $param_PrivateDnsZoneConfigs = @{ +# openAi = @{ +# zoneResourceId = "/subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.openai.azure.com" +# createVNetLink = $false +# } +# webSites = @{ +# zoneResourceId = "/subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.azurewebsites.net" +# } +# } +$param_PrivateDnsZoneConfigs = @{} + #--------------------------------------------------------------------------------------------- @@ -149,6 +251,16 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") { $param_AzureOpenAi_Url = "https://{0}.openai.azure.com/" $param_AiSearch_Url = "https://{0}.search.windows.net" $param_AppService_Environment = "public" + $script:PrivateDnsZoneNames = @{ + aiSearch = "privatelink.search.windows.net" + blobStorage = "privatelink.blob.core.windows.net" + cognitiveServices = "privatelink.cognitiveservices.azure.com" + containerRegistry = "privatelink.azurecr.io" + cosmosDb = "privatelink.documents.azure.com" + keyVault = "privatelink.vaultcore.azure.net" + openAi = "privatelink.openai.azure.com" + webSites = "privatelink.azurewebsites.net" + } } elseif ($globalWhichAzurePlatform -eq "AzureUSGovernment") { $paramCosmosDbUrlTemplate = "https://{0}.documents.azure.us:443/" $ACR_BASE_URL = "$ACR_NAME.azurecr.us" @@ -161,6 +273,16 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") { $param_AzureOpenAi_Url = "https://{0}.openai.azure.us/" $param_AiSearch_Url = "https://{0}.search.azure.us" $param_AppService_Environment = "usgovernment" + $script:PrivateDnsZoneNames = @{ + aiSearch = "privatelink.search.azure.us" + blobStorage = "privatelink.blob.core.usgovcloudapi.net" + cognitiveServices = "privatelink.cognitiveservices.azure.us" + containerRegistry = "privatelink.azurecr.us" + cosmosDb = "privatelink.documents.azure.us" + keyVault = "privatelink.vaultcore.azure.us" + openAi = "privatelink.openai.azure.us" + webSites = "privatelink.azurewebsites.us" + } } elseif ($globalWhichAzurePlatform -eq "AzureSecret") { # SET THESE VALUES FOR il6 (information is tented) $paramCosmosDbUrlTemplate = "" @@ -179,6 +301,53 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") { $paramDateTime_ScriptStart = Get-Date $paramDateTimeStamp = ($paramDateTime_ScriptStart).ToString("yyyy-MM-dd HH:mm:ss") +$script:SupportedPrivateDnsZoneKeys = @("keyVault", "cosmosDb", "containerRegistry", "aiSearch", "blobStorage", "cognitiveServices", "openAi", "webSites") +$script:PrivateDnsZoneLinkSuffixes = @{ + aiSearch = "searchService" + blobStorage = "storage" + cognitiveServices = "docIntelService" + containerRegistry = "acr" + cosmosDb = "cosmosDb" + keyVault = "kv" + openAi = "openAiService" + webSites = "webApp" +} +$script:UseExistingPrivateNetwork = $false +$script:ResolvedVirtualNetworkId = "" +$script:ResolvedAppServiceSubnetId = "" +$script:ResolvedPrivateEndpointSubnetId = "" +$script:CurrentSubscriptionId = "" +$script:SupportedOpenAiDeploymentTypes = @("Standard", "DatazoneStandard", "GlobalStandard") +$script:ResolvedOpenAiDeploymentType = "" + +if ([string]::IsNullOrWhiteSpace($param_AzureOpenAiGptModelVersion)) { + if ($globalWhichAzurePlatform -eq "AzureUSGovernment") { + $param_AzureOpenAiGptModelVersion = "2024-05-13" + } else { + $param_AzureOpenAiGptModelVersion = "2024-11-20" + } +} + +if ([string]::IsNullOrWhiteSpace($param_AzureOpenAiEmbeddingModelName)) { + if ($globalWhichAzurePlatform -eq "AzureUSGovernment" -and $paramLocation -eq "usgovvirginia") { + $param_AzureOpenAiEmbeddingModelName = "text-embedding-ada-002" + } else { + $param_AzureOpenAiEmbeddingModelName = "text-embedding-3-small" + } +} + +if ([string]::IsNullOrWhiteSpace($param_AzureOpenAiEmbeddingDeploymentName)) { + $param_AzureOpenAiEmbeddingDeploymentName = $param_AzureOpenAiEmbeddingModelName +} + +if ([string]::IsNullOrWhiteSpace($param_AzureOpenAiEmbeddingModelVersion)) { + if ($param_AzureOpenAiEmbeddingModelName -eq "text-embedding-ada-002") { + $param_AzureOpenAiEmbeddingModelVersion = "2" + } else { + $param_AzureOpenAiEmbeddingModelVersion = "1" + } +} + $tags = @{ Environment = $paramEnvironment Owner = $paramResourceOwnerId @@ -266,6 +435,303 @@ Function Get-GloballyUniqueResourceName { return $name } +function Ensure-AzureCliAuthenticated { + if (-not (Get-Command "az" -ErrorAction SilentlyContinue)) { + Write-Error "Azure CLI is not installed. Please install it before running this script." + exit 1 + } + + $expectedCloudName = switch ($globalWhichAzurePlatform) { + 'AzureCloud' { 'AzureCloud' } + 'AzureUSGovernment' { 'AzureUSGovernment' } + default { $null } + } + + if (-not $expectedCloudName) { + Write-Error "Automatic Azure CLI login is only supported for AzureCloud and AzureUSGovernment. Current platform: $globalWhichAzurePlatform" + exit 1 + } + + $currentCloudName = az cloud show --query name --output tsv 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($currentCloudName)) { + Write-Warning "Unable to determine the current Azure CLI cloud. Setting it to '$expectedCloudName'." + az cloud set --name $expectedCloudName | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to set Azure CLI cloud to '$expectedCloudName'." + exit 1 + } + } elseif ($currentCloudName -ne $expectedCloudName) { + Write-Host "Switching Azure CLI cloud from '$currentCloudName' to '$expectedCloudName'..." -ForegroundColor Yellow + az cloud set --name $expectedCloudName | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to set Azure CLI cloud to '$expectedCloudName'." + exit 1 + } + } + + az account show --output none 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Host "Azure CLI authentication found." -ForegroundColor Yellow + return + } + + Write-Host "Azure CLI is not authenticated. Running 'az login'..." -ForegroundColor Yellow + az login | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Error "Azure CLI login failed. Run 'az login' manually and try again." + exit 1 + } + + az account show --output none 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Error "Azure CLI authentication could not be verified after login." + exit 1 + } +} + +function Resolve-ContainerImageInfo { + param( + [string]$ImageName + ) + + if ([string]::IsNullOrWhiteSpace($ImageName)) { + Write-Error "IMAGE_NAME must not be empty." + exit 1 + } + + $lastSlashIndex = $ImageName.LastIndexOf('/') + $lastColonIndex = $ImageName.LastIndexOf(':') + + if ($lastColonIndex -gt $lastSlashIndex) { + $repository = $ImageName.Substring(0, $lastColonIndex) + $tag = $ImageName.Substring($lastColonIndex + 1) + } else { + $repository = $ImageName + $tag = "latest" + } + + if ([string]::IsNullOrWhiteSpace($repository) -or [string]::IsNullOrWhiteSpace($tag)) { + Write-Error "IMAGE_NAME must be in the form repository or repository:tag. Current value: $ImageName" + exit 1 + } + + return [PSCustomObject]@{ + Repository = $repository + Tag = $tag + FullName = "$repository`:$tag" + } +} + +function Get-DefaultOpenAiDeploymentType { + if ($globalWhichAzurePlatform -eq 'AzureUSGovernment') { + return 'Standard' + } + + return 'GlobalStandard' +} + +function Select-OpenAiDeploymentType { + param( + [string]$CurrentValue, + [string]$PromptMessage = "Select the Azure OpenAI deployment type to use" + ) + + if ($globalWhichAzurePlatform -eq 'AzureUSGovernment') { + Write-Host "Azure Government uses the Azure OpenAI deployment type 'Standard'." -ForegroundColor Yellow + return 'Standard' + } + + $defaultType = if ([string]::IsNullOrWhiteSpace($CurrentValue)) { Get-DefaultOpenAiDeploymentType } else { $CurrentValue } + + while ($true) { + Write-Host "`n$PromptMessage" -ForegroundColor Cyan + for ($index = 0; $index -lt $script:SupportedOpenAiDeploymentTypes.Count; $index++) { + $deploymentType = $script:SupportedOpenAiDeploymentTypes[$index] + $defaultMarker = if ($deploymentType -eq $defaultType) { ' (default)' } else { '' } + Write-Host "[$($index + 1)] $deploymentType$defaultMarker" + } + + $selection = Read-Host "Enter 1-$($script:SupportedOpenAiDeploymentTypes.Count) or press Enter for '$defaultType'" + if ([string]::IsNullOrWhiteSpace($selection)) { + return $defaultType + } + + if ($selection -match '^\d+$') { + $selectionIndex = [int]$selection - 1 + if ($selectionIndex -ge 0 -and $selectionIndex -lt $script:SupportedOpenAiDeploymentTypes.Count) { + return $script:SupportedOpenAiDeploymentTypes[$selectionIndex] + } + } + + foreach ($deploymentType in $script:SupportedOpenAiDeploymentTypes) { + if ($selection.Equals($deploymentType, [System.StringComparison]::OrdinalIgnoreCase)) { + return $deploymentType + } + } + + Write-Warning "Invalid Azure OpenAI deployment type selection: '$selection'." + } +} + +function Resolve-OpenAiDeploymentType { + param( + [switch]$ForcePrompt + ) + + if ($globalWhichAzurePlatform -eq 'AzureUSGovernment') { + $script:ResolvedOpenAiDeploymentType = 'Standard' + return $script:ResolvedOpenAiDeploymentType + } + + if (-not $ForcePrompt -and -not [string]::IsNullOrWhiteSpace($script:ResolvedOpenAiDeploymentType)) { + return $script:ResolvedOpenAiDeploymentType + } + + if (-not $ForcePrompt -and -not [string]::IsNullOrWhiteSpace($param_AzureOpenAiDeploymentType)) { + foreach ($deploymentType in $script:SupportedOpenAiDeploymentTypes) { + if ($param_AzureOpenAiDeploymentType.Equals($deploymentType, [System.StringComparison]::OrdinalIgnoreCase)) { + $script:ResolvedOpenAiDeploymentType = $deploymentType + return $script:ResolvedOpenAiDeploymentType + } + } + + Write-Error "Invalid param_AzureOpenAiDeploymentType value '$param_AzureOpenAiDeploymentType'. Valid values: $($script:SupportedOpenAiDeploymentTypes -join ', ')." + exit 1 + } + + $script:ResolvedOpenAiDeploymentType = Select-OpenAiDeploymentType -CurrentValue $script:ResolvedOpenAiDeploymentType + return $script:ResolvedOpenAiDeploymentType +} + +function Get-OpenAiModelDeploymentDefinitions { + $deploymentType = Resolve-OpenAiDeploymentType + + return @( + [PSCustomObject]@{ + Kind = 'GPT' + DeploymentName = $param_AzureOpenAiGptDeploymentName + ModelName = $param_AzureOpenAiGptModelName + ModelVersion = $param_AzureOpenAiGptModelVersion + ModelFormat = 'OpenAI' + SkuName = $deploymentType + SkuCapacity = $param_AzureOpenAiGptDeploymentCapacity + }, + [PSCustomObject]@{ + Kind = 'Embeddings' + DeploymentName = $param_AzureOpenAiEmbeddingDeploymentName + ModelName = $param_AzureOpenAiEmbeddingModelName + ModelVersion = $param_AzureOpenAiEmbeddingModelVersion + ModelFormat = 'OpenAI' + SkuName = $deploymentType + SkuCapacity = $param_AzureOpenAiEmbeddingDeploymentCapacity + } + ) +} + +function Ensure-OpenAiModelDeployment { + param( + [string]$AccountName, + [string]$ResourceGroupName, + [string]$SubscriptionId, + [pscustomobject]$ModelDeployment + ) + + $existingDeployment = az cognitiveservices account deployment show \ + --name $AccountName \ + --resource-group $ResourceGroupName \ + --deployment-name $ModelDeployment.DeploymentName \ + --subscription $SubscriptionId \ + --query "name" \ + --output tsv 2>$null + + if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($existingDeployment)) { + Write-Host "Azure OpenAI $($ModelDeployment.Kind) deployment '$($ModelDeployment.DeploymentName)' already exists." -ForegroundColor Yellow + return $true + } + + $currentSkuName = $ModelDeployment.SkuName + + while ($true) { + Write-Host "`n=====> Creating Azure OpenAI $($ModelDeployment.Kind) deployment '$($ModelDeployment.DeploymentName)' using deployment type '$currentSkuName'..." + az cognitiveservices account deployment create \ + --name $AccountName \ + --resource-group $ResourceGroupName \ + --deployment-name $ModelDeployment.DeploymentName \ + --model-name $ModelDeployment.ModelName \ + --model-version $ModelDeployment.ModelVersion \ + --model-format $ModelDeployment.ModelFormat \ + --sku-name $currentSkuName \ + --sku-capacity $ModelDeployment.SkuCapacity \ + --subscription $SubscriptionId | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Host "Azure OpenAI $($ModelDeployment.Kind) deployment '$($ModelDeployment.DeploymentName)' created successfully." -ForegroundColor Green + return $true + } + + Write-Warning "Failed to create Azure OpenAI $($ModelDeployment.Kind) deployment '$($ModelDeployment.DeploymentName)' with deployment type '$currentSkuName'." + + if ($globalWhichAzurePlatform -eq 'AzureUSGovernment') { + return $false + } + + $retryChoice = Read-Host "Try a different Azure OpenAI deployment type for '$($ModelDeployment.DeploymentName)'? Enter Y to retry or press Enter to skip" + if ($retryChoice -notmatch '^(y|yes)$') { + return $false + } + + $currentSkuName = Select-OpenAiDeploymentType -CurrentValue $currentSkuName -PromptMessage "Select a different Azure OpenAI deployment type for '$($ModelDeployment.DeploymentName)'" + $script:ResolvedOpenAiDeploymentType = $currentSkuName + } +} + +function Invoke-AcrContainerBuild { + param( + [string]$RegistryName, + [string]$ImageName, + [string]$DockerfilePath, + [string]$BuildContextPath, + [bool]$PublishLatestTag + ) + + $containerImageInfo = Resolve-ContainerImageInfo -ImageName $ImageName + $resolvedContextPath = Resolve-Path -Path (Join-Path $PSScriptRoot $BuildContextPath) -ErrorAction Stop + $dockerfileFullPath = Join-Path $resolvedContextPath $DockerfilePath + + if (-not (Test-Path -Path $dockerfileFullPath)) { + Write-Error "Dockerfile not found at '$dockerfileFullPath'." + exit 1 + } + + $acrBuildArguments = @( + 'acr', 'build', + '--registry', $RegistryName, + '--file', $DockerfilePath, + '--image', $containerImageInfo.FullName + ) + + if ($PublishLatestTag -and $containerImageInfo.Tag -ne 'latest') { + $acrBuildArguments += @('--image', "$($containerImageInfo.Repository):latest") + } + + $acrBuildArguments += $resolvedContextPath.Path + + Write-Host "`n=====> Building container image in Azure Container Registry via ACR Tasks..." + Write-Host "Registry: $RegistryName" + Write-Host "Image: $($containerImageInfo.FullName)" + if ($PublishLatestTag -and $containerImageInfo.Tag -ne 'latest') { + Write-Host "Additional tag: $($containerImageInfo.Repository):latest" + } + Write-Host "Dockerfile: $DockerfilePath" + Write-Host "Build context: $($resolvedContextPath.Path)" + + & az @acrBuildArguments + if ($LASTEXITCODE -ne 0) { + Write-Error "ACR image build failed for '$($containerImageInfo.FullName)'." + exit 1 + } +} + function CosmosDb_CreateContainer($databaseName, $containerName) { $partitionKeyPath = "/partitionKey" @@ -294,6 +760,447 @@ function CosmosDb_CreateContainer($databaseName, $containerName) } } +function Get-SubnetContext { + param( + [string]$SubnetId + ) + + if ([string]::IsNullOrWhiteSpace($SubnetId)) { + return $null + } + + $match = [regex]::Match( + $SubnetId, + '^/subscriptions/(?[^/]+)/resourceGroups/(?[^/]+)/providers/Microsoft\.Network/virtualNetworks/(?[^/]+)/subnets/(?[^/]+)$', + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase + ) + + if (-not $match.Success) { + Write-Error "Invalid subnet resource ID format: $SubnetId" + exit 1 + } + + return [PSCustomObject]@{ + SubscriptionId = $match.Groups['subscriptionId'].Value + ResourceGroupName = $match.Groups['resourceGroupName'].Value + VirtualNetworkName = $match.Groups['virtualNetworkName'].Value + SubnetName = $match.Groups['subnetName'].Value + VirtualNetworkId = "/subscriptions/$($match.Groups['subscriptionId'].Value)/resourceGroups/$($match.Groups['resourceGroupName'].Value)/providers/Microsoft.Network/virtualNetworks/$($match.Groups['virtualNetworkName'].Value)" + } +} + +function Get-VirtualNetworkContext { + param( + [string]$VirtualNetworkId + ) + + if ([string]::IsNullOrWhiteSpace($VirtualNetworkId)) { + return $null + } + + $match = [regex]::Match( + $VirtualNetworkId, + '^/subscriptions/(?[^/]+)/resourceGroups/(?[^/]+)/providers/Microsoft\.Network/virtualNetworks/(?[^/]+)$', + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase + ) + + if (-not $match.Success) { + Write-Error "Invalid virtual network resource ID format: $VirtualNetworkId" + exit 1 + } + + return [PSCustomObject]@{ + SubscriptionId = $match.Groups['subscriptionId'].Value + ResourceGroupName = $match.Groups['resourceGroupName'].Value + VirtualNetworkName = $match.Groups['virtualNetworkName'].Value + VirtualNetworkId = $VirtualNetworkId + } +} + +function Get-PrivateDnsZoneContext { + param( + [string]$ZoneResourceId + ) + + if ([string]::IsNullOrWhiteSpace($ZoneResourceId)) { + return $null + } + + $match = [regex]::Match( + $ZoneResourceId, + '^/subscriptions/(?[^/]+)/resourceGroups/(?[^/]+)/providers/Microsoft\.Network/privateDnsZones/(?[^/]+)$', + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase + ) + + if (-not $match.Success) { + Write-Error "Invalid private DNS zone resource ID format: $ZoneResourceId" + exit 1 + } + + return [PSCustomObject]@{ + SubscriptionId = $match.Groups['subscriptionId'].Value + ResourceGroupName = $match.Groups['resourceGroupName'].Value + ZoneName = $match.Groups['zoneName'].Value + ZoneResourceId = $ZoneResourceId + } +} + +function Get-PrivateDnsZoneConfig { + param( + [string]$ZoneKey + ) + + $zoneResourceId = "" + $createVNetLink = $true + + if ($param_PrivateDnsZoneConfigs -and $param_PrivateDnsZoneConfigs.ContainsKey($ZoneKey)) { + $zoneConfig = $param_PrivateDnsZoneConfigs[$ZoneKey] + if ($zoneConfig) { + if ($zoneConfig.ContainsKey('zoneResourceId') -and -not [string]::IsNullOrWhiteSpace($zoneConfig.zoneResourceId)) { + $zoneResourceId = $zoneConfig.zoneResourceId + } + + if ($zoneConfig.ContainsKey('createVNetLink')) { + $createVNetLink = [bool]$zoneConfig.createVNetLink + } + } + } + + return [PSCustomObject]@{ + ZoneKey = $ZoneKey + ZoneName = $script:PrivateDnsZoneNames[$ZoneKey] + ZoneResourceId = $zoneResourceId + CreateVNetLink = $createVNetLink + } +} + +function Ensure-ProviderRegistered { + param( + [string]$ProviderNamespace + ) + + $providerState = az provider show --namespace $ProviderNamespace --query "registrationState" --output tsv + if ($providerState -ne "Registered") { + Write-Host "Registering provider namespace: $ProviderNamespace" + az provider register --namespace $ProviderNamespace | Out-Null + + while ($providerState -ne "Registered") { + Start-Sleep -Seconds 5 + $providerState = az provider show --namespace $ProviderNamespace --query "registrationState" --output tsv + } + } +} + +function Show-PrivateNetworkingChecklist { + if (-not $param_EnablePrivateNetworking) { + return + } + + Write-Host "`n============================================" -ForegroundColor Cyan + Write-Host "Private Networking Deployment Checklist" -ForegroundColor Cyan + Write-Host "============================================" -ForegroundColor Cyan + + if ($script:UseExistingPrivateNetwork) { + Write-Host "Mode: Reuse existing virtual network" -ForegroundColor Yellow + Write-Host "- Virtual network: $($script:ResolvedVirtualNetworkId)" + Write-Host "- App Service subnet: $($script:ResolvedAppServiceSubnetId)" + Write-Host "- Private endpoint subnet: $($script:ResolvedPrivateEndpointSubnetId)" + } else { + Write-Host "Mode: Create new virtual network" -ForegroundColor Yellow + Write-Host "- VNet address space: $($param_PrivateNetworkAddressPrefixes -join ', ')" + Write-Host "- App Service subnet prefixes: $($param_AppServiceIntegrationSubnetAddressPrefixes -join ', ')" + Write-Host "- Private endpoint subnet prefixes: $($param_PrivateEndpointSubnetAddressPrefixes -join ', ')" + } + + if ($param_AllowedIpAddresses.Count -gt 0) { + Write-Host "- Allowed admin IP addresses: $($param_AllowedIpAddresses -join ', ')" + } else { + Write-Host "- Allowed admin IP addresses: none configured" + } + + Write-Host "`nPrivate DNS zone plan:" -ForegroundColor Yellow + foreach ($zoneKey in $script:SupportedPrivateDnsZoneKeys) { + $zoneConfig = Get-PrivateDnsZoneConfig -ZoneKey $zoneKey + if ([string]::IsNullOrWhiteSpace($zoneConfig.ZoneResourceId)) { + $zoneAction = "create in deployment resource group" + } else { + $zoneAction = "reuse existing zone" + } + + if ($zoneConfig.CreateVNetLink) { + $linkAction = "ensure VNet link" + } else { + $linkAction = "do not create VNet link" + } + + Write-Host "- $zoneKey => $($zoneConfig.ZoneName) | $zoneAction | $linkAction" + if (-not [string]::IsNullOrWhiteSpace($zoneConfig.ZoneResourceId)) { + Write-Host " zoneResourceId: $($zoneConfig.ZoneResourceId)" + } + } + + Write-Host "`nQuestions to confirm:" -ForegroundColor Yellow + Write-Host "- Is the App Service integration subnet delegated to Microsoft.Web/serverFarms?" + Write-Host "- If you reuse private DNS zones and createVNetLink is false, are the required VNet links already in place?" + Write-Host "- Will administrators reach the application and private endpoints through the intended enterprise network path?" +} + +function Test-PrivateNetworkingConfiguration { + if (-not $param_EnablePrivateNetworking) { + return + } + + foreach ($zoneKey in $param_PrivateDnsZoneConfigs.Keys) { + if ($script:SupportedPrivateDnsZoneKeys -notcontains $zoneKey) { + Write-Error "Unsupported private DNS zone config key '$zoneKey'. Supported keys: $($script:SupportedPrivateDnsZoneKeys -join ', ')" + exit 1 + } + } + + $script:UseExistingPrivateNetwork = + (-not [string]::IsNullOrWhiteSpace($param_ExistingVirtualNetworkId)) -or + (-not [string]::IsNullOrWhiteSpace($param_ExistingAppServiceSubnetId)) -or + (-not [string]::IsNullOrWhiteSpace($param_ExistingPrivateEndpointSubnetId)) + + if ($script:UseExistingPrivateNetwork) { + if ([string]::IsNullOrWhiteSpace($param_ExistingAppServiceSubnetId) -or [string]::IsNullOrWhiteSpace($param_ExistingPrivateEndpointSubnetId)) { + Write-Error "When reusing an existing virtual network, both param_ExistingAppServiceSubnetId and param_ExistingPrivateEndpointSubnetId must be provided." + exit 1 + } + + $appServiceSubnetContext = Get-SubnetContext -SubnetId $param_ExistingAppServiceSubnetId + $privateEndpointSubnetContext = Get-SubnetContext -SubnetId $param_ExistingPrivateEndpointSubnetId + + if ($appServiceSubnetContext.VirtualNetworkId -ne $privateEndpointSubnetContext.VirtualNetworkId) { + Write-Error "The existing App Service subnet and private endpoint subnet must belong to the same virtual network." + exit 1 + } + + if (-not [string]::IsNullOrWhiteSpace($param_ExistingVirtualNetworkId) -and ($param_ExistingVirtualNetworkId -ne $appServiceSubnetContext.VirtualNetworkId)) { + Write-Error "param_ExistingVirtualNetworkId does not match the virtual network inferred from param_ExistingAppServiceSubnetId." + exit 1 + } + + $delegationCount = az network vnet subnet show --ids $param_ExistingAppServiceSubnetId --query "delegations[?serviceName=='Microsoft.Web/serverFarms'] | length(@)" --output tsv + if ($LASTEXITCODE -ne 0 -or [int]$delegationCount -lt 1) { + Write-Error "The existing App Service integration subnet must be delegated to Microsoft.Web/serverFarms before running this deployment." + exit 1 + } + + $script:ResolvedVirtualNetworkId = $appServiceSubnetContext.VirtualNetworkId + $script:ResolvedAppServiceSubnetId = $param_ExistingAppServiceSubnetId + $script:ResolvedPrivateEndpointSubnetId = $param_ExistingPrivateEndpointSubnetId + } + + if ($param_ShowPrivateNetworkingChecklist) { + Show-PrivateNetworkingChecklist + } + + if ($param_ConfirmPrivateNetworkingPlan) { + $confirmation = Read-Host "Continue with the private networking plan above? Enter Y to continue" + if ($confirmation -notin @('Y', 'y', 'Yes', 'YES', 'yes')) { + Write-Host "Deployment cancelled by user." -ForegroundColor Yellow + exit 0 + } + } +} + +function Ensure-PrivateNetworkingInfrastructure { + if (-not $param_EnablePrivateNetworking) { + return + } + + Ensure-ProviderRegistered -ProviderNamespace "Microsoft.Network" + + if ($script:UseExistingPrivateNetwork) { + Write-Host "`n=====> Reusing existing virtual network and subnets for private networking..." + return + } + + $virtualNetworkName = Get-ResourceName -ResourceTypeSuffix "vnet" + $virtualNetwork = az network vnet show --resource-group $resourceGroupName --name $virtualNetworkName --output json 2>$null | ConvertFrom-Json + if (-not $virtualNetwork) { + Write-Host "`n=====> Creating virtual network: $virtualNetworkName" + az network vnet create --resource-group $resourceGroupName --name $virtualNetworkName --location $paramLocation --address-prefixes @($param_PrivateNetworkAddressPrefixes) | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create virtual network '$virtualNetworkName'."; exit 1 } + } else { + Write-Host "Virtual network '$virtualNetworkName' already exists." + } + + $appServiceSubnetName = "AppServiceIntegration" + $privateEndpointSubnetName = "PrivateEndpoints" + + $appServiceSubnet = az network vnet subnet show --resource-group $resourceGroupName --vnet-name $virtualNetworkName --name $appServiceSubnetName --output json 2>$null | ConvertFrom-Json + if (-not $appServiceSubnet) { + Write-Host "Creating App Service integration subnet: $appServiceSubnetName" + az network vnet subnet create --resource-group $resourceGroupName --vnet-name $virtualNetworkName --name $appServiceSubnetName --address-prefixes @($param_AppServiceIntegrationSubnetAddressPrefixes) --delegations Microsoft.Web/serverFarms | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create App Service integration subnet '$appServiceSubnetName'."; exit 1 } + } + + az network vnet subnet update --resource-group $resourceGroupName --vnet-name $virtualNetworkName --name $appServiceSubnetName --disable-private-endpoint-network-policies false --disable-private-link-service-network-policies false | Out-Null + + $privateEndpointSubnet = az network vnet subnet show --resource-group $resourceGroupName --vnet-name $virtualNetworkName --name $privateEndpointSubnetName --output json 2>$null | ConvertFrom-Json + if (-not $privateEndpointSubnet) { + Write-Host "Creating private endpoint subnet: $privateEndpointSubnetName" + az network vnet subnet create --resource-group $resourceGroupName --vnet-name $virtualNetworkName --name $privateEndpointSubnetName --address-prefixes @($param_PrivateEndpointSubnetAddressPrefixes) | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create private endpoint subnet '$privateEndpointSubnetName'."; exit 1 } + } + + az network vnet subnet update --resource-group $resourceGroupName --vnet-name $virtualNetworkName --name $privateEndpointSubnetName --disable-private-endpoint-network-policies false --disable-private-link-service-network-policies false | Out-Null + + $script:ResolvedVirtualNetworkId = az network vnet show --resource-group $resourceGroupName --name $virtualNetworkName --query id --output tsv + $script:ResolvedAppServiceSubnetId = az network vnet subnet show --resource-group $resourceGroupName --vnet-name $virtualNetworkName --name $appServiceSubnetName --query id --output tsv + $script:ResolvedPrivateEndpointSubnetId = az network vnet subnet show --resource-group $resourceGroupName --vnet-name $virtualNetworkName --name $privateEndpointSubnetName --query id --output tsv +} + +function Ensure-PrivateDnsZone { + param( + [string]$ZoneKey + ) + + $zoneConfig = Get-PrivateDnsZoneConfig -ZoneKey $ZoneKey + $zoneResourceId = $zoneConfig.ZoneResourceId + + if ([string]::IsNullOrWhiteSpace($zoneResourceId)) { + $existingZoneId = az network private-dns zone show --resource-group $resourceGroupName --name $zoneConfig.ZoneName --query id --output tsv 2>$null + if (-not $existingZoneId) { + Write-Host "Creating private DNS zone '$($zoneConfig.ZoneName)' in resource group '$resourceGroupName'." + az network private-dns zone create --resource-group $resourceGroupName --name $zoneConfig.ZoneName | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create private DNS zone '$($zoneConfig.ZoneName)'."; exit 1 } + } + + $zoneResourceId = az network private-dns zone show --resource-group $resourceGroupName --name $zoneConfig.ZoneName --query id --output tsv + } else { + $zoneContext = Get-PrivateDnsZoneContext -ZoneResourceId $zoneResourceId + $existingZoneId = az network private-dns zone show --subscription $zoneContext.SubscriptionId --resource-group $zoneContext.ResourceGroupName --name $zoneContext.ZoneName --query id --output tsv 2>$null + if (-not $existingZoneId) { + Write-Error "The configured private DNS zone '$zoneResourceId' was not found." + exit 1 + } + } + + if ($zoneConfig.CreateVNetLink) { + $zoneContext = Get-PrivateDnsZoneContext -ZoneResourceId $zoneResourceId + $linkName = "{0}-{1}-{2}-pe-dnszonelink" -f $paramBaseName.ToLower(), $paramEnvironment.ToLower(), $script:PrivateDnsZoneLinkSuffixes[$ZoneKey] + $existingLink = az network private-dns link vnet show --subscription $zoneContext.SubscriptionId --resource-group $zoneContext.ResourceGroupName --zone-name $zoneContext.ZoneName --name $linkName --query id --output tsv 2>$null + if (-not $existingLink) { + Write-Host "Creating private DNS VNet link '$linkName' for zone '$($zoneContext.ZoneName)'." + az network private-dns link vnet create --subscription $zoneContext.SubscriptionId --resource-group $zoneContext.ResourceGroupName --zone-name $zoneContext.ZoneName --name $linkName --virtual-network $script:ResolvedVirtualNetworkId --registration-enabled false | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create private DNS VNet link '$linkName' for zone '$($zoneContext.ZoneName)'."; exit 1 } + } + } + + return $zoneResourceId +} + +function Ensure-PrivateEndpoint { + param( + [string]$NameSuffix, + [string]$ServiceResourceId, + [string]$GroupId, + [string]$ZoneKey + ) + + if ([string]::IsNullOrWhiteSpace($ServiceResourceId)) { + return + } + + $zoneResourceId = Ensure-PrivateDnsZone -ZoneKey $ZoneKey + $zoneConfig = Get-PrivateDnsZoneConfig -ZoneKey $ZoneKey + $privateEndpointName = ("{0}-{1}-{2}-pe" -f $paramBaseName, $paramEnvironment, $NameSuffix).ToLower() + $privateServiceConnectionName = ("{0}-{1}-{2}-psc" -f $paramBaseName, $paramEnvironment, $NameSuffix).ToLower() + $dnsZoneGroupName = ("{0}-{1}-{2}-dns" -f $paramBaseName, $paramEnvironment, $NameSuffix).ToLower() + + $existingPrivateEndpointId = az network private-endpoint show --resource-group $resourceGroupName --name $privateEndpointName --query id --output tsv 2>$null + if (-not $existingPrivateEndpointId) { + Write-Host "Creating private endpoint '$privateEndpointName' for resource '$ServiceResourceId'." + az network private-endpoint create --resource-group $resourceGroupName --name $privateEndpointName --location $paramLocation --subnet $script:ResolvedPrivateEndpointSubnetId --private-connection-resource-id $ServiceResourceId --group-ids $GroupId --connection-name $privateServiceConnectionName | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create private endpoint '$privateEndpointName'."; exit 1 } + } + + $existingDnsZoneGroup = az network private-endpoint dns-zone-group show --resource-group $resourceGroupName --endpoint-name $privateEndpointName --name $dnsZoneGroupName --query id --output tsv 2>$null + if (-not $existingDnsZoneGroup) { + Write-Host "Associating private endpoint '$privateEndpointName' with private DNS zone '$($zoneConfig.ZoneName)'." + az network private-endpoint dns-zone-group create --resource-group $resourceGroupName --endpoint-name $privateEndpointName --name $dnsZoneGroupName --private-dns-zone $zoneResourceId --zone-name $zoneConfig.ZoneName | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create the DNS zone group for private endpoint '$privateEndpointName'."; exit 1 } + } +} + +function Update-ResourcePropertiesById { + param( + [string]$ResourceId, + [string[]]$PropertyAssignments, + [string]$DisplayName + ) + + if ([string]::IsNullOrWhiteSpace($ResourceId)) { + return + } + + $arguments = @('resource', 'update', '--ids', $ResourceId, '--set') + $PropertyAssignments + & az @arguments | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to update resource properties for $DisplayName. Review this resource after deployment: $ResourceId" + } +} + +function Configure-PrivateNetworkingForDeployment { + param( + [string]$OpenAiResourceId + ) + + if (-not $param_EnablePrivateNetworking) { + return + } + + Ensure-PrivateNetworkingInfrastructure + + Write-Host "`n=====> Configuring App Service VNet integration and web app network settings..." + Update-ResourcePropertiesById -ResourceId $(az webapp show --name $appServiceName --resource-group $resourceGroupName --query id --output tsv) -PropertyAssignments @( + "properties.publicNetworkAccess=Disabled", + "properties.virtualNetworkSubnetId=$($script:ResolvedAppServiceSubnetId)", + "properties.vnetImagePullEnabled=true" + ) -DisplayName "App Service" + + $webAppSiteConfig = @{ vnetRouteAllEnabled = $true; healthCheckPath = "/external/healthcheck" } | ConvertTo-Json -Compress + az webapp config set --resource-group $resourceGroupName --name $appServiceName --generic-configurations $webAppSiteConfig | Out-Null + az webapp config appsettings set --resource-group $resourceGroupName --name $appServiceName --settings WEBSITE_PULL_IMAGE_OVER_VNET=true | Out-Null + + $keyVaultResourceId = az keyvault show --name $keyVaultName --resource-group $resourceGroupName --query id --output tsv + $cosmosResourceId = az cosmosdb show --name $cosmosDbName --resource-group $resourceGroupName --query id --output tsv + $acrResourceId = az acr show --name $ACR_NAME --query id --output tsv + $searchResourceId = az search service show --name $searchServiceName --resource-group $resourceGroupName --query id --output tsv + $docIntelResourceId = az cognitiveservices account show --name $docIntelName --resource-group $resourceGroupName --query id --output tsv + $storageResourceId = az storage account show --name $storageAccountName --resource-group $resourceGroupName --query id --output tsv + $webAppResourceId = az webapp show --name $appServiceName --resource-group $resourceGroupName --query id --output tsv + + Ensure-PrivateEndpoint -NameSuffix "kv" -ServiceResourceId $keyVaultResourceId -GroupId "vault" -ZoneKey "keyVault" + Ensure-PrivateEndpoint -NameSuffix "cosmosdb" -ServiceResourceId $cosmosResourceId -GroupId "sql" -ZoneKey "cosmosDb" + Ensure-PrivateEndpoint -NameSuffix "acr" -ServiceResourceId $acrResourceId -GroupId "registry" -ZoneKey "containerRegistry" + Ensure-PrivateEndpoint -NameSuffix "search" -ServiceResourceId $searchResourceId -GroupId "searchService" -ZoneKey "aiSearch" + Ensure-PrivateEndpoint -NameSuffix "storage" -ServiceResourceId $storageResourceId -GroupId "blob" -ZoneKey "blobStorage" + Ensure-PrivateEndpoint -NameSuffix "docintel" -ServiceResourceId $docIntelResourceId -GroupId "account" -ZoneKey "cognitiveServices" + Ensure-PrivateEndpoint -NameSuffix "webapp" -ServiceResourceId $webAppResourceId -GroupId "sites" -ZoneKey "webSites" + + if (-not [string]::IsNullOrWhiteSpace($OpenAiResourceId)) { + Ensure-PrivateEndpoint -NameSuffix "openai" -ServiceResourceId $OpenAiResourceId -GroupId "account" -ZoneKey "openAi" + } else { + Write-Warning "Skipping Azure OpenAI private endpoint automation because no Azure OpenAI resource ID was available." + } + + Write-Host "`n=====> Locking down public network access for private networking resources..." + Update-ResourcePropertiesById -ResourceId $keyVaultResourceId -PropertyAssignments @("properties.publicNetworkAccess=Disabled") -DisplayName "Key Vault" + Update-ResourcePropertiesById -ResourceId $cosmosResourceId -PropertyAssignments @("properties.publicNetworkAccess=Disabled", "properties.isVirtualNetworkFilterEnabled=true") -DisplayName "Cosmos DB" + Update-ResourcePropertiesById -ResourceId $acrResourceId -PropertyAssignments @("properties.publicNetworkAccess=Disabled") -DisplayName "Azure Container Registry" + Update-ResourcePropertiesById -ResourceId $searchResourceId -PropertyAssignments @("properties.publicNetworkAccess=Disabled") -DisplayName "Azure AI Search" + Update-ResourcePropertiesById -ResourceId $docIntelResourceId -PropertyAssignments @("properties.publicNetworkAccess=Disabled") -DisplayName "Document Intelligence" + Update-ResourcePropertiesById -ResourceId $storageResourceId -PropertyAssignments @("properties.publicNetworkAccess=Disabled") -DisplayName "Storage Account" + + if (-not [string]::IsNullOrWhiteSpace($OpenAiResourceId)) { + Update-ResourcePropertiesById -ResourceId $OpenAiResourceId -PropertyAssignments @("properties.publicNetworkAccess=Disabled") -DisplayName "Azure OpenAI" + } +} + #--------------------------------------------------------------------------------------------- # Construct Resource Names #--------------------------------------------------------------------------------------------- @@ -340,6 +1247,8 @@ Write-Host "-------------------------------------------------------------------- cd $PSScriptRoot #Do Not Modify +Ensure-AzureCliAuthenticated + # Check the ACR configuration Write-Host "`n=====> Checking ACR: $($ACR_NAME) for admin user enabled..." if ((az acr show --name $ACR_NAME --query "adminUserEnabled" --output tsv) -eq 'false') { @@ -350,6 +1259,12 @@ if ((az acr show --name $ACR_NAME --query "adminUserEnabled" --output tsv) -eq ' Write-Host "ACR: $($ACR_NAME) admin user is already enabled." } +if ($param_BuildContainerImageWithAcr) { + Invoke-AcrContainerBuild -RegistryName $ACR_NAME -ImageName $IMAGE_NAME -DockerfilePath $param_DockerfilePath -BuildContextPath $param_DockerBuildContextPath -PublishLatestTag $param_PublishLatestImageTag +} else { + Write-Host "Skipping ACR image build because param_BuildContainerImageWithAcr is set to false." -ForegroundColor Yellow +} + # Check CosmosDB provider state $cosmosDbProviderState = az provider show --namespace Microsoft.DocumentDB --query "registrationState" --output tsv if ($cosmosDbProviderState -ne "Registered") { @@ -377,6 +1292,16 @@ if ($global_userType -eq "servicePrincipal") { } Write-Host "Logged in as: $currentUserObjectId" -ForegroundColor Yellow +$script:CurrentSubscriptionId = az account show --query "id" -o tsv +if ([string]::IsNullOrWhiteSpace($script:CurrentSubscriptionId)) { + Write-Error "Failed to determine the active Azure subscription ID from the current Azure CLI context." + exit 1 +} + +$resolvedExistingOpenAiSubscriptionId = if ([string]::IsNullOrWhiteSpace($param_Existing_AzureOpenAi_SubscriptionId)) { $script:CurrentSubscriptionId } else { $param_Existing_AzureOpenAi_SubscriptionId } + +Test-PrivateNetworkingConfiguration + Write-Host "`nGetting Access Token Refreshed for: $paramArmUrl" -ForegroundColor Yellow az account get-access-token --resource $paramArmUrl --output none if ($LASTEXITCODE -ne 0) { Write-Error "Failed to get ARM Access Token." ; exit 1 } # Basic error check @@ -754,11 +1679,14 @@ az resource update --resource-group $resourceGroupName --name $cosmosDbName --re # $containerName = "archived_conversations" # CosmosDb_CreateContainer $databaseName $containerName -# --- Create Azure OpenAI Service --- +# --- Create Azure OpenAI Service and model deployments --- # Note: Azure OpenAI requires registration and sometimes specific SKU availability. -# This command might fail if the subscription is not enabled for OpenAI or if the SKU isn't available in the region. +# The account resource uses the Cognitive Services account SKU, while each model deployment uses its own deployment type and capacity. $openAiUrl = $null +$openAiAccountName = if ($param_UseExisting_OpenAi_Instance -eq $true) { $param_Existing_AzureOpenAi_ResourceName } else { $openAIName } +$openAiResourceGroupName = if ($param_UseExisting_OpenAi_Instance -eq $true) { $param_Existing_AzureOpenAi_ResourceGroupName } else { $resourceGroupName } +$openAiSubscriptionId = if ($param_UseExisting_OpenAi_Instance -eq $true) { $resolvedExistingOpenAiSubscriptionId } else { $script:CurrentSubscriptionId } if ($param_UseExisting_OpenAi_Instance -eq $true) { Write-Host "`n=====> Using existing Azure OpenAI Service: $($param_Existing_AzureOpenAi_ResourceName)..." #Write-Host "SubscriptionId: $param_Existing_AzureOpenAi_SubscriptionId" @@ -782,6 +1710,28 @@ if ($param_UseExisting_OpenAi_Instance -eq $true) { Write-Host "Open Ai Url: $openAiUrl" +if ($param_DeployAzureOpenAiModels) { + $openAiAccount = az cognitiveservices account show --name $openAiAccountName --resource-group $openAiResourceGroupName --subscription $openAiSubscriptionId --query "name" --output tsv 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($openAiAccount)) { + Write-Warning "Skipping Azure OpenAI model deployment creation because the Azure OpenAI account '$openAiAccountName' is not available." + } else { + $openAiModelDeployments = Get-OpenAiModelDeploymentDefinitions + foreach ($modelDeployment in $openAiModelDeployments) { + $deploymentCreated = Ensure-OpenAiModelDeployment \ + -AccountName $openAiAccountName \ + -ResourceGroupName $openAiResourceGroupName \ + -SubscriptionId $openAiSubscriptionId \ + -ModelDeployment $modelDeployment + + if (-not $deploymentCreated) { + Write-Warning "Simple Chat will still deploy, but review the Azure OpenAI deployment '$($modelDeployment.DeploymentName)' before using $($modelDeployment.Kind.ToLower()) features." + } + } + } +} else { + Write-Host "Skipping Azure OpenAI model deployment creation because param_DeployAzureOpenAiModels is set to false." -ForegroundColor Yellow +} + # --- Create Document Intelligence Service --- Write-Host "`n=====> Creating Document Intelligence Service: $($docIntelName)..." $cogServicesAccount = az cognitiveservices account show --name $docIntelName --resource-group $resourceGroupName 2>$null | ConvertFrom-Json @@ -851,6 +1801,15 @@ az webapp config appsettings set --resource-group $resourceGroupName --name $app if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to update Azure App Service App Settings." } else { Write-Host "Azure App Service App Settings configured." } +$openAiResourceIdForNetworking = "" +if ($param_UseExisting_OpenAi_Instance -eq $false) { + $openAiResourceIdForNetworking = az cognitiveservices account show --name $openAIName --resource-group $resourceGroupName --query "id" --output tsv 2>$null +} elseif (-not [string]::IsNullOrWhiteSpace($param_Existing_AzureOpenAi_ResourceName) -and -not [string]::IsNullOrWhiteSpace($param_Existing_AzureOpenAi_ResourceGroupName)) { + $openAiResourceIdForNetworking = az resource show --name $param_Existing_AzureOpenAi_ResourceName --resource-group $param_Existing_AzureOpenAi_ResourceGroupName --resource-type 'Microsoft.CognitiveServices/accounts' --subscription $resolvedExistingOpenAiSubscriptionId --query "id" --output tsv 2>$null +} + +Configure-PrivateNetworkingForDeployment -OpenAiResourceId $openAiResourceIdForNetworking + ############################################################## # RBAC ASSIGNMENTS @@ -882,7 +1841,7 @@ if ($param_UseExisting_OpenAi_Instance -eq $false) { $resourceId = az cognitiveservices account show --name $openAIName --resource-group $resourceGroupName --query "id" --output tsv } else { $resourceId = az resource show --name $param_Existing_AzureOpenAi_ResourceName --resource-group $param_Existing_AzureOpenAi_ResourceGroupName ` - --resource-type 'Microsoft.CognitiveServices/accounts' --subscription $param_Existing_AzureOpenAi_SubscriptionId --query "id"--output tsv + --resource-type 'Microsoft.CognitiveServices/accounts' --subscription $resolvedExistingOpenAiSubscriptionId --query "id" --output tsv } if ($LASTEXITCODE -ne 0) { Write-Error "Failed to find Open AI resource [$openAIName]." } diff --git a/deployers/azurecli/destroy-simplechat.ps1 b/deployers/azurecli/destroy-simplechat.ps1 index 75fb02f59..e3d545871 100644 --- a/deployers/azurecli/destroy-simplechat.ps1 +++ b/deployers/azurecli/destroy-simplechat.ps1 @@ -1,9 +1,20 @@ <# +How to authenticate before running this script: + +Azure Government: az cloud set --name AzureUSGovernment az login --scope https://management.core.usgovcloudapi.net//.default az login --scope https://graph.microsoft.us//.default -az account get-access-token --resource https://management.core.usgovcloudapi.net/ -az account get-access-token --resource https://graph.microsoft.us/ + +Azure Commercial: +az cloud set --name AzureCloud +az login --scope https://management.azure.com//.default +az login --scope https://graph.microsoft.com//.default + +Cleanup scope note: +- This script deletes the Simple Chat resource group and Entra objects created by the Azure CLI deployer. +- If the deployment reused an existing VNet, existing subnets, or customer-managed private DNS zones outside the deployment resource group, this script does not delete those shared resources. +- Any private networking resources created inside the Simple Chat deployment resource group are removed when the resource group is deleted. #> param ( @@ -28,19 +39,39 @@ $entraGroupName_Users = $entraGroupName + "-Users" $entraGroupName_CreateGroup = $entraGroupName + "-CreateGroup" $entraGroupName_SafetyViolationAdmin = $entraGroupName + "-SafetyViolationAdmin" $entraGroupName_FeedbackAdmin = $entraGroupName + "-FeedbackAdmin" -$entraSecurityGroupNames = @($entraGroupName_Admins, $entraGroupName_Users, $entraGroupName_CreateGroup, $entraGroupName_SafetyViolationAdmin, $entraGroupName_FeedbackAdmin) +$entraGroupName_CreatePublicWorkspace = $entraGroupName + "-CreatePublicWorkspace" +$entraSecurityGroupNames = @($entraGroupName_Admins, $entraGroupName_Users, $entraGroupName_CreateGroup, $entraGroupName_SafetyViolationAdmin, $entraGroupName_FeedbackAdmin, $entraGroupName_CreatePublicWorkspace) + +$currentCloudName = az cloud show --query "name" --output tsv +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($currentCloudName)) { + Write-Error "Failed to determine the active Azure cloud from Azure CLI." + exit 1 +} + +if ($currentCloudName -eq "AzureUSGovernment") { + $armResource = "https://management.core.usgovcloudapi.net/" + $graphResource = "https://graph.microsoft.us/" +} elseif ($currentCloudName -eq "AzureCloud") { + $armResource = "https://management.azure.com/" + $graphResource = "https://graph.microsoft.com/" +} else { + Write-Error "Unsupported Azure cloud '$currentCloudName'. Switch to AzureCloud or AzureUSGovernment before running this script." + exit 1 +} # --- Destroy Instance --- cls Write-Host "`nSimpleChat Destroy Executing:" -ForegroundColor Green +Write-Host "Active Azure cloud: $currentCloudName" -ForegroundColor Green Write-Host "`nHow to run this script: ./destroy-simplechat.ps1 -p -e " -ForegroundColor Yellow +Write-Host "`nCleanup note: shared VNets, shared subnets, and customer-managed private DNS zones reused by the deployment are not deleted by this script." -ForegroundColor Yellow -Write-Host "`nGetting Access Tokeen Refreshed for: https://management.core.usgovcloudapi.net/" -ForegroundColor Yellow -az account get-access-token --resource https://management.core.usgovcloudapi.net/ --output none +Write-Host "`nGetting Access Tokeen Refreshed for: $armResource" -ForegroundColor Yellow +az account get-access-token --resource $armResource --output none if ($LASTEXITCODE -ne 0) { Write-Error "Failed to get ARM Access Token." ; exit 1 } # Basic error check -Write-Host "`nGetting Access Tokeen Refreshed for: https://graph.microsoft.us/" -ForegroundColor Yellow -az account get-access-token --resource https://graph.microsoft.us/ --output none +Write-Host "`nGetting Access Tokeen Refreshed for: $graphResource" -ForegroundColor Yellow +az account get-access-token --resource $graphResource --output none if ($LASTEXITCODE -ne 0) { Write-Error "Failed to get MSGraph Access Token." ; exit 1 } # Basic error check Read-Host -Prompt "Press Enter to delete all created resources in group '$($resourceGroupName)' (or Ctrl+C to exit)" @@ -49,7 +80,7 @@ az group delete --name $resourceGroupName --yes --no-wait if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to delete Resource Group." } else { Write-Host "Resource Group '$($resourceGroupName)' deletion initiated." } -foreach ($securityGroupName in $global_EntraSecurityGroupNames) { +foreach ($securityGroupName in $entraSecurityGroupNames) { Write-Host "`nChecking if exists Security Group: $($securityGroupName)..." -ForegroundColor Yellow $entraGroup = az ad group show --group $securityGroupName --query "id" -o tsv 2>$null if (-not $entraGroup) { diff --git a/deployers/bicep/OneClickDeploy.md b/deployers/bicep/OneClickDeploy.md index d89c7dd65..659750f12 100644 --- a/deployers/bicep/OneClickDeploy.md +++ b/deployers/bicep/OneClickDeploy.md @@ -8,6 +8,13 @@ There are pre-deploy manual steps that must be completed first. After you have deployed, there are additional manual steps that will need to be completed as well. +If you plan to enable private networking and reuse an existing VNet, review the private networking guidance in [README.md](README.md) before starting. In that scenario: + +- You must provide an existing VNet resource ID. +- You must also provide both an existing App Service integration subnet ID and an existing private endpoint subnet ID. +- The deployment does not create subnets in a customer-managed VNet. +- If you reuse or centrally manage private DNS zones, make sure you understand the `privateDnsZoneConfigs` parameter and whether VNet links already exist. + [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fsimplechat%2Frefs%2Fheads%2Fmain%2Fdeployers%2Fbicep%2Fmain.json) [![Deploy to Azure](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fsimplechat%2Frefs%2Fheads%2Fmain%2Fdeployers%2Fbicep%2Fmain.json) diff --git a/deployers/bicep/README.md b/deployers/bicep/README.md index 0cf883afa..f157123f6 100644 --- a/deployers/bicep/README.md +++ b/deployers/bicep/README.md @@ -27,7 +27,8 @@ The following variables will be used within this document: - *\* - This will become the beginning of each of the objects created. Minimum of 3 characters, maximum of 12 characters. No Spaces or special characters. - *\* - This will be used as part of the object names as well as with the AZD environments. **Example:** *dev/qa/prod*. - *\* - Options will be *AzureCloud | AzureUSGovernment* -- *\* - Should be presented in the form *imageName:label* **Example:** *simple-chat:latest* +- *\* - Azure OpenAI deployment type for default model deployments. **Options:** *Standard | DatazoneStandard | GlobalStandard* for Azure Commercial. Use *Standard* for Azure Government. +- *\* - Should be presented in the form *imageName:label* **Default:** *simplechat:latest* --- @@ -37,11 +38,20 @@ Before deploying, ensure you have: 1. **Azure Subscription** with Owner or Contributor permissions 2. **Azure CLI** (version 2.50.0 or later) + Install: https://learn.microsoft.com/cli/azure/install-azure-cli 3. **Azure Developer CLI (azd)** (version 1.5.0 or later) -4. **Docker** installed and running (for container builds) -5. **PowerShell** (for the Entra app registration script) + Install: https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd +4. **Access to Azure Container Registry Tasks** so `azd up` can build the image in ACR with `az acr build` +5. **PowerShell 7** (for the Entra app registration script on Windows, Linux, or macOS) + Install: https://learn.microsoft.com/powershell/scripting/install/installing-powershell 6. **Permissions to create an Entra ID Application Registration** (or coordinate with your Entra admin) +Platform note: + +- Windows users can run the PowerShell examples directly in PowerShell. +- Linux and macOS users can run the same scripts with `pwsh`. +- If you use bash for shell commands, keep bash variable syntax in bash and invoke PowerShell scripts with `pwsh`. + ### Required Azure Resource Providers Ensure the following resource providers are registered in your subscription: - `Microsoft.Web` @@ -66,10 +76,32 @@ The following procedure must be completed with a user that has permissions to cr `./deployers/Initialize-EntraApplication.ps1`
`./deployers/azurecli/appRegistrationRoles.json` +Before running the commands below, choose which shell you are using. + +- PowerShell variable syntax: `$appName = "simplechat"` +- Bash variable syntax: `appName="simplechat"` + +Do not use bash-style variable assignment in PowerShell. `appName = "simplechat"` is not valid PowerShell. + #### Create the application registration: -`cd ./deployers`
-`.\Initialize-EntraApplication.ps1 -AppName "" -Environment "" -AppRolesJsonPath "./azurecli/appRegistrationRoles.json"` +PowerShell: + +```powershell +cd ./deployers +$appName = "simplechat" +$environment = "dev" +.\Initialize-EntraApplication.ps1 -AppName $appName -Environment $environment -AppRolesJsonPath "./azurecli/appRegistrationRoles.json" +``` + +Bash: + +```bash +cd ./deployers +appName="simplechat" +environment="dev" +pwsh ./Initialize-EntraApplication.ps1 -AppName "$appName" -Environment "$environment" -AppRolesJsonPath "./azurecli/appRegistrationRoles.json" +``` This script will create an Entra Enterprise Application, with an App Registration named *\*-*\*-ar for the web service called *\*-*\*-app. The web service name may be overriden with the `-AppServceName` parameter. A user can also specify a different expiration date for the secret which defaults to 180 days with the `-SecretExpirationDays` parameter. @@ -115,14 +147,50 @@ After the application registration has been successfully completed the following #### Configure AZD Environment -Using the bash terminal in Visual Studio Code +You can run AZD commands from either PowerShell or bash in Visual Studio Code. The AZD commands are the same, but any shell variable assignment must match the shell you chose. + +PowerShell example: + +```powershell +cd ./deployers +$environment = "dev" +azd config set cloud.name AzureCloud +azd auth login +# First time in this repo with AZD, or if AZD has not been initialized in this folder yet: +azd init +azd env new $environment +azd env select $environment +azd provision --preview +azd up +``` + +Bash example: + +```bash +cd ./deployers +environment="dev" +azd config set cloud.name AzureCloud +azd auth login +# First time in this repo with AZD, or if AZD has not been initialized in this folder yet: +azd init +azd env new "$environment" +azd env select "$environment" +azd provision --preview +azd up +``` + +If `deployers/azure.yaml` is already being recognized by AZD and the project has already been initialized on your machine, you can skip `azd init` and go straight to `azd env new`, `azd env select`, `azd provision --preview`, or `azd up`. + +If you work with other Azure clouds, you may need to update your cloud like `azd config set cloud.name AzureUSGovernment` - more information here - [Use Azure Developer CLI in sovereign clouds | Microsoft Learn](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/sovereign-clouds) `cd ./deployers` -`azd config set cloud.name AzureCloud` - If you work with other Azure clouds, you may need to update your cloud like `azd config set cloud.name AzureUSGovernment` - more information here - [Use Azure Developer CLI in sovereign clouds | Microsoft Learn](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/sovereign-clouds) +`azd config set cloud.name AzureCloud` `azd auth login` - this will open a browser window that the user with Owner level permissions to the target subscription will need to authenticate with. +`azd init` - initialize the AZD project in the current folder if this is your first time using AZD in this repo or if the folder has not been initialized yet. + `azd env new ` - Use the same value for the \ that was used in the application registration. `azd env select ` - select the new environment. @@ -131,6 +199,8 @@ Using the bash terminal in Visual Studio Code `azd up` - This step will begin the deployment process. +During `azd up`, the predeploy hook now builds the application image in Azure Container Registry by using `az acr build` with `application/single_app/Dockerfile`. A local Docker daemon is no longer required for this deployment path. + #### Service Limitations of USGovCloud > ⚠️ **Important:** Review this section carefully before deploying to Azure Government. @@ -139,8 +209,8 @@ Using the bash terminal in Visual Studio Code - Azure Video Indexer - Set `deployVideoIndexerService` to `false` - **SKU Restrictions:** - - **GlobalStandard SKU is NOT available** - Azure OpenAI models must use `Standard` SKU instead - - Default deployment uses `GlobalStandard` - override `gptModels` and `embeddingModels` parameters + - **GlobalStandard SKU is NOT available** - Azure OpenAI models must use `Standard` SKU instead + - Default model deployments use `Standard` for Azure Government even if you select another `openAIDeploymentType` - **Model Availability:** - Verify the `gptModels` and `embeddingModels` model names and versions are available in your target USGov region @@ -179,10 +249,11 @@ Using the bash terminal in Visual Studio Code > If you are unsure what a parameter is used for, see specific help for each parameter by entering "?" at that prompt. - Select an Azure Subscription to use: *\* +`allowedIpAddresses` is optional. Leave it blank when the machine running `azd up` already has private connectivity to the deployed services through your corporate network, VPN, jump host, or build agent and can resolve the private DNS names. + +`imageName` defaults to `simplechat:latest`. Most deployments can accept that default without entering a custom value. + +`openAIDeploymentType` controls the default Azure OpenAI model deployment type used when this deployment creates the GPT and embedding model deployments for you. In Azure Commercial, choose `Standard`, `DatazoneStandard`, or `GlobalStandard` based on your quota and regional availability. In Azure Government, choose `Standard`. + +If you provide custom `gptModels` or `embeddingModels` arrays, or if you reuse an existing Azure OpenAI endpoint, those explicit settings take precedence over `openAIDeploymentType`. + +Provide a value only when private networking is enabled and the machine running `azd up` must temporarily reach Cosmos DB or Azure Container Registry over a public path during deployment. In that case, enter the public egress IP address or CIDR that Azure sees for that deployment runner. In many enterprise environments this is a company NAT or proxy address, not the laptop's local `10.x.x.x`, `172.16.x.x` to `172.31.x.x`, or `192.168.x.x` address. + +If you need to identify the public egress IP for the deployment runner, use a trusted public IP lookup from that same machine and network path, for example by searching `what is my IP` in Bing. + +When private networking is enabled, the preprovision validation step now attempts to detect the deployment runner public IP automatically and merge it into `ALLOWED_IP_RANGES` for the current AZD environment before provisioning starts. That lets Cosmos DB and Azure Container Registry receive the runner IP in their initial firewall rules without waiting for a later postprovision update. + Provisioning may take between 5-40 minutes depending on the options selected. On the completion of the deployment, a URL will be presented, the user may use to access the site. @@ -244,7 +333,9 @@ User should now be able to fully use Simple Chat application. - If a deployment failure is encountered, often times, rerunning the deployment will clear the temporary error. -- When private networking is selected, 0.0.0.0 (representing the internal Azure Services) is added to the CosmosDB firewall in addition to any IP's added to the 'allowedIpAddresses' parameter. Users are encouraged to include the IP address of the deployment server in the 'allowedIpAddresses. This becomes not-applicable on completion of the deployment when CosmosDB, Key Vault, Azure Container Registry and the Web Application is configured for private networking only. If the 'allowedIpAddresses parameter is not used, the administrator can manually add in the deployment server IP address to the Settings > Networking section of the coresponding service(s) and rerun the deployment. +- When private networking is selected, `0.0.0.0` (representing Azure services) is added to the Cosmos DB firewall in addition to any IPs provided in `allowedIpAddresses`. Leave `allowedIpAddresses` blank when the deployment runner already has private connectivity and private DNS resolution through your enterprise network design. Provide it only when the deployment runner must temporarily reach Cosmos DB or Azure Container Registry from a public egress path during setup. The preprovision step now tries to auto-add the runner public IP to the AZD environment before provisioning. If you later add firewall rules manually in Azure Portal, allow up to 30 minutes for those changes to propagate before rerunning the deployment. + +- When private networking is selected, the Azure Container Registry deployment enables the trusted Azure services bypass so `az acr build` can run through ACR Tasks even while the registry network rule set remains restricted. A denied ACR build that references an Azure-hosted IP such as the task agent usually means the registry was provisioned before this setting was enabled and needs to be reprovisioned. - To evaluate any infrastructure changes between versions, with AZD the user can run: `azd provision --preview` @@ -253,7 +344,48 @@ User should now be able to fully use Simple Chat application. When private networking is configured, access from the developers workstation to push updates and new Azure configurations will be blocked. In addition, testing the web application when not on a VPN attached to the private network subnet is expected to be blocked. -During initial deployment, if post an error is raised "failed running post hooks: 'postprovision'" the deployment is being blocked from executing scripts against the CosmosDB service. Ensure the deployment workstation IP address is added to the "allowedIPAddresses" parameter. Similar messages may be seen from the Azure Container Registry Service. +For enterprise deployments, focus on the machine running `azd up`, not just the user laptop. If that deployment runner is inside the private network path and can resolve the private endpoints, `allowedIpAddresses` is typically not needed. If the deployment runner is outside that private path and must temporarily reach protected services over the internet, provide the runner's public egress IP or CIDR. + +#### Existing VNet Prerequisites + +If you enable private networking and choose to reuse an existing VNet, the deployment assumes the network prerequisites are already in place. + +- Provide `existingVirtualNetworkId` only when you want to reuse an existing VNet. +- When `existingVirtualNetworkId` is provided, you must also provide both `existingAppServiceSubnetId` and `existingPrivateEndpointSubnetId`. +- The template does not create subnets inside an existing customer-managed VNet. +- The App Service integration subnet must already exist and be prepared for App Service regional VNet integration, including delegation to `Microsoft.Web/serverFarms`. +- The private endpoint subnet must already exist and be intended for private endpoints for the Azure services you enable. +- The existing VNet and subnets should be reachable and approved under your organization's network design, including any cross-resource-group or cross-subscription access requirements. + +#### Private DNS Zone Guidance + +Private DNS is required for reliable name resolution when private endpoints are enabled. + +- If you leave `privateDnsZoneConfigs` empty, the deployment creates the required private DNS zones in the deployment resource group and links them to the VNet automatically. +- If your organization already manages private DNS centrally, provide `privateDnsZoneConfigs` with one or more `zoneResourceId` values to reuse those existing zones. +- Set `createVNetLink` to `false` only when the DNS zone is already linked to the target VNet or when your networking team will manage the VNet link separately. +- If you reuse an existing VNet but do not create or link the required private DNS zones, the application may deploy successfully but private name resolution to services such as Cosmos DB, Storage, Azure AI Search, Azure OpenAI, Key Vault, and App Service private endpoints will fail. + +Example `privateDnsZoneConfigs` value: + +```json +{ + "openAi": { + "zoneResourceId": "/subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.openai.azure.com", + "createVNetLink": true + }, + "cosmosDb": { + "zoneResourceId": "/subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.documents.azure.com", + "createVNetLink": false + } +} +``` + +Supported `privateDnsZoneConfigs` keys: `keyVault`, `cosmosDb`, `containerRegistry`, `aiSearch`, `blobStorage`, `cognitiveServices`, `openAi`, and `webSites`. + +During initial deployment, if post an error is raised "failed running post hooks: 'postprovision'" the deployment is being blocked from executing scripts against the CosmosDB service. Ensure the deployment workstation IP address is added to the `allowedIpAddresses` parameter or the `ALLOWED_IP_RANGES` AZD environment value. Similar messages may be seen from the Azure Container Registry service. If you add the firewall rule manually in Azure Portal, wait up to 30 minutes for propagation before rerunning `azd up`. + +If `az acr build` fails with `denied: client with IP '
' is not allowed access`, adding the workstation IP usually does not help because the denied address is commonly the Azure Container Registry Tasks worker, not the local machine. In that case, ensure the registry allows trusted Azure services and reprovision the registry configuration. When private networking is enabled, to test the web applicaiton, users may configure a VPN into the deployed vNet (space is provided for this) or the administration may adjust the networking limitations to the deployed website. This may be accomplished with the following script: @@ -332,8 +464,8 @@ A: Override the `gptModels` parameter with your desired configuration: ] ``` -**Q: What's the difference between GlobalStandard and Standard SKU?** -A: `GlobalStandard` provides access to Azure's global AI infrastructure with higher availability but is not available in Azure Government. `Standard` is region-specific and is required for USGov deployments. +**Q: What's the difference between GlobalStandard, DatazoneStandard, and Standard SKU?** +A: `GlobalStandard` uses Azure's global AI infrastructure, `DatazoneStandard` uses the regional data zone option when available, and `Standard` is region-specific. Availability and quota can differ by region and subscription. Azure Government deployments should use `Standard`. ### Networking @@ -341,7 +473,19 @@ A: `GlobalStandard` provides access to Azure's global AI infrastructure with hig A: Yes, set `enablePrivateNetworking` to `false` initially. You can enable it later but this requires re-running the deployment. **Q: Why do I need to add my IP address to allowedIpAddresses?** -A: During deployment, scripts need to access Cosmos DB and other services. Your IP must be allowed through the firewall temporarily. +A: You only need to provide `allowedIpAddresses` when the machine running `azd up` must temporarily reach Cosmos DB or Azure Container Registry over a public network path during deployment. If your deployment runner is already on a private corporate network, VPN, jump host, or build agent with private connectivity and private DNS resolution to those services, leave the value blank. + +**Q: Which IP address should I provide if I do need it?** +A: Provide the public egress IP address or CIDR that Azure sees for the machine running `azd up`. In enterprise environments this is often a company NAT or proxy address rather than the laptop's local private address. A simple way to identify it is to use a trusted public IP lookup from that same machine and network path, for example by searching `what is my IP` in Bing. + +**Q: What do I need before selecting an existing VNet?** +A: You need an existing VNet plus both an App Service integration subnet and a private endpoint subnet. The deployment does not create subnets in a reused VNet. + +**Q: What happens if I leave privateDnsZoneConfigs empty?** +A: The deployment creates the supported private DNS zones in the deployment resource group and creates VNet links automatically. + +**Q: When should I set createVNetLink to false?** +A: Only when the DNS zone is already linked to the target VNet or when a central networking team manages those links outside this deployment. ### Costs @@ -366,7 +510,7 @@ A: Run `azd up` again from the updated codebase. Use `azd provision --preview` t **Error: "failed running post hooks: 'postprovision'"** - **Cause:** Deployment scripts cannot access Cosmos DB or other services -- **Solution:** Add your IP address to the `allowedIpAddresses` parameter and redeploy +- **Solution:** If the deployment runner is outside the private network path, add its public egress IP or CIDR to `allowedIpAddresses` and redeploy. If the deployment runner is already on the private network path, verify private DNS resolution and routing to the private endpoints instead of adding a public IP. **Error: "The subscription is not registered to use namespace 'Microsoft.CognitiveServices'"** - **Cause:** Required resource provider not registered @@ -374,7 +518,9 @@ A: Run `azd up` again from the updated codebase. Use `azd provision --preview` t **Error: "Quota exceeded for deployment"** - **Cause:** Azure OpenAI quota limits reached -- **Solution:** Request quota increase or reduce `skuCapacity` in model configuration +- **Solution:** Try a different `openAIDeploymentType`, request quota increase, or reduce `skuCapacity` in custom model configuration. Then rerun `azd up`. + +If you do not want this deployment to create new Azure OpenAI model deployments, rerun with an existing Azure OpenAI endpoint and complete the remaining model configuration manually in Simple Chat after deployment. See `docs/setup_instructions_manual.md` and `docs/admin_configuration.md` for the manual steps. **Error: "InvalidTemplateDeployment - GlobalStandard SKU not available"** - **Cause:** Attempting USGov deployment with GlobalStandard SKU diff --git a/deployers/bicep/cosmosDb-postDeployPerms.sh b/deployers/bicep/cosmosDb-postDeployPerms.sh index c4d3c311f..eb8174d84 100644 --- a/deployers/bicep/cosmosDb-postDeployPerms.sh +++ b/deployers/bicep/cosmosDb-postDeployPerms.sh @@ -14,6 +14,35 @@ UPN=$(az account show --query user.name -o tsv) OBJECT_ID=$(az ad signed-in-user show --query id -o tsv) SUBSCRIPTION_ID=$(az account show --query id -o tsv) +is_mfa_error() { + printf '%s' "$1" | grep -qiE 'AADSTS50076|multi-factor authentication|claims challenge' +} + +is_already_exists_error() { + printf '%s' "$1" | grep -qiE 'already exists|RoleAssignmentExists|Conflict' +} + +handle_role_assignment_result() { + local description="$1" + local command_output="$2" + + if is_already_exists_error "$command_output"; then + echo "ℹ $description already exists." + return 0 + fi + + if is_mfa_error "$command_output"; then + echo "⚠ Azure CLI requires multi-factor authentication before it can complete $description." >&2 + echo " Continuing with Cosmos DB key-based post-deployment configuration." >&2 + echo " If you want the signed-in user to keep Cosmos DB access, run 'az login --scope https://management.azure.com//.default' and rerun the deployment later." >&2 + return 0 + fi + + echo "✗ ERROR: Failed to complete $description" >&2 + echo "$command_output" >&2 + exit 1 +} + # Control-plane assignment SCOPE="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG_NAME/providers/Microsoft.DocumentDB/databaseAccounts/$ACCOUNT_NAME" @@ -21,11 +50,11 @@ ROLE_NAME="Contributor" ROLE_ID=$(az role definition list --name "$ROLE_NAME" --query "[0].id" -o tsv) echo "Assigning role '$ROLE_NAME' to user '$UPN' on scope '$SCOPE'..." -az role assignment create \ +CONTROL_PLANE_OUTPUT=$(az role assignment create \ --assignee-object-id "$OBJECT_ID" \ --assignee-principal-type "User" \ --role "$ROLE_ID" \ - --scope "$SCOPE" || echo "Control-plane role may already exist." + --scope "$SCOPE" 2>&1) || handle_role_assignment_result "the control-plane role assignment" "$CONTROL_PLANE_OUTPUT" # Data-plane assignment @@ -37,12 +66,12 @@ DP_ROLE_ID=$(az cosmosdb sql role definition list \ echo "Assigning data-plane role '$DP_ROLE_NAME' to user '$UPN' on Cosmos DB account '$ACCOUNT_NAME'..." -az cosmosdb sql role assignment create \ +DATA_PLANE_OUTPUT=$(az cosmosdb sql role assignment create \ --account-name "$ACCOUNT_NAME" \ --resource-group "$RG_NAME" \ --scope "/" \ --principal-id "$OBJECT_ID" \ - --role-definition-id "$DP_ROLE_ID" || echo "Data-plane role may already exist." + --role-definition-id "$DP_ROLE_ID" 2>&1) || handle_role_assignment_result "the Cosmos DB data-plane role assignment" "$DATA_PLANE_OUTPUT" echo "Assigned Cosmos roles to $UPN ($OBJECT_ID)." echo "===============================" \ No newline at end of file diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index bcf19d318..7e4c8c144 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -36,7 +36,7 @@ param azdEnvironmentName string @description('''The name of the container image to deploy to the web app. - should be in the format :''') -param imageName string +param imageName string = 'simplechat:latest' @description('''Azure AD Application Client ID for enterprise authentication. - Should be the client ID of the registered Azure AD application''') @@ -80,37 +80,67 @@ param enableDiagLogging bool - Default is false''') param enablePrivateNetworking bool +@description('''Optional existing virtual network resource ID to reuse when private networking is enabled. +- May reference a virtual network in the same or another resource group or subscription +- Leave blank to create a new virtual network''') +param existingVirtualNetworkId string = '' + +@description('''Optional existing subnet resource ID to use for App Service VNet integration. +- May reference a subnet in the same or another resource group or subscription +- Required when reusing an existing virtual network because subnets are not created in external virtual networks''') +param existingAppServiceSubnetId string = '' + +@description('''Optional existing subnet resource ID to use for private endpoints. +- May reference a subnet in the same or another resource group or subscription +- Required when reusing an existing virtual network because subnets are not created in external virtual networks''') +param existingPrivateEndpointSubnetId string = '' + +@description('''Optional per-zone private DNS configuration for private networking. +- Leave empty to create all private DNS zones locally and create VNet links automatically +- For each supported key, provide: + - zoneResourceId: Optional existing private DNS zone resource ID to reuse + - createVNetLink: Optional bool, defaults to true. Set to false if the customer manages the VNet link separately +- Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites''') +param privateDnsZoneConfigs object = {} + +@description('''Optional existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint. +- Leave blank to deploy a new Azure OpenAI resource +- Public Azure AI Foundry project endpoints are supported for application configuration, but do not support private endpoint automation''') +param existingOpenAIEndpoint string = '' + +@description('''Optional existing Azure OpenAI resource name. +- Provide this when reusing a standard Azure OpenAI resource and you want managed identity permissions or private endpoint integration configured automatically''') +param existingOpenAIResourceName string = '' + +@description('''Optional resource group for an existing Azure OpenAI resource. +- Used when reusing a standard Azure OpenAI resource across resource groups or subscriptions''') +param existingOpenAIResourceGroup string = '' + +@description('''Optional subscription ID for an existing Azure OpenAI resource. +- Used when reusing a standard Azure OpenAI resource across subscriptions''') +param existingOpenAISubscriptionId string = '' + +@description('''Optional API key for an existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint. +- Required for key-based authentication when reusing an external endpoint and automatic key retrieval is not available''') +@secure() +param existingOpenAIKey string = '' + +@description('''Azure OpenAI deployment type used for the default GPT and embedding model deployments. +- Azure Commercial options: Standard, DatazoneStandard, GlobalStandard +- Azure Government default model deployments use Standard regardless of this selection +- Ignored when you provide custom gptModels or embeddingModels arrays''') +@allowed([ + 'Standard' + 'DatazoneStandard' + 'GlobalStandard' +]) +param openAIDeploymentType string + @description('''Array of GPT model names to deploy to the OpenAI resource.''') -param gptModels array = [ - { - modelName: 'gpt-4.1' - modelVersion: '2025-04-14' - skuName: 'GlobalStandard' - skuCapacity: 150 - } - { - modelName: 'gpt-4o' - modelVersion: '2024-11-20' - skuName: 'GlobalStandard' - skuCapacity: 100 - } -] +param gptModels array = [] @description('''Array of embedding model names to deploy to the OpenAI resource.''') -param embeddingModels array = [ - { - modelName: 'text-embedding-3-small' - modelVersion: '1' - skuName: 'GlobalStandard' - skuCapacity: 150 - } - { - modelName: 'text-embedding-3-large' - modelVersion: '1' - skuName: 'GlobalStandard' - skuCapacity: 150 - } -] +param embeddingModels array = [] //---------------- // allowed IP addresses for resources @@ -120,7 +150,7 @@ Leave blank if not using private networking. - Format for range: 'x.x.x.x/y' - Example: 1.2.3.4, 2.3.4.5/32 ''') -param allowedIpAddresses string +param allowedIpAddresses string = '' var allowedIpAddressesSplit = empty(allowedIpAddresses) ? [] : split(allowedIpAddresses!, ',') var allowedIpAddressesArray = [for ip in allowedIpAddressesSplit: trim(ip)] //---------------- @@ -154,6 +184,40 @@ var acrName = toLower('${appName}${environment}acr') var containerRegistry = '${acrName}${acrCloudSuffix}' var containerImageName = '${containerRegistry}/${imageName}' var vNetName = '${appName}-${environment}-vnet' +var normalizedLocation = toLower(replace(location, ' ', '')) +var resolvedOpenAIDeploymentType = cloudEnvironment == 'AzureUSGovernment' ? 'Standard' : openAIDeploymentType +var defaultGptModels = [ + { + modelName: 'gpt-4o' + modelVersion: cloudEnvironment == 'AzureUSGovernment' ? '2024-05-13' : '2024-11-20' + skuName: resolvedOpenAIDeploymentType + skuCapacity: 100 + } +] +var defaultEmbeddingModels = cloudEnvironment == 'AzureUSGovernment' + ? [ + { + modelName: normalizedLocation == 'usgovvirginia' ? 'text-embedding-ada-002' : 'text-embedding-3-small' + modelVersion: normalizedLocation == 'usgovvirginia' ? '2' : '1' + skuName: resolvedOpenAIDeploymentType + skuCapacity: 100 + } + ] + : [ + { + modelName: 'text-embedding-3-small' + modelVersion: '1' + skuName: resolvedOpenAIDeploymentType + skuCapacity: 100 + } + ] +var resolvedGptModels = empty(gptModels) ? defaultGptModels : gptModels +var resolvedEmbeddingModels = empty(embeddingModels) ? defaultEmbeddingModels : embeddingModels +var hasExistingAppServiceSubnetId = !empty(existingAppServiceSubnetId) +var hasExistingPrivateEndpointSubnetId = !empty(existingPrivateEndpointSubnetId) +var inferredVirtualNetworkId = hasExistingAppServiceSubnetId ? split(existingAppServiceSubnetId, '/subnets/')[0] : (hasExistingPrivateEndpointSubnetId ? split(existingPrivateEndpointSubnetId, '/subnets/')[0] : '') +var resolvedExistingVirtualNetworkId = !empty(existingVirtualNetworkId) ? existingVirtualNetworkId : inferredVirtualNetworkId +var useExistingVirtualNetwork = enablePrivateNetworking && (!empty(resolvedExistingVirtualNetworkId) || hasExistingAppServiceSubnetId || hasExistingPrivateEndpointSubnetId) var allowedIpsForCosmos = union(['0.0.0.0'], allowedIpAddressesArray) var cosmosDbIpRules = [for ip in allowedIpsForCosmos: { ipAddressOrRange: ip @@ -162,6 +226,12 @@ var acrIpRules = [for ip in allowedIpAddressesArray: { action: 'Allow' value: ip }] +#disable-next-line BCP318 // value can't be null when a new virtual network is created +var resolvedVirtualNetworkId = enablePrivateNetworking ? (useExistingVirtualNetwork ? resolvedExistingVirtualNetworkId : virtualNetwork.outputs.vNetId) : '' +#disable-next-line BCP318 // value can't be null when a new virtual network is created +var resolvedAppServiceSubnetId = enablePrivateNetworking ? (useExistingVirtualNetwork ? existingAppServiceSubnetId : virtualNetwork.outputs.appServiceSubnetId) : '' +#disable-next-line BCP318 // value can't be null when a new virtual network is created +var resolvedPrivateEndpointSubnetId = enablePrivateNetworking ? (useExistingVirtualNetwork ? existingPrivateEndpointSubnetId : virtualNetwork.outputs.privateNetworkSubnetId) : '' //========================================================= // Resource group deployment @@ -175,7 +245,7 @@ resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { //========================================================= // Create Virtual Network if private networking is enabled //========================================================= -module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { +module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking && !useExistingVirtualNetwork) { scope: rg name: 'virtualNetwork' params: { @@ -385,8 +455,13 @@ module openAI 'modules/openAI.bicep' = { authenticationType: authenticationType configureApplicationPermissions: configureApplicationPermissions - gptModels: gptModels - embeddingModels: embeddingModels + existingOpenAIEndpoint: existingOpenAIEndpoint + existingOpenAIResourceName: existingOpenAIResourceName + existingOpenAIResourceGroup: existingOpenAIResourceGroup + existingOpenAISubscriptionId: existingOpenAISubscriptionId + existingOpenAIKey: existingOpenAIKey + gptModels: resolvedGptModels + embeddingModels: resolvedEmbeddingModels enablePrivateNetworking: enablePrivateNetworking } @@ -428,6 +503,7 @@ module appService 'modules/appService.bicep' = { cosmosDbName: cosmosDB.outputs.cosmosDbName searchServiceName: searchService.outputs.searchServiceName openAiServiceName: openAI.outputs.openAIName + openAiEndpoint: openAI.outputs.openAIEndpoint openAiResourceGroupName: openAI.outputs.openAIResourceGroup documentIntelligenceServiceName: docIntel.outputs.documentIntelligenceServiceName appInsightsName: applicationInsights.outputs.appInsightsName @@ -437,8 +513,7 @@ module appService 'modules/appService.bicep' = { keyVaultUri: keyVault.outputs.keyVaultUri enablePrivateNetworking: enablePrivateNetworking - #disable-next-line BCP318 // expect one value to be null if private networking is disabled - appServiceSubnetId: enablePrivateNetworking? virtualNetwork.outputs.appServiceSubnetId : '' + appServiceSubnetId: resolvedAppServiceSubnetId } } @@ -548,6 +623,8 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP cosmosDBName: cosmosDB.outputs.cosmosDbName acrName: acr.outputs.acrName openAIName: openAI.outputs.openAIName + openAIResourceGroupName: openAI.outputs.openAIResourceGroup + openAISubscriptionId: openAI.outputs.openAISubscriptionId docIntelName: docIntel.outputs.documentIntelligenceServiceName storageAccountName: storageAccount.outputs.name searchServiceName: searchService.outputs.searchServiceName @@ -571,10 +648,9 @@ module privateNetworking 'modules/privateNetworking.bicep' = if (enablePrivateNe scope: rg params: { - #disable-next-line BCP318 // value can't be null based on enablePrivateNetworking condition - virtualNetworkId: virtualNetwork.outputs.vNetId - #disable-next-line BCP318 // value can't be null based on enablePrivateNetworking condition - privateEndpointSubnetId: virtualNetwork.outputs.privateNetworkSubnetId + virtualNetworkId: resolvedVirtualNetworkId + privateEndpointSubnetId: resolvedPrivateEndpointSubnetId + privateDnsZoneConfigs: privateDnsZoneConfigs location: location appName: appName @@ -588,6 +664,8 @@ module privateNetworking 'modules/privateNetworking.bicep' = if (enablePrivateNe docIntelName: docIntel.outputs.documentIntelligenceServiceName storageAccountName: storageAccount.outputs.name openAIName: openAI.outputs.openAIName + openAIResourceGroupName: openAI.outputs.openAIResourceGroup + openAISubscriptionId: openAI.outputs.openAISubscriptionId webAppName: appService.outputs.name #disable-next-line BCP318 // expect one value to be null @@ -619,9 +697,10 @@ output var_documentIntelligenceServiceEndpoint string = docIntel.outputs.documen output var_keyVaultName string = keyVault.outputs.keyVaultName output var_keyVaultUri string = keyVault.outputs.keyVaultUri output var_openAIEndpoint string = openAI.outputs.openAIEndpoint -output var_openAIGPTModels array = gptModels +output var_openAIGPTModels array = resolvedGptModels output var_openAIResourceGroup string = openAI.outputs.openAIResourceGroup //may be able to remove -output var_openAIEmbeddingModels array = embeddingModels +output var_openAIEmbeddingModels array = resolvedEmbeddingModels +output var_openAISubscriptionId string = openAI.outputs.openAISubscriptionId #disable-next-line BCP318 // expect one value to be null output var_redisCacheHostName string = deployRedisCache ? redisCache.outputs.redisCacheHostName : '' output var_rgName string = rgName diff --git a/deployers/bicep/main.json b/deployers/bicep/main.json index a3e17b225..9c0dcfc3d 100644 --- a/deployers/bicep/main.json +++ b/deployers/bicep/main.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "12894563845993036077" + "version": "0.41.2.15936", + "templateHash": "13825130652851399501" } }, "parameters": { @@ -109,53 +109,88 @@ "description": "Enable private endpoints and virtual network integration for deployed resources. \n- Default is false" } }, + "existingVirtualNetworkId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing virtual network resource ID to reuse when private networking is enabled.\n- May reference a virtual network in the same or another resource group or subscription\n- Leave blank to create a new virtual network" + } + }, + "existingAppServiceSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing subnet resource ID to use for App Service VNet integration.\n- May reference a subnet in the same or another resource group or subscription\n- Required when reusing an existing virtual network because subnets are not created in external virtual networks" + } + }, + "existingPrivateEndpointSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing subnet resource ID to use for private endpoints.\n- May reference a subnet in the same or another resource group or subscription\n- Required when reusing an existing virtual network because subnets are not created in external virtual networks" + } + }, + "privateDnsZoneConfigs": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional per-zone private DNS configuration for private networking.\n- Leave empty to create all private DNS zones locally and create VNet links automatically\n- For each supported key, provide:\n - zoneResourceId: Optional existing private DNS zone resource ID to reuse\n - createVNetLink: Optional bool, defaults to true. Set to false if the customer manages the VNet link separately\n- Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites" + } + }, + "existingOpenAIEndpoint": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint.\n- Leave blank to deploy a new Azure OpenAI resource\n- Public Azure AI Foundry project endpoints are supported for application configuration, but do not support private endpoint automation" + } + }, + "existingOpenAIResourceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing Azure OpenAI resource name.\n- Provide this when reusing a standard Azure OpenAI resource and you want managed identity permissions or private endpoint integration configured automatically" + } + }, + "existingOpenAIResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional resource group for an existing Azure OpenAI resource.\n- Used when reusing a standard Azure OpenAI resource across resource groups or subscriptions" + } + }, + "existingOpenAISubscriptionId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional subscription ID for an existing Azure OpenAI resource.\n- Used when reusing a standard Azure OpenAI resource across subscriptions" + } + }, + "existingOpenAIKey": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional API key for an existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint.\n- Required for key-based authentication when reusing an external endpoint and automatic key retrieval is not available" + } + }, "gptModels": { "type": "array", - "defaultValue": [ - { - "modelName": "gpt-4.1", - "modelVersion": "2025-04-14", - "skuName": "GlobalStandard", - "skuCapacity": 150 - }, - { - "modelName": "gpt-4o", - "modelVersion": "2024-11-20", - "skuName": "GlobalStandard", - "skuCapacity": 100 - } - ], + "defaultValue": [], "metadata": { "description": "Array of GPT model names to deploy to the OpenAI resource." } }, "embeddingModels": { "type": "array", - "defaultValue": [ - { - "modelName": "text-embedding-3-small", - "modelVersion": "1", - "skuName": "GlobalStandard", - "skuCapacity": 150 - }, - { - "modelName": "text-embedding-3-large", - "modelVersion": "1", - "skuName": "GlobalStandard", - "skuCapacity": 150 - } - ], + "defaultValue": [], "metadata": { "description": "Array of embedding model names to deploy to the OpenAI resource." } }, "allowedIpAddresses": { - "type": "array", - "defaultValue": [ - { - "ipAddressOrRange": "0.0.0.0" - } - ] + "type": "string", + "metadata": { + "description": "Comma separated list of IP addresses or ranges to allow access to resources when private networking is enabled.\nLeave blank if not using private networking.\n- Format for single IP: 'x.x.x.x'\n- Format for range: 'x.x.x.x/y'\n- Example: 1.2.3.4, 2.3.4.5/32\n" + } }, "deployContentSafety": { "type": "bool", @@ -183,6 +218,29 @@ } }, "variables": { + "copy": [ + { + "name": "allowedIpAddressesArray", + "count": "[length(variables('allowedIpAddressesSplit'))]", + "input": "[trim(variables('allowedIpAddressesSplit')[copyIndex('allowedIpAddressesArray')])]" + }, + { + "name": "cosmosDbIpRules", + "count": "[length(variables('allowedIpsForCosmos'))]", + "input": { + "ipAddressOrRange": "[variables('allowedIpsForCosmos')[copyIndex('cosmosDbIpRules')]]" + } + }, + { + "name": "acrIpRules", + "count": "[length(variables('allowedIpAddressesArray'))]", + "input": { + "action": "Allow", + "value": "[variables('allowedIpAddressesArray')[copyIndex('acrIpRules')]]" + } + } + ], + "allowedIpAddressesSplit": "[if(empty(parameters('allowedIpAddresses')), createArray(), split(parameters('allowedIpAddresses'), ','))]", "rgName": "[format('{0}-{1}-rg', parameters('appName'), parameters('environment'))]", "requiredTags": { "application": "[parameters('appName')]", @@ -194,7 +252,25 @@ "acrName": "[toLower(format('{0}{1}acr', parameters('appName'), parameters('environment')))]", "containerRegistry": "[format('{0}{1}', variables('acrName'), variables('acrCloudSuffix'))]", "containerImageName": "[format('{0}/{1}', variables('containerRegistry'), parameters('imageName'))]", - "vNetName": "[format('{0}-{1}-vnet', parameters('appName'), parameters('environment'))]" + "vNetName": "[format('{0}-{1}-vnet', parameters('appName'), parameters('environment'))]", + "normalizedLocation": "[toLower(replace(parameters('location'), ' ', ''))]", + "defaultGptModels": [ + { + "modelName": "gpt-4o", + "modelVersion": "[if(equals(parameters('cloudEnvironment'), 'AzureUSGovernment'), '2024-05-13', '2024-11-20')]", + "skuName": "Standard", + "skuCapacity": 100 + } + ], + "defaultEmbeddingModels": "[if(equals(parameters('cloudEnvironment'), 'AzureUSGovernment'), createArray(createObject('modelName', if(equals(variables('normalizedLocation'), 'usgovvirginia'), 'text-embedding-ada-002', 'text-embedding-3-small'), 'modelVersion', if(equals(variables('normalizedLocation'), 'usgovvirginia'), '2', '1'), 'skuName', 'Standard', 'skuCapacity', 100)), createArray(createObject('modelName', 'text-embedding-3-small', 'modelVersion', '1', 'skuName', 'Standard', 'skuCapacity', 100)))]", + "resolvedGptModels": "[if(empty(parameters('gptModels')), variables('defaultGptModels'), parameters('gptModels'))]", + "resolvedEmbeddingModels": "[if(empty(parameters('embeddingModels')), variables('defaultEmbeddingModels'), parameters('embeddingModels'))]", + "hasExistingAppServiceSubnetId": "[not(empty(parameters('existingAppServiceSubnetId')))]", + "hasExistingPrivateEndpointSubnetId": "[not(empty(parameters('existingPrivateEndpointSubnetId')))]", + "inferredVirtualNetworkId": "[if(variables('hasExistingAppServiceSubnetId'), split(parameters('existingAppServiceSubnetId'), '/subnets/')[0], if(variables('hasExistingPrivateEndpointSubnetId'), split(parameters('existingPrivateEndpointSubnetId'), '/subnets/')[0], ''))]", + "resolvedExistingVirtualNetworkId": "[if(not(empty(parameters('existingVirtualNetworkId'))), parameters('existingVirtualNetworkId'), variables('inferredVirtualNetworkId'))]", + "useExistingVirtualNetwork": "[and(parameters('enablePrivateNetworking'), or(or(not(empty(variables('resolvedExistingVirtualNetworkId'))), variables('hasExistingAppServiceSubnetId')), variables('hasExistingPrivateEndpointSubnetId')))]", + "allowedIpsForCosmos": "[union(createArray('0.0.0.0'), variables('allowedIpAddressesArray'))]" }, "resources": [ { @@ -205,7 +281,7 @@ "tags": "[variables('tags')]" }, { - "condition": "[parameters('enablePrivateNetworking')]", + "condition": "[and(parameters('enablePrivateNetworking'), not(variables('useExistingVirtualNetwork')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "virtualNetwork", @@ -253,8 +329,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "10221795613826494860" + "version": "0.41.2.15936", + "templateHash": "493906105301296560" } }, "parameters": { @@ -369,8 +445,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "16638534490731333297" + "version": "0.41.2.15936", + "templateHash": "3702219330633152864" } }, "parameters": { @@ -446,8 +522,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "16543999957549068652" + "version": "0.41.2.15936", + "templateHash": "16478820448576828524" } }, "parameters": { @@ -530,8 +606,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "3503710577838836741" + "version": "0.41.2.15936", + "templateHash": "3020666832422074415" } }, "parameters": { @@ -579,7 +655,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.KeyVault/vaults/{0}', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -607,8 +683,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -777,8 +853,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -857,7 +933,7 @@ "value": "[parameters('enablePrivateNetworking')]" }, "allowedIpAddresses": { - "value": "[parameters('allowedIpAddresses')]" + "value": "[variables('cosmosDbIpRules')]" } }, "template": { @@ -866,8 +942,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "15046922718277168026" + "version": "0.41.2.15936", + "templateHash": "13832537678441688412" } }, "parameters": { @@ -973,7 +1049,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -1001,8 +1077,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -1148,8 +1224,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -1241,6 +1317,9 @@ }, "enablePrivateNetworking": { "value": "[parameters('enablePrivateNetworking')]" + }, + "allowedIpAddresses": { + "value": "[variables('acrIpRules')]" } }, "template": { @@ -1249,8 +1328,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "7200761421625936333" + "version": "0.41.2.15936", + "templateHash": "13779468623687321665" } }, "parameters": { @@ -1280,6 +1359,10 @@ }, "enablePrivateNetworking": { "type": "bool" + }, + "allowedIpAddresses": { + "type": "array", + "defaultValue": [] } }, "resources": [ @@ -1293,7 +1376,8 @@ }, "properties": { "adminUserEnabled": true, - "publicNetworkAccess": "Enabled" + "publicNetworkAccess": "Enabled", + "networkRuleSet": "[if(parameters('enablePrivateNetworking'), createObject('defaultAction', 'Deny', 'ipRules', parameters('allowedIpAddresses')), null())]" }, "tags": "[parameters('tags')]" }, @@ -1301,7 +1385,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('acrName'))]", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", "name": "[toLower(format('{0}-diagnostics', parameters('acrName')))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -1329,8 +1413,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -1476,8 +1560,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -1580,8 +1664,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "12409981559885193891" + "version": "0.41.2.15936", + "templateHash": "593540566927357391" } }, "parameters": { @@ -1643,7 +1727,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Search/searchServices/{0}', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.Search/searchServices', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -1671,8 +1755,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -1818,8 +1902,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -1926,8 +2010,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1403194626393613544" + "version": "0.41.2.15936", + "templateHash": "2407421238679839972" } }, "parameters": { @@ -1982,7 +2066,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -2010,8 +2094,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -2157,8 +2241,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -2265,8 +2349,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1065428217831578633" + "version": "0.41.2.15936", + "templateHash": "811367549199956159" } }, "parameters": { @@ -2354,7 +2438,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -2370,7 +2454,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]", + "scope": "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]", "name": "[toLower(format('{0}-blob-diagnostics', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -2399,8 +2483,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -2546,8 +2630,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -2640,11 +2724,26 @@ "configureApplicationPermissions": { "value": "[parameters('configureApplicationPermissions')]" }, + "existingOpenAIEndpoint": { + "value": "[parameters('existingOpenAIEndpoint')]" + }, + "existingOpenAIResourceName": { + "value": "[parameters('existingOpenAIResourceName')]" + }, + "existingOpenAIResourceGroup": { + "value": "[parameters('existingOpenAIResourceGroup')]" + }, + "existingOpenAISubscriptionId": { + "value": "[parameters('existingOpenAISubscriptionId')]" + }, + "existingOpenAIKey": { + "value": "[parameters('existingOpenAIKey')]" + }, "gptModels": { - "value": "[parameters('gptModels')]" + "value": "[variables('resolvedGptModels')]" }, "embeddingModels": { - "value": "[parameters('embeddingModels')]" + "value": "[variables('resolvedEmbeddingModels')]" }, "enablePrivateNetworking": { "value": "[parameters('enablePrivateNetworking')]" @@ -2656,8 +2755,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "17726947983911725769" + "version": "0.41.2.15936", + "templateHash": "5645264128553701562" } }, "parameters": { @@ -2688,6 +2787,26 @@ "configureApplicationPermissions": { "type": "bool" }, + "existingOpenAIEndpoint": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAIResourceName": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAIResourceGroup": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAISubscriptionId": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAIKey": { + "type": "securestring", + "defaultValue": "" + }, "gptModels": { "type": "array" }, @@ -2698,8 +2817,14 @@ "type": "bool" } }, + "variables": { + "useExistingOpenAIEndpoint": "[not(empty(parameters('existingOpenAIEndpoint')))]", + "resolvedOpenAIResourceGroup": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIResourceGroup'), resourceGroup().name)]", + "resolvedOpenAISubscriptionId": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAISubscriptionId'), subscription().subscriptionId)]" + }, "resources": [ { + "condition": "[not(variables('useExistingOpenAIEndpoint'))]", "type": "Microsoft.CognitiveServices/accounts", "apiVersion": "2024-10-01", "name": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]", @@ -2718,10 +2843,10 @@ "tags": "[parameters('tags')]" }, { - "condition": "[parameters('enableDiagLogging')]", + "condition": "[and(parameters('enableDiagLogging'), not(variables('useExistingOpenAIEndpoint')))]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -2749,8 +2874,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -2872,13 +2997,13 @@ { "copy": { "name": "aiModel", - "count": "[length(concat(parameters('gptModels'), parameters('embeddingModels')))]", + "count": "[length(if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray()))]", "mode": "serial", "batchSize": 1 }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('model-{0}-{1}', replace(concat(parameters('gptModels'), parameters('embeddingModels'))[copyIndex()].modelName, '.', '-'), copyIndex())]", + "name": "[format('model-{0}-{1}', replace(if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelName, '.', '-'), copyIndex())]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -2889,16 +3014,16 @@ "value": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]" }, "modelName": { - "value": "[concat(parameters('gptModels'), parameters('embeddingModels'))[copyIndex()].modelName]" + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelName]" }, "modelVersion": { - "value": "[concat(parameters('gptModels'), parameters('embeddingModels'))[copyIndex()].modelVersion]" + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelVersion]" }, "skuName": { - "value": "[concat(parameters('gptModels'), parameters('embeddingModels'))[copyIndex()].skuName]" + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].skuName]" }, "skuCapacity": { - "value": "[concat(parameters('gptModels'), parameters('embeddingModels'))[copyIndex()].skuCapacity]" + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].skuCapacity]" } }, "template": { @@ -2907,8 +3032,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "3668428745262748302" + "version": "0.41.2.15936", + "templateHash": "12746224250029736064" } }, "parameters": { @@ -2953,7 +3078,7 @@ ] }, { - "condition": "[and(equals(parameters('authenticationType'), 'key'), parameters('configureApplicationPermissions'))]", + "condition": "[and(and(equals(parameters('authenticationType'), 'key'), parameters('configureApplicationPermissions')), or(not(variables('useExistingOpenAIEndpoint')), not(empty(parameters('existingOpenAIKey')))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "storeOpenAISecret", @@ -2969,9 +3094,7 @@ "secretName": { "value": "openAi-key" }, - "secretValue": { - "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))), '2024-10-01').key1]" - } + "secretValue": "[if(variables('useExistingOpenAIEndpoint'), createObject('value', parameters('existingOpenAIKey')), createObject('value', listKeys(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))), '2024-10-01').key1))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -2979,8 +3102,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -3020,15 +3143,19 @@ "outputs": { "openAIName": { "type": "string", - "value": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]" + "value": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIResourceName'), toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]" }, "openAIResourceGroup": { "type": "string", - "value": "[resourceGroup().name]" + "value": "[variables('resolvedOpenAIResourceGroup')]" + }, + "openAISubscriptionId": { + "type": "string", + "value": "[variables('resolvedOpenAISubscriptionId')]" }, "openAIEndpoint": { "type": "string", - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))), '2024-10-01').endpoint]" + "value": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIEndpoint'), reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))), '2024-10-01').endpoint)]" } } } @@ -3075,8 +3202,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "16108907418935593505" + "version": "0.41.2.15936", + "templateHash": "9868911322271424816" } }, "parameters": { @@ -3126,7 +3253,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Web/serverfarms/{0}', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.Web/serverfarms', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -3154,8 +3281,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -3338,6 +3465,9 @@ "openAiServiceName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIName.value]" }, + "openAiEndpoint": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIEndpoint.value]" + }, "openAiResourceGroupName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIResourceGroup.value]" }, @@ -3362,7 +3492,7 @@ "enablePrivateNetworking": { "value": "[parameters('enablePrivateNetworking')]" }, - "appServiceSubnetId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.appServiceSubnetId.value), createObject('value', ''))]" + "appServiceSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingAppServiceSubnetId')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.appServiceSubnetId.value)), createObject('value', ''))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -3370,8 +3500,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "5779075922193615045" + "version": "0.41.2.15936", + "templateHash": "15155999637546305218" } }, "parameters": { @@ -3414,6 +3544,9 @@ "openAiServiceName": { "type": "string" }, + "openAiEndpoint": { + "type": "string" + }, "openAiResourceGroupName": { "type": "string" }, @@ -3467,7 +3600,7 @@ "alwaysOn": true, "ftpsState": "Disabled", "healthCheckPath": "/external/healthcheck", - "appSettings": "[flatten(createArray(createArray(createObject('name', 'AZURE_ENDPOINT', 'value', if(equals(parameters('azurePlatform'), 'AzureUSGovernment'), 'usgovernment', 'public')), createObject('name', 'SCM_DO_BUILD_DURING_DEPLOYMENT', 'value', 'false'), createObject('name', 'AZURE_COSMOS_ENDPOINT', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName')), '2023-04-15').documentEndpoint), createObject('name', 'AZURE_COSMOS_AUTHENTICATION_TYPE', 'value', toLower(parameters('authenticationType')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_COSMOS_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/cosmos-db-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'TENANT_ID', 'value', tenant().tenantId), createObject('name', 'CLIENT_ID', 'value', parameters('enterpriseAppClientId')), createObject('name', 'SECRET_KEY', 'value', if(not(empty(parameters('enterpriseAppClientSecret'))), parameters('enterpriseAppClientSecret'), format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri')))), createObject('name', 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri'))), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', format('https://{0}{1}', parameters('acrName'), variables('acrDomain')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_USERNAME', 'value', listCredentials(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2025-04-01').username)), createArray()), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_PASSWORD', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/container-registry-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', 'value', tenant().tenantId), createObject('name', 'AZURE_OPENAI_RESOURCE_NAME', 'value', parameters('openAiServiceName')), createObject('name', 'AZURE_OPENAI_RESOURCE_GROUP_NAME', 'value', parameters('openAiResourceGroupName')), createObject('name', 'AZURE_OPENAI_URL', 'value', reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiServiceName')), '2024-10-01').endpoint), createObject('name', 'AZURE_SEARCH_SERVICE_NAME', 'value', parameters('searchServiceName'))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_SEARCH_API_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/search-service-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', 'value', reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('documentIntelligenceServiceName')), '2025-06-01').endpoint)), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/document-intelligence-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'APPINSIGHTS_INSTRUMENTATIONKEY', 'value', reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').InstrumentationKey), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').ConnectionString), createObject('name', 'APPINSIGHTS_PROFILERFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPLICATIONINSIGHTS_CONFIGURATION_CONTENT', 'value', ''), createObject('name', 'ApplicationInsightsAgent_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'DiagnosticServices_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'InstrumentationEngine_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'SnapshotDebugger_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_BaseExtensions', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_Mode', 'value', 'recommended'), createObject('name', 'XDT_MicrosoftApplicationInsights_PreemptSdk', 'value', 'disabled'))))]" + "appSettings": "[flatten(createArray(createArray(createObject('name', 'AZURE_ENDPOINT', 'value', if(equals(parameters('azurePlatform'), 'AzureUSGovernment'), 'usgovernment', 'public')), createObject('name', 'SCM_DO_BUILD_DURING_DEPLOYMENT', 'value', 'false'), createObject('name', 'AZURE_COSMOS_ENDPOINT', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName')), '2023-04-15').documentEndpoint), createObject('name', 'AZURE_COSMOS_AUTHENTICATION_TYPE', 'value', toLower(parameters('authenticationType')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_COSMOS_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/cosmos-db-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'TENANT_ID', 'value', tenant().tenantId), createObject('name', 'CLIENT_ID', 'value', parameters('enterpriseAppClientId')), createObject('name', 'SECRET_KEY', 'value', if(not(empty(parameters('enterpriseAppClientSecret'))), parameters('enterpriseAppClientSecret'), format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri')))), createObject('name', 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri'))), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', format('https://{0}{1}', parameters('acrName'), variables('acrDomain')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_USERNAME', 'value', listCredentials(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2025-04-01').username)), createArray()), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_PASSWORD', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/container-registry-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', 'value', tenant().tenantId), createObject('name', 'AZURE_OPENAI_RESOURCE_NAME', 'value', parameters('openAiServiceName')), createObject('name', 'AZURE_OPENAI_RESOURCE_GROUP_NAME', 'value', parameters('openAiResourceGroupName')), createObject('name', 'AZURE_OPENAI_URL', 'value', parameters('openAiEndpoint')), createObject('name', 'AZURE_SEARCH_SERVICE_NAME', 'value', parameters('searchServiceName'))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_SEARCH_API_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/search-service-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', 'value', reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('documentIntelligenceServiceName')), '2025-06-01').endpoint)), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/document-intelligence-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'APPINSIGHTS_INSTRUMENTATIONKEY', 'value', reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').InstrumentationKey), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').ConnectionString), createObject('name', 'APPINSIGHTS_PROFILERFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPLICATIONINSIGHTS_CONFIGURATION_CONTENT', 'value', ''), createObject('name', 'ApplicationInsightsAgent_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'DiagnosticServices_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'InstrumentationEngine_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'SnapshotDebugger_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_BaseExtensions', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_Mode', 'value', 'recommended'), createObject('name', 'XDT_MicrosoftApplicationInsights_PreemptSdk', 'value', 'disabled'))))]" }, "clientAffinityEnabled": false, "httpsOnly": true @@ -3498,7 +3631,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Web/sites/{0}', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -3590,8 +3723,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -3790,8 +3923,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "15792786587982505631" + "version": "0.41.2.15936", + "templateHash": "14712334793390431300" } }, "parameters": { @@ -3846,7 +3979,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -3874,8 +4007,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -4021,8 +4154,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -4123,8 +4256,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "10855483054816904335" + "version": "0.41.2.15936", + "templateHash": "8651721759068501094" } }, "parameters": { @@ -4180,7 +4313,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Cache/redis/{0}', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.Cache/redis', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -4208,8 +4341,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -4355,8 +4488,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -4460,8 +4593,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "12749834719068646180" + "version": "0.41.2.15936", + "templateHash": "2910821044518517112" } }, "parameters": { @@ -4519,7 +4652,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -4547,8 +4680,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -4694,8 +4827,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "18077870757859157550" + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" } }, "parameters": { @@ -4800,8 +4933,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "5008986833957108258" + "version": "0.41.2.15936", + "templateHash": "4707936164725245117" } }, "parameters": { @@ -4857,7 +4990,7 @@ "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.VideoIndexer/accounts/{0}', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment'))))]", + "scope": "[resourceId('Microsoft.VideoIndexer/accounts', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment'))))]", "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", @@ -4884,8 +5017,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14992132232820252472" + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" } }, "variables": { @@ -5057,6 +5190,12 @@ "openAIName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIName.value]" }, + "openAIResourceGroupName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIResourceGroup.value]" + }, + "openAISubscriptionId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAISubscriptionId.value]" + }, "docIntelName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'docIntel'), '2025-04-01').outputs.documentIntelligenceServiceName.value]" }, @@ -5077,8 +5216,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "17165122895470513967" + "version": "0.41.2.15936", + "templateHash": "4113398169754187856" } }, "parameters": { @@ -5103,6 +5242,12 @@ "openAIName": { "type": "string" }, + "openAIResourceGroupName": { + "type": "string" + }, + "openAISubscriptionId": { + "type": "string" + }, "docIntelName": { "type": "string" }, @@ -5125,11 +5270,14 @@ "type": "string" } }, + "variables": { + "useExternalOpenAIResource": "[and(and(not(equals(parameters('openAIName'), '')), not(empty(parameters('openAIResourceGroupName')))), not(empty(parameters('openAISubscriptionId'))))]" + }, "resources": [ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('keyVaultName'))]", + "scope": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]", "name": "[guid(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'kv-secrets-user')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]", @@ -5141,7 +5289,7 @@ "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('cosmosDBName'))]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]", "name": "[guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'cosmos-contributor')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", @@ -5163,7 +5311,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('acrName'))]", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'acr-pull-role')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]", @@ -5172,10 +5320,10 @@ } }, { - "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "condition": "[and(and(equals(parameters('authenticationType'), 'managed_identity'), not(equals(parameters('openAIName'), ''))), not(variables('useExternalOpenAIResource')))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('openAIName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'openai-user')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", @@ -5184,10 +5332,10 @@ } }, { - "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "condition": "[and(and(equals(parameters('authenticationType'), 'managed_identity'), not(equals(parameters('openAIName'), ''))), not(variables('useExternalOpenAIResource')))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('openAIName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'enterpriseApp-CognitiveServicesOpenAIUserRole')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", @@ -5199,7 +5347,7 @@ "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('docIntelName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'doc-intel-user')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", @@ -5211,7 +5359,7 @@ "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'storage-blob-data-contributor')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", @@ -5223,7 +5371,7 @@ "condition": "[and(not(equals(parameters('speechServiceName'), '')), equals(parameters('authenticationType'), 'managed_identity'))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('speechServiceName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('speechServiceName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'speech-service-user')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", @@ -5235,7 +5383,7 @@ "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('searchServiceName'))]", + "scope": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]", "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('searchServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'search-index-data-contributor')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", @@ -5247,7 +5395,7 @@ "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('searchServiceName'))]", + "scope": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]", "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('searchServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'search-service-contributor')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", @@ -5259,7 +5407,7 @@ "condition": "[and(not(equals(parameters('contentSafetyName'), '')), equals(parameters('authenticationType'), 'managed_identity'))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('contentSafetyName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('contentSafetyName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'content-safety-user')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", @@ -5271,7 +5419,7 @@ "condition": "[not(equals(parameters('videoIndexerName'), ''))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-storage-blob-data-contributor')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", @@ -5280,10 +5428,10 @@ } }, { - "condition": "[not(equals(parameters('videoIndexerName'), ''))]", + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(variables('useExternalOpenAIResource')))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('openAIName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-cog-services-contributor')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", @@ -5292,10 +5440,10 @@ } }, { - "condition": "[not(equals(parameters('videoIndexerName'), ''))]", + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(variables('useExternalOpenAIResource')))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('openAIName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-cog-services-user')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", @@ -5307,13 +5455,128 @@ "condition": "[not(equals(parameters('redisCacheName'), ''))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Cache/redis/{0}', parameters('redisCacheName'))]", + "scope": "[resourceId('Microsoft.Cache/redis', parameters('redisCacheName'))]", "name": "[guid(resourceId('Microsoft.Cache/redis', parameters('redisCacheName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'redis-cache-contributor')]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e0f68234-74aa-48ed-b826-c38b57376e17')]", "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", "principalType": "ServicePrincipal" } + }, + { + "condition": "[variables('useExternalOpenAIResource')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAIExternalPermissions", + "subscriptionId": "[parameters('openAISubscriptionId')]", + "resourceGroup": "[parameters('openAIResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "openAIName": { + "value": "[parameters('openAIName')]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "webAppPrincipalId": { + "value": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]" + }, + "enterpriseAppServicePrincipalId": { + "value": "[parameters('enterpriseAppServicePrincipalId')]" + }, + "videoIndexerPrincipalId": "[if(not(equals(parameters('videoIndexerName'), '')), createObject('value', reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId), createObject('value', ''))]", + "videoIndexerName": { + "value": "[parameters('videoIndexerName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14586417486337251686" + } + }, + "parameters": { + "openAIName": { + "type": "string" + }, + "authenticationType": { + "type": "string" + }, + "webAppPrincipalId": { + "type": "string" + }, + "enterpriseAppServicePrincipalId": { + "type": "string" + }, + "videoIndexerPrincipalId": { + "type": "string", + "defaultValue": "" + }, + "videoIndexerName": { + "type": "string", + "defaultValue": "" + } + }, + "resources": [ + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('webAppPrincipalId'), 'openai-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('webAppPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('enterpriseAppServicePrincipalId'), 'enterpriseApp-CognitiveServicesOpenAIUserRole')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('enterpriseAppServicePrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(empty(parameters('videoIndexerPrincipalId'))))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('videoIndexerPrincipalId'), 'video-indexer-cog-services-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "principalId": "[parameters('videoIndexerPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(empty(parameters('videoIndexerPrincipalId'))))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('videoIndexerPrincipalId'), 'video-indexer-cog-services-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[parameters('videoIndexerPrincipalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + } } ] } @@ -5346,11 +5609,10 @@ }, "mode": "Incremental", "parameters": { - "virtualNetworkId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.vNetId.value]" - }, - "privateEndpointSubnetId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.privateNetworkSubnetId.value]" + "virtualNetworkId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', variables('resolvedExistingVirtualNetworkId')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.vNetId.value)), createObject('value', ''))]", + "privateEndpointSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingPrivateEndpointSubnetId')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.privateNetworkSubnetId.value)), createObject('value', ''))]", + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneConfigs')]" }, "location": { "value": "[parameters('location')]" @@ -5385,6 +5647,12 @@ "openAIName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIName.value]" }, + "openAIResourceGroupName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIResourceGroup.value]" + }, + "openAISubscriptionId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAISubscriptionId.value]" + }, "webAppName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'appService'), '2025-04-01').outputs.name.value]" }, @@ -5398,8 +5666,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "3584007603660948740" + "version": "0.41.2.15936", + "templateHash": "5473171678692396413" } }, "parameters": { @@ -5409,6 +5677,10 @@ "privateEndpointSubnetId": { "type": "string" }, + "privateDnsZoneConfigs": { + "type": "object", + "defaultValue": {} + }, "location": { "type": "string" }, @@ -5442,6 +5714,12 @@ "openAIName": { "type": "string" }, + "openAIResourceGroupName": { + "type": "string" + }, + "openAISubscriptionId": { + "type": "string" + }, "webAppName": { "type": "string" }, @@ -5478,6 +5756,7 @@ "webSites": "privatelink.azurewebsites.us" } }, + "useExternalOpenAIResource": "[and(and(not(equals(parameters('openAIName'), '')), not(empty(parameters('openAIResourceGroupName')))), not(empty(parameters('openAISubscriptionId'))))]", "cloudName": "[toLower(environment().name)]", "privateDnsZoneData": "[variables('$fxv#0')]", "aiSearchDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].aisearch]", @@ -5487,7 +5766,19 @@ "cosmosDbDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].cosmosDb]", "keyVaultDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].keyVault]", "openAiDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].openAi]", - "webSitesDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].webSites]" + "webSitesDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].webSites]", + "defaultPrivateDnsZoneConfig": { + "zoneResourceId": "", + "createVNetLink": true + }, + "keyVaultPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'keyVault'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').keyVault), variables('defaultPrivateDnsZoneConfig'))]", + "cosmosDbPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'cosmosDb'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').cosmosDb), variables('defaultPrivateDnsZoneConfig'))]", + "containerRegistryPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'containerRegistry'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').containerRegistry), variables('defaultPrivateDnsZoneConfig'))]", + "aiSearchPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'aiSearch'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').aiSearch), variables('defaultPrivateDnsZoneConfig'))]", + "blobStoragePrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'blobStorage'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').blobStorage), variables('defaultPrivateDnsZoneConfig'))]", + "cognitiveServicesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'cognitiveServices'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').cognitiveServices), variables('defaultPrivateDnsZoneConfig'))]", + "openAiPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'openAi'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').openAi), variables('defaultPrivateDnsZoneConfig'))]", + "webSitesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'webSites'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').webSites), variables('defaultPrivateDnsZoneConfig'))]" }, "resources": [ { @@ -5517,6 +5808,12 @@ }, "tags": { "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('keyVaultPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('keyVaultPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -5525,8 +5822,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "6963784194716310503" + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" } }, "parameters": { @@ -5547,10 +5844,26 @@ }, "tags": { "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true } }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, "resources": [ { + "condition": "[not(variables('useExistingZone'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2020-06-01", "name": "[parameters('zoneName')]", @@ -5558,9 +5871,10 @@ "tags": "[parameters('tags')]" }, { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name'))))]", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", "location": "global", "properties": { "registrationEnabled": false, @@ -5571,16 +5885,77 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } } ], "outputs": { "privateDnsZoneId": { "type": "string", - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" }, "privateDnsZoneName": { "type": "string", - "value": "[parameters('zoneName')]" + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" } } } @@ -5634,8 +6009,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -5748,6 +6123,12 @@ }, "tags": { "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('cosmosDbPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('cosmosDbPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -5756,8 +6137,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "6963784194716310503" + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" } }, "parameters": { @@ -5778,10 +6159,26 @@ }, "tags": { "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true } }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, "resources": [ { + "condition": "[not(variables('useExistingZone'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2020-06-01", "name": "[parameters('zoneName')]", @@ -5789,9 +6186,10 @@ "tags": "[parameters('tags')]" }, { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name'))))]", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", "location": "global", "properties": { "registrationEnabled": false, @@ -5802,16 +6200,77 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } } ], "outputs": { "privateDnsZoneId": { "type": "string", - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" }, "privateDnsZoneName": { "type": "string", - "value": "[parameters('zoneName')]" + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" } } } @@ -5865,8 +6324,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -5979,6 +6438,12 @@ }, "tags": { "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('containerRegistryPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('containerRegistryPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -5987,8 +6452,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "6963784194716310503" + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" } }, "parameters": { @@ -6009,10 +6474,26 @@ }, "tags": { "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true } }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, "resources": [ { + "condition": "[not(variables('useExistingZone'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2020-06-01", "name": "[parameters('zoneName')]", @@ -6020,9 +6501,10 @@ "tags": "[parameters('tags')]" }, { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name'))))]", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", "location": "global", "properties": { "registrationEnabled": false, @@ -6033,16 +6515,77 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } } ], "outputs": { "privateDnsZoneId": { "type": "string", - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" }, "privateDnsZoneName": { "type": "string", - "value": "[parameters('zoneName')]" + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" } } } @@ -6096,8 +6639,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -6210,6 +6753,12 @@ }, "tags": { "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('aiSearchPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('aiSearchPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -6218,8 +6767,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "6963784194716310503" + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" } }, "parameters": { @@ -6240,10 +6789,26 @@ }, "tags": { "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true } }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, "resources": [ { + "condition": "[not(variables('useExistingZone'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2020-06-01", "name": "[parameters('zoneName')]", @@ -6251,9 +6816,10 @@ "tags": "[parameters('tags')]" }, { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name'))))]", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", "location": "global", "properties": { "registrationEnabled": false, @@ -6264,16 +6830,77 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } } ], "outputs": { "privateDnsZoneId": { "type": "string", - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" }, "privateDnsZoneName": { "type": "string", - "value": "[parameters('zoneName')]" + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" } } } @@ -6327,8 +6954,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -6441,6 +7068,12 @@ }, "tags": { "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -6449,8 +7082,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "6963784194716310503" + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" } }, "parameters": { @@ -6471,10 +7104,26 @@ }, "tags": { "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true } }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, "resources": [ { + "condition": "[not(variables('useExistingZone'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2020-06-01", "name": "[parameters('zoneName')]", @@ -6482,9 +7131,10 @@ "tags": "[parameters('tags')]" }, { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name'))))]", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", "location": "global", "properties": { "registrationEnabled": false, @@ -6495,16 +7145,77 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } } ], "outputs": { "privateDnsZoneId": { "type": "string", - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" }, "privateDnsZoneName": { "type": "string", - "value": "[parameters('zoneName')]" + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" } } } @@ -6558,8 +7269,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -6672,6 +7383,12 @@ }, "tags": { "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('blobStoragePrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('blobStoragePrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -6680,8 +7397,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "6963784194716310503" + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" } }, "parameters": { @@ -6702,10 +7419,26 @@ }, "tags": { "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true } }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, "resources": [ { + "condition": "[not(variables('useExistingZone'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2020-06-01", "name": "[parameters('zoneName')]", @@ -6713,9 +7446,10 @@ "tags": "[parameters('tags')]" }, { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name'))))]", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", "location": "global", "properties": { "registrationEnabled": false, @@ -6726,16 +7460,77 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } } ], "outputs": { "privateDnsZoneId": { "type": "string", - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" }, "privateDnsZoneName": { "type": "string", - "value": "[parameters('zoneName')]" + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" } } } @@ -6789,8 +7584,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -6877,6 +7672,7 @@ ] }, { + "condition": "[not(equals(parameters('openAIName'), ''))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "openAiDNSZone", @@ -6903,6 +7699,12 @@ }, "tags": { "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('openAiPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('openAiPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -6911,8 +7713,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "6963784194716310503" + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" } }, "parameters": { @@ -6933,10 +7735,26 @@ }, "tags": { "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true } }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, "resources": [ { + "condition": "[not(variables('useExistingZone'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2020-06-01", "name": "[parameters('zoneName')]", @@ -6944,9 +7762,10 @@ "tags": "[parameters('tags')]" }, { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name'))))]", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", "location": "global", "properties": { "registrationEnabled": false, @@ -6957,22 +7776,84 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } } ], "outputs": { "privateDnsZoneId": { "type": "string", - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" }, "privateDnsZoneName": { "type": "string", - "value": "[parameters('zoneName')]" + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" } } } } }, { + "condition": "[not(equals(parameters('openAIName'), ''))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "openAiPE", @@ -6994,9 +7875,7 @@ "environment": { "value": "[parameters('environment')]" }, - "serviceResourceID": { - "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]" - }, + "serviceResourceID": "[if(variables('useExternalOpenAIResource'), createObject('value', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('openAISubscriptionId'), parameters('openAIResourceGroupName')), 'Microsoft.CognitiveServices/accounts', parameters('openAIName'))), createObject('value', resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))))]", "subnetId": { "value": "[parameters('privateEndpointSubnetId')]" }, @@ -7020,8 +7899,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -7134,6 +8013,12 @@ }, "tags": { "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('webSitesPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('webSitesPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -7142,8 +8027,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "6963784194716310503" + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" } }, "parameters": { @@ -7164,10 +8049,26 @@ }, "tags": { "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true } }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, "resources": [ { + "condition": "[not(variables('useExistingZone'))]", "type": "Microsoft.Network/privateDnsZones", "apiVersion": "2020-06-01", "name": "[parameters('zoneName')]", @@ -7175,9 +8076,10 @@ "tags": "[parameters('tags')]" }, { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name'))))]", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", "location": "global", "properties": { "registrationEnabled": false, @@ -7188,16 +8090,77 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } } ], "outputs": { "privateDnsZoneId": { "type": "string", - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" }, "privateDnsZoneName": { "type": "string", - "value": "[parameters('zoneName')]" + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" } } } @@ -7251,8 +8214,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -7387,8 +8350,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -7523,8 +8486,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -7659,8 +8622,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1838240609393686445" + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" } }, "parameters": { @@ -7817,7 +8780,7 @@ }, "var_openAIGPTModels": { "type": "array", - "value": "[parameters('gptModels')]" + "value": "[variables('resolvedGptModels')]" }, "var_openAIResourceGroup": { "type": "string", @@ -7825,7 +8788,11 @@ }, "var_openAIEmbeddingModels": { "type": "array", - "value": "[parameters('embeddingModels')]" + "value": "[variables('resolvedEmbeddingModels')]" + }, + "var_openAISubscriptionId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAISubscriptionId.value]" }, "var_redisCacheHostName": { "type": "string", @@ -7865,7 +8832,7 @@ }, "var_imageTag": { "type": "string", - "value": "[split(parameters('imageName'), ':')[1]]" + "value": "[if(contains(parameters('imageName'), ':'), split(parameters('imageName'), ':')[1], 'latest')]" }, "var_webService": { "type": "string", diff --git a/deployers/bicep/main.parameters.json b/deployers/bicep/main.parameters.json index dbd83d22d..cd4ccc517 100644 --- a/deployers/bicep/main.parameters.json +++ b/deployers/bicep/main.parameters.json @@ -47,6 +47,36 @@ "enablePrivateNetworking": { "value": "${ENABLE_PRIVATE_NETWORKING}" }, + "existingVirtualNetworkId": { + "value": "${EXISTING_VNET_RESOURCE_ID}" + }, + "existingAppServiceSubnetId": { + "value": "${EXISTING_APP_SERVICE_SUBNET_RESOURCE_ID}" + }, + "existingPrivateEndpointSubnetId": { + "value": "${EXISTING_PRIVATE_ENDPOINT_SUBNET_RESOURCE_ID}" + }, + "privateDnsZoneConfigs": { + "value": {} + }, + "existingOpenAIEndpoint": { + "value": "${EXISTING_AZURE_OPENAI_ENDPOINT}" + }, + "existingOpenAIResourceName": { + "value": "${EXISTING_AZURE_OPENAI_RESOURCE_NAME}" + }, + "existingOpenAIResourceGroup": { + "value": "${EXISTING_AZURE_OPENAI_RESOURCE_GROUP}" + }, + "existingOpenAISubscriptionId": { + "value": "${EXISTING_AZURE_OPENAI_SUBSCRIPTION_ID}" + }, + "existingOpenAIKey": { + "value": "${EXISTING_AZURE_OPENAI_KEY}" + }, + "openAIDeploymentType": { + "value": "${OPENAI_DEPLOYMENT_TYPE}" + }, "gptModels": { "value": "${GPT_MODELS}" }, diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index 5d9aa471b..0b0965a6a 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -15,6 +15,7 @@ param azurePlatform string param cosmosDbName string param searchServiceName string param openAiServiceName string +param openAiEndpoint string param openAiResourceGroupName string param documentIntelligenceServiceName string param appInsightsName string @@ -44,10 +45,6 @@ resource searchService 'Microsoft.Search/searchServices@2025-05-01' existing = { name: searchServiceName } -resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { - name: openAiServiceName -} - resource documentIntelligence 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = { name: documentIntelligenceServiceName } @@ -117,9 +114,9 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { : []) { name: 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', value: tenant().tenantId } - { name: 'AZURE_OPENAI_RESOURCE_NAME', value: openAiService.name } + { name: 'AZURE_OPENAI_RESOURCE_NAME', value: openAiServiceName } { name: 'AZURE_OPENAI_RESOURCE_GROUP_NAME', value: openAiResourceGroupName } - { name: 'AZURE_OPENAI_URL', value: openAiService.properties.endpoint } + { name: 'AZURE_OPENAI_URL', value: openAiEndpoint } { name: 'AZURE_SEARCH_SERVICE_NAME', value: searchService.name } // Only add this setting if authenticationType is 'key' ...(authenticationType == 'key' diff --git a/deployers/bicep/modules/azureContainerRegistry.bicep b/deployers/bicep/modules/azureContainerRegistry.bicep index d4ed9b6d9..2525ffad7 100644 --- a/deployers/bicep/modules/azureContainerRegistry.bicep +++ b/deployers/bicep/modules/azureContainerRegistry.bicep @@ -30,8 +30,9 @@ resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { properties: { adminUserEnabled: true publicNetworkAccess: 'Enabled' // configuration is set in post provision step in azure.yaml with post deployment script + networkRuleBypassOptions: enablePrivateNetworking ? 'AzureServices' : 'None' networkRuleSet: enablePrivateNetworking ? { - defaultAction: 'Deny' + defaultAction: 'Allow' ipRules: allowedIpAddresses } : null } diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index a5d2e1b62..bf34428ec 100644 --- a/deployers/bicep/modules/openAI.bicep +++ b/deployers/bicep/modules/openAI.bicep @@ -11,19 +11,30 @@ param logAnalyticsId string param keyVault string param authenticationType string param configureApplicationPermissions bool +param existingOpenAIEndpoint string = '' +param existingOpenAIResourceName string = '' +param existingOpenAIResourceGroup string = '' +param existingOpenAISubscriptionId string = '' + +@secure() +param existingOpenAIKey string = '' param gptModels array param embeddingModels array param enablePrivateNetworking bool +var useExistingOpenAIEndpoint = !empty(existingOpenAIEndpoint) +var resolvedOpenAIResourceGroup = useExistingOpenAIEndpoint ? existingOpenAIResourceGroup : resourceGroup().name +var resolvedOpenAISubscriptionId = useExistingOpenAIEndpoint ? existingOpenAISubscriptionId : subscription().subscriptionId + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' } // deploy new Azure OpenAI Resource -resource openAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { +resource openAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = if (!useExistingOpenAIEndpoint) { name: toLower('${appName}-${environment}-openai') location: location kind: 'OpenAI' @@ -41,7 +52,7 @@ resource openAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { } // configure diagnostic settings for OpenAI Resource if required -resource openAIDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { +resource openAIDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging && !useExistingOpenAIEndpoint) { name: toLower('${openAI.name}-diagnostics') scope: openAI properties: { @@ -56,7 +67,7 @@ resource openAIDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre // deploy AI models defined in the input arrays @batchSize(1) module aiModel 'aiModel.bicep' = [ - for (model, i) in concat(gptModels, embeddingModels): { + for (model, i) in !useExistingOpenAIEndpoint ? concat(gptModels, embeddingModels) : []: { name: 'model-${replace(model.modelName, '.', '-')}-${i}' params: { parent: openAI.name @@ -71,15 +82,18 @@ module aiModel 'aiModel.bicep' = [ //========================================================= // store openAI keys in key vault if using key authentication and configure app permissions = true //========================================================= -module openAISecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { +module openAISecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions && (!useExistingOpenAIEndpoint || !empty(existingOpenAIKey))) { name: 'storeOpenAISecret' params: { keyVaultName: keyVault secretName: 'openAi-key' - secretValue: openAI.listKeys().key1 + #disable-next-line BCP422 // guarded by useExistingOpenAIEndpoint condition above + secretValue: useExistingOpenAIEndpoint ? existingOpenAIKey : openAI.listKeys().key1 } } -output openAIName string = openAI.name -output openAIResourceGroup string = resourceGroup().name -output openAIEndpoint string = openAI.properties.endpoint +output openAIName string = useExistingOpenAIEndpoint ? existingOpenAIResourceName : openAI.name +output openAIResourceGroup string = resolvedOpenAIResourceGroup +output openAISubscriptionId string = resolvedOpenAISubscriptionId +#disable-next-line BCP318 // guarded by useExistingOpenAIEndpoint condition above +output openAIEndpoint string = useExistingOpenAIEndpoint ? existingOpenAIEndpoint : openAI.properties.endpoint diff --git a/deployers/bicep/modules/privateDNS.bicep b/deployers/bicep/modules/privateDNS.bicep index 9306b6cc6..4c9b3b2e0 100644 --- a/deployers/bicep/modules/privateDNS.bicep +++ b/deployers/bicep/modules/privateDNS.bicep @@ -6,15 +6,28 @@ param environment string param name string param vNetId string param tags object +param existingZoneResourceId string = '' +param createVNetLink bool = true -resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { +var useExistingZone = !empty(existingZoneResourceId) +var linkName = toLower('${appName}-${environment}-${name}-pe-dnszonelink') +var existingZoneSubscriptionId = useExistingZone ? split(existingZoneResourceId, '/')[2] : '' +var existingZoneResourceGroupName = useExistingZone ? split(existingZoneResourceId, '/')[4] : '' +var existingZoneName = useExistingZone ? split(existingZoneResourceId, '/')[8] : '' + +resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (!useExistingZone) { name: zoneName location: 'global' tags: tags } -resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { - name: toLower('${appName}-${environment}-${name}-pe-dnszonelink') +resource existingPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (useExistingZone) { + name: existingZoneName + scope: resourceGroup(existingZoneSubscriptionId, existingZoneResourceGroupName) +} + +resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (!useExistingZone && createVNetLink) { + name: linkName parent: privateDnsZone location: 'global' properties: { @@ -25,5 +38,15 @@ resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLin } } -output privateDnsZoneId string = privateDnsZone.id -output privateDnsZoneName string = privateDnsZone.name +module existingPrivateDnsZoneLink 'privateDNSLink.bicep' = if (useExistingZone && createVNetLink) { + name: 'existing-${name}-privateDnsZoneLink' + scope: resourceGroup(existingZoneSubscriptionId, existingZoneResourceGroupName) + params: { + zoneName: existingZoneName + linkName: linkName + vNetId: vNetId + } +} + +output privateDnsZoneId string = useExistingZone ? existingZoneResourceId : privateDnsZone.id +output privateDnsZoneName string = useExistingZone ? existingPrivateDnsZone.name : privateDnsZone.name diff --git a/deployers/bicep/modules/privateDNSLink.bicep b/deployers/bicep/modules/privateDNSLink.bicep new file mode 100644 index 000000000..e19ef1585 --- /dev/null +++ b/deployers/bicep/modules/privateDNSLink.bicep @@ -0,0 +1,21 @@ +targetScope = 'resourceGroup' + +param zoneName string +param linkName string +param vNetId string + +resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = { + name: zoneName +} + +resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + name: linkName + parent: privateDnsZone + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: vNetId + } + } +} diff --git a/deployers/bicep/modules/privateNetworking.bicep b/deployers/bicep/modules/privateNetworking.bicep index abf5ac39e..34c4dce97 100644 --- a/deployers/bicep/modules/privateNetworking.bicep +++ b/deployers/bicep/modules/privateNetworking.bicep @@ -2,6 +2,7 @@ targetScope = 'resourceGroup' param virtualNetworkId string param privateEndpointSubnetId string +param privateDnsZoneConfigs object = {} param location string param appName string @@ -15,6 +16,8 @@ param searchServiceName string param docIntelName string param storageAccountName string param openAIName string +param openAIResourceGroupName string +param openAISubscriptionId string param webAppName string @@ -23,6 +26,8 @@ param contentSafetyName string param speechServiceName string param videoIndexerName string +var useExternalOpenAIResource = openAIName != '' && !empty(openAIResourceGroupName) && !empty(openAISubscriptionId) + //========================================================= // privateDNSZoneNames var cloudName = toLower(az.environment().name) @@ -36,6 +41,18 @@ var cosmosDbDnsZoneName = privateDnsZoneData[cloudName].cosmosDb var keyVaultDnsZoneName = privateDnsZoneData[cloudName].keyVault var openAiDnsZoneName = privateDnsZoneData[cloudName].openAi var webSitesDnsZoneName = privateDnsZoneData[cloudName].webSites +var defaultPrivateDnsZoneConfig = { + zoneResourceId: '' + createVNetLink: true +} +var keyVaultPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'keyVault') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.keyVault) : defaultPrivateDnsZoneConfig +var cosmosDbPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'cosmosDb') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.cosmosDb) : defaultPrivateDnsZoneConfig +var containerRegistryPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'containerRegistry') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.containerRegistry) : defaultPrivateDnsZoneConfig +var aiSearchPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'aiSearch') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.aiSearch) : defaultPrivateDnsZoneConfig +var blobStoragePrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'blobStorage') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.blobStorage) : defaultPrivateDnsZoneConfig +var cognitiveServicesPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'cognitiveServices') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.cognitiveServices) : defaultPrivateDnsZoneConfig +var openAiPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'openAi') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.openAi) : defaultPrivateDnsZoneConfig +var webSitesPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'webSites') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.webSites) : defaultPrivateDnsZoneConfig //========================================================= // key vault @@ -53,6 +70,8 @@ module keyVaultDNSZone 'privateDNS.bicep' = { name: 'kv' vNetId: virtualNetworkId tags: tags + existingZoneResourceId: keyVaultPrivateDnsZoneConfig.zoneResourceId + createVNetLink: keyVaultPrivateDnsZoneConfig.createVNetLink } } @@ -60,7 +79,6 @@ module keyVaultPE 'privateEndpoint.bicep' = { name: 'keyVaultPE' dependsOn: [ kv - keyVaultDNSZone ] params: { name: 'kv' @@ -94,6 +112,8 @@ module cosmosDbDNSZone 'privateDNS.bicep' = { name: 'cosmosDb' vNetId: virtualNetworkId tags: tags + existingZoneResourceId: cosmosDbPrivateDnsZoneConfig.zoneResourceId + createVNetLink: cosmosDbPrivateDnsZoneConfig.createVNetLink } } @@ -101,7 +121,6 @@ module cosmosDbPE 'privateEndpoint.bicep' = { name: 'cosmosDbPE' dependsOn: [ cosmosDb - cosmosDbDNSZone ] params: { name: 'cosmosDb' @@ -135,6 +154,8 @@ module acrDNSZone 'privateDNS.bicep' = { name: 'acr' vNetId: virtualNetworkId tags: tags + existingZoneResourceId: containerRegistryPrivateDnsZoneConfig.zoneResourceId + createVNetLink: containerRegistryPrivateDnsZoneConfig.createVNetLink } } @@ -142,7 +163,6 @@ module acrPE 'privateEndpoint.bicep' = { name: 'acrPE' dependsOn: [ acr - acrDNSZone ] params: { name: 'acr' @@ -176,6 +196,8 @@ module searchServiceDNSZone 'privateDNS.bicep' = { name: 'searchService' vNetId: virtualNetworkId tags: tags + existingZoneResourceId: aiSearchPrivateDnsZoneConfig.zoneResourceId + createVNetLink: aiSearchPrivateDnsZoneConfig.createVNetLink } } @@ -183,7 +205,6 @@ module searchServicePE 'privateEndpoint.bicep' = { name: 'searchServicePE' dependsOn: [ searchService - searchServiceDNSZone ] params: { name: 'searchService' @@ -217,6 +238,8 @@ module docIntelDNSZone 'privateDNS.bicep' = { name: 'docIntelService' vNetId: virtualNetworkId tags: tags + existingZoneResourceId: cognitiveServicesPrivateDnsZoneConfig.zoneResourceId + createVNetLink: cognitiveServicesPrivateDnsZoneConfig.createVNetLink } } @@ -224,7 +247,6 @@ module docIntelPE 'privateEndpoint.bicep' = { name: 'docIntelPE' dependsOn: [ docIntelService - docIntelDNSZone ] params: { name: 'docIntelService' @@ -258,6 +280,8 @@ module storageAccountDNSZone 'privateDNS.bicep' = { name: 'storage' vNetId: virtualNetworkId tags: tags + existingZoneResourceId: blobStoragePrivateDnsZoneConfig.zoneResourceId + createVNetLink: blobStoragePrivateDnsZoneConfig.createVNetLink } } @@ -265,7 +289,6 @@ module storageAccountPE 'privateEndpoint.bicep' = { name: 'storageAccountPE' dependsOn: [ storageAccount - storageAccountDNSZone ] params: { name: 'storageAccount' @@ -284,11 +307,16 @@ module storageAccountPE 'privateEndpoint.bicep' = { } } //========================================================= -resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { +resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (openAIName != '' && !useExternalOpenAIResource) { + name: openAIName +} + +resource openAiServiceExternal 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (useExternalOpenAIResource) { name: openAIName + scope: resourceGroup(openAISubscriptionId, openAIResourceGroupName) } -module openAiDNSZone 'privateDNS.bicep' = { +module openAiDNSZone 'privateDNS.bicep' = if (openAIName != '') { name: 'openAiDNSZone' params: { zoneName: openAiDnsZoneName @@ -297,26 +325,27 @@ module openAiDNSZone 'privateDNS.bicep' = { name: 'openAiService' vNetId: virtualNetworkId tags: tags + existingZoneResourceId: openAiPrivateDnsZoneConfig.zoneResourceId + createVNetLink: openAiPrivateDnsZoneConfig.createVNetLink } } -module openAiPE 'privateEndpoint.bicep' = { +module openAiPE 'privateEndpoint.bicep' = if (openAIName != '') { name: 'openAiPE' dependsOn: [ - openAiService - openAiDNSZone ] params: { name: 'openAiService' location: location appName: appName environment: environment - serviceResourceID: openAiService.id + serviceResourceID: useExternalOpenAIResource ? openAiServiceExternal.id : openAiService.id subnetId: privateEndpointSubnetId groupIDs: [ 'account' ] privateDnsZoneIds: [ + #disable-next-line BCP318 // module is deployed under the same condition as this private endpoint openAiDNSZone.outputs.privateDnsZoneId ] tags: tags @@ -338,6 +367,8 @@ module webAppDNSZone 'privateDNS.bicep' = { name: 'webApp' vNetId: virtualNetworkId tags: tags + existingZoneResourceId: webSitesPrivateDnsZoneConfig.zoneResourceId + createVNetLink: webSitesPrivateDnsZoneConfig.createVNetLink } } @@ -345,7 +376,6 @@ module webAppPE 'privateEndpoint.bicep' = { name: 'webAppPE' dependsOn: [ webApp - webAppDNSZone ] params: { name: 'webApp' diff --git a/deployers/bicep/modules/setPermissions-openAIExternal.bicep b/deployers/bicep/modules/setPermissions-openAIExternal.bicep new file mode 100644 index 000000000..96d85d9d9 --- /dev/null +++ b/deployers/bicep/modules/setPermissions-openAIExternal.bicep @@ -0,0 +1,64 @@ +targetScope = 'resourceGroup' + +param openAIName string +param authenticationType string +param webAppPrincipalId string +param enterpriseAppServicePrincipalId string +param videoIndexerPrincipalId string = '' +param videoIndexerName string = '' + +resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { + name: openAIName +} + +resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { + scope: openAiService + name: guid(openAiService.id, webAppPrincipalId, 'openai-user') + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + ) + principalId: webAppPrincipalId + principalType: 'ServicePrincipal' + } +} + +resource openAIenterpriseAppUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { + scope: openAiService + name: guid(openAiService.id, enterpriseAppServicePrincipalId, 'enterpriseApp-CognitiveServicesOpenAIUserRole') + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + ) + principalId: enterpriseAppServicePrincipalId + principalType: 'ServicePrincipal' + } +} + +resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !empty(videoIndexerPrincipalId)) { + name: guid(openAiService.id, videoIndexerPrincipalId, 'video-indexer-cog-services-contributor') + scope: openAiService + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' + ) + principalId: videoIndexerPrincipalId + principalType: 'ServicePrincipal' + } +} + +resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !empty(videoIndexerPrincipalId)) { + name: guid(openAiService.id, videoIndexerPrincipalId, 'video-indexer-cog-services-user') + scope: openAiService + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a97b65f3-24c7-4388-baec-2e87135dc908' + ) + principalId: videoIndexerPrincipalId + principalType: 'ServicePrincipal' + } +} diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index 675c12b94..d2259918f 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -7,6 +7,8 @@ param enterpriseAppServicePrincipalId string param cosmosDBName string param acrName string param openAIName string +param openAIResourceGroupName string +param openAISubscriptionId string param docIntelName string param storageAccountName string param speechServiceName string @@ -15,6 +17,8 @@ param redisCacheName string param contentSafetyName string param videoIndexerName string +var useExternalOpenAIResource = openAIName != '' && !empty(openAIResourceGroupName) && !empty(openAISubscriptionId) + resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { name: webAppName } @@ -31,7 +35,7 @@ resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { name: acrName } -resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { +resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (openAIName != '' && !useExternalOpenAIResource) { name: openAIName } @@ -119,7 +123,7 @@ resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { } // Grant the openai service access cognitive services openai user -resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { +resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity' && openAIName != '' && !useExternalOpenAIResource) { scope: openAiService name: guid(openAiService.id, webApp.id, 'openai-user') properties: { @@ -133,7 +137,7 @@ resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = i } // Grant the enterprise application access to the cognitive services openai user -resource openAIenterpriseAppUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { +resource openAIenterpriseAppUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity' && openAIName != '' && !useExternalOpenAIResource) { scope: openAiService name: guid(openAiService.id, webApp.id, 'enterpriseApp-CognitiveServicesOpenAIUserRole') properties: { @@ -146,6 +150,20 @@ resource openAIenterpriseAppUserRole 'Microsoft.Authorization/roleAssignments@20 } } +module openAIExternalPermissions 'setPermissions-openAIExternal.bicep' = if (useExternalOpenAIResource) { + name: 'openAIExternalPermissions' + scope: resourceGroup(openAISubscriptionId, openAIResourceGroupName) + params: { + openAIName: openAIName + authenticationType: authenticationType + webAppPrincipalId: webApp.identity.principalId + enterpriseAppServicePrincipalId: enterpriseAppServicePrincipalId + #disable-next-line BCP318 // may be null if video indexer not deployed + videoIndexerPrincipalId: videoIndexerName != '' ? videoIndexerService.identity.principalId : '' + videoIndexerName: videoIndexerName + } +} + // grant the managed identity access to document intelligence as a Cognitive Services User resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(docIntelService.id, webApp.id, 'doc-intel-user') @@ -246,7 +264,7 @@ resource videoIndexerStorageBlobDataContributorRole 'Microsoft.Authorization/rol } // grant the video indexer service access to OpenAI service as cognitive services Contributor -resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '') { +resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !useExternalOpenAIResource) { name: guid(openAiService.id, videoIndexerService.id, 'video-indexer-cog-services-contributor') scope: openAiService properties: { @@ -261,7 +279,7 @@ resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/ } // grant the video indexer service access to OpenAI service as cognitive services user -resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '') { +resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !useExternalOpenAIResource) { name: guid(openAiService.id, videoIndexerService.id, 'video-indexer-cog-services-user') scope: openAiService properties: { diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index 5a12b56fd..f79ffce03 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -1,15 +1,20 @@ from azure.cosmos import CosmosClient from azure.cosmos.exceptions import CosmosResourceNotFoundError -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from azure.keyvault.secrets import SecretClient import os import json -credential = DefaultAzureCredential() -token = credential.get_token("https://cosmos.azure.com/.default") +credential = AzureCliCredential() cosmosEndpoint = os.getenv("var_cosmosDb_uri") -client = CosmosClient(cosmosEndpoint, credential=credential) +cosmosKey = os.getenv("var_cosmosDb_key") + +if cosmosKey: + client = CosmosClient(cosmosEndpoint, cosmosKey) +else: + credential.get_token("https://cosmos.azure.com/.default") + client = CosmosClient(cosmosEndpoint, credential=credential) database_name = "SimpleChat" container_name = "settings" @@ -35,6 +40,7 @@ var_keyVaultUri = os.getenv("var_keyVaultUri") var_openAIEndpoint = os.getenv("var_openAIEndpoint") +var_openAISubscriptionId = os.getenv("var_openAISubscriptionId") var_openAIResourceGroup = os.getenv("var_openAIResourceGroup") var_subscriptionId = os.getenv("var_subscriptionId") var_rgName = os.getenv("var_rgName") @@ -68,8 +74,15 @@ # AI Models item["azure_openai_gpt_endpoint"] = var_openAIEndpoint item["azure_openai_gpt_authentication_type"] = var_authenticationType -item["azure_openai_gpt_subscription_id"] = var_subscriptionId +item["azure_openai_gpt_subscription_id"] = var_openAISubscriptionId or var_subscriptionId item["azure_openai_gpt_resource_group"] = var_openAIResourceGroup +if keyvault_client and var_authenticationType == "key": + try: + openai_key_secret = keyvault_client.get_secret("openAi-key") + item["azure_openai_gpt_key"] = openai_key_secret.value + print("Retrieved Azure OpenAI key from Key Vault for GPT settings") + except Exception as e: + print(f"Warning: Could not retrieve openAi-key from Key Vault for GPT settings: {e}") item["gpt_model"] = { "selected": [ { @@ -88,8 +101,15 @@ item["azure_openai_embedding_endpoint"] = var_openAIEndpoint item["azure_openai_embedding_authentication_type"] = var_authenticationType -item["azure_openai_embedding_subscription_id"] = var_subscriptionId +item["azure_openai_embedding_subscription_id"] = var_openAISubscriptionId or var_subscriptionId item["azure_openai_embedding_resource_group"] = var_openAIResourceGroup +if keyvault_client and var_authenticationType == "key": + try: + openai_key_secret = keyvault_client.get_secret("openAi-key") + item["azure_openai_embedding_key"] = openai_key_secret.value + print("Retrieved Azure OpenAI key from Key Vault for embedding settings") + except Exception as e: + print(f"Warning: Could not retrieve openAi-key from Key Vault for embedding settings: {e}") item["embedding_model"] = { "selected": [ { diff --git a/deployers/bicep/validate_azd_prerequisites.py b/deployers/bicep/validate_azd_prerequisites.py new file mode 100644 index 000000000..b28a8130e --- /dev/null +++ b/deployers/bicep/validate_azd_prerequisites.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +Validate and explain deployment prerequisites before `azd provision` or `azd up` continues. + +Version: 0.237.018 +Implemented in: 0.237.018 + +This script ensures users understand the prerequisites for reusing an existing VNet +and for configuring private DNS zones when private networking is enabled. +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import urllib.request + + +def _to_bool(value: str | None) -> bool: + return str(value or '').strip().lower() in {'1', 'true', 'yes', 'y'} + + +def _get_env(*names: str) -> str: + for name in names: + value = os.getenv(name) + if value is not None and value != '': + return value + return '' + + +def _print_header(title: str) -> None: + print('') + print('=' * 80) + print(title) + print('=' * 80) + + +def _parse_private_dns(raw_value: str | None) -> tuple[dict, str | None]: + if not raw_value or not raw_value.strip(): + return {}, None + + try: + parsed = json.loads(raw_value) + except json.JSONDecodeError as exc: + return {}, f'privateDnsZoneConfigs is not valid JSON: {exc}' + + if not isinstance(parsed, dict): + return {}, 'privateDnsZoneConfigs must be a JSON object.' + + return parsed, None + + +def _confirm_to_continue() -> bool: + if _to_bool(os.getenv('CI')) or _to_bool(os.getenv('AZD_NONINTERACTIVE')): + return True + + try: + response = input('\nType CONTINUE to proceed with deployment, or anything else to stop: ').strip() + except EOFError: + return False + + return response == 'CONTINUE' + + +def _parse_allowed_ip_ranges(raw_value: str | None) -> list[str]: + if not raw_value or not raw_value.strip(): + return [] + + return [value.strip() for value in raw_value.split(',') if value.strip()] + + +def _get_runner_public_ip() -> str: + with urllib.request.urlopen('https://api.ipify.org', timeout=10) as response: + return response.read().decode().strip() + + +def _persist_allowed_ip_ranges(updated_ranges: str) -> tuple[bool, str | None]: + command = ['azd', 'env', 'set', 'ALLOWED_IP_RANGES', updated_ranges] + result = subprocess.run(command, capture_output=True, text=True, check=False) + if result.returncode != 0: + error_output = (result.stderr or result.stdout or '').strip() + return False, error_output or 'Unknown azd env set failure.' + + os.environ['ALLOWED_IP_RANGES'] = updated_ranges + os.environ['AZURE_ENV_ALLOWED_IP_RANGES'] = updated_ranges + return True, None + + +def _ensure_runner_ip_is_allowed(enable_private_networking: bool) -> None: + if not enable_private_networking: + return + + configured_ranges = _parse_allowed_ip_ranges(_get_env('AZURE_ENV_ALLOWED_IP_RANGES', 'ALLOWED_IP_RANGES')) + + try: + runner_public_ip = _get_runner_public_ip() + except Exception as exc: + print('') + print('WARNING: Could not resolve the deployment runner public IP automatically.') + print(f' Details: {exc}') + print(' If this runner must reach Cosmos DB or Azure Container Registry over a public network path,') + print(' set ALLOWED_IP_RANGES manually before rerunning azd.') + return + + if runner_public_ip in configured_ranges: + print('') + print(f'Runner public IP {runner_public_ip} is already present in ALLOWED_IP_RANGES.') + return + + updated_ranges_list = configured_ranges + [runner_public_ip] + updated_ranges = ','.join(dict.fromkeys(updated_ranges_list)) + persisted, error_output = _persist_allowed_ip_ranges(updated_ranges) + + print('') + if persisted: + print(f'Added deployment runner public IP {runner_public_ip} to ALLOWED_IP_RANGES for this AZD environment.') + print('This makes the runner IP available to Cosmos DB and Azure Container Registry firewall rules during provisioning.') + print('If you later add firewall rules manually in Azure Portal, allow up to 30 minutes for the change to propagate before rerunning azd up.') + else: + print(f'WARNING: Failed to persist deployment runner public IP {runner_public_ip} into ALLOWED_IP_RANGES.') + print(f' Details: {error_output}') + print(' If this runner needs public access to protected resources, set ALLOWED_IP_RANGES manually before rerunning azd.') + + +def main() -> int: + enable_private_networking = _to_bool(_get_env('AZURE_ENV_ENABLE_PRIVATE_NETWORKING', 'ENABLE_PRIVATE_NETWORKING')) + existing_vnet_id = _get_env('AZURE_ENV_EXISTING_VNET_RESOURCE_ID', 'EXISTING_VNET_RESOURCE_ID').strip() + app_subnet_id = _get_env('AZURE_ENV_EXISTING_APP_SERVICE_SUBNET_RESOURCE_ID', 'EXISTING_APP_SERVICE_SUBNET_RESOURCE_ID').strip() + pe_subnet_id = _get_env('AZURE_ENV_EXISTING_PRIVATE_ENDPOINT_SUBNET_RESOURCE_ID', 'EXISTING_PRIVATE_ENDPOINT_SUBNET_RESOURCE_ID').strip() + private_dns_raw = _get_env('AZURE_ENV_PRIVATE_DNS_ZONE_CONFIGS', 'PRIVATE_DNS_ZONE_CONFIGS') + + if not enable_private_networking: + return 0 + + _ensure_runner_ip_is_allowed(enable_private_networking) + + dns_config, dns_error = _parse_private_dns(private_dns_raw) + if dns_error: + print(f'ERROR: {dns_error}', file=sys.stderr) + return 1 + + if existing_vnet_id: + _print_header('PRIVATE NETWORKING PREREQUISITES: EXISTING VNET SELECTED') + print('You selected private networking and supplied an existing VNet resource ID.') + print('Before deployment continues, verify these prerequisites are already in place:') + print('') + print('- Existing VNet is reachable and approved for this deployment') + print('- Existing App Service integration subnet already exists') + print('- Existing private endpoint subnet already exists') + print('- App Service integration subnet is delegated to Microsoft.Web/serverFarms') + print('- Cross-resource-group or cross-subscription access is approved if applicable') + print('- Private DNS zones and VNet links are planned correctly') + + missing = [] + if not app_subnet_id: + missing.append('AZURE_ENV_EXISTING_APP_SERVICE_SUBNET_RESOURCE_ID') + if not pe_subnet_id: + missing.append('AZURE_ENV_EXISTING_PRIVATE_ENDPOINT_SUBNET_RESOURCE_ID') + + if missing: + print('') + print('ERROR: Existing VNet reuse requires the following values:', file=sys.stderr) + for item in missing: + print(f'- {item}', file=sys.stderr) + print('', file=sys.stderr) + print('Pause and supply the missing subnet resource IDs before running azd again.', file=sys.stderr) + return 1 + else: + _print_header('PRIVATE NETWORKING PREREQUISITES: NEW VNET WILL BE CREATED') + print('You selected private networking without an existing VNet resource ID.') + print('The deployment will create the VNet and required subnets for you.') + + _print_header('PRIVATE DNS ZONE BEHAVIOR') + if not dns_config: + print('No privateDnsZoneConfigs value was provided.') + print('The deployment will create the supported private DNS zones locally and create VNet links automatically.') + else: + print('privateDnsZoneConfigs was provided.') + print('The deployment may reuse one or more existing private DNS zones instead of creating them locally.') + print('Verify each reused zone is correct for the service and cloud environment.') + print('') + for zone_name, zone_config in dns_config.items(): + if not isinstance(zone_config, dict): + print(f'- {zone_name}: invalid value; expected an object') + continue + zone_resource_id = zone_config.get('zoneResourceId') + create_vnet_link = zone_config.get('createVNetLink', True) + if zone_resource_id: + print(f'- {zone_name}: reuse zone {zone_resource_id}') + else: + print(f'- {zone_name}: create zone in deployment resource group') + if create_vnet_link: + print(f' - VNet link will be created automatically for {zone_name}') + else: + print(f' - VNet link will NOT be created automatically for {zone_name}') + print(' Ensure the zone is already linked to the target VNet, or name resolution will fail.') + + print('') + print('Required private DNS coverage commonly includes:') + print('- privatelink.azurewebsites.net') + print('- privatelink.documents.azure.com') + print('- privatelink.blob.core.windows.net') + print('- privatelink.search.windows.net') + print('- privatelink.openai.azure.com') + print('- privatelink.cognitiveservices.azure.com') + + if not _confirm_to_continue(): + print('Deployment stopped. Review prerequisites, then rerun azd when ready.', file=sys.stderr) + return 1 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/deployers/terraform/.terraform.lock.hcl b/deployers/terraform/.terraform.lock.hcl index a4c778bc8..393ead271 100644 --- a/deployers/terraform/.terraform.lock.hcl +++ b/deployers/terraform/.terraform.lock.hcl @@ -1,63 +1,82 @@ # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. +provider "registry.terraform.io/azure/azapi" { + version = "2.8.0" + constraints = ">= 2.8.0" + hashes = [ + "h1:OHkG0B1rphuC9qxN4qXNshulMjM3gIqeJVPSvVcewcM=", + "zh:048fa67ba123c6da65a7af12453328e36e1783cac1dbecc905d44ee7a1daa41c", + "zh:08dfb8c493a99aa54ea0c00f5d2e2389aac55d70b31bfc50a38e4ab61800aca8", + "zh:0d5bf53f356864567bf0855eb90b0b9aa4619b60fd1469210461ad88c0508a6f", + "zh:221cc52181d81bd741e8624ba9619ae20438f7a13828b72aa138a51b57bc1483", + "zh:51e7485e4f502cbbefe9b4ea991961eb9b19f41862593150905197bbb37cc6fb", + "zh:6e2d0986176bbeabdfa7dc3d1bf37d0a24549ebff29a3c9e8c5082e03cc38247", + "zh:87e46ceddcd3a4b7ed16f6b853c286840753d8af8ae8df0618ab5f29e950976b", + "zh:894998419943fadb3b85d1469665e9b7cdf492e6dc30907a77e32043e1d52b6a", + "zh:9f1efae3ad37510d947e7a27118a84bae55e35681b047d939781da96dd6ab6c7", + "zh:a201371f6c4c65b6976a8a360223c188ea91b7a33078fdd3a5f5f0ac7b438d35", + "zh:af3cc16bdfc545e61ce66449b9daaebfaa0c5e495777241c9414671a31e37ffa", + "zh:dbbb263a5f4c40624823fd3e68dc046b1f00325548393557384f0914a4694278", + ] +} + provider "registry.terraform.io/hashicorp/azuread" { - version = "3.5.0" + version = "3.8.0" constraints = ">= 3.4.0" hashes = [ - "h1:kNDEw6aRqRXnU/K6Xz4B5MQ9IzKTH/FRa2WlUuXERfo=", - "zh:0cda57e87a296f24f1c085b0343a8d6759cb539a7804f874d7a04bf46ab13c48", - "zh:11723a9bc589c763e2ffc76e6829c968cc01cf9c71b2cfaf56f8054289f8e29d", + "h1:5nlEb2pLnQmbOIWvHOOsYD3dV6zpw/MbPB6//XPSxP4=", + "zh:0d26cfbf9417acd1c2295ccd5b0052abeac85ad1c3f6422ff09bf6a1ce16f00d", + "zh:144d4ea92fed541a6376bc76ad65ba4738dfd7bcab4c9d6cc20d35001338d06d", "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", - "zh:1e5b766bab10c45f09dab18d51ff984016b4ad39cd45a214ec7d62943bbe812a", - "zh:30fb02a2e75210b835673bbf7f8a7f5f0ffefb51bb7acc93724f81e8e5990743", - "zh:3c2436989467839a3183a62338af0f191ed701f4feaacff5a4b9f216b7f73bd5", - "zh:4550b15ec03bd6be78a99743a9ffb527c3651a66083bc6eee8ca39f3e991416b", - "zh:529b46d0d7fcb40d876e2d501c456a11afb9d6172b2def3e61a08471250b42e5", - "zh:5dd2732534bedbe594c0e1ed8bf43efb7fe1821ad96daa5ed6a84079337ae2d9", - "zh:ac2c837ef2572ec82e0d81e9e2a157a4bd2baa4198f03c58610d6394af8808bb", - "zh:b1c807ec370df6a2079af8891d3b83c38af73191ceb1387ba6d88b05641ce3e7", - "zh:edfff8820f1d618ebfb0279d78edd4c97163363a3e96caee97c3582bbb4c606d", + "zh:2061d2cb64d8167d0af37e6610d0dc051977bd1ccc0e5cdd5ab02525ee239f96", + "zh:75562fdc3b313b7e538907199bfa588a1fcbc40113b0f3b7bfb496fbc358a32f", + "zh:78bef022ae9b1b0c636b7dd32bcda13fb273023f3a888cc005f3aa20e365b417", + "zh:ad8dbee59843154f8e93b24db9939a4257d13c7c86331eb93f1691294bc4e31f", + "zh:b3d83d7ac57073631704336a188cb746c473f728fc7ccb76abecb520e83fdf65", + "zh:c0bf9e0be73843de9089597be2720e4093b3ba320fbad99ab86da47681e77949", + "zh:c9a4c27d2b0800d3f4ece19d66c1fa574f7cd4ff66277af8f120d65e8f03f48e", + "zh:cd9ad8c848e17d9824045c33132cc0e87aa4d58cdb7bee6c0f6c3f9bc27892d5", + "zh:fbcafc21cdd19451274b905f9ee8a5b758ca63cc231e7544c815642e4a399c6d", ] } provider "registry.terraform.io/hashicorp/azurerm" { - version = "4.43.0" + version = "4.63.0" constraints = ">= 4.29.0, < 5.0.0" hashes = [ - "h1:5f/N0M1dpF+JkoViARf4kbmhgy1X1WsVh/1Egd9rUxw=", - "zh:02753f079623e4dd29738963c4169649ade36972032d48704255b8982f385490", - "zh:1f263740daa23c7fd8a43df625aef9828d98277f89d6144d94658dec109e542f", - "zh:2b2a98c0f1cba0833ed1d565d2c392fa47c2ae0495f25ef25aad94216547ce9f", - "zh:590b1898b7f3d6f3d9ba1b14473dfb14c3eee14bdcbbd8621b59979727bec202", - "zh:6d515528c935fb372ffbb2774c2b211ecc8c74b1a9335f3631fae3a9d1a234fc", - "zh:73bd1a4595524d0bcff7e9742d3f414e17f0c2603727fb9b7c4b2d41c8cac9eb", + "h1:jjqAsWVyu+nPkwmyk3vxNEF/6ZMfWs8cIxfkj1QGcxA=", + "zh:08415ed33647642c59a73102a1443ef37b0e97d96c027d1364ad7c25b4d4dfbb", + "zh:0cedb8fa48d74e1673bb17efd8eb9e251fc5cf4699450f14c83d94299ce099fd", + "zh:4ded2cc9013ea3dc75fde071505d97d494477a491c601b2e5dd7bfd83189c01d", + "zh:5b5c2bc5a2fe0bc87b3b3800fb724887b0ef04ed99c01622eee8dd6a19fbf201", + "zh:5f720813486ffbe93ffa0382416a4eeb2a7cc5c6788fad5895bb119ec34d0cec", + "zh:6c6be72a06f30b27d0bf80905c2204d5e81bdf26d01bcb87bdca9bd531f65b62", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:800ff99d82448a5943253e9655571e243556a89bfd03d828c04e3d0ce44630be", - "zh:967703b266b2dd6df7429a218228910106dfc91ec9be02773c24698b3ee4fe71", - "zh:a25c703898500e8ee0379141d48ae98e5be3dd94285994b8e1953562b0a83b45", - "zh:aaf8870943e34a3258649387b0c32ad03e3a1930bc2f66dcbad8c58aa612c876", - "zh:c03e04e5ef23556286431fac099c450c97ec17aa1360eebbfbb0de2313b9dbe1", + "zh:8709312ff03cd2023faccde77a0ff791eb1d8012cbc59bf09ffb0c4521584e3c", + "zh:a903b41fa3a48ee686f432e67365da3e3ed0b0af9d06d19641aba6f5d430bff0", + "zh:e9496eae3e66a1ae365b4be7b0f57593f288808e0a37df9f54dfcd7991d0f8fb", + "zh:eb9bc4ea472bbbf74ecf7937272f36d169484ebd0c46c5134c297c9ffcdd072d", + "zh:fe0ab911629ed1cfa5dc0890e6e5fc724492166130bf3367225eac2ef47c8690", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.7.2" + version = "3.8.1" constraints = ">= 3.1.0" hashes = [ - "h1:0hcNr59VEJbhZYwuDE/ysmyTS0evkfcLarlni+zATPM=", - "h1:Def/iHM4HihJCIxQ8AYoxtoVL5lVlYx0V7bX91pxwgM=", - "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", - "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", - "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", - "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3", - "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212", - "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f", + "h1:osH3aBqEARwOz3VBJKdpFKJJCNIdgRC6k8vPojkLmlY=", + "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", + "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", + "zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57", + "zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0", + "zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66", + "zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34", - "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967", - "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d", - "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62", - "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0", + "zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9", + "zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05", + "zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8", + "zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b", + "zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699", ] } diff --git a/deployers/terraform/ReadMe.md b/deployers/terraform/ReadMe.md index fb2dcf7de..1f651d3e6 100644 --- a/deployers/terraform/ReadMe.md +++ b/deployers/terraform/ReadMe.md @@ -21,6 +21,30 @@ After logging into the target Azure subscription, execute the Initalize-AzureEnv Initialize-AzureEnvironment.ps1 -ResourceGroupName "myResourceGroup" -AzureRegion "eastus" -ACRName "myACR" -OpenAiName "myOpenAI" ``` +## Important Terraform Scope Notes + +This Terraform deployer now supports the same core private networking flow as the Bicep deployer for these scenarios: + +- Create a new VNet with the required App Service integration and private endpoint subnets. +- Reuse an existing VNet by supplying existing subnet resource IDs. +- Create private endpoints for Key Vault, Cosmos DB, Storage, Azure AI Search, Azure OpenAI, Document Intelligence, Container Registry, and the App Service. +- Create private DNS zones automatically, or reuse existing private DNS zones by resource ID. +- Create VNet links for each private DNS zone unless you explicitly opt out per zone. + +The same OpenAI limitation still applies as in Bicep: endpoint-only reuse is valid for application configuration, but Terraform can only automate the Azure OpenAI private endpoint when it has Azure resource metadata from either a newly created OpenAI resource or an existing standard Azure OpenAI resource name/resource group. + +If you plan to reuse enterprise-managed networking, make sure these prerequisites already exist: + +- An existing VNet with separate subnets for App Service VNet integration and private endpoints +- Delegation of the App Service integration subnet to `Microsoft.Web/serverFarms` +- Required private DNS zones for services such as App Service, Cosmos DB, Storage, AI Search, Azure OpenAI, and Cognitive Services +- Correct VNet links for each required private DNS zone + +For detailed networking guidance, see: + +- [../bicep/README.md](../bicep/README.md) +- [../../docs/how-to/enterprise_networking.md](../../docs/how-to/enterprise_networking.md) + ## Configure Terraform Secrets Create a terraform.tfvars: Create a terraform.tfvars file (or provide via environment variables) for sensitive variables like ACR credentials: @@ -37,7 +61,19 @@ ACR_PASSWORD = "your_acr_password" ## Deploy initial container -From Github > Actions > "SimpleChat Docker Image Publish" > Run workflow +Terraform does not build the container image itself. It expects `image_name` to point to an image tag that already exists in your Azure Container Registry. + +Recommended options: + +1. Build directly in ACR with Azure CLI: + +```azurecli +az acr build --registry --file application/single_app/Dockerfile --image simple-chat:latest . +``` + +2. Use the repository's GitHub Actions image publish workflow if that matches your release process. + +If you use `az acr build`, run it from the repository root so the Docker build context includes the files referenced by `application/single_app/Dockerfile`. ## Terraform deployment @@ -47,7 +83,7 @@ Apply: Run terraform apply to provision the resources. Login to Azure CLI (See instructions above) terraform init -terraform init -uprade +terraform init -upgrade ### .tfvars @@ -73,15 +109,49 @@ param_base_name = "rudy1" #### Open AI Variables -param_use_existing_openai_instance = "true" +param_use_existing_openai_instance = true param_existing_azure_openai_resource_name = "gregazureopenai1" param_existing_azure_openai_resource_group_name = "azureopenairg" +param_existing_azure_openai_subscription_id = "00000000-0000-0000-0000-000000000000" +# Optional endpoint override for existing Azure OpenAI or Azure AI Foundry-compatible endpoints +# param_existing_azure_openai_endpoint = "https://my-openai-resource.openai.azure.com/" + +#### Private Networking Variables + +param_enable_private_networking = true + +# Option 1: let Terraform create the VNet and required subnets +# param_existing_virtual_network_id = "" +# param_existing_app_service_subnet_id = "" +# param_existing_private_endpoint_subnet_id = "" + +# Option 2: reuse an existing VNet and existing subnets +# param_existing_virtual_network_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/network-rg/providers/Microsoft.Network/virtualNetworks/enterprise-vnet" +# param_existing_app_service_subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/network-rg/providers/Microsoft.Network/virtualNetworks/enterprise-vnet/subnets/AppServiceIntegration" +# param_existing_private_endpoint_subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/network-rg/providers/Microsoft.Network/virtualNetworks/enterprise-vnet/subnets/PrivateEndpoints" + +# Optional: override the default address ranges used when Terraform creates the VNet +# param_private_network_address_space = ["10.0.0.0/21"] +# param_app_service_integration_subnet_prefixes = ["10.0.0.0/24"] +# param_private_endpoint_subnet_prefixes = ["10.0.2.0/24"] + +# Optional: reuse customer-managed private DNS zones per service +# create_vnet_link defaults to true +# param_private_dns_zone_configs = { +# openAi = { +# zone_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/dns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.openai.azure.com" +# create_vnet_link = false +# } +# webSites = { +# zone_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/dns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.azurewebsites.net" +# } +# } #### Other Settings Variables param_resource_owner_id = "Tom Jones" param_resource_owner_email_id = "tom@somedomain.onmicrosoft.us" -param_create_entra_security_groups = "true" +param_create_entra_security_groups = true ### How to deploy with tfvars file @@ -91,6 +161,19 @@ terraform destroy -var-file="./params/rudy1.tfvars" -auto-approve ## Post-Deployment Manual Steps +### Private Networking Notes + +- If `param_enable_private_networking = true` and you supply any existing VNet or subnet IDs, supply **both** `param_existing_app_service_subnet_id` and `param_existing_private_endpoint_subnet_id`. +- If you reuse private DNS zones, use full resource IDs in `param_private_dns_zone_configs`. +- If a central networking team already manages the VNet links for a reused zone, set `create_vnet_link = false` for that zone. +- The App Service private endpoint uses the `sites` subresource and the web app itself is configured with public network access disabled when private networking is enabled. + +### Existing Azure OpenAI Notes + +- If you reuse an existing standard Azure OpenAI resource in another subscription, set `param_existing_azure_openai_subscription_id` so Terraform can look up that resource correctly. +- If you provide only `param_existing_azure_openai_endpoint` without a resource name and resource group, Terraform will use that endpoint for application configuration but will **not** create RBAC assignments against the OpenAI resource. +- Endpoint-only reuse is suitable for cases where the endpoint is externally managed, but managed identity permissions and Azure OpenAI private endpoint automation must then be handled outside this Terraform deployment. + STEP 1) Configure Azure Search indexes: Deploy index as json files to Azure Search: ai_search-index-group.json, ai_search-index-user.json via the portal. @@ -98,19 +181,6 @@ STEP 2) Navigate to Web UI url in a browser. In the web ui, click on "Admin" > "app settings" to configure your app settings. -**NOTE:** When configuring the GPT / Embeddings / Image Generation endpoints, the endpoint / key provided by Azure AI Foundry deployments will cause issues. -The provided Endpoint / Key will work when "Test GPT Connection" is executed, but fail to "Fetch GPT Models". To work around this issue, edit the Endpoint URL to the name of the OpenAI service, Fetch the GPT Models, select models as needed, then revert the name back to the original endpoint. - -EX: -- Azure OpenAI GPT Endpoint: https://northcentralus.api.cognitive.microsoft.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-01-01-preview - -Revise to: - -- Azure OpenAI GPT Endpoint: https://`.api.cognitive.microsoft.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-01-01-preview -- Save pending change -- Fetch GPT Models & select required models -- Save pending change -- Revert OpenAI GPT Endpoint: https://northcentralus.api.cognitive.microsoft.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-01-01-preview -- Save pending change +**NOTE:** Azure AI Foundry-compatible endpoints are supported for application configuration, but endpoint-only reuse does not give Terraform enough Azure resource metadata to automate RBAC or private endpoint integration. If you use an endpoint-only configuration, complete any required managed identity permissions and network isolation steps outside this Terraform deployment. STEP 3) Test Web UI fully. diff --git a/deployers/terraform/main.tf b/deployers/terraform/main.tf index 77b486df5..56df400d6 100644 --- a/deployers/terraform/main.tf +++ b/deployers/terraform/main.tf @@ -22,6 +22,10 @@ terraform { required_version = ">= 1.12.0" required_providers { + azapi = { + source = "Azure/azapi" + version = ">= 2.8.0" + } azuread = { source = "hashicorp/azuread" version = ">= 3.4.0" @@ -55,15 +59,42 @@ provider "azurerm" { } } storage_use_azuread = true - environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : (var.global_which_azure_platform == "AzureCloud" ? "public" : null) - tenant_id = var.param_tenant_id - subscription_id = var.param_subscription_id + environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : (var.global_which_azure_platform == "AzureCloud" ? "public" : null) + tenant_id = var.param_tenant_id + subscription_id = var.param_subscription_id +} + +# Secondary AzureRM provider for looking up an existing Azure OpenAI resource +# that may live in a different subscription. +provider "azurerm" { + alias = "existing_openai" + + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + cognitive_account { + purge_soft_delete_on_destroy = true + } + key_vault { + purge_soft_delete_on_destroy = true + recover_soft_deleted_key_vaults = true + } + log_analytics_workspace { + permanently_delete_on_destroy = true + } + } + + storage_use_azuread = true + environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : (var.global_which_azure_platform == "AzureCloud" ? "public" : null) + tenant_id = var.param_tenant_id + subscription_id = var.param_existing_azure_openai_subscription_id != "" ? var.param_existing_azure_openai_subscription_id : var.param_subscription_id } # Configure the AzureAD Provider provider "azuread" { environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : (var.global_which_azure_platform == "AzureCloud" ? "public" : null) - tenant_id = var.param_tenant_id + tenant_id = var.param_tenant_id } # Variables @@ -143,6 +174,18 @@ variable "param_existing_azure_openai_resource_group_name" { type = string } +variable "param_existing_azure_openai_subscription_id" { + description = "Optional subscription ID for an existing Azure OpenAI resource when it lives outside the primary deployment subscription." + type = string + default = "" +} + +variable "param_existing_azure_openai_endpoint" { + description = "Optional existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint. When provided, Terraform uses this endpoint directly instead of building one from the resource name." + type = string + default = "" +} + variable "param_create_entra_security_groups" { description = "Set to true to create Entra ID security groups." type = bool @@ -164,33 +207,36 @@ variable "acr_password" { # Resource Naming Convention (matching PowerShell script logic) locals { - resource_group_name = "sc-${var.param_base_name}-${var.param_environment}-rg" - app_registration_name = "${var.param_base_name}-${var.param_environment}-ar" - app_service_plan_name = "${var.param_base_name}-${var.param_environment}-asp" - app_service_name = "${var.param_base_name}-${var.param_environment}-app" - app_insights_name = "${var.param_base_name}-${var.param_environment}-ai" - cosmos_db_name = "${var.param_base_name}-${var.param_environment}-cosmos" - open_ai_name = "${var.param_base_name}-${var.param_environment}-oai" - doc_intel_name = "${var.param_base_name}-${var.param_environment}-docintel" - key_vault_name = "${var.param_base_name}-${var.param_environment}-kv" - log_analytics_name = "${var.param_base_name}-${var.param_environment}-la" - managed_identity_name = "${var.param_base_name}-${var.param_environment}-id" - search_service_name = "${var.param_base_name}-${var.param_environment}-search" - storage_account_base = "${var.param_base_name}${var.param_environment}sa" - storage_account_name = substr(replace(local.storage_account_base, "/[^a-z0-9]/", ""), 0, 24) - - acr_base_url = var.global_which_azure_platform == "AzureUSGovernment" ? "${var.acr_name}.azurecr.us" : "${var.acr_name}.azurecr.io" - param_registry_server = var.global_which_azure_platform == "AzureUSGovernment" ? "https://${var.acr_name}.azurecr.us" : "https://${var.acr_name}.azurecr.io" - - app_service_fqdn_suffix = var.global_which_azure_platform == "AzureUSGovernment" ? ".azurewebsites.us" : ".azurewebsites.net" - graph_url = var.global_which_azure_platform == "AzureUSGovernment" ? "https://graph.microsoft.us" : "https://graph.microsoft.com" - cosmos_db_url_template = var.global_which_azure_platform == "AzureUSGovernment" ? "https://%s.documents.azure.us:443/" : "https://%s.documents.azure.com:443/" - openai_url_template = var.global_which_azure_platform == "AzureUSGovernment" ? "https://%s.openai.azure.us/" : "https://%s.openai.azure.com/" + resource_group_name = "sc-${var.param_base_name}-${var.param_environment}-rg" + app_registration_name = "${var.param_base_name}-${var.param_environment}-ar" + app_service_plan_name = "${var.param_base_name}-${var.param_environment}-asp" + app_service_name = "${var.param_base_name}-${var.param_environment}-app" + app_insights_name = "${var.param_base_name}-${var.param_environment}-ai" + cosmos_db_name = "${var.param_base_name}-${var.param_environment}-cosmos" + open_ai_name = "${var.param_base_name}-${var.param_environment}-oai" + doc_intel_name = "${var.param_base_name}-${var.param_environment}-docintel" + key_vault_name = "${var.param_base_name}-${var.param_environment}-kv" + log_analytics_name = "${var.param_base_name}-${var.param_environment}-la" + managed_identity_name = "${var.param_base_name}-${var.param_environment}-id" + search_service_name = "${var.param_base_name}-${var.param_environment}-search" + storage_account_base = "${var.param_base_name}${var.param_environment}sa" + storage_account_name = substr(replace(local.storage_account_base, "/[^a-z0-9]/", ""), 0, 24) + + acr_base_url = var.global_which_azure_platform == "AzureUSGovernment" ? "${var.acr_name}.azurecr.us" : "${var.acr_name}.azurecr.io" + param_registry_server = var.global_which_azure_platform == "AzureUSGovernment" ? "https://${var.acr_name}.azurecr.us" : "https://${var.acr_name}.azurecr.io" + + app_service_fqdn_suffix = var.global_which_azure_platform == "AzureUSGovernment" ? ".azurewebsites.us" : ".azurewebsites.net" + graph_url = var.global_which_azure_platform == "AzureUSGovernment" ? "https://graph.microsoft.us" : "https://graph.microsoft.com" + cosmos_db_url_template = var.global_which_azure_platform == "AzureUSGovernment" ? "https://%s.documents.azure.us:443/" : "https://%s.documents.azure.com:443/" + openai_url_template = var.global_which_azure_platform == "AzureUSGovernment" ? "https://%s.openai.azure.us/" : "https://%s.openai.azure.com/" + existing_openai_subscription_id = var.param_existing_azure_openai_subscription_id != "" ? var.param_existing_azure_openai_subscription_id : var.param_subscription_id + use_existing_openai_resource_metadata = var.param_use_existing_openai_instance && var.param_existing_azure_openai_resource_name != "" && var.param_existing_azure_openai_resource_group_name != "" + enable_openai_rbac_assignments = !var.param_use_existing_openai_instance || local.use_existing_openai_resource_metadata # Tags for resources common_tags = { - Environment = var.param_environment - Owner = var.param_resource_owner_id + Environment = var.param_environment + Owner = var.param_resource_owner_id #CreatedDateTime = formatdate("YYYY-MM-DD hh:mm:ss", timestamp()) # time part causes updates to resources on update. Stopping this. CreatedDateTime = formatdate("YYYY-MM-DD", timestamp()) Project = "SimpleChat" @@ -214,42 +260,42 @@ data "azurerm_container_registry" "acrregistry" { # --- Entra ID Security Groups --- resource "azuread_group" "simplechat_admins" { - count = var.param_create_entra_security_groups ? 1 : 0 - display_name = "${var.param_base_name}-${var.param_environment}-sg-Admins" - mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-Admins" - description = "Security group for ${var.param_base_name} ${var.param_environment} environment" + count = var.param_create_entra_security_groups ? 1 : 0 + display_name = "${var.param_base_name}-${var.param_environment}-sg-Admins" + mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-Admins" + description = "Security group for ${var.param_base_name} ${var.param_environment} environment" security_enabled = true } resource "azuread_group" "simplechat_users" { - count = var.param_create_entra_security_groups ? 1 : 0 - display_name = "${var.param_base_name}-${var.param_environment}-sg-Users" - mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-Users" - description = "Security group for ${var.param_base_name} ${var.param_environment} environment" + count = var.param_create_entra_security_groups ? 1 : 0 + display_name = "${var.param_base_name}-${var.param_environment}-sg-Users" + mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-Users" + description = "Security group for ${var.param_base_name} ${var.param_environment} environment" security_enabled = true } resource "azuread_group" "simplechat_creategroup" { - count = var.param_create_entra_security_groups ? 1 : 0 - display_name = "${var.param_base_name}-${var.param_environment}-sg-CreateGroup" - mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-CreateGroup" - description = "Security group for ${var.param_base_name} ${var.param_environment} environment" + count = var.param_create_entra_security_groups ? 1 : 0 + display_name = "${var.param_base_name}-${var.param_environment}-sg-CreateGroup" + mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-CreateGroup" + description = "Security group for ${var.param_base_name} ${var.param_environment} environment" security_enabled = true } resource "azuread_group" "simplechat_safetyviolationadmin" { - count = var.param_create_entra_security_groups ? 1 : 0 - display_name = "${var.param_base_name}-${var.param_environment}-sg-SafetyViolationAdmin" - mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-SafetyViolationAdmin" - description = "Security group for ${var.param_base_name} ${var.param_environment} environment" + count = var.param_create_entra_security_groups ? 1 : 0 + display_name = "${var.param_base_name}-${var.param_environment}-sg-SafetyViolationAdmin" + mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-SafetyViolationAdmin" + description = "Security group for ${var.param_base_name} ${var.param_environment} environment" security_enabled = true } resource "azuread_group" "simplechat_feedbackadmin" { - count = var.param_create_entra_security_groups ? 1 : 0 - display_name = "${var.param_base_name}-${var.param_environment}-sg-FeedbackAdmin" - mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-FeedbackAdmin" - description = "Security group for ${var.param_base_name} ${var.param_environment} environment" + count = var.param_create_entra_security_groups ? 1 : 0 + display_name = "${var.param_base_name}-${var.param_environment}-sg-FeedbackAdmin" + mail_nickname = "${var.param_base_name}-${var.param_environment}-sg-FeedbackAdmin" + description = "Security group for ${var.param_base_name} ${var.param_environment} environment" security_enabled = true } @@ -278,7 +324,7 @@ resource "azurerm_key_vault" "kv" { location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name enabled_for_disk_encryption = false - purge_protection_enabled = false # Set to true in production + purge_protection_enabled = false # Set to true in production sku_name = "standard" # or "premium" tenant_id = var.param_tenant_id @@ -313,17 +359,17 @@ resource "azurerm_application_insights" "ai" { # --- Storage Account --- resource "azurerm_storage_account" "sa" { - name = local.storage_account_name - resource_group_name = azurerm_resource_group.rg.name - location = azurerm_resource_group.rg.location - account_tier = "Standard" - account_replication_type = "LRS" # Standard_LRS - account_kind = "StorageV2" - access_tier = "Hot" - allow_nested_items_to_be_public = false - public_network_access_enabled = false # From script's allow-blob-public-access false - shared_access_key_enabled = false - tags = local.common_tags + name = local.storage_account_name + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + account_tier = "Standard" + account_replication_type = "LRS" # Standard_LRS + account_kind = "StorageV2" + access_tier = "Hot" + allow_nested_items_to_be_public = false + public_network_access_enabled = var.param_enable_private_networking ? false : true + shared_access_key_enabled = false + tags = local.common_tags } # --- User-Assigned Managed Identity --- @@ -346,22 +392,26 @@ resource "azurerm_service_plan" "asp" { location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name os_type = "Linux" # Script uses --is-linux - sku_name = "P1v3" # Basic tier, 1 core, 1.75GB RAM. + sku_name = "P1v3" # Basic tier, 1 core, 1.75GB RAM. # ["B1" "B2" "B3" "S1" "S2" "S3" "P1v2" "P2v2" "P3v2" "P0v3" "P1v3" "P2v3" "P3v3" # "P1mv3" "P2mv3" "P3mv3" "P4mv3" "P5mv3" "Y1" "EP1" "EP2" "EP3" "FC1" "F1" # "I1" "I2" "I3" "I1v2" "I2v2" "I3v2" "I4v2" "I5v2" "I6v2" "I1mv2" "I2mv2" # "I3mv2" "I4mv2" "I5mv2" "D1" "SHARED" "WS1" "WS2" "WS3"] - tags = local.common_tags + tags = local.common_tags } # --- App Service (Web App) --- resource "azurerm_linux_web_app" "app" { - name = local.app_service_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - service_plan_id = azurerm_service_plan.asp.id - ftp_publish_basic_authentication_enabled = false - webdeploy_publish_basic_authentication_enabled = false + name = local.app_service_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + service_plan_id = azurerm_service_plan.asp.id + https_only = true + public_network_access_enabled = var.param_enable_private_networking ? false : true + virtual_network_subnet_id = local.resolved_app_service_subnet_id + vnet_image_pull_enabled = var.param_enable_private_networking + ftp_publish_basic_authentication_enabled = false + webdeploy_publish_basic_authentication_enabled = false # auth_settings { # enabled = true @@ -385,7 +435,7 @@ resource "azurerm_linux_web_app" "app" { require_https = true active_directory_v2 { - client_id = azuread_application.app_registration.client_id + client_id = azuread_application.app_registration.client_id client_secret_setting_name = "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" # This should be allowed optional tenant_auth_endpoint = var.global_which_azure_platform == "AzureUSGovernment" ? "https://login.microsoftonline.us/${data.azuread_client_config.current.tenant_id}/v2.0" : "https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/v2.0" } @@ -396,52 +446,56 @@ resource "azurerm_linux_web_app" "app" { } app_settings = { - "AZURE_ENDPOINT" = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : "public" - "SCM_DO_BUILD_DURING_DEPLOYMENT" = "false" - "AZURE_COSMOS_AUTHENTICATION_TYPE" = "key" - "AZURE_COSMOS_ENDPOINT" = format(local.cosmos_db_url_template, azurerm_cosmosdb_account.cosmos.name) - "AZURE_COSMOS_KEY" = azurerm_cosmosdb_account.cosmos.primary_key - "TENANT_ID" = var.param_tenant_id - "CLIENT_ID" = azuread_application.app_registration.client_id - "SECRET_KEY" = azuread_application_password.app_registration_secret.value - "WEBSITE_AUTH_AAD_ALLOWED_TENANTS" = var.param_tenant_id - "AZURE_OPENAI_RESOURCE_NAME" = var.param_use_existing_openai_instance ? var.param_existing_azure_openai_resource_name : azurerm_cognitive_account.openai[0].name - "AZURE_OPENAI_RESOURCE_GROUP_NAME" = var.param_use_existing_openai_instance ? var.param_existing_azure_openai_resource_group_name : azurerm_resource_group.rg.name - "AZURE_OPENAI_URL" = var.param_use_existing_openai_instance ? format(local.openai_url_template, var.param_existing_azure_openai_resource_name) : format(local.openai_url_template, azurerm_cognitive_account.openai[0].name) - "AZURE_SEARCH_SERVICE_NAME" = azurerm_search_service.search.name - "AZURE_SEARCH_API_KEY" = azurerm_search_service.search.primary_key - "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT" = azurerm_cognitive_account.docintel.endpoint - "AZURE_DOCUMENT_INTELLIGENCE_API_KEY" = azurerm_cognitive_account.docintel.primary_access_key - "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" = azuread_application_password.app_registration_secret.value - "APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.ai.instrumentation_key - "APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.ai.connection_string - "APPINSIGHTS_PROFILERFEATURE_VERSION" = "1.0.0" - "APPINSIGHTS_SNAPSHOTFEATURE_VERSION" = "1.0.0" - "APPLICATIONINSIGHTS_CONFIGURATION_CONTENT" = "" - "ApplicationInsightsAgent_EXTENSION_VERSION" = "~3" - "DiagnosticServices_EXTENSION_VERSION" = "~3" - "InstrumentationEngine_EXTENSION_VERSION" = "disabled" - "SnapshotDebugger_EXTENSION_VERSION" = "disabled" + "AZURE_ENDPOINT" = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : "public" + "SCM_DO_BUILD_DURING_DEPLOYMENT" = "false" + "WEBSITE_PULL_IMAGE_OVER_VNET" = var.param_enable_private_networking ? "true" : "false" + "AZURE_COSMOS_AUTHENTICATION_TYPE" = "key" + "AZURE_COSMOS_ENDPOINT" = format(local.cosmos_db_url_template, azurerm_cosmosdb_account.cosmos.name) + "AZURE_COSMOS_KEY" = azurerm_cosmosdb_account.cosmos.primary_key + "TENANT_ID" = var.param_tenant_id + "CLIENT_ID" = azuread_application.app_registration.client_id + "SECRET_KEY" = azuread_application_password.app_registration_secret.value + "WEBSITE_AUTH_AAD_ALLOWED_TENANTS" = var.param_tenant_id + "AZURE_OPENAI_RESOURCE_NAME" = var.param_use_existing_openai_instance ? var.param_existing_azure_openai_resource_name : azurerm_cognitive_account.openai[0].name + "AZURE_OPENAI_RESOURCE_GROUP_NAME" = var.param_use_existing_openai_instance ? var.param_existing_azure_openai_resource_group_name : azurerm_resource_group.rg.name + "AZURE_OPENAI_SUBSCRIPTION_ID" = var.param_use_existing_openai_instance ? local.existing_openai_subscription_id : var.param_subscription_id + "AZURE_OPENAI_URL" = var.param_use_existing_openai_instance ? (var.param_existing_azure_openai_endpoint != "" ? var.param_existing_azure_openai_endpoint : (local.use_existing_openai_resource_metadata ? data.azurerm_cognitive_account.existing_openai[0].endpoint : "")) : azurerm_cognitive_account.openai[0].endpoint + "AZURE_SEARCH_SERVICE_NAME" = azurerm_search_service.search.name + "AZURE_SEARCH_API_KEY" = azurerm_search_service.search.primary_key + "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT" = azurerm_cognitive_account.docintel.endpoint + "AZURE_DOCUMENT_INTELLIGENCE_API_KEY" = azurerm_cognitive_account.docintel.primary_access_key + "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" = azuread_application_password.app_registration_secret.value + "APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.ai.instrumentation_key + "APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.ai.connection_string + "APPINSIGHTS_PROFILERFEATURE_VERSION" = "1.0.0" + "APPINSIGHTS_SNAPSHOTFEATURE_VERSION" = "1.0.0" + "APPLICATIONINSIGHTS_CONFIGURATION_CONTENT" = "" + "ApplicationInsightsAgent_EXTENSION_VERSION" = "~3" + "DiagnosticServices_EXTENSION_VERSION" = "~3" + "InstrumentationEngine_EXTENSION_VERSION" = "disabled" + "SnapshotDebugger_EXTENSION_VERSION" = "disabled" "XDT_MicrosoftApplicationInsights_BaseExtensions" = "disabled" - "XDT_MicrosoftApplicationInsights_Mode" = "recommended" - "XDT_MicrosoftApplicationInsights_PreemptSdk" = "disabled" + "XDT_MicrosoftApplicationInsights_Mode" = "recommended" + "XDT_MicrosoftApplicationInsights_PreemptSdk" = "disabled" } site_config { - always_on = true - minimum_tls_version = "1.2" + always_on = true + health_check_path = "/external/healthcheck" + minimum_tls_version = "1.2" container_registry_use_managed_identity = true + vnet_route_all_enabled = var.param_enable_private_networking application_stack { - docker_image_name = "${var.image_name}" + docker_image_name = var.image_name docker_registry_username = var.acr_username docker_registry_password = var.acr_password - docker_registry_url = local.param_registry_server + docker_registry_url = local.param_registry_server } } identity { - type = "SystemAssigned, UserAssigned" + type = "SystemAssigned, UserAssigned" identity_ids = [azurerm_user_assigned_identity.id.id] } @@ -450,14 +504,14 @@ resource "azurerm_linux_web_app" "app" { site_config[0].application_stack[0].docker_image_name ] } - + tags = local.common_tags } # --- Entra App Registration & Service Principal --- resource "azuread_application" "app_registration" { - display_name = local.app_registration_name - owners = [data.azuread_client_config.current.object_id, data.azuread_user.application_owner.object_id] # Owner object ID + display_name = local.app_registration_name + owners = [data.azuread_client_config.current.object_id, data.azuread_user.application_owner.object_id] # Owner object ID # sign_in_audience = local.signInAudience # group_membership_claims = var.groupMembershipClaims @@ -497,8 +551,8 @@ data "azuread_service_principal" "msgraph" { # Add "Expose an API" Permissions (User.Read, Profile, email) ################################################################## resource "azuread_application_api_access" "api_permissions" { - api_client_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"] - application_id = azuread_application.app_registration.id + api_client_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"] + application_id = azuread_application.app_registration.id scope_ids = [ data.azuread_service_principal.msgraph.oauth2_permission_scope_ids["User.Read"], data.azuread_service_principal.msgraph.oauth2_permission_scope_ids["profile"], @@ -525,54 +579,54 @@ resource "azuread_service_principal_delegated_permission_grant" "delegatedpermis # Entra App Registration App Roles ################################################################## resource "azuread_application_app_role" "app_registration_admin" { - application_id = azuread_application.app_registration.id - allowed_member_types = ["User"] - description = "Allows access to Admin Settings page." - display_name = "Admins" - value = "Admin" - role_id = "e9b4823f-d17a-44f1-9d71-0f2c0ee45656" + application_id = azuread_application.app_registration.id + allowed_member_types = ["User"] + description = "Allows access to Admin Settings page." + display_name = "Admins" + value = "Admin" + role_id = "e9b4823f-d17a-44f1-9d71-0f2c0ee45656" } resource "azuread_application_app_role" "app_registration_user" { - application_id = azuread_application.app_registration.id - allowed_member_types = ["User"] - description = "Standard user access to chat features." - display_name = "Users" - value = "User" - role_id = "633746c6-3d03-480f-b273-58ece728be52" + application_id = azuread_application.app_registration.id + allowed_member_types = ["User"] + description = "Standard user access to chat features." + display_name = "Users" + value = "User" + role_id = "633746c6-3d03-480f-b273-58ece728be52" } resource "azuread_application_app_role" "app_registration_feedbackadmin" { - application_id = azuread_application.app_registration.id - allowed_member_types = ["User"] - description = "Allows access to view user feedback admin page." - display_name = "Feedback Admin" - value = "FeedbackAdmin" - role_id = "12e32860-88a8-421e-8a6f-faf08e3efe2e" + application_id = azuread_application.app_registration.id + allowed_member_types = ["User"] + description = "Allows access to view user feedback admin page." + display_name = "Feedback Admin" + value = "FeedbackAdmin" + role_id = "12e32860-88a8-421e-8a6f-faf08e3efe2e" } resource "azuread_application_app_role" "app_registration_safetyviolationadmin" { - application_id = azuread_application.app_registration.id - allowed_member_types = ["User"] - description = "Allows access to view content safety violations." - display_name = "Safety Violation Admin" - value = "SafetyViolationAdmin" - role_id = "877166d4-eaa3-4fa6-8d79-e2f325f0e331" + application_id = azuread_application.app_registration.id + allowed_member_types = ["User"] + description = "Allows access to view content safety violations." + display_name = "Safety Violation Admin" + value = "SafetyViolationAdmin" + role_id = "877166d4-eaa3-4fa6-8d79-e2f325f0e331" } resource "azuread_application_app_role" "app_registration_feedback_admin" { - application_id = azuread_application.app_registration.id - allowed_member_types = ["User"] - description = "Allows user to create new groups (if enabled)." - display_name = "Create Group" - value = "CreateGroups" - role_id = "3a614cbb-7f8b-47e1-8e55-5b3c4f71a1c8" + application_id = azuread_application.app_registration.id + allowed_member_types = ["User"] + description = "Allows user to create new groups (if enabled)." + display_name = "Create Group" + value = "CreateGroups" + role_id = "3a614cbb-7f8b-47e1-8e55-5b3c4f71a1c8" } resource "azuread_service_principal" "app_registration_sp" { - client_id = azuread_application.app_registration.client_id - use_existing = true - owners = [data.azuread_user.application_owner.object_id] + client_id = azuread_application.app_registration.client_id + use_existing = true + owners = [data.azuread_user.application_owner.object_id] } ################################################################# @@ -589,9 +643,9 @@ resource "azuread_app_role_assignment" "assignment_admin" { # Assign member to Admin Security Groups ################################################################# resource "azuread_group_member" "group_membership_admin" { - count = var.param_create_entra_security_groups ? 1 : 0 - group_object_id = azuread_group.simplechat_admins[0].object_id - member_object_id = data.azuread_user.application_owner.object_id + count = var.param_create_entra_security_groups ? 1 : 0 + group_object_id = azuread_group.simplechat_admins[0].object_id + member_object_id = data.azuread_user.application_owner.object_id } ################################################################# @@ -625,45 +679,48 @@ resource "azurerm_cosmosdb_account" "cosmos" { # --- Azure OpenAI Service (Cognitive Services) --- resource "azurerm_cognitive_account" "openai" { - count = var.param_use_existing_openai_instance ? 0 : 1 # Only create if not using existing - name = local.open_ai_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - kind = "OpenAI" - sku_name = "S0" # Standard tier - tags = local.common_tags + count = var.param_use_existing_openai_instance ? 0 : 1 # Only create if not using existing + name = local.open_ai_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kind = "OpenAI" + sku_name = "S0" # Standard tier + public_network_access_enabled = var.param_enable_private_networking ? false : true + tags = local.common_tags } # Data source for existing OpenAI instance data "azurerm_cognitive_account" "existing_openai" { - count = var.param_use_existing_openai_instance ? 1 : 0 + provider = azurerm.existing_openai + count = local.use_existing_openai_resource_metadata ? 1 : 0 name = var.param_existing_azure_openai_resource_name resource_group_name = var.param_existing_azure_openai_resource_group_name } # --- Document Intelligence Service (Cognitive Services) --- resource "azurerm_cognitive_account" "docintel" { - name = local.doc_intel_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - kind = "FormRecognizer" - sku_name = "S0" # Standard tier - custom_subdomain_name = local.doc_intel_name # Maps to --custom-domain - tags = local.common_tags + name = local.doc_intel_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + kind = "FormRecognizer" + sku_name = "S0" # Standard tier + custom_subdomain_name = local.doc_intel_name # Maps to --custom-domain + public_network_access_enabled = var.param_enable_private_networking ? false : true + tags = local.common_tags } # https://medium.com/expert-thinking/mastering-azure-search-with-terraform-a-how-to-guide-7edc3a6b1ee3 # --- Azure AI Search Service --- resource "azurerm_search_service" "search" { - name = local.search_service_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - sku = "basic" # Other options: standard, standard2, standard3 - replica_count = 1 - partition_count = 1 - semantic_search_sku = "standard" #other options: free - public_network_access_enabled = true # From script's public-network-access enabled - tags = local.common_tags + name = local.search_service_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + sku = "basic" # Other options: standard, standard2, standard3 + replica_count = 1 + partition_count = 1 + semantic_search_sku = "standard" #other options: free + public_network_access_enabled = var.param_enable_private_networking ? false : true + tags = local.common_tags } @@ -676,6 +733,7 @@ resource "azurerm_search_service" "search" { # Managed Identity RBAC # Cognitive Services Contributor on OpenAI resource "azurerm_role_assignment" "managed_identity_openai_contributor" { + count = local.enable_openai_rbac_assignments ? 1 : 0 scope = var.param_use_existing_openai_instance ? data.azurerm_cognitive_account.existing_openai[0].id : azurerm_cognitive_account.openai[0].id role_definition_name = "Cognitive Services Contributor" principal_id = azurerm_user_assigned_identity.id.principal_id @@ -683,6 +741,7 @@ resource "azurerm_role_assignment" "managed_identity_openai_contributor" { # Cognitive Services User on OpenAI resource "azurerm_role_assignment" "managed_identity_openai_user" { + count = local.enable_openai_rbac_assignments ? 1 : 0 scope = var.param_use_existing_openai_instance ? data.azurerm_cognitive_account.existing_openai[0].id : azurerm_cognitive_account.openai[0].id role_definition_name = "Cognitive Services User" principal_id = azurerm_user_assigned_identity.id.principal_id @@ -705,6 +764,7 @@ resource "azurerm_role_assignment" "managed_identity_storage_contributor" { # App Registration Service Principal RBAC # Cognitive Services OpenAI Contributor on OpenAI resource "azurerm_role_assignment" "app_reg_sp_openai_contributor" { + count = local.enable_openai_rbac_assignments ? 1 : 0 scope = var.param_use_existing_openai_instance ? data.azurerm_cognitive_account.existing_openai[0].id : azurerm_cognitive_account.openai[0].id role_definition_name = "Cognitive Services OpenAI Contributor" principal_id = azuread_service_principal.app_registration_sp.object_id @@ -712,6 +772,7 @@ resource "azurerm_role_assignment" "app_reg_sp_openai_contributor" { # Cognitive Services OpenAI User on OpenAI resource "azurerm_role_assignment" "app_reg_sp_openai_user" { + count = local.enable_openai_rbac_assignments ? 1 : 0 scope = var.param_use_existing_openai_instance ? data.azurerm_cognitive_account.existing_openai[0].id : azurerm_cognitive_account.openai[0].id role_definition_name = "Cognitive Services OpenAI User" principal_id = azuread_service_principal.app_registration_sp.object_id @@ -720,6 +781,7 @@ resource "azurerm_role_assignment" "app_reg_sp_openai_user" { # App Service System Managed Identity RBAC # Cognitive Services OpenAI User on OpenAI resource "azurerm_role_assignment" "app_service_smi_openai_user" { + count = local.enable_openai_rbac_assignments ? 1 : 0 scope = var.param_use_existing_openai_instance ? data.azurerm_cognitive_account.existing_openai[0].id : azurerm_cognitive_account.openai[0].id role_definition_name = "Cognitive Services OpenAI User" principal_id = azurerm_linux_web_app.app.identity[0].principal_id diff --git a/deployers/terraform/private_networking.tf b/deployers/terraform/private_networking.tf new file mode 100644 index 000000000..1df57f132 --- /dev/null +++ b/deployers/terraform/private_networking.tf @@ -0,0 +1,329 @@ +#################################################################################################### +# File: private_networking.tf +# Description: Terraform private networking support for the Simple Chat deployment. +# Author: Microsoft Federal +# Created: 2026-Mar-11 +#################################################################################################### + +provider "azapi" {} + +variable "param_enable_private_networking" { + description = "Set to true to enable private endpoints and App Service VNet integration." + type = bool + default = false +} + +variable "param_existing_virtual_network_id" { + description = "Optional existing virtual network resource ID to reuse when private networking is enabled." + type = string + default = "" +} + +variable "param_existing_app_service_subnet_id" { + description = "Optional existing subnet resource ID to use for App Service VNet integration." + type = string + default = "" +} + +variable "param_existing_private_endpoint_subnet_id" { + description = "Optional existing subnet resource ID to use for private endpoints." + type = string + default = "" +} + +variable "param_private_network_address_space" { + description = "Address spaces to use when Terraform creates a new virtual network for private networking." + type = list(string) + default = ["10.0.0.0/21"] +} + +variable "param_app_service_integration_subnet_prefixes" { + description = "Address prefixes to use when Terraform creates the App Service integration subnet." + type = list(string) + default = ["10.0.0.0/24"] +} + +variable "param_private_endpoint_subnet_prefixes" { + description = "Address prefixes to use when Terraform creates the private endpoint subnet." + type = list(string) + default = ["10.0.2.0/24"] +} + +variable "param_private_dns_zone_configs" { + description = "Optional per-zone configuration for private DNS create or reuse behavior. Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites." + type = map(object({ + zone_resource_id = optional(string, "") + create_vnet_link = optional(bool, true) + })) + default = {} + + validation { + condition = length(setsubtract( + toset(keys(var.param_private_dns_zone_configs)), + toset(["keyVault", "cosmosDb", "containerRegistry", "aiSearch", "blobStorage", "cognitiveServices", "openAi", "webSites"]) + )) == 0 + error_message = "param_private_dns_zone_configs only supports these keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites." + } +} + +locals { + inferred_virtual_network_id = var.param_existing_app_service_subnet_id != "" ? regexreplace(var.param_existing_app_service_subnet_id, "/subnets/[^/]+$", "") : (var.param_existing_private_endpoint_subnet_id != "" ? regexreplace(var.param_existing_private_endpoint_subnet_id, "/subnets/[^/]+$", "") : "") + resolved_existing_virtual_network_id = var.param_existing_virtual_network_id != "" ? var.param_existing_virtual_network_id : local.inferred_virtual_network_id + use_existing_private_network = var.param_enable_private_networking && (local.resolved_existing_virtual_network_id != "" || var.param_existing_app_service_subnet_id != "" || var.param_existing_private_endpoint_subnet_id != "") + + private_dns_zone_names = var.global_which_azure_platform == "AzureUSGovernment" ? { + aiSearch = "privatelink.search.azure.us" + blobStorage = "privatelink.blob.core.usgovcloudapi.net" + cognitiveServices = "privatelink.cognitiveservices.azure.us" + containerRegistry = "privatelink.azurecr.us" + cosmosDb = "privatelink.documents.azure.us" + keyVault = "privatelink.vaultcore.azure.us" + openAi = "privatelink.openai.azure.us" + webSites = "privatelink.azurewebsites.us" + } : { + aiSearch = "privatelink.search.windows.net" + blobStorage = "privatelink.blob.core.windows.net" + cognitiveServices = "privatelink.cognitiveservices.azure.com" + containerRegistry = "privatelink.azurecr.io" + cosmosDb = "privatelink.documents.azure.com" + keyVault = "privatelink.vaultcore.azure.net" + openAi = "privatelink.openai.azure.com" + webSites = "privatelink.azurewebsites.net" + } + + private_dns_zone_link_suffixes = { + aiSearch = "searchService" + blobStorage = "storage" + cognitiveServices = "docIntelService" + containerRegistry = "acr" + cosmosDb = "cosmosDb" + keyVault = "kv" + openAi = "openAiService" + webSites = "webApp" + } + + resolved_private_dns_zone_configs = { + for zone_key, zone_name in local.private_dns_zone_names : zone_key => { + name = zone_name + zone_resource_id = try(var.param_private_dns_zone_configs[zone_key].zone_resource_id, "") + create_vnet_link = try(var.param_private_dns_zone_configs[zone_key].create_vnet_link, true) + link_name = lower("${var.param_base_name}-${var.param_environment}-${local.private_dns_zone_link_suffixes[zone_key]}-pe-dnszonelink") + } + } +} + +resource "terraform_data" "private_networking_validation" { + input = { + private_networking_enabled = var.param_enable_private_networking + } + + lifecycle { + precondition { + condition = !var.param_enable_private_networking || !local.use_existing_private_network || (var.param_existing_app_service_subnet_id != "" && var.param_existing_private_endpoint_subnet_id != "") + error_message = "When reusing an existing virtual network for private networking, both param_existing_app_service_subnet_id and param_existing_private_endpoint_subnet_id must be provided." + } + + precondition { + condition = !var.param_enable_private_networking || !local.use_existing_private_network || local.resolved_existing_virtual_network_id != "" + error_message = "When reusing an existing virtual network for private networking, Terraform must be able to resolve the virtual network ID from param_existing_virtual_network_id or one of the provided subnet IDs." + } + } +} + +resource "azurerm_virtual_network" "private" { + count = var.param_enable_private_networking && !local.use_existing_private_network ? 1 : 0 + name = lower("${var.param_base_name}-${var.param_environment}-vnet") + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + address_space = var.param_private_network_address_space + tags = local.common_tags + + depends_on = [terraform_data.private_networking_validation] +} + +resource "azurerm_subnet" "app_service_integration" { + count = var.param_enable_private_networking && !local.use_existing_private_network ? 1 : 0 + name = "AppServiceIntegration" + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.private[0].name + address_prefixes = var.param_app_service_integration_subnet_prefixes + + private_endpoint_network_policies = "Enabled" + private_link_service_network_policies_enabled = true + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + } + } +} + +resource "azurerm_subnet" "private_endpoints" { + count = var.param_enable_private_networking && !local.use_existing_private_network ? 1 : 0 + name = "PrivateEndpoints" + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.private[0].name + address_prefixes = var.param_private_endpoint_subnet_prefixes + + private_endpoint_network_policies = "Enabled" + private_link_service_network_policies_enabled = true +} + +locals { + resolved_virtual_network_id = var.param_enable_private_networking ? (local.use_existing_private_network ? local.resolved_existing_virtual_network_id : azurerm_virtual_network.private[0].id) : "" + resolved_app_service_subnet_id = var.param_enable_private_networking ? (local.use_existing_private_network ? var.param_existing_app_service_subnet_id : azurerm_subnet.app_service_integration[0].id) : null + resolved_private_endpoint_subnet_id = var.param_enable_private_networking ? (local.use_existing_private_network ? var.param_existing_private_endpoint_subnet_id : azurerm_subnet.private_endpoints[0].id) : null +} + +resource "azurerm_private_dns_zone" "managed" { + for_each = { + for zone_key, zone in local.resolved_private_dns_zone_configs : zone_key => zone + if var.param_enable_private_networking && zone.zone_resource_id == "" + } + + name = each.value.name + resource_group_name = azurerm_resource_group.rg.name +} + +locals { + private_dns_zone_ids = merge( + { + for zone_key, zone in local.resolved_private_dns_zone_configs : zone_key => zone.zone_resource_id + if zone.zone_resource_id != "" + }, + { + for zone_key, zone in azurerm_private_dns_zone.managed : zone_key => zone.id + } + ) +} + +resource "azapi_resource" "private_dns_zone_link" { + for_each = { + for zone_key, zone in local.resolved_private_dns_zone_configs : zone_key => zone + if var.param_enable_private_networking && zone.create_vnet_link + } + + type = "Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01" + name = each.value.link_name + parent_id = each.value.zone_resource_id != "" ? each.value.zone_resource_id : azurerm_private_dns_zone.managed[each.key].id + location = "global" + schema_validation_enabled = false + + body = { + properties = { + registrationEnabled = false + virtualNetwork = { + id = local.resolved_virtual_network_id + } + } + } + + depends_on = [ + terraform_data.private_networking_validation, + azurerm_virtual_network.private, + azurerm_private_dns_zone.managed, + ] +} + +locals { + openai_private_endpoint_resource_id = var.param_use_existing_openai_instance ? (local.use_existing_openai_resource_metadata ? data.azurerm_cognitive_account.existing_openai[0].id : "") : azurerm_cognitive_account.openai[0].id + + private_endpoint_targets = var.param_enable_private_networking ? merge( + { + keyVault = { + name = "kv" + resource_id = azurerm_key_vault.kv.id + subresource = "vault" + dns_zone_key = "keyVault" + } + cosmosDb = { + name = "cosmosdb" + resource_id = azurerm_cosmosdb_account.cosmos.id + subresource = "sql" + dns_zone_key = "cosmosDb" + } + containerRegistry = { + name = "acr" + resource_id = data.azurerm_container_registry.acrregistry.id + subresource = "registry" + dns_zone_key = "containerRegistry" + } + aiSearch = { + name = "search" + resource_id = azurerm_search_service.search.id + subresource = "searchService" + dns_zone_key = "aiSearch" + } + blobStorage = { + name = "storage" + resource_id = azurerm_storage_account.sa.id + subresource = "blob" + dns_zone_key = "blobStorage" + } + cognitiveServices = { + name = "docintel" + resource_id = azurerm_cognitive_account.docintel.id + subresource = "account" + dns_zone_key = "cognitiveServices" + } + webSites = { + name = "webapp" + resource_id = azurerm_linux_web_app.app.id + subresource = "sites" + dns_zone_key = "webSites" + } + }, + local.openai_private_endpoint_resource_id != "" ? { + openAi = { + name = "openai" + resource_id = local.openai_private_endpoint_resource_id + subresource = "account" + dns_zone_key = "openAi" + } + } : {} + ) : {} +} + +resource "azurerm_private_endpoint" "service" { + for_each = local.private_endpoint_targets + + name = lower("${var.param_base_name}-${var.param_environment}-${each.value.name}-pe") + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = local.resolved_private_endpoint_subnet_id + tags = local.common_tags + + private_service_connection { + name = lower("${var.param_base_name}-${var.param_environment}-${each.value.name}-psc") + private_connection_resource_id = each.value.resource_id + subresource_names = [each.value.subresource] + is_manual_connection = false + } + + private_dns_zone_group { + name = lower("${var.param_base_name}-${var.param_environment}-${each.value.name}-dns") + private_dns_zone_ids = [local.private_dns_zone_ids[each.value.dns_zone_key]] + } + + depends_on = [ + terraform_data.private_networking_validation, + azapi_resource.private_dns_zone_link, + ] +} + +output "virtual_network_id" { + description = "The virtual network ID used for private networking, if enabled." + value = local.resolved_virtual_network_id +} + +output "app_service_subnet_id" { + description = "The App Service VNet integration subnet ID used for private networking, if enabled." + value = local.resolved_app_service_subnet_id +} + +output "private_endpoint_subnet_id" { + description = "The private endpoint subnet ID used for private networking, if enabled." + value = local.resolved_private_endpoint_subnet_id +} diff --git a/docs/explanation/features/ACR_BUILD_WORKFLOW_FOR_DEPLOYERS.md b/docs/explanation/features/ACR_BUILD_WORKFLOW_FOR_DEPLOYERS.md new file mode 100644 index 000000000..ae74459da --- /dev/null +++ b/docs/explanation/features/ACR_BUILD_WORKFLOW_FOR_DEPLOYERS.md @@ -0,0 +1,74 @@ +# ACR Build Workflow for Deployers + +Fixed/Implemented in version: **0.237.025** + +## Overview + +This update aligns the deployment experience around Azure Container Registry Tasks so deployers can publish the application image in ACR without requiring a local Docker daemon. + +## Scope + +This change affects: + +- AZD/Bicep deployment flow in `deployers/azure.yaml` +- Azure CLI deployment guidance in `deployers/azurecli/deploy-simplechat.ps1` and `deployers/azurecli/README.md` +- Azure environment bootstrap guidance in `deployers/Initialize-AzureEnvironment.ps1` +- Terraform deployment guidance in `deployers/terraform/ReadMe.md` + +## Technical Details + +### AZD / Bicep + +The `predeploy` hook in `deployers/azure.yaml` now uses: + +- `az acr build` +- `application/single_app/Dockerfile` +- a timestamped tag and `latest` + +This removes the previous dependency on: + +- local `docker build` +- local `docker tag` +- local `docker push` + +### Azure CLI + +The Azure CLI deployer already supports building the image in ACR through configurable settings in `deploy-simplechat.ps1`. + +### Environment Bootstrap + +`deployers/Initialize-AzureEnvironment.ps1` now reflects that ACR is also used for deployer-driven image builds, not only for GitHub Actions secrets. + +The script now emits: + +- ACR resource identifiers +- ACR login server and credentials +- Azure OpenAI values +- An example `az acr build` command + +### Terraform + +Terraform still consumes an already-published image tag through `image_name`, but the deployment guidance now recommends `az acr build` as the primary way to publish that image. + +## Usage + +Example command from the repository root: + +```azurecli +az acr build --registry --file application/single_app/Dockerfile --image simplechat:latest . +``` + +## Benefits + +- No local Docker daemon required for AZD/Bicep deployments +- More consistent deployment behavior across deployment paths +- Better fit for locked-down administrator workstations +- Keeps container builds inside Azure infrastructure + +## Validation + +Validation completed with: + +- workspace error checks on the updated YAML and Markdown files +- review of the updated deployment flow in `deployers/azure.yaml` +- confirmation that Terraform remains image-tag-driven while its guidance now matches the ACR build approach diff --git a/docs/explanation/features/AZURECLI_AOAI_MODEL_DEPLOYMENTS.md b/docs/explanation/features/AZURECLI_AOAI_MODEL_DEPLOYMENTS.md new file mode 100644 index 000000000..6cda4ce85 --- /dev/null +++ b/docs/explanation/features/AZURECLI_AOAI_MODEL_DEPLOYMENTS.md @@ -0,0 +1,54 @@ +# Azure CLI Azure OpenAI Model Deployments + +Fixed/Implemented in version: **0.237.048** + +## Overview + +This update extends the Azure CLI deployer so it can create the default Azure OpenAI GPT and embedding model deployments, not just the Azure OpenAI account resource. + +## Scope + +This change affects: + +- `deployers/azurecli/deploy-simplechat.ps1` +- `deployers/azurecli/README.md` +- `functional_tests/test_azurecli_aoai_model_deployments.py` + +## Technical Details + +### Deployment Behavior + +The Azure CLI deployer can now: + +- create model deployments for a newly created Azure OpenAI account +- create missing model deployments for a reused Azure OpenAI account +- skip model deployment creation when `param_DeployAzureOpenAiModels = $false` +- prompt for `Standard`, `DatazoneStandard`, or `GlobalStandard` in Azure Commercial when no deployment type is preconfigured +- retry a failed model deployment with a different deployment type when quota or regional availability blocks the first attempt + +### Default Model Definitions + +The deployer now includes cloud-aware defaults that mirror the main infrastructure deployment path: + +- GPT deployment name: `gpt-4o` +- GPT model version: `2024-11-20` in Azure Commercial, `2024-05-13` in Azure Government +- Embedding deployment: `text-embedding-3-small` by default, with `text-embedding-ada-002` retained for `usgovvirginia` +- Deployment capacities: `100` for GPT and `80` for embeddings + +### Idempotency + +Before creating a model deployment, the script checks whether that deployment name already exists on the target Azure OpenAI account. Existing deployments are left in place so the script can be rerun safely. + +## Benefits + +- Reduces manual Azure OpenAI setup after Azure CLI deployments +- Aligns the Azure CLI deployer more closely with the Bicep deployer behavior +- Gives operators a direct retry path when the first deployment type hits quota or availability issues + +## Validation + +Validation completed with: + +- workspace error checks on the updated PowerShell and Markdown files +- review of the Azure CLI model deployment command flow in `deployers/azurecli/deploy-simplechat.ps1` +- regression coverage in `functional_tests/test_azurecli_aoai_model_deployments.py` \ No newline at end of file diff --git a/docs/explanation/features/AZURECLI_PRIVATE_NETWORKING_PARITY.md b/docs/explanation/features/AZURECLI_PRIVATE_NETWORKING_PARITY.md new file mode 100644 index 000000000..c1fbd2202 --- /dev/null +++ b/docs/explanation/features/AZURECLI_PRIVATE_NETWORKING_PARITY.md @@ -0,0 +1,148 @@ +# Azure CLI Private Networking Parity + +Fixed/Implemented in version: **0.237.023** + +## Overview + +This feature extends the Azure CLI deployer so it can support the same core private networking flow already available in the Bicep deployer for the services provisioned by `deploy-simplechat.ps1`. + +The Azure CLI deployment script can now: + +- Create a new deployment VNet when private networking is enabled +- Reuse an existing VNet by supplying existing subnet resource IDs +- Require separate App Service integration and private endpoint subnets when reusing enterprise networking +- Submit the application container build to Azure Container Registry with `az acr build` instead of requiring a local Docker build step in the Azure CLI deployment path +- Create private endpoints for the main Simple Chat dependencies +- Create private DNS zones automatically when customer-managed zones are not supplied +- Reuse existing private DNS zones by resource ID +- Create or skip private DNS VNet links on a per-zone basis +- Disable public network access on the private-networked resources after private endpoint configuration + +## Dependencies + +- Azure CLI with support for networking, web app, private endpoint, and private DNS commands +- PowerShell +- Rights to manage: + - Virtual networks and subnets + - Private endpoints + - Private DNS zones + - Private DNS VNet links + - Target application resources such as App Service, Storage, Search, Key Vault, Cosmos DB, and Cognitive Services + +## Technical Specifications + +### Architecture Overview + +The Azure CLI script now follows the same general private networking shape as the Bicep deployer: + +1. Dedicated subnet for App Service regional VNet integration +2. Dedicated subnet for private endpoints +3. Private DNS zones per service, with optional zone reuse +4. VNet links per private DNS zone, with optional suppression when central IT manages links separately +5. Private endpoints for: + - Key Vault + - Cosmos DB + - Azure Container Registry + - Azure AI Search + - Azure OpenAI + - Document Intelligence + - Storage + - App Service + +### Configuration Options + +Key script settings added or expanded in `deploy-simplechat.ps1`: + +- `param_BuildContainerImageWithAcr` +- `param_DockerfilePath` +- `param_DockerBuildContextPath` +- `param_PublishLatestImageTag` +- `param_EnablePrivateNetworking` +- `param_ExistingVirtualNetworkId` +- `param_ExistingAppServiceSubnetId` +- `param_ExistingPrivateEndpointSubnetId` +- `param_PrivateNetworkAddressPrefixes` +- `param_AppServiceIntegrationSubnetAddressPrefixes` +- `param_PrivateEndpointSubnetAddressPrefixes` +- `param_AllowedIpAddresses` +- `param_PrivateDnsZoneConfigs` +- `param_ShowPrivateNetworkingChecklist` +- `param_ConfirmPrivateNetworkingPlan` + +### File Structure + +Primary files involved: + +- `deployers/azurecli/deploy-simplechat.ps1` +- `deployers/azurecli/destroy-simplechat.ps1` +- `deployers/azurecli/README.md` +- `application/single_app/config.py` + +## Usage Instructions + +### New VNet Flow + +Set: + +- `param_EnablePrivateNetworking = $true` +- Leave the existing VNet and subnet ID values blank + +The script will create: + +- A VNet +- An `AppServiceIntegration` subnet +- A `PrivateEndpoints` subnet +- Private DNS zones when overrides are not provided +- Private endpoints for the supported services + +### Existing VNet Flow + +Set: + +- `param_EnablePrivateNetworking = $true` +- `param_ExistingVirtualNetworkId` (optional if both subnet IDs already imply the same VNet) +- `param_ExistingAppServiceSubnetId` +- `param_ExistingPrivateEndpointSubnetId` + +The script validates that: + +- Both subnet IDs are provided +- Both subnets belong to the same VNet +- The App Service subnet is delegated to `Microsoft.Web/serverFarms` + +### Private DNS Zone Reuse + +Use `param_PrivateDnsZoneConfigs` to reuse enterprise-managed zones and optionally skip VNet link creation. + +### Container Build Flow + +By default, the Azure CLI deployer now submits the container build to ACR Tasks before the web app is created. + +This allows administrators to use the Azure CLI deployment path without building the image locally first, as long as: + +- the target ACR already exists +- the deployment identity can run `az acr build` +- the configured build context includes the repository files required by `application/single_app/Dockerfile` + +## Testing and Validation + +### Validation Approach + +Validation performed for this change included: + +- PowerShell parser and workspace error checks on the updated script +- Review of the Azure CLI deployer README and supporting documentation +- Cross-checking the Azure CLI logic against the Bicep deployer private networking structure + +### Known Limitations + +- The Azure CLI deployer now covers the core private networking scenario for the services it provisions, but the Bicep deployer remains the most complete deployment path across the repository. +- Azure OpenAI private endpoint automation still requires a resolvable Azure resource ID. Endpoint-only reuse does not provide enough metadata for that automation. +- Private networking in enterprise environments still depends on customer DNS architecture, routing, and access controls outside the script. + +## Cleanup Behavior + +- The Azure CLI destroy script now better reflects the private networking deployment model. +- Resource group deletion removes networking resources only when they were created inside the deployment resource group. +- Reused enterprise VNets, shared subnets, and customer-managed private DNS zones outside the deployment resource group are intentionally left in place. +- The cleanup script also removes the full set of Entra security groups created by the Azure CLI deployer, including the public workspace creation group. diff --git a/docs/explanation/features/TERRAFORM_PRIVATE_NETWORKING_PARITY.md b/docs/explanation/features/TERRAFORM_PRIVATE_NETWORKING_PARITY.md new file mode 100644 index 000000000..cfbf6d7b5 --- /dev/null +++ b/docs/explanation/features/TERRAFORM_PRIVATE_NETWORKING_PARITY.md @@ -0,0 +1,67 @@ +# Terraform Private Networking Parity + +Fixed/Implemented in version: **0.237.020** + +## Overview + +This feature brings the Terraform deployer closer to the Bicep deployer for private networking scenarios. + +Terraform now supports: + +- Creating a deployment VNet when private networking is enabled +- Reusing an existing VNet and existing subnets by resource ID +- Creating private endpoints for the core Simple Chat dependencies +- Creating private DNS zones automatically when customer-managed zones are not supplied +- Reusing existing private DNS zones by resource ID +- Creating per-zone VNet links, with the option to suppress link creation when networking teams manage that separately + +## Dependencies + +- Terraform `azurerm` provider +- Terraform `azuread` provider +- Terraform `azapi` provider for cross-scope private DNS VNet links +- Existing Azure OpenAI metadata when Azure OpenAI private endpoint automation is required for a reused resource + +## Architecture Overview + +The Terraform implementation now mirrors the Bicep private networking shape by using: + +1. A dedicated App Service integration subnet +2. A dedicated private endpoint subnet +3. Private endpoints for Key Vault, Cosmos DB, Storage, Azure AI Search, Azure OpenAI, Document Intelligence, Container Registry, and the App Service +4. Private DNS zones for each private-link-enabled dependency +5. Optional reuse of enterprise-managed private DNS zones + +## Configuration + +Key Terraform variables: + +- `param_enable_private_networking` +- `param_existing_virtual_network_id` +- `param_existing_app_service_subnet_id` +- `param_existing_private_endpoint_subnet_id` +- `param_private_dns_zone_configs` +- `param_existing_azure_openai_resource_name` +- `param_existing_azure_openai_resource_group_name` +- `param_existing_azure_openai_subscription_id` + +## Usage Notes + +- If you reuse an existing VNet, provide both existing subnet IDs. +- If central IT manages private DNS VNet links, set `create_vnet_link = false` for the relevant zone entries. +- Endpoint-only Azure OpenAI reuse still supports application configuration, but it does not provide enough Azure resource metadata for Terraform to automate the Azure OpenAI private endpoint. + +## Validation + +Validation completed with: + +- `terraform fmt` +- `terraform init -upgrade` +- `terraform validate` + +## Related Files + +- `deployers/terraform/main.tf` +- `deployers/terraform/private_networking.tf` +- `deployers/terraform/ReadMe.md` +- `application/single_app/config.py` diff --git a/docs/explanation/features/v0.237.001/PRIVATE_NETWORKING_SUPPORT.md b/docs/explanation/features/v0.237.001/PRIVATE_NETWORKING_SUPPORT.md index 5379b73d5..055c94194 100644 --- a/docs/explanation/features/v0.237.001/PRIVATE_NETWORKING_SUPPORT.md +++ b/docs/explanation/features/v0.237.001/PRIVATE_NETWORKING_SUPPORT.md @@ -101,6 +101,14 @@ az deployment sub create \ | `APP_SUBNET_PREFIX` | App Service subnet prefix | `10.0.1.0/24` | | `PRIVATE_ENDPOINT_SUBNET_PREFIX` | Private endpoints subnet prefix | `10.0.2.0/24` | +### Deployment Runner Access + +When private networking is enabled, `allowedIpAddresses` is optional and is only intended for the machine running `azd up` when that machine must temporarily reach Cosmos DB or Azure Container Registry over a public path during deployment. + +In enterprise environments, leave `allowedIpAddresses` blank when the deployment runner is already on a connected corporate network, VPN, jump host, or build agent with private connectivity and private DNS resolution to the deployed services. + +If a value is required, provide the public egress IP address or CIDR that Azure sees for the deployment runner. This is often a company NAT or proxy address rather than the laptop's local private address. + ## Deployment Hooks ### Post-Provision Hook @@ -161,6 +169,7 @@ After deployment, validate: 1. **DNS Propagation Delay**: Allow 5-10 minutes for DNS changes to propagate 2. **VNet Peering**: Additional configuration needed if peering with existing VNets 3. **On-Premises Connectivity**: Requires ExpressRoute or VPN Gateway for hybrid scenarios +4. **Deployment Runner Reachability**: If post-provision steps cannot reach Cosmos DB or Azure Container Registry, either ensure the deployment runner has private connectivity and private DNS resolution or temporarily provide the runner's public egress IP through `allowedIpAddresses` ## Files Modified diff --git a/docs/explanation/fixes/v0.237.049/AZD_WINDOWS_POSTPROVISION_HOOK_FIX.md b/docs/explanation/fixes/v0.237.049/AZD_WINDOWS_POSTPROVISION_HOOK_FIX.md new file mode 100644 index 000000000..a62d39eec --- /dev/null +++ b/docs/explanation/fixes/v0.237.049/AZD_WINDOWS_POSTPROVISION_HOOK_FIX.md @@ -0,0 +1,35 @@ +# AZD Windows Postprovision Hook Fix + +Fixed/Implemented in version: **0.237.049** + +## Issue Description + +Windows `azd provision` runs were failing after infrastructure creation with an invalid hook configuration error for `postprovision`. + +## Root Cause Analysis + +`deployers/azure.yaml` defined a Windows hook for `preprovision`, but `postprovision`, `predeploy`, and `postup` only defined `posix` hook blocks. On Windows, AZD requires a valid `windows` hook definition with a `run` command for hooks that will execute on that platform. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_azd_windows_hooks.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added a native `windows` `postprovision` hook that performs Cosmos DB role assignment, installs Python dependencies, runs post-configuration, and restarts the web app. +- Added native `windows` `predeploy` and `postup` hooks so the Windows deployment path remains valid beyond provisioning. +- Added a regression test that checks for required Windows hook coverage in `deployers/azure.yaml`. + +## Validation + +- Workspace validation confirms the updated YAML, Python test, and version metadata are syntactically valid. +- Functional regression coverage confirms the Windows hook definitions are present in `deployers/azure.yaml`. + +## Impact Analysis + +- Fixes Windows AZD deployments that previously failed immediately after provisioning completed. +- Prevents the next AZD lifecycle stage from failing for the same missing Windows hook pattern. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.050/AZD_WINDOWS_RGNAME_RESOLUTION_FIX.md b/docs/explanation/fixes/v0.237.050/AZD_WINDOWS_RGNAME_RESOLUTION_FIX.md new file mode 100644 index 000000000..d37f17236 --- /dev/null +++ b/docs/explanation/fixes/v0.237.050/AZD_WINDOWS_RGNAME_RESOLUTION_FIX.md @@ -0,0 +1,36 @@ +# AZD Windows Resource Group Name Resolution Fix + +Fixed/Implemented in version: **0.237.050** + +## Issue Description + +Windows AZD hooks in `deployers/azure.yaml` could fail when `var_rgName` was stale or missing, which caused Cosmos DB role assignment, web app restart, and post-up resource updates to target a resource group that did not exist. + +## Root Cause Analysis + +The Windows hook implementations relied directly on the AZD environment variable `var_rgName` without validating it against Azure or falling back to discover the actual resource group for the deployed resources. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_azd_windows_hooks.py` +- `application/single_app/config.py` +- `docs/explanation/release_notes.md` + +### Code Changes Summary + +- Added a `Resolve-ResourceGroupName` helper to the Windows `postprovision`, `predeploy`, and `postup` hooks. +- The helper validates `var_rgName`, falls back to Azure lookups through Cosmos DB and the web app, and writes the resolved value back to `var_rgName` for downstream steps such as `postconfig.py`. +- Extended regression coverage to verify the fallback logic remains present in `deployers/azure.yaml`. + +## Validation + +- Workspace validation confirms the updated YAML, test, documentation, and version metadata are syntactically valid. +- Functional regression coverage confirms the Windows hooks now include resource group fallback logic. + +## Impact Analysis + +- Fixes Windows AZD deployment failures caused by stale environment state. +- Keeps the Windows hook flow working even when the original resource group output is missing or out of date. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.051/AZD_WINDOWS_SUBSCRIPTION_TARGETING_FIX.md b/docs/explanation/fixes/v0.237.051/AZD_WINDOWS_SUBSCRIPTION_TARGETING_FIX.md new file mode 100644 index 000000000..2ba93f058 --- /dev/null +++ b/docs/explanation/fixes/v0.237.051/AZD_WINDOWS_SUBSCRIPTION_TARGETING_FIX.md @@ -0,0 +1,23 @@ +# AZD Windows Subscription Targeting Fix + +Fixed in version: 0.237.051 + +## Issue + +Windows `azd postprovision`, `predeploy`, and `postup` hooks could fail to resolve the deployment resource group even when the resource group existed. + +## Root Cause + +The Windows hook fallback logic validated `var_rgName` and queried Cosmos DB and Web App metadata by using the Azure CLI's current default subscription. If the local Azure CLI context was pointed at a different subscription than the active AZD environment, the helper incorrectly concluded that the resource group did not exist. + +## Technical Details + +- Updated the Windows hook logic in `deployers/azure.yaml` to resolve the target subscription from `AZURE_SUBSCRIPTION_ID` or `var_subscriptionId` before falling back to `az account show`. +- Added `--subscription $subscriptionId` to the Windows Azure CLI calls used for resource-group validation, Cosmos DB lookup, Web App lookup, role-definition lookup, web app restart/stop/start, ACR build, and private networking updates. +- Updated the Windows hook regression test to verify explicit subscription targeting. +- Updated `application/single_app/config.py` to version `0.237.051`. + +## Validation + +- Functional test: `functional_tests/test_azd_windows_hooks.py` +- Expected outcome: Windows AZD hooks resolve and operate on the deployment resources even when the local Azure CLI default subscription is different from the AZD target subscription. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.052/POSTCONFIG_AZURECLI_CREDENTIAL_FIX.md b/docs/explanation/fixes/v0.237.052/POSTCONFIG_AZURECLI_CREDENTIAL_FIX.md new file mode 100644 index 000000000..e91021972 --- /dev/null +++ b/docs/explanation/fixes/v0.237.052/POSTCONFIG_AZURECLI_CREDENTIAL_FIX.md @@ -0,0 +1,30 @@ +# Postconfig Azure CLI Credential Fix + +Fixed/Implemented in version: **0.237.052** + +## Issue Description + +Windows `azd provision` runs could still fail in the `postprovision` hook after Cosmos DB role assignments succeeded. + +## Root Cause Analysis + +The Windows hook granted Cosmos DB permissions to the signed-in Azure CLI user, but `deployers/bicep/postconfig.py` used `DefaultAzureCredential`. On developer workstations, that credential chain can prefer a different identity source than the Azure CLI session, which caused Cosmos DB to reject the metadata read request with a native RBAC authorization error. + +## Technical Details + +### Files Modified + +- `deployers/bicep/postconfig.py` +- `functional_tests/test_postconfig_azurecli_credential.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Replaced `DefaultAzureCredential` with `AzureCliCredential` in `deployers/bicep/postconfig.py`. +- Kept the deployment-time token preflight so authentication failures still surface early. +- Added regression coverage to ensure the post-configuration script continues using the Azure CLI credential path. + +## Validation + +- Functional test: `functional_tests/test_postconfig_azurecli_credential.py` +- Expected outcome: the AZD Windows `postprovision` hook uses the same Azure CLI identity for Cosmos DB and Key Vault access that the hook granted permissions to earlier in the workflow. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.053/AZD_POSTPROVISION_COSMOS_KEY_FALLBACK_FIX.md b/docs/explanation/fixes/v0.237.053/AZD_POSTPROVISION_COSMOS_KEY_FALLBACK_FIX.md new file mode 100644 index 000000000..b273e9291 --- /dev/null +++ b/docs/explanation/fixes/v0.237.053/AZD_POSTPROVISION_COSMOS_KEY_FALLBACK_FIX.md @@ -0,0 +1,33 @@ +# AZD Postprovision Cosmos Key Fallback Fix + +Fixed/Implemented in version: **0.237.053** + +## Issue Description + +`azd up` and `azd provision` could repeatedly fail in the `postprovision` hook when `deployers/bicep/postconfig.py` attempted to read Cosmos DB settings through native RBAC before the data-plane role assignment was fully effective. + +## Root Cause Analysis + +The post-provision hook granted Cosmos DB data-plane access and then immediately invoked `postconfig.py`. Even when the role assignment command succeeded, Cosmos DB RBAC propagation could lag behind the next metadata read, causing the deployment to fail nondeterministically. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `deployers/bicep/postconfig.py` +- `functional_tests/test_postconfig_azurecli_credential.py` +- `functional_tests/test_azd_windows_hooks.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Updated the AZD postprovision hooks to resolve the Cosmos DB primary key before running `postconfig.py`. +- Updated `postconfig.py` to use the deployment-time Cosmos DB key when present, while retaining Azure CLI credentials for Key Vault access and for Cosmos DB fallback behavior. +- Added regression coverage so the postconfig script and Windows hook definitions continue to include the deployment-time Cosmos key path. + +## Validation + +- Functional test: `functional_tests/test_postconfig_azurecli_credential.py` +- Functional test: `functional_tests/test_azd_windows_hooks.py` +- Expected outcome: AZD postprovision runs succeed repeatedly without depending on Cosmos DB RBAC propagation timing for the deployment-time settings update. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.054/AZD_POSTPROVISION_COSMOS_FIREWALL_RUNNER_ACCESS_FIX.md b/docs/explanation/fixes/v0.237.054/AZD_POSTPROVISION_COSMOS_FIREWALL_RUNNER_ACCESS_FIX.md new file mode 100644 index 000000000..ff9978d65 --- /dev/null +++ b/docs/explanation/fixes/v0.237.054/AZD_POSTPROVISION_COSMOS_FIREWALL_RUNNER_ACCESS_FIX.md @@ -0,0 +1,30 @@ +# AZD Postprovision Cosmos Firewall Runner Access Fix + +Fixed/Implemented in version: **0.237.054** + +## Issue Description + +`azd provision` and `azd up` could still fail in `postprovision` after the Cosmos DB key fallback was added, because the deployment runner itself was not allowed through the Cosmos DB firewall. + +## Root Cause Analysis + +When private networking is enabled and the deployment runner is outside the private network path, Cosmos DB can block the local post-deployment settings update even when the script uses the primary key. The request still originates from the runner's public egress IP and must be allowed by the Cosmos DB firewall. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_azd_windows_hooks.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added postprovision helper logic to detect the deployment runner public IP through `api.ipify.org`. +- Added hook logic to merge that IP into the existing Cosmos DB IP rules before `postconfig.py` connects. +- Kept the Cosmos DB key fallback so the deployment remains independent of Cosmos RBAC propagation timing. + +## Validation + +- Functional test: `functional_tests/test_azd_windows_hooks.py` +- Expected outcome: repeated `azd up` and `azd provision` runs can seed Cosmos DB settings from a public deployment runner without manual Cosmos firewall edits. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.055/AZD_POSTPROVISION_MFA_COSMOS_GUIDANCE_FIX.md b/docs/explanation/fixes/v0.237.055/AZD_POSTPROVISION_MFA_COSMOS_GUIDANCE_FIX.md new file mode 100644 index 000000000..6bacd20e9 --- /dev/null +++ b/docs/explanation/fixes/v0.237.055/AZD_POSTPROVISION_MFA_COSMOS_GUIDANCE_FIX.md @@ -0,0 +1,32 @@ +# AZD Postprovision MFA Cosmos Guidance Fix + +Fixed/Implemented in version: **0.237.055** + +## Issue Description + +`azd up` and `azd provision` could fail during Cosmos DB postprovision steps with a generic message that implied the required role assignments or firewall updates might already exist, even when Azure CLI was actually blocked by a multi-factor authentication or claims challenge. + +## Root Cause Analysis + +The postprovision flow treated several Azure CLI failures as benign and discarded the original error output. That hid the real cause and gave the user no actionable recovery path when Azure required another MFA prompt. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `deployers/bicep/cosmosDb-postDeployPerms.sh` +- `functional_tests/test_azd_windows_hooks.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added explicit MFA and claims-challenge detection for Cosmos DB control-plane and data-plane role assignment steps. +- Replaced ambiguous "may already exist" handling with differentiated logic for existing assignments versus MFA failures. +- Added a manual recovery message that tells the user to add the detected runner IP to the new Cosmos DB account firewall when the automated firewall update is blocked by MFA. +- Added explicit guidance to rerun `az login --scope https://management.azure.com//.default` when Azure CLI cannot read the Cosmos DB key because of MFA. + +## Validation + +- Functional test: `functional_tests/test_azd_windows_hooks.py` +- Expected outcome: Windows postprovision failures now surface actionable MFA guidance instead of masking the root cause. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.056/AZD_PRIVATE_NETWORK_RUNNER_IP_PRIMING_FIX.md b/docs/explanation/fixes/v0.237.056/AZD_PRIVATE_NETWORK_RUNNER_IP_PRIMING_FIX.md new file mode 100644 index 000000000..c4d4d27ed --- /dev/null +++ b/docs/explanation/fixes/v0.237.056/AZD_PRIVATE_NETWORK_RUNNER_IP_PRIMING_FIX.md @@ -0,0 +1,33 @@ +# AZD Private Network Runner IP Priming Fix + +Fixed/Implemented in version: **0.237.056** + +## Issue Description + +When private networking was enabled, deployment runners that needed temporary public access to Cosmos DB or Azure Container Registry could still fail during `postprovision` because their public IP was not present in the initial firewall rules created during provisioning. + +## Root Cause Analysis + +The deployment runner IP was only handled after infrastructure creation, which is too late for firewall rules that are established by the Bicep deployment itself. Manual firewall edits also caused confusion because propagation can take time after the portal update is saved. + +## Technical Details + +### Files Modified + +- `deployers/bicep/validate_azd_prerequisites.py` +- `deployers/azure.yaml` +- `deployers/bicep/README.md` +- `functional_tests/test_azd_prerequisites_allowed_ip_auto_merge.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added runner public IP detection to the preprovision prerequisite script. +- Added automatic persistence of the resolved IP into `ALLOWED_IP_RANGES` through `azd env set` before infrastructure provisioning starts. +- Added explicit guidance that manual Cosmos DB firewall updates can take up to 30 minutes to propagate before rerunning `azd up`. + +## Validation + +- Functional test: `functional_tests/test_azd_prerequisites_allowed_ip_auto_merge.py` +- Functional test: `functional_tests/test_azd_windows_hooks.py` +- Expected outcome: private-network deployments can carry the deployment runner IP into initial Cosmos DB and Azure Container Registry firewall rules without requiring a later manual retry. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.057/AZD_POSTPROVISION_COSMOS_ACCESS_PROBE_FIX.md b/docs/explanation/fixes/v0.237.057/AZD_POSTPROVISION_COSMOS_ACCESS_PROBE_FIX.md new file mode 100644 index 000000000..e72bda4ff --- /dev/null +++ b/docs/explanation/fixes/v0.237.057/AZD_POSTPROVISION_COSMOS_ACCESS_PROBE_FIX.md @@ -0,0 +1,30 @@ +# AZD Postprovision Cosmos Access Probe Fix + +Fixed/Implemented in version: **0.237.057** + +## Issue Description + +`azd up` could still fail in `postprovision` with an MFA-gated Cosmos DB firewall update even after an administrator had already added the deployment runner IP manually. + +## Root Cause Analysis + +The postprovision hook tried to update the Cosmos DB firewall before proving whether the deployment runner already had Cosmos DB data-plane access. That caused an unnecessary write attempt through Azure CLI, which could still be blocked by conditional access and MFA requirements. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_azd_windows_hooks.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added a Cosmos DB data-plane access probe that runs after the primary key is retrieved. +- Short-circuited the firewall update step when the deployment runner can already reach Cosmos DB with the retrieved key. +- Updated the manual guidance to tell users that if they already added the IP manually, firewall propagation can still take up to 30 minutes. + +## Validation + +- Functional test: `functional_tests/test_azd_windows_hooks.py` +- Expected outcome: manual Cosmos DB firewall changes no longer trigger an unnecessary Azure CLI firewall update once the runner already has data-plane access. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.058/ACR_TASKS_TRUSTED_AZURE_SERVICES_FIX.md b/docs/explanation/fixes/v0.237.058/ACR_TASKS_TRUSTED_AZURE_SERVICES_FIX.md new file mode 100644 index 000000000..873950e0b --- /dev/null +++ b/docs/explanation/fixes/v0.237.058/ACR_TASKS_TRUSTED_AZURE_SERVICES_FIX.md @@ -0,0 +1,31 @@ +# ACR Tasks Trusted Azure Services Fix + +Fixed/Implemented in version: **0.237.058** + +## Issue Description + +`azd up` could fail during `predeploy` when `az acr build` submitted a cloud build to Azure Container Registry Tasks and the registry firewall denied the task worker IP. + +## Root Cause Analysis + +The registry network rules allowed only explicit IP addresses from `allowedIpAddresses`. That list is appropriate for deployment-runner access, but `az acr build` executes inside Azure through ACR Tasks, so the denied client IP is often an Azure-hosted worker rather than the local machine. + +## Technical Details + +### Files Modified + +- `deployers/bicep/modules/azureContainerRegistry.bicep` +- `deployers/bicep/README.md` +- `functional_tests/test_acr_trusted_services_bypass.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Enabled `networkRuleBypassOptions: 'AzureServices'` for private-network Azure Container Registry deployments. +- Preserved the existing ACR network rule set and allowed IP list behavior. +- Documented that ACR build failures referencing an Azure-hosted IP are not solved by adding the workstation IP. + +## Validation + +- Functional test: `functional_tests/test_acr_trusted_services_bypass.py` +- Expected outcome: `az acr build` can authenticate through ACR Tasks on restricted registries once the updated registry configuration is provisioned. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.059/COSMOS_FIREWALL_CIDR_PROPAGATION_FIX.md b/docs/explanation/fixes/v0.237.059/COSMOS_FIREWALL_CIDR_PROPAGATION_FIX.md new file mode 100644 index 000000000..40952f566 --- /dev/null +++ b/docs/explanation/fixes/v0.237.059/COSMOS_FIREWALL_CIDR_PROPAGATION_FIX.md @@ -0,0 +1,30 @@ +# Cosmos Firewall CIDR and Propagation Fix + +Fixed/Implemented in version: **0.237.059** + +## Issue Description + +`azd up` could still fail in `postprovision` after a manual Cosmos DB firewall change because the hook only recognized exact IP matches and did not tolerate propagation delay after the rule was configured. + +## Root Cause Analysis + +Manual firewall entries may be configured as CIDR ranges such as `/32`, and Cosmos DB firewall activation can lag behind the ARM configuration change. The hook treated those cases as missing rules and attempted another Azure CLI firewall update, which could be blocked by MFA. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_azd_windows_hooks.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added CIDR-aware runner IP matching for Cosmos DB firewall rules. +- Added a short propagation wait loop when the runner IP is already configured on the Cosmos DB firewall or has just been added. +- Preserved the explicit MFA guidance when the hook truly needs to perform a firewall update and Azure CLI blocks it. + +## Validation + +- Functional test: `functional_tests/test_azd_windows_hooks.py` +- Expected outcome: manual Cosmos DB firewall rules such as `x.x.x.x/32` no longer trigger an unnecessary MFA-gated update, and recent firewall changes are given time to propagate before the deployment fails. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.060/COSMOS_FIREWALL_MFA_FALLBACK_ACCESS_CHECK_FIX.md b/docs/explanation/fixes/v0.237.060/COSMOS_FIREWALL_MFA_FALLBACK_ACCESS_CHECK_FIX.md new file mode 100644 index 000000000..ee05fd2c5 --- /dev/null +++ b/docs/explanation/fixes/v0.237.060/COSMOS_FIREWALL_MFA_FALLBACK_ACCESS_CHECK_FIX.md @@ -0,0 +1,30 @@ +# Cosmos Firewall MFA Fallback Access Check Fix + +Fixed/Implemented in version: **0.237.060** + +## Issue Description + +`azd up` could still fail immediately when Azure CLI hit an MFA challenge while trying to update the Cosmos DB firewall, even if the required firewall rule had already been added manually and only propagation remained. + +## Root Cause Analysis + +The postprovision hook treated an MFA-blocked `az cosmosdb update` as a terminal failure. That stopped the deployment before it could verify whether Cosmos DB data-plane access had already become available through an existing or newly propagated firewall rule. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_azd_windows_hooks.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Changed the Cosmos DB firewall update path so MFA errors no longer fail immediately. +- Added a fallback check that probes and waits for real Cosmos DB data-plane access after the MFA-blocked update attempt. +- Preserved the manual remediation guidance when the runner still cannot access Cosmos DB after the wait window. + +## Validation + +- Functional test: `functional_tests/test_azd_windows_hooks.py` +- Expected outcome: an MFA challenge during the firewall update step no longer blocks deployments that already have the correct Cosmos DB firewall rule in place and only need propagation time. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.061/ACR_PREDEPLOY_TEMPORARY_FIREWALL_OPEN_FIX.md b/docs/explanation/fixes/v0.237.061/ACR_PREDEPLOY_TEMPORARY_FIREWALL_OPEN_FIX.md new file mode 100644 index 000000000..d31d740ea --- /dev/null +++ b/docs/explanation/fixes/v0.237.061/ACR_PREDEPLOY_TEMPORARY_FIREWALL_OPEN_FIX.md @@ -0,0 +1,30 @@ +# ACR Predeploy Temporary Firewall Open Fix + +Fixed/Implemented in version: **0.237.061** + +## Issue Description + +`azd up` could still fail during `predeploy` because `az acr build` runs through Azure Container Registry Tasks and the registry firewall denied the Azure-hosted build worker IP. + +## Root Cause Analysis + +The registry remained in a deny-by-default state during the predeploy cloud build step. Even with explicit runner IP handling and trusted Azure services bypass configured, the ACR Tasks worker still could not always authenticate through the registry firewall in this deployment flow. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_acr_trusted_services_bypass.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added predeploy helper logic to temporarily switch the ACR firewall default action to `Allow` during `az acr build` when private networking is enabled. +- Added restoration logic to return the ACR firewall default action to `Deny` after the build and during predeploy cleanup if the build fails. +- Kept the final `postup` step that disables ACR public access entirely after deployment finishes. + +## Validation + +- Functional test: `functional_tests/test_acr_trusted_services_bypass.py` +- Expected outcome: `az acr build` can complete during predeploy, and the registry returns to a restricted state after the build finishes or fails. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.062/ACR_DEPLOYMENT_TIME_PUBLIC_ACCESS_FIX.md b/docs/explanation/fixes/v0.237.062/ACR_DEPLOYMENT_TIME_PUBLIC_ACCESS_FIX.md new file mode 100644 index 000000000..12ab0685d --- /dev/null +++ b/docs/explanation/fixes/v0.237.062/ACR_DEPLOYMENT_TIME_PUBLIC_ACCESS_FIX.md @@ -0,0 +1,31 @@ +# ACR Deployment-Time Public Access Fix + +Fixed/Implemented in version: **0.237.062** + +## Issue Description + +`azd up` could fail during `predeploy` because the hook attempted to change Azure Container Registry firewall settings live with Azure CLI, and that network mutation itself was blocked by MFA. + +## Root Cause Analysis + +The deployment flow relied on a privileged `az acr update` during predeploy to open the registry for `az acr build`. In environments with conditional access or MFA requirements, that update could fail before the build even started. + +## Technical Details + +### Files Modified + +- `deployers/bicep/modules/azureContainerRegistry.bicep` +- `deployers/azure.yaml` +- `functional_tests/test_acr_trusted_services_bypass.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Changed the ACR deployment-time network rule default action to `Allow` when private networking is enabled. +- Removed the predeploy Azure CLI firewall toggle logic for ACR. +- Kept the final `postup` step that disables ACR public access after deployment completes. + +## Validation + +- Functional test: `functional_tests/test_acr_trusted_services_bypass.py` +- Expected outcome: `az acr build` can complete without requiring a live `az acr update` during predeploy, and the registry is still locked down in the final deployment step. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.063/WINDOWS_ACR_LOG_STREAM_ENCODING_FIX.md b/docs/explanation/fixes/v0.237.063/WINDOWS_ACR_LOG_STREAM_ENCODING_FIX.md new file mode 100644 index 000000000..1a7f508c1 --- /dev/null +++ b/docs/explanation/fixes/v0.237.063/WINDOWS_ACR_LOG_STREAM_ENCODING_FIX.md @@ -0,0 +1,30 @@ +# Windows ACR Log Stream Encoding Fix + +Fixed/Implemented in version: **0.237.063** + +## Issue Description + +`azd up` on Windows could fail during `predeploy` even when the ACR build itself was progressing, because Azure CLI crashed while streaming build logs containing characters that could not be encoded by the local `cp1252` console. + +## Root Cause Analysis + +The Windows predeploy hook called `az acr build` in streaming mode. Azure CLI attempted to render remote ACR Task log lines directly to the local console, and `colorama` raised a `UnicodeEncodeError` when the output included characters outside the active Windows code page. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_acr_trusted_services_bypass.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Changed the Windows predeploy hook to queue ACR builds with `--no-logs` and capture the returned run ID. +- Added polling with `az acr task show-run` until the remote build completes, fails, or times out. +- Preserved the existing POSIX streaming behavior and the deployment-time ACR public access model. + +## Validation + +- Functional test: `functional_tests/test_acr_trusted_services_bypass.py` +- Expected outcome: Windows `azd up` no longer fails because Azure CLI cannot render Unicode ACR build logs to the local console. \ No newline at end of file diff --git a/docs/explanation/fixes/v0.237.064/POSTPROVISION_VENV_PIP_USER_FIX.md b/docs/explanation/fixes/v0.237.064/POSTPROVISION_VENV_PIP_USER_FIX.md new file mode 100644 index 000000000..8292fa996 --- /dev/null +++ b/docs/explanation/fixes/v0.237.064/POSTPROVISION_VENV_PIP_USER_FIX.md @@ -0,0 +1,31 @@ +# Postprovision Venv Pip User Fix + +Fixed/Implemented in version: **0.237.064** + +## Issue Description + +`azd up` could fail in `postprovision` at the Python dependency installation step when the deployment was launched from an activated virtual environment. + +## Root Cause Analysis + +The postprovision hook always used `pip install --user`, but pip rejects `--user` installs when the active interpreter is already inside a virtual environment because user site-packages are not visible there. + +## Technical Details + +### Files Modified + +- `deployers/azure.yaml` +- `functional_tests/test_postprovision_python_dependency_install.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added Python virtual environment detection to the POSIX and Windows postprovision hooks. +- Changed the dependency install step to omit `--user` when the active interpreter is inside a virtual environment. +- Improved error reporting so pip output is surfaced when the install still fails. + +## Validation + +- Functional test: `functional_tests/test_postprovision_python_dependency_install.py` +- Reproduced root cause locally with: `python -m pip install --user ...` under the repo virtual environment. +- Expected outcome: postprovision dependency installation succeeds whether deployment runs from a virtual environment or a system interpreter. \ No newline at end of file diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 2b0022858..b39156013 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,142 @@ # Feature Release +### **(v0.237.050)** + +#### Bug Fixes + +* **AZD Windows Hook Resource Group Resolution Fix** + * Fixed AZD Windows hook execution that failed when `var_rgName` was stale or missing from the deployment context, preventing resource group resolution during `postprovision`, `predeploy`, and `postup` hook phases. + * Added direct Azure resource group name lookup as a fallback when `var_rgName` is unavailable, ensuring hooks can reliably resolve the correct resource group even if the variable state becomes outdated. + * Added regression coverage to prevent resource group resolution failures from recurring in future deployments that rely on the AZD Windows hook framework. + * (Ref: `deployers/azure.yaml`, `functional_tests/test_azd_windows_hooks.py`, `docs/explanation/fixes/v0.237.050/AZD_WINDOWS_RGNAME_RESOLUTION_FIX.md`) + +### **(v0.237.049)** + +#### Bug Fixes + +* **AZD Windows Hook Validation Fix** + * Fixed Windows `azd provision` and `azd up` runs that failed after infrastructure provisioning because `postprovision`, `predeploy`, and `postup` did not define valid Windows hook commands in `azure.yaml`. + * Added native PowerShell hook implementations for the Windows deployment path so post-provision configuration, image build orchestration, and private-network finalization can execute on Windows without relying on POSIX-only hook blocks. + * Added regression coverage to prevent the required Windows AZD hook definitions from being removed or reformatted incorrectly in future changes. + * (Ref: `deployers/azure.yaml`, `functional_tests/test_azd_windows_hooks.py`, `docs/explanation/fixes/v0.237.049/AZD_WINDOWS_POSTPROVISION_HOOK_FIX.md`) + +### **(v0.237.048)** + +#### New Features + +* **Azure CLI Azure OpenAI Model Deployment Support** + * Extended the Azure CLI deployer so it can create the default GPT and embedding model deployments for either a newly created Azure OpenAI account or a reused existing account. + * Added configurable Azure OpenAI deployment type handling for `Standard`, `DatazoneStandard`, and `GlobalStandard`, along with retry guidance when quota or regional availability blocks the initial deployment type. + * Updated deployment documentation and added regression coverage so the Azure CLI path now reflects the same Azure OpenAI model deployment expectations more clearly. + * (Ref: `deployers/azurecli/deploy-simplechat.ps1`, `deployers/azurecli/README.md`, `functional_tests/test_azurecli_aoai_model_deployments.py`, `docs/explanation/features/AZURECLI_AOAI_MODEL_DEPLOYMENTS.md`) + +### **(v0.237.022)** + +#### New Features + +* **Azure CLI Private Networking Parity** + * Extended the Azure CLI deployer to support the same core private networking flow as the Bicep deployer for the services it provisions. + * Added support for creating a deployment VNet, reusing existing App Service and private endpoint subnets, creating private endpoints, creating or reusing private DNS zones, and optionally suppressing VNet link creation per zone. + * Added private networking validation and operator guidance directly in the deployment script so administrators are prompted to confirm subnet delegation, DNS ownership, and VNet link expectations before deployment continues. + * (Ref: `deployers/azurecli/deploy-simplechat.ps1`, `deployers/azurecli/README.md`, private endpoints, private DNS, App Service VNet integration) + +#### Bug Fixes + +* **Azure CLI Cleanup and Documentation Alignment** + * Updated the Azure CLI destroy script to better reflect the new private networking model by clarifying that shared enterprise VNets, subnets, and customer-managed private DNS zones are not removed when they live outside the deployment resource group. + * Added cloud-aware token refresh handling for both Azure Commercial and Azure Government and fixed Entra security group cleanup to include the public workspace creation group. + * Updated feature documentation and deployment guidance to describe the new cleanup behavior and aligned the application version metadata with the Azure CLI deployment changes. + * (Ref: `deployers/azurecli/destroy-simplechat.ps1`, `deployers/azurecli/README.md`, `docs/explanation/features/AZURECLI_PRIVATE_NETWORKING_PARITY.md`, `application/single_app/config.py`) + +### **(v0.237.020)** + +#### New Features + +* **Terraform Private Networking Parity** + * Added core private networking support to the Terraform deployer so it can now create or reuse a virtual network, use dedicated App Service integration and private endpoint subnets, and provision private endpoints for the main Simple Chat dependencies. + * Added support for automatic private DNS zone creation, enterprise private DNS zone reuse by resource ID, and optional per-zone VNet link suppression when central networking teams manage those links separately. + * Added Terraform support for private networking configuration of the web app, storage account, Azure AI Search, Document Intelligence, and Azure OpenAI so public access is disabled when private networking is enabled. + * (Ref: `deployers/terraform/private_networking.tf`, `deployers/terraform/main.tf`, private endpoints, private DNS, App Service VNet integration) + +#### Bug Fixes + +* **Terraform Deployment Guidance and Provider Support Alignment** + * Updated the Terraform deployment guidance to match the new private networking capabilities, including examples for new VNet creation, existing subnet reuse, and private DNS zone reuse. + * Added the `azapi` provider requirement so Terraform can manage private DNS VNet links needed for cross-scope and enterprise-managed networking scenarios. + * Added feature documentation for the new Terraform private networking flow and aligned the application version metadata with the release. + * (Ref: `deployers/terraform/ReadMe.md`, `deployers/terraform/.terraform.lock.hcl`, `docs/explanation/features/TERRAFORM_PRIVATE_NETWORKING_PARITY.md`, `application/single_app/config.py`) + +### **(v0.237.019)** + +#### Bug Fixes + +* **Terraform Existing Azure OpenAI Reuse Alignment** + * Updated the Terraform deployer to better align with the newer Azure OpenAI deployment patterns already supported in the Bicep deployer. + * Added support for specifying an existing Azure OpenAI subscription ID when the reused resource lives outside the primary deployment subscription. + * Added support for an explicit existing Azure OpenAI or Azure AI Foundry-compatible endpoint override so Terraform can configure the application with externally managed endpoints instead of deriving the endpoint from the resource name. + * OpenAI RBAC assignment logic now skips automatic role assignment when only an endpoint is supplied and no deployable Azure OpenAI resource metadata is available. + * (Ref: `deployers/terraform/main.tf`, existing Azure OpenAI reuse, cross-subscription lookup, endpoint override) + +* **Terraform Deployment Guidance Improvements** + * Expanded Terraform deployment documentation to clearly explain that the Terraform deployer does not currently automate existing VNet reuse, subnet creation, private endpoint deployment, or private DNS zone creation/linking. + * Added prerequisite guidance for administrators who plan to enforce private networking after deployment, including subnet delegation and private DNS requirements. + * Added documentation for endpoint-only Azure OpenAI reuse and cross-subscription Azure OpenAI resource lookup. + * (Ref: `deployers/terraform/ReadMe.md`, Terraform deployment guidance, private networking prerequisites) + +### **(v0.237.018)** + +#### New Features + +* **Existing VNet and Subnet Reuse for Private Networking** + * Added support for deploying Simple Chat into an existing customer-managed virtual network when private networking is enabled. + * New infrastructure parameters allow reuse of an existing VNet, an existing App Service VNet integration subnet, and an existing private endpoint subnet. + * Existing network resources may live in the same resource group, a different resource group, or a different subscription. + * The deployment now clearly distinguishes between creating a new VNet and reusing an existing one, and it avoids creating subnets inside external customer-managed VNets. + * (Ref: `deployers/bicep/main.bicep`, `deployers/bicep/main.parameters.json`, private networking, existing VNet support) + +* **Private DNS Zone Reuse and Link Control** + * Added per-zone private DNS configuration through `privateDnsZoneConfigs` so deployments can either create local private DNS zones or reuse centrally managed ones. + * Each supported DNS zone can now reuse an existing zone by resource ID and optionally skip automatic VNet link creation when the customer manages DNS links separately. + * This enables mixed enterprise scenarios where some zones are managed locally and others are managed by a central networking team. + * (Ref: `deployers/bicep/main.bicep`, `deployers/bicep/modules/privateDNS.bicep`, `deployers/bicep/modules/privateDNSLink.bicep`, `deployers/bicep/modules/privateNetworking.bicep`) + +* **Existing Azure OpenAI and Azure AI Foundry Endpoint Support** + * Added support for reusing an existing Azure OpenAI resource or a public Azure AI Foundry OpenAI-compatible endpoint instead of always deploying a new Azure OpenAI resource. + * New parameters support external endpoint reuse, external resource metadata, cross-resource-group and cross-subscription scenarios, and optional API key injection when automatic key retrieval is not available. + * Model discovery now falls back to configured deployment values when Azure management metadata is unavailable, which improves compatibility with endpoint-only and AI Foundry scenarios. + * (Ref: `deployers/bicep/modules/openAI.bicep`, `deployers/bicep/modules/appService.bicep`, `deployers/bicep/postconfig.py`, `application/single_app/route_backend_models.py`) + +* **Region-Aware Azure OpenAI Default Model Updates** + * Updated default GPT and embedding model deployments for Azure Commercial and Azure Government regions. + * Default GPT model deployment is now `gpt-4o` with `Standard` SKU. + * Default embedding deployment is now `text-embedding-3-small` in Azure Commercial and US Gov Arizona, with `text-embedding-ada-002` retained for US Gov Virginia where model availability differs. + * (Ref: `deployers/bicep/main.bicep`, Azure OpenAI defaults, Azure Government model support) + +* **AZD Pre-Provision Prerequisite Validation for Private Networking** + * Added a pre-provision validation step for `azd up` / `azd provision` that inspects the user's networking choices before infrastructure deployment begins. + * When private networking is enabled, the deployment now explains the prerequisites for reusing an existing VNet, describes private DNS behavior, and stops early if required subnet resource IDs are missing. + * This gives administrators a clear chance to pause, prepare prerequisites, and rerun the deployment instead of discovering missing dependencies later in the provisioning flow. + * (Ref: `deployers/azure.yaml`, `deployers/bicep/validate_azd_prerequisites.py`, `deployers/bicep/README.md`) + +#### Bug Fixes + +* **Cross-Scope Private DNS and External OpenAI Deployment Fixes** + * Fixed Bicep compilation and scoping issues affecting private DNS zone reuse, VNet link creation, and role assignments for externally managed Azure OpenAI resources. + * Introduced helper modules to perform private DNS VNet link creation and external OpenAI RBAC assignment at the correct deployment scope. + * Resolved nullability and dependency warnings in the infrastructure modules so the updated templates compile cleanly. + * (Ref: `deployers/bicep/modules/privateDNSLink.bicep`, `deployers/bicep/modules/setPermissions-openAIExternal.bicep`, `deployers/bicep/modules/setPermissions.bicep`, `deployers/bicep/modules/privateNetworking.bicep`) + +* **AZD OpenAI Output Export Alignment Fix** + * Fixed an AZD environment export mismatch where `azure.yaml` referenced singular OpenAI model output names that no longer matched the Bicep template outputs. + * Updated the deployment hooks to export the correct plural GPT and embedding model arrays and the Azure OpenAI subscription ID, ensuring post-deployment configuration receives the expected values. + * (Ref: `deployers/azure.yaml`, `deployers/bicep/main.bicep`, `deployers/bicep/postconfig.py`) + +* **Deployment Guidance Improvements for Private Networking** + * Expanded deployment guidance so administrators understand the prerequisites and consequences of choosing existing VNet reuse and centralized private DNS management. + * Updated one-click deployment and Azure CLI deployment guidance to explain when existing subnet IDs, subnet delegation, DNS zones, and VNet links must already be in place. + * (Ref: `deployers/bicep/README.md`, `deployers/bicep/OneClickDeploy.md`, `deployers/azurecli/deploy-simplechat.ps1`, `docs/how-to/enterprise_networking.md`) + ### **(v0.237.011)** #### Bug Fixes diff --git a/docs/how-to/enterprise_networking.md b/docs/how-to/enterprise_networking.md index 7856b6f7c..d4469cc78 100644 --- a/docs/how-to/enterprise_networking.md +++ b/docs/how-to/enterprise_networking.md @@ -25,6 +25,24 @@ Enterprise networking for Simple Chat involves: - Understanding of Azure networking concepts - Planning for IP address ranges and network topology +## Deployment Runner Connectivity + +For enterprise deployments, the important question is whether the machine running `azd up` can already reach the private endpoints for Cosmos DB, Azure Container Registry, and the other protected services. + +Leave `allowedIpAddresses` blank when: +- The deployment runner is on a corporate network with routing to the private LAN or connected VNet. +- The deployment runner is connected through VPN or ExpressRoute and can resolve the private DNS zones. +- The deployment runs from a jump host, build agent, or management workstation already inside the private network boundary. + +Provide `allowedIpAddresses` when: +- Private networking is enabled. +- The machine running `azd up` must temporarily reach protected services over a public path during deployment. +- Cosmos DB or Azure Container Registry still need a temporary allowlist entry for the deployment runner before public access is disabled at the end of the workflow. + +When a value is needed, provide the public egress IP address or CIDR that Azure sees for the deployment runner. In many enterprises this is a company NAT or proxy address, not the laptop's local `10.x.x.x`, `172.16.x.x` to `172.31.x.x`, or `192.168.x.x` address. + +If you are unsure which public egress IP is being used, run the lookup from the same machine and network path that will execute `azd up`, for example by searching `what is my IP` in Bing. + ## Step 1: Plan Your Network Architecture ### Network Topology Planning diff --git a/functional_tests/test_acr_trusted_services_bypass.py b/functional_tests/test_acr_trusted_services_bypass.py new file mode 100644 index 000000000..6081314c6 --- /dev/null +++ b/functional_tests/test_acr_trusted_services_bypass.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# test_acr_trusted_services_bypass.py +""" +Functional test for ACR trusted Azure services bypass. +Version: 0.237.063 +Implemented in: 0.237.063 + +This test ensures private-network Azure Container Registry deployments keep the +registry reachable for az acr build until the final postup lockdown step and avoid +Windows log-streaming failures by polling run status instead. +""" + +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +ACR_MODULE = REPO_ROOT / "deployers" / "bicep" / "modules" / "azureContainerRegistry.bicep" +AZURE_YAML = REPO_ROOT / "deployers" / "azure.yaml" + + +def require_contains(content: str, expected: str, description: str) -> None: + if expected not in content: + raise AssertionError(f"Missing {description}: {expected}") + + +def test_acr_trusted_services_bypass() -> bool: + print("🧪 Testing ACR trusted Azure services bypass") + print("=" * 70) + + content = ACR_MODULE.read_text(encoding="utf-8") + hook_content = AZURE_YAML.read_text(encoding="utf-8") + + require_contains(content, "networkRuleBypassOptions: enablePrivateNetworking ? 'AzureServices' : 'None'", "trusted Azure services bypass") + require_contains(content, "networkRuleSet: enablePrivateNetworking ? {", "private-network registry rule set") + require_contains(content, "defaultAction: 'Allow'", "deployment-time ACR public access") + require_contains(content, "ipRules: allowedIpAddresses", "registry IP allow list wiring") + require_contains(hook_content, "ACR public access remains enabled until POST-UP", "deployment-time ACR access message") + require_contains(hook_content, "--public-network-enabled false", "postup ACR public access disable step") + require_contains(hook_content, "--no-logs --query runId -o tsv", "Windows non-streaming ACR build invocation") + require_contains(hook_content, "Wait-ForAcrBuildRun", "Windows ACR run polling helper") + require_contains(hook_content, "az acr task show-run", "Windows ACR run status polling command") + + print("✅ ACR module enables trusted Azure services when private networking is on") + print("✅ ACR module keeps the registry firewall rule set in place") + print("✅ ACR module keeps public access enabled during deployment-time builds") + print("✅ ACR module still honors explicit allowed IP addresses") + print("✅ Predeploy relies on deployment-time ACR access instead of live firewall edits") + print("✅ POST-UP still disables ACR public access after deployment") + print("✅ Windows predeploy queues ACR builds without streaming logs") + print("✅ Windows predeploy polls ACR run status instead of streaming Unicode logs") + return True + + +if __name__ == "__main__": + try: + success = test_acr_trusted_services_bypass() + except Exception as exc: + print(f"❌ Test failed: {exc}") + raise + + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/functional_tests/test_azd_prerequisites_allowed_ip_auto_merge.py b/functional_tests/test_azd_prerequisites_allowed_ip_auto_merge.py new file mode 100644 index 000000000..fe113c0e1 --- /dev/null +++ b/functional_tests/test_azd_prerequisites_allowed_ip_auto_merge.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# test_azd_prerequisites_allowed_ip_auto_merge.py +""" +Functional test for AZD prerequisite runner IP auto-merge. +Version: 0.237.056 +Implemented in: 0.237.056 + +This test ensures the preprovision prerequisite script can add the deployment +runner public IP into the AZD environment before private-network firewall rules +are created and that it documents propagation delays for manual firewall edits. +""" + +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +PREREQUISITES = REPO_ROOT / "deployers" / "bicep" / "validate_azd_prerequisites.py" + + +def require_contains(content: str, expected: str, description: str) -> None: + if expected not in content: + raise AssertionError(f"Missing {description}: {expected}") + + +def test_azd_prerequisites_auto_merges_runner_ip() -> bool: + print("🧪 Testing AZD prerequisite runner IP auto-merge") + print("=" * 70) + + content = PREREQUISITES.read_text(encoding="utf-8") + + require_contains(content, "azd', 'env', 'set', 'ALLOWED_IP_RANGES'", "AZD environment persistence") + require_contains(content, "https://api.ipify.org", "deployment runner public IP lookup") + require_contains(content, "Added deployment runner public IP", "auto-merge status message") + require_contains(content, "allow up to 30 minutes", "manual firewall propagation guidance") + require_contains(content, "ALLOWED_IP_RANGES", "allowed IP ranges environment variable handling") + + print("✅ Prerequisite script persists ALLOWED_IP_RANGES through azd env set") + print("✅ Prerequisite script resolves the deployment runner public IP") + print("✅ Prerequisite script reports the auto-merge behavior") + print("✅ Prerequisite script warns about manual firewall propagation delay") + return True + + +if __name__ == "__main__": + try: + success = test_azd_prerequisites_auto_merges_runner_ip() + except Exception as exc: + print(f"❌ Test failed: {exc}") + raise + + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/functional_tests/test_azd_windows_hooks.py b/functional_tests/test_azd_windows_hooks.py new file mode 100644 index 000000000..b85bca4c3 --- /dev/null +++ b/functional_tests/test_azd_windows_hooks.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# test_azd_windows_hooks.py +""" +Functional test for AZD Windows hook coverage. +Version: 0.237.060 +Implemented in: 0.237.060 + +This test ensures that azure.yaml defines Windows run hooks for the AZD lifecycle +stages that are used after infrastructure provisioning. +""" + +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +AZURE_YAML = REPO_ROOT / "deployers" / "azure.yaml" + + +def require_contains(content: str, expected: str, description: str) -> None: + if expected not in content: + raise AssertionError(f"Missing {description}: {expected}") + + +def test_azd_windows_hooks() -> bool: + print("🧪 Testing AZD Windows hook coverage") + print("=" * 60) + + content = AZURE_YAML.read_text(encoding="utf-8") + + require_contains(content, "postprovision:\n", "postprovision hook section") + require_contains(content, "predeploy:\n", "predeploy hook section") + require_contains(content, "postup:\n", "postup hook section") + require_contains(content, "shell: pwsh", "Windows shell declarations") + require_contains(content, "POST-PROVISION: Starting configuration", "Windows postprovision hook") + require_contains(content, "PRE-DEPLOY: Building and pushing image", "Windows predeploy hook") + require_contains(content, "POST-UP: Final configuration", "Windows postup hook") + require_contains(content, "function Resolve-ResourceGroupName", "helper function for RG fallback") + require_contains(content, "function Get-TargetSubscriptionId", "helper function for subscription targeting") + require_contains(content, "az group exists --name", "resource group validation") + require_contains(content, "az cosmosdb list", "Cosmos DB RG discovery fallback") + require_contains(content, "az cosmosdb keys list", "Cosmos DB key retrieval for postconfig") + require_contains(content, "Ensure-CosmosRunnerAccess", "Cosmos firewall helper") + require_contains(content, "Test-CosmosRunnerAccess", "Cosmos access probe helper") + require_contains(content, "Test-RunnerIpMatchesRules", "Cosmos firewall CIDR coverage helper") + require_contains(content, "Wait-ForCosmosRunnerAccess", "Cosmos firewall propagation wait helper") + require_contains(content, "api.ipify.org", "deployment runner public IP lookup") + require_contains(content, "az cosmosdb update", "Cosmos firewall update command") + require_contains(content, "Manually add IP $runnerPublicIp", "manual Cosmos firewall guidance") + require_contains(content, "Azure CLI requires multi-factor authentication", "explicit MFA guidance") + require_contains(content, "Checking whether CosmosDB access is already available without an Azure CLI firewall update", "Cosmos MFA fallback access check") + require_contains(content, "Deployment runner already has CosmosDB data-plane access", "Cosmos access short-circuit") + require_contains(content, "Waiting for CosmosDB firewall propagation", "Cosmos propagation wait messaging") + require_contains(content, "$env:var_cosmosDb_key", "Cosmos DB key propagation") + require_contains(content, "--subscription $subscriptionId", "subscription-pinned Azure CLI commands") + require_contains(content, "$env:var_rgName = $resolvedResourceGroup", "resolved RG propagation") + + print("✅ Windows postprovision hook is present") + print("✅ Windows predeploy hook is present") + print("✅ Windows postup hook is present") + print("✅ Windows RG fallback logic is validated") + print("✅ Windows postconfig Cosmos key fallback is validated") + print("✅ Windows Cosmos firewall runner access handling is validated") + print("✅ Windows Cosmos access short-circuit is validated") + print("✅ Windows Cosmos firewall propagation handling is validated") + print("✅ Windows Cosmos MFA fallback access handling is validated") + print("✅ Windows MFA recovery guidance is validated") + print("✅ Windows subscription targeting is validated") + print("✅ Environment variable propagation is covered") + return True + + +if __name__ == "__main__": + try: + success = test_azd_windows_hooks() + except Exception as exc: + print(f"❌ Test failed: {exc}") + raise + + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/functional_tests/test_azurecli_aoai_model_deployments.py b/functional_tests/test_azurecli_aoai_model_deployments.py new file mode 100644 index 000000000..37b4545f1 --- /dev/null +++ b/functional_tests/test_azurecli_aoai_model_deployments.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# test_azurecli_aoai_model_deployments.py +""" +Functional test for Azure CLI Azure OpenAI model deployment support. +Version: 0.237.048 +Implemented in: 0.237.048 + +This test ensures that the Azure CLI deployer and its documentation include +the Azure OpenAI model deployment configuration, creation logic, and retry path. +""" + +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEPLOYER_SCRIPT = REPO_ROOT / "deployers" / "azurecli" / "deploy-simplechat.ps1" +DEPLOYER_README = REPO_ROOT / "deployers" / "azurecli" / "README.md" + + +def require_contains(content: str, expected: str, description: str) -> None: + if expected not in content: + raise AssertionError(f"Missing {description}: {expected}") + + +def test_azurecli_aoai_model_deployment_support() -> bool: + print("🧪 Testing Azure CLI Azure OpenAI model deployment support") + print("=" * 70) + + script_content = DEPLOYER_SCRIPT.read_text(encoding="utf-8") + readme_content = DEPLOYER_README.read_text(encoding="utf-8") + + require_contains( + script_content, + "$param_DeployAzureOpenAiModels = $true", + "Azure OpenAI model deployment toggle", + ) + require_contains( + script_content, + "$param_AzureOpenAiDeploymentType = \"\"", + "Azure OpenAI deployment type configuration", + ) + require_contains( + script_content, + "function Ensure-OpenAiModelDeployment", + "Azure OpenAI model deployment helper", + ) + require_contains( + script_content, + "az cognitiveservices account deployment create", + "Azure CLI model deployment create command", + ) + require_contains( + script_content, + "Try a different Azure OpenAI deployment type", + "deployment type retry prompt", + ) + + require_contains( + readme_content, + "The Azure CLI deployer can now create the default GPT and embedding model deployments", + "README Azure OpenAI deployment description", + ) + require_contains( + readme_content, + "param_AzureOpenAiDeploymentType", + "README configuration guidance", + ) + + print("✅ Azure CLI deployer contains Azure OpenAI model deployment configuration") + print("✅ Azure CLI deployer contains idempotent model deployment creation logic") + print("✅ Azure CLI README documents deployment type selection and retry guidance") + return True + + +if __name__ == "__main__": + try: + success = test_azurecli_aoai_model_deployment_support() + except Exception as exc: + print(f"❌ Test failed: {exc}") + raise + + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/functional_tests/test_postconfig_azurecli_credential.py b/functional_tests/test_postconfig_azurecli_credential.py new file mode 100644 index 000000000..919b7a145 --- /dev/null +++ b/functional_tests/test_postconfig_azurecli_credential.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# test_postconfig_azurecli_credential.py +""" +Functional test for postconfig deployment credential usage. +Version: 0.237.053 +Implemented in: 0.237.053 + +This test ensures the AZD post-deployment configuration script supports a +deployment-time Cosmos DB key fallback while still using Azure CLI credentials +for other Azure resource access such as Key Vault. +""" + +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +POSTCONFIG = REPO_ROOT / "deployers" / "bicep" / "postconfig.py" + + +def require_contains(content: str, expected: str, description: str) -> None: + if expected not in content: + raise AssertionError(f"Missing {description}: {expected}") + + +def require_not_contains(content: str, unexpected: str, description: str) -> None: + if unexpected in content: + raise AssertionError(f"Unexpected {description}: {unexpected}") + + +def test_postconfig_uses_repeatable_deployment_credentials() -> bool: + print("🧪 Testing postconfig deployment credential usage") + print("=" * 70) + + content = POSTCONFIG.read_text(encoding="utf-8") + + require_contains(content, "from azure.identity import AzureCliCredential", "Azure CLI credential import") + require_contains(content, "credential = AzureCliCredential()", "Azure CLI credential initialization") + require_contains(content, "cosmosKey = os.getenv(\"var_cosmosDb_key\")", "deployment Cosmos key input") + require_contains(content, "if cosmosKey:", "Cosmos key fallback branch") + require_contains(content, "client = CosmosClient(cosmosEndpoint, cosmosKey)", "Cosmos key client initialization") + require_contains(content, "credential.get_token(\"https://cosmos.azure.com/.default\")", "Azure CLI token fallback") + require_not_contains(content, "DefaultAzureCredential()", "DefaultAzureCredential initialization") + + print("✅ postconfig imports AzureCliCredential") + print("✅ postconfig accepts a deployment Cosmos DB key fallback") + print("✅ postconfig retains Azure CLI credential fallback for Cosmos and Key Vault") + print("✅ postconfig no longer initializes DefaultAzureCredential") + return True + + +if __name__ == "__main__": + try: + success = test_postconfig_uses_repeatable_deployment_credentials() + except Exception as exc: + print(f"❌ Test failed: {exc}") + raise + + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/functional_tests/test_postprovision_python_dependency_install.py b/functional_tests/test_postprovision_python_dependency_install.py new file mode 100644 index 000000000..152044847 --- /dev/null +++ b/functional_tests/test_postprovision_python_dependency_install.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# test_postprovision_python_dependency_install.py +""" +Functional test for postprovision Python dependency installation. +Version: 0.237.064 +Implemented in: 0.237.064 + +This test ensures the AZD postprovision hook avoids `pip install --user` when the +current Python interpreter is already inside a virtual environment. +""" + +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +AZURE_YAML = REPO_ROOT / "deployers" / "azure.yaml" + + +def require_contains(content: str, expected: str, description: str) -> None: + if expected not in content: + raise AssertionError(f"Missing {description}: {expected}") + + +def test_postprovision_python_dependency_install() -> bool: + print("🧪 Testing postprovision Python dependency install handling") + print("=" * 70) + + content = AZURE_YAML.read_text(encoding="utf-8") + + require_contains(content, "python_is_virtual_environment", "POSIX virtual environment detection helper") + require_contains(content, "Test-PythonVirtualEnvironment", "Windows virtual environment detection helper") + require_contains(content, "install_postprovision_python_dependencies", "POSIX dependency install helper") + require_contains(content, "Install-PostprovisionPythonDependencies", "Windows dependency install helper") + require_contains(content, "sys.prefix != getattr(sys, 'base_prefix', sys.prefix)", "virtual environment detection expression") + require_contains(content, "python3 -m pip install -r ./bicep/requirements.txt", "POSIX venv-aware install command") + require_contains(content, "python -m pip install -r .\\bicep\\requirements.txt", "Windows venv-aware install command") + require_contains(content, "python3 -m pip install --user -r ./bicep/requirements.txt", "POSIX non-venv install command") + require_contains(content, "python -m pip install --user -r .\\bicep\\requirements.txt", "Windows non-venv install command") + + print("✅ Postprovision hook detects Python virtual environments") + print("✅ Postprovision hook skips --user when running inside a virtual environment") + print("✅ Postprovision hook retains --user installs outside virtual environments") + return True + + +if __name__ == "__main__": + try: + success = test_postprovision_python_dependency_install() + except Exception as exc: + print(f"❌ Test failed: {exc}") + raise + + sys.exit(0 if success else 1) \ No newline at end of file From 39fa29903821ce82fdb6ded705f0b996f5a67d9a Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Tue, 24 Mar 2026 10:42:46 -0400 Subject: [PATCH 2/7] reconciled deployment code with latest dev --- application/single_app/config.py | 2 +- deployers/azurecli/README.md | 15 + deployers/bicep/main.bicep | 10 +- deployers/bicep/main.json | 587 ++++++++++++++++++++----------- deployers/terraform/main.tf | 2 +- 5 files changed, 398 insertions(+), 218 deletions(-) diff --git a/application/single_app/config.py b/application/single_app/config.py index 059e87069..8748c9780 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.239.150" +VERSION = "0.240.001" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/deployers/azurecli/README.md b/deployers/azurecli/README.md index d347cfb60..c3905cca4 100644 --- a/deployers/azurecli/README.md +++ b/deployers/azurecli/README.md @@ -10,6 +10,8 @@ This option is useful when you want a script-driven deployment flow without usin It is also useful when you want more script-level control over deployment sequencing and post-deployment recovery steps than the native AZD workflow provides. +For day-to-day work on this deployer, Visual Studio Code and Dev Containers are recommended so the script, repository layout, and Docker build context stay consistent across environments. + ## What this deployer does The Azure CLI deployer provisions the main Simple Chat application resources, including: @@ -63,6 +65,12 @@ Before running the deployment: 7. If private networking is enabled, make sure you can manage VNets, subnets, private endpoints, private DNS zones, and private DNS VNet links 8. If `param_BuildContainerImageWithAcr = $true`, make sure the target source context contains [application/single_app/Dockerfile](application/single_app/Dockerfile) and the rest of the repository files needed by that Docker build +Platform note: + +- Windows users can run the examples directly in PowerShell. +- Linux and macOS users should run the same script with `pwsh`. +- Keep PowerShell variable syntax in PowerShell. This deployer is PowerShell-first, even when Azure CLI commands are part of the workflow. + For Azure Government: ```azurecli @@ -99,6 +107,13 @@ cd deployers/azurecli .\deploy-simplechat.ps1 ``` +Linux or macOS example: + +```bash +cd deployers/azurecli +pwsh ./deploy-simplechat.ps1 +``` + ## Configuration areas to review The script contains editable configuration variables for: diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index f7145b34b..a226b9b0a 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -198,22 +198,24 @@ param deployVideoIndexerService bool var rgName = '${appName}-${environment}-rg' var requiredTags = { application: appName, environment: environment, 'azd-env-name': azdEnvironmentName } var tags = union(requiredTags, specialTags) -var acrCloudSuffix = cloudEnvironment == 'AzureCloud' ? '.azurecr.io' : '.azurecr.us' +var isPublicCloud = scCloudEnvironment == 'public' +var isUsGovernmentCloud = scCloudEnvironment == 'usgovernment' +var acrCloudSuffix = isPublicCloud ? '.azurecr.io' : '.azurecr.us' var acrName = toLower('${appName}${environment}acr') var containerRegistry = '${acrName}${acrCloudSuffix}' var containerImageName = '${containerRegistry}/${imageName}' var vNetName = '${appName}-${environment}-vnet' var normalizedLocation = toLower(replace(location, ' ', '')) -var resolvedOpenAIDeploymentType = cloudEnvironment == 'AzureUSGovernment' ? 'Standard' : openAIDeploymentType +var resolvedOpenAIDeploymentType = isUsGovernmentCloud ? 'Standard' : openAIDeploymentType var defaultGptModels = [ { modelName: 'gpt-4o' - modelVersion: cloudEnvironment == 'AzureUSGovernment' ? '2024-05-13' : '2024-11-20' + modelVersion: isUsGovernmentCloud ? '2024-05-13' : '2024-11-20' skuName: resolvedOpenAIDeploymentType skuCapacity: 100 } ] -var defaultEmbeddingModels = cloudEnvironment == 'AzureUSGovernment' +var defaultEmbeddingModels = isUsGovernmentCloud ? [ { modelName: normalizedLocation == 'usgovvirginia' ? 'text-embedding-ada-002' : 'text-embedding-3-small' diff --git a/deployers/bicep/main.json b/deployers/bicep/main.json index 9c0dcfc3d..dcf1cea98 100644 --- a/deployers/bicep/main.json +++ b/deployers/bicep/main.json @@ -1,11 +1,12 @@ { "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "languageVersion": "2.0", "contentVersion": "1.0.0.0", "metadata": { "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "13825130652851399501" + "templateHash": "521914542700510995" } }, "parameters": { @@ -18,12 +19,16 @@ }, "cloudEnvironment": { "type": "string", + "defaultValue": "[if(equals(environment().name, 'AzureCloud'), 'public', if(equals(environment().name, 'AzureUSGovernment'), 'usgovernment', 'custom'))]", "allowedValues": [ "AzureCloud", - "AzureUSGovernment" + "AzureUSGovernment", + "public", + "usgovernment", + "custom" ], "metadata": { - "description": "The target Azure Cloud environment.\n- Accepted values are: AzureCloud, AzureUSGovernment\n- Default is AzureCloud" + "description": "The target Azure Cloud environment.\n- Accepted values are: AzureCloud, AzureUSGovernment, public, usgovernment, custom\n- Default is based on the ARM cloud name" } }, "appName": { @@ -52,6 +57,7 @@ }, "imageName": { "type": "string", + "defaultValue": "simplechat:latest", "metadata": { "description": "The name of the container image to deploy to the web app.\n- should be in the format :" } @@ -172,6 +178,59 @@ "description": "Optional API key for an existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint.\n- Required for key-based authentication when reusing an external endpoint and automatic key retrieval is not available" } }, + "openAIDeploymentType": { + "type": "string", + "allowedValues": [ + "Standard", + "DatazoneStandard", + "GlobalStandard" + ], + "metadata": { + "description": "Azure OpenAI deployment type used for the default GPT and embedding model deployments.\n- Azure Commercial options: Standard, DatazoneStandard, GlobalStandard\n- Azure Government default model deployments use Standard regardless of this selection\n- Ignored when you provide custom gptModels or embeddingModels arrays" + } + }, + "customBlobStorageSuffix": { + "type": "string", + "defaultValue": "[format('blob.{0}', environment().suffixes.storage)]", + "metadata": { + "description": "Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net" + } + }, + "customGraphUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Graph API URL, e.g. https://graph.microsoft.us" + } + }, + "customIdentityUrl": { + "type": "string", + "defaultValue": "[environment().authentication.loginEndpoint]", + "metadata": { + "description": "Custom Identity URL, e.g. https://login.microsoftonline.us/" + } + }, + "customResourceManagerUrl": { + "type": "string", + "defaultValue": "[environment().resourceManager]", + "metadata": { + "description": "Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net" + } + }, + "customCognitiveServicesScope": { + "type": "string", + "defaultValue": "https://cognitiveservices.azure.com/.default", + "metadata": { + "description": "Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default" + } + }, + "customSearchResourceUrl": { + "type": "string", + "defaultValue": "https://search.azure.com", + "metadata": { + "description": "Custom search resource URL for token audience, e.g. https://search.azure.us" + } + }, "gptModels": { "type": "array", "defaultValue": [], @@ -188,6 +247,7 @@ }, "allowedIpAddresses": { "type": "string", + "defaultValue": "", "metadata": { "description": "Comma separated list of IP addresses or ranges to allow access to resources when private networking is enabled.\nLeave blank if not using private networking.\n- Format for single IP: 'x.x.x.x'\n- Format for range: 'x.x.x.x/y'\n- Example: 1.2.3.4, 2.3.4.5/32\n" } @@ -240,6 +300,7 @@ } } ], + "scCloudEnvironment": "[if(equals(parameters('cloudEnvironment'), 'AzureCloud'), 'public', if(equals(parameters('cloudEnvironment'), 'AzureUSGovernment'), 'usgovernment', parameters('cloudEnvironment')))]", "allowedIpAddressesSplit": "[if(empty(parameters('allowedIpAddresses')), createArray(), split(parameters('allowedIpAddresses'), ','))]", "rgName": "[format('{0}-{1}-rg', parameters('appName'), parameters('environment'))]", "requiredTags": { @@ -248,21 +309,24 @@ "azd-env-name": "[parameters('azdEnvironmentName')]" }, "tags": "[union(variables('requiredTags'), parameters('specialTags'))]", - "acrCloudSuffix": "[if(equals(parameters('cloudEnvironment'), 'AzureCloud'), '.azurecr.io', '.azurecr.us')]", + "isPublicCloud": "[equals(variables('scCloudEnvironment'), 'public')]", + "isUsGovernmentCloud": "[equals(variables('scCloudEnvironment'), 'usgovernment')]", + "acrCloudSuffix": "[if(variables('isPublicCloud'), '.azurecr.io', '.azurecr.us')]", "acrName": "[toLower(format('{0}{1}acr', parameters('appName'), parameters('environment')))]", "containerRegistry": "[format('{0}{1}', variables('acrName'), variables('acrCloudSuffix'))]", "containerImageName": "[format('{0}/{1}', variables('containerRegistry'), parameters('imageName'))]", "vNetName": "[format('{0}-{1}-vnet', parameters('appName'), parameters('environment'))]", "normalizedLocation": "[toLower(replace(parameters('location'), ' ', ''))]", + "resolvedOpenAIDeploymentType": "[if(variables('isUsGovernmentCloud'), 'Standard', parameters('openAIDeploymentType'))]", "defaultGptModels": [ { "modelName": "gpt-4o", - "modelVersion": "[if(equals(parameters('cloudEnvironment'), 'AzureUSGovernment'), '2024-05-13', '2024-11-20')]", - "skuName": "Standard", + "modelVersion": "[if(variables('isUsGovernmentCloud'), '2024-05-13', '2024-11-20')]", + "skuName": "[variables('resolvedOpenAIDeploymentType')]", "skuCapacity": 100 } ], - "defaultEmbeddingModels": "[if(equals(parameters('cloudEnvironment'), 'AzureUSGovernment'), createArray(createObject('modelName', if(equals(variables('normalizedLocation'), 'usgovvirginia'), 'text-embedding-ada-002', 'text-embedding-3-small'), 'modelVersion', if(equals(variables('normalizedLocation'), 'usgovvirginia'), '2', '1'), 'skuName', 'Standard', 'skuCapacity', 100)), createArray(createObject('modelName', 'text-embedding-3-small', 'modelVersion', '1', 'skuName', 'Standard', 'skuCapacity', 100)))]", + "defaultEmbeddingModels": "[if(variables('isUsGovernmentCloud'), createArray(createObject('modelName', if(equals(variables('normalizedLocation'), 'usgovvirginia'), 'text-embedding-ada-002', 'text-embedding-3-small'), 'modelVersion', if(equals(variables('normalizedLocation'), 'usgovvirginia'), '2', '1'), 'skuName', variables('resolvedOpenAIDeploymentType'), 'skuCapacity', 100)), createArray(createObject('modelName', 'text-embedding-3-small', 'modelVersion', '1', 'skuName', variables('resolvedOpenAIDeploymentType'), 'skuCapacity', 100)))]", "resolvedGptModels": "[if(empty(parameters('gptModels')), variables('defaultGptModels'), parameters('gptModels'))]", "resolvedEmbeddingModels": "[if(empty(parameters('embeddingModels')), variables('defaultEmbeddingModels'), parameters('embeddingModels'))]", "hasExistingAppServiceSubnetId": "[not(empty(parameters('existingAppServiceSubnetId')))]", @@ -272,15 +336,15 @@ "useExistingVirtualNetwork": "[and(parameters('enablePrivateNetworking'), or(or(not(empty(variables('resolvedExistingVirtualNetworkId'))), variables('hasExistingAppServiceSubnetId')), variables('hasExistingPrivateEndpointSubnetId')))]", "allowedIpsForCosmos": "[union(createArray('0.0.0.0'), variables('allowedIpAddressesArray'))]" }, - "resources": [ - { + "resources": { + "rg": { "type": "Microsoft.Resources/resourceGroups", "apiVersion": "2022-09-01", "name": "[variables('rgName')]", "location": "[parameters('location')]", "tags": "[variables('tags')]" }, - { + "virtualNetwork": { "condition": "[and(parameters('enablePrivateNetworking'), not(variables('useExistingVirtualNetwork')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -412,10 +476,10 @@ } }, "dependsOn": [ - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "rg" ] }, - { + "logAnalytics": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "logAnalytics", @@ -486,10 +550,10 @@ } }, "dependsOn": [ - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "rg" ] }, - { + "applicationInsights": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "applicationInsights", @@ -513,7 +577,7 @@ "value": "[variables('tags')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" } }, "template": { @@ -566,11 +630,11 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "logAnalytics", + "rg" ] }, - { + "keyVault": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "keyVault", @@ -597,7 +661,7 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" } }, "template": { @@ -821,11 +885,11 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "logAnalytics", + "rg" ] }, - { + "storeEnterpriseAppSecret": { "condition": "[not(empty(parameters('enterpriseAppClientSecret')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -838,7 +902,7 @@ "mode": "Incremental", "parameters": { "keyVaultName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "secretName": { "value": "enterprise-app-client-secret" @@ -887,11 +951,11 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "rg" ] }, - { + "cosmosDB": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "cosmosDB", @@ -918,10 +982,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -1275,12 +1339,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "acr": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "azureContainerRegistry", @@ -1304,10 +1368,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -1329,7 +1393,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "13779468623687321665" + "templateHash": "3987765018822910546" } }, "parameters": { @@ -1377,7 +1441,8 @@ "properties": { "adminUserEnabled": true, "publicNetworkAccess": "Enabled", - "networkRuleSet": "[if(parameters('enablePrivateNetworking'), createObject('defaultAction', 'Deny', 'ipRules', parameters('allowedIpAddresses')), null())]" + "networkRuleBypassOptions": "[if(parameters('enablePrivateNetworking'), 'AzureServices', 'None')]", + "networkRuleSet": "[if(parameters('enablePrivateNetworking'), createObject('defaultAction', 'Allow', 'ipRules', parameters('allowedIpAddresses')), null())]" }, "tags": "[parameters('tags')]" }, @@ -1611,12 +1676,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "searchService": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "searchService", @@ -1643,10 +1708,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -1957,12 +2022,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "docIntel": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "docIntel", @@ -1989,10 +2054,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -2296,12 +2361,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "storageAccount": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "storageAccount", @@ -2328,10 +2393,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -2681,12 +2746,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "openAI": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "openAI", @@ -2713,10 +2778,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -3161,12 +3226,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "appServicePlan": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "appServicePlan", @@ -3193,7 +3258,7 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" } }, "template": { @@ -3411,11 +3476,11 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "logAnalytics", + "rg" ] }, - { + "appService": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "appService", @@ -3439,43 +3504,43 @@ "value": "[variables('tags')]" }, "acrName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'azureContainerRegistry'), '2025-04-01').outputs.acrName.value]" + "value": "[reference('acr').outputs.acrName.value]" }, "enableDiagLogging": { "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "appServicePlanId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'appServicePlan'), '2025-04-01').outputs.appServicePlanId.value]" + "value": "[reference('appServicePlan').outputs.appServicePlanId.value]" }, "containerImageName": { "value": "[variables('containerImageName')]" }, "azurePlatform": { - "value": "[parameters('cloudEnvironment')]" + "value": "[variables('scCloudEnvironment')]" }, "cosmosDbName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'cosmosDB'), '2025-04-01').outputs.cosmosDbName.value]" + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" }, "searchServiceName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'searchService'), '2025-04-01').outputs.searchServiceName.value]" + "value": "[reference('searchService').outputs.searchServiceName.value]" }, "openAiServiceName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIName.value]" + "value": "[reference('openAI').outputs.openAIName.value]" }, "openAiEndpoint": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIEndpoint.value]" + "value": "[reference('openAI').outputs.openAIEndpoint.value]" }, "openAiResourceGroupName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIResourceGroup.value]" + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" }, "documentIntelligenceServiceName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'docIntel'), '2025-04-01').outputs.documentIntelligenceServiceName.value]" + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" }, "appInsightsName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'applicationInsights'), '2025-04-01').outputs.appInsightsName.value]" + "value": "[reference('applicationInsights').outputs.appInsightsName.value]" }, "enterpriseAppClientId": { "value": "[parameters('enterpriseAppClientId')]" @@ -3487,21 +3552,40 @@ "value": "[parameters('authenticationType')]" }, "keyVaultUri": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultUri.value]" + "value": "[reference('keyVault').outputs.keyVaultUri.value]" }, "enablePrivateNetworking": { "value": "[parameters('enablePrivateNetworking')]" }, - "appServiceSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingAppServiceSubnetId')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.appServiceSubnetId.value)), createObject('value', ''))]" + "appServiceSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingAppServiceSubnetId')), createObject('value', reference('virtualNetwork').outputs.appServiceSubnetId.value)), createObject('value', ''))]", + "customBlobStorageSuffix": { + "value": "[parameters('customBlobStorageSuffix')]" + }, + "customGraphUrl": { + "value": "[parameters('customGraphUrl')]" + }, + "customIdentityUrl": { + "value": "[parameters('customIdentityUrl')]" + }, + "customResourceManagerUrl": { + "value": "[parameters('customResourceManagerUrl')]" + }, + "customCognitiveServicesScope": { + "value": "[parameters('customCognitiveServicesScope')]" + }, + "customSearchResourceUrl": { + "value": "[parameters('customSearchResourceUrl')]" + } }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", "contentVersion": "1.0.0.0", "metadata": { "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "15155999637546305218" + "templateHash": "6496170582734407734" } }, "parameters": { @@ -3576,13 +3660,87 @@ "appServiceSubnetId": { "type": "string", "defaultValue": "" + }, + "customBlobStorageSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net" + } + }, + "customGraphUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Graph API URL, e.g. https://graph.microsoft.us" + } + }, + "customIdentityUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Identity URL, e.g. https://login.microsoftonline.us" + } + }, + "customResourceManagerUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net" + } + }, + "customCognitiveServicesScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default" + } + }, + "customSearchResourceUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom search resource URL for token audience, e.g. https://search.azure.us" + } } }, "variables": { - "acrDomain": "[if(equals(parameters('azurePlatform'), 'AzureUSGovernment'), '.azurecr.us', '.azurecr.io')]" + "tenantId": "[tenant().tenantId]", + "openIdMetadataUrl": "[format('{0}{1}/v2.0/.well-known/openid-configuration', environment().authentication.loginEndpoint, variables('tenantId'))]", + "acrDomain": "[environment().suffixes.acrLoginServer]" }, - "resources": [ - { + "resources": { + "acrService": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-04-01", + "name": "[parameters('acrName')]" + }, + "cosmosDb": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2023-04-15", + "name": "[parameters('cosmosDbName')]" + }, + "searchService": { + "existing": true, + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-05-01", + "name": "[parameters('searchServiceName')]" + }, + "documentIntelligence": { + "existing": true, + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[parameters('documentIntelligenceServiceName')]" + }, + "appInsights": { + "existing": true, + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[parameters('appInsightsName')]" + }, + "webApp": { "type": "Microsoft.Web/sites", "apiVersion": "2022-03-01", "name": "[toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))]", @@ -3600,7 +3758,7 @@ "alwaysOn": true, "ftpsState": "Disabled", "healthCheckPath": "/external/healthcheck", - "appSettings": "[flatten(createArray(createArray(createObject('name', 'AZURE_ENDPOINT', 'value', if(equals(parameters('azurePlatform'), 'AzureUSGovernment'), 'usgovernment', 'public')), createObject('name', 'SCM_DO_BUILD_DURING_DEPLOYMENT', 'value', 'false'), createObject('name', 'AZURE_COSMOS_ENDPOINT', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName')), '2023-04-15').documentEndpoint), createObject('name', 'AZURE_COSMOS_AUTHENTICATION_TYPE', 'value', toLower(parameters('authenticationType')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_COSMOS_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/cosmos-db-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'TENANT_ID', 'value', tenant().tenantId), createObject('name', 'CLIENT_ID', 'value', parameters('enterpriseAppClientId')), createObject('name', 'SECRET_KEY', 'value', if(not(empty(parameters('enterpriseAppClientSecret'))), parameters('enterpriseAppClientSecret'), format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri')))), createObject('name', 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri'))), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', format('https://{0}{1}', parameters('acrName'), variables('acrDomain')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_USERNAME', 'value', listCredentials(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), '2025-04-01').username)), createArray()), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_PASSWORD', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/container-registry-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', 'value', tenant().tenantId), createObject('name', 'AZURE_OPENAI_RESOURCE_NAME', 'value', parameters('openAiServiceName')), createObject('name', 'AZURE_OPENAI_RESOURCE_GROUP_NAME', 'value', parameters('openAiResourceGroupName')), createObject('name', 'AZURE_OPENAI_URL', 'value', parameters('openAiEndpoint')), createObject('name', 'AZURE_SEARCH_SERVICE_NAME', 'value', parameters('searchServiceName'))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_SEARCH_API_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/search-service-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', 'value', reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('documentIntelligenceServiceName')), '2025-06-01').endpoint)), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/document-intelligence-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'APPINSIGHTS_INSTRUMENTATIONKEY', 'value', reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').InstrumentationKey), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').ConnectionString), createObject('name', 'APPINSIGHTS_PROFILERFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPLICATIONINSIGHTS_CONFIGURATION_CONTENT', 'value', ''), createObject('name', 'ApplicationInsightsAgent_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'DiagnosticServices_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'InstrumentationEngine_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'SnapshotDebugger_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_BaseExtensions', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_Mode', 'value', 'recommended'), createObject('name', 'XDT_MicrosoftApplicationInsights_PreemptSdk', 'value', 'disabled'))))]" + "appSettings": "[flatten(createArray(createArray(createObject('name', 'AZURE_ENVIRONMENT', 'value', parameters('azurePlatform')), createObject('name', 'SCM_DO_BUILD_DURING_DEPLOYMENT', 'value', 'false'), createObject('name', 'AZURE_COSMOS_ENDPOINT', 'value', reference('cosmosDb').documentEndpoint), createObject('name', 'AZURE_COSMOS_AUTHENTICATION_TYPE', 'value', toLower(parameters('authenticationType')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_COSMOS_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/cosmos-db-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'TENANT_ID', 'value', tenant().tenantId), createObject('name', 'CLIENT_ID', 'value', parameters('enterpriseAppClientId')), createObject('name', 'SECRET_KEY', 'value', if(not(empty(parameters('enterpriseAppClientSecret'))), parameters('enterpriseAppClientSecret'), format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri')))), createObject('name', 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri'))), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', format('https://{0}{1}', parameters('acrName'), variables('acrDomain')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_USERNAME', 'value', listCredentials('acrService', '2025-04-01').username)), createArray()), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_PASSWORD', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/container-registry-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', 'value', tenant().tenantId), createObject('name', 'AZURE_OPENAI_RESOURCE_NAME', 'value', parameters('openAiServiceName')), createObject('name', 'AZURE_OPENAI_RESOURCE_GROUP_NAME', 'value', parameters('openAiResourceGroupName')), createObject('name', 'AZURE_OPENAI_URL', 'value', parameters('openAiEndpoint')), createObject('name', 'AZURE_SEARCH_SERVICE_NAME', 'value', parameters('searchServiceName'))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_SEARCH_API_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/search-service-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', 'value', reference('documentIntelligence').endpoint)), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', 'value', format('@Microsoft.KeyVault(SecretUri={0}secrets/document-intelligence-key)', parameters('keyVaultUri')))), createArray()), createArray(createObject('name', 'APPINSIGHTS_INSTRUMENTATIONKEY', 'value', reference('appInsights').InstrumentationKey), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference('appInsights').ConnectionString), createObject('name', 'APPINSIGHTS_PROFILERFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPLICATIONINSIGHTS_CONFIGURATION_CONTENT', 'value', ''), createObject('name', 'ApplicationInsightsAgent_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'DiagnosticServices_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'InstrumentationEngine_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'SnapshotDebugger_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_BaseExtensions', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_Mode', 'value', 'recommended'), createObject('name', 'XDT_MicrosoftApplicationInsights_PreemptSdk', 'value', 'disabled')), if(equals(parameters('azurePlatform'), 'custom'), createArray(createObject('name', 'CUSTOM_GRAPH_URL_VALUE', 'value', coalesce(parameters('customGraphUrl'), '')), createObject('name', 'CUSTOM_IDENTITY_URL_VALUE', 'value', coalesce(parameters('customIdentityUrl'), '')), createObject('name', 'CUSTOM_RESOURCE_MANAGER_URL_VALUE', 'value', coalesce(parameters('customResourceManagerUrl'), '')), createObject('name', 'CUSTOM_BLOB_STORAGE_URL_VALUE', 'value', coalesce(parameters('customBlobStorageSuffix'), '')), createObject('name', 'CUSTOM_COGNITIVE_SERVICES_URL_VALUE', 'value', coalesce(parameters('customCognitiveServicesScope'), '')), createObject('name', 'CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE', 'value', coalesce(parameters('customSearchResourceUrl'), '')), createObject('name', 'KEY_VAULT_DOMAIN', 'value', environment().suffixes.keyvaultDns), createObject('name', 'CUSTOM_OIDC_METADATA_URL_VALUE', 'value', coalesce(variables('openIdMetadataUrl'), ''))), createArray())))]" }, "clientAffinityEnabled": false, "httpsOnly": true @@ -3608,9 +3766,14 @@ "identity": { "type": "SystemAssigned" }, - "tags": "[union(parameters('tags'), createObject('azd-service-name', 'web'))]" + "tags": "[union(parameters('tags'), createObject('azd-service-name', 'web'))]", + "dependsOn": [ + "appInsights", + "cosmosDb", + "documentIntelligence" + ] }, - { + "webAppLogging": { "type": "Microsoft.Web/sites/config", "apiVersion": "2022-03-01", "name": "[format('{0}/{1}', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))), 'logs')]", @@ -3624,10 +3787,10 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]" + "webApp" ] }, - { + "webAppDiagnostics": { "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", @@ -3635,15 +3798,15 @@ "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))))]", "properties": { "workspaceId": "[parameters('logAnalyticsId')]", - "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.webAppLogCategories.value]", - "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + "logs": "[reference('diagnosticConfigs').outputs.webAppLogCategories.value]", + "metrics": "[reference('diagnosticConfigs').outputs.standardMetricsCategories.value]" }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", - "[resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]" + "diagnosticConfigs", + "webApp" ] }, - { + "authSettings": { "type": "Microsoft.Web/sites/config", "apiVersion": "2022-03-01", "name": "[format('{0}/{1}', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))), 'authsettingsV2')]", @@ -3657,7 +3820,7 @@ "azureActiveDirectory": { "enabled": true, "registration": { - "openIdIssuer": "[if(equals(parameters('azurePlatform'), 'AzureUSGovernment'), format('https://login.microsoftonline.us/{0}/', tenant().tenantId), format('https://sts.windows.net/{0}/', tenant().tenantId))]", + "openIdIssuer": "[format('{0}{1}/', environment().authentication.loginEndpoint, tenant().tenantId)]", "clientId": "[parameters('enterpriseAppClientId')]", "clientSecretSettingName": "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" }, @@ -3704,10 +3867,10 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]" + "webApp" ] }, - { + "diagnosticConfigs": { "condition": "[parameters('enableDiagLogging')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -3843,7 +4006,7 @@ } } } - ], + }, "outputs": { "name": { "type": "string", @@ -3851,7 +4014,7 @@ }, "defaultHostName": { "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))), '2022-03-01').defaultHostName]" + "value": "[reference('webApp').defaultHostName]" }, "resourceId": { "type": "string", @@ -3861,20 +4024,20 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'azureContainerRegistry')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'applicationInsights')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'appServicePlan')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'cosmosDB')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'docIntel')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'searchService')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork')]" + "acr", + "applicationInsights", + "appServicePlan", + "cosmosDB", + "docIntel", + "keyVault", + "logAnalytics", + "openAI", + "rg", + "searchService", + "virtualNetwork" ] }, - { + "contentSafety": { "condition": "[parameters('deployContentSafety')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -3902,10 +4065,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -4205,12 +4368,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "redisCache": { "condition": "[parameters('deployRedisCache')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -4238,10 +4401,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -4539,12 +4702,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "speechService": { "condition": "[parameters('deploySpeechService')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -4572,10 +4735,10 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "keyVault": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -4882,12 +5045,12 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + "keyVault", + "logAnalytics", + "rg" ] }, - { + "videoIndexerService": { "condition": "[parameters('deployVideoIndexerService')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -4915,13 +5078,13 @@ "value": "[parameters('enableDiagLogging')]" }, "logAnalyticsId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics'), '2025-04-01').outputs.logAnalyticsId.value]" + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" }, "storageAccount": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storageAccount'), '2025-04-01').outputs.name.value]" + "value": "[reference('storageAccount').outputs.name.value]" }, "openAiServiceName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIName.value]" + "value": "[reference('openAI').outputs.openAIName.value]" }, "enablePrivateNetworking": { "value": "[parameters('enablePrivateNetworking')]" @@ -5151,13 +5314,13 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'logAnalytics')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storageAccount')]" + "logAnalytics", + "openAI", + "rg", + "storageAccount" ] }, - { + "setPermissions": { "condition": "[parameters('configureApplicationPermissions')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -5170,7 +5333,7 @@ "mode": "Incremental", "parameters": { "webAppName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'appService'), '2025-04-01').outputs.name.value]" + "value": "[reference('appService').outputs.name.value]" }, "authenticationType": { "value": "[parameters('authenticationType')]" @@ -5179,36 +5342,36 @@ "value": "[parameters('enterpriseAppServicePrincipalId')]" }, "keyVaultName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "cosmosDBName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'cosmosDB'), '2025-04-01').outputs.cosmosDbName.value]" + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" }, "acrName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'azureContainerRegistry'), '2025-04-01').outputs.acrName.value]" + "value": "[reference('acr').outputs.acrName.value]" }, "openAIName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIName.value]" + "value": "[reference('openAI').outputs.openAIName.value]" }, "openAIResourceGroupName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIResourceGroup.value]" + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" }, "openAISubscriptionId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAISubscriptionId.value]" + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" }, "docIntelName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'docIntel'), '2025-04-01').outputs.documentIntelligenceServiceName.value]" + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" }, "storageAccountName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storageAccount'), '2025-04-01').outputs.name.value]" + "value": "[reference('storageAccount').outputs.name.value]" }, "searchServiceName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'searchService'), '2025-04-01').outputs.searchServiceName.value]" + "value": "[reference('searchService').outputs.searchServiceName.value]" }, - "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'speechService'), '2025-04-01').outputs.speechServiceName.value), createObject('value', ''))]", - "redisCacheName": "[if(parameters('deployRedisCache'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'redisCache'), '2025-04-01').outputs.redisCacheName.value), createObject('value', ''))]", - "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'contentSafety'), '2025-04-01').outputs.contentSafetyName.value), createObject('value', ''))]", - "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'videoIndexerService'), '2025-04-01').outputs.videoIndexerServiceName.value), createObject('value', ''))]" + "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference('speechService').outputs.speechServiceName.value), createObject('value', ''))]", + "redisCacheName": "[if(parameters('deployRedisCache'), createObject('value', reference('redisCache').outputs.redisCacheName.value), createObject('value', ''))]", + "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference('contentSafety').outputs.contentSafetyName.value), createObject('value', ''))]", + "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference('videoIndexerService').outputs.videoIndexerServiceName.value), createObject('value', ''))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -5582,22 +5745,22 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'azureContainerRegistry')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'appService')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'contentSafety')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'cosmosDB')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'docIntel')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'redisCache')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'searchService')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'speechService')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storageAccount')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'videoIndexerService')]" + "acr", + "appService", + "contentSafety", + "cosmosDB", + "docIntel", + "keyVault", + "openAI", + "redisCache", + "rg", + "searchService", + "speechService", + "storageAccount", + "videoIndexerService" ] }, - { + "privateNetworking": { "condition": "[parameters('enablePrivateNetworking')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -5609,8 +5772,8 @@ }, "mode": "Incremental", "parameters": { - "virtualNetworkId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', variables('resolvedExistingVirtualNetworkId')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.vNetId.value)), createObject('value', ''))]", - "privateEndpointSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingPrivateEndpointSubnetId')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork'), '2025-04-01').outputs.privateNetworkSubnetId.value)), createObject('value', ''))]", + "virtualNetworkId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', variables('resolvedExistingVirtualNetworkId')), createObject('value', reference('virtualNetwork').outputs.vNetId.value)), createObject('value', ''))]", + "privateEndpointSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingPrivateEndpointSubnetId')), createObject('value', reference('virtualNetwork').outputs.privateNetworkSubnetId.value)), createObject('value', ''))]", "privateDnsZoneConfigs": { "value": "[parameters('privateDnsZoneConfigs')]" }, @@ -5627,38 +5790,38 @@ "value": "[variables('tags')]" }, "keyVaultName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "cosmosDBName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'cosmosDB'), '2025-04-01').outputs.cosmosDbName.value]" + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" }, "acrName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'azureContainerRegistry'), '2025-04-01').outputs.acrName.value]" + "value": "[reference('acr').outputs.acrName.value]" }, "searchServiceName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'searchService'), '2025-04-01').outputs.searchServiceName.value]" + "value": "[reference('searchService').outputs.searchServiceName.value]" }, "docIntelName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'docIntel'), '2025-04-01').outputs.documentIntelligenceServiceName.value]" + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" }, "storageAccountName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storageAccount'), '2025-04-01').outputs.name.value]" + "value": "[reference('storageAccount').outputs.name.value]" }, "openAIName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIName.value]" + "value": "[reference('openAI').outputs.openAIName.value]" }, "openAIResourceGroupName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIResourceGroup.value]" + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" }, "openAISubscriptionId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAISubscriptionId.value]" + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" }, "webAppName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'appService'), '2025-04-01').outputs.name.value]" + "value": "[reference('appService').outputs.name.value]" }, - "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'contentSafety'), '2025-04-01').outputs.contentSafetyName.value), createObject('value', ''))]", - "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'speechService'), '2025-04-01').outputs.speechServiceName.value), createObject('value', ''))]", - "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'videoIndexerService'), '2025-04-01').outputs.videoIndexerServiceName.value), createObject('value', ''))]" + "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference('contentSafety').outputs.contentSafetyName.value), createObject('value', ''))]", + "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference('speechService').outputs.speechServiceName.value), createObject('value', ''))]", + "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference('videoIndexerService').outputs.videoIndexerServiceName.value), createObject('value', ''))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -8713,22 +8876,22 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'azureContainerRegistry')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'appService')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'contentSafety')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'cosmosDB')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'docIntel')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI')]", - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'searchService')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'speechService')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storageAccount')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'videoIndexerService')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'virtualNetwork')]" + "acr", + "appService", + "contentSafety", + "cosmosDB", + "docIntel", + "keyVault", + "openAI", + "rg", + "searchService", + "speechService", + "storageAccount", + "videoIndexerService", + "virtualNetwork" ] } - ], + }, "outputs": { "var_acrName": { "type": "string", @@ -8740,7 +8903,7 @@ }, "var_blobStorageEndpoint": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storageAccount'), '2025-04-01').outputs.endpoint.value]" + "value": "[reference('storageAccount').outputs.endpoint.value]" }, "var_configureApplication": { "type": "bool", @@ -8748,35 +8911,35 @@ }, "var_contentSafetyEndpoint": { "type": "string", - "value": "[if(parameters('deployContentSafety'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'contentSafety'), '2025-04-01').outputs.contentSafetyEndpoint.value, '')]" + "value": "[if(parameters('deployContentSafety'), reference('contentSafety').outputs.contentSafetyEndpoint.value, '')]" }, "var_cosmosDb_accountName": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'cosmosDB'), '2025-04-01').outputs.cosmosDbName.value]" + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" }, "var_cosmosDb_uri": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'cosmosDB'), '2025-04-01').outputs.cosmosDbUri.value]" + "value": "[reference('cosmosDB').outputs.cosmosDbUri.value]" }, "var_deploymentLocation": { "type": "string", - "value": "[reference(subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName')), '2022-09-01', 'full').location]" + "value": "[reference('rg', '2022-09-01', 'full').location]" }, "var_documentIntelligenceServiceEndpoint": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'docIntel'), '2025-04-01').outputs.documentIntelligenceServiceEndpoint.value]" + "value": "[reference('docIntel').outputs.documentIntelligenceServiceEndpoint.value]" }, "var_keyVaultName": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultName.value]" + "value": "[reference('keyVault').outputs.keyVaultName.value]" }, "var_keyVaultUri": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyVault'), '2025-04-01').outputs.keyVaultUri.value]" + "value": "[reference('keyVault').outputs.keyVaultUri.value]" }, "var_openAIEndpoint": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIEndpoint.value]" + "value": "[reference('openAI').outputs.openAIEndpoint.value]" }, "var_openAIGPTModels": { "type": "array", @@ -8784,7 +8947,7 @@ }, "var_openAIResourceGroup": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAIResourceGroup.value]" + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" }, "var_openAIEmbeddingModels": { "type": "array", @@ -8792,11 +8955,11 @@ }, "var_openAISubscriptionId": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'openAI'), '2025-04-01').outputs.openAISubscriptionId.value]" + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" }, "var_redisCacheHostName": { "type": "string", - "value": "[if(parameters('deployRedisCache'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'redisCache'), '2025-04-01').outputs.redisCacheHostName.value, '')]" + "value": "[if(parameters('deployRedisCache'), reference('redisCache').outputs.redisCacheHostName.value, '')]" }, "var_rgName": { "type": "string", @@ -8804,11 +8967,11 @@ }, "var_searchServiceEndpoint": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'searchService'), '2025-04-01').outputs.searchServiceEndpoint.value]" + "value": "[reference('searchService').outputs.searchServiceEndpoint.value]" }, "var_speechServiceEndpoint": { "type": "string", - "value": "[if(parameters('deploySpeechService'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'speechService'), '2025-04-01').outputs.speechServiceEndpoint.value, '')]" + "value": "[if(parameters('deploySpeechService'), reference('speechService').outputs.speechServiceEndpoint.value, '')]" }, "var_subscriptionId": { "type": "string", @@ -8816,11 +8979,11 @@ }, "var_videoIndexerAccountId": { "type": "string", - "value": "[if(parameters('deployVideoIndexerService'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'videoIndexerService'), '2025-04-01').outputs.videoIndexerAccountId.value, '')]" + "value": "[if(parameters('deployVideoIndexerService'), reference('videoIndexerService').outputs.videoIndexerAccountId.value, '')]" }, "var_videoIndexerName": { "type": "string", - "value": "[if(parameters('deployVideoIndexerService'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'videoIndexerService'), '2025-04-01').outputs.videoIndexerServiceName.value, '')]" + "value": "[if(parameters('deployVideoIndexerService'), reference('videoIndexerService').outputs.videoIndexerServiceName.value, '')]" }, "var_containerRegistry": { "type": "string", @@ -8836,7 +8999,7 @@ }, "var_webService": { "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'appService'), '2025-04-01').outputs.name.value]" + "value": "[reference('appService').outputs.name.value]" }, "var_enablePrivateNetworking": { "type": "bool", diff --git a/deployers/terraform/main.tf b/deployers/terraform/main.tf index 96daee52b..e4827f03f 100644 --- a/deployers/terraform/main.tf +++ b/deployers/terraform/main.tf @@ -449,7 +449,7 @@ resource "azurerm_linux_web_app" "app" { } app_settings = { - "AZURE_ENDPOINT" = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : "public" + "AZURE_ENVIRONMENT" = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : "public" "SCM_DO_BUILD_DURING_DEPLOYMENT" = "false" "WEBSITE_PULL_IMAGE_OVER_VNET" = var.param_enable_private_networking ? "true" : "false" "AZURE_COSMOS_AUTHENTICATION_TYPE" = "key" From a2b129f94a9801bcfce4990c7de4490a713c9b8a Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 25 Mar 2026 12:28:02 -0400 Subject: [PATCH 3/7] updated logic for key usage --- application/single_app/config.py | 2 +- deployers/bicep/main.bicep | 36 - deployers/bicep/main.parameters.json | 3 - deployers/bicep/modules/appService.bicep | 8 +- .../modules/azureContainerRegistry.bicep | 16 - deployers/bicep/modules/contentSafety.bicep | 16 - deployers/bicep/modules/cosmosDb.bicep | 16 - .../bicep/modules/documentIntelligence.bicep | 16 - deployers/bicep/modules/openAI.bicep | 19 - deployers/bicep/modules/redisCache.bicep | 16 - deployers/bicep/modules/search.bicep | 17 - deployers/bicep/modules/speechService.bicep | 17 - deployers/bicep/postconfig.py | 288 +- deployers/logs.txt | 17675 ++++++++++++++++ .../fixes/CORE_SERVICE_KEY_DEPLOYMENT_FIX.md | 76 + .../test_core_service_key_deployment_path.py | 168 + 16 files changed, 18147 insertions(+), 242 deletions(-) create mode 100644 deployers/logs.txt create mode 100644 docs/explanation/fixes/CORE_SERVICE_KEY_DEPLOYMENT_FIX.md create mode 100644 functional_tests/test_core_service_key_deployment_path.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 8748c9780..7d1df5a31 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.240.001" +VERSION = "0.240.002" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index a226b9b0a..e471c38c7 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -125,11 +125,6 @@ param existingOpenAIResourceGroup string = '' - Used when reusing a standard Azure OpenAI resource across subscriptions''') param existingOpenAISubscriptionId string = '' -@description('''Optional API key for an existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint. -- Required for key-based authentication when reusing an external endpoint and automatic key retrieval is not available''') -@secure() -param existingOpenAIKey string = '' - @description('''Azure OpenAI deployment type used for the default GPT and embedding model deployments. - Azure Commercial options: Standard, DatazoneStandard, GlobalStandard - Azure Government default model deployments use Standard regardless of this selection @@ -363,9 +358,6 @@ module cosmosDB 'modules/cosmosDb.bicep' = { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions enablePrivateNetworking: enablePrivateNetworking allowedIpAddresses: cosmosDbIpRules } @@ -384,9 +376,6 @@ module acr 'modules/azureContainerRegistry.bicep' = { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions enablePrivateNetworking: enablePrivateNetworking allowedIpAddresses: acrIpRules } @@ -406,10 +395,6 @@ module searchService 'modules/search.bicep' = { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions - enablePrivateNetworking: enablePrivateNetworking } } @@ -428,10 +413,6 @@ module docIntel 'modules/documentIntelligence.bicep' = { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions - enablePrivateNetworking: enablePrivateNetworking } } @@ -472,15 +453,10 @@ module openAI 'modules/openAI.bicep' = { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions - existingOpenAIEndpoint: existingOpenAIEndpoint existingOpenAIResourceName: existingOpenAIResourceName existingOpenAIResourceGroup: existingOpenAIResourceGroup existingOpenAISubscriptionId: existingOpenAISubscriptionId - existingOpenAIKey: existingOpenAIKey gptModels: resolvedGptModels embeddingModels: resolvedEmbeddingModels @@ -564,10 +540,6 @@ module contentSafety 'modules/contentSafety.bicep' = if (deployContentSafety) { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions - enablePrivateNetworking: enablePrivateNetworking } } @@ -586,10 +558,6 @@ module redisCache 'modules/redisCache.bicep' = if (deployRedisCache) { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions - //enablePrivateNetworking: enablePrivateNetworking } } @@ -608,10 +576,6 @@ module speechService 'modules/speechService.bicep' = if (deploySpeechService) { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions - enablePrivateNetworking: enablePrivateNetworking } } diff --git a/deployers/bicep/main.parameters.json b/deployers/bicep/main.parameters.json index cd4ccc517..e7907f703 100644 --- a/deployers/bicep/main.parameters.json +++ b/deployers/bicep/main.parameters.json @@ -71,9 +71,6 @@ "existingOpenAISubscriptionId": { "value": "${EXISTING_AZURE_OPENAI_SUBSCRIPTION_ID}" }, - "existingOpenAIKey": { - "value": "${EXISTING_AZURE_OPENAI_KEY}" - }, "openAIDeploymentType": { "value": "${OPENAI_DEPLOYMENT_TYPE}" }, diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index edd4ed726..5ce6b1908 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -101,7 +101,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { // Only add this setting if authenticationType is 'key' ...(authenticationType == 'key' - ? [{ name: 'AZURE_COSMOS_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)' }] + ? [{ name: 'AZURE_COSMOS_KEY', value: cosmosDb.listKeys().primaryMasterKey }] : []) { name: 'TENANT_ID', value: tenant().tenantId } @@ -128,7 +128,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { ? [ { name: 'DOCKER_REGISTRY_SERVER_PASSWORD' - value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/container-registry-key)' + value: acrService.listCredentials().passwords[0].value } ] : []) @@ -143,7 +143,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { ? [ { name: 'AZURE_SEARCH_API_KEY' - value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/search-service-key)' + value: searchService.listAdminKeys().primaryKey } ] : []) @@ -153,7 +153,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { ? [ { name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY' - value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/document-intelligence-key)' + value: documentIntelligence.listKeys().key1 } ] : []) diff --git a/deployers/bicep/modules/azureContainerRegistry.bicep b/deployers/bicep/modules/azureContainerRegistry.bicep index 2525ffad7..a2e6caeb8 100644 --- a/deployers/bicep/modules/azureContainerRegistry.bicep +++ b/deployers/bicep/modules/azureContainerRegistry.bicep @@ -7,10 +7,6 @@ param tags object param enableDiagLogging bool param logAnalyticsId string -param keyVault string -param authenticationType string -param configureApplicationPermissions bool - param enablePrivateNetworking bool param allowedIpAddresses array = [] @@ -39,18 +35,6 @@ resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { tags: tags } -//========================================================= -// store container registry keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module containerRegistrySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { - name: 'storeContainerRegistrySecret' - params: { - keyVaultName: keyVault - secretName: 'container-registry-key' - secretValue: acr.listCredentials().passwords[0].value - } -} - // configure diagnostic settings for azure container registry resource acrDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${acr.name}-diagnostics') diff --git a/deployers/bicep/modules/contentSafety.bicep b/deployers/bicep/modules/contentSafety.bicep index 02ba06c7f..adb809572 100644 --- a/deployers/bicep/modules/contentSafety.bicep +++ b/deployers/bicep/modules/contentSafety.bicep @@ -8,10 +8,6 @@ param tags object param enableDiagLogging bool param logAnalyticsId string -param keyVault string -param authenticationType string -param configureApplicationPermissions bool - param enablePrivateNetworking bool // Import diagnostic settings configurations @@ -47,17 +43,5 @@ resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 } } -//========================================================= -// store contentSafety keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module contentSafetySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { - name: 'storeContentSafetySecret' - params: { - keyVaultName: keyVault - secretName: 'content-safety-key' - secretValue: contentSafety.listKeys().key1 - } -} - output contentSafetyName string = contentSafety.name output contentSafetyEndpoint string = contentSafety.properties.endpoint diff --git a/deployers/bicep/modules/cosmosDb.bicep b/deployers/bicep/modules/cosmosDb.bicep index abb6f6748..0eca4cdf8 100644 --- a/deployers/bicep/modules/cosmosDb.bicep +++ b/deployers/bicep/modules/cosmosDb.bicep @@ -8,10 +8,6 @@ param tags object param enableDiagLogging bool param logAnalyticsId string -param keyVault string -param authenticationType string -param configureApplicationPermissions bool - param enablePrivateNetworking bool param allowedIpAddresses array = [] @@ -89,17 +85,5 @@ resource cosmosDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre } } -//========================================================= -// store cosmos db keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module storeCosmosDbSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { - name: 'storeCosmosDbSecret' - params: { - keyVaultName: keyVault - secretName: 'cosmos-db-key' - secretValue: cosmosDb.listKeys().primaryMasterKey - } -} - output cosmosDbName string = cosmosDb.name output cosmosDbUri string = cosmosDb.properties.documentEndpoint diff --git a/deployers/bicep/modules/documentIntelligence.bicep b/deployers/bicep/modules/documentIntelligence.bicep index b340c7950..0bb52bfdc 100644 --- a/deployers/bicep/modules/documentIntelligence.bicep +++ b/deployers/bicep/modules/documentIntelligence.bicep @@ -8,10 +8,6 @@ param tags object param enableDiagLogging bool param logAnalyticsId string -param keyVault string -param authenticationType string -param configureApplicationPermissions bool - param enablePrivateNetworking bool // Import diagnostic settings configurations @@ -47,18 +43,6 @@ resource docIntelDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-p } } -//========================================================= -// store document intelligence keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module documentIntelligenceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { - name: 'storeDocumentIntelligenceSecret' - params: { - keyVaultName: keyVault - secretName: 'document-intelligence-key' - secretValue: docIntel.listKeys().key1 - } -} - output documentIntelligenceServiceName string = docIntel.name output diagnosticLoggingEnabled bool = enableDiagLogging output documentIntelligenceServiceEndpoint string = docIntel.properties.endpoint diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index bf34428ec..78fbfc8ec 100644 --- a/deployers/bicep/modules/openAI.bicep +++ b/deployers/bicep/modules/openAI.bicep @@ -8,17 +8,11 @@ param tags object param enableDiagLogging bool param logAnalyticsId string -param keyVault string -param authenticationType string -param configureApplicationPermissions bool param existingOpenAIEndpoint string = '' param existingOpenAIResourceName string = '' param existingOpenAIResourceGroup string = '' param existingOpenAISubscriptionId string = '' -@secure() -param existingOpenAIKey string = '' - param gptModels array param embeddingModels array @@ -79,19 +73,6 @@ module aiModel 'aiModel.bicep' = [ } ] -//========================================================= -// store openAI keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module openAISecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions && (!useExistingOpenAIEndpoint || !empty(existingOpenAIKey))) { - name: 'storeOpenAISecret' - params: { - keyVaultName: keyVault - secretName: 'openAi-key' - #disable-next-line BCP422 // guarded by useExistingOpenAIEndpoint condition above - secretValue: useExistingOpenAIEndpoint ? existingOpenAIKey : openAI.listKeys().key1 - } -} - output openAIName string = useExistingOpenAIEndpoint ? existingOpenAIResourceName : openAI.name output openAIResourceGroup string = resolvedOpenAIResourceGroup output openAISubscriptionId string = resolvedOpenAISubscriptionId diff --git a/deployers/bicep/modules/redisCache.bicep b/deployers/bicep/modules/redisCache.bicep index e782318ae..7c67acfb6 100644 --- a/deployers/bicep/modules/redisCache.bicep +++ b/deployers/bicep/modules/redisCache.bicep @@ -8,10 +8,6 @@ param tags object param enableDiagLogging bool param logAnalyticsId string -param keyVault string -param authenticationType string -param configureApplicationPermissions bool - // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' @@ -49,17 +45,5 @@ resource redisCacheDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01 } } -//========================================================= -// store redis cache keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module redisCacheSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { - name: 'storeRedisCacheSecret' - params: { - keyVaultName: keyVault - secretName: 'redis-cache-key' - secretValue: redisCache.listKeys().primaryKey - } -} - output redisCacheName string = redisCache.name output redisCacheHostName string = redisCache.properties.hostName diff --git a/deployers/bicep/modules/search.bicep b/deployers/bicep/modules/search.bicep index c474ade57..0a0d1cdf8 100644 --- a/deployers/bicep/modules/search.bicep +++ b/deployers/bicep/modules/search.bicep @@ -8,10 +8,6 @@ param tags object param enableDiagLogging bool param logAnalyticsId string -param keyVault string -param authenticationType string -param configureApplicationPermissions bool - param enablePrivateNetworking bool // Import diagnostic settings configurations @@ -53,18 +49,5 @@ resource searchDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre } } -//========================================================= -// store search Service keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module searchServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { - name: 'storeSearchServiceSecret' - params: { - keyVaultName: keyVault - secretName: 'search-service-key' - secretValue: searchService.listAdminKeys().primaryKey - } -} - output searchServiceName string = searchService.name output searchServiceEndpoint string = searchService.properties.endpoint -output searchServiceAuthencationType string = authenticationType diff --git a/deployers/bicep/modules/speechService.bicep b/deployers/bicep/modules/speechService.bicep index d7f1de1fd..04cd6bfba 100644 --- a/deployers/bicep/modules/speechService.bicep +++ b/deployers/bicep/modules/speechService.bicep @@ -8,10 +8,6 @@ param tags object param enableDiagLogging bool param logAnalyticsId string -param keyVault string -param authenticationType string -param configureApplicationPermissions bool - param enablePrivateNetworking bool // Import diagnostic settings configurations @@ -50,18 +46,5 @@ resource speechServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 } } -//========================================================= -// store speech Service keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module speechServiceSecret 'keyVault-Secrets.bicep' = if ((authenticationType == 'key') && (configureApplicationPermissions)) { - name: 'storeSpeechServiceSecret' - params: { - keyVaultName: keyVault - secretName: 'speech-service-key' - secretValue: speechService.listKeys().key1 - } -} - output speechServiceName string = speechService.name output speechServiceEndpoint string = speechService.properties.endpoint -output speechServiceAuthenticationType string = authenticationType diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index f79ffce03..7289c5d3a 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -1,12 +1,206 @@ +# postconfig.py from azure.cosmos import CosmosClient from azure.cosmos.exceptions import CosmosResourceNotFoundError from azure.identity import AzureCliCredential -from azure.keyvault.secrets import SecretClient -import os import json +import os +import shutil +import subprocess +from urllib.parse import urlparse credential = AzureCliCredential() + +def get_azure_cli_executable(): + """Resolve the Azure CLI executable path for subprocess usage.""" + configured_path = os.getenv("AZURE_CLI_PATH") + if configured_path and os.path.exists(configured_path): + return configured_path + + candidate_names = ["az.cmd", "az.exe", "az"] + for candidate_name in candidate_names: + resolved_path = shutil.which(candidate_name) + if resolved_path: + return resolved_path + + windows_fallback_paths = [ + r"C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin\az.cmd", + r"C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin\az.cmd", + ] + for fallback_path in windows_fallback_paths: + if os.path.exists(fallback_path): + return fallback_path + + raise FileNotFoundError( + "Azure CLI executable was not found. Set AZURE_CLI_PATH or ensure az.cmd is installed and available on PATH." + ) + + +def run_azure_cli_command(command_args, description): + """Run an Azure CLI command and return the trimmed result.""" + azure_cli_executable = get_azure_cli_executable() + result = subprocess.run( + [azure_cli_executable, *command_args], + capture_output=True, + text=True, + check=False, + ) + + if result.returncode != 0: + raise RuntimeError( + f"Failed to retrieve {description}: {result.stderr.strip() or result.stdout.strip()}" + ) + + value = result.stdout.strip() + if not value: + raise RuntimeError(f"Failed to retrieve {description}: empty response from Azure CLI") + + print(f"Retrieved {description}") + return value + + +def extract_resource_name_from_endpoint(endpoint): + """Extract the Azure resource name from a standard service endpoint.""" + parsed_endpoint = urlparse(endpoint or "") + hostname = parsed_endpoint.netloc or parsed_endpoint.path + + if not hostname: + return "" + + return hostname.split(".")[0] + + +def get_cognitive_services_key(resource_name, resource_group, subscription_id, description): + return run_azure_cli_command( + [ + "cognitiveservices", + "account", + "keys", + "list", + "--name", + resource_name, + "--resource-group", + resource_group, + "--subscription", + subscription_id, + "--query", + "key1", + "-o", + "tsv", + ], + description, + ) + + +def get_search_service_key(resource_name, resource_group, subscription_id): + return run_azure_cli_command( + [ + "search", + "admin-key", + "show", + "--service-name", + resource_name, + "--resource-group", + resource_group, + "--subscription", + subscription_id, + "--query", + "primaryKey", + "-o", + "tsv", + ], + "Azure AI Search admin key", + ) + + +def get_redis_cache_key(resource_name, resource_group, subscription_id): + return run_azure_cli_command( + [ + "redis", + "list-keys", + "--name", + resource_name, + "--resource-group", + resource_group, + "--subscription", + subscription_id, + "--query", + "primaryKey", + "-o", + "tsv", + ], + "Redis cache primary key", + ) + + +def get_core_service_keys( + authentication_type, + openai_endpoint, + openai_resource_group, + openai_subscription_id, + subscription_id, + resource_group, + content_safety_endpoint, + search_service_endpoint, + document_intelligence_endpoint, + redis_cache_host_name, + speech_service_endpoint, +): + if authentication_type != "key": + return {} + + openai_resource_name = extract_resource_name_from_endpoint(openai_endpoint) + search_resource_name = extract_resource_name_from_endpoint(search_service_endpoint) + docintel_resource_name = extract_resource_name_from_endpoint(document_intelligence_endpoint) + redis_resource_name = extract_resource_name_from_endpoint(redis_cache_host_name) + content_safety_resource_name = extract_resource_name_from_endpoint(content_safety_endpoint) + speech_resource_name = extract_resource_name_from_endpoint(speech_service_endpoint) + + keys = { + "azure_openai_key": get_cognitive_services_key( + openai_resource_name, + openai_resource_group, + openai_subscription_id or subscription_id, + "Azure OpenAI key", + ), + "azure_ai_search_key": get_search_service_key( + search_resource_name, + resource_group, + subscription_id, + ), + "azure_document_intelligence_key": get_cognitive_services_key( + docintel_resource_name, + resource_group, + subscription_id, + "Azure Document Intelligence key", + ), + } + + if redis_resource_name: + keys["redis_key"] = get_redis_cache_key( + redis_resource_name, + resource_group, + subscription_id, + ) + + if content_safety_resource_name: + keys["content_safety_key"] = get_cognitive_services_key( + content_safety_resource_name, + resource_group, + subscription_id, + "Content Safety key", + ) + + if speech_resource_name: + keys["speech_service_key"] = get_cognitive_services_key( + speech_resource_name, + resource_group, + subscription_id, + "Speech Service key", + ) + + return keys + cosmosEndpoint = os.getenv("var_cosmosDb_uri") cosmosKey = os.getenv("var_cosmosDb_key") @@ -37,7 +231,6 @@ # Get values from environment variables var_authenticationType = os.getenv("var_authenticationType") -var_keyVaultUri = os.getenv("var_keyVaultUri") var_openAIEndpoint = os.getenv("var_openAIEndpoint") var_openAISubscriptionId = os.getenv("var_openAISubscriptionId") @@ -59,12 +252,19 @@ var_speechServiceEndpoint = os.getenv("var_speechServiceEndpoint") var_speechServiceLocation = os.getenv("var_deploymentLocation") -# Initialize Key Vault client if Key Vault URI is provided -if var_keyVaultUri: - keyvault_client = SecretClient( - vault_url=var_keyVaultUri, credential=credential) -else: - keyvault_client = None +core_service_keys = get_core_service_keys( + authentication_type=var_authenticationType, + openai_endpoint=var_openAIEndpoint, + openai_resource_group=var_openAIResourceGroup, + openai_subscription_id=var_openAISubscriptionId, + subscription_id=var_subscriptionId, + resource_group=var_rgName, + content_safety_endpoint=var_contentSafetyEndpoint, + search_service_endpoint=var_searchServiceEndpoint, + document_intelligence_endpoint=var_documentIntelligenceServiceEndpoint, + redis_cache_host_name=var_redisCacheHostName, + speech_service_endpoint=var_speechServiceEndpoint, +) # 4. Update the Configurations @@ -76,13 +276,8 @@ item["azure_openai_gpt_authentication_type"] = var_authenticationType item["azure_openai_gpt_subscription_id"] = var_openAISubscriptionId or var_subscriptionId item["azure_openai_gpt_resource_group"] = var_openAIResourceGroup -if keyvault_client and var_authenticationType == "key": - try: - openai_key_secret = keyvault_client.get_secret("openAi-key") - item["azure_openai_gpt_key"] = openai_key_secret.value - print("Retrieved Azure OpenAI key from Key Vault for GPT settings") - except Exception as e: - print(f"Warning: Could not retrieve openAi-key from Key Vault for GPT settings: {e}") +if var_authenticationType == "key": + item["azure_openai_gpt_key"] = core_service_keys["azure_openai_key"] item["gpt_model"] = { "selected": [ { @@ -103,13 +298,8 @@ item["azure_openai_embedding_authentication_type"] = var_authenticationType item["azure_openai_embedding_subscription_id"] = var_openAISubscriptionId or var_subscriptionId item["azure_openai_embedding_resource_group"] = var_openAIResourceGroup -if keyvault_client and var_authenticationType == "key": - try: - openai_key_secret = keyvault_client.get_secret("openAi-key") - item["azure_openai_embedding_key"] = openai_key_secret.value - print("Retrieved Azure OpenAI key from Key Vault for embedding settings") - except Exception as e: - print(f"Warning: Could not retrieve openAi-key from Key Vault for embedding settings: {e}") +if var_authenticationType == "key": + item["azure_openai_embedding_key"] = core_service_keys["azure_openai_key"] item["embedding_model"] = { "selected": [ { @@ -153,29 +343,16 @@ item["enable_content_safety"] = True item["content_safety_endpoint"] = var_contentSafetyEndpoint item["content_safety_authentication_type"] = var_authenticationType -if keyvault_client and var_authenticationType == "key": - try: - contentSafety_key_secret = keyvault_client.get_secret( - "content-safety-key") - item["content_safety_key"] = contentSafety_key_secret.value - print("Retrieved contentSafety service key from Key Vault") - except Exception as e: - print( - f"Warning: Could not retrieve content-safety-key from Key Vault: {e}") +if var_authenticationType == "key" and "content_safety_key" in core_service_keys: + item["content_safety_key"] = core_service_keys["content_safety_key"] # Redis Cache Configuration if var_redisCacheHostName and var_redisCacheHostName.strip(): item["enable_redis_cache"] = True item["redis_url"] = var_redisCacheHostName item["redis_auth_type"] = var_authenticationType -if keyvault_client and var_authenticationType == "key": - try: - redis_key_secret = keyvault_client.get_secret("redis-cache-key") - item["redis_key"] = redis_key_secret.value - print("Retrieved redis cache key from Key Vault") - except Exception as e: - print( - f"Warning: Could not retrieve redis-cache-key from Key Vault: {e}") +if var_authenticationType == "key" and "redis_key" in core_service_keys: + item["redis_key"] = core_service_keys["redis_key"] # Safety > Conversation Archiving item["enable_conversation_archiving"] = True @@ -183,27 +360,14 @@ # Search and Extract > Azure AI Search item["azure_ai_search_endpoint"] = var_searchServiceEndpoint item["azure_ai_search_authentication_type"] = var_authenticationType -if keyvault_client and var_authenticationType == "key": - try: - search_key_secret = keyvault_client.get_secret("search-service-key") - item["azure_ai_search_key"] = search_key_secret.value - print("Retrieved search service key from Key Vault") - except Exception as e: - print( - f"Warning: Could not retrieve search-service-key from Key Vault: {e}") +if var_authenticationType == "key": + item["azure_ai_search_key"] = core_service_keys["azure_ai_search_key"] # Search and Extract > Azure Document Intelligence item["azure_document_intelligence_endpoint"] = var_documentIntelligenceServiceEndpoint item["azure_document_intelligence_authentication_type"] = var_authenticationType -if keyvault_client and var_authenticationType == "key": - try: - documentIntelligence_key_secret = keyvault_client.get_secret( - "document-intelligence-key") - item["azure_document_intelligence_key"] = documentIntelligence_key_secret.value - print("Retrieved document intelligence service key from Key Vault") - except Exception as e: - print( - f"Warning: Could not retrieve document-intelligence-key from Key Vault: {e}") +if var_authenticationType == "key": + item["azure_document_intelligence_key"] = core_service_keys["azure_document_intelligence_key"] # Search and Extract > Multimedia Support # Video Indexer Configuration @@ -220,14 +384,8 @@ item["enable_audio_file_support"] = True item["speech_service_endpoint"] = var_speechServiceEndpoint item["speech_service_location"] = var_speechServiceLocation -if keyvault_client and var_authenticationType == "key": - try: - speech_key_secret = keyvault_client.get_secret("speech-service-key") - item["speech_service_key"] = speech_key_secret.value - print("Retrieved speech service key from Key Vault") - except Exception as e: - print( - f"Warning: Could not retrieve speech-service-key from Key Vault: {e}") +if var_authenticationType == "key" and "speech_service_key" in core_service_keys: + item["speech_service_key"] = core_service_keys["speech_service_key"] # 5. Upsert the updated items back into Cosmos DB response = container.upsert_item(item) diff --git a/deployers/logs.txt b/deployers/logs.txt new file mode 100644 index 000000000..9617225f4 --- /dev/null +++ b/deployers/logs.txt @@ -0,0 +1,17675 @@ +2026/03/25 11:26:45 main.go:58: azd version: 1.23.12 (commit b37e4350c0229afb71e98bbb5f6c14497659e3b3) +2026/03/25 11:26:45 detect_process.go:56: detect_process.go: Parent process detection: depth=0, pid=85924, ppid=64444, name="pwsh.exe", executable="C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.6.0.0_x64__8wekyb3d8bbwe\\pwsh.exe" +2026/03/25 11:26:45 detect_process.go:56: detect_process.go: Parent process detection: depth=1, pid=64444, ppid=50364, name="Code.exe", executable="C:\\Users\\paullizer\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe" +2026/03/25 11:26:45 detect_process.go:56: detect_process.go: Parent process detection: depth=2, pid=50364, ppid=45912, name="Code.exe", executable="C:\\Users\\paullizer\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe" +2026/03/25 11:26:45 detect_process.go:56: detect_process.go: Parent process detection: depth=3, pid=45912, ppid=64768, name="GitHubDesktop.exe", executable="C:\\Users\\paullizer\\AppData\\Local\\GitHubDesktop\\app-3.5.6\\GitHubDesktop.exe" +2026/03/25 11:26:45 detect_process.go:56: detect_process.go: Parent process detection: depth=4, pid=64768, ppid=47968, name="GitHubDesktop.exe", executable="C:\\Users\\paullizer\\AppData\\Local\\GitHubDesktop\\app-3.5.6\\GitHubDesktop.exe" +2026/03/25 11:26:45 detect_process.go:52: detect_process.go: Failed to get process info for pid 47968: failed to open process 47968: The parameter is incorrect. +2026/03/25 11:26:45 detect_process.go:72: detect_process.go: Parent process detection: no agent found in process tree +2026/03/25 11:26:45 detect.go:25: Agent detection result: detected=false, no AI coding agent detected +2026/03/25 11:26:45 project.go:152: Reading project from file 'C:\repos\simplechat\deployers\azure.yaml' +2026/03/25 11:26:45 middleware.go:100: running middleware 'debug' +2026/03/25 11:26:45 middleware.go:100: running middleware 'ux' +2026/03/25 11:26:45 middleware.go:100: running middleware 'telemetry' +2026/03/25 11:26:45 telemetry.go:68: TraceID: dd6b9467838cd44356a50058eb0073ab +2026/03/25 11:26:45 middleware.go:100: running middleware 'error' +2026/03/25 11:26:45 middleware.go:100: running middleware 'loginGuard' +2026/03/25 11:26:46 middleware.go:100: running middleware 'hooks' +2026/03/25 11:26:46 hooks.go:130: service 'web' does not require any command hooks. +2026/03/25 11:26:46 middleware.go:100: running middleware 'extensions' +2026/03/25 11:26:46 importer.go:324: using infrastructure from C:\repos\simplechat\deployers\bicep directory +Initialize bicep provider +2026/03/25 11:26:46 command_runner.go:325: Run exec: 'C:\Users\paullizer\.azd\bin\bicep.exe --version' , exit code: 0 +-------------------------------------stdout------------------------------------------- +Bicep CLI version 0.41.2 (3e403ea7c1) +2026/03/25 11:26:46 bicep.go:112: bicep version: 0.41.2 +2026/03/25 11:26:46 bicep.go:126: using local bicep: C:\Users\paullizer\.azd\bin\bicep.exe +2026/03/25 11:26:50 command_runner.go:325: Run exec: 'C:\Users\paullizer\.azd\bin\bicep.exe build C:\repos\simplechat\deployers\bicep\main.bicep --stdout' , exit code: 0 +-------------------------------------stdout------------------------------------------- +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "10060260231818509949" + } + }, + "parameters": { + "location": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "The Azure region where resources will be deployed. \n- Region must align to the target cloud environment" + } + }, + "cloudEnvironment": { + "type": "string", + "defaultValue": "[if(equals(environment().name, 'AzureCloud'), 'public', if(equals(environment().name, 'AzureUSGovernment'), 'usgovernment', 'custom'))]", + "allowedValues": [ + "AzureCloud", + "AzureUSGovernment", + "public", + "usgovernment", + "custom" + ], + "metadata": { + "description": "The target Azure Cloud environment.\n- Accepted values are: AzureCloud, AzureUSGovernment, public, usgovernment, custom\n- Default is based on the ARM cloud name" + } + }, + "appName": { + "type": "string", + "minLength": 3, + "maxLength": 12, + "metadata": { + "description": "The name of the application to be deployed. \n- Name may only contain letters and numbers\n- Between 3 and 12 characters in length \n- No spaces or special characters" + } + }, + "environment": { + "type": "string", + "minLength": 2, + "maxLength": 10, + "metadata": { + "description": "The dev/qa/prod environment or as named in your environment. This will be used to create resource group names and tags.\n- Must be between 2 and 10 characters in length\n- No spaces or special characters" + } + }, + "azdEnvironmentName": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "metadata": { + "description": "Name of the AZD environment" + } + }, + "imageName": { + "type": "string", + "defaultValue": "simplechat:latest", + "metadata": { + "description": "The name of the container image to deploy to the web app.\n- should be in the format :" + } + }, + "enterpriseAppClientId": { + "type": "string", + "metadata": { + "description": "Azure AD Application Client ID for enterprise authentication.\n- Should be the client ID of the registered Azure AD application" + } + }, + "enterpriseAppServicePrincipalId": { + "type": "string", + "metadata": { + "description": "Azure AD Application Service Principal Id for the enterprise application.\n- Should be the Service Principal ID of the registered Azure AD application" + } + }, + "enterpriseAppClientSecret": { + "type": "securestring", + "metadata": { + "description": "Azure AD Application Client Secret for enterprise authentication.\n- Required if enableEnterpriseApp is true\n- Should be created in Azure AD App Registration and passed via environment variable\n- Will be stored securely in Azure Key Vault during deployment" + } + }, + "authenticationType": { + "type": "string", + "allowedValues": [ + "key", + "managed_identity" + ], + "metadata": { + "description": "Authentication type for resources that support Managed Identity or Key authentication.\n- Key: Use access keys for authentication (application keys will be stored in Key Vault)\n- managed_identity: Use Managed Identity for authentication" + } + }, + "configureApplicationPermissions": { + "type": "bool", + "metadata": { + "description": "Configure permissions (based on authenticationType) for the deployed web application to access required resources.\n" + } + }, + "specialTags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional object containing additional tags to apply to all resources." + } + }, + "enableDiagLogging": { + "type": "bool", + "metadata": { + "description": "Enable diagnostic logging for resources deployed in the resource group. \n- All content will be sent to the deployed Log Analytics workspace\n- Default is false" + } + }, + "enablePrivateNetworking": { + "type": "bool", + "metadata": { + "description": "Enable private endpoints and virtual network integration for deployed resources. \n- Default is false" + } + }, + "existingVirtualNetworkId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing virtual network resource ID to reuse when private networking is enabled.\n- May reference a virtual network in the same or another resource group or subscription\n- Leave blank to create a new virtual network" + } + }, + "existingAppServiceSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing subnet resource ID to use for App Service VNet integration.\n- May reference a subnet in the same or another resource group or subscription\n- Required when reusing an existing virtual network because subnets are not created in external virtual networks" + } + }, + "existingPrivateEndpointSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing subnet resource ID to use for private endpoints.\n- May reference a subnet in the same or another resource group or subscription\n- Required when reusing an existing virtual network because subnets are not created in external virtual networks" + } + }, + "privateDnsZoneConfigs": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional per-zone private DNS configuration for private networking.\n- Leave empty to create all private DNS zones locally and create VNet links automatically\n- For each supported key, provide:\n - zoneResourceId: Optional existing private DNS zone resource ID to reuse\n - createVNetLink: Optional bool, defaults to true. Set to false if the customer manages the VNet link separately\n- Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites" + } + }, + "existingOpenAIEndpoint": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint.\n- Leave blank to deploy a new Azure OpenAI resource\n- Public Azure AI Foundry project endpoints are supported for application configuration, but do not support private endpoint automation" + } + }, + "existingOpenAIResourceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing Azure OpenAI resource name.\n- Provide this when reusing a standard Azure OpenAI resource and you want managed identity permissions or private endpoint integration configured automatically" + } + }, + "existingOpenAIResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional resource group for an existing Azure OpenAI resource.\n- Used when reusing a standard Azure OpenAI resource across resource groups or subscriptions" + } + }, + "existingOpenAISubscriptionId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional subscription ID for an existing Azure OpenAI resource.\n- Used when reusing a standard Azure OpenAI resource across subscriptions" + } + }, + "openAIDeploymentType": { + "type": "string", + "allowedValues": [ + "Standard", + "DatazoneStandard", + "GlobalStandard" + ], + "metadata": { + "description": "Azure OpenAI deployment type used for the default GPT and embedding model deployments.\n- Azure Commercial options: Standard, DatazoneStandard, GlobalStandard\n- Azure Government default model deployments use Standard regardless of this selection\n- Ignored when you provide custom gptModels or embeddingModels arrays" + } + }, + "customBlobStorageSuffix": { + "type": "string", + "defaultValue": "[format('blob.{0}', environment().suffixes.storage)]", + "metadata": { + "description": "Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net" + } + }, + "customGraphUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Graph API URL, e.g. https://graph.microsoft.us" + } + }, + "customIdentityUrl": { + "type": "string", + "defaultValue": "[environment().authentication.loginEndpoint]", + "metadata": { + "description": "Custom Identity URL, e.g. https://login.microsoftonline.us/" + } + }, + "customResourceManagerUrl": { + "type": "string", + "defaultValue": "[environment().resourceManager]", + "metadata": { + "description": "Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net" + } + }, + "customCognitiveServicesScope": { + "type": "string", + "defaultValue": "https://cognitiveservices.azure.com/.default", + "metadata": { + "description": "Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default" + } + }, + "customSearchResourceUrl": { + "type": "string", + "defaultValue": "https://search.azure.com", + "metadata": { + "description": "Custom search resource URL for token audience, e.g. https://search.azure.us" + } + }, + "gptModels": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Array of GPT model names to deploy to the OpenAI resource." + } + }, + "embeddingModels": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Array of embedding model names to deploy to the OpenAI resource." + } + }, + "allowedIpAddresses": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Comma separated list of IP addresses or ranges to allow access to resources when private networking is enabled.\nLeave blank if not using private networking.\n- Format for single IP: 'x.x.x.x'\n- Format for range: 'x.x.x.x/y'\n- Example: 1.2.3.4, 2.3.4.5/32\n" + } + }, + "deployContentSafety": { + "type": "bool", + "metadata": { + "description": "Enable deployment of Content Safety service and related resources.\n- Default is false" + } + }, + "deployRedisCache": { + "type": "bool", + "metadata": { + "description": "Enable deployment of Azure Cache for Redis and related resources.\n- Default is false" + } + }, + "deploySpeechService": { + "type": "bool", + "metadata": { + "description": "Enable deployment of Azure Speech service and related resources.\n- Default is false" + } + }, + "deployVideoIndexerService": { + "type": "bool", + "metadata": { + "description": "Enable deployment of Azure Video Indexer service and related resources.\n- Default is false" + } + } + }, + "variables": { + "copy": [ + { + "name": "allowedIpAddressesArray", + "count": "[length(variables('allowedIpAddressesSplit'))]", + "input": "[trim(variables('allowedIpAddressesSplit')[copyIndex('allowedIpAddressesArray')])]" + }, + { + "name": "cosmosDbIpRules", + "count": "[length(variables('allowedIpsForCosmos'))]", + "input": { + "ipAddressOrRange": "[variables('allowedIpsForCosmos')[copyIndex('cosmosDbIpRules')]]" + } + }, + { + "name": "acrIpRules", + "count": "[length(variables('allowedIpAddressesArray'))]", + "input": { + "action": "Allow", + "value": "[variables('allowedIpAddressesArray')[copyIndex('acrIpRules')]]" + } + } + ], + "scCloudEnvironment": "[if(equals(parameters('cloudEnvironment'), 'AzureCloud'), 'public', if(equals(parameters('cloudEnvironment'), 'AzureUSGovernment'), 'usgovernment', parameters('cloudEnvironment')))]", + "allowedIpAddressesSplit": "[if(empty(parameters('allowedIpAddresses')), createArray(), split(parameters('allowedIpAddresses'), ','))]", + "rgName": "[format('{0}-{1}-rg', parameters('appName'), parameters('environment'))]", + "requiredTags": { + "application": "[parameters('appName')]", + "environment": "[parameters('environment')]", + "azd-env-name": "[parameters('azdEnvironmentName')]" + }, + "tags": "[union(variables('requiredTags'), parameters('specialTags'))]", + "isPublicCloud": "[equals(variables('scCloudEnvironment'), 'public')]", + "isUsGovernmentCloud": "[equals(variables('scCloudEnvironment'), 'usgovernment')]", + "acrCloudSuffix": "[if(variables('isPublicCloud'), '.azurecr.io', '.azurecr.us')]", + "acrName": "[toLower(format('{0}{1}acr', parameters('appName'), parameters('environment')))]", + "containerRegistry": "[format('{0}{1}', variables('acrName'), variables('acrCloudSuffix'))]", + "containerImageName": "[format('{0}/{1}', variables('containerRegistry'), parameters('imageName'))]", + "vNetName": "[format('{0}-{1}-vnet', parameters('appName'), parameters('environment'))]", + "normalizedLocation": "[toLower(replace(parameters('location'), ' ', ''))]", + "resolvedOpenAIDeploymentType": "[if(variables('isUsGovernmentCloud'), 'Standard', parameters('openAIDeploymentType'))]", + "defaultGptModels": [ + { + "modelName": "gpt-4o", + "modelVersion": "[if(variables('isUsGovernmentCloud'), '2024-05-13', '2024-11-20')]", + "skuName": "[variables('resolvedOpenAIDeploymentType')]", + "skuCapacity": 100 + } + ], + "defaultEmbeddingModels": "[if(variables('isUsGovernmentCloud'), createArray(createObject('modelName', if(equals(variables('normalizedLocation'), 'usgovvirginia'), 'text-embedding-ada-002', 'text-embedding-3-small'), 'modelVersion', if(equals(variables('normalizedLocation'), 'usgovvirginia'), '2', '1'), 'skuName', variables('resolvedOpenAIDeploymentType'), 'skuCapacity', 100)), createArray(createObject('modelName', 'text-embedding-3-small', 'modelVersion', '1', 'skuName', variables('resolvedOpenAIDeploymentType'), 'skuCapacity', 100)))]", + "resolvedGptModels": "[if(empty(parameters('gptModels')), variables('defaultGptModels'), parameters('gptModels'))]", + "resolvedEmbeddingModels": "[if(empty(parameters('embeddingModels')), variables('defaultEmbeddingModels'), parameters('embeddingModels'))]", + "hasExistingAppServiceSubnetId": "[not(empty(parameters('existingAppServiceSubnetId')))]", + "hasExistingPrivateEndpointSubnetId": "[not(empty(parameters('existingPrivateEndpointSubnetId')))]", + "inferredVirtualNetworkId": "[if(variables('hasExistingAppServiceSubnetId'), split(parameters('existingAppServiceSubnetId'), '/subnets/')[0], if(variables('hasExistingPrivateEndpointSubnetId'), split(parameters('existingPrivateEndpointSubnetId'), '/subnets/')[0], ''))]", + "resolvedExistingVirtualNetworkId": "[if(not(empty(parameters('existingVirtualNetworkId'))), parameters('existingVirtualNetworkId'), variables('inferredVirtualNetworkId'))]", + "useExistingVirtualNetwork": "[and(parameters('enablePrivateNetworking'), or(or(not(empty(variables('resolvedExistingVirtualNetworkId'))), variables('hasExistingAppServiceSubnetId')), variables('hasExistingPrivateEndpointSubnetId')))]", + "allowedIpsForCosmos": "[union(createArray('0.0.0.0'), variables('allowedIpAddressesArray'))]" + }, + "resources": { + "rg": { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2022-09-01", + "name": "[variables('rgName')]", + "location": "[parameters('location')]", + "tags": "[variables('tags')]" + }, + "virtualNetwork": { + "condition": "[and(parameters('enablePrivateNetworking'), not(variables('useExistingVirtualNetwork')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "virtualNetwork", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "vNetName": { + "value": "[variables('vNetName')]" + }, + "addressSpaces": { + "value": [ + "10.0.0.0/21" + ] + }, + "subnetConfigs": { + "value": [ + { + "name": "AppServiceIntegration", + "addressPrefix": "10.0.0.0/24", + "enablePrivateEndpointNetworkPolicies": true, + "enablePrivateLinkServiceNetworkPolicies": true + }, + { + "name": "PrivateEndpoints", + "addressPrefix": "10.0.2.0/24", + "enablePrivateEndpointNetworkPolicies": true, + "enablePrivateLinkServiceNetworkPolicies": true + } + ] + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "493906105301296560" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "vNetName": { + "type": "string" + }, + "addressSpaces": { + "type": "array" + }, + "subnetConfigs": { + "type": "array" + }, + "tags": { + "type": "object" + } + }, + "variables": { + "copy": [ + { + "name": "subnetIds", + "count": "[length(parameters('subnetConfigs'))]", + "input": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vNetName'), parameters('subnetConfigs')[copyIndex('subnetIds')].name)]" + }, + { + "name": "subnetNames", + "count": "[length(parameters('subnetConfigs'))]", + "input": "[parameters('subnetConfigs')[copyIndex('subnetNames')].name]" + } + ], + "appServiceIntegrationSubnetIndex": "[indexOf(variables('subnetNames'), 'AppServiceIntegration')]", + "privateEndpointIndex": "[indexOf(variables('subnetNames'), 'PrivateEndpoints')]" + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2021-05-01", + "name": "[parameters('vNetName')]", + "location": "[parameters('location')]", + "properties": { + "copy": [ + { + "name": "subnets", + "count": "[length(parameters('subnetConfigs'))]", + "input": { + "name": "[parameters('subnetConfigs')[copyIndex('subnets')].name]", + "properties": { + "addressPrefix": "[parameters('subnetConfigs')[copyIndex('subnets')].addressPrefix]", + "privateEndpointNetworkPolicies": "[if(parameters('subnetConfigs')[copyIndex('subnets')].enablePrivateEndpointNetworkPolicies, 'Enabled', 'Disabled')]", + "privateLinkServiceNetworkPolicies": "[if(parameters('subnetConfigs')[copyIndex('subnets')].enablePrivateLinkServiceNetworkPolicies, 'Enabled', 'Disabled')]", + "delegations": "[if(equals(parameters('subnetConfigs')[copyIndex('subnets')].name, 'AppServiceIntegration'), createArray(createObject('name', 'delegation', 'properties', createObject('serviceName', 'Microsoft.Web/serverFarms'))), createArray())]" + } + } + } + ], + "addressSpace": { + "addressPrefixes": "[parameters('addressSpaces')]" + } + }, + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "vNetId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vNetName'))]" + }, + "privateNetworkSubnetId": { + "type": "string", + "value": "[if(equals(variables('privateEndpointIndex'), -1), '', variables('subnetIds')[variables('privateEndpointIndex')])]" + }, + "appServiceSubnetId": { + "type": "string", + "value": "[if(equals(variables('appServiceIntegrationSubnetIndex'), -1), '', variables('subnetIds')[variables('appServiceIntegrationSubnetIndex')])]" + } + } + } + }, + "dependsOn": [ + "rg" + ] + }, + "logAnalytics": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "logAnalytics", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3702219330633152864" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[toLower(format('{0}-{1}-la', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + } + }, + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "logAnalyticsId": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', toLower(format('{0}-{1}-la', parameters('appName'), parameters('environment'))))]" + } + } + } + }, + "dependsOn": [ + "rg" + ] + }, + "applicationInsights": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "applicationInsights", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16478820448576828524" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "logAnalyticsId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[toLower(format('{0}-{1}-ai', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[parameters('logAnalyticsId')]" + }, + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "appInsightsName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-ai', parameters('appName'), parameters('environment')))]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "keyVault": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "keyVault", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3020666832422074415" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "properties": { + "tenantId": "[subscription().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "accessPolicies": [], + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": false, + "publicNetworkAccess": "Enabled", + "enableRbacAuthorization": true + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "keyVaultId": { + "type": "string", + "value": "[resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment'))))]" + }, + "keyVaultName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))]" + }, + "keyVaultUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))), '2024-11-01').vaultUri]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "storeEnterpriseAppSecret": { + "condition": "[not(empty(parameters('enterpriseAppClientSecret')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storeEnterpriseAppSecret", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "secretName": { + "value": "enterprise-app-client-secret" + }, + "secretValue": { + "value": "[parameters('enterpriseAppClientSecret')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" + } + }, + "parameters": { + "keyVaultName": { + "type": "string" + }, + "secretName": { + "type": "string" + }, + "secretValue": { + "type": "securestring" + } + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2025-05-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretName'))]", + "properties": { + "value": "[parameters('secretValue')]" + } + } + ], + "outputs": { + "SecretUri": { + "type": "string", + "value": "[format('{0}secrets/{1}', reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), '2025-05-01').vaultUri, parameters('secretName'))]" + } + } + } + }, + "dependsOn": [ + "keyVault", + "rg" + ] + }, + "cosmosDB": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "cosmosDB", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "allowedIpAddresses": { + "value": "[variables('cosmosDbIpRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "12491761736151044216" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + }, + "allowedIpAddresses": { + "type": "array", + "defaultValue": [] + } + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2023-04-15", + "name": "[toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "GlobalDocumentDB", + "properties": { + "publicNetworkAccess": "Enabled", + "databaseAccountOfferType": "Standard", + "capabilities": [ + { + "name": "EnableServerless" + } + ], + "isVirtualNetworkFilterEnabled": "[if(parameters('enablePrivateNetworking'), true(), false())]", + "ipRules": "[if(parameters('enablePrivateNetworking'), parameters('allowedIpAddresses'), createArray())]", + "locations": [ + { + "locationName": "[parameters('location')]", + "failoverPriority": 0, + "isZoneRedundant": false + } + ], + "consistencyPolicy": { + "defaultConsistencyLevel": "Session" + } + }, + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2023-04-15", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))), 'SimpleChat')]", + "properties": { + "resource": { + "id": "SimpleChat" + }, + "options": {} + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2023-04-15", + "name": "[format('{0}/{1}/{2}', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))), 'SimpleChat', 'settings')]", + "properties": { + "resource": { + "id": "settings", + "partitionKey": { + "paths": [ + "/id" + ] + } + }, + "options": {} + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))), 'SimpleChat')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": [] + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))))]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "cosmosDbName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))]" + }, + "cosmosDbUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))), '2023-04-15').documentEndpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "acr": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "azureContainerRegistry", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "acrName": { + "value": "[variables('acrName')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "allowedIpAddresses": { + "value": "[variables('acrIpRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "8868053889964006912" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "acrName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + }, + "allowedIpAddresses": { + "type": "array", + "defaultValue": [] + } + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-04-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[if(parameters('enablePrivateNetworking'), 'Premium', 'Standard')]" + }, + "properties": { + "adminUserEnabled": true, + "publicNetworkAccess": "Enabled", + "networkRuleBypassOptions": "[if(parameters('enablePrivateNetworking'), 'AzureServices', 'None')]", + "networkRuleSet": "[if(parameters('enablePrivateNetworking'), createObject('defaultAction', 'Allow', 'ipRules', parameters('allowedIpAddresses')), null())]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[toLower(format('{0}-diagnostics', parameters('acrName')))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "acrName": { + "type": "string", + "value": "[parameters('acrName')]" + }, + "acrResourceGroup": { + "type": "string", + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "searchService": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "searchService", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "433641641835184139" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-05-01", + "name": "[toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "sku": { + "name": "basic" + }, + "properties": { + "hostingMode": "default", + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "replicaCount": 1, + "partitionCount": 1, + "authOptions": { + "aadOrApiKey": { + "aadAuthFailureMode": "http403" + } + }, + "disableLocalAuth": false + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Search/searchServices', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.Search/searchServices', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "searchServiceName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))]" + }, + "searchServiceEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Search/searchServices', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))), '2025-05-01').endpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "docIntel": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "docIntel", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "1751663477897380161" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "FormRecognizer", + "sku": { + "name": "S0" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "customSubDomainName": "[toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "documentIntelligenceServiceName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))]" + }, + "diagnosticLoggingEnabled": { + "type": "bool", + "value": "[parameters('enableDiagLogging')]" + }, + "documentIntelligenceServiceEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))), '2025-06-01').endpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "storageAccount": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storageAccount", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "keyVault": { + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "configureApplicationPermissions": { + "value": "[parameters('configureApplicationPermissions')]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "811367549199956159" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "keyVault": { + "type": "string" + }, + "authenticationType": { + "type": "string" + }, + "configureApplicationPermissions": { + "type": "bool" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-09-01", + "name": "[toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "accessTier": "Hot", + "allowBlobPublicAccess": false, + "allowSharedKeyAccess": true, + "isHnsEnabled": true + }, + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}/{2}', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default', 'user-documents')]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}/{2}', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default', 'group-documents')]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": [], + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.transactionMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]", + "name": "[toLower(format('{0}-blob-diagnostics', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.transactionMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + }, + { + "condition": "[and(equals(parameters('authenticationType'), 'key'), parameters('configureApplicationPermissions'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storeStorageAccountSecret", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[parameters('keyVault')]" + }, + "secretName": { + "value": "storage-account-key" + }, + "secretValue": { + "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))), '2022-09-01').keys[0].value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" + } + }, + "parameters": { + "keyVaultName": { + "type": "string" + }, + "secretName": { + "type": "string" + }, + "secretValue": { + "type": "securestring" + } + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2025-05-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretName'))]", + "properties": { + "value": "[parameters('secretValue')]" + } + } + ], + "outputs": { + "SecretUri": { + "type": "string", + "value": "[format('{0}secrets/{1}', reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), '2025-05-01').vaultUri, parameters('secretName'))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))]" + }, + "endpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))), '2022-09-01').primaryEndpoints.blob]" + } + } + } + }, + "dependsOn": [ + "keyVault", + "logAnalytics", + "rg" + ] + }, + "openAI": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAI", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "existingOpenAIEndpoint": { + "value": "[parameters('existingOpenAIEndpoint')]" + }, + "existingOpenAIResourceName": { + "value": "[parameters('existingOpenAIResourceName')]" + }, + "existingOpenAIResourceGroup": { + "value": "[parameters('existingOpenAIResourceGroup')]" + }, + "existingOpenAISubscriptionId": { + "value": "[parameters('existingOpenAISubscriptionId')]" + }, + "gptModels": { + "value": "[variables('resolvedGptModels')]" + }, + "embeddingModels": { + "value": "[variables('resolvedEmbeddingModels')]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3132373501615141396" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "existingOpenAIEndpoint": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAIResourceName": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAIResourceGroup": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAISubscriptionId": { + "type": "string", + "defaultValue": "" + }, + "gptModels": { + "type": "array" + }, + "embeddingModels": { + "type": "array" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "variables": { + "useExistingOpenAIEndpoint": "[not(empty(parameters('existingOpenAIEndpoint')))]", + "resolvedOpenAIResourceGroup": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIResourceGroup'), resourceGroup().name)]", + "resolvedOpenAISubscriptionId": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAISubscriptionId'), subscription().subscriptionId)]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingOpenAIEndpoint'))]", + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2024-10-01", + "name": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "OpenAI", + "sku": { + "name": "S0" + }, + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "customSubDomainName": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(parameters('enableDiagLogging'), not(variables('useExistingOpenAIEndpoint')))]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + }, + { + "copy": { + "name": "aiModel", + "count": "[length(if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray()))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('model-{0}-{1}', replace(if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelName, '.', '-'), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "parent": { + "value": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]" + }, + "modelName": { + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelName]" + }, + "modelVersion": { + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelVersion]" + }, + "skuName": { + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].skuName]" + }, + "skuCapacity": { + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].skuCapacity]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "12746224250029736064" + } + }, + "parameters": { + "parent": { + "type": "string" + }, + "modelName": { + "type": "string" + }, + "modelVersion": { + "type": "string" + }, + "skuName": { + "type": "string" + }, + "skuCapacity": { + "type": "int" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2025-06-01", + "name": "[format('{0}/{1}', parameters('parent'), parameters('modelName'))]", + "properties": { + "model": { + "format": "OpenAI", + "name": "[parameters('modelName')]", + "version": "[parameters('modelVersion')]" + } + }, + "sku": { + "name": "[parameters('skuName')]", + "capacity": "[parameters('skuCapacity')]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]" + ] + } + ], + "outputs": { + "openAIName": { + "type": "string", + "value": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIResourceName'), toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]" + }, + "openAIResourceGroup": { + "type": "string", + "value": "[variables('resolvedOpenAIResourceGroup')]" + }, + "openAISubscriptionId": { + "type": "string", + "value": "[variables('resolvedOpenAISubscriptionId')]" + }, + "openAIEndpoint": { + "type": "string", + "value": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIEndpoint'), reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))), '2024-10-01').endpoint)]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "appServicePlan": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "appServicePlan", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "9868911322271424816" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2022-03-01", + "name": "[toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "sku": { + "name": "P1v3", + "tier": "PremiumV3", + "size": "P1v3", + "capacity": 1 + }, + "kind": "app,linux,container", + "properties": { + "reserved": true, + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Web/serverfarms', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": [], + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment'))))]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "appServicePlanId": { + "type": "string", + "value": "[resourceId('Microsoft.Web/serverfarms', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment'))))]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "appService": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "appService", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "acrName": { + "value": "[reference('acr').outputs.acrName.value]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "appServicePlanId": { + "value": "[reference('appServicePlan').outputs.appServicePlanId.value]" + }, + "containerImageName": { + "value": "[variables('containerImageName')]" + }, + "azurePlatform": { + "value": "[variables('scCloudEnvironment')]" + }, + "cosmosDbName": { + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" + }, + "searchServiceName": { + "value": "[reference('searchService').outputs.searchServiceName.value]" + }, + "openAiServiceName": { + "value": "[reference('openAI').outputs.openAIName.value]" + }, + "openAiEndpoint": { + "value": "[reference('openAI').outputs.openAIEndpoint.value]" + }, + "openAiResourceGroupName": { + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" + }, + "documentIntelligenceServiceName": { + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" + }, + "appInsightsName": { + "value": "[reference('applicationInsights').outputs.appInsightsName.value]" + }, + "enterpriseAppClientId": { + "value": "[parameters('enterpriseAppClientId')]" + }, + "enterpriseAppClientSecret": { + "value": "[parameters('enterpriseAppClientSecret')]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "keyVaultUri": { + "value": "[reference('keyVault').outputs.keyVaultUri.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "appServiceSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingAppServiceSubnetId')), createObject('value', reference('virtualNetwork').outputs.appServiceSubnetId.value)), createObject('value', ''))]", + "customBlobStorageSuffix": { + "value": "[parameters('customBlobStorageSuffix')]" + }, + "customGraphUrl": { + "value": "[parameters('customGraphUrl')]" + }, + "customIdentityUrl": { + "value": "[parameters('customIdentityUrl')]" + }, + "customResourceManagerUrl": { + "value": "[parameters('customResourceManagerUrl')]" + }, + "customCognitiveServicesScope": { + "value": "[parameters('customCognitiveServicesScope')]" + }, + "customSearchResourceUrl": { + "value": "[parameters('customSearchResourceUrl')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "7866543323088689102" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "acrName": { + "type": "string" + }, + "appServicePlanId": { + "type": "string" + }, + "containerImageName": { + "type": "string" + }, + "azurePlatform": { + "type": "string" + }, + "cosmosDbName": { + "type": "string" + }, + "searchServiceName": { + "type": "string" + }, + "openAiServiceName": { + "type": "string" + }, + "openAiEndpoint": { + "type": "string" + }, + "openAiResourceGroupName": { + "type": "string" + }, + "documentIntelligenceServiceName": { + "type": "string" + }, + "appInsightsName": { + "type": "string" + }, + "enterpriseAppClientId": { + "type": "string", + "defaultValue": "" + }, + "authenticationType": { + "type": "string" + }, + "enterpriseAppClientSecret": { + "type": "securestring", + "defaultValue": "" + }, + "keyVaultUri": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + }, + "appServiceSubnetId": { + "type": "string", + "defaultValue": "" + }, + "customBlobStorageSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net" + } + }, + "customGraphUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Graph API URL, e.g. https://graph.microsoft.us" + } + }, + "customIdentityUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Identity URL, e.g. https://login.microsoftonline.us" + } + }, + "customResourceManagerUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net" + } + }, + "customCognitiveServicesScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default" + } + }, + "customSearchResourceUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom search resource URL for token audience, e.g. https://search.azure.us" + } + } + }, + "variables": { + "tenantId": "[tenant().tenantId]", + "openIdMetadataUrl": "[format('{0}{1}/v2.0/.well-known/openid-configuration', environment().authentication.loginEndpoint, variables('tenantId'))]", + "acrDomain": "[environment().suffixes.acrLoginServer]" + }, + "resources": { + "acrService": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-04-01", + "name": "[parameters('acrName')]" + }, + "cosmosDb": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2023-04-15", + "name": "[parameters('cosmosDbName')]" + }, + "searchService": { + "existing": true, + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-05-01", + "name": "[parameters('searchServiceName')]" + }, + "documentIntelligence": { + "existing": true, + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[parameters('documentIntelligenceServiceName')]" + }, + "appInsights": { + "existing": true, + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[parameters('appInsightsName')]" + }, + "webApp": { + "type": "Microsoft.Web/sites", + "apiVersion": "2022-03-01", + "name": "[toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "app,linux,container", + "properties": { + "serverFarmId": "[parameters('appServicePlanId')]", + "virtualNetworkSubnetId": "[if(not(equals(parameters('appServiceSubnetId'), '')), parameters('appServiceSubnetId'), null())]", + "publicNetworkAccess": "Enabled", + "vnetImagePullEnabled": "[if(parameters('enablePrivateNetworking'), true(), false())]", + "siteConfig": { + "linuxFxVersion": "[format('DOCKER|{0}', parameters('containerImageName'))]", + "acrUseManagedIdentityCreds": true, + "acrUserManagedIdentityID": "", + "alwaysOn": true, + "ftpsState": "Disabled", + "healthCheckPath": "/external/healthcheck", + "appSettings": "[flatten(createArray(createArray(createObject('name', 'AZURE_ENVIRONMENT', 'value', parameters('azurePlatform')), createObject('name', 'SCM_DO_BUILD_DURING_DEPLOYMENT', 'value', 'false'), createObject('name', 'AZURE_COSMOS_ENDPOINT', 'value', reference('cosmosDb').documentEndpoint), createObject('name', 'AZURE_COSMOS_AUTHENTICATION_TYPE', 'value', toLower(parameters('authenticationType')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_COSMOS_KEY', 'value', listKeys('cosmosDb', '2023-04-15').primaryMasterKey)), createArray()), createArray(createObject('name', 'TENANT_ID', 'value', tenant().tenantId), createObject('name', 'CLIENT_ID', 'value', parameters('enterpriseAppClientId')), createObject('name', 'SECRET_KEY', 'value', if(not(empty(parameters('enterpriseAppClientSecret'))), parameters('enterpriseAppClientSecret'), format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri')))), createObject('name', 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', 'value', format('@Microsoft.KeyVault(SecretUri= parameters('keyVaultUri'))), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', format('https://{0}{1}', parameters('acrName'), variables('acrDomain')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_USERNAME', 'value', listCredentials('acrService', '2025-04-01').username)), createArray()), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_PASSWORD', 'value', listCredentials('acrService', '2025-04-01').passwords[0].value)), createArray()), createArray(createObject('name', 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', 'value', tenant().tenantId), createObject('name', 'AZURE_OPENAI_RESOURCE_NAME', 'value', parameters('openAiServiceName')), createObject('name', 'AZURE_OPENAI_RESOURCE_GROUP_NAME', 'value', parameters('openAiResourceGroupName')), createObject('name', 'AZURE_OPENAI_URL', 'value', parameters('openAiEndpoint')), createObject('name', 'AZURE_SEARCH_SERVICE_NAME', 'value', parameters('searchServiceName'))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_SEARCH_API_KEY', 'value', listAdminKeys('searchService', '2025-05-01').primaryKey)), createArray()), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', 'value', reference('documentIntelligence').endpoint)), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', 'value', listKeys('documentIntelligence', '2025-06-01').key1)), createArray()), createArray(createObject('name', 'APPINSIGHTS_INSTRUMENTATIONKEY', 'value', reference('appInsights').InstrumentationKey), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference('appInsights').ConnectionString), createObject('name', 'APPINSIGHTS_PROFILERFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPLICATIONINSIGHTS_CONFIGURATION_CONTENT', 'value', ''), createObject('name', 'ApplicationInsightsAgent_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'DiagnosticServices_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'InstrumentationEngine_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'SnapshotDebugger_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_BaseExtensions', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_Mode', 'value', 'recommended'), createObject('name', 'XDT_MicrosoftApplicationInsights_PreemptSdk', 'value', 'disabled')), if(equals(parameters('azurePlatform'), 'custom'), createArray(createObject('name', 'CUSTOM_GRAPH_URL_VALUE', 'value', coalesce(parameters('customGraphUrl'), '')), createObject('name', 'CUSTOM_IDENTITY_URL_VALUE', 'value', coalesce(parameters('customIdentityUrl'), '')), createObject('name', 'CUSTOM_RESOURCE_MANAGER_URL_VALUE', 'value', coalesce(parameters('customResourceManagerUrl'), '')), createObject('name', 'CUSTOM_BLOB_STORAGE_URL_VALUE', 'value', coalesce(parameters('customBlobStorageSuffix'), '')), createObject('name', 'CUSTOM_COGNITIVE_SERVICES_URL_VALUE', 'value', coalesce(parameters('customCognitiveServicesScope'), '')), createObject('name', 'CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE', 'value', coalesce(parameters('customSearchResourceUrl'), '')), createObject('name', 'KEY_VAULT_DOMAIN', 'value', environment().suffixes.keyvaultDns), createObject('name', 'CUSTOM_OIDC_METADATA_URL_VALUE', 'value', coalesce(variables('openIdMetadataUrl'), ''))), createArray())))]" + }, + "clientAffinityEnabled": false, + "httpsOnly": true + }, + "identity": { + "type": "SystemAssigned" + }, + "tags": "[union(parameters('tags'), createObject('azd-service-name', 'web'))]", + "dependsOn": [ + "appInsights", + "cosmosDb", + "documentIntelligence" + ] + }, + "webAppLogging": { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))), 'logs')]", + "properties": { + "httpLogs": { + "fileSystem": { + "enabled": true, + "retentionInDays": 7, + "retentionInMb": 35 + } + } + }, + "dependsOn": [ + "webApp" + ] + }, + "webAppDiagnostics": { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference('diagnosticConfigs').outputs.webAppLogCategories.value]", + "metrics": "[reference('diagnosticConfigs').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "diagnosticConfigs", + "webApp" + ] + }, + "authSettings": { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))), 'authsettingsV2')]", + "properties": { + "globalValidation": { + "requireAuthentication": true, + "unauthenticatedClientAction": "RedirectToLoginPage", + "redirectToProvider": "azureActiveDirectory" + }, + "identityProviders": { + "azureActiveDirectory": { + "enabled": true, + "registration": { + "openIdIssuer": "[format('{0}{1}/', environment().authentication.loginEndpoint, tenant().tenantId)]", + "clientId": "[parameters('enterpriseAppClientId')]", + "clientSecretSettingName": "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" + }, + "validation": { + "jwtClaimChecks": {}, + "allowedAudiences": [ + "[format('api://{0}', parameters('enterpriseAppClientId'))]", + "[parameters('enterpriseAppClientId')]" + ] + }, + "isAutoProvisioned": false + } + }, + "login": { + "routes": { + "logoutEndpoint": "/.auth/logout" + }, + "tokenStore": { + "enabled": true, + "tokenRefreshExtensionHours": 72, + "fileSystem": { + "directory": "/home/data/.auth" + } + }, + "preserveUrlFragmentsForLogins": false, + "allowedExternalRedirectUrls": [], + "cookieExpiration": { + "convention": "FixedTime", + "timeToExpiration": "08:00:00" + }, + "nonce": { + "validateNonce": true, + "nonceExpirationInterval": "00:05:00" + } + }, + "httpSettings": { + "requireHttps": true, + "routes": { + "apiPrefix": "/.auth" + }, + "forwardProxy": { + "convention": "NoProxy" + } + } + }, + "dependsOn": [ + "webApp" + ] + }, + "diagnosticConfigs": { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "value": "[toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))]" + }, + "defaultHostName": { + "type": "string", + "value": "[reference('webApp').defaultHostName]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]" + } + } + } + }, + "dependsOn": [ + "acr", + "applicationInsights", + "appServicePlan", + "cosmosDB", + "docIntel", + "keyVault", + "logAnalytics", + "openAI", + "rg", + "searchService", + "virtualNetwork" + ] + }, + "contentSafety": { + "condition": "[parameters('deployContentSafety')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "contentSafety", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "7932852522879306913" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "ContentSafety", + "sku": { + "name": "S0" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "customSubDomainName": "[toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment'))))]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "contentSafetyName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))]" + }, + "contentSafetyEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))), '2025-06-01').endpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "redisCache": { + "condition": "[parameters('deployRedisCache')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "redisCache", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "17045519057214581310" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Cache/redis", + "apiVersion": "2024-11-01", + "name": "[toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "Standard", + "family": "C", + "capacity": 0 + }, + "enableNonSslPort": false, + "minimumTlsVersion": "1.2", + "redisConfiguration": { + "aad-enabled": "true" + } + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Cache/redis', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.Cache/redis', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "redisCacheName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))]" + }, + "redisCacheHostName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Cache/redis', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))), '2024-11-01').hostName]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "speechService": { + "condition": "[parameters('deploySpeechService')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "speechService", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "15886240553047875398" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2024-10-01", + "name": "[toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "SpeechServices", + "sku": { + "name": "S0" + }, + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "customSubDomainName": "[toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "speechServiceName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))]" + }, + "speechServiceEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))), '2024-10-01').endpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "videoIndexerService": { + "condition": "[parameters('deployVideoIndexerService')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "videoIndexerService", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "storageAccount": { + "value": "[reference('storageAccount').outputs.name.value]" + }, + "openAiServiceName": { + "value": "[reference('openAI').outputs.openAIName.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "4707936164725245117" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "storageAccount": { + "type": "string" + }, + "openAiServiceName": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.VideoIndexer/accounts", + "apiVersion": "2025-04-01", + "name": "[toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "storageServices": { + "resourceId": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccount'))]" + }, + "openAiServices": { + "resourceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiServiceName'))]" + } + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.VideoIndexer/accounts', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.limitedLogCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.VideoIndexer/accounts', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "videoIndexerServiceName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))]" + }, + "videoIndexerAccountId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.VideoIndexer/accounts', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))), '2025-04-01').accountId]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "openAI", + "rg", + "storageAccount" + ] + }, + "setPermissions": { + "condition": "[parameters('configureApplicationPermissions')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "setPermissions", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "webAppName": { + "value": "[reference('appService').outputs.name.value]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "enterpriseAppServicePrincipalId": { + "value": "[parameters('enterpriseAppServicePrincipalId')]" + }, + "keyVaultName": { + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "cosmosDBName": { + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" + }, + "acrName": { + "value": "[reference('acr').outputs.acrName.value]" + }, + "openAIName": { + "value": "[reference('openAI').outputs.openAIName.value]" + }, + "openAIResourceGroupName": { + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" + }, + "openAISubscriptionId": { + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" + }, + "docIntelName": { + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" + }, + "storageAccountName": { + "value": "[reference('storageAccount').outputs.name.value]" + }, + "searchServiceName": { + "value": "[reference('searchService').outputs.searchServiceName.value]" + }, + "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference('speechService').outputs.speechServiceName.value), createObject('value', ''))]", + "redisCacheName": "[if(parameters('deployRedisCache'), createObject('value', reference('redisCache').outputs.redisCacheName.value), createObject('value', ''))]", + "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference('contentSafety').outputs.contentSafetyName.value), createObject('value', ''))]", + "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference('videoIndexerService').outputs.videoIndexerServiceName.value), createObject('value', ''))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "4113398169754187856" + } + }, + "parameters": { + "webAppName": { + "type": "string" + }, + "authenticationType": { + "type": "string" + }, + "keyVaultName": { + "type": "string" + }, + "enterpriseAppServicePrincipalId": { + "type": "string" + }, + "cosmosDBName": { + "type": "string" + }, + "acrName": { + "type": "string" + }, + "openAIName": { + "type": "string" + }, + "openAIResourceGroupName": { + "type": "string" + }, + "openAISubscriptionId": { + "type": "string" + }, + "docIntelName": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + }, + "speechServiceName": { + "type": "string" + }, + "searchServiceName": { + "type": "string" + }, + "redisCacheName": { + "type": "string" + }, + "contentSafetyName": { + "type": "string" + }, + "videoIndexerName": { + "type": "string" + } + }, + "variables": { + "useExternalOpenAIResource": "[and(and(not(equals(parameters('openAIName'), '')), not(empty(parameters('openAIResourceGroupName')))), not(empty(parameters('openAISubscriptionId'))))]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]", + "name": "[guid(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'kv-secrets-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]", + "name": "[guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'cosmos-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2023-04-15", + "name": "[format('{0}/{1}', parameters('cosmosDBName'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'cosmos-data-contributor'))]", + "properties": { + "roleDefinitionId": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'acr-pull-role')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(and(equals(parameters('authenticationType'), 'managed_identity'), not(equals(parameters('openAIName'), ''))), not(variables('useExternalOpenAIResource')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'openai-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(and(equals(parameters('authenticationType'), 'managed_identity'), not(equals(parameters('openAIName'), ''))), not(variables('useExternalOpenAIResource')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'enterpriseApp-CognitiveServicesOpenAIUserRole')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('enterpriseAppServicePrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'doc-intel-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'storage-blob-data-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('speechServiceName'), '')), equals(parameters('authenticationType'), 'managed_identity'))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('speechServiceName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'speech-service-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]", + "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('searchServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'search-index-data-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]", + "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('searchServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'search-service-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('contentSafetyName'), '')), equals(parameters('authenticationType'), 'managed_identity'))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('contentSafetyName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'content-safety-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[not(equals(parameters('videoIndexerName'), ''))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-storage-blob-data-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalId": "[reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(variables('useExternalOpenAIResource')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-cog-services-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "principalId": "[reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(variables('useExternalOpenAIResource')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-cog-services-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[not(equals(parameters('redisCacheName'), ''))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Cache/redis', parameters('redisCacheName'))]", + "name": "[guid(resourceId('Microsoft.Cache/redis', parameters('redisCacheName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'redis-cache-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e0f68234-74aa-48ed-b826-c38b57376e17')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[variables('useExternalOpenAIResource')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAIExternalPermissions", + "subscriptionId": "[parameters('openAISubscriptionId')]", + "resourceGroup": "[parameters('openAIResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "openAIName": { + "value": "[parameters('openAIName')]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "webAppPrincipalId": { + "value": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]" + }, + "enterpriseAppServicePrincipalId": { + "value": "[parameters('enterpriseAppServicePrincipalId')]" + }, + "videoIndexerPrincipalId": "[if(not(equals(parameters('videoIndexerName'), '')), createObject('value', reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId), createObject('value', ''))]", + "videoIndexerName": { + "value": "[parameters('videoIndexerName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14586417486337251686" + } + }, + "parameters": { + "openAIName": { + "type": "string" + }, + "authenticationType": { + "type": "string" + }, + "webAppPrincipalId": { + "type": "string" + }, + "enterpriseAppServicePrincipalId": { + "type": "string" + }, + "videoIndexerPrincipalId": { + "type": "string", + "defaultValue": "" + }, + "videoIndexerName": { + "type": "string", + "defaultValue": "" + } + }, + "resources": [ + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('webAppPrincipalId'), 'openai-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('webAppPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('enterpriseAppServicePrincipalId'), 'enterpriseApp-CognitiveServicesOpenAIUserRole')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('enterpriseAppServicePrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(empty(parameters('videoIndexerPrincipalId'))))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('videoIndexerPrincipalId'), 'video-indexer-cog-services-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "principalId": "[parameters('videoIndexerPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(empty(parameters('videoIndexerPrincipalId'))))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('videoIndexerPrincipalId'), 'video-indexer-cog-services-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[parameters('videoIndexerPrincipalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + } + } + ] + } + }, + "dependsOn": [ + "acr", + "appService", + "contentSafety", + "cosmosDB", + "docIntel", + "keyVault", + "openAI", + "redisCache", + "rg", + "searchService", + "speechService", + "storageAccount", + "videoIndexerService" + ] + }, + "privateNetworking": { + "condition": "[parameters('enablePrivateNetworking')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "privateNetworking", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualNetworkId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', variables('resolvedExistingVirtualNetworkId')), createObject('value', reference('virtualNetwork').outputs.vNetId.value)), createObject('value', ''))]", + "privateEndpointSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingPrivateEndpointSubnetId')), createObject('value', reference('virtualNetwork').outputs.privateNetworkSubnetId.value)), createObject('value', ''))]", + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneConfigs')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "keyVaultName": { + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "cosmosDBName": { + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" + }, + "acrName": { + "value": "[reference('acr').outputs.acrName.value]" + }, + "searchServiceName": { + "value": "[reference('searchService').outputs.searchServiceName.value]" + }, + "docIntelName": { + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" + }, + "storageAccountName": { + "value": "[reference('storageAccount').outputs.name.value]" + }, + "openAIName": { + "value": "[reference('openAI').outputs.openAIName.value]" + }, + "openAIResourceGroupName": { + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" + }, + "openAISubscriptionId": { + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" + }, + "webAppName": { + "value": "[reference('appService').outputs.name.value]" + }, + "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference('contentSafety').outputs.contentSafetyName.value), createObject('value', ''))]", + "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference('speechService').outputs.speechServiceName.value), createObject('value', ''))]", + "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference('videoIndexerService').outputs.videoIndexerServiceName.value), createObject('value', ''))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "5473171678692396413" + } + }, + "parameters": { + "virtualNetworkId": { + "type": "string" + }, + "privateEndpointSubnetId": { + "type": "string" + }, + "privateDnsZoneConfigs": { + "type": "object", + "defaultValue": {} + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "keyVaultName": { + "type": "string" + }, + "cosmosDBName": { + "type": "string" + }, + "acrName": { + "type": "string" + }, + "searchServiceName": { + "type": "string" + }, + "docIntelName": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + }, + "openAIName": { + "type": "string" + }, + "openAIResourceGroupName": { + "type": "string" + }, + "openAISubscriptionId": { + "type": "string" + }, + "webAppName": { + "type": "string" + }, + "contentSafetyName": { + "type": "string" + }, + "speechServiceName": { + "type": "string" + }, + "videoIndexerName": { + "type": "string" + } + }, + "variables": { + "$fxv#0": { + "azurecloud": { + "aisearch": "privatelink.search.windows.net", + "blobStorage": "privatelink.blob.core.windows.net", + "cognitiveServices": "privatelink.cognitiveservices.azure.com", + "containerRegistry": "privatelink.azurecr.io", + "cosmosDb": "privatelink.documents.azure.com", + "keyVault": "privatelink.vaultcore.azure.net", + "openAi": "privatelink.openai.azure.com", + "webSites": "privatelink.azurewebsites.net" + }, + "azureusgovernment": { + "aisearch": "privatelink.search.azure.us", + "blobStorage": "privatelink.blob.core.usgovcloudapi.net", + "cognitiveServices": "privatelink.cognitiveservices.azure.us", + "containerRegistry": "privatelink.azurecr.us", + "cosmosDb": "privatelink.documents.azure.us", + "keyVault": "privatelink.vaultcore.azure.us", + "openAi": "privatelink.openai.azure.us", + "webSites": "privatelink.azurewebsites.us" + } + }, + "useExternalOpenAIResource": "[and(and(not(equals(parameters('openAIName'), '')), not(empty(parameters('openAIResourceGroupName')))), not(empty(parameters('openAISubscriptionId'))))]", + "cloudName": "[toLower(environment().name)]", + "privateDnsZoneData": "[variables('$fxv#0')]", + "aiSearchDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].aisearch]", + "blobStorageDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].blobStorage]", + "cognitiveServicesDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].cognitiveServices]", + "containerRegistryDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].containerRegistry]", + "cosmosDbDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].cosmosDb]", + "keyVaultDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].keyVault]", + "openAiDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].openAi]", + "webSitesDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].webSites]", + "defaultPrivateDnsZoneConfig": { + "zoneResourceId": "", + "createVNetLink": true + }, + "keyVaultPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'keyVault'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').keyVault), variables('defaultPrivateDnsZoneConfig'))]", + "cosmosDbPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'cosmosDb'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').cosmosDb), variables('defaultPrivateDnsZoneConfig'))]", + "containerRegistryPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'containerRegistry'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').containerRegistry), variables('defaultPrivateDnsZoneConfig'))]", + "aiSearchPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'aiSearch'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').aiSearch), variables('defaultPrivateDnsZoneConfig'))]", + "blobStoragePrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'blobStorage'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').blobStorage), variables('defaultPrivateDnsZoneConfig'))]", + "cognitiveServicesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'cognitiveServices'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').cognitiveServices), variables('defaultPrivateDnsZoneConfig'))]", + "openAiPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'openAi'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').openAi), variables('defaultPrivateDnsZoneConfig'))]", + "webSitesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'webSites'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').webSites), variables('defaultPrivateDnsZoneConfig'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "keyVaultDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('keyVaultDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "kv" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('keyVaultPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('keyVaultPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "keyVaultPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "kv" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "vault" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'keyVaultDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'keyVaultDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "cosmosDbDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('cosmosDbDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "cosmosDb" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('cosmosDbPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('cosmosDbPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "cosmosDbPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "cosmosDb" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "sql" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'cosmosDbDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'cosmosDbDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "acrDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('containerRegistryDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "acr" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('containerRegistryPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('containerRegistryPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "acrPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "acr" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "registry" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'acrDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'acrDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "searchServiceDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('aiSearchDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "searchService" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('aiSearchPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('aiSearchPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "searchServicePE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "searchService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "searchService" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'searchServiceDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'searchServiceDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "docIntelDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('cognitiveServicesDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "docIntelService" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "docIntelPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "docIntelService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storageAccountDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('blobStorageDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "storage" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('blobStoragePrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('blobStoragePrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storageAccountPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "storageAccount" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "blob" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('openAIName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAiDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('openAiDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "openAiService" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('openAiPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('openAiPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "condition": "[not(equals(parameters('openAIName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAiPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "openAiService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": "[if(variables('useExternalOpenAIResource'), createObject('value', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('openAISubscriptionId'), parameters('openAIResourceGroupName')), 'Microsoft.CognitiveServices/accounts', parameters('openAIName'))), createObject('value', resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))))]", + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'openAiDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'openAiDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "webAppDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('webSitesDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "webApp" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('webSitesPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('webSitesPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "webAppPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "webApp" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "sites" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'webAppDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'webAppDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('contentSafetyName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "contentSafetyPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "contentSafety" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('contentSafetyName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('speechServiceName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "speechServicePE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "speechService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('speechServiceName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('videoIndexerName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "videoIndexerPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "videoIndexerService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + ] + } + ] + } + }, + "dependsOn": [ + "acr", + "appService", + "contentSafety", + "cosmosDB", + "docIntel", + "keyVault", + "openAI", + "rg", + "searchService", + "speechService", + "storageAccount", + "videoIndexerService", + "virtualNetwork" + ] + } + }, + "outputs": { + "var_acrName": { + "type": "string", + "value": "[toLower(format('{0}{1}acr', parameters('appName'), parameters('environment')))]" + }, + "var_authenticationType": { + "type": "string", + "value": "[toLower(parameters('authenticationType'))]" + }, + "var_blobStorageEndpoint": { + "type": "string", + "value": "[reference('storageAccount').outputs.endpoint.value]" + }, + "var_configureApplication": { + "type": "bool", + "value": "[parameters('configureApplicationPermissions')]" + }, + "var_contentSafetyEndpoint": { + "type": "string", + "value": "[if(parameters('deployContentSafety'), reference('contentSafety').outputs.contentSafetyEndpoint.value, '')]" + }, + "var_cosmosDb_accountName": { + "type": "string", + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" + }, + "var_cosmosDb_uri": { + "type": "string", + "value": "[reference('cosmosDB').outputs.cosmosDbUri.value]" + }, + "var_deploymentLocation": { + "type": "string", + "value": "[reference('rg', '2022-09-01', 'full').location]" + }, + "var_documentIntelligenceServiceEndpoint": { + "type": "string", + "value": "[reference('docIntel').outputs.documentIntelligenceServiceEndpoint.value]" + }, + "var_keyVaultName": { + "type": "string", + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "var_keyVaultUri": { + "type": "string", + "value": "[reference('keyVault').outputs.keyVaultUri.value]" + }, + "var_openAIEndpoint": { + "type": "string", + "value": "[reference('openAI').outputs.openAIEndpoint.value]" + }, + "var_openAIGPTModels": { + "type": "array", + "value": "[variables('resolvedGptModels')]" + }, + "var_openAIResourceGroup": { + "type": "string", + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" + }, + "var_openAIEmbeddingModels": { + "type": "array", + "value": "[variables('resolvedEmbeddingModels')]" + }, + "var_openAISubscriptionId": { + "type": "string", + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" + }, + "var_redisCacheHostName": { + "type": "string", + "value": "[if(parameters('deployRedisCache'), reference('redisCache').outputs.redisCacheHostName.value, '')]" + }, + "var_rgName": { + "type": "string", + "value": "[variables('rgName')]" + }, + "var_searchServiceEndpoint": { + "type": "string", + "value": "[reference('searchService').outputs.searchServiceEndpoint.value]" + }, + "var_speechServiceEndpoint": { + "type": "string", + "value": "[if(parameters('deploySpeechService'), reference('speechService').outputs.speechServiceEndpoint.value, '')]" + }, + "var_subscriptionId": { + "type": "string", + "value": "[subscription().subscriptionId]" + }, + "var_videoIndexerAccountId": { + "type": "string", + "value": "[if(parameters('deployVideoIndexerService'), reference('videoIndexerService').outputs.videoIndexerAccountId.value, '')]" + }, + "var_videoIndexerName": { + "type": "string", + "value": "[if(parameters('deployVideoIndexerService'), reference('videoIndexerService').outputs.videoIndexerServiceName.value, '')]" + }, + "var_containerRegistry": { + "type": "string", + "value": "[variables('containerRegistry')]" + }, + "var_imageName": { + "type": "string", + "value": "[if(contains(parameters('imageName'), ':'), split(parameters('imageName'), ':')[0], parameters('imageName'))]" + }, + "var_imageTag": { + "type": "string", + "value": "[if(contains(parameters('imageName'), ':'), split(parameters('imageName'), ':')[1], 'latest')]" + }, + "var_webService": { + "type": "string", + "value": "[reference('appService').outputs.name.value]" + }, + "var_enablePrivateNetworking": { + "type": "bool", + "value": "[parameters('enablePrivateNetworking')]" + } + } +} +2026/03/25 11:26:51 main.go:54: Retry: =====> Try=1 for GET https://graph.microsoft.com/v1.0/me +2026/03/25 11:26:51 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://graph.microsoft.com/v1.0/me + Authorization: REDACTED + User-Agent: azsdk-go-graph/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: REDACTED + +2026/03/25 11:26:51 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/200.0306ms, OpTime=200.0306ms) -- RESPONSE RECEIVED + GET https://graph.microsoft.com/v1.0/me + Authorization: REDACTED + User-Agent: azsdk-go-graph/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: REDACTED + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Client-Request-Id: REDACTED + Content-Type: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Date: Wed, 25 Mar 2026 15:26:50 GMT + Odata-Version: REDACTED + Request-Id: 1734b612-8dc5-46a6-81d5-1319062cb67c + Strict-Transport-Security: REDACTED + Vary: REDACTED + X-Cache: REDACTED + X-Ms-Ags-Diagnostic: REDACTED + X-Ms-Resource-Unit: REDACTED + +2026/03/25 11:26:51 main.go:54: Retry: response 200 +2026/03/25 11:26:51 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:26:51 middleware.go:100: running middleware 'debug' +2026/03/25 11:26:51 middleware.go:100: running middleware 'ux' +2026/03/25 11:26:51 middleware.go:100: running middleware 'telemetry' +2026/03/25 11:26:51 telemetry.go:68: TraceID: dd6b9467838cd44356a50058eb0073ab +2026/03/25 11:26:51 middleware.go:100: running middleware 'error' +2026/03/25 11:26:51 middleware.go:100: running middleware 'hooks' + +Packaging services (azd package) + +2026/03/25 11:26:51 hooks.go:130: service 'web' does not require any command hooks. +2026/03/25 11:26:51 middleware.go:100: running middleware 'extensions' +2026/03/25 11:26:51 service_manager.go:677: Attempting to resolve language 'python' for service 'web' +2026/03/25 11:26:51 service_manager.go:696: Successfully resolved language 'python' for service 'web' +2026/03/25 11:26:51 service_manager.go:677: Attempting to resolve language 'python' for service 'web' +2026/03/25 11:26:51 service_manager.go:696: Successfully resolved language 'python' for service 'web' +2026/03/25 11:26:51 command_runner.go:325: Run exec: 'python --version' , exit code: 0 +-------------------------------------stdout------------------------------------------- +Python 3.12.10 +2026/03/25 11:26:51 python.go:49: python version: Python 3.12.10 +2026/03/25 11:26:51 service_manager.go:677: Attempting to resolve language 'python' for service 'web' +2026/03/25 11:26:51 service_manager.go:696: Successfully resolved language 'python' for service 'web' +Packaging service web +Packaging service web (Compressing deployment artifacts) + (Γ£ô) Done: Packaging service web + - Build Output: C:\repos\simplechat\application\single_app + - Package Output: C:\Users\PAULLI~1\AppData\Local\Temp\simplechat-web-azddeploy-1774452411.zip + +2026/03/25 11:26:52 middleware.go:100: running middleware 'debug' +2026/03/25 11:26:52 middleware.go:100: running middleware 'ux' +2026/03/25 11:26:52 middleware.go:100: running middleware 'telemetry' +2026/03/25 11:26:52 telemetry.go:68: TraceID: dd6b9467838cd44356a50058eb0073ab +2026/03/25 11:26:52 middleware.go:100: running middleware 'error' +2026/03/25 11:26:52 middleware.go:100: running middleware 'loginGuard' +2026/03/25 11:26:52 middleware.go:100: running middleware 'hooks' +2026/03/25 11:26:52 hooks.go:130: service 'web' does not require any command hooks. +2026/03/25 11:26:52 hooks_runner.go:195: Executing script 'C:\Users\PAULLI~1\AppData\Local\Temp\azd-preprovision-3334517763.ps1' +2026/03/25 11:26:55 command_runner.go:325: Run exec: 'pwsh C:\Users\PAULLI~1\AppData\Local\Temp\azd-preprovision-3334517763.ps1' , exit code: 0 +Additional env: + var_authenticationType= + var_containerRegistry= + var_subscriptionId= + var_cosmosDb_uri= + AZURE_SUBSCRIPTION_ID= + var_imageName= + var_videoIndexerAccountId= + AZURE_ENV_NAME= + var_openAIResourceGroup= + var_documentIntelligenceServiceEndpoint= + var_videoIndexerName= + var_speechServiceEndpoint= + var_configureApplication= + var_openAIEmbeddingModels= + var_enablePrivateNetworking= + var_deploymentLocation= + var_imageTag= + var_webService= + var_contentSafetyEndpoint= + var_cosmosDb_accountName= + var_openAIEndpoint= + AZURE_LOCATION= + var_keyVaultName= + var_keyVaultUri= + var_rgName= + var_openAIGPTModels= + var_acrName= + var_blobStorageEndpoint= + var_openAISubscriptionId= + var_searchServiceEndpoint= + var_redisCacheHostName= +-------------------------------------stdout------------------------------------------- +======================================= +PRE-PROVISION: Validating prerequisites +======================================= +2026/03/25 11:26:55 middleware.go:100: running middleware 'extensions' + +Provisioning Azure resources (azd provision) +Provisioning Azure resources can take some time. + +2026/03/25 11:26:55 service_manager.go:677: Attempting to resolve language 'python' for service 'web' +2026/03/25 11:26:55 service_manager.go:696: Successfully resolved language 'python' for service 'web' +2026/03/25 11:26:55 service_manager.go:677: Attempting to resolve language 'python' for service 'web' +2026/03/25 11:26:55 service_manager.go:696: Successfully resolved language 'python' for service 'web' +2026/03/25 11:26:55 ensure.go:52: Skipping install check for 'Python CLI'. It was previously confirmed. +2026/03/25 11:26:55 importer.go:324: using infrastructure from C:\repos\simplechat\deployers\bicep directory +Initialize bicep provider +2026/03/25 11:27:00 command_runner.go:325: Run exec: 'C:\Users\paullizer\.azd\bin\bicep.exe build C:\repos\simplechat\deployers\bicep\main.bicep --stdout' , exit code: 0 +-------------------------------------stdout------------------------------------------- +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "10060260231818509949" + } + }, + "parameters": { + "location": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "The Azure region where resources will be deployed. \n- Region must align to the target cloud environment" + } + }, + "cloudEnvironment": { + "type": "string", + "defaultValue": "[if(equals(environment().name, 'AzureCloud'), 'public', if(equals(environment().name, 'AzureUSGovernment'), 'usgovernment', 'custom'))]", + "allowedValues": [ + "AzureCloud", + "AzureUSGovernment", + "public", + "usgovernment", + "custom" + ], + "metadata": { + "description": "The target Azure Cloud environment.\n- Accepted values are: AzureCloud, AzureUSGovernment, public, usgovernment, custom\n- Default is based on the ARM cloud name" + } + }, + "appName": { + "type": "string", + "minLength": 3, + "maxLength": 12, + "metadata": { + "description": "The name of the application to be deployed. \n- Name may only contain letters and numbers\n- Between 3 and 12 characters in length \n- No spaces or special characters" + } + }, + "environment": { + "type": "string", + "minLength": 2, + "maxLength": 10, + "metadata": { + "description": "The dev/qa/prod environment or as named in your environment. This will be used to create resource group names and tags.\n- Must be between 2 and 10 characters in length\n- No spaces or special characters" + } + }, + "azdEnvironmentName": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "metadata": { + "description": "Name of the AZD environment" + } + }, + "imageName": { + "type": "string", + "defaultValue": "simplechat:latest", + "metadata": { + "description": "The name of the container image to deploy to the web app.\n- should be in the format :" + } + }, + "enterpriseAppClientId": { + "type": "string", + "metadata": { + "description": "Azure AD Application Client ID for enterprise authentication.\n- Should be the client ID of the registered Azure AD application" + } + }, + "enterpriseAppServicePrincipalId": { + "type": "string", + "metadata": { + "description": "Azure AD Application Service Principal Id for the enterprise application.\n- Should be the Service Principal ID of the registered Azure AD application" + } + }, + "enterpriseAppClientSecret": { + "type": "securestring", + "metadata": { + "description": "Azure AD Application Client Secret for enterprise authentication.\n- Required if enableEnterpriseApp is true\n- Should be created in Azure AD App Registration and passed via environment variable\n- Will be stored securely in Azure Key Vault during deployment" + } + }, + "authenticationType": { + "type": "string", + "allowedValues": [ + "key", + "managed_identity" + ], + "metadata": { + "description": "Authentication type for resources that support Managed Identity or Key authentication.\n- Key: Use access keys for authentication (application keys will be stored in Key Vault)\n- managed_identity: Use Managed Identity for authentication" + } + }, + "configureApplicationPermissions": { + "type": "bool", + "metadata": { + "description": "Configure permissions (based on authenticationType) for the deployed web application to access required resources.\n" + } + }, + "specialTags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional object containing additional tags to apply to all resources." + } + }, + "enableDiagLogging": { + "type": "bool", + "metadata": { + "description": "Enable diagnostic logging for resources deployed in the resource group. \n- All content will be sent to the deployed Log Analytics workspace\n- Default is false" + } + }, + "enablePrivateNetworking": { + "type": "bool", + "metadata": { + "description": "Enable private endpoints and virtual network integration for deployed resources. \n- Default is false" + } + }, + "existingVirtualNetworkId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing virtual network resource ID to reuse when private networking is enabled.\n- May reference a virtual network in the same or another resource group or subscription\n- Leave blank to create a new virtual network" + } + }, + "existingAppServiceSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing subnet resource ID to use for App Service VNet integration.\n- May reference a subnet in the same or another resource group or subscription\n- Required when reusing an existing virtual network because subnets are not created in external virtual networks" + } + }, + "existingPrivateEndpointSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing subnet resource ID to use for private endpoints.\n- May reference a subnet in the same or another resource group or subscription\n- Required when reusing an existing virtual network because subnets are not created in external virtual networks" + } + }, + "privateDnsZoneConfigs": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional per-zone private DNS configuration for private networking.\n- Leave empty to create all private DNS zones locally and create VNet links automatically\n- For each supported key, provide:\n - zoneResourceId: Optional existing private DNS zone resource ID to reuse\n - createVNetLink: Optional bool, defaults to true. Set to false if the customer manages the VNet link separately\n- Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites" + } + }, + "existingOpenAIEndpoint": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint.\n- Leave blank to deploy a new Azure OpenAI resource\n- Public Azure AI Foundry project endpoints are supported for application configuration, but do not support private endpoint automation" + } + }, + "existingOpenAIResourceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional existing Azure OpenAI resource name.\n- Provide this when reusing a standard Azure OpenAI resource and you want managed identity permissions or private endpoint integration configured automatically" + } + }, + "existingOpenAIResourceGroup": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional resource group for an existing Azure OpenAI resource.\n- Used when reusing a standard Azure OpenAI resource across resource groups or subscriptions" + } + }, + "existingOpenAISubscriptionId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional subscription ID for an existing Azure OpenAI resource.\n- Used when reusing a standard Azure OpenAI resource across subscriptions" + } + }, + "openAIDeploymentType": { + "type": "string", + "allowedValues": [ + "Standard", + "DatazoneStandard", + "GlobalStandard" + ], + "metadata": { + "description": "Azure OpenAI deployment type used for the default GPT and embedding model deployments.\n- Azure Commercial options: Standard, DatazoneStandard, GlobalStandard\n- Azure Government default model deployments use Standard regardless of this selection\n- Ignored when you provide custom gptModels or embeddingModels arrays" + } + }, + "customBlobStorageSuffix": { + "type": "string", + "defaultValue": "[format('blob.{0}', environment().suffixes.storage)]", + "metadata": { + "description": "Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net" + } + }, + "customGraphUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Graph API URL, e.g. https://graph.microsoft.us" + } + }, + "customIdentityUrl": { + "type": "string", + "defaultValue": "[environment().authentication.loginEndpoint]", + "metadata": { + "description": "Custom Identity URL, e.g. https://login.microsoftonline.us/" + } + }, + "customResourceManagerUrl": { + "type": "string", + "defaultValue": "[environment().resourceManager]", + "metadata": { + "description": "Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net" + } + }, + "customCognitiveServicesScope": { + "type": "string", + "defaultValue": "https://cognitiveservices.azure.com/.default", + "metadata": { + "description": "Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default" + } + }, + "customSearchResourceUrl": { + "type": "string", + "defaultValue": "https://search.azure.com", + "metadata": { + "description": "Custom search resource URL for token audience, e.g. https://search.azure.us" + } + }, + "gptModels": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Array of GPT model names to deploy to the OpenAI resource." + } + }, + "embeddingModels": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Array of embedding model names to deploy to the OpenAI resource." + } + }, + "allowedIpAddresses": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Comma separated list of IP addresses or ranges to allow access to resources when private networking is enabled.\nLeave blank if not using private networking.\n- Format for single IP: 'x.x.x.x'\n- Format for range: 'x.x.x.x/y'\n- Example: 1.2.3.4, 2.3.4.5/32\n" + } + }, + "deployContentSafety": { + "type": "bool", + "metadata": { + "description": "Enable deployment of Content Safety service and related resources.\n- Default is false" + } + }, + "deployRedisCache": { + "type": "bool", + "metadata": { + "description": "Enable deployment of Azure Cache for Redis and related resources.\n- Default is false" + } + }, + "deploySpeechService": { + "type": "bool", + "metadata": { + "description": "Enable deployment of Azure Speech service and related resources.\n- Default is false" + } + }, + "deployVideoIndexerService": { + "type": "bool", + "metadata": { + "description": "Enable deployment of Azure Video Indexer service and related resources.\n- Default is false" + } + } + }, + "variables": { + "copy": [ + { + "name": "allowedIpAddressesArray", + "count": "[length(variables('allowedIpAddressesSplit'))]", + "input": "[trim(variables('allowedIpAddressesSplit')[copyIndex('allowedIpAddressesArray')])]" + }, + { + "name": "cosmosDbIpRules", + "count": "[length(variables('allowedIpsForCosmos'))]", + "input": { + "ipAddressOrRange": "[variables('allowedIpsForCosmos')[copyIndex('cosmosDbIpRules')]]" + } + }, + { + "name": "acrIpRules", + "count": "[length(variables('allowedIpAddressesArray'))]", + "input": { + "action": "Allow", + "value": "[variables('allowedIpAddressesArray')[copyIndex('acrIpRules')]]" + } + } + ], + "scCloudEnvironment": "[if(equals(parameters('cloudEnvironment'), 'AzureCloud'), 'public', if(equals(parameters('cloudEnvironment'), 'AzureUSGovernment'), 'usgovernment', parameters('cloudEnvironment')))]", + "allowedIpAddressesSplit": "[if(empty(parameters('allowedIpAddresses')), createArray(), split(parameters('allowedIpAddresses'), ','))]", + "rgName": "[format('{0}-{1}-rg', parameters('appName'), parameters('environment'))]", + "requiredTags": { + "application": "[parameters('appName')]", + "environment": "[parameters('environment')]", + "azd-env-name": "[parameters('azdEnvironmentName')]" + }, + "tags": "[union(variables('requiredTags'), parameters('specialTags'))]", + "isPublicCloud": "[equals(variables('scCloudEnvironment'), 'public')]", + "isUsGovernmentCloud": "[equals(variables('scCloudEnvironment'), 'usgovernment')]", + "acrCloudSuffix": "[if(variables('isPublicCloud'), '.azurecr.io', '.azurecr.us')]", + "acrName": "[toLower(format('{0}{1}acr', parameters('appName'), parameters('environment')))]", + "containerRegistry": "[format('{0}{1}', variables('acrName'), variables('acrCloudSuffix'))]", + "containerImageName": "[format('{0}/{1}', variables('containerRegistry'), parameters('imageName'))]", + "vNetName": "[format('{0}-{1}-vnet', parameters('appName'), parameters('environment'))]", + "normalizedLocation": "[toLower(replace(parameters('location'), ' ', ''))]", + "resolvedOpenAIDeploymentType": "[if(variables('isUsGovernmentCloud'), 'Standard', parameters('openAIDeploymentType'))]", + "defaultGptModels": [ + { + "modelName": "gpt-4o", + "modelVersion": "[if(variables('isUsGovernmentCloud'), '2024-05-13', '2024-11-20')]", + "skuName": "[variables('resolvedOpenAIDeploymentType')]", + "skuCapacity": 100 + } + ], + "defaultEmbeddingModels": "[if(variables('isUsGovernmentCloud'), createArray(createObject('modelName', if(equals(variables('normalizedLocation'), 'usgovvirginia'), 'text-embedding-ada-002', 'text-embedding-3-small'), 'modelVersion', if(equals(variables('normalizedLocation'), 'usgovvirginia'), '2', '1'), 'skuName', variables('resolvedOpenAIDeploymentType'), 'skuCapacity', 100)), createArray(createObject('modelName', 'text-embedding-3-small', 'modelVersion', '1', 'skuName', variables('resolvedOpenAIDeploymentType'), 'skuCapacity', 100)))]", + "resolvedGptModels": "[if(empty(parameters('gptModels')), variables('defaultGptModels'), parameters('gptModels'))]", + "resolvedEmbeddingModels": "[if(empty(parameters('embeddingModels')), variables('defaultEmbeddingModels'), parameters('embeddingModels'))]", + "hasExistingAppServiceSubnetId": "[not(empty(parameters('existingAppServiceSubnetId')))]", + "hasExistingPrivateEndpointSubnetId": "[not(empty(parameters('existingPrivateEndpointSubnetId')))]", + "inferredVirtualNetworkId": "[if(variables('hasExistingAppServiceSubnetId'), split(parameters('existingAppServiceSubnetId'), '/subnets/')[0], if(variables('hasExistingPrivateEndpointSubnetId'), split(parameters('existingPrivateEndpointSubnetId'), '/subnets/')[0], ''))]", + "resolvedExistingVirtualNetworkId": "[if(not(empty(parameters('existingVirtualNetworkId'))), parameters('existingVirtualNetworkId'), variables('inferredVirtualNetworkId'))]", + "useExistingVirtualNetwork": "[and(parameters('enablePrivateNetworking'), or(or(not(empty(variables('resolvedExistingVirtualNetworkId'))), variables('hasExistingAppServiceSubnetId')), variables('hasExistingPrivateEndpointSubnetId')))]", + "allowedIpsForCosmos": "[union(createArray('0.0.0.0'), variables('allowedIpAddressesArray'))]" + }, + "resources": { + "rg": { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2022-09-01", + "name": "[variables('rgName')]", + "location": "[parameters('location')]", + "tags": "[variables('tags')]" + }, + "virtualNetwork": { + "condition": "[and(parameters('enablePrivateNetworking'), not(variables('useExistingVirtualNetwork')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "virtualNetwork", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "vNetName": { + "value": "[variables('vNetName')]" + }, + "addressSpaces": { + "value": [ + "10.0.0.0/21" + ] + }, + "subnetConfigs": { + "value": [ + { + "name": "AppServiceIntegration", + "addressPrefix": "10.0.0.0/24", + "enablePrivateEndpointNetworkPolicies": true, + "enablePrivateLinkServiceNetworkPolicies": true + }, + { + "name": "PrivateEndpoints", + "addressPrefix": "10.0.2.0/24", + "enablePrivateEndpointNetworkPolicies": true, + "enablePrivateLinkServiceNetworkPolicies": true + } + ] + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "493906105301296560" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "vNetName": { + "type": "string" + }, + "addressSpaces": { + "type": "array" + }, + "subnetConfigs": { + "type": "array" + }, + "tags": { + "type": "object" + } + }, + "variables": { + "copy": [ + { + "name": "subnetIds", + "count": "[length(parameters('subnetConfigs'))]", + "input": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vNetName'), parameters('subnetConfigs')[copyIndex('subnetIds')].name)]" + }, + { + "name": "subnetNames", + "count": "[length(parameters('subnetConfigs'))]", + "input": "[parameters('subnetConfigs')[copyIndex('subnetNames')].name]" + } + ], + "appServiceIntegrationSubnetIndex": "[indexOf(variables('subnetNames'), 'AppServiceIntegration')]", + "privateEndpointIndex": "[indexOf(variables('subnetNames'), 'PrivateEndpoints')]" + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2021-05-01", + "name": "[parameters('vNetName')]", + "location": "[parameters('location')]", + "properties": { + "copy": [ + { + "name": "subnets", + "count": "[length(parameters('subnetConfigs'))]", + "input": { + "name": "[parameters('subnetConfigs')[copyIndex('subnets')].name]", + "properties": { + "addressPrefix": "[parameters('subnetConfigs')[copyIndex('subnets')].addressPrefix]", + "privateEndpointNetworkPolicies": "[if(parameters('subnetConfigs')[copyIndex('subnets')].enablePrivateEndpointNetworkPolicies, 'Enabled', 'Disabled')]", + "privateLinkServiceNetworkPolicies": "[if(parameters('subnetConfigs')[copyIndex('subnets')].enablePrivateLinkServiceNetworkPolicies, 'Enabled', 'Disabled')]", + "delegations": "[if(equals(parameters('subnetConfigs')[copyIndex('subnets')].name, 'AppServiceIntegration'), createArray(createObject('name', 'delegation', 'properties', createObject('serviceName', 'Microsoft.Web/serverFarms'))), createArray())]" + } + } + } + ], + "addressSpace": { + "addressPrefixes": "[parameters('addressSpaces')]" + } + }, + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "vNetId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vNetName'))]" + }, + "privateNetworkSubnetId": { + "type": "string", + "value": "[if(equals(variables('privateEndpointIndex'), -1), '', variables('subnetIds')[variables('privateEndpointIndex')])]" + }, + "appServiceSubnetId": { + "type": "string", + "value": "[if(equals(variables('appServiceIntegrationSubnetIndex'), -1), '', variables('subnetIds')[variables('appServiceIntegrationSubnetIndex')])]" + } + } + } + }, + "dependsOn": [ + "rg" + ] + }, + "logAnalytics": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "logAnalytics", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3702219330633152864" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[toLower(format('{0}-{1}-la', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + } + }, + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "logAnalyticsId": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', toLower(format('{0}-{1}-la', parameters('appName'), parameters('environment'))))]" + } + } + } + }, + "dependsOn": [ + "rg" + ] + }, + "applicationInsights": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "applicationInsights", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16478820448576828524" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "logAnalyticsId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[toLower(format('{0}-{1}-ai', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[parameters('logAnalyticsId')]" + }, + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "appInsightsName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-ai', parameters('appName'), parameters('environment')))]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "keyVault": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "keyVault", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3020666832422074415" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "properties": { + "tenantId": "[subscription().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "accessPolicies": [], + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": false, + "publicNetworkAccess": "Enabled", + "enableRbacAuthorization": true + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "keyVaultId": { + "type": "string", + "value": "[resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment'))))]" + }, + "keyVaultName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))]" + }, + "keyVaultUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.KeyVault/vaults', toLower(format('{0}-{1}-kv', parameters('appName'), parameters('environment')))), '2024-11-01').vaultUri]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "storeEnterpriseAppSecret": { + "condition": "[not(empty(parameters('enterpriseAppClientSecret')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storeEnterpriseAppSecret", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "secretName": { + "value": "enterprise-app-client-secret" + }, + "secretValue": { + "value": "[parameters('enterpriseAppClientSecret')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" + } + }, + "parameters": { + "keyVaultName": { + "type": "string" + }, + "secretName": { + "type": "string" + }, + "secretValue": { + "type": "securestring" + } + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2025-05-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretName'))]", + "properties": { + "value": "[parameters('secretValue')]" + } + } + ], + "outputs": { + "SecretUri": { + "type": "string", + "value": "[format('{0}secrets/{1}', reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), '2025-05-01').vaultUri, parameters('secretName'))]" + } + } + } + }, + "dependsOn": [ + "keyVault", + "rg" + ] + }, + "cosmosDB": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "cosmosDB", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "allowedIpAddresses": { + "value": "[variables('cosmosDbIpRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "12491761736151044216" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + }, + "allowedIpAddresses": { + "type": "array", + "defaultValue": [] + } + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2023-04-15", + "name": "[toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "GlobalDocumentDB", + "properties": { + "publicNetworkAccess": "Enabled", + "databaseAccountOfferType": "Standard", + "capabilities": [ + { + "name": "EnableServerless" + } + ], + "isVirtualNetworkFilterEnabled": "[if(parameters('enablePrivateNetworking'), true(), false())]", + "ipRules": "[if(parameters('enablePrivateNetworking'), parameters('allowedIpAddresses'), createArray())]", + "locations": [ + { + "locationName": "[parameters('location')]", + "failoverPriority": 0, + "isZoneRedundant": false + } + ], + "consistencyPolicy": { + "defaultConsistencyLevel": "Session" + } + }, + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2023-04-15", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))), 'SimpleChat')]", + "properties": { + "resource": { + "id": "SimpleChat" + }, + "options": {} + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2023-04-15", + "name": "[format('{0}/{1}/{2}', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))), 'SimpleChat', 'settings')]", + "properties": { + "resource": { + "id": "settings", + "partitionKey": { + "paths": [ + "/id" + ] + } + }, + "options": {} + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))), 'SimpleChat')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": [] + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment'))))]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "cosmosDbName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))]" + }, + "cosmosDbUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('{0}-{1}-cosmos', parameters('appName'), parameters('environment')))), '2023-04-15').documentEndpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "acr": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "azureContainerRegistry", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "acrName": { + "value": "[variables('acrName')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "allowedIpAddresses": { + "value": "[variables('acrIpRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "8868053889964006912" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "acrName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + }, + "allowedIpAddresses": { + "type": "array", + "defaultValue": [] + } + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-04-01", + "name": "[parameters('acrName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[if(parameters('enablePrivateNetworking'), 'Premium', 'Standard')]" + }, + "properties": { + "adminUserEnabled": true, + "publicNetworkAccess": "Enabled", + "networkRuleBypassOptions": "[if(parameters('enablePrivateNetworking'), 'AzureServices', 'None')]", + "networkRuleSet": "[if(parameters('enablePrivateNetworking'), createObject('defaultAction', 'Allow', 'ipRules', parameters('allowedIpAddresses')), null())]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[toLower(format('{0}-diagnostics', parameters('acrName')))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "acrName": { + "type": "string", + "value": "[parameters('acrName')]" + }, + "acrResourceGroup": { + "type": "string", + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "searchService": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "searchService", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "433641641835184139" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-05-01", + "name": "[toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "sku": { + "name": "basic" + }, + "properties": { + "hostingMode": "default", + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "replicaCount": 1, + "partitionCount": 1, + "authOptions": { + "aadOrApiKey": { + "aadAuthFailureMode": "http403" + } + }, + "disableLocalAuth": false + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Search/searchServices', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.Search/searchServices', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "searchServiceName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))]" + }, + "searchServiceEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Search/searchServices', toLower(format('{0}-{1}-search', parameters('appName'), parameters('environment')))), '2025-05-01').endpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "docIntel": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "docIntel", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "1751663477897380161" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "FormRecognizer", + "sku": { + "name": "S0" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "customSubDomainName": "[toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "documentIntelligenceServiceName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))]" + }, + "diagnosticLoggingEnabled": { + "type": "bool", + "value": "[parameters('enableDiagLogging')]" + }, + "documentIntelligenceServiceEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-docintel', parameters('appName'), parameters('environment')))), '2025-06-01').endpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "storageAccount": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storageAccount", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "keyVault": { + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "configureApplicationPermissions": { + "value": "[parameters('configureApplicationPermissions')]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "811367549199956159" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "keyVault": { + "type": "string" + }, + "authenticationType": { + "type": "string" + }, + "configureApplicationPermissions": { + "type": "bool" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-09-01", + "name": "[toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "accessTier": "Hot", + "allowBlobPublicAccess": false, + "allowSharedKeyAccess": true, + "isHnsEnabled": true + }, + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}/{2}', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default', 'user-documents')]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}/{2}', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default', 'group-documents')]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": [], + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.transactionMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]", + "name": "[toLower(format('{0}-blob-diagnostics', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.transactionMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))), 'default')]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + }, + { + "condition": "[and(equals(parameters('authenticationType'), 'key'), parameters('configureApplicationPermissions'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storeStorageAccountSecret", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[parameters('keyVault')]" + }, + "secretName": { + "value": "storage-account-key" + }, + "secretValue": { + "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))), '2022-09-01').keys[0].value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "15395992383660736405" + } + }, + "parameters": { + "keyVaultName": { + "type": "string" + }, + "secretName": { + "type": "string" + }, + "secretValue": { + "type": "securestring" + } + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2025-05-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretName'))]", + "properties": { + "value": "[parameters('secretValue')]" + } + } + ], + "outputs": { + "SecretUri": { + "type": "string", + "value": "[format('{0}secrets/{1}', reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), '2025-05-01').vaultUri, parameters('secretName'))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment'))))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))]" + }, + "endpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', toLower(format('{0}{1}sa', parameters('appName'), parameters('environment')))), '2022-09-01').primaryEndpoints.blob]" + } + } + } + }, + "dependsOn": [ + "keyVault", + "logAnalytics", + "rg" + ] + }, + "openAI": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAI", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "existingOpenAIEndpoint": { + "value": "[parameters('existingOpenAIEndpoint')]" + }, + "existingOpenAIResourceName": { + "value": "[parameters('existingOpenAIResourceName')]" + }, + "existingOpenAIResourceGroup": { + "value": "[parameters('existingOpenAIResourceGroup')]" + }, + "existingOpenAISubscriptionId": { + "value": "[parameters('existingOpenAISubscriptionId')]" + }, + "gptModels": { + "value": "[variables('resolvedGptModels')]" + }, + "embeddingModels": { + "value": "[variables('resolvedEmbeddingModels')]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3132373501615141396" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "existingOpenAIEndpoint": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAIResourceName": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAIResourceGroup": { + "type": "string", + "defaultValue": "" + }, + "existingOpenAISubscriptionId": { + "type": "string", + "defaultValue": "" + }, + "gptModels": { + "type": "array" + }, + "embeddingModels": { + "type": "array" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "variables": { + "useExistingOpenAIEndpoint": "[not(empty(parameters('existingOpenAIEndpoint')))]", + "resolvedOpenAIResourceGroup": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIResourceGroup'), resourceGroup().name)]", + "resolvedOpenAISubscriptionId": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAISubscriptionId'), subscription().subscriptionId)]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingOpenAIEndpoint'))]", + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2024-10-01", + "name": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "OpenAI", + "sku": { + "name": "S0" + }, + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "customSubDomainName": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(parameters('enableDiagLogging'), not(variables('useExistingOpenAIEndpoint')))]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + }, + { + "copy": { + "name": "aiModel", + "count": "[length(if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray()))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('model-{0}-{1}', replace(if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelName, '.', '-'), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "parent": { + "value": "[toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))]" + }, + "modelName": { + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelName]" + }, + "modelVersion": { + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].modelVersion]" + }, + "skuName": { + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].skuName]" + }, + "skuCapacity": { + "value": "[if(not(variables('useExistingOpenAIEndpoint')), concat(parameters('gptModels'), parameters('embeddingModels')), createArray())[copyIndex()].skuCapacity]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "12746224250029736064" + } + }, + "parameters": { + "parent": { + "type": "string" + }, + "modelName": { + "type": "string" + }, + "modelVersion": { + "type": "string" + }, + "skuName": { + "type": "string" + }, + "skuCapacity": { + "type": "int" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2025-06-01", + "name": "[format('{0}/{1}', parameters('parent'), parameters('modelName'))]", + "properties": { + "model": { + "format": "OpenAI", + "name": "[parameters('modelName')]", + "version": "[parameters('modelVersion')]" + } + }, + "sku": { + "name": "[parameters('skuName')]", + "capacity": "[parameters('skuCapacity')]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]" + ] + } + ], + "outputs": { + "openAIName": { + "type": "string", + "value": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIResourceName'), toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment'))))]" + }, + "openAIResourceGroup": { + "type": "string", + "value": "[variables('resolvedOpenAIResourceGroup')]" + }, + "openAISubscriptionId": { + "type": "string", + "value": "[variables('resolvedOpenAISubscriptionId')]" + }, + "openAIEndpoint": { + "type": "string", + "value": "[if(variables('useExistingOpenAIEndpoint'), parameters('existingOpenAIEndpoint'), reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-openai', parameters('appName'), parameters('environment')))), '2024-10-01').endpoint)]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "appServicePlan": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "appServicePlan", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "9868911322271424816" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2022-03-01", + "name": "[toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "sku": { + "name": "P1v3", + "tier": "PremiumV3", + "size": "P1v3", + "capacity": 1 + }, + "kind": "app,linux,container", + "properties": { + "reserved": true, + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Web/serverfarms', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": [], + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment'))))]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "appServicePlanId": { + "type": "string", + "value": "[resourceId('Microsoft.Web/serverfarms', toLower(format('{0}-{1}-asp', parameters('appName'), parameters('environment'))))]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "appService": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "appService", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "acrName": { + "value": "[reference('acr').outputs.acrName.value]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "appServicePlanId": { + "value": "[reference('appServicePlan').outputs.appServicePlanId.value]" + }, + "containerImageName": { + "value": "[variables('containerImageName')]" + }, + "azurePlatform": { + "value": "[variables('scCloudEnvironment')]" + }, + "cosmosDbName": { + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" + }, + "searchServiceName": { + "value": "[reference('searchService').outputs.searchServiceName.value]" + }, + "openAiServiceName": { + "value": "[reference('openAI').outputs.openAIName.value]" + }, + "openAiEndpoint": { + "value": "[reference('openAI').outputs.openAIEndpoint.value]" + }, + "openAiResourceGroupName": { + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" + }, + "documentIntelligenceServiceName": { + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" + }, + "appInsightsName": { + "value": "[reference('applicationInsights').outputs.appInsightsName.value]" + }, + "enterpriseAppClientId": { + "value": "[parameters('enterpriseAppClientId')]" + }, + "enterpriseAppClientSecret": { + "value": "[parameters('enterpriseAppClientSecret')]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "keyVaultUri": { + "value": "[reference('keyVault').outputs.keyVaultUri.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "appServiceSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingAppServiceSubnetId')), createObject('value', reference('virtualNetwork').outputs.appServiceSubnetId.value)), createObject('value', ''))]", + "customBlobStorageSuffix": { + "value": "[parameters('customBlobStorageSuffix')]" + }, + "customGraphUrl": { + "value": "[parameters('customGraphUrl')]" + }, + "customIdentityUrl": { + "value": "[parameters('customIdentityUrl')]" + }, + "customResourceManagerUrl": { + "value": "[parameters('customResourceManagerUrl')]" + }, + "customCognitiveServicesScope": { + "value": "[parameters('customCognitiveServicesScope')]" + }, + "customSearchResourceUrl": { + "value": "[parameters('customSearchResourceUrl')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "7866543323088689102" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "acrName": { + "type": "string" + }, + "appServicePlanId": { + "type": "string" + }, + "containerImageName": { + "type": "string" + }, + "azurePlatform": { + "type": "string" + }, + "cosmosDbName": { + "type": "string" + }, + "searchServiceName": { + "type": "string" + }, + "openAiServiceName": { + "type": "string" + }, + "openAiEndpoint": { + "type": "string" + }, + "openAiResourceGroupName": { + "type": "string" + }, + "documentIntelligenceServiceName": { + "type": "string" + }, + "appInsightsName": { + "type": "string" + }, + "enterpriseAppClientId": { + "type": "string", + "defaultValue": "" + }, + "authenticationType": { + "type": "string" + }, + "enterpriseAppClientSecret": { + "type": "securestring", + "defaultValue": "" + }, + "keyVaultUri": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + }, + "appServiceSubnetId": { + "type": "string", + "defaultValue": "" + }, + "customBlobStorageSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net" + } + }, + "customGraphUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Graph API URL, e.g. https://graph.microsoft.us" + } + }, + "customIdentityUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Identity URL, e.g. https://login.microsoftonline.us" + } + }, + "customResourceManagerUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net" + } + }, + "customCognitiveServicesScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default" + } + }, + "customSearchResourceUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Custom search resource URL for token audience, e.g. https://search.azure.us" + } + } + }, + "variables": { + "tenantId": "[tenant().tenantId]", + "openIdMetadataUrl": "[format('{0}{1}/v2.0/.well-known/openid-configuration', environment().authentication.loginEndpoint, variables('tenantId'))]", + "acrDomain": "[environment().suffixes.acrLoginServer]" + }, + "resources": { + "acrService": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-04-01", + "name": "[parameters('acrName')]" + }, + "cosmosDb": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2023-04-15", + "name": "[parameters('cosmosDbName')]" + }, + "searchService": { + "existing": true, + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-05-01", + "name": "[parameters('searchServiceName')]" + }, + "documentIntelligence": { + "existing": true, + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[parameters('documentIntelligenceServiceName')]" + }, + "appInsights": { + "existing": true, + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[parameters('appInsightsName')]" + }, + "webApp": { + "type": "Microsoft.Web/sites", + "apiVersion": "2022-03-01", + "name": "[toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "app,linux,container", + "properties": { + "serverFarmId": "[parameters('appServicePlanId')]", + "virtualNetworkSubnetId": "[if(not(equals(parameters('appServiceSubnetId'), '')), parameters('appServiceSubnetId'), null())]", + "publicNetworkAccess": "Enabled", + "vnetImagePullEnabled": "[if(parameters('enablePrivateNetworking'), true(), false())]", + "siteConfig": { + "linuxFxVersion": "[format('DOCKER|{0}', parameters('containerImageName'))]", + "acrUseManagedIdentityCreds": true, + "acrUserManagedIdentityID": "", + "alwaysOn": true, + "ftpsState": "Disabled", + "healthCheckPath": "/external/healthcheck", + "appSettings": "[flatten(createArray(createArray(createObject('name', 'AZURE_ENVIRONMENT', 'value', parameters('azurePlatform')), createObject('name', 'SCM_DO_BUILD_DURING_DEPLOYMENT', 'value', 'false'), createObject('name', 'AZURE_COSMOS_ENDPOINT', 'value', reference('cosmosDb').documentEndpoint), createObject('name', 'AZURE_COSMOS_AUTHENTICATION_TYPE', 'value', toLower(parameters('authenticationType')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_COSMOS_KEY', 'value', listKeys('cosmosDb', '2023-04-15').primaryMasterKey)), createArray()), createArray(createObject('name', 'TENANT_ID', 'value', tenant().tenantId), createObject('name', 'CLIENT_ID', 'value', parameters('enterpriseAppClientId')), createObject('name', 'SECRET_KEY', 'value', if(not(empty(parameters('enterpriseAppClientSecret'))), parameters('enterpriseAppClientSecret'), format('@Microsoft.KeyVault(SecretUri={0}secrets/enterprise-app-client-secret)', parameters('keyVaultUri')))), createObject('name', 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', 'value', format('@Microsoft.KeyVault(SecretUri= parameters('keyVaultUri'))), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', format('https://{0}{1}', parameters('acrName'), variables('acrDomain')))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_USERNAME', 'value', listCredentials('acrService', '2025-04-01').username)), createArray()), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_PASSWORD', 'value', listCredentials('acrService', '2025-04-01').passwords[0].value)), createArray()), createArray(createObject('name', 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', 'value', tenant().tenantId), createObject('name', 'AZURE_OPENAI_RESOURCE_NAME', 'value', parameters('openAiServiceName')), createObject('name', 'AZURE_OPENAI_RESOURCE_GROUP_NAME', 'value', parameters('openAiResourceGroupName')), createObject('name', 'AZURE_OPENAI_URL', 'value', parameters('openAiEndpoint')), createObject('name', 'AZURE_SEARCH_SERVICE_NAME', 'value', parameters('searchServiceName'))), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_SEARCH_API_KEY', 'value', listAdminKeys('searchService', '2025-05-01').primaryKey)), createArray()), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', 'value', reference('documentIntelligence').endpoint)), if(equals(parameters('authenticationType'), 'key'), createArray(createObject('name', 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', 'value', listKeys('documentIntelligence', '2025-06-01').key1)), createArray()), createArray(createObject('name', 'APPINSIGHTS_INSTRUMENTATIONKEY', 'value', reference('appInsights').InstrumentationKey), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference('appInsights').ConnectionString), createObject('name', 'APPINSIGHTS_PROFILERFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION', 'value', '1.0.0'), createObject('name', 'APPLICATIONINSIGHTS_CONFIGURATION_CONTENT', 'value', ''), createObject('name', 'ApplicationInsightsAgent_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'DiagnosticServices_EXTENSION_VERSION', 'value', '~3'), createObject('name', 'InstrumentationEngine_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'SnapshotDebugger_EXTENSION_VERSION', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_BaseExtensions', 'value', 'disabled'), createObject('name', 'XDT_MicrosoftApplicationInsights_Mode', 'value', 'recommended'), createObject('name', 'XDT_MicrosoftApplicationInsights_PreemptSdk', 'value', 'disabled')), if(equals(parameters('azurePlatform'), 'custom'), createArray(createObject('name', 'CUSTOM_GRAPH_URL_VALUE', 'value', coalesce(parameters('customGraphUrl'), '')), createObject('name', 'CUSTOM_IDENTITY_URL_VALUE', 'value', coalesce(parameters('customIdentityUrl'), '')), createObject('name', 'CUSTOM_RESOURCE_MANAGER_URL_VALUE', 'value', coalesce(parameters('customResourceManagerUrl'), '')), createObject('name', 'CUSTOM_BLOB_STORAGE_URL_VALUE', 'value', coalesce(parameters('customBlobStorageSuffix'), '')), createObject('name', 'CUSTOM_COGNITIVE_SERVICES_URL_VALUE', 'value', coalesce(parameters('customCognitiveServicesScope'), '')), createObject('name', 'CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE', 'value', coalesce(parameters('customSearchResourceUrl'), '')), createObject('name', 'KEY_VAULT_DOMAIN', 'value', environment().suffixes.keyvaultDns), createObject('name', 'CUSTOM_OIDC_METADATA_URL_VALUE', 'value', coalesce(variables('openIdMetadataUrl'), ''))), createArray())))]" + }, + "clientAffinityEnabled": false, + "httpsOnly": true + }, + "identity": { + "type": "SystemAssigned" + }, + "tags": "[union(parameters('tags'), createObject('azd-service-name', 'web'))]", + "dependsOn": [ + "appInsights", + "cosmosDb", + "documentIntelligence" + ] + }, + "webAppLogging": { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))), 'logs')]", + "properties": { + "httpLogs": { + "fileSystem": { + "enabled": true, + "retentionInDays": 7, + "retentionInMb": 35 + } + } + }, + "dependsOn": [ + "webApp" + ] + }, + "webAppDiagnostics": { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference('diagnosticConfigs').outputs.webAppLogCategories.value]", + "metrics": "[reference('diagnosticConfigs').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "diagnosticConfigs", + "webApp" + ] + }, + "authSettings": { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))), 'authsettingsV2')]", + "properties": { + "globalValidation": { + "requireAuthentication": true, + "unauthenticatedClientAction": "RedirectToLoginPage", + "redirectToProvider": "azureActiveDirectory" + }, + "identityProviders": { + "azureActiveDirectory": { + "enabled": true, + "registration": { + "openIdIssuer": "[format('{0}{1}/', environment().authentication.loginEndpoint, tenant().tenantId)]", + "clientId": "[parameters('enterpriseAppClientId')]", + "clientSecretSettingName": "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" + }, + "validation": { + "jwtClaimChecks": {}, + "allowedAudiences": [ + "[format('api://{0}', parameters('enterpriseAppClientId'))]", + "[parameters('enterpriseAppClientId')]" + ] + }, + "isAutoProvisioned": false + } + }, + "login": { + "routes": { + "logoutEndpoint": "/.auth/logout" + }, + "tokenStore": { + "enabled": true, + "tokenRefreshExtensionHours": 72, + "fileSystem": { + "directory": "/home/data/.auth" + } + }, + "preserveUrlFragmentsForLogins": false, + "allowedExternalRedirectUrls": [], + "cookieExpiration": { + "convention": "FixedTime", + "timeToExpiration": "08:00:00" + }, + "nonce": { + "validateNonce": true, + "nonceExpirationInterval": "00:05:00" + } + }, + "httpSettings": { + "requireHttps": true, + "routes": { + "apiPrefix": "/.auth" + }, + "forwardProxy": { + "convention": "NoProxy" + } + } + }, + "dependsOn": [ + "webApp" + ] + }, + "diagnosticConfigs": { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "value": "[toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment')))]" + }, + "defaultHostName": { + "type": "string", + "value": "[reference('webApp').defaultHostName]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Web/sites', toLower(format('{0}-{1}-app', parameters('appName'), parameters('environment'))))]" + } + } + } + }, + "dependsOn": [ + "acr", + "applicationInsights", + "appServicePlan", + "cosmosDB", + "docIntel", + "keyVault", + "logAnalytics", + "openAI", + "rg", + "searchService", + "virtualNetwork" + ] + }, + "contentSafety": { + "condition": "[parameters('deployContentSafety')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "contentSafety", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "7932852522879306913" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "ContentSafety", + "sku": { + "name": "S0" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "customSubDomainName": "[toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment'))))]", + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "contentSafetyName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))]" + }, + "contentSafetyEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-contentsafety', parameters('appName'), parameters('environment')))), '2025-06-01').endpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "redisCache": { + "condition": "[parameters('deployRedisCache')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "redisCache", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "17045519057214581310" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Cache/redis", + "apiVersion": "2024-11-01", + "name": "[toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "Standard", + "family": "C", + "capacity": 0 + }, + "enableNonSslPort": false, + "minimumTlsVersion": "1.2", + "redisConfiguration": { + "aad-enabled": "true" + } + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.Cache/redis', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.Cache/redis', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "redisCacheName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))]" + }, + "redisCacheHostName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Cache/redis', toLower(format('{0}-{1}-redis', parameters('appName'), parameters('environment')))), '2024-11-01').hostName]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "speechService": { + "condition": "[parameters('deploySpeechService')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "speechService", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "15886240553047875398" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2024-10-01", + "name": "[toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "kind": "SpeechServices", + "sku": { + "name": "S0" + }, + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "customSubDomainName": "[toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))]" + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardLogCategories.value]", + "metrics": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.standardMetricsCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "speechServiceName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))]" + }, + "speechServiceEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', toLower(format('{0}-{1}-speech', parameters('appName'), parameters('environment')))), '2024-10-01').endpoint]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "rg" + ] + }, + "videoIndexerService": { + "condition": "[parameters('deployVideoIndexerService')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "videoIndexerService", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "enableDiagLogging": { + "value": "[parameters('enableDiagLogging')]" + }, + "logAnalyticsId": { + "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "storageAccount": { + "value": "[reference('storageAccount').outputs.name.value]" + }, + "openAiServiceName": { + "value": "[reference('openAI').outputs.openAIName.value]" + }, + "enablePrivateNetworking": { + "value": "[parameters('enablePrivateNetworking')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "4707936164725245117" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "enableDiagLogging": { + "type": "bool" + }, + "logAnalyticsId": { + "type": "string" + }, + "storageAccount": { + "type": "string" + }, + "openAiServiceName": { + "type": "string" + }, + "enablePrivateNetworking": { + "type": "bool" + } + }, + "resources": [ + { + "type": "Microsoft.VideoIndexer/accounts", + "apiVersion": "2025-04-01", + "name": "[toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]", + "storageServices": { + "resourceId": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccount'))]" + }, + "openAiServices": { + "resourceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiServiceName'))]" + } + }, + "tags": "[parameters('tags')]" + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.VideoIndexer/accounts', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment'))))]", + "name": "[toLower(format('{0}-diagnostics', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))))]", + "properties": { + "workspaceId": "[parameters('logAnalyticsId')]", + "logs": "[reference(resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs'), '2025-04-01').outputs.limitedLogCategories.value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'diagnosticConfigs')]", + "[resourceId('Microsoft.VideoIndexer/accounts', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "condition": "[parameters('enableDiagLogging')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "diagnosticConfigs", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14011201271499176208" + } + }, + "variables": { + "standardRetentionPolicy": { + "enabled": false, + "days": 0 + }, + "standardLogCategories": [ + { + "categoryGroup": "Audit", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "limitedLogCategories": [ + { + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "standardMetricsCategories": [ + { + "category": "AllMetrics", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "transactionMetricsCategories": [ + { + "category": "Transaction", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ], + "webAppLogCategories": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceConsoleLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAppLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceFileAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceIPSecAuditLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServicePlatformLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + }, + { + "category": "AppServiceAuthenticationLogs", + "enabled": true, + "retentionPolicy": "[variables('standardRetentionPolicy')]" + } + ] + }, + "resources": [], + "outputs": { + "limitedLogCategories": { + "type": "array", + "value": "[variables('limitedLogCategories')]" + }, + "standardRetentionPolicy": { + "type": "object", + "value": "[variables('standardRetentionPolicy')]" + }, + "standardLogCategories": { + "type": "array", + "value": "[variables('standardLogCategories')]" + }, + "standardMetricsCategories": { + "type": "array", + "value": "[variables('standardMetricsCategories')]" + }, + "transactionMetricsCategories": { + "type": "array", + "value": "[variables('transactionMetricsCategories')]" + }, + "webAppLogCategories": { + "type": "array", + "value": "[variables('webAppLogCategories')]" + } + } + } + } + } + ], + "outputs": { + "videoIndexerServiceName": { + "type": "string", + "value": "[toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))]" + }, + "videoIndexerAccountId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.VideoIndexer/accounts', toLower(format('{0}-{1}-video', parameters('appName'), parameters('environment')))), '2025-04-01').accountId]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "openAI", + "rg", + "storageAccount" + ] + }, + "setPermissions": { + "condition": "[parameters('configureApplicationPermissions')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "setPermissions", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "webAppName": { + "value": "[reference('appService').outputs.name.value]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "enterpriseAppServicePrincipalId": { + "value": "[parameters('enterpriseAppServicePrincipalId')]" + }, + "keyVaultName": { + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "cosmosDBName": { + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" + }, + "acrName": { + "value": "[reference('acr').outputs.acrName.value]" + }, + "openAIName": { + "value": "[reference('openAI').outputs.openAIName.value]" + }, + "openAIResourceGroupName": { + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" + }, + "openAISubscriptionId": { + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" + }, + "docIntelName": { + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" + }, + "storageAccountName": { + "value": "[reference('storageAccount').outputs.name.value]" + }, + "searchServiceName": { + "value": "[reference('searchService').outputs.searchServiceName.value]" + }, + "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference('speechService').outputs.speechServiceName.value), createObject('value', ''))]", + "redisCacheName": "[if(parameters('deployRedisCache'), createObject('value', reference('redisCache').outputs.redisCacheName.value), createObject('value', ''))]", + "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference('contentSafety').outputs.contentSafetyName.value), createObject('value', ''))]", + "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference('videoIndexerService').outputs.videoIndexerServiceName.value), createObject('value', ''))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "4113398169754187856" + } + }, + "parameters": { + "webAppName": { + "type": "string" + }, + "authenticationType": { + "type": "string" + }, + "keyVaultName": { + "type": "string" + }, + "enterpriseAppServicePrincipalId": { + "type": "string" + }, + "cosmosDBName": { + "type": "string" + }, + "acrName": { + "type": "string" + }, + "openAIName": { + "type": "string" + }, + "openAIResourceGroupName": { + "type": "string" + }, + "openAISubscriptionId": { + "type": "string" + }, + "docIntelName": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + }, + "speechServiceName": { + "type": "string" + }, + "searchServiceName": { + "type": "string" + }, + "redisCacheName": { + "type": "string" + }, + "contentSafetyName": { + "type": "string" + }, + "videoIndexerName": { + "type": "string" + } + }, + "variables": { + "useExternalOpenAIResource": "[and(and(not(equals(parameters('openAIName'), '')), not(empty(parameters('openAIResourceGroupName')))), not(empty(parameters('openAISubscriptionId'))))]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]", + "name": "[guid(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'kv-secrets-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]", + "name": "[guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'cosmos-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2023-04-15", + "name": "[format('{0}/{1}', parameters('cosmosDBName'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'cosmos-data-contributor'))]", + "properties": { + "roleDefinitionId": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'acr-pull-role')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(and(equals(parameters('authenticationType'), 'managed_identity'), not(equals(parameters('openAIName'), ''))), not(variables('useExternalOpenAIResource')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'openai-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(and(equals(parameters('authenticationType'), 'managed_identity'), not(equals(parameters('openAIName'), ''))), not(variables('useExternalOpenAIResource')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'enterpriseApp-CognitiveServicesOpenAIUserRole')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('enterpriseAppServicePrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'doc-intel-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'storage-blob-data-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('speechServiceName'), '')), equals(parameters('authenticationType'), 'managed_identity'))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('speechServiceName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'speech-service-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]", + "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('searchServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'search-index-data-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]", + "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('searchServiceName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'search-service-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('contentSafetyName'), '')), equals(parameters('authenticationType'), 'managed_identity'))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('contentSafetyName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'content-safety-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[not(equals(parameters('videoIndexerName'), ''))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-storage-blob-data-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalId": "[reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(variables('useExternalOpenAIResource')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-cog-services-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "principalId": "[reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(variables('useExternalOpenAIResource')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), 'video-indexer-cog-services-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[not(equals(parameters('redisCacheName'), ''))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Cache/redis', parameters('redisCacheName'))]", + "name": "[guid(resourceId('Microsoft.Cache/redis', parameters('redisCacheName')), resourceId('Microsoft.Web/sites', parameters('webAppName')), 'redis-cache-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e0f68234-74aa-48ed-b826-c38b57376e17')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[variables('useExternalOpenAIResource')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAIExternalPermissions", + "subscriptionId": "[parameters('openAISubscriptionId')]", + "resourceGroup": "[parameters('openAIResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "openAIName": { + "value": "[parameters('openAIName')]" + }, + "authenticationType": { + "value": "[parameters('authenticationType')]" + }, + "webAppPrincipalId": { + "value": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-03-01', 'full').identity.principalId]" + }, + "enterpriseAppServicePrincipalId": { + "value": "[parameters('enterpriseAppServicePrincipalId')]" + }, + "videoIndexerPrincipalId": "[if(not(equals(parameters('videoIndexerName'), '')), createObject('value', reference(resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName')), '2025-04-01', 'full').identity.principalId), createObject('value', ''))]", + "videoIndexerName": { + "value": "[parameters('videoIndexerName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "14586417486337251686" + } + }, + "parameters": { + "openAIName": { + "type": "string" + }, + "authenticationType": { + "type": "string" + }, + "webAppPrincipalId": { + "type": "string" + }, + "enterpriseAppServicePrincipalId": { + "type": "string" + }, + "videoIndexerPrincipalId": { + "type": "string", + "defaultValue": "" + }, + "videoIndexerName": { + "type": "string", + "defaultValue": "" + } + }, + "resources": [ + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('webAppPrincipalId'), 'openai-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('webAppPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[equals(parameters('authenticationType'), 'managed_identity')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('enterpriseAppServicePrincipalId'), 'enterpriseApp-CognitiveServicesOpenAIUserRole')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('enterpriseAppServicePrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(empty(parameters('videoIndexerPrincipalId'))))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('videoIndexerPrincipalId'), 'video-indexer-cog-services-contributor')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "principalId": "[parameters('videoIndexerPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[and(not(equals(parameters('videoIndexerName'), '')), not(empty(parameters('videoIndexerPrincipalId'))))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('videoIndexerPrincipalId'), 'video-indexer-cog-services-user')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "principalId": "[parameters('videoIndexerPrincipalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + } + } + ] + } + }, + "dependsOn": [ + "acr", + "appService", + "contentSafety", + "cosmosDB", + "docIntel", + "keyVault", + "openAI", + "redisCache", + "rg", + "searchService", + "speechService", + "storageAccount", + "videoIndexerService" + ] + }, + "privateNetworking": { + "condition": "[parameters('enablePrivateNetworking')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "privateNetworking", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualNetworkId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', variables('resolvedExistingVirtualNetworkId')), createObject('value', reference('virtualNetwork').outputs.vNetId.value)), createObject('value', ''))]", + "privateEndpointSubnetId": "[if(parameters('enablePrivateNetworking'), if(variables('useExistingVirtualNetwork'), createObject('value', parameters('existingPrivateEndpointSubnetId')), createObject('value', reference('virtualNetwork').outputs.privateNetworkSubnetId.value)), createObject('value', ''))]", + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneConfigs')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "keyVaultName": { + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "cosmosDBName": { + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" + }, + "acrName": { + "value": "[reference('acr').outputs.acrName.value]" + }, + "searchServiceName": { + "value": "[reference('searchService').outputs.searchServiceName.value]" + }, + "docIntelName": { + "value": "[reference('docIntel').outputs.documentIntelligenceServiceName.value]" + }, + "storageAccountName": { + "value": "[reference('storageAccount').outputs.name.value]" + }, + "openAIName": { + "value": "[reference('openAI').outputs.openAIName.value]" + }, + "openAIResourceGroupName": { + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" + }, + "openAISubscriptionId": { + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" + }, + "webAppName": { + "value": "[reference('appService').outputs.name.value]" + }, + "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference('contentSafety').outputs.contentSafetyName.value), createObject('value', ''))]", + "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference('speechService').outputs.speechServiceName.value), createObject('value', ''))]", + "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference('videoIndexerService').outputs.videoIndexerServiceName.value), createObject('value', ''))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "5473171678692396413" + } + }, + "parameters": { + "virtualNetworkId": { + "type": "string" + }, + "privateEndpointSubnetId": { + "type": "string" + }, + "privateDnsZoneConfigs": { + "type": "object", + "defaultValue": {} + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "keyVaultName": { + "type": "string" + }, + "cosmosDBName": { + "type": "string" + }, + "acrName": { + "type": "string" + }, + "searchServiceName": { + "type": "string" + }, + "docIntelName": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + }, + "openAIName": { + "type": "string" + }, + "openAIResourceGroupName": { + "type": "string" + }, + "openAISubscriptionId": { + "type": "string" + }, + "webAppName": { + "type": "string" + }, + "contentSafetyName": { + "type": "string" + }, + "speechServiceName": { + "type": "string" + }, + "videoIndexerName": { + "type": "string" + } + }, + "variables": { + "$fxv#0": { + "azurecloud": { + "aisearch": "privatelink.search.windows.net", + "blobStorage": "privatelink.blob.core.windows.net", + "cognitiveServices": "privatelink.cognitiveservices.azure.com", + "containerRegistry": "privatelink.azurecr.io", + "cosmosDb": "privatelink.documents.azure.com", + "keyVault": "privatelink.vaultcore.azure.net", + "openAi": "privatelink.openai.azure.com", + "webSites": "privatelink.azurewebsites.net" + }, + "azureusgovernment": { + "aisearch": "privatelink.search.azure.us", + "blobStorage": "privatelink.blob.core.usgovcloudapi.net", + "cognitiveServices": "privatelink.cognitiveservices.azure.us", + "containerRegistry": "privatelink.azurecr.us", + "cosmosDb": "privatelink.documents.azure.us", + "keyVault": "privatelink.vaultcore.azure.us", + "openAi": "privatelink.openai.azure.us", + "webSites": "privatelink.azurewebsites.us" + } + }, + "useExternalOpenAIResource": "[and(and(not(equals(parameters('openAIName'), '')), not(empty(parameters('openAIResourceGroupName')))), not(empty(parameters('openAISubscriptionId'))))]", + "cloudName": "[toLower(environment().name)]", + "privateDnsZoneData": "[variables('$fxv#0')]", + "aiSearchDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].aisearch]", + "blobStorageDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].blobStorage]", + "cognitiveServicesDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].cognitiveServices]", + "containerRegistryDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].containerRegistry]", + "cosmosDbDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].cosmosDb]", + "keyVaultDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].keyVault]", + "openAiDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].openAi]", + "webSitesDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].webSites]", + "defaultPrivateDnsZoneConfig": { + "zoneResourceId": "", + "createVNetLink": true + }, + "keyVaultPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'keyVault'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').keyVault), variables('defaultPrivateDnsZoneConfig'))]", + "cosmosDbPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'cosmosDb'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').cosmosDb), variables('defaultPrivateDnsZoneConfig'))]", + "containerRegistryPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'containerRegistry'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').containerRegistry), variables('defaultPrivateDnsZoneConfig'))]", + "aiSearchPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'aiSearch'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').aiSearch), variables('defaultPrivateDnsZoneConfig'))]", + "blobStoragePrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'blobStorage'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').blobStorage), variables('defaultPrivateDnsZoneConfig'))]", + "cognitiveServicesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'cognitiveServices'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').cognitiveServices), variables('defaultPrivateDnsZoneConfig'))]", + "openAiPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'openAi'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').openAi), variables('defaultPrivateDnsZoneConfig'))]", + "webSitesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'webSites'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').webSites), variables('defaultPrivateDnsZoneConfig'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "keyVaultDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('keyVaultDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "kv" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('keyVaultPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('keyVaultPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "keyVaultPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "kv" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "vault" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'keyVaultDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'keyVaultDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "cosmosDbDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('cosmosDbDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "cosmosDb" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('cosmosDbPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('cosmosDbPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "cosmosDbPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "cosmosDb" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "sql" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'cosmosDbDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'cosmosDbDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "acrDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('containerRegistryDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "acr" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('containerRegistryPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('containerRegistryPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "acrPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "acr" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "registry" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'acrDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'acrDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "searchServiceDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('aiSearchDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "searchService" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('aiSearchPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('aiSearchPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "searchServicePE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "searchService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "searchService" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'searchServiceDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'searchServiceDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "docIntelDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('cognitiveServicesDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "docIntelService" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "docIntelPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "docIntelService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storageAccountDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('blobStorageDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "storage" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('blobStoragePrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('blobStoragePrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "storageAccountPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "storageAccount" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "blob" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('openAIName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAiDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('openAiDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "openAiService" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('openAiPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('openAiPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "condition": "[not(equals(parameters('openAIName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAiPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "openAiService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": "[if(variables('useExternalOpenAIResource'), createObject('value', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('openAISubscriptionId'), parameters('openAIResourceGroupName')), 'Microsoft.CognitiveServices/accounts', parameters('openAIName'))), createObject('value', resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))))]", + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'openAiDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'openAiDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "webAppDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('webSitesDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "webApp" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('webSitesPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('webSitesPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "3501998361364242008" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16255331454976703178" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "webAppPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "webApp" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "sites" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'webAppDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'webAppDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('contentSafetyName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "contentSafetyPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "contentSafety" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('contentSafetyName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('speechServiceName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "speechServicePE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "speechService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('speechServiceName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('videoIndexerName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "videoIndexerPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "videoIndexerService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('Microsoft.VideoIndexer/accounts', parameters('videoIndexerName'))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18226293550364152784" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + ] + } + ] + } + }, + "dependsOn": [ + "acr", + "appService", + "contentSafety", + "cosmosDB", + "docIntel", + "keyVault", + "openAI", + "rg", + "searchService", + "speechService", + "storageAccount", + "videoIndexerService", + "virtualNetwork" + ] + } + }, + "outputs": { + "var_acrName": { + "type": "string", + "value": "[toLower(format('{0}{1}acr', parameters('appName'), parameters('environment')))]" + }, + "var_authenticationType": { + "type": "string", + "value": "[toLower(parameters('authenticationType'))]" + }, + "var_blobStorageEndpoint": { + "type": "string", + "value": "[reference('storageAccount').outputs.endpoint.value]" + }, + "var_configureApplication": { + "type": "bool", + "value": "[parameters('configureApplicationPermissions')]" + }, + "var_contentSafetyEndpoint": { + "type": "string", + "value": "[if(parameters('deployContentSafety'), reference('contentSafety').outputs.contentSafetyEndpoint.value, '')]" + }, + "var_cosmosDb_accountName": { + "type": "string", + "value": "[reference('cosmosDB').outputs.cosmosDbName.value]" + }, + "var_cosmosDb_uri": { + "type": "string", + "value": "[reference('cosmosDB').outputs.cosmosDbUri.value]" + }, + "var_deploymentLocation": { + "type": "string", + "value": "[reference('rg', '2022-09-01', 'full').location]" + }, + "var_documentIntelligenceServiceEndpoint": { + "type": "string", + "value": "[reference('docIntel').outputs.documentIntelligenceServiceEndpoint.value]" + }, + "var_keyVaultName": { + "type": "string", + "value": "[reference('keyVault').outputs.keyVaultName.value]" + }, + "var_keyVaultUri": { + "type": "string", + "value": "[reference('keyVault').outputs.keyVaultUri.value]" + }, + "var_openAIEndpoint": { + "type": "string", + "value": "[reference('openAI').outputs.openAIEndpoint.value]" + }, + "var_openAIGPTModels": { + "type": "array", + "value": "[variables('resolvedGptModels')]" + }, + "var_openAIResourceGroup": { + "type": "string", + "value": "[reference('openAI').outputs.openAIResourceGroup.value]" + }, + "var_openAIEmbeddingModels": { + "type": "array", + "value": "[variables('resolvedEmbeddingModels')]" + }, + "var_openAISubscriptionId": { + "type": "string", + "value": "[reference('openAI').outputs.openAISubscriptionId.value]" + }, + "var_redisCacheHostName": { + "type": "string", + "value": "[if(parameters('deployRedisCache'), reference('redisCache').outputs.redisCacheHostName.value, '')]" + }, + "var_rgName": { + "type": "string", + "value": "[variables('rgName')]" + }, + "var_searchServiceEndpoint": { + "type": "string", + "value": "[reference('searchService').outputs.searchServiceEndpoint.value]" + }, + "var_speechServiceEndpoint": { + "type": "string", + "value": "[if(parameters('deploySpeechService'), reference('speechService').outputs.speechServiceEndpoint.value, '')]" + }, + "var_subscriptionId": { + "type": "string", + "value": "[subscription().subscriptionId]" + }, + "var_videoIndexerAccountId": { + "type": "string", + "value": "[if(parameters('deployVideoIndexerService'), reference('videoIndexerService').outputs.videoIndexerAccountId.value, '')]" + }, + "var_videoIndexerName": { + "type": "string", + "value": "[if(parameters('deployVideoIndexerService'), reference('videoIndexerService').outputs.videoIndexerServiceName.value, '')]" + }, + "var_containerRegistry": { + "type": "string", + "value": "[variables('containerRegistry')]" + }, + "var_imageName": { + "type": "string", + "value": "[if(contains(parameters('imageName'), ':'), split(parameters('imageName'), ':')[0], parameters('imageName'))]" + }, + "var_imageTag": { + "type": "string", + "value": "[if(contains(parameters('imageName'), ':'), split(parameters('imageName'), ':')[1], 'latest')]" + }, + "var_webService": { + "type": "string", + "value": "[reference('appService').outputs.name.value]" + }, + "var_enablePrivateNetworking": { + "type": "bool", + "value": "[parameters('enablePrivateNetworking')]" + } + } +} +2026/03/25 11:27:00 main.go:54: Retry: =====> Try=1 for GET https://graph.microsoft.com/v1.0/me +2026/03/25 11:27:00 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://graph.microsoft.com/v1.0/me + Authorization: REDACTED + User-Agent: azsdk-go-graph/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: REDACTED + +2026/03/25 11:27:00 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/92.7349ms, OpTime=92.7349ms) -- RESPONSE RECEIVED + GET https://graph.microsoft.com/v1.0/me + Authorization: REDACTED + User-Agent: azsdk-go-graph/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: REDACTED + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Client-Request-Id: REDACTED + Content-Type: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Date: Wed, 25 Mar 2026 15:26:59 GMT + Odata-Version: REDACTED + Request-Id: 5ab22ea1-ede5-4086-bddd-2055a09cf554 + Strict-Transport-Security: REDACTED + Vary: REDACTED + X-Cache: REDACTED + X-Ms-Ags-Diagnostic: REDACTED + X-Ms-Resource-Unit: REDACTED + +2026/03/25 11:27:00 main.go:54: Retry: response 200 +2026/03/25 11:27:00 main.go:54: Retry: exit due to non-retriable status code +Reading subscription and location from environment... +2026/03/25 11:27:00 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/locations?api-version=2022-12-01 +2026/03/25 11:27:00 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/locations?api-version=2022-12-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armsubscriptions/v1.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:27:01 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/1.185824s, OpTime=1.1868244s) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/locations?api-version=2022-12-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armsubscriptions/v1.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 47870 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:27:01 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 41fec410-15ce-48ee-b137-00e1f31b8a7b + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:27:01 main.go:54: Retry: response 200 +2026/03/25 11:27:01 main.go:54: Retry: exit due to non-retriable status code +Subscription: TeamOne-AppAlpha-Dev (9698dd71-9367-49c2-bede-fd0deecfad62) +Location: North Central US + +Creating a deployment plan +2026/03/25 11:27:01 main.go:54: Retry: =====> Try=1 for GET https://graph.microsoft.com/v1.0/me +2026/03/25 11:27:01 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://graph.microsoft.com/v1.0/me + Authorization: REDACTED + User-Agent: azsdk-go-graph/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: REDACTED + +2026/03/25 11:27:01 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/90.9621ms, OpTime=90.9621ms) -- RESPONSE RECEIVED + GET https://graph.microsoft.com/v1.0/me + Authorization: REDACTED + User-Agent: azsdk-go-graph/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: REDACTED + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Client-Request-Id: REDACTED + Content-Type: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Date: Wed, 25 Mar 2026 15:27:01 GMT + Odata-Version: REDACTED + Request-Id: 9a51504a-7e5b-4179-a59b-f419816ac21b + Strict-Transport-Security: REDACTED + Vary: REDACTED + X-Cache: REDACTED + X-Ms-Ags-Diagnostic: REDACTED + X-Ms-Resource-Unit: REDACTED + +2026/03/25 11:27:01 main.go:54: Retry: response 200 +2026/03/25 11:27:01 main.go:54: Retry: exit due to non-retriable status code +Comparing deployment state +2026/03/25 11:27:01 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/providers/Microsoft.Resources/deployments/?api-version=2021-04-01 +2026/03/25 11:27:01 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/providers/Microsoft.Resources/deployments/?api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:27:06 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/4.6774449s, OpTime=4.6774449s) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/providers/Microsoft.Resources/deployments/?api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 5368968 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:27:05 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: b7473750-b912-461e-be99-023d72950045 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:27:06 main.go:54: Retry: response 200 +2026/03/25 11:27:06 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:27:06 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/providers/Microsoft.Resources/deployments/?%24skiptoken=REDACTED&api-version=2021-04-01 +2026/03/25 11:27:06 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/providers/Microsoft.Resources/deployments/?%24skiptoken=REDACTED&api-version=2021-04-01 + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:27:08 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/1.2315903s, OpTime=1.2315903s) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/providers/Microsoft.Resources/deployments/?%24skiptoken=REDACTED&api-version=2021-04-01 + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 553183 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:27:07 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: b97d201b-dc4e-48b5-a613-5e0b38ff1c29 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:27:08 main.go:54: Retry: response 200 +2026/03/25 11:27:08 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:27:08 deployment_manager.go:124: completedDeployments: matched deployment 'test-1774451207' using layerName: +2026/03/25 11:27:08 main.go:54: Retry: =====> Try=1 for POST https://management.azure.com/providers/Microsoft.Resources/calculateTemplateHash?api-version=2021-04-01 +2026/03/25 11:27:08 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + POST https://management.azure.com/providers/Microsoft.Resources/calculateTemplateHash?api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + Content-Length: 211849 + Content-Type: application/json + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:27:08 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/194.0207ms, OpTime=194.0207ms) -- RESPONSE RECEIVED + POST https://management.azure.com/providers/Microsoft.Resources/calculateTemplateHash?api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + Content-Length: 211849 + Content-Type: application/json + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 229612 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:27:07 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Tenant-Writes: REDACTED + X-Ms-Request-Id: 06b94a5c-3b18-445f-8b3b-ae20db6eb189 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:27:08 main.go:54: Retry: response 200 +2026/03/25 11:27:08 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:27:08 bicep_provider.go:607: deployment-state: : Previous deployment state is equal to current deployment. Deployment can be skipped. +2026/03/25 11:27:08 main.go:54: Retry: =====> Try=1 for HEAD https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg?api-version=2025-03-01 +2026/03/25 11:27:08 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + HEAD https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg?api-version=2025-03-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:27:08 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/82.8467ms, OpTime=82.8467ms) -- RESPONSE RECEIVED + HEAD https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg?api-version=2025-03-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 204 No Content + Cache-Control: no-cache + Content-Length: 0 + Date: Wed, 25 Mar 2026 15:27:07 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: aa98a7a9-0955-4ee9-9127-aac1078a4cf0 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:27:08 main.go:54: Retry: response 204 +2026/03/25 11:27:08 main.go:54: Retry: exit due to non-retriable status code + (-) Skipped: Didn't find new changes. +2026/03/25 11:27:10 hooks_runner.go:195: Executing script 'C:\Users\PAULLI~1\AppData\Local\Temp\azd-postprovision-83652536.ps1' +2026/03/25 11:28:04 command_runner.go:325: Run exec: 'pwsh C:\Users\PAULLI~1\AppData\Local\Temp\azd-postprovision-83652536.ps1' , exit code: 0 +Additional env: + var_keyVaultName= + AZURE_SUBSCRIPTION_ID= + var_openAIEmbeddingModels= + var_videoIndexerAccountId= + var_deploymentLocation= + AZURE_LOCATION= + var_keyVaultUri= + var_openAIEndpoint= + var_imageName= + var_openAIGPTModels= + var_openAISubscriptionId= + var_cosmosDb_accountName= + var_imageTag= + var_acrName= + var_containerRegistry= + var_contentSafetyEndpoint= + var_webService= + var_openAIResourceGroup= + var_rgName= + var_videoIndexerName= + var_speechServiceEndpoint= + var_cosmosDb_uri= + var_enablePrivateNetworking= + var_configureApplication= + var_searchServiceEndpoint= + var_subscriptionId= + var_blobStorageEndpoint= + var_authenticationType= + var_documentIntelligenceServiceEndpoint= + AZURE_ENV_NAME= + var_redisCacheHostName= +-------------------------------------stdout------------------------------------------- +======================================= +POST-PROVISION: Starting configuration +======================================= + +[1/4] Granting permissions to CosmosDB... +WARNING: Azure CLI requires multi-factor authentication before it can create the Cosmos DB control-plane role assignment. +WARNING: Continuing with Cosmos DB key-based post-deployment configuration. +WARNING: Azure CLI requires multi-factor authentication before it can create the Cosmos DB data-plane role assignment. +WARNING: Continuing with Cosmos DB key-based post-deployment configuration. +Γ£ô CosmosDB permissions granted successfully + +[2/4] Installing Python dependencies... +Γ£ô Dependencies installed successfully +Γ£ô Deployment runner already has CosmosDB data-plane access + +[3/4] Running post-deployment configuration... +Found existing app_setting document +Retrieved Azure OpenAI key +Retrieved Azure AI Search admin key +Retrieved Azure Document Intelligence key +Retrieved Redis cache primary key +Retrieved Content Safety key +Retrieved Speech Service key +Updated item: app_settings with enable_external_healthcheck = True +Γ£ô Post-deployment configuration completed + +[4/4] Restarting web service to apply settings... +Γ£ô Web service restarted successfully + +======================================= +POST-PROVISION: Completed successfully +======================================= +2026/03/25 11:28:04 middleware.go:100: running middleware 'debug' +2026/03/25 11:28:04 middleware.go:100: running middleware 'ux' +2026/03/25 11:28:04 middleware.go:100: running middleware 'telemetry' +2026/03/25 11:28:04 telemetry.go:68: TraceID: dd6b9467838cd44356a50058eb0073ab +2026/03/25 11:28:04 middleware.go:100: running middleware 'error' +2026/03/25 11:28:04 middleware.go:100: running middleware 'loginGuard' +2026/03/25 11:28:04 middleware.go:100: running middleware 'hooks' +2026/03/25 11:28:04 hooks.go:130: service 'web' does not require any command hooks. +2026/03/25 11:28:04 hooks_runner.go:195: Executing script 'C:\Users\PAULLI~1\AppData\Local\Temp\azd-predeploy-2815995305.ps1' +2026/03/25 11:31:43 command_runner.go:325: Run exec: 'pwsh C:\Users\PAULLI~1\AppData\Local\Temp\azd-predeploy-2815995305.ps1' , exit code: 0 +Additional env: + var_configureApplication= + var_containerRegistry= + var_searchServiceEndpoint= + var_cosmosDb_accountName= + var_webService= + var_blobStorageEndpoint= + var_acrName= + var_openAIGPTModels= + var_openAIEmbeddingModels= + var_imageName= + AZURE_ENV_NAME= + var_redisCacheHostName= + var_subscriptionId= + var_authenticationType= + var_speechServiceEndpoint= + var_openAISubscriptionId= + AZURE_SUBSCRIPTION_ID= + var_imageTag= + var_keyVaultName= + var_rgName= + var_cosmosDb_uri= + var_deploymentLocation= + AZURE_LOCATION= + var_openAIEndpoint= + var_enablePrivateNetworking= + var_keyVaultUri= + var_openAIResourceGroup= + var_videoIndexerAccountId= + var_documentIntelligenceServiceEndpoint= + var_videoIndexerName= + var_contentSafetyEndpoint= +-------------------------------------stdout------------------------------------------- +======================================= +PRE-DEPLOY: Building and pushing image +======================================= + +Deployment timestamp: 20260325-112809 +Image: simplechattestacr.azurecr.io/simplechat:20260325-112809 + +[1/4] Stopping web service... +Γ£ô Web service stopped successfully + +[2/4] Building image in ACR Tasks... +Context: C:\repos\simplechat +Dockerfile: application/single_app/Dockerfile +Queued ACR build run ID: cp6 +Γ£ô ACR build run cp6 completed successfully +Γ£ô ACR image build completed successfully + +[3/4] Restarting web service... +Γ£ô Web service restarted successfully + +[4/4] Image build and restart complete + -> Published simplechat:20260325-112809 + -> Published simplechat:latest + +======================================= +PRE-DEPLOY: Completed successfully +======================================= +-------------------------------------stderr------------------------------------------- +WARNING: Packing source code into tar to upload... +WARNING: Excluding '.git' based on default ignore rules +WARNING: Excluding '.gitignore' based on default ignore rules +WARNING: Excluding '.venv' directory for performance +WARNING: Excluding 'application/external_apps/bulkloader/.gitignore' based on default ignore rules +WARNING: Excluding 'application/external_apps/databaseseeder/.gitignore' based on default ignore rules +WARNING: Excluding 'application/single_app/.gitignore' based on default ignore rules +WARNING: Excluding 'application/single_app/uploaded_openapi_files/.gitignore' based on default ignore rules +WARNING: Excluding 'deployers/.azure/.gitignore' based on default ignore rules +WARNING: Excluding 'deployers/terraform/.gitignore' based on default ignore rules +WARNING: Excluding 'docs/.gitignore' based on default ignore rules +WARNING: Uploading archived source code from 'C:\Users\PAULLI~1\AppData\Local\Temp\build_archive_be4ddbe76f214cf7b6a4ddc12e7a4397.tar.gz'... +WARNING: Sending context (37.775 MiB) to registry: simplechattestacr... +WARNING: Queued a build with ID: cp6 +WARNING: Waiting for an agent... +2026/03/25 11:31:43 middleware.go:100: running middleware 'extensions' +2026/03/25 11:31:43 service_manager.go:677: Attempting to resolve language 'python' for service 'web' +2026/03/25 11:31:43 service_manager.go:696: Successfully resolved language 'python' for service 'web' + +Deploying services (azd deploy) + +Deploying service web +2026/03/25 11:31:43 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 +2026/03/25 11:31:43 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:43 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/237.4544ms, OpTime=237.4544ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 355 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:42 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 377f4db1-f2d8-4742-b6ed-678e051e8166 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:31:43 main.go:54: Retry: response 200 +2026/03/25 11:31:43 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:44 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/resources?%24filter=REDACTED&api-version=2021-04-01 +2026/03/25 11:31:44 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/resources?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:44 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/326.1582ms, OpTime=326.1582ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/resources?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 553 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:43 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 1065b58a-e562-4f4f-a59b-6046f8164af4 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:31:44 main.go:54: Retry: response 200 +2026/03/25 11:31:44 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:44 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 +2026/03/25 11:31:44 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:44 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/216.588ms, OpTime=216.588ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 355 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:43 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 2a03bd82-4600-4ccf-9040-287ffc7c44ff + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:31:44 main.go:54: Retry: response 200 +2026/03/25 11:31:44 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:44 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/resources?%24filter=REDACTED&api-version=2021-04-01 +2026/03/25 11:31:44 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/resources?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:44 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/214.1772ms, OpTime=214.1772ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/resources?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 553 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:43 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 5c8a81da-3a20-46f2-a8cd-6a28c8b80237 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:31:44 main.go:54: Retry: response 200 +2026/03/25 11:31:44 main.go:54: Retry: exit due to non-retriable status code +Deploying service web (Checking deployment history) +2026/03/25 11:31:44 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/deployments?api-version=2023-01-01 +2026/03/25 11:31:44 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/deployments?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:45 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/918.6488ms, OpTime=918.6488ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/deployments?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 5502 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:44 GMT + Etag: "8de8a82d6ef09c3" + Expires: -1 + Pragma: no-cache + Set-Cookie: REDACTED + Strict-Transport-Security: REDACTED + Vary: REDACTED + X-Aspnet-Version: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Operation-Identifier: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 305370c3-9ddf-4fd9-8669-5e72240651ec + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + X-Powered-By: REDACTED + +2026/03/25 11:31:45 main.go:54: Retry: response 200 +2026/03/25 11:31:45 main.go:54: Retry: exit due to non-retriable status code +Deploying service web (Checking deployment slots) +2026/03/25 11:31:45 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/slots?api-version=2023-01-01 +2026/03/25 11:31:45 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/slots?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:45 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/183.0457ms, OpTime=183.6845ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/slots?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 12 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:44 GMT + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Aspnet-Version: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Original-Request-Ids: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: caeb3f81-9860-4933-8adf-3ae37a61103d + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + X-Powered-By: REDACTED + +2026/03/25 11:31:45 main.go:54: Retry: response 200 +2026/03/25 11:31:45 main.go:54: Retry: exit due to non-retriable status code +Deploying service web (Uploading deployment package) +2026/03/25 11:31:45 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app?api-version=2023-01-01 +2026/03/25 11:31:45 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:46 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/200.3026ms, OpTime=200.3026ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 9431 + Content-Type: application/json + Date: Wed, 25 Mar 2026 15:31:44 GMT + Etag: 1DCBC6C7B49594B + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Aspnet-Version: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 5c985d08-5e74-4206-b9bb-2b963c7523d2 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + X-Powered-By: REDACTED + +2026/03/25 11:31:46 main.go:54: Retry: response 200 +2026/03/25 11:31:46 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:46 main.go:54: Retry: =====> Try=1 for POST https://simplechat-test-app.scm.azurewebsites.net/api/zipdeploy?isAsync=REDACTED +2026/03/25 11:31:46 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + POST https://simplechat-test-app.scm.azurewebsites.net/api/zipdeploy?isAsync=REDACTED + Accept: application/json + Authorization: REDACTED + Content-Length: 5267162 + Content-Type: application/octet-stream + User-Agent: azsdk-go-zip-deploy/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:47 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/855.1784ms, OpTime=855.1784ms) -- RESPONSE RECEIVED + POST https://simplechat-test-app.scm.azurewebsites.net/api/zipdeploy?isAsync=REDACTED + Accept: application/json + Authorization: REDACTED + Content-Length: 5267162 + Content-Type: application/octet-stream + User-Agent: azsdk-go-zip-deploy/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 202 Accepted + Content-Length: 0 + Date: Wed, 25 Mar 2026 15:31:46 GMT + Location: REDACTED + Retryafter: REDACTED + Scm-Deployment-Id: REDACTED + Server: Kestrel + Set-Cookie: REDACTED + +2026/03/25 11:31:47 main.go:54: Retry: response 202 +2026/03/25 11:31:47 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:47 main.go:54: LongRunningOperation: BEGIN PollUntilDone() for *azsdk.deployPollingHandler +2026/03/25 11:31:47 main.go:54: Retry: =====> Try=1 for GET https://simplechat-test-app.scm.azurewebsites.net:443/api/deployments/latest?deployer=REDACTED&time=REDACTED +2026/03/25 11:31:47 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://simplechat-test-app.scm.azurewebsites.net:443/api/deployments/latest?deployer=REDACTED&time=REDACTED + Authorization: REDACTED + User-Agent: azsdk-go-zip-deploy/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:47 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/738.3633ms, OpTime=738.3633ms) -- RESPONSE RECEIVED + GET https://simplechat-test-app.scm.azurewebsites.net:443/api/deployments/latest?deployer=REDACTED&time=REDACTED + Authorization: REDACTED + User-Agent: azsdk-go-zip-deploy/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 202 Accepted + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:47 GMT + Location: REDACTED + Server: Kestrel + Set-Cookie: REDACTED + Vary: REDACTED + +2026/03/25 11:31:47 main.go:54: Retry: response 202 +2026/03/25 11:31:47 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:47 main.go:54: LongRunningOperation: delay for 10s +2026/03/25 11:31:57 main.go:54: Retry: =====> Try=1 for GET https://simplechat-test-app.scm.azurewebsites.net:443/api/deployments/latest?deployer=REDACTED&time=REDACTED +2026/03/25 11:31:57 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://simplechat-test-app.scm.azurewebsites.net:443/api/deployments/latest?deployer=REDACTED&time=REDACTED + Authorization: REDACTED + User-Agent: azsdk-go-zip-deploy/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:58 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/545.0668ms, OpTime=545.0668ms) -- RESPONSE RECEIVED + GET https://simplechat-test-app.scm.azurewebsites.net:443/api/deployments/latest?deployer=REDACTED&time=REDACTED + Authorization: REDACTED + User-Agent: azsdk-go-zip-deploy/1.0.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:57 GMT + Server: Kestrel + Set-Cookie: REDACTED + Vary: REDACTED + +2026/03/25 11:31:58 main.go:54: Retry: response 200 +2026/03/25 11:31:58 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:58 main.go:54: LongRunningOperation: END PollUntilDone() for *azsdk.deployPollingHandler: succeeded, total time: 11.2853145s +Deploying service web (Fetching endpoints for app service) +2026/03/25 11:31:58 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app?api-version=2023-01-01 +2026/03/25 11:31:58 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:59 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/1.0207185s, OpTime=1.0207185s) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 9431 + Content-Type: application/json + Date: Wed, 25 Mar 2026 15:31:58 GMT + Etag: 1DCBC6C7B49594B + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Aspnet-Version: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 63ac1b17-ddbd-4502-acf5-19ac8946e0a1 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + X-Powered-By: REDACTED + +2026/03/25 11:31:59 main.go:54: Retry: response 200 +2026/03/25 11:31:59 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:59 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/slots?api-version=2023-01-01 +2026/03/25 11:31:59 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/slots?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:59 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/107.9592ms, OpTime=107.9592ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourceGroups/simplechat-test-rg/providers/Microsoft.Web/sites/simplechat-test-app/slots?api-version=2023-01-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armappservice/v2.3.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 12 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:58 GMT + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Aspnet-Version: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Original-Request-Ids: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: 03f223a9-5640-484d-b8ab-56c6e855c962 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + X-Powered-By: REDACTED + +2026/03/25 11:31:59 main.go:54: Retry: response 200 +2026/03/25 11:31:59 main.go:54: Retry: exit due to non-retriable status code + (Γ£ô) Done: Deploying service web + - Endpoint: https://simplechat-test-app.azurewebsites.net/ + +2026/03/25 11:31:59 main.go:54: Retry: =====> Try=1 for GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 +2026/03/25 11:31:59 main.go:54: Request: ==> OUTGOING REQUEST (Try=1) + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + +2026/03/25 11:31:59 main.go:54: Response: ==> REQUEST/RESPONSE (Try=1/199.4013ms, OpTime=199.4013ms) -- RESPONSE RECEIVED + GET https://management.azure.com/subscriptions/9698dd71-9367-49c2-bede-fd0deecfad62/resourcegroups?%24filter=REDACTED&api-version=2021-04-01 + Accept: application/json + Authorization: REDACTED + User-Agent: azsdk-go-armresources/v1.2.0 (go1.26.0-X:loopvar; Windows_NT),azdev/1.23.12 (Go go1.26.0-X:loopvar; windows/amd64) + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + -------------------------------------------------------------------------------- + RESPONSE Status: 200 OK + Cache-Control: no-cache + Content-Length: 355 + Content-Type: application/json; charset=utf-8 + Date: Wed, 25 Mar 2026 15:31:58 GMT + Expires: -1 + Pragma: no-cache + Strict-Transport-Security: REDACTED + X-Cache: REDACTED + X-Content-Type-Options: REDACTED + X-Ms-Correlation-Request-Id: dd6b9467838cd44356a50058eb0073ab + X-Ms-Ratelimit-Remaining-Subscription-Global-Reads: REDACTED + X-Ms-Ratelimit-Remaining-Subscription-Reads: REDACTED + X-Ms-Request-Id: b04b1fc9-1f4f-4122-b727-ca1726135276 + X-Ms-Routing-Request-Id: REDACTED + X-Msedge-Ref: REDACTED + +2026/03/25 11:31:59 main.go:54: Retry: response 200 +2026/03/25 11:31:59 main.go:54: Retry: exit due to non-retriable status code +2026/03/25 11:31:59 hooks_runner.go:195: Executing script 'C:\Users\PAULLI~1\AppData\Local\Temp\azd-postup-2869181376.ps1' +2026/03/25 11:32:05 command_runner.go:325: Run exec: 'pwsh C:\Users\PAULLI~1\AppData\Local\Temp\azd-postup-2869181376.ps1' , exit code: 0 +Additional env: + var_documentIntelligenceServiceEndpoint= + var_speechServiceEndpoint= + var_keyVaultName= + var_enablePrivateNetworking= + var_redisCacheHostName= + var_contentSafetyEndpoint= + var_videoIndexerAccountId= + var_keyVaultUri= + var_acrName= + var_imageTag= + var_cosmosDb_accountName= + var_blobStorageEndpoint= + var_searchServiceEndpoint= + AZURE_ENV_NAME= + var_webService= + var_openAIResourceGroup= + AZURE_LOCATION= + AZURE_SUBSCRIPTION_ID= + var_imageName= + var_videoIndexerName= + var_openAISubscriptionId= + var_subscriptionId= + var_openAIEmbeddingModels= + var_openAIEndpoint= + var_authenticationType= + var_configureApplication= + var_openAIGPTModels= + var_containerRegistry= + var_deploymentLocation= + var_rgName= + var_cosmosDb_uri= +-------------------------------------stdout------------------------------------------- +======================================= +POST-UP: Final configuration +======================================= + +Skipping private networking configuration (var_enablePrivateNetworking is not true) + +======================================= +Γ£ô DEPLOYMENT COMPLETED SUCCESSFULLY +======================================= + +SUCCESS: Your up workflow to provision and deploy to Azure completed in 5 minutes 8 seconds. diff --git a/docs/explanation/fixes/CORE_SERVICE_KEY_DEPLOYMENT_FIX.md b/docs/explanation/fixes/CORE_SERVICE_KEY_DEPLOYMENT_FIX.md new file mode 100644 index 000000000..4bab51dda --- /dev/null +++ b/docs/explanation/fixes/CORE_SERVICE_KEY_DEPLOYMENT_FIX.md @@ -0,0 +1,76 @@ +# Core Service Key Deployment Fix + +Fixed/Implemented in version: **0.240.002** + +## Issue Description + +Fresh Azure deployments using `authenticationType == key` could complete successfully while still leaving core runtime settings incomplete. + +The most visible failure was Redis cache startup, where the application attempted to use key-based Redis authentication but the deployment had not populated `redis_key` into the `app_settings` document. + +## Root Cause Analysis + +The `deployers/bicep/postconfig.py` workflow was reading core service secrets from Key Vault after provisioning. + +That created two problems: + +- the deployer identity running `azd up` needed Key Vault secret-read permissions for all core services +- deployment-time failures to read those secrets were logged as warnings and the script still upserted partial settings into Cosmos DB + +This conflicted with the intended architecture, where core application service keys should be obtained directly from Azure resources during deployment and only Semantic Kernel or plugin secrets should remain Key Vault-backed. + +## Technical Details + +### Files Modified + +- `deployers/bicep/postconfig.py` +- `deployers/bicep/modules/appService.bicep` +- `deployers/bicep/modules/azureContainerRegistry.bicep` +- `deployers/bicep/modules/contentSafety.bicep` +- `deployers/bicep/modules/cosmosDb.bicep` +- `deployers/bicep/modules/documentIntelligence.bicep` +- `deployers/bicep/modules/openAI.bicep` +- `deployers/bicep/modules/redisCache.bicep` +- `deployers/bicep/modules/search.bicep` +- `deployers/bicep/modules/speechService.bicep` +- `application/single_app/config.py` +- `functional_tests/test_core_service_key_deployment_path.py` + +### Code Changes Summary + +- Refactored `postconfig.py` to retrieve core service keys directly from Azure resources through Azure CLI commands instead of Key Vault secret reads. +- Updated App Service key-auth environment wiring to use direct `listKeys()` and `listCredentials()` values for Cosmos DB, Azure AI Search, Document Intelligence, and ACR. +- Removed deployment-time Key Vault secret creation for core service keys from the relevant Bicep modules. +- Added regression coverage to ensure the deployment path stays aligned with the intended core-service-versus-plugin secret boundary. +- Bumped the application version to `0.240.002`. + +### Testing Approach + +- Added `functional_tests/test_core_service_key_deployment_path.py`. +- The regression test verifies that: + - `postconfig.py` uses direct Azure resource key retrieval + - App Service core key-auth settings are direct resource values + - core Bicep modules no longer store deployment-time core secrets in Key Vault + - `config.py` reflects the implementation version + +## Validation + +### Before + +- key-auth deployments depended on Key Vault reads for core service settings +- deployer RBAC failures could leave Redis, Search, OpenAI, Document Intelligence, Speech, or Content Safety keys unset +- App Service still referenced several core key-auth values through Key Vault secrets + +### After + +- key-auth deployments collect core service keys directly from the provisioned Azure resources +- App Service receives direct core key-auth values where runtime environment variables still require them +- only plugin and Semantic Kernel secret flows remain intended Key Vault consumers + +### Impact Analysis + +This fix is intentionally scoped to deployment-time core service configuration. + +- it improves first-run deployment reliability for `azd up` +- it preserves the ability for admins to later choose alternate auth modes in application settings +- it does not change the plugin or Semantic Kernel Key Vault secret model \ No newline at end of file diff --git a/functional_tests/test_core_service_key_deployment_path.py b/functional_tests/test_core_service_key_deployment_path.py new file mode 100644 index 000000000..7644ed260 --- /dev/null +++ b/functional_tests/test_core_service_key_deployment_path.py @@ -0,0 +1,168 @@ +# test_core_service_key_deployment_path.py +#!/usr/bin/env python3 +""" +Functional test for core service key deployment path. +Version: 0.240.002 +Implemented in: 0.240.002 + +This test ensures that core service key authentication is populated directly from +Azure resources during deployment instead of depending on Key Vault secret reads. +""" + +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +POSTCONFIG_PATH = REPO_ROOT / "deployers" / "bicep" / "postconfig.py" +APP_SERVICE_BICEP_PATH = REPO_ROOT / "deployers" / "bicep" / "modules" / "appService.bicep" +CONFIG_PATH = REPO_ROOT / "application" / "single_app" / "config.py" +CORE_MODULE_PATHS = [ + REPO_ROOT / "deployers" / "bicep" / "modules" / "azureContainerRegistry.bicep", + REPO_ROOT / "deployers" / "bicep" / "modules" / "contentSafety.bicep", + REPO_ROOT / "deployers" / "bicep" / "modules" / "cosmosDb.bicep", + REPO_ROOT / "deployers" / "bicep" / "modules" / "documentIntelligence.bicep", + REPO_ROOT / "deployers" / "bicep" / "modules" / "openAI.bicep", + REPO_ROOT / "deployers" / "bicep" / "modules" / "redisCache.bicep", + REPO_ROOT / "deployers" / "bicep" / "modules" / "search.bicep", + REPO_ROOT / "deployers" / "bicep" / "modules" / "speechService.bicep", +] + + +def read_text(path): + return path.read_text(encoding="utf-8") + + +def test_postconfig_uses_direct_core_key_retrieval(): + """Verify postconfig retrieves core keys from Azure resources instead of Key Vault.""" + print("🔍 Testing postconfig core key retrieval path...") + + content = read_text(POSTCONFIG_PATH) + + required_snippets = [ + "get_azure_cli_executable()", + 'candidate_names = ["az.cmd", "az.exe", "az"]', + "cognitiveservices", + "search", + "admin-key", + "redis", + "list-keys", + "get_core_service_keys(", + ] + forbidden_snippets = [ + 'get_secret("openAi-key")', + 'get_secret("content-safety-key")', + 'get_secret("redis-cache-key")', + 'get_secret("search-service-key")', + 'get_secret("document-intelligence-key")', + 'get_secret("speech-service-key")', + "SecretClient", + ] + + for snippet in required_snippets: + if snippet not in content: + print(f"❌ Missing expected postconfig snippet: {snippet}") + return False + + for snippet in forbidden_snippets: + if snippet in content: + print(f"❌ Found forbidden Key Vault dependency in postconfig: {snippet}") + return False + + print("✅ Postconfig retrieves core keys directly from Azure resources") + return True + + +def test_app_service_uses_direct_core_key_values(): + """Verify App Service no longer depends on Key Vault references for core key auth.""" + print("🔍 Testing App Service core key wiring...") + + content = read_text(APP_SERVICE_BICEP_PATH) + + required_snippets = [ + "AZURE_COSMOS_KEY', value: cosmosDb.listKeys().primaryMasterKey", + "DOCKER_REGISTRY_SERVER_PASSWORD", + "value: acrService.listCredentials().passwords[0].value", + "AZURE_SEARCH_API_KEY", + "value: searchService.listAdminKeys().primaryKey", + "AZURE_DOCUMENT_INTELLIGENCE_API_KEY", + "value: documentIntelligence.listKeys().key1", + ] + forbidden_snippets = [ + "secrets/cosmos-db-key", + "secrets/container-registry-key", + "secrets/search-service-key", + "secrets/document-intelligence-key", + ] + + for snippet in required_snippets: + if snippet not in content: + print(f"❌ Missing expected App Service wiring snippet: {snippet}") + return False + + for snippet in forbidden_snippets: + if snippet in content: + print(f"❌ Found forbidden Key Vault App Service reference: {snippet}") + return False + + print("✅ App Service uses direct core resource values for key auth") + return True + + +def test_core_modules_stop_storing_core_keys_in_key_vault(): + """Verify core service modules no longer write deployment-time core secrets to Key Vault.""" + print("🔍 Testing core module Key Vault secret removal...") + + forbidden_secret_names = [ + "container-registry-key", + "content-safety-key", + "cosmos-db-key", + "document-intelligence-key", + "openAi-key", + "redis-cache-key", + "search-service-key", + "speech-service-key", + ] + + for module_path in CORE_MODULE_PATHS: + content = read_text(module_path) + for secret_name in forbidden_secret_names: + if secret_name in content: + print(f"❌ Found forbidden core secret storage in {module_path.name}: {secret_name}") + return False + + print("✅ Core service modules no longer store deployment-time core keys in Key Vault") + return True + + +def test_version_updated(): + """Verify the config version matches this deployment fix.""" + print("🔍 Testing config version update...") + + content = read_text(CONFIG_PATH) + expected_version = 'VERSION = "0.240.002"' + + if expected_version not in content: + print(f"❌ Expected version not found in config.py: {expected_version}") + return False + + print("✅ Config version updated for deployment fix") + return True + + +if __name__ == "__main__": + tests = [ + test_postconfig_uses_direct_core_key_retrieval, + test_app_service_uses_direct_core_key_values, + test_core_modules_stop_storing_core_keys_in_key_vault, + test_version_updated, + ] + results = [] + + for test in tests: + print(f"\n🧪 Running {test.__name__}...") + results.append(test()) + + success = all(results) + print(f"\n📊 Results: {sum(results)}/{len(results)} tests passed") + sys.exit(0 if success else 1) \ No newline at end of file From b2a589e1ec5b50ef78bafe81d5028552706ffa95 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 25 Mar 2026 21:46:33 -0400 Subject: [PATCH 4/7] fixed enhanced citation --- application/single_app/config.py | 2 +- deployers/bicep/postconfig.py | 30 ++++++++ ...CITATIONS_STORAGE_CONNECTION_STRING_FIX.md | 59 ++++++++++++++ .../test_core_service_key_deployment_path.py | 4 +- ...hanced_citations_storage_deployment_fix.py | 76 +++++++++++++++++++ 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 docs/explanation/fixes/ENHANCED_CITATIONS_STORAGE_CONNECTION_STRING_FIX.md create mode 100644 functional_tests/test_enhanced_citations_storage_deployment_fix.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 7d1df5a31..8bdcc4b00 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.240.002" +VERSION = "0.240.003" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index 7289c5d3a..cbf94b002 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -133,6 +133,27 @@ def get_redis_cache_key(resource_name, resource_group, subscription_id): ) +def get_storage_account_connection_string(resource_name, resource_group, subscription_id): + return run_azure_cli_command( + [ + "storage", + "account", + "show-connection-string", + "--name", + resource_name, + "--resource-group", + resource_group, + "--subscription", + subscription_id, + "--query", + "connectionString", + "-o", + "tsv", + ], + "Storage account connection string", + ) + + def get_core_service_keys( authentication_type, openai_endpoint, @@ -337,6 +358,15 @@ def get_core_service_keys( item["enable_enhanced_citations"] = True item["office_docs_authentication_type"] = var_authenticationType item["office_docs_storage_account_blob_endpoint"] = var_blobStorageEndpoint +if var_authenticationType == "key" and var_blobStorageEndpoint and var_blobStorageEndpoint.strip(): + storage_account_name = extract_resource_name_from_endpoint(var_blobStorageEndpoint) + item["office_docs_storage_account_url"] = get_storage_account_connection_string( + storage_account_name, + var_rgName, + var_subscriptionId, + ) +else: + item["office_docs_storage_account_url"] = "" # Safety > Content Safety if var_contentSafetyEndpoint and var_contentSafetyEndpoint.strip(): diff --git a/docs/explanation/fixes/ENHANCED_CITATIONS_STORAGE_CONNECTION_STRING_FIX.md b/docs/explanation/fixes/ENHANCED_CITATIONS_STORAGE_CONNECTION_STRING_FIX.md new file mode 100644 index 000000000..1dc0b890e --- /dev/null +++ b/docs/explanation/fixes/ENHANCED_CITATIONS_STORAGE_CONNECTION_STRING_FIX.md @@ -0,0 +1,59 @@ +# Enhanced Citations Storage Connection String Fix + +Fixed/Implemented in version: **0.240.003** + +## Issue Description + +`azd` deployments could provision the Azure Storage account used for Enhanced Citations, enable the feature in application settings, and still leave the storage connection string blank. + +That produced an incomplete admin configuration for key-based storage authentication, leaving Enhanced Citations enabled but not fully usable until an administrator manually added the connection string. + +## Root Cause Analysis + +The `deployers/bicep/postconfig.py` script enabled Enhanced Citations and stored the blob endpoint, but it never retrieved and persisted the storage account connection string required by the application when `office_docs_authentication_type` is `key`. + +As a result, the deployment pipeline populated only part of the required storage configuration. + +## Technical Details + +### Files Modified + +- `deployers/bicep/postconfig.py` +- `application/single_app/config.py` +- `functional_tests/test_core_service_key_deployment_path.py` +- `functional_tests/test_enhanced_citations_storage_deployment_fix.py` + +### Code Changes Summary + +- Added Azure CLI retrieval of the storage account connection string during post-provision configuration. +- Persisted `office_docs_storage_account_url` into the Cosmos DB `app_settings` document when Enhanced Citations uses key authentication. +- Cleared the stored connection string when deployment config uses managed identity to avoid stale key-auth values. +- Added regression coverage for the new storage deployment path. +- Bumped the application version to `0.240.003`. + +### Testing Approach + +- Added `functional_tests/test_enhanced_citations_storage_deployment_fix.py`. +- Updated `functional_tests/test_core_service_key_deployment_path.py` to align with the current version. + +## Validation + +### Before + +- `azd` post-provision enabled Enhanced Citations. +- the storage blob endpoint was written to app settings. +- the storage connection string remained blank for key-auth deployments. + +### After + +- `azd` post-provision still enables Enhanced Citations. +- the blob endpoint is preserved. +- the storage connection string is now written automatically for key-auth deployments, so the feature has the settings it needs immediately after provisioning. + +### Impact Analysis + +This fix is scoped to deployment-time population of Enhanced Citations storage settings. + +- it improves first-run reliability for `azd up` and `azd provision` +- it removes the need for manual admin repair of the storage connection string after provisioning +- it preserves the managed identity path by keeping the connection-string field empty when key authentication is not used \ No newline at end of file diff --git a/functional_tests/test_core_service_key_deployment_path.py b/functional_tests/test_core_service_key_deployment_path.py index 7644ed260..c18ede480 100644 --- a/functional_tests/test_core_service_key_deployment_path.py +++ b/functional_tests/test_core_service_key_deployment_path.py @@ -2,7 +2,7 @@ #!/usr/bin/env python3 """ Functional test for core service key deployment path. -Version: 0.240.002 +Version: 0.240.003 Implemented in: 0.240.002 This test ensures that core service key authentication is populated directly from @@ -140,7 +140,7 @@ def test_version_updated(): print("🔍 Testing config version update...") content = read_text(CONFIG_PATH) - expected_version = 'VERSION = "0.240.002"' + expected_version = 'VERSION = "0.240.003"' if expected_version not in content: print(f"❌ Expected version not found in config.py: {expected_version}") diff --git a/functional_tests/test_enhanced_citations_storage_deployment_fix.py b/functional_tests/test_enhanced_citations_storage_deployment_fix.py new file mode 100644 index 000000000..b206f3389 --- /dev/null +++ b/functional_tests/test_enhanced_citations_storage_deployment_fix.py @@ -0,0 +1,76 @@ +# test_enhanced_citations_storage_deployment_fix.py +#!/usr/bin/env python3 +""" +Functional test for enhanced citations storage deployment fix. +Version: 0.240.003 +Implemented in: 0.240.003 + +This test ensures the azd post-provision configuration persists the storage +account connection string required for enhanced citations key authentication. +""" + +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +POSTCONFIG_PATH = REPO_ROOT / "deployers" / "bicep" / "postconfig.py" +CONFIG_PATH = REPO_ROOT / "application" / "single_app" / "config.py" + + +def read_text(path): + return path.read_text(encoding="utf-8") + + +def test_postconfig_populates_storage_connection_string(): + """Verify postconfig writes the enhanced citations storage connection string.""" + print("🔍 Testing enhanced citations storage connection string deployment path...") + + content = read_text(POSTCONFIG_PATH) + + required_snippets = [ + "def get_storage_account_connection_string(resource_name, resource_group, subscription_id):", + '"storage",', + '"show-connection-string",', + 'item["office_docs_storage_account_url"] = get_storage_account_connection_string(', + 'storage_account_name = extract_resource_name_from_endpoint(var_blobStorageEndpoint)', + ] + + for snippet in required_snippets: + if snippet not in content: + print(f"❌ Missing expected postconfig snippet: {snippet}") + return False + + print("✅ Postconfig retrieves and stores the enhanced citations storage connection string") + return True + + +def test_version_updated(): + """Verify the config version matches this enhanced citations deployment fix.""" + print("🔍 Testing config version update...") + + content = read_text(CONFIG_PATH) + expected_version = 'VERSION = "0.240.003"' + + if expected_version not in content: + print(f"❌ Expected version not found in config.py: {expected_version}") + return False + + print("✅ Config version updated for enhanced citations deployment fix") + return True + + +if __name__ == "__main__": + tests = [ + test_postconfig_populates_storage_connection_string, + test_version_updated, + ] + results = [] + + for test in tests: + print(f"\n🧪 Running {test.__name__}...") + results.append(test()) + + success = all(results) + print(f"\n📊 Results: {sum(results)}/{len(results)} tests passed") + sys.exit(0 if success else 1) \ No newline at end of file From e8d73c8afa5429af51ffabdb803f784ffeb6c40f Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Fri, 27 Mar 2026 11:20:55 -0400 Subject: [PATCH 5/7] improved video indexer support for gov and customer uses the correct api version and removed openai support for gov --- application/single_app/config.py | 2 +- application/single_app/functions_settings.py | 7 +- deployers/azure.yaml | 2 + deployers/azurecli/deploy-simplechat.ps1 | 157 ++++++++++++++-- deployers/bicep/README.md | 12 +- deployers/bicep/main.bicep | 24 +++ deployers/bicep/main.parameters.json | 6 + deployers/bicep/modules/appService.bicep | 4 + .../bicep/modules/privateNetworking.bicep | 5 +- .../setPermissions-openAIExternal.bicep | 5 +- deployers/bicep/modules/setPermissions.bicep | 8 +- deployers/bicep/modules/videoIndexer.bicep | 46 ++++- deployers/bicep/postconfig.py | 2 + deployers/terraform/ReadMe.md | 7 + deployers/terraform/main.tf | 176 ++++++++++++++++-- deployers/terraform/private_networking.tf | 8 + 16 files changed, 417 insertions(+), 54 deletions(-) diff --git a/application/single_app/config.py b/application/single_app/config.py index 8bdcc4b00..13806428c 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.240.003" +VERSION = "0.240.004" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py index a282f4090..e3279955a 100644 --- a/application/single_app/functions_settings.py +++ b/application/single_app/functions_settings.py @@ -5,6 +5,11 @@ import app_settings_cache import inspect +DEFAULT_VIDEO_INDEXER_ARM_API_VERSION = os.getenv( + 'VIDEO_INDEXER_ARM_API_VERSION', + '2024-01-01' if AZURE_ENVIRONMENT == 'usgovernment' else '2025-04-01' +) + def get_settings(use_cosmos=False): import secrets default_settings = { @@ -288,7 +293,7 @@ def get_settings(use_cosmos=False): 'video_indexer_resource_group': '', 'video_indexer_subscription_id': '', 'video_indexer_account_name': '', - 'video_indexer_arm_api_version': '2024-01-01', + 'video_indexer_arm_api_version': DEFAULT_VIDEO_INDEXER_ARM_API_VERSION, 'video_index_timeout': 600, # Audio file settings with Azure speech service diff --git a/deployers/azure.yaml b/deployers/azure.yaml index db399c41d..c96b949ec 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -66,6 +66,8 @@ hooks: export var_searchServiceEndpoint=${var_searchServiceEndpoint} export var_documentIntelligenceServiceEndpoint=${var_documentIntelligenceServiceEndpoint} export var_redisCacheHostName=${var_redisCacheHostName} + export var_videoIndexerArmApiVersion=${var_videoIndexerArmApiVersion} + export var_videoIndexerEndpoint=${var_videoIndexerEndpoint} export var_videoIndexerName=${var_videoIndexerName} export var_deploymentLocation=${var_deploymentLocation} export var_videoIndexerAccountId=${var_videoIndexerAccountId} diff --git a/deployers/azurecli/deploy-simplechat.ps1 b/deployers/azurecli/deploy-simplechat.ps1 index ca91e3dfd..3a0c9eced 100644 --- a/deployers/azurecli/deploy-simplechat.ps1 +++ b/deployers/azurecli/deploy-simplechat.ps1 @@ -174,7 +174,8 @@ $ErrorActionPreference = "Continue" #--------------------------------------------------------------------------------------------- # Configuration Variables - MODIFY THESE VALUES AS NEEDED #--------------------------------------------------------------------------------------------- -$globalWhichAzurePlatform = "AzureCloud" # Set to "AzureUSGovernment" for Azure Government, "AzureCloud" for Azure Commercial +$globalWhichAzurePlatform = "AzureCloud" # Set to "AzureUSGovernment" for Azure Government, "AzureCloud" for Azure Commercial, or "Custom" for a registered customer cloud +$param_AzureCliCustomCloudName = "" # Required when globalWhichAzurePlatform is Custom. Must match an az cloud show/list name. $paramTenantId = "" # Tenant ID $paramLocation = "EastUS2" # Primary Azure region for deployments (e.g., eastus, eastus2, usgovvirginia, usgovarizona, usgovtexas) $paramResourceOwnerId = "" # used for tagging resources @@ -201,6 +202,14 @@ $param_AzureOpenAiEmbeddingDeploymentName = "" $param_AzureOpenAiEmbeddingModelName = "" $param_AzureOpenAiEmbeddingModelVersion = "" # Leave blank to use the default for the selected cloud and region. $param_AzureOpenAiEmbeddingDeploymentCapacity = 80 +$param_DeployVideoIndexerService = $false +$param_CustomGraphUrl = "" +$param_CustomIdentityUrl = "" +$param_CustomResourceManagerUrl = "" +$param_CustomCognitiveServicesScope = "" +$param_CustomSearchResourceUrl = "" +$param_CustomVideoIndexerEndpoint = "" +$param_CustomVideoIndexerArmApiVersion = "" $paramCreateEntraSecurityGroups = $true # Set to true to create Entra ID security groups $param_Existing_ResourceGroupName = "RG-SimpleChat-Experiemental" # Leave empty if not using an existing resource group name, one will be dynamically generated @@ -251,6 +260,8 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") { $param_AzureOpenAi_Url = "https://{0}.openai.azure.com/" $param_AiSearch_Url = "https://{0}.search.windows.net" $param_AppService_Environment = "public" + $param_VideoIndexerEndpoint = "https://api.videoindexer.ai" + $param_VideoIndexerArmApiVersion = "2025-04-01" $script:PrivateDnsZoneNames = @{ aiSearch = "privatelink.search.windows.net" blobStorage = "privatelink.blob.core.windows.net" @@ -269,10 +280,12 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") { $APPREG_REDIRECT_URI1 = "https://{0}.azurewebsites.us/getAToken" $APPREG_LOGOUT_URL = "https://{0}.azurewebsites.us/logout" $paramGraphUrl = "https://graph.microsoft.us" - $paramArmUrl = "https://management.core.usgovcloudapi.net/" + $paramArmUrl = "https://management.usgovcloudapi.net/" $param_AzureOpenAi_Url = "https://{0}.openai.azure.us/" $param_AiSearch_Url = "https://{0}.search.azure.us" $param_AppService_Environment = "usgovernment" + $param_VideoIndexerEndpoint = "https://api.videoindexer.ai.azure.us" + $param_VideoIndexerArmApiVersion = "2024-01-01" $script:PrivateDnsZoneNames = @{ aiSearch = "privatelink.search.azure.us" blobStorage = "privatelink.blob.core.usgovcloudapi.net" @@ -283,18 +296,32 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") { openAi = "privatelink.openai.azure.us" webSites = "privatelink.azurewebsites.us" } -} elseif ($globalWhichAzurePlatform -eq "AzureSecret") { - # SET THESE VALUES FOR il6 (information is tented) - $paramCosmosDbUrlTemplate = "" - $ACR_BASE_URL = "" - $paramRegistryServer = "" - $APPREG_REDIRECT_URI = "" - $APPREG_REDIRECT_URI1 = "" - $APPREG_LOGOUT_URL = "" - $paramGraphUrl = "" - $param_AzureOpenAi_Url = "" +} elseif ($globalWhichAzurePlatform -eq "Custom") { + $paramCosmosDbUrlTemplate = "https://{0}.documents.azure.com:443/" + $ACR_BASE_URL = "$ACR_NAME.azurecr.io" + $paramRegistryServer = "https://$ACR_NAME.azurecr.io" + $APPREG_REDIRECT_URI = "https://{0}.azurewebsites.net/.auth/login/aad/callback" + $APPREG_REDIRECT_URI1 = "https://{0}.azurewebsites.net/getAToken" + $APPREG_LOGOUT_URL = "https://{0}.azurewebsites.net/logout" + $paramGraphUrl = $param_CustomGraphUrl + $paramArmUrl = $param_CustomResourceManagerUrl + $param_AzureOpenAi_Url = "https://{0}.openai.azure.com/" + $param_AiSearch_Url = "https://{0}.search.windows.net" + $param_AppService_Environment = "custom" + $param_VideoIndexerEndpoint = if ([string]::IsNullOrWhiteSpace($param_CustomVideoIndexerEndpoint)) { "https://api.videoindexer.ai" } else { $param_CustomVideoIndexerEndpoint } + $param_VideoIndexerArmApiVersion = if ([string]::IsNullOrWhiteSpace($param_CustomVideoIndexerArmApiVersion)) { "2025-04-01" } else { $param_CustomVideoIndexerArmApiVersion } + $script:PrivateDnsZoneNames = @{ + aiSearch = "privatelink.search.windows.net" + blobStorage = "privatelink.blob.core.windows.net" + cognitiveServices = "privatelink.cognitiveservices.azure.com" + containerRegistry = "privatelink.azurecr.io" + cosmosDb = "privatelink.documents.azure.com" + keyVault = "privatelink.vaultcore.azure.net" + openAi = "privatelink.openai.azure.com" + webSites = "privatelink.azurewebsites.net" + } } else { - Write-Error "Invalid Azure platform specified. Please set to 'AzureUSGovernment', 'AzureCloud', or 'AzureSecret" + Write-Error "Invalid Azure platform specified. Please set to 'AzureUSGovernment', 'AzureCloud', or 'Custom'" exit 1 } @@ -373,6 +400,7 @@ $paramKeyVaultSuffix = "kv" # Note: Key Vault names need to be globally unique $paramLogAnalyticsSuffix = "la" $paramManagedIdentitySuffix = "id" $paramSearchServiceSuffix = "search" # Note: Search service names need to be globally unique +$paramVideoIndexerSuffix = "video" $paramStorageAccountSuffix = "sa" # Note: Storage account names need to be globally unique, lowercase alphanumeric, 3-24 chars #$paramContainerRegistrySuffix = "acr" # Note: ACR names need to be globally unique, lowercase alphanumeric @@ -444,11 +472,12 @@ function Ensure-AzureCliAuthenticated { $expectedCloudName = switch ($globalWhichAzurePlatform) { 'AzureCloud' { 'AzureCloud' } 'AzureUSGovernment' { 'AzureUSGovernment' } + 'Custom' { $param_AzureCliCustomCloudName } default { $null } } if (-not $expectedCloudName) { - Write-Error "Automatic Azure CLI login is only supported for AzureCloud and AzureUSGovernment. Current platform: $globalWhichAzurePlatform" + Write-Error "Automatic Azure CLI login requires AzureCloud, AzureUSGovernment, or a registered custom cloud name via param_AzureCliCustomCloudName. Current platform: $globalWhichAzurePlatform" exit 1 } @@ -1222,6 +1251,7 @@ $keyVaultName = Get-ResourceName -ResourceTypeSuffix $paramKeyVaultSuffix $logAnalyticsName = Get-ResourceName -ResourceTypeSuffix $paramLogAnalyticsSuffix $managedIdentityName = Get-ResourceName -ResourceTypeSuffix $paramManagedIdentitySuffix $searchServiceName = Get-ResourceName -ResourceTypeSuffix $paramSearchServiceSuffix +$videoIndexerName = Get-ResourceName -ResourceTypeSuffix $paramVideoIndexerSuffix $storageAccountName = Get-GloballyUniqueResourceName -ResourceTypeSuffix $paramStorageAccountSuffix # Storage names are strict (lowercase, no hyphens, 3-24 chars) if ($storageAccountName.Length -gt 24) { $storageAccountName = $storageAccountName.Substring(0, 24) } if ($storageAccountName.Length -lt 3) { Write-Error "Generated storage account name '$storageAccountName' is too short. Adjust base name or suffix." ; exit 1 } @@ -1778,6 +1808,57 @@ if (-not $searchService) { $searchServiceUrl = $param_AiSearch_Url -f $searchServiceName +# --- Create Azure Video Indexer Service --- +if ($param_DeployVideoIndexerService) { + Write-Host "`n=====> Creating Azure Video Indexer Service: $($videoIndexerName)..." + $videoIndexerSubscriptionId = az account show --query id --output tsv + $videoIndexerArmBaseUrl = $paramArmUrl.TrimEnd('/') + $videoIndexerUri = "$videoIndexerArmBaseUrl/subscriptions/$videoIndexerSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.VideoIndexer/accounts/$videoIndexerName?api-version=$param_VideoIndexerArmApiVersion" + $existingVideoIndexer = az rest --method get --uri $videoIndexerUri 2>$null | ConvertFrom-Json + + if (-not $existingVideoIndexer) { + $videoIndexerProperties = @{ + storageServices = @{ + resourceId = $(az storage account show --name $storageAccountName --resource-group $resourceGroupName --query id --output tsv) + } + } + + if ($param_VideoIndexerArmApiVersion -eq '2025-04-01') { + $openAiVideoIndexerResourceId = if ($param_UseExisting_OpenAi_Instance -eq $true) { + az resource show --name $param_Existing_AzureOpenAi_ResourceName --resource-group $param_Existing_AzureOpenAi_ResourceGroupName --resource-type 'Microsoft.CognitiveServices/accounts' --subscription $resolvedExistingOpenAiSubscriptionId --query "id" --output tsv + } else { + az cognitiveservices account show --name $openAIName --resource-group $resourceGroupName --query "id" --output tsv + } + + $videoIndexerProperties.publicNetworkAccess = if ($param_EnablePrivateNetworking) { 'Disabled' } else { 'Enabled' } + if (-not [string]::IsNullOrWhiteSpace($openAiVideoIndexerResourceId)) { + $videoIndexerProperties.openAiServices = @{ + resourceId = $openAiVideoIndexerResourceId + } + } + } + + $videoIndexerBody = @{ + location = $paramLocation + identity = @{ + type = 'SystemAssigned' + } + properties = $videoIndexerProperties + tags = $tags + } | ConvertTo-Json -Depth 10 -Compress + + az rest --method put --uri $videoIndexerUri --headers 'Content-Type=application/json' --body $videoIndexerBody | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to create Azure Video Indexer Service '$($videoIndexerName)'." + } else { + Write-Host "Azure Video Indexer Service '$($videoIndexerName)' created successfully." + $existingVideoIndexer = az rest --method get --uri $videoIndexerUri 2>$null | ConvertFrom-Json + } + } else { + Write-Host "Azure Video Indexer Service '$videoIndexerName' already exists." + } +} + # --- Create App Service Settings --- Write-Host "`n=====> Setting Azure App Service App Settings : $($appServiceName)..." $paramCosmosDbPrimaryKey = $(az cosmosdb keys list --name $cosmosDbName --resource-group $resourceGroupName --query primaryMasterKey --output tsv) @@ -1804,6 +1885,26 @@ az webapp config appsettings set --resource-group $resourceGroupName --name $app if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to update Azure App Service App Settings." } else { Write-Host "Azure App Service App Settings configured." } +$additionalAppSettings = @( + "VIDEO_INDEXER_ARM_API_VERSION=$param_VideoIndexerArmApiVersion" +) + +if ($param_AppService_Environment -eq 'custom') { + $additionalAppSettings += @( + "CUSTOM_GRAPH_URL_VALUE=$param_CustomGraphUrl", + "CUSTOM_IDENTITY_URL_VALUE=$param_CustomIdentityUrl", + "CUSTOM_RESOURCE_MANAGER_URL_VALUE=$param_CustomResourceManagerUrl", + "CUSTOM_COGNITIVE_SERVICES_URL_VALUE=$param_CustomCognitiveServicesScope", + "CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE=$param_CustomSearchResourceUrl", + "CUSTOM_VIDEO_INDEXER_ENDPOINT=$param_VideoIndexerEndpoint" + ) +} + +az webapp config appsettings set --resource-group $resourceGroupName --name $appServiceName --settings $additionalAppSettings | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to update additional Video Indexer or custom cloud app settings." +} + $openAiResourceIdForNetworking = "" if ($param_UseExisting_OpenAi_Instance -eq $false) { $openAiResourceIdForNetworking = az cognitiveservices account show --name $openAIName --resource-group $resourceGroupName --query "id" --output tsv 2>$null @@ -1987,6 +2088,34 @@ if (-not $assignment) { Write-Host "RBAC assignment already exists." } +if ($param_DeployVideoIndexerService) { + Write-Host "`nGetting Azure Video Indexer principal" + $videoIndexerResourceId = az resource show --name $videoIndexerName --resource-group $resourceGroupName --resource-type 'Microsoft.VideoIndexer/accounts' --query "id" --output tsv 2>$null + $videoIndexerPrincipalId = az resource show --ids $videoIndexerResourceId --api-version $param_VideoIndexerArmApiVersion --query "identity.principalId" --output tsv 2>$null + + if (-not [string]::IsNullOrWhiteSpace($videoIndexerPrincipalId)) { + $storageResourceId = az storage account show --name $storageAccountName --resource-group $resourceGroupName --query "id" --output tsv + $assignment = az role assignment list --assignee $videoIndexerPrincipalId --scope "$storageResourceId" --query "[?roleDefinitionName=='Storage Blob Data Contributor']" --output json | ConvertFrom-Json + if (-not $assignment) { + az role assignment create --assignee $videoIndexerPrincipalId --role "Storage Blob Data Contributor" --scope "$storageResourceId" | Out-Null + } + + if ($param_VideoIndexerArmApiVersion -eq '2025-04-01') { + $assignment = az role assignment list --assignee $videoIndexerPrincipalId --scope "$resourceId" --query "[?roleDefinitionName=='Cognitive Services Contributor']" --output json | ConvertFrom-Json + if (-not $assignment) { + az role assignment create --assignee $videoIndexerPrincipalId --role "Cognitive Services Contributor" --scope "$resourceId" | Out-Null + } + + $assignment = az role assignment list --assignee $videoIndexerPrincipalId --scope "$resourceId" --query "[?roleDefinitionName=='Cognitive Services User']" --output json | ConvertFrom-Json + if (-not $assignment) { + az role assignment create --assignee $videoIndexerPrincipalId --role "Cognitive Services User" --scope "$resourceId" | Out-Null + } + } + } else { + Write-Warning "Azure Video Indexer principal ID could not be resolved. Skipping Video Indexer RBAC assignments." + } +} + #------------------------------------------------------------- Write-Host "Getting Cosmos DB: Resource ID" diff --git a/deployers/bicep/README.md b/deployers/bicep/README.md index f00176576..a1153abf1 100644 --- a/deployers/bicep/README.md +++ b/deployers/bicep/README.md @@ -26,7 +26,7 @@ The following variables will be used within this document: - *\* - This will become the beginning of each of the objects created. Minimum of 3 characters, maximum of 12 characters. No Spaces or special characters. - *\* - This will be used as part of the object names as well as with the AZD environments. **Example:** *dev/qa/prod*. -- *\* - Options will be *AzureCloud | AzureUSGovernment* +- *\* - Options will be *AzureCloud | AzureUSGovernment | custom* - *\* - Azure OpenAI deployment type for default model deployments. **Options:** *Standard | DatazoneStandard | GlobalStandard* for Azure Commercial. Use *Standard* for Azure Government. - *\* - Should be presented in the form *imageName:label* **Default:** *simplechat:latest* @@ -218,8 +218,8 @@ During `azd up`, the predeploy hook now builds the application image in Azure Co > ⚠️ **Important:** Review this section carefully before deploying to Azure Government. -- **Services NOT available in Azure Government:** - - Azure Video Indexer - Set `deployVideoIndexerService` to `false` +- **Services with cloud-specific deployment behavior in Azure Government:** + - Azure Video Indexer - use the government ARM API profile (`2024-01-01`), which does not support the newer OpenAI integration and private endpoint properties used in Azure Commercial - **SKU Restrictions:** - **GlobalStandard SKU is NOT available** - Azure OpenAI models must use `Standard` SKU instead @@ -433,7 +433,7 @@ az resource update --name --app --resource-group | Log Analytics | ✅ | ✅ | | | Content Safety | ✅ | ⚠️ Limited | Not all regions | | Speech Service | ✅ | ⚠️ Limited | Feature restrictions | -| Video Indexer | ✅ | ❌ Not Available | | +| Video Indexer | ✅ | ✅ Limited | Azure Government uses the `2024-01-01` ARM API profile; newer OpenAI integration and private endpoint properties remain commercial/custom-cloud only | | Redis Cache | ✅ | ✅ | Standard tier | ### Endpoint Differences @@ -540,8 +540,8 @@ If you do not want this deployment to create new Azure OpenAI model deployments, - **Solution:** Use `Standard` SKU for all models in Azure Government **Error: "Resource 'Microsoft.VideoIndexer/accounts' not found"** -- **Cause:** Video Indexer not available in region (especially USGov) -- **Solution:** Set `deployVideoIndexerService` to `false` +- **Cause:** Video Indexer API capabilities differ by cloud and region +- **Solution:** Use the cloud-appropriate API profile. Azure Government deployments use the `2024-01-01` profile; commercial deployments use `2025-04-01`; custom clouds can override the API version and endpoint. ### Post-Deployment Issues diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index e471c38c7..d89cf67f3 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -113,6 +113,16 @@ param privateDnsZoneConfigs object = {} - Public Azure AI Foundry project endpoints are supported for application configuration, but do not support private endpoint automation''') param existingOpenAIEndpoint string = '' +@description('''Optional Video Indexer ARM API version override for custom cloud deployments. +- Leave blank to use the cloud default +- Public defaults to 2025-04-01 +- Azure Government defaults to 2024-01-01''') +param customVideoIndexerArmApiVersion string = '' + +@description('''Optional Video Indexer endpoint override for custom cloud deployments. +- Leave blank to use the public endpoint default''') +param customVideoIndexerEndpoint string = '' + @description('''Optional existing Azure OpenAI resource name. - Provide this when reusing a standard Azure OpenAI resource and you want managed identity permissions or private endpoint integration configured automatically''') param existingOpenAIResourceName string = '' @@ -229,6 +239,14 @@ var defaultEmbeddingModels = isUsGovernmentCloud ] var resolvedGptModels = empty(gptModels) ? defaultGptModels : gptModels var resolvedEmbeddingModels = empty(embeddingModels) ? defaultEmbeddingModels : embeddingModels +var resolvedVideoIndexerArmApiVersion = scCloudEnvironment == 'usgovernment' + ? '2024-01-01' + : (scCloudEnvironment == 'custom' && !empty(customVideoIndexerArmApiVersion) ? customVideoIndexerArmApiVersion : '2025-04-01') +var resolvedVideoIndexerEndpoint = scCloudEnvironment == 'usgovernment' + ? 'https://api.videoindexer.ai.azure.us' + : (scCloudEnvironment == 'custom' && !empty(customVideoIndexerEndpoint) ? customVideoIndexerEndpoint : 'https://api.videoindexer.ai') +var videoIndexerSupportsOpenAiIntegration = resolvedVideoIndexerArmApiVersion == '2025-04-01' +var videoIndexerSupportsPrivateEndpoints = resolvedVideoIndexerArmApiVersion == '2025-04-01' var hasExistingAppServiceSubnetId = !empty(existingAppServiceSubnetId) var hasExistingPrivateEndpointSubnetId = !empty(existingPrivateEndpointSubnetId) var inferredVirtualNetworkId = hasExistingAppServiceSubnetId ? split(existingAppServiceSubnetId, '/subnets/')[0] : (hasExistingPrivateEndpointSubnetId ? split(existingPrivateEndpointSubnetId, '/subnets/')[0] : '') @@ -519,6 +537,7 @@ module appService 'modules/appService.bicep' = { customResourceManagerUrl: customResourceManagerUrl customCognitiveServicesScope: customCognitiveServicesScope customSearchResourceUrl: customSearchResourceUrl + customVideoIndexerEndpoint: resolvedVideoIndexerEndpoint } } @@ -596,6 +615,7 @@ module videoIndexerService 'modules/videoIndexer.bicep' = if (deployVideoIndexer storageAccount: storageAccount.outputs.name openAiServiceName: openAI.outputs.openAIName + videoIndexerArmApiVersion: resolvedVideoIndexerArmApiVersion enablePrivateNetworking: enablePrivateNetworking } @@ -630,6 +650,7 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP contentSafetyName: deployContentSafety ? contentSafety.outputs.contentSafetyName : '' #disable-next-line BCP318 // expect one value to be null videoIndexerName: deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerServiceName : '' + videoIndexerSupportsOpenAiIntegration: videoIndexerSupportsOpenAiIntegration } } @@ -667,6 +688,7 @@ module privateNetworking 'modules/privateNetworking.bicep' = if (enablePrivateNe speechServiceName: deploySpeechService ? speechService.outputs.speechServiceName : '' #disable-next-line BCP318 // expect one value to be null videoIndexerName: deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerServiceName : '' + videoIndexerSupportsPrivateEndpoints: videoIndexerSupportsPrivateEndpoints } } @@ -701,8 +723,10 @@ output var_searchServiceEndpoint string = searchService.outputs.searchServiceEnd #disable-next-line BCP318 // expect one value to be null output var_speechServiceEndpoint string = deploySpeechService ? speechService.outputs.speechServiceEndpoint : '' output var_subscriptionId string = subscription().subscriptionId +output var_videoIndexerArmApiVersion string = resolvedVideoIndexerArmApiVersion #disable-next-line BCP318 // expect one value to be null output var_videoIndexerAccountId string = deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerAccountId : '' +output var_videoIndexerEndpoint string = resolvedVideoIndexerEndpoint #disable-next-line BCP318 // expect one value to be null output var_videoIndexerName string = deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerServiceName : '' diff --git a/deployers/bicep/main.parameters.json b/deployers/bicep/main.parameters.json index e7907f703..a5239c837 100644 --- a/deployers/bicep/main.parameters.json +++ b/deployers/bicep/main.parameters.json @@ -62,6 +62,12 @@ "existingOpenAIEndpoint": { "value": "${EXISTING_AZURE_OPENAI_ENDPOINT}" }, + "customVideoIndexerArmApiVersion": { + "value": "${CUSTOM_VIDEO_INDEXER_ARM_API_VERSION}" + }, + "customVideoIndexerEndpoint": { + "value": "${CUSTOM_VIDEO_INDEXER_ENDPOINT}" + }, "existingOpenAIResourceName": { "value": "${EXISTING_AZURE_OPENAI_RESOURCE_NAME}" }, diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index 5ce6b1908..527373723 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -41,6 +41,8 @@ param customResourceManagerUrl string? param customCognitiveServicesScope string? @description('Custom search resource URL for token audience, e.g. https://search.azure.us') param customSearchResourceUrl string? +@description('Custom Video Indexer endpoint, e.g. https://api.videoindexer.ai') +param customVideoIndexerEndpoint string? var tenantId = tenant().tenantId var openIdMetadataUrl = '${az.environment().authentication.loginEndpoint}${tenantId}/v2.0/.well-known/openid-configuration' @@ -137,6 +139,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { { name: 'AZURE_OPENAI_RESOURCE_NAME', value: openAiServiceName } { name: 'AZURE_OPENAI_RESOURCE_GROUP_NAME', value: openAiResourceGroupName } { name: 'AZURE_OPENAI_URL', value: openAiEndpoint } + { name: 'VIDEO_INDEXER_ARM_API_VERSION', value: azurePlatform == 'usgovernment' ? '2024-01-01' : '2025-04-01' } { name: 'AZURE_SEARCH_SERVICE_NAME', value: searchService.name } // Only add this setting if authenticationType is 'key' ...(authenticationType == 'key' @@ -176,6 +179,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { {name: 'CUSTOM_BLOB_STORAGE_URL_VALUE', value: customBlobStorageSuffix ?? ''} {name: 'CUSTOM_COGNITIVE_SERVICES_URL_VALUE', value: customCognitiveServicesScope ?? ''} {name: 'CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE', value: customSearchResourceUrl ?? ''} + {name: 'CUSTOM_VIDEO_INDEXER_ENDPOINT', value: customVideoIndexerEndpoint ?? ''} {name: 'KEY_VAULT_DOMAIN', value: az.environment().suffixes.keyvaultDns} {name: 'CUSTOM_OIDC_METADATA_URL_VALUE', value: openIdMetadataUrl ?? ''}] : []) diff --git a/deployers/bicep/modules/privateNetworking.bicep b/deployers/bicep/modules/privateNetworking.bicep index 34c4dce97..d4850959a 100644 --- a/deployers/bicep/modules/privateNetworking.bicep +++ b/deployers/bicep/modules/privateNetworking.bicep @@ -25,6 +25,7 @@ param webAppName string param contentSafetyName string param speechServiceName string param videoIndexerName string +param videoIndexerSupportsPrivateEndpoints bool = true var useExternalOpenAIResource = openAIName != '' && !empty(openAIResourceGroupName) && !empty(openAISubscriptionId) @@ -452,11 +453,11 @@ module speechServicePE 'privateEndpoint.bicep' = if (speechServiceName != '') { //========================================================= // video indexer service - Optional //========================================================= -resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' existing = if (videoIndexerName != '') { +resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' existing = if (videoIndexerName != '' && videoIndexerSupportsPrivateEndpoints) { name: videoIndexerName } -module videoIndexerPE 'privateEndpoint.bicep' = if (videoIndexerName != '') { +module videoIndexerPE 'privateEndpoint.bicep' = if (videoIndexerName != '' && videoIndexerSupportsPrivateEndpoints) { name: 'videoIndexerPE' dependsOn: [ videoIndexerService diff --git a/deployers/bicep/modules/setPermissions-openAIExternal.bicep b/deployers/bicep/modules/setPermissions-openAIExternal.bicep index 96d85d9d9..aa6b228f8 100644 --- a/deployers/bicep/modules/setPermissions-openAIExternal.bicep +++ b/deployers/bicep/modules/setPermissions-openAIExternal.bicep @@ -6,6 +6,7 @@ param webAppPrincipalId string param enterpriseAppServicePrincipalId string param videoIndexerPrincipalId string = '' param videoIndexerName string = '' +param videoIndexerSupportsOpenAiIntegration bool = true resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { name: openAIName @@ -37,7 +38,7 @@ resource openAIenterpriseAppUserRole 'Microsoft.Authorization/roleAssignments@20 } } -resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !empty(videoIndexerPrincipalId)) { +resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !empty(videoIndexerPrincipalId) && videoIndexerSupportsOpenAiIntegration) { name: guid(openAiService.id, videoIndexerPrincipalId, 'video-indexer-cog-services-contributor') scope: openAiService properties: { @@ -50,7 +51,7 @@ resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/ } } -resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !empty(videoIndexerPrincipalId)) { +resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !empty(videoIndexerPrincipalId) && videoIndexerSupportsOpenAiIntegration) { name: guid(openAiService.id, videoIndexerPrincipalId, 'video-indexer-cog-services-user') scope: openAiService properties: { diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index d2259918f..49c249717 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -16,6 +16,7 @@ param searchServiceName string param redisCacheName string param contentSafetyName string param videoIndexerName string +param videoIndexerSupportsOpenAiIntegration bool = true var useExternalOpenAIResource = openAIName != '' && !empty(openAIResourceGroupName) && !empty(openAISubscriptionId) @@ -63,7 +64,7 @@ resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' existin name: contentSafetyName } -resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' existing = if (videoIndexerName != '') { +resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2024-01-01' existing = if (videoIndexerName != '') { name: videoIndexerName } @@ -161,6 +162,7 @@ module openAIExternalPermissions 'setPermissions-openAIExternal.bicep' = if (use #disable-next-line BCP318 // may be null if video indexer not deployed videoIndexerPrincipalId: videoIndexerName != '' ? videoIndexerService.identity.principalId : '' videoIndexerName: videoIndexerName + videoIndexerSupportsOpenAiIntegration: videoIndexerSupportsOpenAiIntegration } } @@ -264,7 +266,7 @@ resource videoIndexerStorageBlobDataContributorRole 'Microsoft.Authorization/rol } // grant the video indexer service access to OpenAI service as cognitive services Contributor -resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !useExternalOpenAIResource) { +resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !useExternalOpenAIResource && videoIndexerSupportsOpenAiIntegration) { name: guid(openAiService.id, videoIndexerService.id, 'video-indexer-cog-services-contributor') scope: openAiService properties: { @@ -279,7 +281,7 @@ resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/ } // grant the video indexer service access to OpenAI service as cognitive services user -resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !useExternalOpenAIResource) { +resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' && !useExternalOpenAIResource && videoIndexerSupportsOpenAiIntegration) { name: guid(openAiService.id, videoIndexerService.id, 'video-indexer-cog-services-user') scope: openAiService properties: { diff --git a/deployers/bicep/modules/videoIndexer.bicep b/deployers/bicep/modules/videoIndexer.bicep index 786d71080..d91572e4c 100644 --- a/deployers/bicep/modules/videoIndexer.bicep +++ b/deployers/bicep/modules/videoIndexer.bicep @@ -10,6 +10,7 @@ param logAnalyticsId string param storageAccount string param openAiServiceName string +param videoIndexerArmApiVersion string param enablePrivateNetworking bool @@ -26,8 +27,9 @@ resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existin name: openAiServiceName } -// deploy video indexer service if required -resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' = { +var useLegacyVideoIndexerApi = videoIndexerArmApiVersion == '2024-01-01' + +resource videoIndexerServiceCurrent 'Microsoft.VideoIndexer/accounts@2025-04-01' = if (!useLegacyVideoIndexerApi) { name: toLower('${appName}-${environment}-video') location: location @@ -50,10 +52,38 @@ resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' = { ] } +resource videoIndexerServiceLegacy 'Microsoft.VideoIndexer/accounts@2024-01-01' = if (useLegacyVideoIndexerApi) { + name: toLower('${appName}-${environment}-video') + location: location + + identity: { + type: 'SystemAssigned' + } + properties: { + storageServices: { + resourceId: storage.id + } + } + tags: tags + dependsOn: [ + storage + ] +} + // configure diagnostic settings for video indexer service -resource videoIndexerServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { - name: toLower('${videoIndexerService.name}-diagnostics') - scope: videoIndexerService +resource videoIndexerServiceCurrentDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging && !useLegacyVideoIndexerApi) { + name: toLower('${videoIndexerServiceCurrent.name}-diagnostics') + scope: videoIndexerServiceCurrent + properties: { + workspaceId: logAnalyticsId + #disable-next-line BCP318 // expect one value to be null + logs: diagnosticConfigs.outputs.limitedLogCategories + } +} + +resource videoIndexerServiceLegacyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging && useLegacyVideoIndexerApi) { + name: toLower('${videoIndexerServiceLegacy.name}-diagnostics') + scope: videoIndexerServiceLegacy properties: { workspaceId: logAnalyticsId #disable-next-line BCP318 // expect one value to be null @@ -61,5 +91,7 @@ resource videoIndexerServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2 } } -output videoIndexerServiceName string = videoIndexerService.name -output videoIndexerAccountId string = videoIndexerService.properties.accountId +#disable-next-line BCP318 // exactly one conditional resource exists for the selected API version +output videoIndexerServiceName string = useLegacyVideoIndexerApi ? videoIndexerServiceLegacy.name : videoIndexerServiceCurrent.name +#disable-next-line BCP318 // exactly one conditional resource exists for the selected API version +output videoIndexerAccountId string = useLegacyVideoIndexerApi ? videoIndexerServiceLegacy.properties.accountId : videoIndexerServiceCurrent.properties.accountId diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index cbf94b002..b89dc470e 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -406,8 +406,10 @@ def get_core_service_keys( item["video_indexer_resource_group"] = var_rgName item["video_indexer_subscription_id"] = var_subscriptionId item["video_indexer_account_name"] = var_videoIndexerName +item["video_indexer_endpoint"] = os.getenv("var_videoIndexerEndpoint", item.get("video_indexer_endpoint", "")) item["video_indexer_location"] = var_videoIndexerLocation item["video_indexer_account_id"] = var_videoIndexerAccountId +item["video_indexer_arm_api_version"] = os.getenv("var_videoIndexerArmApiVersion", item.get("video_indexer_arm_api_version", "2024-01-01")) # Speech Service Configuration if var_speechServiceEndpoint and var_speechServiceEndpoint.strip(): diff --git a/deployers/terraform/ReadMe.md b/deployers/terraform/ReadMe.md index 545e3e4e3..4a456f8eb 100644 --- a/deployers/terraform/ReadMe.md +++ b/deployers/terraform/ReadMe.md @@ -13,6 +13,12 @@ az login --scope https://graph.microsoft.us//.default --service-principal --user az account set -s '@AZURE_SUBSCRIPTION_ID' ``` +`global_which_azure_platform` now accepts `AzureCloud`, `AzureUSGovernment`, and `Custom`. + +- `AzureCloud` deploys the commercial Video Indexer profile (`2025-04-01`). +- `AzureUSGovernment` deploys the government Video Indexer profile (`2024-01-01`). +- `Custom` currently supports custom runtime settings for Video Indexer, but Terraform blocks Video Indexer resource provisioning until provider-level custom cloud support is validated. + ## Execute Initalize-AzureEnvironment.ps1 After logging into the target Azure subscription, execute the Initalize-AzureEnvironment.ps1 script. This will configure items not set in the terraform script but is required for the remaining deployment steps. @@ -118,6 +124,7 @@ param_base_name = "rudy1" param_use_existing_openai_instance = true param_existing_azure_openai_resource_name = "gregazureopenai1" param_existing_azure_openai_resource_group_name = "azureopenairg" +param_deploy_video_indexer_service = true param_existing_azure_openai_subscription_id = "00000000-0000-0000-0000-000000000000" # Optional endpoint override for existing Azure OpenAI or Azure AI Foundry-compatible endpoints # param_existing_azure_openai_endpoint = "https://my-openai-resource.openai.azure.com/" diff --git a/deployers/terraform/main.tf b/deployers/terraform/main.tf index e4827f03f..eee3718ba 100644 --- a/deployers/terraform/main.tf +++ b/deployers/terraform/main.tf @@ -59,7 +59,7 @@ provider "azurerm" { } } storage_use_azuread = true - environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : (var.global_which_azure_platform == "AzureCloud" ? "public" : null) + environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : "public" tenant_id = var.param_tenant_id subscription_id = var.param_subscription_id } @@ -86,28 +86,76 @@ provider "azurerm" { } storage_use_azuread = true - environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : (var.global_which_azure_platform == "AzureCloud" ? "public" : null) + environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : "public" tenant_id = var.param_tenant_id subscription_id = var.param_existing_azure_openai_subscription_id != "" ? var.param_existing_azure_openai_subscription_id : var.param_subscription_id } # Configure the AzureAD Provider provider "azuread" { - environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : (var.global_which_azure_platform == "AzureCloud" ? "public" : null) + environment = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : "public" tenant_id = var.param_tenant_id } # Variables variable "global_which_azure_platform" { - description = "Set to 'AzureUSGovernment' for Azure Government, 'AzureCloud' for Azure Commercial." + description = "Set to 'AzureUSGovernment' for Azure Government, 'AzureCloud' for Azure Commercial, or 'Custom' for a customer-managed cloud." type = string default = "AzureUSGovernment" # Default from script validation { - condition = contains(["AzureUSGovernment", "AzureCloud"], var.global_which_azure_platform) - error_message = "Invalid Azure platform. Must be 'AzureUSGovernment' or 'AzureCloud'." + condition = contains(["AzureUSGovernment", "AzureCloud", "Custom"], var.global_which_azure_platform) + error_message = "Invalid Azure platform. Must be 'AzureUSGovernment', 'AzureCloud', or 'Custom'." } } +variable "param_deploy_video_indexer_service" { + description = "Set to true to deploy an Azure Video Indexer resource." + type = bool + default = false +} + +variable "param_custom_identity_url" { + description = "Custom cloud login endpoint used by the app when global_which_azure_platform is Custom." + type = string + default = "" +} + +variable "param_custom_resource_manager_url" { + description = "Custom cloud ARM endpoint used by the app when global_which_azure_platform is Custom." + type = string + default = "" +} + +variable "param_custom_graph_url" { + description = "Custom cloud Graph endpoint used by the app when global_which_azure_platform is Custom." + type = string + default = "" +} + +variable "param_custom_cognitive_services_scope" { + description = "Custom cloud Cognitive Services scope used by the app when global_which_azure_platform is Custom." + type = string + default = "" +} + +variable "param_custom_search_resource_url" { + description = "Custom cloud Search token audience used by the app when global_which_azure_platform is Custom." + type = string + default = "" +} + +variable "param_custom_video_indexer_endpoint" { + description = "Custom cloud Video Indexer data-plane endpoint used by the app when global_which_azure_platform is Custom." + type = string + default = "" +} + +variable "param_custom_video_indexer_arm_api_version" { + description = "Optional custom cloud Video Indexer ARM API version override." + type = string + default = "" +} + variable "param_subscription_id" { description = "Your Azure Subscription ID." type = string @@ -207,6 +255,8 @@ variable "acr_password" { # Resource Naming Convention (matching PowerShell script logic) locals { + is_usgovernment_cloud = var.global_which_azure_platform == "AzureUSGovernment" + is_custom_cloud = var.global_which_azure_platform == "Custom" resource_group_name = "sc-${var.param_base_name}-${var.param_environment}-rg" app_registration_name = "${var.param_base_name}-${var.param_environment}-ar" app_service_plan_name = "${var.param_base_name}-${var.param_environment}-asp" @@ -219,16 +269,48 @@ locals { log_analytics_name = "${var.param_base_name}-${var.param_environment}-la" managed_identity_name = "${var.param_base_name}-${var.param_environment}-id" search_service_name = "${var.param_base_name}-${var.param_environment}-search" + video_indexer_name = "${var.param_base_name}-${var.param_environment}-video" storage_account_base = "${var.param_base_name}${var.param_environment}sa" storage_account_name = substr(replace(local.storage_account_base, "/[^a-z0-9]/", ""), 0, 24) - acr_base_url = var.global_which_azure_platform == "AzureUSGovernment" ? "${var.acr_name}.azurecr.us" : "${var.acr_name}.azurecr.io" - param_registry_server = var.global_which_azure_platform == "AzureUSGovernment" ? "https://${var.acr_name}.azurecr.us" : "https://${var.acr_name}.azurecr.io" - - app_service_fqdn_suffix = var.global_which_azure_platform == "AzureUSGovernment" ? ".azurewebsites.us" : ".azurewebsites.net" - graph_url = var.global_which_azure_platform == "AzureUSGovernment" ? "https://graph.microsoft.us" : "https://graph.microsoft.com" - cosmos_db_url_template = var.global_which_azure_platform == "AzureUSGovernment" ? "https://%s.documents.azure.us:443/" : "https://%s.documents.azure.com:443/" - openai_url_template = var.global_which_azure_platform == "AzureUSGovernment" ? "https://%s.openai.azure.us/" : "https://%s.openai.azure.com/" + acr_base_url = local.is_usgovernment_cloud ? "${var.acr_name}.azurecr.us" : "${var.acr_name}.azurecr.io" + param_registry_server = local.is_usgovernment_cloud ? "https://${var.acr_name}.azurecr.us" : "https://${var.acr_name}.azurecr.io" + + app_service_fqdn_suffix = local.is_usgovernment_cloud ? ".azurewebsites.us" : ".azurewebsites.net" + graph_url = local.is_usgovernment_cloud ? "https://graph.microsoft.us" : "https://graph.microsoft.com" + cosmos_db_url_template = local.is_usgovernment_cloud ? "https://%s.documents.azure.us:443/" : "https://%s.documents.azure.com:443/" + openai_url_template = local.is_usgovernment_cloud ? "https://%s.openai.azure.us/" : "https://%s.openai.azure.com/" + azure_environment_name = local.is_usgovernment_cloud ? "usgovernment" : (local.is_custom_cloud ? "custom" : "public") + video_indexer_arm_api_version = local.is_usgovernment_cloud ? "2024-01-01" : (local.is_custom_cloud && var.param_custom_video_indexer_arm_api_version != "" ? var.param_custom_video_indexer_arm_api_version : "2025-04-01") + video_indexer_endpoint = local.is_usgovernment_cloud ? "https://api.videoindexer.ai.azure.us" : (local.is_custom_cloud && var.param_custom_video_indexer_endpoint != "" ? var.param_custom_video_indexer_endpoint : "https://api.videoindexer.ai") + video_indexer_supports_openai = local.video_indexer_arm_api_version == "2025-04-01" + video_indexer_supports_private_endpoints = local.video_indexer_arm_api_version == "2025-04-01" + video_indexer_body_json = local.video_indexer_supports_openai ? jsonencode({ + identity = { + type = "SystemAssigned" + } + properties = { + storageServices = { + resourceId = azurerm_storage_account.sa.id + } + openAiServices = { + resourceId = local.openai_resource_id + } + publicNetworkAccess = var.param_enable_private_networking ? "Disabled" : "Enabled" + } + tags = local.common_tags + }) : jsonencode({ + identity = { + type = "SystemAssigned" + } + properties = { + storageServices = { + resourceId = azurerm_storage_account.sa.id + } + } + tags = local.common_tags + }) + openai_resource_id = var.param_use_existing_openai_instance ? (local.use_existing_openai_resource_metadata ? data.azurerm_cognitive_account.existing_openai[0].id : "") : azurerm_cognitive_account.openai[0].id existing_openai_subscription_id = var.param_existing_azure_openai_subscription_id != "" ? var.param_existing_azure_openai_subscription_id : var.param_subscription_id use_existing_openai_resource_metadata = var.param_use_existing_openai_instance && var.param_existing_azure_openai_resource_name != "" && var.param_existing_azure_openai_resource_group_name != "" enable_openai_rbac_assignments = !var.param_use_existing_openai_instance || local.use_existing_openai_resource_metadata @@ -402,10 +484,10 @@ resource "azurerm_service_plan" "asp" { # --- App Service (Web App) --- resource "azurerm_linux_web_app" "app" { - name = local.app_service_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - service_plan_id = azurerm_service_plan.asp.id + name = local.app_service_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + service_plan_id = azurerm_service_plan.asp.id # This Terraform deployer uses a container-based Linux Web App. # Gunicorn startup comes from the container image entrypoint, # so native Python app_command_line/startup settings are not used here. @@ -449,7 +531,7 @@ resource "azurerm_linux_web_app" "app" { } app_settings = { - "AZURE_ENVIRONMENT" = var.global_which_azure_platform == "AzureUSGovernment" ? "usgovernment" : "public" + "AZURE_ENVIRONMENT" = local.azure_environment_name "SCM_DO_BUILD_DURING_DEPLOYMENT" = "false" "WEBSITE_PULL_IMAGE_OVER_VNET" = var.param_enable_private_networking ? "true" : "false" "AZURE_COSMOS_AUTHENTICATION_TYPE" = "key" @@ -463,6 +545,7 @@ resource "azurerm_linux_web_app" "app" { "AZURE_OPENAI_RESOURCE_GROUP_NAME" = var.param_use_existing_openai_instance ? var.param_existing_azure_openai_resource_group_name : azurerm_resource_group.rg.name "AZURE_OPENAI_SUBSCRIPTION_ID" = var.param_use_existing_openai_instance ? local.existing_openai_subscription_id : var.param_subscription_id "AZURE_OPENAI_URL" = var.param_use_existing_openai_instance ? (var.param_existing_azure_openai_endpoint != "" ? var.param_existing_azure_openai_endpoint : (local.use_existing_openai_resource_metadata ? data.azurerm_cognitive_account.existing_openai[0].endpoint : "")) : azurerm_cognitive_account.openai[0].endpoint + "VIDEO_INDEXER_ARM_API_VERSION" = local.video_indexer_arm_api_version "AZURE_SEARCH_SERVICE_NAME" = azurerm_search_service.search.name "AZURE_SEARCH_API_KEY" = azurerm_search_service.search.primary_key "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT" = azurerm_cognitive_account.docintel.endpoint @@ -480,6 +563,12 @@ resource "azurerm_linux_web_app" "app" { "XDT_MicrosoftApplicationInsights_BaseExtensions" = "disabled" "XDT_MicrosoftApplicationInsights_Mode" = "recommended" "XDT_MicrosoftApplicationInsights_PreemptSdk" = "disabled" + "CUSTOM_GRAPH_URL_VALUE" = local.is_custom_cloud ? var.param_custom_graph_url : "" + "CUSTOM_IDENTITY_URL_VALUE" = local.is_custom_cloud ? var.param_custom_identity_url : "" + "CUSTOM_RESOURCE_MANAGER_URL_VALUE" = local.is_custom_cloud ? var.param_custom_resource_manager_url : "" + "CUSTOM_COGNITIVE_SERVICES_URL_VALUE" = local.is_custom_cloud ? var.param_custom_cognitive_services_scope : "" + "CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE" = local.is_custom_cloud ? var.param_custom_search_resource_url : "" + "CUSTOM_VIDEO_INDEXER_ENDPOINT" = local.is_custom_cloud ? local.video_indexer_endpoint : "" } site_config { @@ -726,6 +815,36 @@ resource "azurerm_search_service" "search" { tags = local.common_tags } +resource "terraform_data" "video_indexer_custom_cloud_validation" { + input = { + deploy_video_indexer = var.param_deploy_video_indexer_service + custom_cloud = local.is_custom_cloud + } + + lifecycle { + precondition { + condition = !(local.is_custom_cloud && var.param_deploy_video_indexer_service) + error_message = "Terraform can inject custom-cloud Video Indexer runtime settings, but Video Indexer resource provisioning is only implemented for AzureCloud and AzureUSGovernment right now. Set param_deploy_video_indexer_service=false for Custom and configure an existing account." + } + } +} + +resource "azapi_resource" "video_indexer" { + count = var.param_deploy_video_indexer_service ? 1 : 0 + type = "Microsoft.VideoIndexer/accounts@${local.video_indexer_arm_api_version}" + name = local.video_indexer_name + parent_id = azurerm_resource_group.rg.id + location = azurerm_resource_group.rg.location + schema_validation_enabled = false + response_export_values = ["identity.principalId", "properties.accountId"] + + body = jsondecode(local.video_indexer_body_json) + + depends_on = [ + terraform_data.video_indexer_custom_cloud_validation, + ] +} + ######################################### # @@ -797,6 +916,27 @@ resource "azurerm_role_assignment" "app_service_smi_storage_contributor" { principal_id = azurerm_linux_web_app.app.identity[0].principal_id } +resource "azurerm_role_assignment" "video_indexer_storage_contributor" { + count = var.param_deploy_video_indexer_service ? 1 : 0 + scope = azurerm_storage_account.sa.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = jsondecode(azapi_resource.video_indexer[0].output).identity.principalId +} + +resource "azurerm_role_assignment" "video_indexer_openai_contributor" { + count = var.param_deploy_video_indexer_service && local.video_indexer_supports_openai && local.openai_resource_id != "" ? 1 : 0 + scope = local.openai_resource_id + role_definition_name = "Cognitive Services Contributor" + principal_id = jsondecode(azapi_resource.video_indexer[0].output).identity.principalId +} + +resource "azurerm_role_assignment" "video_indexer_openai_user" { + count = var.param_deploy_video_indexer_service && local.video_indexer_supports_openai && local.openai_resource_id != "" ? 1 : 0 + scope = local.openai_resource_id + role_definition_name = "Cognitive Services User" + principal_id = jsondecode(azapi_resource.video_indexer[0].output).identity.principalId +} + # Storage Blob Data Contributor on Storage Account resource "azurerm_role_assignment" "acr_pull" { scope = data.azurerm_container_registry.acrregistry.id diff --git a/deployers/terraform/private_networking.tf b/deployers/terraform/private_networking.tf index 1df57f132..72350a305 100644 --- a/deployers/terraform/private_networking.tf +++ b/deployers/terraform/private_networking.tf @@ -282,6 +282,14 @@ locals { subresource = "account" dns_zone_key = "openAi" } + } : {}, + var.param_deploy_video_indexer_service && local.video_indexer_supports_private_endpoints ? { + videoIndexer = { + name = "videoindexer" + resource_id = azapi_resource.video_indexer[0].id + subresource = "account" + dns_zone_key = "cognitiveServices" + } } : {} ) : {} } From 177c7c5e1f26898a6cb45d976e26b87e8f798b3a Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Tue, 31 Mar 2026 14:06:09 -0400 Subject: [PATCH 6/7] Update release_notes.md --- docs/explanation/release_notes.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 09224196c..1531879d5 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,15 @@ # Feature Release +### **(v0.240.004)** + +#### New Features + +* **Cross-Cloud Deployment Improvements** + * Updated the Azure CLI, AZD, Bicep, and Terraform deployment paths to better align with the current SimpleChat runtime configuration and reduce post-deployment manual fixes. + * Added optional Azure Video Indexer deployment support with cloud-aware defaults, including the correct endpoint and ARM API version handling for Azure Commercial, Azure Government, and registered custom clouds. + * (Ref: `deployers/azure.yaml`, `deployers/azurecli/deploy-simplechat.ps1`, `deployers/bicep/main.bicep`, `deployers/bicep/modules/videoIndexer.bicep`, `deployers/terraform/main.tf`, `application/single_app/functions_settings.py`) + ### **(v0.240.001)** #### Bug Fixes From 4bbb1e7036f13e4f9edd4445d62c5dc55f7d333d Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Tue, 31 Mar 2026 14:56:32 -0400 Subject: [PATCH 7/7] resolved PRIVATE_DNS_ZONE_CONFIGS and bicep variable for AOAI --- application/single_app/config.py | 2 +- deployers/bicep/README.md | 3 + deployers/bicep/main.parameters.json | 2 +- deployers/bicep/postconfig.py | 91 +++++++++++++++++-- .../OPENAI_ENDPOINT_ONLY_POSTCONFIG_FIX.md | 75 +++++++++++++++ ...PRIVATE_DNS_ZONE_CONFIGS_AZD_WIRING_FIX.md | 75 +++++++++++++++ ...test_azd_private_dns_zone_config_wiring.py | 78 ++++++++++++++++ .../test_core_service_key_deployment_path.py | 38 +++++++- 8 files changed, 347 insertions(+), 17 deletions(-) create mode 100644 docs/explanation/fixes/OPENAI_ENDPOINT_ONLY_POSTCONFIG_FIX.md create mode 100644 docs/explanation/fixes/PRIVATE_DNS_ZONE_CONFIGS_AZD_WIRING_FIX.md create mode 100644 functional_tests/test_azd_private_dns_zone_config_wiring.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 13806428c..0871294c0 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.240.004" +VERSION = "0.240.006" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/deployers/bicep/README.md b/deployers/bicep/README.md index a1153abf1..d7749b568 100644 --- a/deployers/bicep/README.md +++ b/deployers/bicep/README.md @@ -378,6 +378,7 @@ Private DNS is required for reliable name resolution when private endpoints are - If your organization already manages private DNS centrally, provide `privateDnsZoneConfigs` with one or more `zoneResourceId` values to reuse those existing zones. - Set `createVNetLink` to `false` only when the DNS zone is already linked to the target VNet or when your networking team will manage the VNet link separately. - If you reuse an existing VNet but do not create or link the required private DNS zones, the application may deploy successfully but private name resolution to services such as Cosmos DB, Storage, Azure AI Search, Azure OpenAI, Key Vault, and App Service private endpoints will fail. +- For `azd up` and `azd provision`, store the same JSON object in the `PRIVATE_DNS_ZONE_CONFIGS` AZD environment value so `deployers/bicep/main.parameters.json` can pass it through to the Bicep parameter. Example `privateDnsZoneConfigs` value: @@ -396,6 +397,8 @@ Example `privateDnsZoneConfigs` value: Supported `privateDnsZoneConfigs` keys: `keyVault`, `cosmosDb`, `containerRegistry`, `aiSearch`, `blobStorage`, `cognitiveServices`, `openAi`, and `webSites`. +If you use AZD, set `PRIVATE_DNS_ZONE_CONFIGS` to the JSON object above instead of editing the parameter file directly. + During initial deployment, if post an error is raised "failed running post hooks: 'postprovision'" the deployment is being blocked from executing scripts against the CosmosDB service. Ensure the deployment workstation IP address is added to the `allowedIpAddresses` parameter or the `ALLOWED_IP_RANGES` AZD environment value. Similar messages may be seen from the Azure Container Registry service. If you add the firewall rule manually in Azure Portal, wait up to 30 minutes for propagation before rerunning `azd up`. If `az acr build` fails with `denied: client with IP '
' is not allowed access`, adding the workstation IP usually does not help because the denied address is commonly the Azure Container Registry Tasks worker, not the local machine. In that case, ensure the registry allows trusted Azure services and reprovision the registry configuration. diff --git a/deployers/bicep/main.parameters.json b/deployers/bicep/main.parameters.json index a5239c837..e4bda0f9f 100644 --- a/deployers/bicep/main.parameters.json +++ b/deployers/bicep/main.parameters.json @@ -57,7 +57,7 @@ "value": "${EXISTING_PRIVATE_ENDPOINT_SUBNET_RESOURCE_ID}" }, "privateDnsZoneConfigs": { - "value": {} + "value": "${PRIVATE_DNS_ZONE_CONFIGS}" }, "existingOpenAIEndpoint": { "value": "${EXISTING_AZURE_OPENAI_ENDPOINT}" diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index b89dc470e..dcb0e2ccd 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -10,6 +10,12 @@ credential = AzureCliCredential() +STANDARD_AZURE_OPENAI_HOST_SUFFIXES = ( + ".openai.azure.com", + ".openai.azure.us", + ".openai.azure.cn", +) + def get_azure_cli_executable(): """Resolve the Azure CLI executable path for subprocess usage.""" @@ -59,10 +65,15 @@ def run_azure_cli_command(command_args, description): return value +def get_endpoint_hostname(endpoint): + """Extract a normalized hostname from a service endpoint.""" + parsed_endpoint = urlparse(endpoint or "") + return (parsed_endpoint.netloc or parsed_endpoint.path).strip().lower() + + def extract_resource_name_from_endpoint(endpoint): """Extract the Azure resource name from a standard service endpoint.""" - parsed_endpoint = urlparse(endpoint or "") - hostname = parsed_endpoint.netloc or parsed_endpoint.path + hostname = get_endpoint_hostname(endpoint) if not hostname: return "" @@ -70,6 +81,53 @@ def extract_resource_name_from_endpoint(endpoint): return hostname.split(".")[0] +def is_standard_azure_openai_endpoint(endpoint): + """Return True when the endpoint points to a standard Azure OpenAI resource.""" + hostname = get_endpoint_hostname(endpoint) + if not hostname: + return False + + return any( + hostname.endswith(suffix) + for suffix in STANDARD_AZURE_OPENAI_HOST_SUFFIXES + ) + + +def can_auto_retrieve_openai_key(endpoint, resource_name, resource_group, subscription_id): + """Return True when postconfig has enough metadata to safely fetch an OpenAI key.""" + if not endpoint: + print( + "Skipping Azure OpenAI key retrieval: endpoint is empty; manual configuration is required." + ) + return False + + if not resource_name: + print( + "Skipping Azure OpenAI key retrieval: resource name could not be derived from the endpoint; manual configuration is required." + ) + return False + + if not resource_group: + print( + "Skipping Azure OpenAI key retrieval: resource group metadata is missing; manual configuration is required." + ) + return False + + if not subscription_id: + print( + "Skipping Azure OpenAI key retrieval: subscription metadata is missing; manual configuration is required." + ) + return False + + if not is_standard_azure_openai_endpoint(endpoint): + print( + "Skipping Azure OpenAI key retrieval: endpoint is not a standard Azure OpenAI resource endpoint; manual configuration is required." + ) + return False + + return True + + def get_cognitive_services_key(resource_name, resource_group, subscription_id, description): return run_azure_cli_command( [ @@ -176,14 +234,9 @@ def get_core_service_keys( redis_resource_name = extract_resource_name_from_endpoint(redis_cache_host_name) content_safety_resource_name = extract_resource_name_from_endpoint(content_safety_endpoint) speech_resource_name = extract_resource_name_from_endpoint(speech_service_endpoint) + resolved_openai_subscription_id = openai_subscription_id or subscription_id keys = { - "azure_openai_key": get_cognitive_services_key( - openai_resource_name, - openai_resource_group, - openai_subscription_id or subscription_id, - "Azure OpenAI key", - ), "azure_ai_search_key": get_search_service_key( search_resource_name, resource_group, @@ -197,6 +250,19 @@ def get_core_service_keys( ), } + if can_auto_retrieve_openai_key( + openai_endpoint, + openai_resource_name, + openai_resource_group, + resolved_openai_subscription_id, + ): + keys["azure_openai_key"] = get_cognitive_services_key( + openai_resource_name, + openai_resource_group, + resolved_openai_subscription_id, + "Azure OpenAI key", + ) + if redis_resource_name: keys["redis_key"] = get_redis_cache_key( redis_resource_name, @@ -286,6 +352,7 @@ def get_core_service_keys( redis_cache_host_name=var_redisCacheHostName, speech_service_endpoint=var_speechServiceEndpoint, ) +openai_key = core_service_keys.get("azure_openai_key") # 4. Update the Configurations @@ -298,7 +365,9 @@ def get_core_service_keys( item["azure_openai_gpt_subscription_id"] = var_openAISubscriptionId or var_subscriptionId item["azure_openai_gpt_resource_group"] = var_openAIResourceGroup if var_authenticationType == "key": - item["azure_openai_gpt_key"] = core_service_keys["azure_openai_key"] + item.setdefault("azure_openai_gpt_key", "") + if openai_key: + item["azure_openai_gpt_key"] = openai_key item["gpt_model"] = { "selected": [ { @@ -320,7 +389,9 @@ def get_core_service_keys( item["azure_openai_embedding_subscription_id"] = var_openAISubscriptionId or var_subscriptionId item["azure_openai_embedding_resource_group"] = var_openAIResourceGroup if var_authenticationType == "key": - item["azure_openai_embedding_key"] = core_service_keys["azure_openai_key"] + item.setdefault("azure_openai_embedding_key", "") + if openai_key: + item["azure_openai_embedding_key"] = openai_key item["embedding_model"] = { "selected": [ { diff --git a/docs/explanation/fixes/OPENAI_ENDPOINT_ONLY_POSTCONFIG_FIX.md b/docs/explanation/fixes/OPENAI_ENDPOINT_ONLY_POSTCONFIG_FIX.md new file mode 100644 index 000000000..ecb8c6a6a --- /dev/null +++ b/docs/explanation/fixes/OPENAI_ENDPOINT_ONLY_POSTCONFIG_FIX.md @@ -0,0 +1,75 @@ +# OpenAI Endpoint-Only Postconfig Fix + +Fixed/Implemented in version: **0.240.005** + +## Issue Description + +Deployments that reused an external Azure OpenAI-compatible endpoint with `authenticationType == key` +could fail during `postconfig.py` even when the endpoint was intentionally configured without Azure +resource metadata. + +This was most visible for endpoint-only reuse scenarios such as Azure AI Foundry project endpoints, +where the deployment can configure the application endpoint but cannot safely derive a Cognitive +Services resource for Azure CLI key lookup. + +## Root Cause Analysis + +`deployers/bicep/postconfig.py` always attempted to retrieve an Azure OpenAI key through Azure CLI +whenever key authentication was enabled. + +That behavior assumed all configured OpenAI-compatible endpoints mapped cleanly to a standard +`Microsoft.CognitiveServices/accounts` Azure OpenAI resource and that the deployment always had the +resource group and subscription metadata needed to resolve it. + +For endpoint-only reuse, those assumptions are false, so Azure CLI key retrieval failed and aborted +postconfig instead of allowing manual key configuration. + +## Technical Details + +### Files Modified + +- `deployers/bicep/postconfig.py` +- `functional_tests/test_core_service_key_deployment_path.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Added guarded Azure OpenAI key retrieval so postconfig only uses Azure CLI when the endpoint is a + recognized standard Azure OpenAI resource endpoint and the required resource metadata is present. +- Added a manual-configuration fallback that preserves any existing OpenAI key values on redeploys + and defaults fresh deployments to a blank key when automatic retrieval is unavailable. +- Added regression coverage for the endpoint-only reuse path. +- Bumped the application version to `0.240.005`. + +### Testing Approach + +- Updated `functional_tests/test_core_service_key_deployment_path.py`. +- Added assertions that verify: + - postconfig recognizes standard Azure OpenAI endpoints before attempting Azure CLI key retrieval + - endpoint-only reuse paths keep the deployment from failing hard + - manual OpenAI key configuration is preserved when automatic retrieval is skipped + +## Validation + +### Before + +- key-auth postconfig always attempted Azure CLI key lookup for the configured OpenAI endpoint +- endpoint-only reuse scenarios could fail even though manual key entry was the intended fallback +- redeployments risked overwriting manual OpenAI key configuration with deployment-time failures + +### After + +- postconfig only attempts automatic Azure OpenAI key retrieval when the endpoint and resource + metadata clearly identify a standard Azure OpenAI resource +- endpoint-only reuse scenarios now complete postconfig successfully and leave OpenAI keys available + for manual configuration +- existing manual OpenAI key values are preserved on redeploy when automatic retrieval is skipped + +### Impact Analysis + +This fix is intentionally limited to deployment-time OpenAI key population. + +- standard Azure OpenAI deployments with valid metadata continue to retrieve keys automatically +- Azure AI Foundry and other endpoint-only reuse scenarios no longer hard-fail postconfig +- no changes were made to the authentication behavior of search, document intelligence, Redis, + Content Safety, or Speech services \ No newline at end of file diff --git a/docs/explanation/fixes/PRIVATE_DNS_ZONE_CONFIGS_AZD_WIRING_FIX.md b/docs/explanation/fixes/PRIVATE_DNS_ZONE_CONFIGS_AZD_WIRING_FIX.md new file mode 100644 index 000000000..97270de28 --- /dev/null +++ b/docs/explanation/fixes/PRIVATE_DNS_ZONE_CONFIGS_AZD_WIRING_FIX.md @@ -0,0 +1,75 @@ +# Private DNS Zone Configs AZD Wiring Fix + +Fixed/Implemented in version: **0.240.006** + +## Issue Description + +The Bicep deployment supported `privateDnsZoneConfigs`, and the documentation described how to +reuse centrally managed private DNS zones, but AZD deployments could not actually supply a non-empty +value. + +`deployers/bicep/main.parameters.json` hard-coded `privateDnsZoneConfigs` to `{}`, so `azd up` and +`azd provision` always sent an empty object to the infrastructure template even when the user had +prepared valid JSON for private DNS zone reuse. + +## Root Cause Analysis + +The infrastructure template, module wiring, prerequisite validator, and README were already aligned +around a `PRIVATE_DNS_ZONE_CONFIGS` input path, but the AZD parameters file never forwarded that +environment value into the deployment. + +That created a mismatch where: + +- documentation described a supported deployment option +- preprovision validation accepted and explained the JSON object +- the actual AZD deployment path silently discarded the value + +## Technical Details + +### Files Modified + +- `deployers/bicep/main.parameters.json` +- `deployers/bicep/README.md` +- `functional_tests/test_azd_private_dns_zone_config_wiring.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Replaced the hard-coded empty object in `main.parameters.json` with the `PRIVATE_DNS_ZONE_CONFIGS` + AZD environment placeholder. +- Added README guidance that tells AZD users to provide the private DNS configuration through the + AZD environment value rather than editing the parameter file directly. +- Added a focused functional regression test that verifies the parameter wiring, prerequisite lookup, + documentation guidance, and version update. +- Bumped the application version to `0.240.006`. + +### Testing Approach + +- Added `functional_tests/test_azd_private_dns_zone_config_wiring.py`. +- The test validates that: + - AZD parameter substitution now forwards `PRIVATE_DNS_ZONE_CONFIGS` + - preprovision validation still reads the expected environment values + - deployment documentation mentions the AZD-specific configuration path + - the config version reflects the fix + +## Validation + +### Before + +- AZD deployments always passed `{}` for `privateDnsZoneConfigs` +- private DNS reuse worked in the Bicep template only if parameters were supplied outside the AZD flow +- README guidance for `privateDnsZoneConfigs` did not describe the AZD environment variable path clearly + +### After + +- AZD deployments can pass a JSON object through `PRIVATE_DNS_ZONE_CONFIGS` +- the existing prerequisite validator and Bicep template now align with the actual AZD deployment path +- users have explicit documentation for how to supply the setting during `azd up` + +### Impact Analysis + +This fix is limited to the AZD parameter pass-through path for private networking. + +- deployments that leave `PRIVATE_DNS_ZONE_CONFIGS` empty still default to automatic private DNS zone creation +- deployments that reuse enterprise-managed private DNS zones can now do so through the standard AZD workflow +- no private networking resource behavior changed beyond receiving the intended parameter value \ No newline at end of file diff --git a/functional_tests/test_azd_private_dns_zone_config_wiring.py b/functional_tests/test_azd_private_dns_zone_config_wiring.py new file mode 100644 index 000000000..4d28c0098 --- /dev/null +++ b/functional_tests/test_azd_private_dns_zone_config_wiring.py @@ -0,0 +1,78 @@ +# test_azd_private_dns_zone_config_wiring.py +#!/usr/bin/env python3 +""" +Functional test for AZD private DNS zone config wiring. +Version: 0.240.006 +Implemented in: 0.240.006 + +This test ensures that AZD deployments can pass private DNS zone reuse settings +through the PRIVATE_DNS_ZONE_CONFIGS environment value instead of silently +forcing privateDnsZoneConfigs to an empty object. +""" + +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +CONFIG_PATH = REPO_ROOT / "application" / "single_app" / "config.py" +PARAMETERS_PATH = REPO_ROOT / "deployers" / "bicep" / "main.parameters.json" +PREREQUISITES_PATH = REPO_ROOT / "deployers" / "bicep" / "validate_azd_prerequisites.py" +README_PATH = REPO_ROOT / "deployers" / "bicep" / "README.md" + + +def require_contains(content: str, expected: str, description: str) -> None: + if expected not in content: + raise AssertionError(f"Missing {description}: {expected}") + + +def test_azd_private_dns_zone_configs_are_wired() -> bool: + print("🧪 Testing AZD private DNS zone config parameter wiring") + print("=" * 70) + + parameters_content = PARAMETERS_PATH.read_text(encoding="utf-8") + prerequisites_content = PREREQUISITES_PATH.read_text(encoding="utf-8") + readme_content = README_PATH.read_text(encoding="utf-8") + config_content = CONFIG_PATH.read_text(encoding="utf-8") + + require_contains( + parameters_content, + '"value": "${PRIVATE_DNS_ZONE_CONFIGS}"', + "AZD parameter pass-through for privateDnsZoneConfigs", + ) + require_contains( + prerequisites_content, + "AZURE_ENV_PRIVATE_DNS_ZONE_CONFIGS", + "preprovision AZD environment variable lookup", + ) + require_contains( + prerequisites_content, + "PRIVATE_DNS_ZONE_CONFIGS", + "preprovision local environment variable lookup", + ) + require_contains( + readme_content, + "PRIVATE_DNS_ZONE_CONFIGS", + "README guidance for AZD private DNS zone configuration", + ) + require_contains( + config_content, + 'VERSION = "0.240.006"', + "config version bump", + ) + + print("✅ AZD parameters file passes PRIVATE_DNS_ZONE_CONFIGS into Bicep") + print("✅ Preprovision validation reads PRIVATE_DNS_ZONE_CONFIGS before deployment") + print("✅ README documents the AZD environment variable path") + print("✅ Config version was updated for the fix") + return True + + +if __name__ == "__main__": + try: + success = test_azd_private_dns_zone_configs_are_wired() + except Exception as exc: + print(f"❌ Test failed: {exc}") + raise + + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/functional_tests/test_core_service_key_deployment_path.py b/functional_tests/test_core_service_key_deployment_path.py index c18ede480..94705d976 100644 --- a/functional_tests/test_core_service_key_deployment_path.py +++ b/functional_tests/test_core_service_key_deployment_path.py @@ -1,12 +1,13 @@ # test_core_service_key_deployment_path.py #!/usr/bin/env python3 """ -Functional test for core service key deployment path. -Version: 0.240.003 -Implemented in: 0.240.002 +Functional test for core service key deployment path and OpenAI endpoint-only fallback. +Version: 0.240.005 +Implemented in: 0.240.005 This test ensures that core service key authentication is populated directly from -Azure resources during deployment instead of depending on Key Vault secret reads. +Azure resources during deployment and that reused OpenAI-compatible endpoints fall +back to manual key configuration when automatic retrieval is not available. """ import sys @@ -73,6 +74,32 @@ def test_postconfig_uses_direct_core_key_retrieval(): return True +def test_postconfig_preserves_manual_openai_key_fallback(): + """Verify postconfig skips unsupported OpenAI key auto-retrieval paths.""" + print("🔍 Testing postconfig OpenAI endpoint-only fallback...") + + content = read_text(POSTCONFIG_PATH) + + required_snippets = [ + "STANDARD_AZURE_OPENAI_HOST_SUFFIXES = (", + "def is_standard_azure_openai_endpoint(endpoint):", + "def can_auto_retrieve_openai_key(", + "Skipping Azure OpenAI key retrieval:", + "if can_auto_retrieve_openai_key(", + 'openai_key = core_service_keys.get("azure_openai_key")', + 'item.setdefault("azure_openai_gpt_key", "")', + 'item.setdefault("azure_openai_embedding_key", "")', + ] + + for snippet in required_snippets: + if snippet not in content: + print(f"❌ Missing expected endpoint-only fallback snippet: {snippet}") + return False + + print("✅ Postconfig preserves manual OpenAI key configuration when auto-retrieval is unavailable") + return True + + def test_app_service_uses_direct_core_key_values(): """Verify App Service no longer depends on Key Vault references for core key auth.""" print("🔍 Testing App Service core key wiring...") @@ -140,7 +167,7 @@ def test_version_updated(): print("🔍 Testing config version update...") content = read_text(CONFIG_PATH) - expected_version = 'VERSION = "0.240.003"' + expected_version = 'VERSION = "0.240.005"' if expected_version not in content: print(f"❌ Expected version not found in config.py: {expected_version}") @@ -153,6 +180,7 @@ def test_version_updated(): if __name__ == "__main__": tests = [ test_postconfig_uses_direct_core_key_retrieval, + test_postconfig_preserves_manual_openai_key_fallback, test_app_service_uses_direct_core_key_values, test_core_modules_stop_storing_core_keys_in_key_vault, test_version_updated,