diff --git a/infra/main.bicep b/infra/main.bicep index 58f4db04..aa6f0a67 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -17,12 +17,31 @@ param principalId string = '' @minLength(1) @description('Location for the OpenAI resource') -// Look for gpt-35-turbo 0125 on the availability table: -// https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#standard-deployment-model-availability +// Look for desired models on the availability table: +// https://learn.microsoft.com/azure/ai-services/openai/concepts/models#global-standard-model-availability @allowed([ + 'australiaeast' + 'brazilsouth' 'canadaeast' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'japaneast' + 'koreacentral' 'northcentralus' + 'norwayeast' + 'polandcentral' + 'spaincentral' + 'southafricanorth' 'southcentralus' + 'southindia' + 'swedencentral' + 'switzerlandnorth' + 'uksouth' + 'westeurope' + 'westus' + 'westus3' ]) @metadata({ azd: { @@ -37,62 +56,105 @@ param openAIResourceGroupName string = '' @description('Whether to deploy Azure OpenAI resources') param deployAzureOpenAI bool = true -@description('Name of the GPT model to deploy') -param chatModelName string = '' -@description('Name of the model deployment') -param chatDeploymentName string = '' +@allowed([ + 'azure' + 'openaicom' +]) +param openAIChatHost string = 'azure' -@description('Version of the GPT model to deploy') -// See version availability in this table: -// https://learn.microsoft.com/azure/ai-services/openai/concepts/models#gpt-4-and-gpt-4-turbo-preview-models -param chatDeploymentVersion string = '' +@allowed([ + 'azure' + 'openaicom' +]) +param openAIEmbedHost string = 'azure' + +@secure() +param openAIComKey string = '' param azureOpenAIAPIVersion string = '2024-03-01-preview' @secure() param azureOpenAIKey string = '' + @description('Azure OpenAI endpoint to use, if not using the one deployed here.') param azureOpenAIEndpoint string = '' -@description('Whether to use Azure OpenAI (either deployed here or elsewhere) or OpenAI.com') -var useAzureOpenAI = deployAzureOpenAI || !empty(azureOpenAIEndpoint) +// Chat completion model +@description('Name of the chat model to deploy') +param chatModelName string // Set in main.parameters.json +@description('Name of the model deployment') +param chatDeploymentName string // Set in main.parameters.json -@description('Capacity of the GPT deployment') +@description('Version of the chat model to deploy') +// See version availability in this table: +// https://learn.microsoft.com/azure/ai-services/openai/concepts/models#global-standard-model-availability +param chatDeploymentVersion string // Set in main.parameters.json + +@description('Sku of the chat deployment') +param chatDeploymentSku string // Set in main.parameters.json + +@description('Capacity of the chat deployment') // You can increase this, but capacity is limited per model/region, so you will get errors if you go over // https://learn.microsoft.com/en-us/azure/ai-services/openai/quotas-limits -param chatDeploymentCapacity int = 0 -var chatConfig = { - modelName: !empty(chatModelName) ? chatModelName : (useAzureOpenAI ? 'gpt-35-turbo' : 'gpt-3.5-turbo') - deploymentName: !empty(chatDeploymentName) ? chatDeploymentName : 'gpt-35-turbo' - deploymentVersion: !empty(chatDeploymentVersion) ? chatDeploymentVersion : '0125' - deploymentCapacity: chatDeploymentCapacity != 0 ? chatDeploymentCapacity : 30 -} +param chatDeploymentCapacity int // Set in main.parameters.json -param embedModelName string = '' -param embedDeploymentName string = '' -param embedDeploymentVersion string = '' -param embedDeploymentCapacity int = 0 -param embedDimensions int = 0 - -var embedConfig = { - modelName: !empty(embedModelName) ? embedModelName : 'text-embedding-ada-002' - deploymentName: !empty(embedDeploymentName) ? embedDeploymentName : 'text-embedding-ada-002' - deploymentVersion: !empty(embedDeploymentVersion) ? embedDeploymentVersion : '2' - deploymentCapacity: embedDeploymentCapacity != 0 ? embedDeploymentCapacity : 30 - dimensions: embedDimensions != 0 ? embedDimensions : 1536 -} +@description('Whether to deploy the evaluation model') +param deployEvalModel bool // Set in main.parameters.json + +// Chat completion model used for evaluations (use most powerful model) +@description('Name of the chat model to use for evaluations') +param evalModelName string // Set in main.parameters.json +@description('Name of the model deployment for the evaluation model') +param evalDeploymentName string // Set in main.parameters.json + +@description('Version of the chat model to deploy for evaluations') +// See version availability in this table: +// https://learn.microsoft.com/azure/ai-services/openai/concepts/models#global-standard-model-availability +param evalDeploymentVersion string // Set in main.parameters.json + +@description('Sku of the model deployment for evaluations') +param evalDeploymentSku string // Set in main.parameters.json + +@description('Capacity of the chat deployment for evaluations (Go as high as possible)') +// You can increase this, but capacity is limited per model/region, so you will get errors if you go over +// https://learn.microsoft.com/en-us/azure/ai-services/openai/quotas-limits +param evalDeploymentCapacity string // Set in main.parameters.json + + +// Embedding model +@description('Name of the embedding model to deploy') +param embedModelName string // Set in main.parameters.json +@description('Name of the embedding model deployment') +param embedDeploymentName string // Set in main.parameters.json + +@description('Version of the embedding model to deploy') +// See version availability in this table: +// https://learn.microsoft.com/azure/ai-services/openai/concepts/models#embeddings-models +param embedDeploymentVersion string // Set in main.parameters.json + +@description('Sku of the embeddings model deployment') +param embedDeploymentSku string // Set in main.parameters.json + +@description('Capacity of the embedding deployment') +// You can increase this, but capacity is limited per model/region, so you will get errors if you go over +// https://learn.microsoft.com/en-us/azure/ai-services/openai/quotas-limits +param embedDeploymentCapacity int // Set in main.parameters.json + +@description('Dimensions of the embedding model') +param embedDimensions int // Set in main.parameters.json + +@description('Use AI project') +param useAiProject bool = false param webAppExists bool = false var resourceToken = toLower(uniqueString(subscription().id, name, location)) -var prefix = '${name}-${resourceToken}' +var prefix = '${toLower(name)}-${resourceToken}' var tags = { 'azd-env-name': name } -resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { - name: '${name}-rg' - location: location - tags: tags +// ✅ Use existing ResourceGroup1 instead of creating a new one +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' existing = { + name: 'ResourceGroup1' } - var postgresServerName = '${prefix}-postgresql' var postgresDatabaseName = 'postgres' var postgresEntraAdministratorObjectId = principalId @@ -130,12 +192,22 @@ module monitoring 'core/monitor/monitoring.bicep' = { params: { location: location tags: tags - applicationInsightsDashboardName: '${prefix}-appinsights-dashboard' applicationInsightsName: '${prefix}-appinsights' logAnalyticsName: '${take(prefix, 50)}-loganalytics' // Max 63 chars } } + +module applicationInsightsDashboard 'backend-dashboard.bicep' = { + name: 'application-insights-dashboard' + scope: resourceGroup + params: { + name: '${prefix}-appinsights-dashboard' + location: location + applicationInsightsName: monitoring.outputs.applicationInsightsName + } +} + // Container apps host (including container registry) module containerApps 'core/host/container-apps.bicep' = { name: 'container-apps' @@ -152,7 +224,37 @@ module containerApps 'core/host/container-apps.bicep' = { // Web frontend var webAppName = replace('${take(prefix, 19)}-ca', '--', '-') var webAppIdentityName = '${prefix}-id-web' -var webAppEnv = [ + +var azureOpenAIKeySecret = !empty(azureOpenAIKey) + ? { + 'azure-openai-key': azureOpenAIKey + } + : {} +var openAIComKeySecret = !empty(openAIComKey) + ? { + 'openaicom-key': openAIComKey + } + : {} +var secrets = union(azureOpenAIKeySecret, openAIComKeySecret) + +var azureOpenAIKeyEnv = !empty(azureOpenAIKey) + ? [ + { + name: 'AZURE_OPENAI_KEY' + secretRef: 'azure-openai-key' + } + ] + : [] +var openAIComKeyEnv = !empty(openAIComKey) + ? [ + { + name: 'OPENAICOM_KEY' + secretRef: 'openaicom-key' + } + ] + : [] + +var webAppEnv = union(azureOpenAIKeyEnv, openAIComKeyEnv, [ { name: 'POSTGRES_HOST' value: postgresServer.outputs.POSTGRES_DOMAIN_NAME @@ -179,63 +281,53 @@ var webAppEnv = [ } { name: 'OPENAI_CHAT_HOST' - value: useAzureOpenAI ? 'azure' : 'openaicom' + value: openAIChatHost } { name: 'AZURE_OPENAI_CHAT_DEPLOYMENT' - value: useAzureOpenAI ? chatConfig.deploymentName : '' + value: openAIChatHost == 'azure' ? chatDeploymentName : '' } { name: 'AZURE_OPENAI_CHAT_MODEL' - value: useAzureOpenAI ? chatConfig.modelName : '' + value: openAIChatHost == 'azure' ? chatModelName : '' } { name: 'OPENAICOM_CHAT_MODEL' - value: useAzureOpenAI ? '' : 'gpt-3.5-turbo' + value: openAIChatHost == 'openaicom' ? 'gpt-3.5-turbo' : '' } { name: 'OPENAI_EMBED_HOST' - value: useAzureOpenAI ? 'azure' : 'openaicom' + value: openAIEmbedHost } { - name: 'OPENAICOM_EMBED_MODEL_DIMENSIONS' - value: useAzureOpenAI ? '' : '1536' + name: 'OPENAICOM_EMBED_DIMENSIONS' + value: openAIEmbedHost == 'openaicom' ? '1024' : '' } { name: 'OPENAICOM_EMBED_MODEL' - value: useAzureOpenAI ? '' : 'text-embedding-ada-002' + value: openAIEmbedHost == 'openaicom' ? 'text-embedding-3-large' : '' } { name: 'AZURE_OPENAI_EMBED_MODEL' - value: useAzureOpenAI ? embedConfig.modelName : '' + value: openAIEmbedHost == 'azure' ? embedModelName : '' } { name: 'AZURE_OPENAI_EMBED_DEPLOYMENT' - value: useAzureOpenAI ? embedConfig.deploymentName : '' + value: openAIEmbedHost == 'azure' ? embedDeploymentName : '' } { - name: 'AZURE_OPENAI_EMBED_MODEL_DIMENSIONS' - value: useAzureOpenAI ? string(embedConfig.dimensions) : '' + name: 'AZURE_OPENAI_EMBED_DIMENSIONS' + value: openAIEmbedHost == 'azure' ? string(embedDimensions) : '' } { name: 'AZURE_OPENAI_ENDPOINT' - value: useAzureOpenAI ? (deployAzureOpenAI ? openAI.outputs.endpoint : azureOpenAIEndpoint) : '' + value: !empty(azureOpenAIEndpoint) ? azureOpenAIEndpoint : (deployAzureOpenAI ? openAI.outputs.endpoint : '') } { name: 'AZURE_OPENAI_VERSION' - value: useAzureOpenAI ? azureOpenAIAPIVersion : '' - } -] -var webAppEnvWithSecret = !empty(azureOpenAIKey) ? union(webAppEnv, [ - { - name: 'AZURE_OPENAI_KEY' - secretRef: 'azure-openai-key' + value: openAIChatHost == 'azure' ? azureOpenAIAPIVersion : '' } -]) : webAppEnv - -var secrets = !empty(azureOpenAIKey) ? { - 'azure-openai-key': azureOpenAIKey -} : {} +]) module web 'web.bicep' = { name: 'web' @@ -248,15 +340,56 @@ module web 'web.bicep' = { containerAppsEnvironmentName: containerApps.outputs.environmentName containerRegistryName: containerApps.outputs.registryName exists: webAppExists - environmentVariables: webAppEnvWithSecret + environmentVariables: webAppEnv secrets: secrets } } -resource openAIResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' existing = - if (!empty(openAIResourceGroupName)) { - name: !empty(openAIResourceGroupName) ? openAIResourceGroupName : resourceGroup.name +resource openAIResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' existing = if (!empty(openAIResourceGroupName)) { + name: !empty(openAIResourceGroupName) ? openAIResourceGroupName : resourceGroup.name +} + +var defaultDeployments = [ + { + name: chatDeploymentName + model: { + format: 'OpenAI' + name: chatModelName + version: chatDeploymentVersion + } + sku: { + name: chatDeploymentSku + capacity: chatDeploymentCapacity } +} +{ + name: embedDeploymentName + model: { + format: 'OpenAI' + name: embedModelName + version: embedDeploymentVersion + } + sku: { + name: embedDeploymentSku + capacity: embedDeploymentCapacity + } +}] + +var evalDeployment = { + name: evalDeploymentName + model: { + format: 'OpenAI' + name: evalModelName + version: evalDeploymentVersion + } + sku: { + name: evalDeploymentSku + capacity: evalDeploymentCapacity + } +} + +var openAiDeployments = deployEvalModel ? union([evalDeployment], defaultDeployments) : defaultDeployments + module openAI 'core/ai/cognitiveservices.bicep' = if (deployAzureOpenAI) { name: 'openai' @@ -269,46 +402,109 @@ module openAI 'core/ai/cognitiveservices.bicep' = if (deployAzureOpenAI) { name: 'S0' } disableLocalAuth: true - deployments: [ + deployments: openAiDeployments + } +} + +module storage 'br/public:avm/res/storage/storage-account:0.9.1' = if (useAiProject) { + name: 'storage' + scope: resourceGroup + params: { + name: '${take(replace(prefix, '-', ''), 17)}storage' + location: location + tags: tags + kind: 'StorageV2' + skuName: 'Standard_LRS' + networkAcls: { + defaultAction: 'Allow' + bypass: 'AzureServices' + } + allowBlobPublicAccess: false + allowSharedKeyAccess: false + roleAssignments: [ { - name: chatConfig.deploymentName - model: { - format: 'OpenAI' - name: chatConfig.modelName - version: chatConfig.deploymentVersion - } - sku: { - name: 'Standard' - capacity: chatConfig.deploymentCapacity - } + principalId: principalId + principalType: 'User' + roleDefinitionIdOrName: 'Storage Blob Data Contributor' } - { - name: embedConfig.deploymentName - model: { - format: 'OpenAI' - name: embedConfig.modelName - version: embedConfig.deploymentVersion + ] + blobServices: { + containers: [ + { + name: 'default' + publicAccess: 'None' } - sku: { - name: 'Standard' - capacity: embedConfig.deploymentCapacity + ] + cors: { + corsRules: [ + { + allowedOrigins: [ + 'https://mlworkspace.azure.ai' + 'https://ml.azure.com' + 'https://*.ml.azure.com' + 'https://ai.azure.com' + 'https://*.ai.azure.com' + 'https://mlworkspacecanary.azure.ai' + 'https://mlworkspace.azureml-test.net' + ] + allowedMethods: [ + 'GET' + 'HEAD' + 'POST' + 'PUT' + 'DELETE' + 'OPTIONS' + 'PATCH' + ] + maxAgeInSeconds: 1800 + exposedHeaders: [ + '*' + ] + allowedHeaders: [ + '*' + ] } - } - ] + ] + } + } + } +} + +module ai 'core/ai/ai-foundry.bicep' = if (useAiProject) { + name: 'ai' + scope: resourceGroup + params: { + location: 'swedencentral' + tags: tags + foundryName: 'aifoundry-${resourceToken}' + projectName: 'aiproject-${resourceToken}' + storageAccountName: storage.outputs.name + principalId: principalId + principalType: empty(runningOnGh) ? 'User' : 'ServicePrincipal' } } // USER ROLES -module openAIRoleUser 'core/security/role.bicep' = - if (empty(runningOnGh)) { - scope: openAIResourceGroup - name: 'openai-role-user' - params: { - principalId: principalId - roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' - principalType: 'User' - } +module openAIRoleUser 'core/security/role.bicep' = { + scope: openAIResourceGroup + name: 'openai-role-user' + params: { + principalId: principalId + roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User + principalType: empty(runningOnGh) ? 'User' : 'ServicePrincipal' } +} + +module azureAiUserRole 'core/security/role.bicep' = if (useAiProject && resourceGroup.name != openAIResourceGroup.name) { + name: 'azureai-role-user' + scope: resourceGroup + params: { + principalId: principalId + roleDefinitionId: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User + principalType: empty(runningOnGh) ? 'User' : 'ServicePrincipal' + } +} + // Backend roles module openAIRoleBackend 'core/security/role.bicep' = { @@ -321,7 +517,21 @@ module openAIRoleBackend 'core/security/role.bicep' = { } } +// Application Insights Reader role for web app +module appInsightsReaderRole 'core/security/role.bicep' = { + scope: resourceGroup + name: 'appinsights-reader-role' + params: { + principalId: principalId + roleDefinitionId: '43d0d8ad-25c7-4714-9337-8ba259a9fe05' // Application Insights Component Reader + principalType: 'User' + } +} + output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output AZURE_RESOURCE_GROUP string = resourceGroup.name + output APPLICATIONINSIGHTS_NAME string = monitoring.outputs.applicationInsightsName output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName @@ -334,13 +544,34 @@ output SERVICE_WEB_NAME string = web.outputs.SERVICE_WEB_NAME output SERVICE_WEB_URI string = web.outputs.SERVICE_WEB_URI output SERVICE_WEB_IMAGE_NAME string = web.outputs.SERVICE_WEB_IMAGE_NAME -output AZURE_OPENAI_ENDPOINT string = useAzureOpenAI ? (deployAzureOpenAI ? openAI.outputs.endpoint : azureOpenAIEndpoint) : '' -output AZURE_OPENAI_VERSION string = useAzureOpenAI ? azureOpenAIAPIVersion : '' -output AZURE_OPENAI_CHAT_DEPLOYMENT string = useAzureOpenAI ? chatConfig.deploymentName : '' -output AZURE_OPENAI_EMBED_DEPLOYMENT string = useAzureOpenAI ? embedConfig.deploymentName : '' -output AZURE_OPENAI_CHAT_MODEL string = useAzureOpenAI ? chatConfig.modelName : '' -output AZURE_OPENAI_EMBED_MODEL string = useAzureOpenAI ? embedConfig.modelName : '' -output AZURE_OPENAI_EMBED_MODEL_DIMENSIONS int = useAzureOpenAI ? embedConfig.dimensions : 0 +output OPENAI_CHAT_HOST string = openAIChatHost +output OPENAI_EMBED_HOST string = openAIEmbedHost +output AZURE_OPENAI_SERVICE string = deployAzureOpenAI ? openAI.outputs.name : '' +output AZURE_OPENAI_RESOURCE_GROUP string = deployAzureOpenAI ? openAIResourceGroup.name : '' +output AZURE_OPENAI_ENDPOINT string = !empty(azureOpenAIEndpoint) + ? azureOpenAIEndpoint + : (deployAzureOpenAI ? openAI.outputs.endpoint : '') +output AZURE_OPENAI_VERSION string = azureOpenAIAPIVersion +output AZURE_OPENAI_CHAT_DEPLOYMENT string = deployAzureOpenAI ? chatDeploymentName : '' +output AZURE_OPENAI_CHAT_DEPLOYMENT_VERSION string = deployAzureOpenAI ? chatDeploymentVersion : '' +output AZURE_OPENAI_CHAT_DEPLOYMENT_CAPACITY int = deployAzureOpenAI ? chatDeploymentCapacity : 0 +output AZURE_OPENAI_CHAT_DEPLOYMENT_SKU string = deployAzureOpenAI ? chatDeploymentSku : '' +output AZURE_OPENAI_CHAT_MODEL string = deployAzureOpenAI ? chatModelName : '' +output AZURE_OPENAI_EMBED_DEPLOYMENT string = deployAzureOpenAI ? embedDeploymentName : '' +output AZURE_OPENAI_EMBED_DEPLOYMENT_VERSION string = deployAzureOpenAI ? embedDeploymentVersion : '' +output AZURE_OPENAI_EMBED_DEPLOYMENT_CAPACITY int = deployAzureOpenAI ? embedDeploymentCapacity : 0 +output AZURE_OPENAI_EMBED_DEPLOYMENT_SKU string = deployAzureOpenAI ? embedDeploymentSku : '' +output AZURE_OPENAI_EMBED_MODEL string = deployAzureOpenAI ? embedModelName : '' +output AZURE_OPENAI_EMBED_DIMENSIONS string = deployAzureOpenAI ? string(embedDimensions) : '' + +output AZURE_OPENAI_EVAL_DEPLOYMENT string = deployAzureOpenAI ? evalDeploymentName : '' +output AZURE_OPENAI_EVAL_DEPLOYMENT_VERSION string = deployAzureOpenAI ? evalDeploymentVersion : '' +output AZURE_OPENAI_EVAL_DEPLOYMENT_CAPACITY string = deployAzureOpenAI ? evalDeploymentCapacity : '' +output AZURE_OPENAI_EVAL_DEPLOYMENT_SKU string = deployAzureOpenAI ? evalDeploymentSku : '' +output AZURE_OPENAI_EVAL_MODEL string = deployAzureOpenAI ? evalModelName : '' + +output AZURE_AI_FOUNDRY string = useAiProject ? ai.outputs.foundryName : '' +output AZURE_AI_PROJECT string = useAiProject ? ai.outputs.projectName : '' output POSTGRES_HOST string = postgresServer.outputs.POSTGRES_DOMAIN_NAME output POSTGRES_USERNAME string = postgresEntraAdministratorName