diff --git a/README.md b/README.md index f646b0e1..33ee1466 100644 --- a/README.md +++ b/README.md @@ -237,15 +237,17 @@ You can optionally use a local development server to test app changes locally. M ``` 4. Install [Node.js](https://nodejs.org/) (v20 or later). + +5. Install [pnpm](https://pnpm.io/installation) -5. Navigate to the frontend directory and setup for React UI: +6. Navigate to the frontend directory and setup for React UI: ```shell cd src/frontend pnpm run setup ``` -6. Fill in the environment variables in `.env`. +7. Fill in the environment variables in `.env`. (Optional) if you have changes in `src/frontend`, execute: @@ -277,13 +279,7 @@ The build output will be placed in the `../api/static/react` directory, where th Once you've opened the project in [Codespaces](#github-codespaces) or in [Dev Containers](#vs-code-dev-containers) or [locally](#local-environment), you can deploy it to Azure following the following steps. -1. Login to Azure: - - ```shell - azd auth login - ``` - -2. (Optional) If you would like to customize the deployment to [disable resources](docs/deploy_customization.md#disabling-resources), [customize resource names](docs/deploy_customization.md#customizing-resource-names), [customize the models](docs/deploy_customization.md#customizing-model-deployments) or [increase quota](docs/deploy_customization.md#setting-capacity-and-deployment-sku), you can follow those steps now. +1. (Optional) If you would like to customize the deployment to [disable resources](docs/deploy_customization.md#disabling-resources), [customize resource names](docs/deploy_customization.md#customizing-resource-names), [customize the models](docs/deploy_customization.md#customizing-model-deployments) or [increase quota](docs/deploy_customization.md#setting-capacity-and-deployment-sku), you can follow those steps now. ⚠️ **NOTE!** For optimal performance, the recommended quota is 100k tokens per minute. If you have the capacity, we recommend increasing the quota by running the following command: @@ -293,13 +289,13 @@ Once you've opened the project in [Codespaces](#github-codespaces) or in [Dev Co ⚠️ If you do not increase your quota, you may encounter rate limit issues. If needed, you can increase the quota after deployment by editing your model in the Models and Endpoints tab of the [Azure AI Foundry Portal](https://ai.azure.com/). -3. Provision and deploy all the resources with public docker image `azdtemplate.azurecr.io/get-start-with-ai-agents:latest` by running the following in get-started-with-ai-agents directory: +2. Provision and deploy all the resources with public docker image `azdtemplate.azurecr.io/get-start-with-ai-agents:latest` by running the following in get-started-with-ai-agents directory: ```shell azd up ``` -4. You will be prompted to provide an `azd` environment name (like "azureaiapp"), select a subscription from your Azure account, and select a location which has quota for all the resources. Then, it will provision the resources in your account and deploy the latest code. +3. You will be prompted to provide an `azd` environment name (like "azureaiapp"), select a subscription from your Azure account, and select a location which has quota for all the resources. Then, it will provision the resources in your account and deploy the latest code. - For guidance on selecting a region with quota and model availability, follow the instructions in the [quota recommendations](#quota-recommendations-optional) section and ensure that your model is available in your selected region by checking the [list of models supported by Azure AI Agent Service](https://learn.microsoft.com/azure/ai-services/agents/concepts/model-region-support) - This deployment will take 7-10 minutes to provision the resources in your account and set up the solution with sample data. @@ -307,7 +303,7 @@ Once you've opened the project in [Codespaces](#github-codespaces) or in [Dev Co **NOTE!** If you get authorization failed and/or permission related errors during the deployment, please refer to the Azure account requirements in the [Prerequisites](#prerequisites) section. If you were recently granted these permissions, it may take a few minutes for the authorization to apply. -5. When `azd` has finished deploying, you'll see an endpoint URI in the command output. Visit that URI, and you should see the app! 🎉 +4. When `azd` has finished deploying, you'll see an endpoint URI in the command output. Visit that URI, and you should see the app! 🎉 - From here, you can interact with the agent. Try chatting with the agent by asking for a joke, or you could try a more specific query to see the agent's citation capabilities. By default, this solution uploads two documents from the `src/files` folder. To see the agent use this information, try asking about Contoso's products. @@ -317,13 +313,13 @@ Once you've opened the project in [Codespaces](#github-codespaces) or in [Dev Co azd show ``` -6. (Optional) Now that your app has deployed, you can view your resources in the Azure Portal and your deployments in Azure AI Foundry. +5. (Optional) Now that your app has deployed, you can view your resources in the Azure Portal and your deployments in Azure AI Foundry. - In the [Azure Portal](https://portal.azure.com/), navigate to your environment's resource group. The name will be `rg-[your environment name]`. Here, you should see your container app, storage account, and all of the other [resources](#resources) that are created in the deployment. - In the [Azure AI Foundry Portal](https://ai.azure.com/), select your project. If you navigate to the Agents tab, you should be able to view your new agent, named `agent-template-assistant`. If you navigate to the Models and Endpoints tab, you should see your AI Services connection with your model deployments. -7. (Optional) You can use a local development server to test app changes locally. To do so, follow the steps in [local deployment server](#local-development-server) after your app is deployed. +6. (Optional) You can use a local development server to test app changes locally. To do so, follow the steps in [local deployment server](#local-development-server) after your app is deployed. -8. (Optional) Follow this [tutorial](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-tutorial-quick-task) to build your changes into a Docker image and deploy to Azure Container App. +7. (Optional) To redeploy, run `azd deploy`. This will cause new docker image rebuilt, push to Azure Container Registry, and a new revision in Azure Container App with a new docker image. ## Tracing and Monitoring diff --git a/azure.yaml b/azure.yaml index 3443d0ac..ff6588a3 100644 --- a/azure.yaml +++ b/azure.yaml @@ -4,7 +4,7 @@ name: azd-get-started-with-ai-agents metadata: - template: azd-get-started-with-ai-agents@1.0.0 + template: azd-get-started-with-ai-agents@1.0.1 hooks: preup: @@ -29,7 +29,25 @@ hooks: run: chmod u+r+x ./scripts/write_env.sh; ./scripts/write_env.sh; continueOnError: true interactive: true - + postdeploy: + windows: + shell: pwsh + run: ./scripts/postdeploy.ps1 + continueOnError: true + interactive: true + posix: + shell: sh + run: chmod u+r+x ./scripts/postdeploy.sh; ./scripts/postdeploy.sh; + continueOnError: true + interactive: true +services: + api_and_frontend: + project: ./src + language: py + host: containerapp + docker: + image: api_and_frontend + remoteBuild: true pipeline: variables: - AZURE_RESOURCE_GROUP diff --git a/docs/deploy_customization.md b/docs/deploy_customization.md index db24e4fa..de723bbb 100644 --- a/docs/deploy_customization.md +++ b/docs/deploy_customization.md @@ -9,7 +9,15 @@ This document describes how to customize the deployment of the Agents Chat with * [Customizing model deployments](#customizing-model-deployments) ## Use existing resources -Be default, this template provisions a new resource group along with other resources. If you already have provisioned Azure AI Foundry and Azure AI Foundry Project, you might reuse these resources by setting: +Be default, this template provisions a new resource group along with other resources. If you already have provisioned Azure AI Foundry and Azure AI Foundry Project (not a hub based project), you might reuse these resources by setting: + +To find the value: + +1. Open the azure portal +2. Navigate to the AI foundry resource +3. Select projects in the sidebar and open the desired project +4. Oo to 'Resource Management' -> 'Properties' in the sidebar +5. Copy the value from 'Resource ID' ```shell azd env set AZURE_EXISTING_AIPROJECT_RESOURCE_ID "https://.services.ai.azure.com/api/projects/" diff --git a/infra/api.bicep b/infra/api.bicep index 33c7622f..e9747273 100644 --- a/infra/api.bicep +++ b/infra/api.bicep @@ -2,6 +2,7 @@ param name string param location string = resourceGroup().location param tags object = {} +param containerRegistryName string param identityName string param containerAppsEnvironmentName string param azureExistingAIProjectResourceId string @@ -13,7 +14,6 @@ param embeddingDeploymentDimensions string param searchServiceEndpoint string param agentName string param agentID string -param projectName string param enableAzureMonitorTracing bool param azureTracingGenAIContentRecordingEnabled bool param projectEndpoint string @@ -89,12 +89,12 @@ module app 'core/host/container-app-upsert.bicep' = { params: { name: name location: location - tags: tags + tags: union(tags, { 'azd-service-name': 'api_and_frontend' }) identityName: apiIdentity.name + containerRegistryName: containerRegistryName containerAppsEnvironmentName: containerAppsEnvironmentName targetPort: 50505 env: env - projectName: projectName } } diff --git a/infra/core/host/container-app-upsert.bicep b/infra/core/host/container-app-upsert.bicep index cec0c464..69b895da 100644 --- a/infra/core/host/container-app-upsert.bicep +++ b/infra/core/host/container-app-upsert.bicep @@ -23,6 +23,12 @@ param containerMinReplicas int = 1 @description('The name of the container') param containerName string = 'main' +@description('The name of the container registry') +param containerRegistryName string + +@description('Hostname suffix for container registry. Set when deploying to sovereign clouds') +param containerRegistryHostSuffix string = 'azurecr.io' + @allowed([ 'http', 'grpc' ]) @description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC') param daprAppProtocol string = 'http' @@ -43,6 +49,9 @@ param identityType string = 'None' @description('The name of the user-assigned identity') param identityName string = '' +@description('The name of the container image') +param imageName string = '' + @description('The secrets required for the container') @secure() param secrets object = {} @@ -59,8 +68,12 @@ param serviceBinds array = [] @description('The target port for the container') param targetPort int = 80 -param projectName string +@description('Specifies if the resource already exists') +param exists bool = false +resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { + name: name +} module app 'container-app.bicep' = { name: '${deployment().name}-update' @@ -73,6 +86,8 @@ module app 'container-app.bicep' = { ingressEnabled: ingressEnabled containerName: containerName containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + containerRegistryHostSuffix: containerRegistryHostSuffix containerCpuCoreCount: containerCpuCoreCount containerMemory: containerMemory containerMinReplicas: containerMinReplicas @@ -83,9 +98,9 @@ module app 'container-app.bicep' = { secrets: secrets external: external env: env + imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : '' targetPort: targetPort serviceBinds: serviceBinds - dependOn: projectName } } diff --git a/infra/core/host/container-app.bicep b/infra/core/host/container-app.bicep index 64569d71..8f149f4d 100644 --- a/infra/core/host/container-app.bicep +++ b/infra/core/host/container-app.bicep @@ -25,6 +25,12 @@ param containerMinReplicas int = 1 @description('The name of the container') param containerName string = 'main' +@description('The name of the container registry') +param containerRegistryName string = '' + +@description('Hostname suffix for container registry. Set when deploying to sovereign clouds') +param containerRegistryHostSuffix string = 'azurecr.io' + @description('The protocol used by Dapr to connect to the app, e.g., http or grpc') @allowed([ 'http', 'grpc' ]) param daprAppProtocol string = 'http' @@ -48,6 +54,8 @@ param identityName string = '' @allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) param identityType string = 'None' +@description('The name of the container image') +param imageName string = '' @description('Specifies if Ingress is enabled for the container app') param ingressEnabled bool = true @@ -73,9 +81,20 @@ resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01- name: identityName } +// Private registry support requires both an ACR name and a User Assigned managed identity +var usePrivateRegistry = !empty(identityName) && !empty(containerRegistryName) + // Automatically set to `UserAssigned` when an `identityName` has been set var normalizedIdentityType = !empty(identityName) ? 'UserAssigned' : identityType +module containerRegistryAccess '../security/registry-access.bicep' = if (usePrivateRegistry) { + name: '${deployment().name}-registry-access' + params: { + containerRegistryName: containerRegistryName + principalId: usePrivateRegistry ? userIdentity.properties.principalId : '' + } +} + resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { name: name location: location @@ -112,13 +131,18 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: secret.value }] service: !empty(serviceType) ? { type: serviceType } : null - registries: [] + registries: usePrivateRegistry ? [ + { + server: '${containerRegistryName}.${containerRegistryHostSuffix}' + identity: userIdentity.id + } + ] : [] } template: { serviceBinds: !empty(serviceBinds) ? serviceBinds : null containers: [ { - image: 'azdtemplate.azurecr.io/get-start-with-ai-agents:latest' + image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' name: containerName env: env resources: { diff --git a/infra/core/host/container-apps.bicep b/infra/core/host/container-apps.bicep index 42def2b9..dc4c1499 100644 --- a/infra/core/host/container-apps.bicep +++ b/infra/core/host/container-apps.bicep @@ -1,9 +1,11 @@ -metadata description = 'Creates an Azure Container Apps environment.' +metadata description = 'Creates an Azure Container Registry and an Azure Container Apps environment.' param name string param location string = resourceGroup().location param tags object = {} param containerAppsEnvironmentName string +param containerRegistryName string +param containerRegistryAdminUserEnabled bool = false param logAnalyticsWorkspaceName string param applicationInsightsName string = '' @@ -18,7 +20,20 @@ module containerAppsEnvironment 'container-apps-environment.bicep' = { } } +module containerRegistry 'container-registry.bicep' = { + name: '${name}-container-registry' + scope: resourceGroup() + params: { + name: containerRegistryName + location: location + adminUserEnabled: containerRegistryAdminUserEnabled + tags: tags + } +} + output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain output environmentName string = containerAppsEnvironment.outputs.name output environmentId string = containerAppsEnvironment.outputs.id +output registryLoginServer string = containerRegistry.outputs.loginServer +output registryName string = containerRegistry.outputs.name diff --git a/infra/core/host/container-registry.bicep b/infra/core/host/container-registry.bicep new file mode 100644 index 00000000..0b9d8788 --- /dev/null +++ b/infra/core/host/container-registry.bicep @@ -0,0 +1,137 @@ +metadata description = 'Creates an Azure Container Registry.' +param name string +param location string = resourceGroup().location +param tags object = {} + +@description('Indicates whether admin user is enabled') +param adminUserEnabled bool = false + +@description('Indicates whether anonymous pull is enabled') +param anonymousPullEnabled bool = false + +@description('Azure ad authentication as arm policy settings') +param azureADAuthenticationAsArmPolicy object = { + status: 'enabled' +} + +@description('Indicates whether data endpoint is enabled') +param dataEndpointEnabled bool = false + +@description('Encryption settings') +param encryption object = { + status: 'disabled' +} + +@description('Export policy settings') +param exportPolicy object = { + status: 'enabled' +} + +@description('Metadata search settings') +param metadataSearch string = 'Disabled' + +@description('Options for bypassing network rules') +param networkRuleBypassOptions string = 'AzureServices' + +@description('Public network access setting') +param publicNetworkAccess string = 'Enabled' + +@description('Quarantine policy settings') +param quarantinePolicy object = { + status: 'disabled' +} + +@description('Retention policy settings') +param retentionPolicy object = { + days: 7 + status: 'disabled' +} + +@description('Scope maps setting') +param scopeMaps array = [] + +@description('SKU settings') +param sku object = { + name: 'Basic' +} + +@description('Soft delete policy settings') +param softDeletePolicy object = { + retentionDays: 7 + status: 'disabled' +} + +@description('Trust policy settings') +param trustPolicy object = { + type: 'Notary' + status: 'disabled' +} + +@description('Zone redundancy setting') +param zoneRedundancy string = 'Disabled' + +@description('The log analytics workspace ID used for logging and monitoring') +param workspaceId string = '' + +// 2023-11-01-preview needed for metadataSearch +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = { + name: name + location: location + tags: tags + sku: sku + properties: { + adminUserEnabled: adminUserEnabled + anonymousPullEnabled: anonymousPullEnabled + dataEndpointEnabled: dataEndpointEnabled + encryption: encryption + metadataSearch: metadataSearch + networkRuleBypassOptions: networkRuleBypassOptions + policies:{ + quarantinePolicy: quarantinePolicy + trustPolicy: trustPolicy + retentionPolicy: retentionPolicy + exportPolicy: exportPolicy + azureADAuthenticationAsArmPolicy: azureADAuthenticationAsArmPolicy + softDeletePolicy: softDeletePolicy + } + publicNetworkAccess: publicNetworkAccess + zoneRedundancy: zoneRedundancy + } + + resource scopeMap 'scopeMaps' = [for scopeMap in scopeMaps: { + name: scopeMap.name + properties: scopeMap.properties + }] +} + +// TODO: Update diagnostics to be its own module +// Blocking issue: https://github.com/Azure/bicep/issues/622 +// Unable to pass in a `resource` scope or unable to use string interpolation in resource types +resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { + name: 'registry-diagnostics' + scope: containerRegistry + properties: { + workspaceId: workspaceId + logs: [ + { + category: 'ContainerRegistryRepositoryEvents' + enabled: true + } + { + category: 'ContainerRegistryLoginEvents' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + timeGrain: 'PT1M' + } + ] + } +} + +output id string = containerRegistry.id +output loginServer string = containerRegistry.properties.loginServer +output name string = containerRegistry.name diff --git a/infra/core/security/registry-access.bicep b/infra/core/security/registry-access.bicep index fc66837a..e58c9769 100644 --- a/infra/core/security/registry-access.bicep +++ b/infra/core/security/registry-access.bicep @@ -1,19 +1,19 @@ -metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.' -param containerRegistryName string -param principalId string - -var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - -resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: containerRegistry // Use when specifying a scope that is different than the deployment scope - name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) - properties: { - roleDefinitionId: acrPullRole - principalType: 'ServicePrincipal' - principalId: principalId - } -} - -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { - name: containerRegistryName -} +metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.' +param containerRegistryName string +param principalId string + +var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + +resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: containerRegistry // Use when specifying a scope that is different than the deployment scope + name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) + properties: { + roleDefinitionId: acrPullRole + principalType: 'ServicePrincipal' + principalId: principalId + } +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { + name: containerRegistryName +} diff --git a/infra/main.bicep b/infra/main.bicep index e43955a0..fa1ab7ca 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -266,6 +266,7 @@ module containerApps 'core/host/container-apps.bicep' = { params: { name: 'app' location: location + containerRegistryName: '${abbrs.containerRegistryRegistries}${resourceToken}' tags: tags containerAppsEnvironmentName: 'containerapps-env-${resourceToken}' logAnalyticsWorkspaceName: empty(azureExistingAIProjectResourceId) @@ -285,6 +286,7 @@ module api 'api.bicep' = { identityName: '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' containerAppsEnvironmentName: containerApps.outputs.environmentName azureExistingAIProjectResourceId: projectResourceId + containerRegistryName: containerApps.outputs.registryName agentDeploymentName: agentDeploymentName searchConnectionName: searchConnectionName aiSearchIndexName: aiSearchIndexName @@ -293,7 +295,6 @@ module api 'api.bicep' = { embeddingDeploymentDimensions: embeddingDeploymentDimensions agentName: agentName agentID: agentID - projectName: aiProjectName enableAzureMonitorTracing: enableAzureMonitorTracing azureTracingGenAIContentRecordingEnabled: azureTracingGenAIContentRecordingEnabled projectEndpoint: projectEndpoint @@ -447,3 +448,4 @@ output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME output SERVICE_API_URI string = api.outputs.SERVICE_API_URI output SERVICE_API_ENDPOINTS array = ['${api.outputs.SERVICE_API_URI}'] output SEARCH_CONNECTION_ID string = '' +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer diff --git a/scripts/postdeploy.ps1 b/scripts/postdeploy.ps1 new file mode 100644 index 00000000..dffa49a9 --- /dev/null +++ b/scripts/postdeploy.ps1 @@ -0,0 +1 @@ +Write-Host "💡 (Optional) To setup username and password for the web application, run '.\scripts\setup_credential.ps1'." diff --git a/scripts/postdeploy.sh b/scripts/postdeploy.sh new file mode 100755 index 00000000..b64c818a --- /dev/null +++ b/scripts/postdeploy.sh @@ -0,0 +1 @@ +echo "💡 (Optional) To setup username and password for the web application, run './scripts/setup_credential.sh'." \ No newline at end of file diff --git a/scripts/validate_env_vars.sh b/scripts/validate_env_vars.sh old mode 100644 new mode 100755 diff --git a/scripts/write_env.ps1 b/scripts/write_env.ps1 index 4ef0d402..8f164b0b 100755 --- a/scripts/write_env.ps1 +++ b/scripts/write_env.ps1 @@ -34,8 +34,3 @@ Add-Content -Path $envFilePath -Value "AZURE_AI_AGENT_NAME=$azureAiAgentName" Add-Content -Path $envFilePath -Value "AZURE_TENANT_ID=$azureTenantId" Add-Content -Path $envFilePath -Value "ENABLE_AZURE_MONITOR_TRACING=$enableAzureMonitorTracing" Add-Content -Path $envFilePath -Value "AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED=$azureTracingGenAIContentRecordingEnabled" - -Write-Host "🌐 Please visit web app URL:" -Write-Host $serviceAPIUri -ForegroundColor Cyan - -Write-Host "💡 (Optional) To setup username and password for the web application, run '.\scripts\setup_credential.ps1'." diff --git a/scripts/write_env.sh b/scripts/write_env.sh index 05724ae5..862f4ab9 100755 --- a/scripts/write_env.sh +++ b/scripts/write_env.sh @@ -21,10 +21,4 @@ echo "AZURE_EXISTING_AIPROJECT_ENDPOINT=$(azd env get-value AZURE_EXISTING_AIPRO echo "ENABLE_AZURE_MONITOR_TRACING=$(azd env get-value ENABLE_AZURE_MONITOR_TRACING 2>/dev/null)" >> $ENV_FILE_PATH echo "AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED=$(azd env get-value AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED 2>/dev/null)" >> $ENV_FILE_PATH - -echo "🌐 Please visit web app URL:" -echo -e "\033[0;36m$(azd env get-value SERVICE_API_URI 2>/dev/null)\033[0m" - -echo "💡 (Optional) To setup username and password for the web application, run './scripts/setup_credential.sh'." - exit 0 \ No newline at end of file