diff --git a/.github/workflows/CAdeploy.yml b/.github/workflows/CAdeploy.yml index 1bcbf08e3..94869636c 100644 --- a/.github/workflows/CAdeploy.yml +++ b/.github/workflows/CAdeploy.yml @@ -141,10 +141,13 @@ jobs: # Install azd (Azure Developer CLI) - required by process_sample_data.sh curl -fsSL https://aka.ms/install-azd.sh | bash + # Generate current timestamp in desired format: YYYY-MM-DDTHH:MM:SS.SSSSSSSZ + current_date=$(date -u +"%Y-%m-%dT%H:%M:%S.%7NZ") + DEPLOY_OUTPUT=$(az deployment group create \ --resource-group ${{ env.RESOURCE_GROUP_NAME }} \ --template-file infra/main.bicep \ - --parameters location=${{ env.AZURE_LOCATION }} azureAiServiceLocation=${{ env.AZURE_LOCATION }} solutionName=${{ env.SOLUTION_PREFIX }} cosmosLocation=westus gptModelCapacity=${{ env.GPT_MIN_CAPACITY }} embeddingDeploymentCapacity=${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} imageTag=${{ env.IMAGE_TAG }} createdBy="Pipeline" \ + --parameters location=${{ env.AZURE_LOCATION }} azureAiServiceLocation=${{ env.AZURE_LOCATION }} solutionName=${{ env.SOLUTION_PREFIX }} cosmosLocation=westus gptModelCapacity=${{ env.GPT_MIN_CAPACITY }} embeddingDeploymentCapacity=${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} imageTag=${{ env.IMAGE_TAG }} createdBy="Pipeline" tags="{'SecurityControl':'Ignore','Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" \ --query "properties.outputs" -o json) echo "Deployment output: $DEPLOY_OUTPUT" diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 10f4737bf..b1ef09810 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -1,39 +1,37 @@ -name: Azure Template Validation -on: - push: - branches: - - main - - dev - workflow_dispatch: - -permissions: - contents: read - id-token: write - pull-requests: write - -jobs: - template_validation_job: +name: Azure Template Validation +on: + workflow_dispatch: + push: + branches: + - main +permissions: + contents: read + id-token: write + pull-requests: write +jobs: + template_validation_job: + runs-on: ubuntu-latest environment: production - runs-on: ubuntu-latest - name: Template validation - - steps: - # Step 1: Checkout the code from your repository - - name: Checkout code - uses: actions/checkout@v4 - - # Step 2: Validate the Azure template using microsoft/template-validation-action - - name: Validate Azure Template - uses: microsoft/template-validation-action@v0.3.5 - id: validation - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Step 3: Print the result of the validation - - name: Print result - run: cat ${{ steps.validation.outputs.resultFile }} + name: Template validation + steps: + # Step 1: Checkout the code from your repository + - name: Checkout code + uses: actions/checkout@v4 + # Step 2: Validate the Azure template using microsoft/template-validation-action + - name: Validate Azure Template + uses: microsoft/template-validation-action@v0.4.2 + id: validation + with: + useDevContainer: false + validateTests: '' + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Step 3: Print the result of the validation + - name: Print result + run: cat ${{ steps.validation.outputs.resultFile }} diff --git a/README.md b/README.md index 839859577..be09669ba 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ Quick deploy ### How to install or deploy Follow the quick deploy steps on the deployment guide to deploy this solution to your own Azure subscription. +> **Note:** This solution accelerator requires **Azure Developer CLI (azd) version 1.18.0 or higher**. Please ensure you have the latest version installed before proceeding with deployment. [Download azd here](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd). + [Click here to launch the deployment guide](./docs/DeploymentGuide.md)

diff --git a/azure.yaml b/azure.yaml index 851de4db5..f8a7f3607 100644 --- a/azure.yaml +++ b/azure.yaml @@ -3,7 +3,7 @@ name: build-your-own-copilot-solution-accelerator requiredVersions: - azd: ">= 1.15.0" + azd: ">= 1.18.0" metadata: template: build-your-own-copilot-solution-accelerator@1.0 diff --git a/docs/ACRBuildAndPushGuide.md b/docs/ACRBuildAndPushGuide.md new file mode 100644 index 000000000..f75b32d6c --- /dev/null +++ b/docs/ACRBuildAndPushGuide.md @@ -0,0 +1,94 @@ +# Azure Container Registry (ACR) – Build & Push Guide + +This guide provides step-by-step instructions to build and push Docker images for **WebApp** and **Backend** services into Azure Container Registry (ACR). + +## 📋 Prerequisites +Before starting, ensure you have: +- An active [Azure Subscription](https://portal.azure.com/) +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) installed and logged in +- [Docker Desktop](https://docs.docker.com/get-docker/) installed and running +- Access to your Azure Container Registry (ACR) +- To create an Azure Container Registry (ACR), you can refer to the following guides: + + - [Create Container Registry using Azure CLI](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-get-started-azure-cli) + + - [Create Container Registry using Azure Portal](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-get-started-portal?tabs=azure-cli) + + - [Create Container Registry using PowerShell](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-get-started-powershell) + + - [Create Container Registry using ARM Template](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-get-started-geo-replication-template) + + - [Create Container Registry using Bicep](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-get-started-bicep?tabs=CLI) + +--- + +Login to ACR : +``` bash +az acr login --name $ACR_NAME +``` + +## 🚀 Build and Push Images + +**Backend :** + + ```bash +az acr login --name +docker build --no-cache -f docker/Backend.Dockerfile -t /: . +docker push /: + ``` + + If you want to update image tag and image manually you can follow below steps: +- Go to your **Container App** in the [Azure Portal](https://portal.azure.com/#home). +- In the left menu, select **Containers**. +- Under your container, update: + + - Image source → Azure Container Registry / Docker Hub. + + - Image name → myapp/backend. + + - Tag → change to the new one you pushed (e.g., v2). + +- Click **Save** → this will create a new revision automatically with the updated image. + +![alt text](./images/ContainerApp.png) + +**WebApp :** + +```bash +az acr login --name +docker build --no-cache -f docker/Frontend.Dockerfile -t /: . +docker push /: +``` + +If you want to update image tag and image manually you can follow below steps: +- Go to your App Service in the [Azure Portal](https://portal.azure.com/#home). +- In the left menu, select Deployment → Deployment Center +- Under Container settings, you can configure: + + - Image Source → (e.g., Azure Container Registry / Docker Hub / Other). + + - Image Name → e.g., myapp/backend. + + - Tag → e.g., v1.2.3. + +![alt text](./images/AppServiceContainer.png) + + +## ✅ Verification + +Run the following command to verify that images were pushed successfully: +```bash +az acr repository list --name $ACR_NAME --output table +``` + +You should see repositories in the output. + +## 📝 Notes + +- Always use meaningful tags (v1.0.0, staging, prod) instead of just latest. + +- If you are pushing from a CI/CD pipeline, make sure the pipeline agent has access to Docker and ACR. + +- For private images, ensure your services (e.g., Azure Container Apps, AKS, App Service) are configured with appropriate ACR pull permissions. + + diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 449db6bfe..496f00523 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -111,7 +111,7 @@ If you're not using one of the above options for opening the project, then you'l 1. Make sure the following tools are installed: - [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.5) (v7.0+) - available for Windows, macOS, and Linux. - - [Azure Developer CLI (azd)](https://aka.ms/install-azd) (v1.15.0+) - version + - [Azure Developer CLI (azd)](https://aka.ms/install-azd) (v1.18.0+) - version - [Python 3.9 to 3.11](https://www.python.org/downloads/) - [Docker Desktop](https://www.docker.com/products/docker-desktop/) - [Git](https://git-scm.com/downloads) @@ -213,6 +213,7 @@ Once you've opened the project in [Codespaces](#github-codespaces), [Dev Contain ```shell azd up ``` + > **Note:** This solution accelerator requires **Azure Developer CLI (azd) version 1.18.0 or higher**. Please ensure you have the latest version installed before proceeding with deployment. [Download azd here](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd). 3. Provide an `azd` environment name (e.g., "byocaapp"). 4. Select a subscription from your Azure account and choose a location that has quota for all the resources. diff --git a/docs/TroubleShootingSteps.md b/docs/TroubleShootingSteps.md index 89570275e..6ef587600 100644 --- a/docs/TroubleShootingSteps.md +++ b/docs/TroubleShootingSteps.md @@ -1,4 +1,3 @@ - # 🛠️ Troubleshooting When deploying Azure resources, you may come across different error codes that stop or delay the deployment process. This section lists some of the most common errors along with possible causes and step-by-step resolutions. @@ -103,13 +102,13 @@ based on available quota you can deploy application otherwise, you can request f
-DeploymentModelNotSupported +DeploymentModelNotSupported/ ServiceModelDeprecated/ InvalidResourceProperties - The updated model may not be supported in the selected region. Please verify its availability in the [Azure AI Foundry models](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/models?tabs=global-standard%2Cstandard-chat-completions) document.
-LinkedInvalidPropertyId/ ResourceNotFound/DeploymentOutputEvaluationFailed/ CanNotRestoreANonExistingResource +LinkedInvalidPropertyId/ ResourceNotFound/DeploymentOutputEvaluationFailed/ CanNotRestoreANonExistingResource / The language expression property array index is out of bounds - Before using any resource ID, ensure it follows the correct format. - Verify that the resource ID you are passing actually exists. @@ -128,6 +127,8 @@ based on available quota you can deploy application otherwise, you can request f ``` /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/workspaces/{name} ``` +- You may encounter the error `The language expression property array index '8' is out of bounds` if the resource ID is incomplete. Please ensure your resource ID is correct and contains all required information, as shown in sample resource IDs. + - For more information refer [Resource Not Found errors solutions](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-not-found?tabs=bicep)
@@ -318,10 +319,10 @@ The subscription 'xxxx-xxxx' cannot have more than 1 Container App Environments
ResourceProviderError - -- This error occurs when the resource provider is not registered in your subscription. -- To register it, refer to [Register Resource Provider](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types) documentation. - + +- This error occurs when the resource provider is not registered in your subscription. +- To register it, refer to [Register Resource Provider](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-register-resource-provider?tabs=azure-cli) documentation. +
Conflict - Cannot use the SKU Basic with File Change Audit for site. @@ -351,6 +352,228 @@ The subscription 'xxxx-xxxx' cannot have more than 1 Container App Environments
+
InvalidRequestContent + +- The deployment values either include values that aren't recognized, or required values are missing. Confirm the values for your resource type. +- You can refer [Invalid Request Content error](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors#:~:text=InvalidRequestContent,Template%20reference) documentation. + +
+ +
ReadOnlyDisabledSubscription + +- Depending on the type of the Azure Subscription, the expiration date might have been reached. + +- You have to activate the Azure Subscription before creating any Azure resource. +- You can refer [Reactivate a disabled Azure subscription](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/subscription-disabled) Documentation. + +
+ + +
SkuNotAvailable + +- You receive this error in the following scenarios: + - When the resource SKU you've selected, such as VM size, isn't available for a location or zone. + - If you're deploying an Azure Spot VM or Spot scale set instance, and there isn't any capacity for Azure Spot in this location. For more information, see Spot error messages. +
+ +
CrossTenantDeploymentNotPermitted + +- Check tenant match: Ensure your deployment identity (user/SP) and the target resource group are in the same tenant. + ``` + az account show + az group show --name + ``` + +- Verify pipeline/service principal: If using CI/CD, confirm the service principal belongs to the same tenant and has permissions on the resource group. + +- Avoid cross-tenant references: Make sure your Bicep doesn’t reference subscriptions, resource groups, or resources in another tenant. + +- Test minimal deployment: Deploy a simple resource to the same resource group to confirm identity and tenant are correct. + +- Guest/external accounts: Avoid using guest users from other tenants; use native accounts or SPs in the tenant. + +
+ +
RequestDisallowedByPolicy + +- This typically indicates that an Azure Policy is preventing the requested action due to policy restrictions in your subscription. + +- For more details and guidance on resolving this issue, please refer to the official Microsoft documentation: [RequestDisallowedByPolicy](https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/create-upgrade-delete/error-code-requestdisallowedbypolicy) + +
+ +
+FlagMustBeSetForRestore/NameUnavailable/CustomDomainInUse + +- This error occurs when you try to deploy a Cognitive Services resource that was **soft-deleted** earlier. +- Azure requires you to explicitly set the **`restore` flag** to `true` if you want to recover the soft-deleted resource. +- If you don’t want to restore the resource, you must **purge the deleted resource** first before redeploying. +Example causes: +- Trying to redeploy a Cognitive Services account with the same name as a previously deleted one. +- The deleted resource still exists in a **soft-delete retention state**. +**How to fix:** +1. If you want to restore → add `"restore": true` in your template properties. +2. If you want a fresh deployment → purge the resource using: + ```bash + az cognitiveservices account purge \ + --name \ + --resource-group \ + --location + ``` +For more details, refer to [Soft delete and resource restore](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/delete-resource-group?tabs=azure-powershell). +
+ +
+PrincipalNotFound + +- This error occurs when the **principal ID** (Service Principal, User, or Group) specified in a role assignment or deployment does not exist in the Azure Active Directory tenant. +- It can also happen due to **replication delays** right after creating a new principal. +**Example causes:** +- The specified **Object ID** is invalid or belongs to another tenant. +- The principal was recently created but Azure AD has not yet replicated it. +- Attempting to assign a role to a non-existing or deleted Service Principal/User/Group. +**How to fix:** +1. Verify that the **principal ID is correct** and exists in the same directory/tenant. + ```bash + az ad sp show --id + ``` +2. If the principal was just created, wait a few minutes and retry. +3. Explicitly set the principalType property (ServicePrincipal, User, or Group) in your ARM/Bicep template to avoid replication delays. +4. If the principal does not exist, create it again before assigning roles. +For more details, see [Azure PrincipalType documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/troubleshooting?tabs=bicep) +
+
+RedundancyConfigurationNotAvailableInRegion + +- This issue happens when you try to create a **Storage Account** with a redundancy configuration (e.g., `Standard_GRS`) that is **not supported in the selected Azure region**. +- Example: Creating a storage account with **GRS** in **italynorth** will fail with this error. +```bash +az storage account create -n mystorageacct123 -g myResourceGroup -l italynorth --sku Standard_GRS --kind StorageV2 +``` +- To check supported SKUs for your region: +```bash +az storage account list-skus -l italynorth -o table +``` +Use a supported redundancy option (e.g., Standard_LRS) in the same region +Or deploy the Storage Account in a region that supports your chosen redundancy. +For more details, refer to [Azure Storage redundancy documentation](https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy?utm_source=chatgpt.com). +
+ +
DeploymentNotFound + +- This issue occurs when the user deletes a previous deployment along with the resource group (RG), and then redeploys the same RG with the same environment name but in a different location. + +- To avoid the DeploymentNotFound error, Do not change the location when redeploying a deleted RG, or Use new names for the RG and environment during redeployment. +
+ +
DeploymentCanceled(user.canceled) + +- Indicates the deployment was manually canceled by the user (Portal, CLI, or pipeline). + +- Check deployment history and logs to confirm who/when it was canceled. + +- If accidental, retry the deployment. + +- For pipelines, ensure no automation or timeout is triggering cancellation. + +- Use deployment locks or retry logic to prevent accidental cancellations. + +
+ +
ResourceGroupDeletionTimeout + +- Some resources in the resource group may be stuck deleting or have dependencies; check RG resources and status. + +- Ensure no resource locks or Azure Policies are blocking deletion. + +- Retry deletion via CLI/PowerShell `(az group delete --name --yes --no-wait)`. + +- Check Activity Log to identify failing resources; escalate to Azure Support if deletion is stuck. + +
+ +
+SubscriptionDoesNotHaveServer + +- This issue happens when you try to reference an **Azure SQL Server** (`Microsoft.Sql/servers`) that does not exist in the selected subscription. +- It can occur if: + - The SQL server name is typed incorrectly. + - The SQL server was **deleted** but is still being referenced. + - You are working in the **wrong subscription context**. + - The server exists in a **different subscription/tenant** where you don’t have access. + +**Reproduce:** +1. Run an Azure CLI command with a non-existent server name: +```bash + az sql db list --server sql-doesnotexist --resource-group myResourceGroup +``` + + or + +```bash + az sql server show --name sql-caqfrhxr4i3hyj --resource-group myResourceGroup + +``` + +Resolution: + +Verify the SQL Server name exists in your subscription: + +```bash + az sql server list --output table +``` +Make sure you are targeting the correct subscription: + +```bash + az account show + az account set --subscription +``` +If the server was deleted, either restore it (if possible) or update references to use a valid existing server. + +
+ + +
BadRequest - DatabaseAccount is in a failed provisioning state because the previous attempt to create it was not successful + +- This error occurs when a user attempts to redeploy a resource that previously failed to provision. + +- To resolve the issue, delete the failed deployment first, then start a new deployment. + +- For guidance on deleting a resource from a Resource Group, refer to the following link: [Delete an Azure Cosmos DB account](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/manage-with-powershell#delete-account:~:text=%3A%24enableMultiMaster-,Delete%20an%20Azure%20Cosmos%20DB%20account,-This%20command%20deletes) + +
+ +
+ +SpecialFeatureOrQuotaIdRequired + +This error occurs when your subscription does not have access to certain Azure OpenAI models. + +**Example error message:** +`SpecialFeatureOrQuotaIdRequired: The current subscription does not have access to this model 'Format:OpenAI,Name:o3,Version:2025-04-16'.` + +**Resolution:** +To gain access, submit a request using the official form: +👉 [Azure OpenAI Model Access Request](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUQ1VGQUEzRlBIMVU2UFlHSFpSNkpOR0paRSQlQCN0PWcu) + +You’ll need to use this form if you require access to the following restricted models: +- gpt-5 +- o3 +- o3-pro +- deep research +- reasoning summary +- gpt-image-1 + +Once your request is approved, redeploy your resource. + +
+ +
+ContainerAppOperationError + +- The error is likely due to an improperly built container image. For resolution steps, refer to the [Azure Container Registry (ACR) – Build & Push Guide](./ACRBuildAndPushGuide.md) + +
💡 Note: If you encounter any other issues, you can refer to the [Common Deployment Errors](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors) documentation. If the problem persists, you can also raise an bug in our [BYOC-Client Advisor Github Issues](https://github.com/microsoft/Build-your-own-copilot-Solution-Accelerator/issues) for further support. diff --git a/docs/images/AppServiceContainer.png b/docs/images/AppServiceContainer.png new file mode 100644 index 000000000..3786259ae Binary files /dev/null and b/docs/images/AppServiceContainer.png differ diff --git a/docs/images/ContainerApp.png b/docs/images/ContainerApp.png new file mode 100644 index 000000000..bdb99fd3d Binary files /dev/null and b/docs/images/ContainerApp.png differ diff --git a/infra/main.bicep b/infra/main.bicep index 578156a8f..b6e9df9c2 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -53,9 +53,6 @@ param embeddingModel string = 'text-embedding-ada-002' @description('Optional. Capacity of the Embedding Model deployment') param embeddingDeploymentCapacity int = 80 -// @description('Fabric Workspace Id if you have one, else leave it empty. ') -// param fabricWorkspaceId string - //restricting to these regions because assistants api for gpt-4o-mini is available only in these regions @allowed([ 'australiaeast' @@ -68,8 +65,6 @@ param embeddingDeploymentCapacity int = 80 'westus' 'westus3' ]) -// @description('Azure OpenAI Location') -// param AzureOpenAILocation string = 'eastus2' @metadata({ azd: { type: 'location' @@ -174,12 +169,10 @@ param vmSize string? @description('Optional. Admin username for the Jumpbox Virtual Machine. Set to custom value if enablePrivateNetworking is true.') @secure() -//param vmAdminUsername string = take(newGuid(), 20) param vmAdminUsername string? @description('Optional. Admin password for the Jumpbox Virtual Machine. Set to custom value if enablePrivateNetworking is true.') @secure() -//param vmAdminPassword string = newGuid() param vmAdminPassword string? var functionAppSqlPrompt = '''Generate a valid T-SQL query to find {query} for tables and columns provided below: @@ -264,19 +257,11 @@ var cosmosDbHaLocation = cosmosDbZoneRedundantHaRegionPairs[resourceGroup().loca // Extracts subscription, resource group, and workspace name from the resource ID when using an existing Log Analytics workspace var useExistingLogAnalytics = !empty(existingLogAnalyticsWorkspaceId) -var existingLawSubscription = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[2] : '' -var existingLawResourceGroup = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[4] : '' -var existingLawName = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[8] : '' - -resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-08-01' existing = if (useExistingLogAnalytics) { - name: existingLawName - scope: resourceGroup(existingLawSubscription, existingLawResourceGroup) -} var logAnalyticsWorkspaceResourceId = useExistingLogAnalytics ? existingLogAnalyticsWorkspaceId : logAnalyticsWorkspace!.outputs.resourceId -@description('Optional created by user name') -param createdBy string = empty(deployer().userPrincipalName) ? '' : split(deployer().userPrincipalName, '@')[0] +@description('Tag, Created by user name') +param createdBy string = contains(deployer(), 'userPrincipalName')? split(deployer().userPrincipalName, '@')[0]: deployer().objectId // ========== Resource Group Tag ========== // resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { @@ -285,6 +270,7 @@ resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { tags: { ...tags TemplateName: 'Client Advisor' + Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' CreatedBy: createdBy } } @@ -387,6 +373,19 @@ module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-id } } +// ========== SQL Operations User Assigned Identity ========== // +// Dedicated identity for backend SQL operations with limited permissions (db_datareader, db_datawriter) +var sqlUserAssignedIdentityResourceName = 'id-sql-${solutionSuffix}' +module sqlUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = { + name: take('avm.res.managed-identity.user-assigned-identity.${sqlUserAssignedIdentityResourceName}', 64) + params: { + name: sqlUserAssignedIdentityResourceName + location: solutionLocation + tags: tags + enableTelemetry: enableTelemetry + } +} + // ========== Network Module ========== // module network 'modules/network.bicep' = if (enablePrivateNetworking) { name: take('network-${solutionSuffix}-deployment', 64) @@ -449,7 +448,7 @@ var aiRelatedDnsZoneIndices = [ @batchSize(5) module avmPrivateDnsZones 'br/public:avm/res/network/private-dns-zone:0.7.1' = [ for (zone, i) in privateDnsZones: if (enablePrivateNetworking && (empty(existingFoundryProjectResourceId) || !contains(aiRelatedDnsZoneIndices, i))) { - name: 'dns-zone-${i}' + name: 'avm.res.network.private-dns-zone.${split(zone, '.')[1]}' params: { name: zone tags: tags @@ -508,6 +507,11 @@ module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = { principalType: 'ServicePrincipal' roleDefinitionIdOrName: 'Key Vault Administrator' } + { + principalId: sqlUserAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'Key Vault Secrets User' + } ] secrets: [ { @@ -895,7 +899,11 @@ module sqlDBModule 'br/public:avm/res/sql/server:0.20.1' = { connectionPolicy: 'Redirect' databases: [ { - availabilityZone: enableRedundancy ? 1 : -1 + zoneRedundant: enableRedundancy + // When enableRedundancy is true (zoneRedundant=true), set availabilityZone to -1 + // to let Azure automatically manage zone placement across multiple zones. + // When enableRedundancy is false, also use -1 (no specific zone assignment). + availabilityZone: -1 collation: 'SQL_Latin1_General_CP1_CI_AS' diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] @@ -987,7 +995,7 @@ module webSite 'modules/web-sites.bicep' = { name: webSiteResourceName tags: tags location: solutionLocation - managedIdentities: { userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId] } + managedIdentities: { userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId, sqlUserAssignedIdentity!.outputs.resourceId] } kind: 'app,linux,container' serverFarmResourceId: webServerFarm.?outputs.resourceId siteConfig: { @@ -1034,7 +1042,7 @@ module webSite 'modules/web-sites.bicep' = { AZURE_COSMOSDB_CONVERSATIONS_CONTAINER: collectionName AZURE_COSMOSDB_DATABASE: cosmosDbDatabaseName AZURE_COSMOSDB_ENABLE_FEEDBACK: azureCosmosDbEnableFeedback - SQLDB_USER_MID: userAssignedIdentity.outputs.clientId + SQLDB_USER_MID: sqlUserAssignedIdentity.outputs.clientId AZURE_AI_SEARCH_ENDPOINT: 'https://${aiSearchName}.search.windows.net' AZURE_SQL_SYSTEM_PROMPT: functionAppSqlPrompt AZURE_CALL_TRANSCRIPT_SYSTEM_PROMPT: functionAppCallTranscriptSystemPrompt @@ -1225,6 +1233,12 @@ output MANAGEDIDENTITY_WEBAPP_NAME string = userAssignedIdentity.outputs.name @description('Client ID of the managed identity used by the web app.') output MANAGEDIDENTITY_WEBAPP_CLIENTID string = userAssignedIdentity.outputs.clientId + +@description('Name of the managed identity used for SQL database operations.') +output MANAGEDIDENTITY_SQL_NAME string = sqlUserAssignedIdentity.outputs.name + +@description('Client ID of the managed identity used for SQL database operations.') +output MANAGEDIDENTITY_SQL_CLIENTID string = sqlUserAssignedIdentity.outputs.clientId @description('Name of the AI Search service.') output AI_SEARCH_SERVICE_NAME string = aiSearchName @@ -1366,3 +1380,6 @@ output USE_AI_PROJECT_CLIENT string = useAIProjectClientFlag @description('Indicates whether the internal stream should be used.') output USE_INTERNAL_STREAM string = useInternalStream + +@description('The client ID of the managed identity.') +output AZURE_CLIENT_ID string = userAssignedIdentity.outputs.clientId diff --git a/infra/main.json b/infra/main.json index 7976c4482..d3dc9ca02 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.37.4.10188", - "templateHash": "14524300692672359611" + "templateHash": "7078592419671147745" } }, "parameters": { @@ -258,9 +258,9 @@ }, "createdBy": { "type": "string", - "defaultValue": "[if(empty(deployer().userPrincipalName), '', split(deployer().userPrincipalName, '@')[0])]", + "defaultValue": "[if(contains(deployer(), 'userPrincipalName'), split(deployer().userPrincipalName, '@')[0], deployer().objectId)]", "metadata": { - "description": "Optional created by user name" + "description": "Tag, Created by user name" } } }, @@ -322,12 +322,10 @@ "allTags": "[union(createObject('azd-env-name', parameters('solutionName')), parameters('tags'))]", "cosmosDbHaLocation": "[variables('cosmosDbZoneRedundantHaRegionPairs')[resourceGroup().location]]", "useExistingLogAnalytics": "[not(empty(parameters('existingLogAnalyticsWorkspaceId')))]", - "existingLawSubscription": "[if(variables('useExistingLogAnalytics'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[2], '')]", - "existingLawResourceGroup": "[if(variables('useExistingLogAnalytics'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[4], '')]", - "existingLawName": "[if(variables('useExistingLogAnalytics'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[8], '')]", "logAnalyticsWorkspaceResourceName": "[format('log-{0}', variables('solutionSuffix'))]", "applicationInsightsResourceName": "[format('appi-{0}', variables('solutionSuffix'))]", "userAssignedIdentityResourceName": "[format('id-{0}', variables('solutionSuffix'))]", + "sqlUserAssignedIdentityResourceName": "[format('id-sql-{0}', variables('solutionSuffix'))]", "privateDnsZones": [ "privatelink.cognitiveservices.azure.com", "privatelink.openai.azure.com", @@ -396,21 +394,12 @@ "aiSearchName": "[format('srch-{0}', variables('solutionSuffix'))]" }, "resources": { - "existingLogAnalyticsWorkspace": { - "condition": "[variables('useExistingLogAnalytics')]", - "existing": true, - "type": "Microsoft.OperationalInsights/workspaces", - "apiVersion": "2020-08-01", - "subscriptionId": "[variables('existingLawSubscription')]", - "resourceGroup": "[variables('existingLawResourceGroup')]", - "name": "[variables('existingLawName')]" - }, "resourceGroupTags": { "type": "Microsoft.Resources/tags", "apiVersion": "2021-04-01", "name": "default", "properties": { - "tags": "[shallowMerge(createArray(parameters('tags'), createObject('TemplateName', 'Client Advisor', 'CreatedBy', parameters('createdBy'))))]" + "tags": "[shallowMerge(createArray(parameters('tags'), createObject('TemplateName', 'Client Advisor', 'Type', if(parameters('enablePrivateNetworking'), 'WAF', 'Non-WAF'), 'CreatedBy', parameters('createdBy'))))]" } }, "existingAiFoundryAiServices": { @@ -4784,6 +4773,488 @@ } } }, + "sqlUserAssignedIdentity": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[take(format('avm.res.managed-identity.user-assigned-identity.{0}', variables('sqlUserAssignedIdentityResourceName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('sqlUserAssignedIdentityResourceName')]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + } + }, + "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.34.44.8038", + "templateHash": "16707109626832623586" + }, + "name": "User Assigned Identities", + "description": "This module deploys a User Assigned Identity." + }, + "definitions": { + "federatedIdentityCredentialType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the federated identity credential." + } + }, + "audiences": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The list of audiences that can appear in the issued token." + } + }, + "issuer": { + "type": "string", + "metadata": { + "description": "Required. The URL of the issuer to be trusted." + } + }, + "subject": { + "type": "string", + "metadata": { + "description": "Required. The identifier of the external identity." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the federated identity credential." + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the User Assigned Identity." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "federatedIdentityCredentials": { + "type": "array", + "items": { + "$ref": "#/definitions/federatedIdentityCredentialType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The federated identity credentials list to indicate which token from the external IdP should be trusted by your application. Federated identity credentials are supported on applications only. A maximum of 20 federated identity credentials can be added per application object." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Managed Identity Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e40ec5ca-96e0-45a2-b4ff-59039f2c2b59')]", + "Managed Identity Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f1a07417-d97a-45cb-824c-7a7467783830')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.managedidentity-userassignedidentity.{0}.{1}', replace('0.4.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "userAssignedIdentity": { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + "userAssignedIdentity_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.ManagedIdentity/userAssignedIdentities/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "userAssignedIdentity_roleAssignments": { + "copy": { + "name": "userAssignedIdentity_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.ManagedIdentity/userAssignedIdentities/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "userAssignedIdentity_federatedIdentityCredentials": { + "copy": { + "name": "userAssignedIdentity_federatedIdentityCredentials", + "count": "[length(coalesce(parameters('federatedIdentityCredentials'), createArray()))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-UserMSI-FederatedIdentityCred-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('federatedIdentityCredentials'), createArray())[copyIndex()].name]" + }, + "userAssignedIdentityName": { + "value": "[parameters('name')]" + }, + "audiences": { + "value": "[coalesce(parameters('federatedIdentityCredentials'), createArray())[copyIndex()].audiences]" + }, + "issuer": { + "value": "[coalesce(parameters('federatedIdentityCredentials'), createArray())[copyIndex()].issuer]" + }, + "subject": { + "value": "[coalesce(parameters('federatedIdentityCredentials'), createArray())[copyIndex()].subject]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13656021764446440473" + }, + "name": "User Assigned Identity Federated Identity Credential", + "description": "This module deploys a User Assigned Identity Federated Identity Credential." + }, + "parameters": { + "userAssignedIdentityName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent user assigned identity. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret." + } + }, + "audiences": { + "type": "array", + "metadata": { + "description": "Required. The list of audiences that can appear in the issued token. Should be set to api://AzureADTokenExchange for Azure AD. It says what Microsoft identity platform should accept in the aud claim in the incoming token. This value represents Azure AD in your external identity provider and has no fixed value across identity providers - you might need to create a new application registration in your IdP to serve as the audience of this token." + } + }, + "issuer": { + "type": "string", + "metadata": { + "description": "Required. The URL of the issuer to be trusted. Must match the issuer claim of the external token being exchanged." + } + }, + "subject": { + "type": "string", + "metadata": { + "description": "Required. The identifier of the external software workload within the external identity provider. Like the audience value, it has no fixed format, as each IdP uses their own - sometimes a GUID, sometimes a colon delimited identifier, sometimes arbitrary strings. The value here must match the sub claim within the token presented to Azure AD." + } + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials", + "apiVersion": "2024-11-30", + "name": "[format('{0}/{1}', parameters('userAssignedIdentityName'), parameters('name'))]", + "properties": { + "audiences": "[parameters('audiences')]", + "issuer": "[parameters('issuer')]", + "subject": "[parameters('subject')]" + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the federated identity credential." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the federated identity credential." + }, + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials', parameters('userAssignedIdentityName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the federated identity credential was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the user assigned identity." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the user assigned identity." + }, + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" + }, + "principalId": { + "type": "string", + "metadata": { + "description": "The principal ID (object ID) of the user assigned identity." + }, + "value": "[reference('userAssignedIdentity').principalId]" + }, + "clientId": { + "type": "string", + "metadata": { + "description": "The client ID (application ID) of the user assigned identity." + }, + "value": "[reference('userAssignedIdentity').clientId]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the user assigned identity was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('userAssignedIdentity', '2024-11-30', 'full').location]" + } + } + } + } + }, "network": { "condition": "[parameters('enablePrivateNetworking')]", "type": "Microsoft.Resources/deployments", @@ -20533,7 +21004,7 @@ "condition": "[and(parameters('enablePrivateNetworking'), or(empty(parameters('existingFoundryProjectResourceId')), not(contains(variables('aiRelatedDnsZoneIndices'), copyIndex()))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", - "name": "[format('dns-zone-{0}', copyIndex())]", + "name": "[format('avm.res.network.private-dns-zone.{0}', split(variables('privateDnsZones')[copyIndex()], '.')[1])]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -23754,6 +24225,11 @@ "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", "principalType": "ServicePrincipal", "roleDefinitionIdOrName": "Key Vault Administrator" + }, + { + "principalId": "[reference('sqlUserAssignedIdentity').outputs.principalId.value]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "Key Vault Secrets User" } ] }, @@ -26915,6 +27391,7 @@ "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').keyVault)]", "logAnalyticsWorkspace", "network", + "sqlUserAssignedIdentity", "userAssignedIdentity" ] }, @@ -32702,9 +33179,9 @@ } }, "dependsOn": [ - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "logAnalyticsWorkspace", "network", "userAssignedIdentity" @@ -42311,8 +42788,8 @@ } }, "dependsOn": [ - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", "keyvault", "network", "userAssignedIdentity" @@ -45518,7 +45995,8 @@ "databases": { "value": [ { - "availabilityZone": "[if(parameters('enableRedundancy'), 1, -1)]", + "zoneRedundant": "[parameters('enableRedundancy')]", + "availabilityZone": -1, "collation": "SQL_Latin1_General_CP1_CI_AS", "diagnosticSettings": "[if(parameters('enableMonitoring'), createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), reference('logAnalyticsWorkspace').outputs.resourceId.value))), null())]", "licenseType": "LicenseIncluded", @@ -52724,7 +53202,8 @@ "managedIdentities": { "value": { "userAssignedResourceIds": [ - "[reference('userAssignedIdentity').outputs.resourceId.value]" + "[reference('userAssignedIdentity').outputs.resourceId.value]", + "[reference('sqlUserAssignedIdentity').outputs.resourceId.value]" ] } }, @@ -52781,7 +53260,7 @@ "AZURE_COSMOSDB_CONVERSATIONS_CONTAINER": "[variables('collectionName')]", "AZURE_COSMOSDB_DATABASE": "[variables('cosmosDbDatabaseName')]", "AZURE_COSMOSDB_ENABLE_FEEDBACK": "[variables('azureCosmosDbEnableFeedback')]", - "SQLDB_USER_MID": "[reference('userAssignedIdentity').outputs.clientId.value]", + "SQLDB_USER_MID": "[reference('sqlUserAssignedIdentity').outputs.clientId.value]", "AZURE_AI_SEARCH_ENDPOINT": "[format('https://{0}.search.windows.net', variables('aiSearchName'))]", "AZURE_SQL_SYSTEM_PROMPT": "[variables('functionAppSqlPrompt')]", "AZURE_CALL_TRANSCRIPT_SYSTEM_PROMPT": "[variables('functionAppCallTranscriptSystemPrompt')]", @@ -54790,6 +55269,7 @@ "cosmosDb", "logAnalyticsWorkspace", "network", + "sqlUserAssignedIdentity", "userAssignedIdentity", "webServerFarm" ] @@ -57434,6 +57914,20 @@ }, "value": "[reference('userAssignedIdentity').outputs.clientId.value]" }, + "MANAGEDIDENTITY_SQL_NAME": { + "type": "string", + "metadata": { + "description": "Name of the managed identity used for SQL database operations." + }, + "value": "[reference('sqlUserAssignedIdentity').outputs.name.value]" + }, + "MANAGEDIDENTITY_SQL_CLIENTID": { + "type": "string", + "metadata": { + "description": "Client ID of the managed identity used for SQL database operations." + }, + "value": "[reference('sqlUserAssignedIdentity').outputs.clientId.value]" + }, "AI_SEARCH_SERVICE_NAME": { "type": "string", "metadata": { @@ -57762,6 +58256,13 @@ "description": "Indicates whether the internal stream should be used." }, "value": "[variables('useInternalStream')]" + }, + "AZURE_CLIENT_ID": { + "type": "string", + "metadata": { + "description": "The client ID of the managed identity." + }, + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" } } } \ No newline at end of file diff --git a/infra/modules/network.bicep b/infra/modules/network.bicep index 7a69101fd..ace0139f1 100644 --- a/infra/modules/network.bicep +++ b/infra/modules/network.bicep @@ -63,7 +63,7 @@ param vmSize string // - Document subnet usage and purpose in code comments. // - For AVM modules, ensure only one delegation per subnet and leave delegations empty if not required. -module network 'network/main.bicep' = { +module network 'network/network-resources.bicep' = { name: take('network-${resourcesName}-create', 64) params: { resourcesName: resourcesName diff --git a/infra/modules/network/main.bicep b/infra/modules/network/network-resources.bicep similarity index 100% rename from infra/modules/network/main.bicep rename to infra/modules/network/network-resources.bicep diff --git a/infra/scripts/process_sample_data.sh b/infra/scripts/process_sample_data.sh index c0b9f8a34..b92f6e931 100644 --- a/infra/scripts/process_sample_data.sh +++ b/infra/scripts/process_sample_data.sh @@ -8,8 +8,8 @@ keyvaultName="$5" sqlServerName="$6" SqlDatabaseName="$7" - webAppManagedIdentityClientId="$8" - webAppManagedIdentityDisplayName="$9" + sqlManagedIdentityClientId="$8" + sqlManagedIdentityDisplayName="$9" aiSearchName="${10}" aif_resource_id="${11}" @@ -81,10 +81,13 @@ aif_resource_name=$(basename "$aif_account_resource_id") # Extract resource group from the AI Foundry account resource ID aif_resource_group=$(echo "$aif_account_resource_id" | sed -n 's|.*/resourceGroups/\([^/]*\)/.*|\1|p') - + # Extract subscription ID from the AI Foundry account resource ID + aif_subscription_id=$(echo "$aif_account_resource_id" | sed -n 's|.*/subscriptions/\([^/]*\)/.*|\1|p') + original_foundry_public_access=$(az cognitiveservices account show \ --name "$aif_resource_name" \ --resource-group "$aif_resource_group" \ + --subscription "$aif_subscription_id" \ --query "properties.publicNetworkAccess" \ --output tsv) if [ -z "$original_foundry_public_access" ] || [ "$original_foundry_public_access" = "null" ]; then @@ -316,12 +319,14 @@ SqlDatabaseName=$(azd env get-value SQLDB_DATABASE) fi - if [ -z "$webAppManagedIdentityClientId" ]; then - webAppManagedIdentityClientId=$(azd env get-value MANAGEDIDENTITY_WEBAPP_CLIENTID) + if [ -z "$sqlManagedIdentityClientId" ]; then + # Use the SQL-specific managed identity for database operations with limited permissions + sqlManagedIdentityClientId=$(azd env get-value MANAGEDIDENTITY_SQL_CLIENTID) fi - if [ -z "$webAppManagedIdentityDisplayName" ]; then - webAppManagedIdentityDisplayName=$(azd env get-value MANAGEDIDENTITY_WEBAPP_NAME) + if [ -z "$sqlManagedIdentityDisplayName" ]; then + # Use the SQL-specific managed identity for database operations with limited permissions + sqlManagedIdentityDisplayName=$(azd env get-value MANAGEDIDENTITY_SQL_NAME) fi if [ -z "$aiSearchName" ]; then @@ -335,8 +340,8 @@ azSubscriptionId=$(azd env get-value AZURE_SUBSCRIPTION_ID) # Check if all required arguments are provided - if [ -z "$resourceGroupName" ] || [ -z "$cosmosDbAccountName" ] || [ -z "$storageAccount" ] || [ -z "$fileSystem" ] || [ -z "$keyvaultName" ] || [ -z "$sqlServerName" ] || [ -z "$SqlDatabaseName" ] || [ -z "$webAppManagedIdentityClientId" ] || [ -z "$webAppManagedIdentityDisplayName" ] || [ -z "$aiSearchName" ] || [ -z "$aif_resource_id" ]; then - echo "Usage: $0 " + if [ -z "$resourceGroupName" ] || [ -z "$cosmosDbAccountName" ] || [ -z "$storageAccount" ] || [ -z "$fileSystem" ] || [ -z "$keyvaultName" ] || [ -z "$sqlServerName" ] || [ -z "$SqlDatabaseName" ] || [ -z "$sqlManagedIdentityClientId" ] || [ -z "$sqlManagedIdentityDisplayName" ] || [ -z "$aiSearchName" ] || [ -z "$aif_resource_id" ]; then + echo "Usage: $0 " exit 1 fi @@ -437,8 +442,8 @@ # Call create_sql_user_and_role.sh echo "Running create_sql_user_and_role.sh" bash infra/scripts/add_user_scripts/create_sql_user_and_role.sh "$sqlServerName.database.windows.net" "$SqlDatabaseName" '[ - {"clientId":"'"$webAppManagedIdentityClientId"'", "displayName":"'"$webAppManagedIdentityDisplayName"'", "role":"db_datareader"}, - {"clientId":"'"$webAppManagedIdentityClientId"'", "displayName":"'"$webAppManagedIdentityDisplayName"'", "role":"db_datawriter"} + {"clientId":"'"$sqlManagedIdentityClientId"'", "displayName":"'"$sqlManagedIdentityDisplayName"'", "role":"db_datareader"}, + {"clientId":"'"$sqlManagedIdentityClientId"'", "displayName":"'"$sqlManagedIdentityDisplayName"'", "role":"db_datawriter"} ]' if [ $? -ne 0 ]; then echo "Error: create_sql_user_and_role.sh failed." diff --git a/src/App/backend/common/config.py b/src/App/backend/common/config.py index 06073a427..f841fb43d 100644 --- a/src/App/backend/common/config.py +++ b/src/App/backend/common/config.py @@ -142,7 +142,8 @@ def __init__(self): self.SQL_USERNAME = os.getenv("SQLDB_USERNAME") self.SQL_PASSWORD = os.getenv("SQLDB_PASSWORD") self.ODBC_DRIVER = "{ODBC Driver 18 for SQL Server}" - self.MID_ID = os.getenv("SQLDB_USER_MID") + self.MID_ID = os.getenv("AZURE_CLIENT_ID") + self.SQL_MID_ID = os.getenv("SQLDB_USER_MID") # System Prompts self.SQL_SYSTEM_PROMPT = os.environ.get("AZURE_SQL_SYSTEM_PROMPT") diff --git a/src/App/backend/services/sqldb_service.py b/src/App/backend/services/sqldb_service.py index 063c08bd1..1e003616a 100644 --- a/src/App/backend/services/sqldb_service.py +++ b/src/App/backend/services/sqldb_service.py @@ -17,7 +17,7 @@ database = config.SQL_DATABASE username = config.SQL_USERNAME password = config.SQL_PASSWORD -mid_id = config.MID_ID +mid_id = config.SQL_MID_ID def dict_cursor(cursor): diff --git a/tests/e2e-test/pages/homePage.py b/tests/e2e-test/pages/homePage.py index 2b354f01e..9a3b6aeb6 100644 --- a/tests/e2e-test/pages/homePage.py +++ b/tests/e2e-test/pages/homePage.py @@ -56,8 +56,9 @@ def delete_chat_history(self): else: self.page.locator(self.CLEAR_CHAT_HISTORY_MENU).click() self.page.locator(self.CLEAR_CHAT_HISTORY).click() + self.page.wait_for_timeout(4000) self.page.get_by_role("button", name="Clear All").click() - self.page.wait_for_timeout(10000) + self.page.wait_for_timeout(6000) self.page.locator(self.HIDE_CHAT_HISTORY_BUTTON).click() self.page.wait_for_load_state('networkidle') self.page.wait_for_timeout(2000)