From 34c380d17e604fd100a5f54dd1a78b257d299376 Mon Sep 17 00:00:00 2001 From: John Miller Date: Fri, 1 May 2026 16:45:21 -0400 Subject: [PATCH 1/5] Refactor AI Foundry infrastructure: - Remove storage.bicep and integrate storage logic into new modules. - Update main.bicep to streamline resource group and AI project creation. - Introduce new modules for ACR, AI project, AI search, and Bing grounding. - Enhance connection management with a dedicated connection module. - Adjust parameters and outputs for better clarity and usability. --- infra/abbreviations.json | 137 -- infra/core/ai/acr-role-assignment.bicep | 27 - infra/core/ai/ai-project.bicep | 413 ------ infra/core/ai/connection.bicep | 112 -- infra/core/ai/existing-ai-project.bicep | 70 - infra/core/host/acr.bicep | 88 -- .../applicationinsights-dashboard.bicep | 1236 ----------------- infra/core/monitor/applicationinsights.bicep | 47 - infra/core/monitor/loganalytics.bicep | 22 - infra/core/search/azure_ai_search.bicep | 211 --- infra/core/search/bing_custom_grounding.bicep | 84 -- infra/core/search/bing_grounding.bicep | 83 -- infra/core/storage/storage.bicep | 113 -- infra/main.bicep | 215 +-- infra/main.parameters.json | 117 +- infra/modules/acr.bicep | 86 ++ infra/modules/ai-project.bicep | 216 +++ infra/modules/ai-search.bicep | 143 ++ infra/modules/bing-grounding.bicep | 66 + infra/modules/connection.bicep | 38 + infra/modules/storage.bicep | 89 ++ 21 files changed, 740 insertions(+), 2873 deletions(-) delete mode 100644 infra/abbreviations.json delete mode 100644 infra/core/ai/acr-role-assignment.bicep delete mode 100644 infra/core/ai/ai-project.bicep delete mode 100644 infra/core/ai/connection.bicep delete mode 100644 infra/core/ai/existing-ai-project.bicep delete mode 100644 infra/core/host/acr.bicep delete mode 100644 infra/core/monitor/applicationinsights-dashboard.bicep delete mode 100644 infra/core/monitor/applicationinsights.bicep delete mode 100644 infra/core/monitor/loganalytics.bicep delete mode 100644 infra/core/search/azure_ai_search.bicep delete mode 100644 infra/core/search/bing_custom_grounding.bicep delete mode 100644 infra/core/search/bing_grounding.bicep delete mode 100644 infra/core/storage/storage.bicep create mode 100644 infra/modules/acr.bicep create mode 100644 infra/modules/ai-project.bicep create mode 100644 infra/modules/ai-search.bicep create mode 100644 infra/modules/bing-grounding.bicep create mode 100644 infra/modules/connection.bicep create mode 100644 infra/modules/storage.bicep diff --git a/infra/abbreviations.json b/infra/abbreviations.json deleted file mode 100644 index 00cef3f..0000000 --- a/infra/abbreviations.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "aiFoundryAccounts": "aif", - "analysisServicesServers": "as", - "apiManagementService": "apim-", - "appConfigurationStores": "appcs-", - "appManagedEnvironments": "cae-", - "appContainerApps": "ca-", - "authorizationPolicyDefinitions": "policy-", - "automationAutomationAccounts": "aa-", - "blueprintBlueprints": "bp-", - "blueprintBlueprintsArtifacts": "bpa-", - "cacheRedis": "redis-", - "cdnProfiles": "cdnp-", - "cdnProfilesEndpoints": "cdne-", - "cognitiveServicesAccounts": "cog-", - "cognitiveServicesFormRecognizer": "cog-fr-", - "cognitiveServicesTextAnalytics": "cog-ta-", - "computeAvailabilitySets": "avail-", - "computeCloudServices": "cld-", - "computeDiskEncryptionSets": "des", - "computeDisks": "disk", - "computeDisksOs": "osdisk", - "computeGalleries": "gal", - "computeSnapshots": "snap-", - "computeVirtualMachines": "vm", - "computeVirtualMachineScaleSets": "vmss-", - "containerInstanceContainerGroups": "ci", - "containerRegistryRegistries": "cr", - "containerServiceManagedClusters": "aks-", - "databricksWorkspaces": "dbw-", - "dataFactoryFactories": "adf-", - "dataLakeAnalyticsAccounts": "dla", - "dataLakeStoreAccounts": "dls", - "dataMigrationServices": "dms-", - "dBforMySQLServers": "mysql-", - "dBforPostgreSQLServers": "psql-", - "devicesIotHubs": "iot-", - "devicesProvisioningServices": "provs-", - "devicesProvisioningServicesCertificates": "pcert-", - "documentDBDatabaseAccounts": "cosmos-", - "documentDBMongoDatabaseAccounts": "cosmon-", - "eventGridDomains": "evgd-", - "eventGridDomainsTopics": "evgt-", - "eventGridEventSubscriptions": "evgs-", - "eventHubNamespaces": "evhns-", - "eventHubNamespacesEventHubs": "evh-", - "hdInsightClustersHadoop": "hadoop-", - "hdInsightClustersHbase": "hbase-", - "hdInsightClustersKafka": "kafka-", - "hdInsightClustersMl": "mls-", - "hdInsightClustersSpark": "spark-", - "hdInsightClustersStorm": "storm-", - "hybridComputeMachines": "arcs-", - "insightsActionGroups": "ag-", - "insightsComponents": "appi-", - "keyVaultVaults": "kv-", - "kubernetesConnectedClusters": "arck", - "kustoClusters": "dec", - "kustoClustersDatabases": "dedb", - "logicIntegrationAccounts": "ia-", - "logicWorkflows": "logic-", - "machineLearningServicesWorkspaces": "mlw-", - "managedIdentityUserAssignedIdentities": "id-", - "managementManagementGroups": "mg-", - "migrateAssessmentProjects": "migr-", - "networkApplicationGateways": "agw-", - "networkApplicationSecurityGroups": "asg-", - "networkAzureFirewalls": "afw-", - "networkBastionHosts": "bas-", - "networkConnections": "con-", - "networkDnsZones": "dnsz-", - "networkExpressRouteCircuits": "erc-", - "networkFirewallPolicies": "afwp-", - "networkFirewallPoliciesWebApplication": "waf", - "networkFirewallPoliciesRuleGroups": "wafrg", - "networkFrontDoors": "fd-", - "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", - "networkLoadBalancersExternal": "lbe-", - "networkLoadBalancersInternal": "lbi-", - "networkLoadBalancersInboundNatRules": "rule-", - "networkLocalNetworkGateways": "lgw-", - "networkNatGateways": "ng-", - "networkNetworkInterfaces": "nic-", - "networkNetworkSecurityGroups": "nsg-", - "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", - "networkNetworkWatchers": "nw-", - "networkPrivateDnsZones": "pdnsz-", - "networkPrivateLinkServices": "pl-", - "networkPublicIPAddresses": "pip-", - "networkPublicIPPrefixes": "ippre-", - "networkRouteFilters": "rf-", - "networkRouteTables": "rt-", - "networkRouteTablesRoutes": "udr-", - "networkTrafficManagerProfiles": "traf-", - "networkVirtualNetworkGateways": "vgw-", - "networkVirtualNetworks": "vnet-", - "networkVirtualNetworksSubnets": "snet-", - "networkVirtualNetworksVirtualNetworkPeerings": "peer-", - "networkVirtualWans": "vwan-", - "networkVpnGateways": "vpng-", - "networkVpnGatewaysVpnConnections": "vcn-", - "networkVpnGatewaysVpnSites": "vst-", - "notificationHubsNamespaces": "ntfns-", - "notificationHubsNamespacesNotificationHubs": "ntf-", - "operationalInsightsWorkspaces": "log-", - "portalDashboards": "dash-", - "powerBIDedicatedCapacities": "pbi-", - "purviewAccounts": "pview-", - "recoveryServicesVaults": "rsv-", - "resourcesResourceGroups": "rg-", - "searchSearchServices": "srch-", - "serviceBusNamespaces": "sb-", - "serviceBusNamespacesQueues": "sbq-", - "serviceBusNamespacesTopics": "sbt-", - "serviceEndPointPolicies": "se-", - "serviceFabricClusters": "sf-", - "signalRServiceSignalR": "sigr", - "sqlManagedInstances": "sqlmi-", - "sqlServers": "sql-", - "sqlServersDataWarehouse": "sqldw-", - "sqlServersDatabases": "sqldb-", - "sqlServersDatabasesStretch": "sqlstrdb-", - "storageStorageAccounts": "st", - "storageStorageAccountsVm": "stvm", - "storSimpleManagers": "ssimp", - "streamAnalyticsCluster": "asa-", - "synapseWorkspaces": "syn", - "synapseWorkspacesAnalyticsWorkspaces": "synw", - "synapseWorkspacesSqlPoolsDedicated": "syndp", - "synapseWorkspacesSqlPoolsSpark": "synsp", - "timeSeriesInsightsEnvironments": "tsi-", - "webServerFarms": "plan-", - "webSitesAppService": "app-", - "webSitesAppServiceEnvironment": "ase-", - "webSitesFunctions": "func-", - "webStaticSites": "stapp-" -} diff --git a/infra/core/ai/acr-role-assignment.bicep b/infra/core/ai/acr-role-assignment.bicep deleted file mode 100644 index 3e0c2b2..0000000 --- a/infra/core/ai/acr-role-assignment.bicep +++ /dev/null @@ -1,27 +0,0 @@ -targetScope = 'resourceGroup' - -@description('Name of the existing container registry') -param acrName string - -@description('Principal ID to grant AcrPull role') -param principalId string - -@description('Full resource ID of the ACR (for generating unique GUID)') -param acrResourceId string - -// Reference the existing ACR in this resource group -resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = { - name: acrName -} - -// Grant AcrPull role to the AI project's managed identity -resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: acr - name: guid(acrResourceId, principalId, '7f951dda-4ed3-4680-a7ca-43fe172d538d') - properties: { - principalId: principalId - principalType: 'ServicePrincipal' - // AcrPull role - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } -} diff --git a/infra/core/ai/ai-project.bicep b/infra/core/ai/ai-project.bicep deleted file mode 100644 index 0e01670..0000000 --- a/infra/core/ai/ai-project.bicep +++ /dev/null @@ -1,413 +0,0 @@ -targetScope = 'resourceGroup' - -@description('Tags that will be applied to all resources') -param tags object = {} - -@description('Main location for the resources') -param location string - -var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) - -@description('Name of the project') -param aiFoundryProjectName string - -param deployments deploymentsType - -@description('Id of the user or app to assign application roles') -param principalId string - -@description('Principal type of user or app') -param principalType string - -@description('Optional. Name of an existing AI Services account in the current resource group. If not provided, a new one will be created.') -param existingAiAccountName string = '' - -@description('List of connections to provision') -param connections array = [] - -@secure() -@description('Map of connection name to credentials object. Kept as @secure to prevent secrets from appearing in deployment logs. Example: { "my-conn": { "key": "secret" } }') -param connectionCredentials object = {} - -@description('Also provision dependent resources and connect to the project') -param additionalDependentResources dependentResourcesType - -@description('Enable monitoring via appinsights and log analytics') -param enableMonitoring bool = true - -@description('Enable hosted agent deployment') -param enableHostedAgents bool = false - -@description('Enable the capability host for agent conversations. When false and hosted agents are enabled, the capability host is not created (v2 hosted agents handle storage automatically).') -param enableCapabilityHost bool = true - -@description('Optional. Existing container registry resource ID. If provided, a connection will be created to this ACR instead of creating a new one.') -param existingContainerRegistryResourceId string = '' - -@description('Optional. Existing container registry login server (e.g., myregistry.azurecr.io). Required if existingContainerRegistryResourceId is provided.') -param existingContainerRegistryEndpoint string = '' - -@description('Optional. Name of an existing ACR connection on the Foundry project. If provided, no new ACR or connection will be created.') -param existingAcrConnectionName string = '' - -@description('Optional. Existing Application Insights connection string. If provided, a connection will be created but no new App Insights resource.') -param existingApplicationInsightsConnectionString string = '' - -@description('Optional. Existing Application Insights resource ID. Used for connection metadata when providing an existing App Insights.') -param existingApplicationInsightsResourceId string = '' - -@description('Optional. Name of an existing Application Insights connection on the Foundry project. If provided, no new App Insights or connection will be created.') -param existingAppInsightsConnectionName string = '' - -// Load abbreviations -var abbrs = loadJsonContent('../../abbreviations.json') - -// Determine which resources to create based on connections -var hasStorageConnection = length(filter(additionalDependentResources, conn => conn.resource == 'storage')) > 0 -var hasAcrConnection = length(filter(additionalDependentResources, conn => conn.resource == 'registry')) > 0 -var hasExistingAcr = !empty(existingContainerRegistryResourceId) -var hasExistingAcrConnection = !empty(existingAcrConnectionName) -var hasExistingAppInsightsConnection = !empty(existingAppInsightsConnectionName) -var hasExistingAppInsightsConnectionString = !empty(existingApplicationInsightsConnectionString) -// Only create new App Insights resources if monitoring enabled and no existing connection/connection string -var shouldCreateAppInsights = enableMonitoring && !hasExistingAppInsightsConnection && !hasExistingAppInsightsConnectionString -var hasSearchConnection = length(filter(additionalDependentResources, conn => conn.resource == 'azure_ai_search')) > 0 -var hasBingConnection = length(filter(additionalDependentResources, conn => conn.resource == 'bing_grounding')) > 0 -var hasBingCustomConnection = length(filter(additionalDependentResources, conn => conn.resource == 'bing_custom_grounding')) > 0 - -// Extract connection names from ai.yaml for each resource type -var storageConnectionName = hasStorageConnection ? filter(additionalDependentResources, conn => conn.resource == 'storage')[0].connectionName : '' -var acrConnectionName = hasAcrConnection ? filter(additionalDependentResources, conn => conn.resource == 'registry')[0].connectionName : '' -var searchConnectionName = hasSearchConnection ? filter(additionalDependentResources, conn => conn.resource == 'azure_ai_search')[0].connectionName : '' -var bingConnectionName = hasBingConnection ? filter(additionalDependentResources, conn => conn.resource == 'bing_grounding')[0].connectionName : '' -var bingCustomConnectionName = hasBingCustomConnection ? filter(additionalDependentResources, conn => conn.resource == 'bing_custom_grounding')[0].connectionName : '' - -// Enable monitoring via Log Analytics and Application Insights -module logAnalytics '../monitor/loganalytics.bicep' = if (shouldCreateAppInsights) { - name: 'logAnalytics' - params: { - location: location - tags: tags - name: 'logs-${resourceToken}' - } -} - -module applicationInsights '../monitor/applicationinsights.bicep' = if (shouldCreateAppInsights) { - name: 'applicationInsights' - params: { - location: location - tags: tags - name: 'appi-${resourceToken}' - logAnalyticsWorkspaceId: logAnalytics.outputs.id - projectMIPrincipalId: aiAccount::project.identity.principalId - } -} - -// Always create a new AI Account for now (simplified approach) -// TODO: Add support for existing accounts in a future version -resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-06-01' = { - name: !empty(existingAiAccountName) ? existingAiAccountName : 'ai-account-${resourceToken}' - location: location - tags: tags - sku: { - name: 'S0' - } - kind: 'AIServices' - identity: { - type: 'SystemAssigned' - } - properties: { - allowProjectManagement: true - customSubDomainName: !empty(existingAiAccountName) ? existingAiAccountName : 'ai-account-${resourceToken}' - networkAcls: { - defaultAction: 'Allow' - virtualNetworkRules: [] - ipRules: [] - } - publicNetworkAccess: 'Enabled' - disableLocalAuth: true - } - - @batchSize(1) - resource seqDeployments 'deployments' = [ - for dep in (deployments??[]): { - name: dep.name - properties: { - model: dep.model - } - sku: dep.sku - } - ] - - resource project 'projects' = { - name: aiFoundryProjectName - location: location - identity: { - type: 'SystemAssigned' - } - properties: { - description: '${aiFoundryProjectName} Project' - displayName: '${aiFoundryProjectName}Project' - } - dependsOn: [ - seqDeployments - ] - } - - resource aiFoundryAccountCapabilityHost 'capabilityHosts@2025-10-01-preview' = if (enableHostedAgents && enableCapabilityHost) { - name: 'agents' - properties: { - capabilityHostKind: 'Agents' - // IMPORTANT: this is required to enable hosted agents deployment - // if no BYO Net is provided - enablePublicHostingEnvironment: true - } - } -} - - -// Create connection towards appinsights: -// - when we create a new App Insights resource, OR -// - when the user provided an existing App Insights connection string + resource ID but no existing connection name -// Both cases are merged into a single resource to avoid duplicate ARM resource definitions (which fail deployment). -var shouldCreateExistingAppInsightsConnection = enableMonitoring && hasExistingAppInsightsConnectionString && !hasExistingAppInsightsConnection && !empty(existingApplicationInsightsResourceId) -var shouldCreateAppInsightsConnection = shouldCreateAppInsights || shouldCreateExistingAppInsightsConnection - -resource appInsightConnection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = if (shouldCreateAppInsightsConnection) { - parent: aiAccount::project - name: 'appi-${resourceToken}' - properties: { - category: 'AppInsights' - target: shouldCreateAppInsights ? applicationInsights.outputs.id : existingApplicationInsightsResourceId - authType: 'ApiKey' - isSharedToAll: true - credentials: { - key: shouldCreateAppInsights ? applicationInsights.outputs.connectionString : existingApplicationInsightsConnectionString - } - metadata: { - ApiType: 'Azure' - ResourceId: shouldCreateAppInsights ? applicationInsights.outputs.id : existingApplicationInsightsResourceId - } - } -} - -// Create additional connections from ai.yaml configuration -module aiConnections './connection.bicep' = [for (connection, index) in connections: { - name: 'connection-${connection.name}' - params: { - aiServicesAccountName: aiAccount.name - aiProjectName: aiAccount::project.name - connectionConfig: connection - credentials: connectionCredentials[?connection.name] ?? {} - } -}] - -// Azure AI User for the developer, scoped to the Foundry Project. -// Project scope is sufficient for creating/running agents and calling models via the project endpoint. -resource localUserAzureAIUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: aiAccount::project - name: guid(subscription().id, resourceGroup().id, principalId, '53ca6127-db72-4b80-b1b0-d745d6d5456d') - properties: { - principalId: principalId - principalType: principalType - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d') - } -} - - -// All connections are now created directly within their respective resource modules -// using the centralized ./connection.bicep module - -// Storage module - deploy if storage connection is defined in ai.yaml -module storage '../storage/storage.bicep' = if (hasStorageConnection) { - name: 'storage' - params: { - location: location - tags: tags - resourceName: 'st${resourceToken}' - connectionName: storageConnectionName - principalId: principalId - principalType: principalType - aiServicesAccountName: aiAccount.name - aiProjectName: aiAccount::project.name - } -} - -// Azure Container Registry module - deploy if ACR connection is defined in ai.yaml -module acr '../host/acr.bicep' = if (hasAcrConnection) { - name: 'acr' - params: { - location: location - tags: tags - resourceName: '${abbrs.containerRegistryRegistries}${resourceToken}' - connectionName: acrConnectionName - principalId: principalId - principalType: principalType - aiServicesAccountName: aiAccount.name - aiProjectName: aiAccount::project.name - } -} - -// Connection for existing ACR - create if user provided an existing ACR resource ID but no existing connection -module existingAcrConnection './connection.bicep' = if (hasExistingAcr && !hasExistingAcrConnection) { - name: 'existing-acr-connection' - params: { - aiServicesAccountName: aiAccount.name - aiProjectName: aiAccount::project.name - connectionConfig: { - name: 'acr-${resourceToken}' - category: 'ContainerRegistry' - target: existingContainerRegistryEndpoint - authType: 'ManagedIdentity' - isSharedToAll: true - metadata: { - ResourceId: existingContainerRegistryResourceId - } - } - credentials: { - clientId: aiAccount::project.identity.principalId - resourceId: existingContainerRegistryResourceId - } - } -} - -// Extract resource group name from the existing ACR resource ID -// Resource ID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ContainerRegistry/registries/{name} -var existingAcrResourceGroup = hasExistingAcr ? split(existingContainerRegistryResourceId, '/')[4] : '' -var existingAcrName = hasExistingAcr ? last(split(existingContainerRegistryResourceId, '/')) : '' - -// Grant AcrPull role to the AI project's managed identity on the existing ACR -// This allows the hosted agents to pull images from the user-provided registry -// Note: User must have permission to assign roles on the existing ACR (Owner or User Access Administrator) -// Using a module allows scoping to a different resource group if the ACR isn't in the same RG -// Skip if connection already exists (role assignment should already be in place) -module existingAcrRoleAssignment './acr-role-assignment.bicep' = if (hasExistingAcr && !hasExistingAcrConnection) { - name: 'existing-acr-role-assignment' - scope: resourceGroup(existingAcrResourceGroup) - params: { - acrName: existingAcrName - acrResourceId: existingContainerRegistryResourceId - principalId: aiAccount::project.identity.principalId - } -} - -// Bing Search grounding module - deploy if Bing connection is defined in ai.yaml or parameter is enabled -module bingGrounding '../search/bing_grounding.bicep' = if (hasBingConnection) { - name: 'bing-grounding' - params: { - tags: tags - resourceName: 'bing-${resourceToken}' - connectionName: bingConnectionName - aiServicesAccountName: aiAccount.name - aiProjectName: aiAccount::project.name - } -} - -// Bing Custom Search grounding module - deploy if custom Bing connection is defined in ai.yaml or parameter is enabled -module bingCustomGrounding '../search/bing_custom_grounding.bicep' = if (hasBingCustomConnection) { - name: 'bing-custom-grounding' - params: { - tags: tags - resourceName: 'bingcustom-${resourceToken}' - connectionName: bingCustomConnectionName - aiServicesAccountName: aiAccount.name - aiProjectName: aiAccount::project.name - } -} - -// Azure AI Search module - deploy if search connection is defined in ai.yaml -module azureAiSearch '../search/azure_ai_search.bicep' = if (hasSearchConnection) { - name: 'azure-ai-search' - params: { - tags: tags - resourceName: 'search-${resourceToken}' - connectionName: searchConnectionName - storageAccountResourceId: hasStorageConnection ? storage!.outputs.storageAccountId : '' - containerName: 'knowledge' - aiServicesAccountName: aiAccount.name - aiProjectName: aiAccount::project.name - principalId: principalId - principalType: principalType - location: location - } -} - -// Outputs -output AZURE_AI_PROJECT_ENDPOINT string = aiAccount::project.properties.endpoints['AI Foundry API'] -output AZURE_OPENAI_ENDPOINT string = aiAccount.properties.endpoints['OpenAI Language Model Instance API'] -output aiServicesEndpoint string = aiAccount.properties.endpoint -output accountId string = aiAccount.id -output projectId string = aiAccount::project.id -output aiServicesAccountName string = aiAccount.name -output aiServicesProjectName string = aiAccount::project.name -output aiServicesPrincipalId string = aiAccount.identity.principalId -output projectName string = aiAccount::project.name -output APPLICATIONINSIGHTS_CONNECTION_STRING string = shouldCreateAppInsights ? applicationInsights.outputs.connectionString : (hasExistingAppInsightsConnectionString ? existingApplicationInsightsConnectionString : '') -output APPLICATIONINSIGHTS_RESOURCE_ID string = shouldCreateAppInsights ? applicationInsights.outputs.id : (hasExistingAppInsightsConnectionString ? existingApplicationInsightsResourceId : '') - -// Connection outputs from the connections array -output connectionIds array = [for (connection, index) in (connections ?? []): { - name: aiConnections[index].outputs.connectionName - id: aiConnections[index].outputs.connectionId -}] - -// Grouped dependent resources outputs -output dependentResources object = { - registry: { - name: hasAcrConnection ? acr!.outputs.containerRegistryName : '' - loginServer: hasAcrConnection ? acr!.outputs.containerRegistryLoginServer : ((hasExistingAcr || hasExistingAcrConnection) ? existingContainerRegistryEndpoint : '') - connectionName: hasAcrConnection ? acr!.outputs.containerRegistryConnectionName : (hasExistingAcrConnection ? existingAcrConnectionName : (hasExistingAcr ? 'acr-${resourceToken}' : '')) - } - bing_grounding: { - name: (hasBingConnection) ? bingGrounding!.outputs.bingGroundingName : '' - connectionName: (hasBingConnection) ? bingGrounding!.outputs.bingGroundingConnectionName : '' - connectionId: (hasBingConnection) ? bingGrounding!.outputs.bingGroundingConnectionId : '' - } - bing_custom_grounding: { - name: (hasBingCustomConnection) ? bingCustomGrounding!.outputs.bingCustomGroundingName : '' - connectionName: (hasBingCustomConnection) ? bingCustomGrounding!.outputs.bingCustomGroundingConnectionName : '' - connectionId: (hasBingCustomConnection) ? bingCustomGrounding!.outputs.bingCustomGroundingConnectionId : '' - } - search: { - serviceName: hasSearchConnection ? azureAiSearch!.outputs.searchServiceName : '' - connectionName: hasSearchConnection ? azureAiSearch!.outputs.searchConnectionName : '' - } - storage: { - accountName: hasStorageConnection ? storage!.outputs.storageAccountName : '' - connectionName: hasStorageConnection ? storage!.outputs.storageConnectionName : '' - } -} - -type deploymentsType = { - @description('Specify the name of cognitive service account deployment.') - name: string - - @description('Required. Properties of Cognitive Services account deployment model.') - model: { - @description('Required. The name of Cognitive Services account deployment model.') - name: string - - @description('Required. The format of Cognitive Services account deployment model.') - format: string - - @description('Required. The version of Cognitive Services account deployment model.') - version: string - } - - @description('The resource model definition representing SKU.') - sku: { - @description('Required. The name of the resource model definition representing SKU.') - name: string - - @description('The capacity of the resource model definition representing SKU.') - capacity: int - } -}[]? - -type dependentResourcesType = { - @description('The type of dependent resource to create') - resource: 'storage' | 'registry' | 'azure_ai_search' | 'bing_grounding' | 'bing_custom_grounding' - - @description('The connection name for this resource') - connectionName: string -}[] diff --git a/infra/core/ai/connection.bicep b/infra/core/ai/connection.bicep deleted file mode 100644 index a087266..0000000 --- a/infra/core/ai/connection.bicep +++ /dev/null @@ -1,112 +0,0 @@ -targetScope = 'resourceGroup' - -@description('AI Services account name') -param aiServicesAccountName string - -@description('AI project name') -param aiProjectName string - -// Connection configuration type definition -type ConnectionConfig = { - @description('Name of the connection') - name: string - - @description('Category of the connection (e.g., ContainerRegistry, AzureStorageAccount, CognitiveSearch, AzureOpenAI)') - category: string - - @description('Target endpoint or URL for the connection') - target: string - - @description('Authentication type') - authType: 'AAD' | 'AccessKey' | 'AccountKey' | 'AgenticIdentity' | 'ApiKey' | 'CustomKeys' | 'ManagedIdentity' | 'None' | 'OAuth2' | 'PAT' | 'SAS' | 'ServicePrincipal' | 'UsernamePassword' | 'UserEntraToken' | 'ProjectManagedIdentity' - - @description('Whether the connection is shared to all users (optional, defaults to true)') - isSharedToAll: bool? - - @description('Additional metadata for the connection (optional)') - metadata: object? - - @description('Error message if the connection fails (optional)') - error: string? - - @description('Expiry time for the connection (optional)') - expiryTime: string? - - @description('Private endpoint requirement: Required, NotRequired, or NotApplicable (optional)') - peRequirement: ('NotApplicable' | 'NotRequired' | 'Required')? - - @description('Private endpoint status: Active, Inactive, or NotApplicable (optional)') - peStatus: ('Active' | 'Inactive' | 'NotApplicable')? - - @description('List of users to share the connection with (optional, alternative to isSharedToAll)') - sharedUserList: string[]? - - @description('Whether to use workspace managed identity (optional)') - useWorkspaceManagedIdentity: bool? - - @description('OAuth2 authorization endpoint URL (optional, OAuth2 authType only)') - authorizationUrl: string? - - @description('OAuth2 token endpoint URL (optional, OAuth2 authType only)') - tokenUrl: string? - - @description('OAuth2 refresh token endpoint URL (optional, OAuth2 authType only)') - refreshUrl: string? - - @description('OAuth2 scopes to request (optional, OAuth2 authType only)') - scopes: string[]? - - @description('Token audience for UserEntraToken / AgenticIdentity auth types (optional)') - audience: string? - - @description('Managed connector name for OAuth2 managed connectors (optional)') - connectorName: string? -} - -@description('Connection configuration') -param connectionConfig ConnectionConfig - -@secure() -@description('Credentials for the connection. Kept as a separate @secure parameter to prevent secrets from appearing in deployment logs. Shape depends on authType — e.g. { key: "..." } for ApiKey, { clientId: "...", clientSecret: "..." } for OAuth2/ServicePrincipal.') -param credentials object = {} - - -// Get reference to the AI Services account and project -resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { - name: aiServicesAccountName - - resource project 'projects' existing = { - name: aiProjectName - } -} - -// Create the connection -resource connection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = { - parent: aiAccount::project - name: connectionConfig.name - properties: { - category: connectionConfig.category - target: connectionConfig.target - authType: connectionConfig.authType - isSharedToAll: connectionConfig.?isSharedToAll ?? true - credentials: !empty(credentials) ? credentials : null - metadata: connectionConfig.?metadata - // Only include if they appear in the connectionConfig - ...connectionConfig.?error != null ? { error: connectionConfig.?error } : {} - ...connectionConfig.?expiryTime != null ? { expiryTime: connectionConfig.?expiryTime } : {} - ...connectionConfig.?peRequirement != null ? { peRequirement: connectionConfig.?peRequirement } : {} - ...connectionConfig.?peStatus != null ? { peStatus: connectionConfig.?peStatus } : {} - ...connectionConfig.?sharedUserList != null ? { sharedUserList: connectionConfig.?sharedUserList } : {} - ...connectionConfig.?useWorkspaceManagedIdentity != null ? { useWorkspaceManagedIdentity: connectionConfig.?useWorkspaceManagedIdentity } : {} - ...connectionConfig.?authorizationUrl != null ? { authorizationUrl: connectionConfig.?authorizationUrl } : {} - ...connectionConfig.?tokenUrl != null ? { tokenUrl: connectionConfig.?tokenUrl } : {} - ...connectionConfig.?refreshUrl != null ? { refreshUrl: connectionConfig.?refreshUrl } : {} - ...connectionConfig.?scopes != null ? { scopes: connectionConfig.?scopes } : {} - ...connectionConfig.?audience != null ? { audience: connectionConfig.?audience } : {} - ...connectionConfig.?connectorName != null ? { connectorName: connectionConfig.?connectorName } : {} - } -} - -// Outputs -output connectionName string = connection.name -output connectionId string = connection.id diff --git a/infra/core/ai/existing-ai-project.bicep b/infra/core/ai/existing-ai-project.bicep deleted file mode 100644 index 4f057b0..0000000 --- a/infra/core/ai/existing-ai-project.bicep +++ /dev/null @@ -1,70 +0,0 @@ -targetScope = 'resourceGroup' - -@description('Name of the existing AI Services account') -param aiServicesAccountName string - -@description('Name of the existing AI Foundry project') -param aiFoundryProjectName string - -@description('Existing ACR connection name (already set in the environment)') -param existingAcrConnectionName string = '' - -@description('Existing container registry endpoint (already set in the environment)') -param existingContainerRegistryEndpoint string = '' - -@description('Existing Application Insights connection string (already set in the environment)') -param existingApplicationInsightsConnectionString string = '' - -@description('Existing Application Insights resource ID (already set in the environment)') -param existingApplicationInsightsResourceId string = '' - -// Reference the existing account and project — read-only, no modifications -resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = { - name: aiServicesAccountName - - resource project 'projects' existing = { - name: aiFoundryProjectName - } -} - -// Outputs — same shape as ai-project.bicep so main.bicep can use either interchangeably -output AZURE_AI_PROJECT_ENDPOINT string = aiAccount::project.properties.endpoints['AI Foundry API'] -output AZURE_OPENAI_ENDPOINT string = aiAccount.properties.endpoints['OpenAI Language Model Instance API'] -output aiServicesEndpoint string = aiAccount.properties.endpoint -output accountId string = aiAccount.id -output projectId string = aiAccount::project.id -output aiServicesAccountName string = aiAccount.name -output aiServicesProjectName string = aiAccount::project.name -output aiServicesPrincipalId string = aiAccount.identity.principalId -output projectName string = aiAccount::project.name -output APPLICATIONINSIGHTS_CONNECTION_STRING string = existingApplicationInsightsConnectionString -output APPLICATIONINSIGHTS_RESOURCE_ID string = existingApplicationInsightsResourceId - -// Empty connection outputs — these are already set in the azd environment from init -output connectionIds array = [] - -output dependentResources object = { - registry: { - name: '' - loginServer: existingContainerRegistryEndpoint - connectionName: existingAcrConnectionName - } - bing_grounding: { - name: '' - connectionName: '' - connectionId: '' - } - bing_custom_grounding: { - name: '' - connectionName: '' - connectionId: '' - } - search: { - serviceName: '' - connectionName: '' - } - storage: { - accountName: '' - connectionName: '' - } -} diff --git a/infra/core/host/acr.bicep b/infra/core/host/acr.bicep deleted file mode 100644 index 92c80c5..0000000 --- a/infra/core/host/acr.bicep +++ /dev/null @@ -1,88 +0,0 @@ -targetScope = 'resourceGroup' - -@description('The location used for all deployed resources') -param location string = resourceGroup().location - -@description('Tags that will be applied to all resources') -param tags object = {} - -@description('Resource name for the container registry') -param resourceName string - -@description('Id of the user or app to assign application roles') -param principalId string - -@description('Principal type of user or app') -param principalType string - -@description('AI Services account name for the project parent') -param aiServicesAccountName string = '' - -@description('AI project name for creating the connection') -param aiProjectName string = '' - -@description('Name for the AI Foundry ACR connection') -param connectionName string - -// Get reference to the AI Services account and project to access their managed identities -resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: aiServicesAccountName - - resource aiProject 'projects' existing = { - name: aiProjectName - } -} - -// Create the Container Registry -module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { - name: 'registry' - params: { - name: resourceName - location: location - tags: tags - publicNetworkAccess: 'Enabled' - roleAssignments:[ - { - principalId: principalId - principalType: principalType - // Container Registry Tasks Contributor — build images with ACR tasks and push container images - roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fb382eab-e894-4461-af04-94435c366c3f') - } - // TODO SEPARATELY - { - // the foundry project itself can pull from the ACR - principalId: aiAccount::aiProject.identity.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } - ] - } -} - -// Create the ACR connection using the centralized connection module -module acrConnection '../ai/connection.bicep' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: 'acr-connection-creation' - params: { - aiServicesAccountName: aiServicesAccountName - aiProjectName: aiProjectName - connectionConfig: { - name: connectionName - category: 'ContainerRegistry' - target: containerRegistry.outputs.loginServer - authType: 'ManagedIdentity' - isSharedToAll: true - metadata: { - ResourceId: containerRegistry.outputs.resourceId - } - } - credentials: { - clientId: aiAccount::aiProject.identity.principalId - resourceId: containerRegistry.outputs.resourceId - } - } -} - -output containerRegistryName string = containerRegistry.outputs.name -output containerRegistryLoginServer string = containerRegistry.outputs.loginServer -output containerRegistryResourceId string = containerRegistry.outputs.resourceId -output containerRegistryConnectionName string = acrConnection.outputs.connectionName diff --git a/infra/core/monitor/applicationinsights-dashboard.bicep b/infra/core/monitor/applicationinsights-dashboard.bicep deleted file mode 100644 index f3e0952..0000000 --- a/infra/core/monitor/applicationinsights-dashboard.bicep +++ /dev/null @@ -1,1236 +0,0 @@ -metadata description = 'Creates a dashboard for an Application Insights instance.' -param name string -param applicationInsightsName string -param location string = resourceGroup().location -param tags object = {} - -// 2020-09-01-preview because that is the latest valid version -resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { - name: name - location: location - tags: tags - properties: { - lenses: [ - { - order: 0 - parts: [ - { - position: { - x: 0 - y: 0 - colSpan: 2 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'id' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' - asset: { - idInputName: 'id' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'overview' - } - } - { - position: { - x: 2 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'ProactiveDetection' - } - } - { - position: { - x: 3 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 4 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-04T01:20:33.345Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 5 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-08T18:47:35.237Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'ConfigurationId' - value: '78ce933e-e864-4b05-a27b-71fd55a6afad' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 0 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Usage' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 3 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-04T01:22:35.782Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 4 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Reliability' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 7 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'DataModel' - value: { - version: '1.0.0' - timeContext: { - durationMs: 86400000 - createdTime: '2018-05-04T23:42:40.072Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - isOptional: true - } - { - name: 'ConfigurationId' - value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' - isAdapter: true - asset: { - idInputName: 'ResourceId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'failures' - } - } - { - position: { - x: 8 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Responsiveness\r\n' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 11 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'DataModel' - value: { - version: '1.0.0' - timeContext: { - durationMs: 86400000 - createdTime: '2018-05-04T23:43:37.804Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - isOptional: true - } - { - name: 'ConfigurationId' - value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' - isAdapter: true - asset: { - idInputName: 'ResourceId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'performance' - } - } - { - position: { - x: 12 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Browser' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 15 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'MetricsExplorerJsonDefinitionId' - value: 'BrowserPerformanceTimelineMetrics' - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - createdTime: '2018-05-08T12:16:27.534Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'CurrentFilter' - value: { - eventTypes: [ - 4 - 1 - 3 - 5 - 2 - 6 - 13 - ] - typeFacets: {} - isPermissive: false - } - } - { - name: 'id' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'browser' - } - } - { - position: { - x: 0 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'sessions/count' - aggregationType: 5 - namespace: 'microsoft.insights/components/kusto' - metricVisualization: { - displayName: 'Sessions' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'users/count' - aggregationType: 5 - namespace: 'microsoft.insights/components/kusto' - metricVisualization: { - displayName: 'Users' - color: '#7E58FF' - } - } - ] - title: 'Unique sessions and users' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'segmentationUsers' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'requests/failed' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Failed requests' - color: '#EC008C' - } - } - ] - title: 'Failed requests' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'failures' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'requests/duration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Server response time' - color: '#00BCF2' - } - } - ] - title: 'Server response time' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'performance' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 12 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/networkDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Page load network connect time' - color: '#7E58FF' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/processingDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Client processing time' - color: '#44F1C8' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/sendDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Send request time' - color: '#EB9371' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/receiveDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Receiving response time' - color: '#0672F1' - } - } - ] - title: 'Average page load time breakdown' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 0 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'availabilityResults/availabilityPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Availability' - color: '#47BDF5' - } - } - ] - title: 'Average availability' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'availability' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'exceptions/server' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Server exceptions' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'dependencies/failed' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Dependency failures' - color: '#7E58FF' - } - } - ] - title: 'Server exceptions and Dependency failures' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processorCpuPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Processor time' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processCpuPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Process CPU' - color: '#7E58FF' - } - } - ] - title: 'Average processor and process CPU utilization' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 12 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'exceptions/browser' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Browser exceptions' - color: '#47BDF5' - } - } - ] - title: 'Browser exceptions' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 0 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'availabilityResults/count' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Availability test results count' - color: '#47BDF5' - } - } - ] - title: 'Availability test results count' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processIOBytesPerSecond' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Process IO rate' - color: '#47BDF5' - } - } - ] - title: 'Average process I/O rate' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/memoryAvailableBytes' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Available memory' - color: '#47BDF5' - } - } - ] - title: 'Average available memory' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - ] - } - ] - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { - name: applicationInsightsName -} diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep deleted file mode 100644 index 18b6176..0000000 --- a/infra/core/monitor/applicationinsights.bicep +++ /dev/null @@ -1,47 +0,0 @@ -metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' -param name string -param dashboardName string = '' -param location string = resourceGroup().location -param tags object = {} -param logAnalyticsWorkspaceId string - -@description('Optional. Principal ID of the Foundry Project managed identity to grant Log Analytics Reader.') -param projectMIPrincipalId string = '' - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: name - location: location - tags: tags - kind: 'web' - properties: { - Application_Type: 'web' - WorkspaceResourceId: logAnalyticsWorkspaceId - } -} - -module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { - name: 'application-insights-dashboard' - params: { - name: dashboardName - location: location - applicationInsightsName: applicationInsights.name - } -} - -// Log Analytics Reader for the Foundry Project managed identity. -// Required for running evaluations on traces generated by agents. -resource logAnalyticsReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(projectMIPrincipalId)) { - scope: applicationInsights - name: guid(applicationInsights.id, projectMIPrincipalId, '73c42c96-874c-492b-b04d-ab87d138a893') - properties: { - principalId: projectMIPrincipalId - principalType: 'ServicePrincipal' - // Log Analytics Reader - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '73c42c96-874c-492b-b04d-ab87d138a893') - } -} - -output connectionString string = applicationInsights.properties.ConnectionString -output id string = applicationInsights.id -output instrumentationKey string = applicationInsights.properties.InstrumentationKey -output name string = applicationInsights.name diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep deleted file mode 100644 index bf87f54..0000000 --- a/infra/core/monitor/loganalytics.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Creates a Log Analytics workspace.' -param name string -param location string = resourceGroup().location -param tags object = {} - -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: name - location: location - tags: tags - properties: any({ - retentionInDays: 30 - features: { - searchVersion: 1 - } - sku: { - name: 'PerGB2018' - } - }) -} - -output id string = logAnalytics.id -output name string = logAnalytics.name diff --git a/infra/core/search/azure_ai_search.bicep b/infra/core/search/azure_ai_search.bicep deleted file mode 100644 index b4c8287..0000000 --- a/infra/core/search/azure_ai_search.bicep +++ /dev/null @@ -1,211 +0,0 @@ -targetScope = 'resourceGroup' - -@description('Tags that will be applied to all resources') -param tags object = {} - -@description('Azure Search resource name') -param resourceName string - -@description('Azure Search SKU name') -param azureSearchSkuName string = 'basic' - -@description('Azure storage account resource ID') -param storageAccountResourceId string - -@description('container name') -param containerName string = 'knowledgebase' - -@description('AI Services account name for the project parent') -param aiServicesAccountName string = '' - -@description('AI project name for creating the connection') -param aiProjectName string = '' - -@description('Id of the user or app to assign application roles') -param principalId string - -@description('Principal type of user or app') -param principalType string - -@description('Name for the AI Foundry search connection') -param connectionName string - -@description('Location for all resources') -param location string = resourceGroup().location - -// Get reference to the AI Services account and project to access their managed identities -resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: aiServicesAccountName - - resource aiProject 'projects' existing = { - name: aiProjectName - } -} - -// Azure Search Service -resource searchService 'Microsoft.Search/searchServices@2024-06-01-preview' = { - name: resourceName - location: location - tags: tags - sku: { - name: azureSearchSkuName - } - identity: { - type: 'SystemAssigned' - } - properties: { - replicaCount: 1 - partitionCount: 1 - hostingMode: 'default' - authOptions: { - aadOrApiKey: { - aadAuthFailureMode: 'http401WithBearerChallenge' - } - } - disableLocalAuth: false - encryptionWithCmk: { - enforcement: 'Unspecified' - } - publicNetworkAccess: 'enabled' - } -} - -// Reference to existing Storage Account -resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { - name: last(split(storageAccountResourceId, '/')) -} - -// Reference to existing Blob Service -resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' existing = { - parent: storageAccount - name: 'default' -} - -// Storage Container (create if it doesn't exist) -resource storageContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = { - parent: blobService - name: containerName - properties: { - publicAccess: 'None' - } -} - -// RBAC Assignments - -// Search needs to read from Storage -resource searchToStorageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storageAccount.id, searchService.id, 'Storage Blob Data Reader', uniqueString(deployment().name)) - scope: storageAccount - properties: { - // GOOD - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1') // Storage Blob Data Reader - principalId: searchService.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// Search needs OpenAI access (AI Services account) -resource searchToAIServicesRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiServicesAccountName)) { - name: guid(aiServicesAccountName, searchService.id, 'Cognitive Services OpenAI User', uniqueString(deployment().name)) - properties: { - // GOOD - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') // Cognitive Services OpenAI User - principalId: searchService.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// AI Project needs Search access - Service Contributor -resource aiServicesToSearchServiceRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: guid(searchService.id, aiServicesAccountName, aiProjectName, 'Search Service Contributor', uniqueString(deployment().name)) - scope: searchService - properties: { - // GOOD - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0') // Search Service Contributor - principalId: aiAccount::aiProject.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// AI Project needs Search access - Index Data Contributor -resource aiServicesToSearchDataRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: guid(searchService.id, aiServicesAccountName, aiProjectName, 'Search Index Data Contributor', uniqueString(deployment().name)) - scope: searchService - properties: { - // GOOD - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7') // Search Index Data Contributor - principalId: aiAccount::aiProject.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// User permissions - Search Index Data Contributor -resource userToSearchRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(searchService.id, principalId, 'Search Index Data Contributor', uniqueString(deployment().name)) - scope: searchService - properties: { - // GOOD - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7') // Search Index Data Contributor - principalId: principalId - principalType: principalType - } -} - -// // User permissions - Storage Blob Data Contributor -// resource userToStorageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { -// name: guid(storageAccount.id, principalId, 'Storage Blob Data Contributor', uniqueString(deployment().name)) -// scope: storageAccount -// properties: { -// roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor -// principalId: principalId -// principalType: principalType -// } -// } - -// // Project needs Search access - Index Data Contributor -// resource projectToSearchRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { -// name: guid(searchService.id, aiProjectName, 'Search Index Data Contributor', uniqueString(deployment().name)) -// scope: searchService -// properties: { -// roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7') // Search Index Data Contributor -// principalId: aiAccountPrincipalId // Using AI account principal ID as project identity -// principalType: 'ServicePrincipal' -// } -// } - -// Create the AI Search connection using the centralized connection module -module aiSearchConnection '../ai/connection.bicep' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: 'ai-search-connection-creation' - params: { - aiServicesAccountName: aiServicesAccountName - aiProjectName: aiProjectName - connectionConfig: { - name: connectionName - category: 'CognitiveSearch' - target: 'https://${searchService.name}.search.windows.net' - authType: 'AAD' - isSharedToAll: true - metadata: { - ApiVersion: '2024-07-01' - ResourceId: searchService.id - ApiType: 'Azure' - type: 'azure_ai_search' - } - } - } - dependsOn: [ - aiServicesToSearchDataRoleAssignment - ] -} - -// Outputs -output searchServiceName string = searchService.name -output searchServiceId string = searchService.id -output searchServicePrincipalId string = searchService.identity.principalId -output storageAccountName string = storageAccount.name -output storageAccountId string = storageAccount.id -output containerName string = storageContainer.name -output storageAccountPrincipalId string = storageAccount.identity.principalId -output searchConnectionName string = (!empty(aiServicesAccountName) && !empty(aiProjectName)) ? aiSearchConnection!.outputs.connectionName : '' -output searchConnectionId string = (!empty(aiServicesAccountName) && !empty(aiProjectName)) ? aiSearchConnection!.outputs.connectionId : '' - diff --git a/infra/core/search/bing_custom_grounding.bicep b/infra/core/search/bing_custom_grounding.bicep deleted file mode 100644 index 9361bd4..0000000 --- a/infra/core/search/bing_custom_grounding.bicep +++ /dev/null @@ -1,84 +0,0 @@ -targetScope = 'resourceGroup' - -@description('Tags that will be applied to all resources') -param tags object = {} - -@description('Bing custom grounding resource name') -param resourceName string - -@description('AI Services account name for the project parent') -param aiServicesAccountName string = '' - -@description('AI project name for creating the connection') -param aiProjectName string = '' - -@description('Name for the AI Foundry Bing Custom Search connection') -param connectionName string - -// Get reference to the AI Services account and project to access their managed identities -resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: aiServicesAccountName - - resource aiProject 'projects' existing = { - name: aiProjectName - } -} - -// Bing Search resource for grounding capability -resource bingCustomSearch 'Microsoft.Bing/accounts@2020-06-10' = { - name: resourceName - location: 'global' - tags: tags - sku: { - name: 'G1' - } - properties: { - statisticsEnabled: false - } - kind: 'Bing.CustomGrounding' -} - -// Role assignment to allow AI project to use Bing Search -resource bingCustomSearchRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - scope: bingCustomSearch - name: guid(subscription().id, resourceGroup().id, 'bing-search-role', aiServicesAccountName, aiProjectName) - properties: { - principalId: aiAccount::aiProject.identity.principalId - principalType: 'ServicePrincipal' - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908') // Cognitive Services User - } -} - -// Create the Bing Custom Search connection using the centralized connection module -module aiSearchConnection '../ai/connection.bicep' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: 'bing-custom-search-connection-creation' - params: { - aiServicesAccountName: aiServicesAccountName - aiProjectName: aiProjectName - connectionConfig: { - name: connectionName - category: 'GroundingWithCustomSearch' - target: bingCustomSearch.properties.endpoint - authType: 'ApiKey' - isSharedToAll: true - metadata: { - Location: 'global' - ResourceId: bingCustomSearch.id - ApiType: 'Azure' - type: 'bing_custom_search' - } - } - credentials: { - key: bingCustomSearch.listKeys().key1 - } - } - dependsOn: [ - bingCustomSearchRoleAssignment - ] -} - -// Outputs -output bingCustomGroundingName string = bingCustomSearch.name -output bingCustomGroundingConnectionName string = aiSearchConnection.outputs.connectionName -output bingCustomGroundingResourceId string = bingCustomSearch.id -output bingCustomGroundingConnectionId string = aiSearchConnection.outputs.connectionId diff --git a/infra/core/search/bing_grounding.bicep b/infra/core/search/bing_grounding.bicep deleted file mode 100644 index 6c9fd15..0000000 --- a/infra/core/search/bing_grounding.bicep +++ /dev/null @@ -1,83 +0,0 @@ -targetScope = 'resourceGroup' - -@description('Tags that will be applied to all resources') -param tags object = {} - -@description('Bing grounding resource name') -param resourceName string - -@description('AI Services account name for the project parent') -param aiServicesAccountName string = '' - -@description('AI project name for creating the connection') -param aiProjectName string = '' - -@description('Name for the AI Foundry Bing Search connection') -param connectionName string - -// Get reference to the AI Services account and project to access their managed identities -resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: aiServicesAccountName - - resource aiProject 'projects' existing = { - name: aiProjectName - } -} - -// Bing Search resource for grounding capability -resource bingSearch 'Microsoft.Bing/accounts@2020-06-10' = { - name: resourceName - location: 'global' - tags: tags - sku: { - name: 'G1' - } - properties: { - statisticsEnabled: false - } - kind: 'Bing.Grounding' -} - -// Role assignment to allow AI project to use Bing Search -resource bingSearchRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - scope: bingSearch - name: guid(subscription().id, resourceGroup().id, 'bing-search-role', aiServicesAccountName, aiProjectName) - properties: { - principalId: aiAccount::aiProject.identity.principalId - principalType: 'ServicePrincipal' - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908') // Cognitive Services User - } -} - -// Create the Bing Search connection using the centralized connection module -module bingSearchConnection '../ai/connection.bicep' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: 'bing-search-connection-creation' - params: { - aiServicesAccountName: aiServicesAccountName - aiProjectName: aiProjectName - connectionConfig: { - name: connectionName - category: 'GroundingWithBingSearch' - target: bingSearch.properties.endpoint - authType: 'ApiKey' - isSharedToAll: true - metadata: { - Location: 'global' - ResourceId: bingSearch.id - ApiType: 'Azure' - type: 'bing_grounding' - } - } - credentials: { - key: bingSearch.listKeys().key1 - } - } - dependsOn: [ - bingSearchRoleAssignment - ] -} - -output bingGroundingName string = bingSearch.name -output bingGroundingConnectionName string = bingSearchConnection.outputs.connectionName -output bingGroundingResourceId string = bingSearch.id -output bingGroundingConnectionId string = bingSearchConnection.outputs.connectionId diff --git a/infra/core/storage/storage.bicep b/infra/core/storage/storage.bicep deleted file mode 100644 index 7a35761..0000000 --- a/infra/core/storage/storage.bicep +++ /dev/null @@ -1,113 +0,0 @@ -targetScope = 'resourceGroup' - -@description('The location used for all deployed resources') -param location string = resourceGroup().location - -@description('Tags that will be applied to all resources') -param tags object = {} - -@description('Storage account resource name') -param resourceName string - -@description('Id of the user or app to assign application roles') -param principalId string - -@description('Principal type of user or app') -param principalType string - -@description('AI Services account name for the project parent') -param aiServicesAccountName string = '' - -@description('AI project name for creating the connection') -param aiProjectName string = '' - -@description('Name for the AI Foundry storage connection') -param connectionName string - -// Storage Account for the AI Services account -resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { - name: resourceName - location: location - tags: tags - sku: { - name: 'Standard_LRS' - } - kind: 'StorageV2' - identity: { - type: 'SystemAssigned' - } - properties: { - supportsHttpsTrafficOnly: true - allowBlobPublicAccess: false - minimumTlsVersion: 'TLS1_2' - accessTier: 'Hot' - encryption: { - services: { - blob: { - enabled: true - } - file: { - enabled: true - } - } - keySource: 'Microsoft.Storage' - } - } -} - -// Get reference to the AI Services account and project to access their managed identities -resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: aiServicesAccountName - - resource aiProject 'projects' existing = { - name: aiProjectName - } -} - -// Role assignment for AI Services to access the storage account -resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: guid(storageAccount.id, aiAccount.id, 'ai-storage-contributor') - scope: storageAccount - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor - principalId: aiAccount::aiProject.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// User permissions - Storage Blob Data Contributor -resource userStorageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storageAccount.id, principalId, 'Storage Blob Data Contributor') - scope: storageAccount - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor - principalId: principalId - principalType: principalType - } -} - -// Create the storage connection using the centralized connection module -module storageConnection '../ai/connection.bicep' = if (!empty(aiServicesAccountName) && !empty(aiProjectName)) { - name: 'storage-connection-creation' - params: { - aiServicesAccountName: aiServicesAccountName - aiProjectName: aiProjectName - connectionConfig: { - name: connectionName - category: 'AzureStorageAccount' - target: storageAccount.properties.primaryEndpoints.blob - authType: 'AAD' - isSharedToAll: true - metadata: { - ApiType: 'Azure' - ResourceId: storageAccount.id - location: storageAccount.location - } - } - } -} - -output storageAccountName string = storageAccount.name -output storageAccountId string = storageAccount.id -output storageAccountPrincipalId string = storageAccount.identity.principalId -output storageConnectionName string = storageConnection.outputs.connectionName diff --git a/infra/main.bicep b/infra/main.bicep index 6e8d7b6..4a1fc7d 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,5 +1,4 @@ targetScope = 'subscription' -// targetScope = 'resourceGroup' @minLength(1) @maxLength(64) @@ -11,227 +10,121 @@ param environmentName string @description('Name of the resource group to use or create') param resourceGroupName string = 'rg-${environmentName}' -// Restricted locations to match list from -// https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/responses?tabs=python-key#region-availability @minLength(1) @description('Primary location for all resources') -@allowed([ - 'australiaeast' - 'brazilsouth' - 'canadacentral' - 'canadaeast' - 'eastus' - 'eastus2' - 'francecentral' - 'germanywestcentral' - 'italynorth' - 'japaneast' - 'koreacentral' - 'northcentralus' - 'norwayeast' - 'polandcentral' - 'southafricanorth' - 'southcentralus' - 'southeastasia' - 'southindia' - 'spaincentral' - 'swedencentral' - 'switzerlandnorth' - 'uaenorth' - 'uksouth' - 'westus' - 'westus2' - 'westus3' -]) param location string -param aiDeploymentsLocation string - @description('Id of the user or app to assign application roles') param principalId string @description('Principal type of user or app') param principalType string -@description('Optional. Name of an existing AI Services account within the resource group. If not provided, a new one will be created.') +@description('Optional. Name of the AI Account. If not provided, a new one will be created with an auto-generated name.') param aiFoundryResourceName string = '' -@description('Optional. Name of the AI Foundry project. If not provided, a default name will be used.') +@description('Name of the AI Foundry project') param aiFoundryProjectName string = 'ai-project-${environmentName}' -@description('List of model deployments') +@description('When true, reference an existing Foundry project instead of creating one') +param useExistingAiProject bool = false + +// Extension-injected from azure.yaml service config +@description('Model deployments (JSON array from azure.yaml)') param aiProjectDeploymentsJson string = '[]' -@description('List of connections') +@description('Connections (JSON array from azure.yaml)') param aiProjectConnectionsJson string = '[]' @secure() -@description('JSON map of connection name to credentials object. Example: {"my-conn":{"key":"secret"}}') +@description('Connection credentials (JSON map from azure.yaml)') +#disable-next-line secure-parameter-default param aiProjectConnectionCredentialsJson string = '{}' -@description('List of resources to create and connect to the AI project') -param aiProjectDependentResourcesJson string = '[]' - -var aiProjectDeployments = json(aiProjectDeploymentsJson) -var aiProjectConnections = json(aiProjectConnectionsJson) -var aiProjectConnectionCreds = json(aiProjectConnectionCredentialsJson) -var aiProjectDependentResources = json(aiProjectDependentResourcesJson) - -@description('Enable hosted agent deployment') -param enableHostedAgents bool - -@description('Enable the capability host for supporting BYO storage of agent conversations. When false and hosted agents are enabled, the capability host is not created.') -param enableCapabilityHost bool - -@description('Enable monitoring for the AI project') -param enableMonitoring bool - -@description('When true, skip Foundry project/role/connection provisioning and reference the existing project read-only. Use when pointing at an existing Foundry project via --project-id.') -param useExistingAiProject bool = false - -@description('Optional. Existing container registry resource ID. If provided, no new ACR will be created and a connection to this ACR will be established.') -param existingContainerRegistryResourceId string = '' +// Existing resource detection (set by extension when reusing resources) +@description('Existing ACR connection name on the Foundry project. If set, ACR creation is skipped.') +param existingAcrConnectionName string = '' -@description('Optional. Existing container registry endpoint (login server). Required if existingContainerRegistryResourceId is provided.') +@description('Existing ACR login server endpoint. Used as output when ACR creation is skipped.') param existingContainerRegistryEndpoint string = '' -@description('Optional. Name of an existing ACR connection on the Foundry project. If provided, no new ACR or connection will be created.') -param existingAcrConnectionName string = '' - -@description('Optional. Existing Application Insights connection string. If provided, a connection will be created but no new App Insights resource.') +@description('Existing App Insights connection string (for existing projects)') param existingApplicationInsightsConnectionString string = '' -@description('Optional. Existing Application Insights resource ID. Used for connection metadata when providing an existing App Insights.') +@description('Existing App Insights resource ID (for existing projects)') param existingApplicationInsightsResourceId string = '' -@description('Optional. Name of an existing Application Insights connection on the Foundry project. If provided, no new App Insights or connection will be created.') -param existingAppInsightsConnectionName string = '' - -// Tags that should be applied to all resources. -// -// Note that 'azd-service-name' tags should be applied separately to service host resources. -// Example usage: -// tags: union(tags, { 'azd-service-name': }) -var tags = { - 'azd-env-name': environmentName -} +var tags = { 'azd-env-name': environmentName } +var createAcr = empty(existingAcrConnectionName) -// Check if resource group exists and create it if it doesn't resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: resourceGroupName location: location tags: tags } -// Build dependent resources array conditionally -// Check if ACR already exists in the user-provided array to avoid duplicates -// Also skip if user provided an existing container registry endpoint or connection name -var hasAcr = contains(map(aiProjectDependentResources, r => r.resource), 'registry') -var shouldCreateAcr = enableHostedAgents && !hasAcr && empty(existingContainerRegistryResourceId) && empty(existingAcrConnectionName) -var dependentResources = shouldCreateAcr ? union(aiProjectDependentResources, [ - { - resource: 'registry' - connectionName: 'acr-${uniqueString(subscription().id, resourceGroupName, location)}' - } -]) : aiProjectDependentResources +// ── AI Foundry Project (account + project + monitoring + RBAC) ── -// AI Project module — only when creating new resources -module aiProject 'core/ai/ai-project.bicep' = if (!useExistingAiProject) { +module aiProject './modules/ai-project.bicep' = { scope: rg name: 'ai-project' params: { + location: location tags: tags - location: aiDeploymentsLocation aiFoundryProjectName: aiFoundryProjectName + aiAccountName: aiFoundryResourceName + deployments: json(aiProjectDeploymentsJson) + connections: json(aiProjectConnectionsJson) + connectionCredentials: json(aiProjectConnectionCredentialsJson) principalId: principalId principalType: principalType - existingAiAccountName: aiFoundryResourceName - deployments: aiProjectDeployments - connections: aiProjectConnections - connectionCredentials: aiProjectConnectionCreds - additionalDependentResources: dependentResources - enableMonitoring: enableMonitoring - enableHostedAgents: enableHostedAgents - enableCapabilityHost: enableCapabilityHost - existingContainerRegistryResourceId: existingContainerRegistryResourceId - existingContainerRegistryEndpoint: existingContainerRegistryEndpoint - existingAcrConnectionName: existingAcrConnectionName - existingApplicationInsightsConnectionString: existingApplicationInsightsConnectionString - existingApplicationInsightsResourceId: existingApplicationInsightsResourceId - existingAppInsightsConnectionName: existingAppInsightsConnectionName - } -} - -// Existing project module — read-only reference when reusing an existing Foundry project -module existingAiProject 'core/ai/existing-ai-project.bicep' = if (useExistingAiProject) { - scope: rg - name: 'existing-ai-project' - params: { - aiServicesAccountName: aiFoundryResourceName - aiFoundryProjectName: aiFoundryProjectName - existingAcrConnectionName: existingAcrConnectionName - existingContainerRegistryEndpoint: existingContainerRegistryEndpoint - existingApplicationInsightsConnectionString: existingApplicationInsightsConnectionString - existingApplicationInsightsResourceId: existingApplicationInsightsResourceId + useExistingAiProject: useExistingAiProject + existingAppInsightsConnectionString: existingApplicationInsightsConnectionString + existingAppInsightsResourceId: existingApplicationInsightsResourceId } } -// ACR for existing project — create when hosted agents need a registry but the existing project has none -var shouldCreateAcrForExistingProject = useExistingAiProject && shouldCreateAcr -var acrConnectionName = 'acr-${uniqueString(subscription().id, resourceGroupName, location)}' +// ── Container Registry (for hosted agent image builds) ── -module acrForExistingProject 'core/host/acr.bicep' = if (shouldCreateAcrForExistingProject) { +module acr './modules/acr.bicep' = if (createAcr) { scope: rg - name: 'acr-for-existing-project' + name: 'acr' params: { location: location tags: tags - resourceName: 'cr${uniqueString(subscription().id, resourceGroupName, location)}' - connectionName: acrConnectionName + aiAccountName: aiProject.outputs.accountName + aiProjectName: aiProject.outputs.projectName + projectPrincipalId: aiProject.outputs.projectPrincipalId principalId: principalId principalType: principalType - aiServicesAccountName: aiFoundryResourceName - aiProjectName: aiFoundryProjectName } } +// ═══════════════════════════════════════════════════════ +// Outputs +// ═══════════════════════════════════════════════════════ + // Resources output AZURE_RESOURCE_GROUP string = resourceGroupName -output AZURE_AI_ACCOUNT_ID string = useExistingAiProject ? existingAiProject.outputs.accountId : aiProject.outputs.accountId -output AZURE_AI_PROJECT_ID string = useExistingAiProject ? existingAiProject.outputs.projectId : aiProject.outputs.projectId -output AZURE_AI_FOUNDRY_PROJECT_ID string = useExistingAiProject ? existingAiProject.outputs.projectId : aiProject.outputs.projectId -output AZURE_AI_ACCOUNT_NAME string = useExistingAiProject ? existingAiProject.outputs.aiServicesAccountName : aiProject.outputs.aiServicesAccountName -output AZURE_AI_PROJECT_NAME string = useExistingAiProject ? existingAiProject.outputs.projectName : aiProject.outputs.projectName +output AZURE_AI_ACCOUNT_ID string = aiProject.outputs.accountId +output AZURE_AI_PROJECT_ID string = aiProject.outputs.projectId +output AZURE_AI_FOUNDRY_PROJECT_ID string = aiProject.outputs.projectId +output AZURE_AI_ACCOUNT_NAME string = aiProject.outputs.accountName +output AZURE_AI_PROJECT_NAME string = aiProject.outputs.projectName // Endpoints -output AZURE_AI_PROJECT_ENDPOINT string = useExistingAiProject ? existingAiProject.outputs.AZURE_AI_PROJECT_ENDPOINT : aiProject.outputs.AZURE_AI_PROJECT_ENDPOINT -output AZURE_OPENAI_ENDPOINT string = useExistingAiProject ? existingAiProject.outputs.AZURE_OPENAI_ENDPOINT : aiProject.outputs.AZURE_OPENAI_ENDPOINT -output APPLICATIONINSIGHTS_CONNECTION_STRING string = useExistingAiProject ? existingAiProject.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING : aiProject.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING -output APPLICATIONINSIGHTS_RESOURCE_ID string = useExistingAiProject ? existingAiProject.outputs.APPLICATIONINSIGHTS_RESOURCE_ID : aiProject.outputs.APPLICATIONINSIGHTS_RESOURCE_ID - -// Dependent Resources and Connections - -// ACR -output AZURE_AI_PROJECT_ACR_CONNECTION_NAME string = shouldCreateAcrForExistingProject ? acrForExistingProject.outputs.containerRegistryConnectionName : (useExistingAiProject ? existingAiProject.outputs.dependentResources.registry.connectionName : aiProject.outputs.dependentResources.registry.connectionName) -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = shouldCreateAcrForExistingProject ? acrForExistingProject.outputs.containerRegistryLoginServer : (useExistingAiProject ? existingAiProject.outputs.dependentResources.registry.loginServer : aiProject.outputs.dependentResources.registry.loginServer) - -// Bing Search -output BING_GROUNDING_CONNECTION_NAME string = useExistingAiProject ? existingAiProject.outputs.dependentResources.bing_grounding.connectionName : aiProject.outputs.dependentResources.bing_grounding.connectionName -output BING_GROUNDING_RESOURCE_NAME string = useExistingAiProject ? existingAiProject.outputs.dependentResources.bing_grounding.name : aiProject.outputs.dependentResources.bing_grounding.name -output BING_GROUNDING_CONNECTION_ID string = useExistingAiProject ? existingAiProject.outputs.dependentResources.bing_grounding.connectionId : aiProject.outputs.dependentResources.bing_grounding.connectionId - -// Bing Custom Search -output BING_CUSTOM_GROUNDING_CONNECTION_NAME string = useExistingAiProject ? existingAiProject.outputs.dependentResources.bing_custom_grounding.connectionName : aiProject.outputs.dependentResources.bing_custom_grounding.connectionName -output BING_CUSTOM_GROUNDING_NAME string = useExistingAiProject ? existingAiProject.outputs.dependentResources.bing_custom_grounding.name : aiProject.outputs.dependentResources.bing_custom_grounding.name -output BING_CUSTOM_GROUNDING_CONNECTION_ID string = useExistingAiProject ? existingAiProject.outputs.dependentResources.bing_custom_grounding.connectionId : aiProject.outputs.dependentResources.bing_custom_grounding.connectionId +output AZURE_AI_PROJECT_ENDPOINT string = aiProject.outputs.projectEndpoint +output AZURE_OPENAI_ENDPOINT string = aiProject.outputs.openAiEndpoint -// Azure AI Search -output AZURE_AI_SEARCH_CONNECTION_NAME string = useExistingAiProject ? existingAiProject.outputs.dependentResources.search.connectionName : aiProject.outputs.dependentResources.search.connectionName -output AZURE_AI_SEARCH_SERVICE_NAME string = useExistingAiProject ? existingAiProject.outputs.dependentResources.search.serviceName : aiProject.outputs.dependentResources.search.serviceName +// Monitoring +output APPLICATIONINSIGHTS_CONNECTION_STRING string = aiProject.outputs.appInsightsConnectionString +output APPLICATIONINSIGHTS_RESOURCE_ID string = aiProject.outputs.appInsightsResourceId -// Azure Storage -output AZURE_STORAGE_CONNECTION_NAME string = useExistingAiProject ? existingAiProject.outputs.dependentResources.storage.connectionName : aiProject.outputs.dependentResources.storage.connectionName -output AZURE_STORAGE_ACCOUNT_NAME string = useExistingAiProject ? existingAiProject.outputs.dependentResources.storage.accountName : aiProject.outputs.dependentResources.storage.accountName +// Container Registry +#disable-next-line BCP318 +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = createAcr ? acr.outputs.loginServer : existingContainerRegistryEndpoint +#disable-next-line BCP318 +output AZURE_AI_PROJECT_ACR_CONNECTION_NAME string = createAcr ? acr.outputs.connectionName : existingAcrConnectionName // Connections -output AI_PROJECT_CONNECTION_IDS_JSON string = useExistingAiProject ? string(existingAiProject.outputs.connectionIds) : string(aiProject.outputs.connectionIds) +output AI_PROJECT_CONNECTION_IDS_JSON string = string(aiProject.outputs.connectionIds) diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 99a8a1a..3e9e481 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -1,72 +1,51 @@ { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "resourceGroupName": { - "value": "${AZURE_RESOURCE_GROUP}" - }, - "environmentName": { - "value": "${AZURE_ENV_NAME}" - }, - "location": { - "value": "${AZURE_LOCATION}" - }, - "aiFoundryResourceName": { - "value": "${AZURE_AI_ACCOUNT_NAME}" - }, - "aiFoundryProjectName": { - "value": "${AZURE_AI_PROJECT_NAME}" - }, - "aiDeploymentsLocation": { - "value": "${AZURE_LOCATION}" - }, - "principalId": { - "value": "${AZURE_PRINCIPAL_ID}" - }, - "principalType": { - "value": "${AZURE_PRINCIPAL_TYPE}" - }, - "aiProjectDeploymentsJson": { - "value": "${AI_PROJECT_DEPLOYMENTS=[]}" - }, - "aiProjectConnectionsJson": { - "value": "${AI_PROJECT_CONNECTIONS=[]}" - }, - "aiProjectConnectionCredentialsJson": { - "value": "${AI_PROJECT_CONNECTION_CREDENTIALS}" - }, - "aiProjectDependentResourcesJson": { - "value": "${AI_PROJECT_DEPENDENT_RESOURCES=[]}" - }, - "enableMonitoring": { - "value": "${ENABLE_MONITORING=true}" - }, - "enableHostedAgents": { - "value": "${ENABLE_HOSTED_AGENTS=false}" - }, - "enableCapabilityHost": { - "value": "${ENABLE_CAPABILITY_HOST=true}" - }, - "useExistingAiProject": { - "value": "${USE_EXISTING_AI_PROJECT=false}" - }, - "existingContainerRegistryResourceId": { - "value": "${AZURE_CONTAINER_REGISTRY_RESOURCE_ID=}" - }, - "existingContainerRegistryEndpoint": { - "value": "${AZURE_CONTAINER_REGISTRY_ENDPOINT=}" - }, - "existingAcrConnectionName": { - "value": "${AZURE_AI_PROJECT_ACR_CONNECTION_NAME=}" - }, - "existingApplicationInsightsConnectionString": { - "value": "${APPLICATIONINSIGHTS_CONNECTION_STRING=}" - }, - "existingApplicationInsightsResourceId": { - "value": "${APPLICATIONINSIGHTS_RESOURCE_ID=}" - }, - "existingAppInsightsConnectionName": { - "value": "${APPLICATIONINSIGHTS_CONNECTION_NAME=}" - } + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "resourceGroupName": { + "value": "${AZURE_RESOURCE_GROUP}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "principalType": { + "value": "${AZURE_PRINCIPAL_TYPE}" + }, + "aiFoundryResourceName": { + "value": "${AZURE_AI_ACCOUNT_NAME}" + }, + "aiFoundryProjectName": { + "value": "${AZURE_AI_PROJECT_NAME}" + }, + "useExistingAiProject": { + "value": "${USE_EXISTING_AI_PROJECT=false}" + }, + "aiProjectDeploymentsJson": { + "value": "${AI_PROJECT_DEPLOYMENTS=[]}" + }, + "aiProjectConnectionsJson": { + "value": "${AI_PROJECT_CONNECTIONS=[]}" + }, + "aiProjectConnectionCredentialsJson": { + "value": "${AI_PROJECT_CONNECTION_CREDENTIALS}" + }, + "existingAcrConnectionName": { + "value": "${AZURE_AI_PROJECT_ACR_CONNECTION_NAME=}" + }, + "existingContainerRegistryEndpoint": { + "value": "${AZURE_CONTAINER_REGISTRY_ENDPOINT=}" + }, + "existingApplicationInsightsConnectionString": { + "value": "${APPLICATIONINSIGHTS_CONNECTION_STRING=}" + }, + "existingApplicationInsightsResourceId": { + "value": "${APPLICATIONINSIGHTS_RESOURCE_ID=}" } + } } diff --git a/infra/modules/acr.bicep b/infra/modules/acr.bicep new file mode 100644 index 0000000..dd4dba5 --- /dev/null +++ b/infra/modules/acr.bicep @@ -0,0 +1,86 @@ +targetScope = 'resourceGroup' + +@description('Location for the container registry') +param location string = resourceGroup().location + +@description('Tags for all resources') +param tags object = {} + +@description('AI Services account name (for creating the project connection)') +param aiAccountName string + +@description('AI project name (for creating the project connection)') +param aiProjectName string + +@description('Managed identity principal ID of the AI project (for AcrPull role)') +param projectPrincipalId string + +@description('Developer principal ID (for Container Registry Tasks Contributor role)') +param principalId string + +@description('Developer principal type') +param principalType string + +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) +var registryName = 'cr${resourceToken}' +var connectionName = 'acr-${resourceToken}' + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: registryName + location: location + tags: tags + sku: { name: 'Basic' } + properties: { + adminUserEnabled: false + publicNetworkAccess: 'Enabled' + } +} + +// Developer: build & push images via ACR Tasks +resource developerRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: containerRegistry + name: guid(containerRegistry.id, principalId, 'fb382eab-e894-4461-af04-94435c366c3f') + properties: { + principalId: principalId + principalType: principalType + // Container Registry Tasks Contributor + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fb382eab-e894-4461-af04-94435c366c3f') + } +} + +// Project managed identity: pull images for hosted agents +resource projectPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: containerRegistry + name: guid(containerRegistry.id, projectPrincipalId, '7f951dda-4ed3-4680-a7ca-43fe172d538d') + properties: { + principalId: projectPrincipalId + principalType: 'ServicePrincipal' + // AcrPull + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } +} + +// Connection to Foundry Project +module acrConnection './connection.bicep' = { + name: 'acr-connection' + params: { + aiAccountName: aiAccountName + aiProjectName: aiProjectName + connectionConfig: { + name: connectionName + category: 'ContainerRegistry' + target: containerRegistry.properties.loginServer + authType: 'ManagedIdentity' + isSharedToAll: true + metadata: { ResourceId: containerRegistry.id } + } + credentials: { + clientId: projectPrincipalId + resourceId: containerRegistry.id + } + } +} + +output registryName string = containerRegistry.name +output loginServer string = containerRegistry.properties.loginServer +output connectionName string = acrConnection.outputs.connectionName diff --git a/infra/modules/ai-project.bicep b/infra/modules/ai-project.bicep new file mode 100644 index 0000000..db7da24 --- /dev/null +++ b/infra/modules/ai-project.bicep @@ -0,0 +1,216 @@ +targetScope = 'resourceGroup' + +@description('Tags for all resources') +param tags object = {} + +@description('Location for resources') +param location string + +@description('Name of the AI Foundry project') +param aiFoundryProjectName string + +@description('Optional name for the AI Account. If empty, auto-generated.') +param aiAccountName string = '' + +@description('Model deployments to create') +param deployments array = [] + +@description('Connections to create from azure.yaml') +param connections array = [] + +@secure() +@description('Credentials map for connections: { "conn-name": { "key": "..." } }') +param connectionCredentials object = {} + +@description('Developer principal ID for RBAC') +param principalId string + +@description('Developer principal type') +param principalType string + +@description('Use an existing Foundry project instead of creating one') +param useExistingAiProject bool = false + +@description('Existing App Insights connection string (for existing projects)') +param existingAppInsightsConnectionString string = '' + +@description('Existing App Insights resource ID (for existing projects)') +param existingAppInsightsResourceId string = '' + +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) +var resolvedAccountName = !empty(aiAccountName) ? aiAccountName : 'ai-account-${resourceToken}' + +// ═══════════════════════════════════════════════════════ +// New project resources +// ═══════════════════════════════════════════════════════ + +resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-06-01' = if (!useExistingAiProject) { + name: resolvedAccountName + location: location + tags: tags + sku: { name: 'S0' } + kind: 'AIServices' + identity: { type: 'SystemAssigned' } + properties: { + allowProjectManagement: true + customSubDomainName: resolvedAccountName + publicNetworkAccess: 'Enabled' + disableLocalAuth: true + } + + @batchSize(1) + resource modelDeployments 'deployments' = [for dep in deployments: { + name: dep.name + properties: { model: dep.model } + sku: dep.sku + }] + + resource project 'projects' = { + name: aiFoundryProjectName + location: location + identity: { type: 'SystemAssigned' } + properties: { + description: '${aiFoundryProjectName} Project' + displayName: '${aiFoundryProjectName}Project' + } + dependsOn: [modelDeployments] + } + + resource capabilityHost 'capabilityHosts@2025-10-01-preview' = { + name: 'agents' + properties: { + capabilityHostKind: 'Agents' + enablePublicHostingEnvironment: true + } + } +} + +// Monitoring (new project only) +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = if (!useExistingAiProject) { + name: 'logs-${resourceToken}' + location: location + tags: tags + properties: { + retentionInDays: 30 + features: { searchVersion: 1 } + sku: { name: 'PerGB2018' } + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = if (!useExistingAiProject) { + name: 'appi-${resourceToken}' + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + } +} + +resource appInsightsConnection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = if (!useExistingAiProject) { + parent: aiAccount::project + name: 'appi-${resourceToken}' + properties: { + category: 'AppInsights' + target: appInsights.id + authType: 'ApiKey' + isSharedToAll: true + #disable-next-line BCP318 + credentials: { key: appInsights.properties.ConnectionString } + metadata: { + ApiType: 'Azure' + #disable-next-line BCP318 + ResourceId: appInsights.id + } + } +} + +// Log Analytics Reader for project managed identity (enables trace evaluations) +resource logAnalyticsReaderRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!useExistingAiProject) { + scope: appInsights + name: guid(appInsights.id, aiAccount::project.name, '73c42c96-874c-492b-b04d-ab87d138a893') + properties: { + #disable-next-line BCP318 + principalId: aiAccount::project.identity.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '73c42c96-874c-492b-b04d-ab87d138a893') + } +} + +// ═══════════════════════════════════════════════════════ +// Existing project reference +// ═══════════════════════════════════════════════════════ + +resource existingAiAccount 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = if (useExistingAiProject) { + name: resolvedAccountName + + resource project 'projects' existing = { + name: aiFoundryProjectName + } +} + +// ═══════════════════════════════════════════════════════ +// RBAC — Azure AI User for the developer on the project +// ═══════════════════════════════════════════════════════ + +var aiUserRoleId = '53ca6127-db72-4b80-b1b0-d745d6d5456d' + +resource newProjectRbac 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!useExistingAiProject) { + scope: aiAccount::project + name: guid(subscription().id, resourceGroup().id, principalId, aiUserRoleId) + properties: { + principalId: principalId + principalType: principalType + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', aiUserRoleId) + } +} + +resource existingProjectRbac 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (useExistingAiProject) { + scope: existingAiAccount::project + name: guid(subscription().id, resourceGroup().id, principalId, aiUserRoleId) + properties: { + principalId: principalId + principalType: principalType + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', aiUserRoleId) + } +} + +// ═══════════════════════════════════════════════════════ +// Connections from azure.yaml (works for both new and existing) +// ═══════════════════════════════════════════════════════ + +module aiConnections './connection.bicep' = [for (conn, i) in connections: { + name: 'connection-${conn.name}' + params: { + aiAccountName: resolvedAccountName + aiProjectName: aiFoundryProjectName + connectionConfig: conn + credentials: connectionCredentials[?conn.name] ?? {} + } +}] + +// ═══════════════════════════════════════════════════════ +// Outputs +// ═══════════════════════════════════════════════════════ + +output accountName string = resolvedAccountName +output projectName string = aiFoundryProjectName +#disable-next-line BCP318 +output accountId string = useExistingAiProject ? existingAiAccount.id : aiAccount.id +#disable-next-line BCP318 +output projectId string = useExistingAiProject ? existingAiAccount::project.id : aiAccount::project.id +#disable-next-line BCP318 +output projectPrincipalId string = useExistingAiProject ? existingAiAccount::project.identity.principalId : aiAccount::project.identity.principalId +#disable-next-line BCP318 +output projectEndpoint string = useExistingAiProject ? existingAiAccount::project.properties.endpoints['AI Foundry API'] : aiAccount::project.properties.endpoints['AI Foundry API'] +#disable-next-line BCP318 +output openAiEndpoint string = useExistingAiProject ? existingAiAccount.properties.endpoints['OpenAI Language Model Instance API'] : aiAccount.properties.endpoints['OpenAI Language Model Instance API'] +#disable-next-line BCP318 +output appInsightsConnectionString string = useExistingAiProject ? existingAppInsightsConnectionString : appInsights.properties.ConnectionString +#disable-next-line BCP318 +output appInsightsResourceId string = useExistingAiProject ? existingAppInsightsResourceId : appInsights.id +output connectionIds array = [for (conn, i) in connections: { + name: aiConnections[i].outputs.connectionName + id: aiConnections[i].outputs.connectionId +}] diff --git a/infra/modules/ai-search.bicep b/infra/modules/ai-search.bicep new file mode 100644 index 0000000..2442715 --- /dev/null +++ b/infra/modules/ai-search.bicep @@ -0,0 +1,143 @@ +targetScope = 'resourceGroup' + +@description('Location for all resources') +param location string = resourceGroup().location + +@description('Tags for all resources') +param tags object = {} + +@description('AI Services account name') +param aiAccountName string + +@description('AI project name') +param aiProjectName string + +@description('Managed identity principal ID of the AI project') +param projectPrincipalId string + +@description('Developer principal ID') +param principalId string + +@description('Developer principal type') +param principalType string + +@description('Connection name for the Foundry Project') +param connectionName string + +@description('Storage account resource ID (for knowledge container and search indexer)') +param storageAccountId string + +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) + +resource searchService 'Microsoft.Search/searchServices@2024-06-01-preview' = { + name: 'search-${resourceToken}' + location: location + tags: tags + sku: { name: 'basic' } + identity: { type: 'SystemAssigned' } + properties: { + replicaCount: 1 + partitionCount: 1 + hostingMode: 'default' + authOptions: { + aadOrApiKey: { aadAuthFailureMode: 'http401WithBearerChallenge' } + } + publicNetworkAccess: 'enabled' + } +} + +// Knowledge container in the linked storage account +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { + name: last(split(storageAccountId, '/')) +} + +resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' existing = { + parent: storageAccount + name: 'default' +} + +resource knowledgeContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = { + parent: blobService + name: 'knowledge' + properties: { publicAccess: 'None' } +} + +// Search → Storage: Blob Data Reader +resource searchToStorageRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storageAccount + name: guid(storageAccount.id, searchService.id, '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1') + properties: { + principalId: searchService.identity.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1') + } +} + +// Search → AI Services: Cognitive Services OpenAI User +resource searchToAiRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(aiAccountName, searchService.id, '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') + properties: { + principalId: searchService.identity.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') + } +} + +// Project MI → Search: Search Service Contributor +resource projectToSearchContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: searchService + name: guid(searchService.id, projectPrincipalId, '7ca78c08-252a-4471-8644-bb5ff32d4ba0') + properties: { + principalId: projectPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0') + } +} + +// Project MI → Search: Search Index Data Contributor +resource projectToSearchDataRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: searchService + name: guid(searchService.id, projectPrincipalId, '8ebe5a00-799e-43f5-93ac-243d3dce84a7') + properties: { + principalId: projectPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7') + } +} + +// Developer → Search: Search Index Data Contributor +resource userToSearchRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: searchService + name: guid(searchService.id, principalId, '8ebe5a00-799e-43f5-93ac-243d3dce84a7') + properties: { + principalId: principalId + principalType: principalType + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7') + } +} + +// Connection to Foundry Project +module searchConnection './connection.bicep' = { + name: 'search-connection' + params: { + aiAccountName: aiAccountName + aiProjectName: aiProjectName + connectionConfig: { + name: connectionName + category: 'CognitiveSearch' + target: 'https://${searchService.name}.search.windows.net' + authType: 'AAD' + isSharedToAll: true + metadata: { + ApiVersion: '2024-07-01' + ResourceId: searchService.id + ApiType: 'Azure' + type: 'azure_ai_search' + } + } + } + dependsOn: [projectToSearchDataRole] +} + +output serviceName string = searchService.name +output connectionName string = searchConnection.outputs.connectionName diff --git a/infra/modules/bing-grounding.bicep b/infra/modules/bing-grounding.bicep new file mode 100644 index 0000000..a01f084 --- /dev/null +++ b/infra/modules/bing-grounding.bicep @@ -0,0 +1,66 @@ +targetScope = 'resourceGroup' + +@description('Tags for all resources') +param tags object = {} + +@description('AI Services account name') +param aiAccountName string + +@description('AI project name') +param aiProjectName string + +@description('Managed identity principal ID of the AI project') +param projectPrincipalId string + +@description('Connection name for the Foundry Project') +param connectionName string + +var resourceToken = uniqueString(subscription().id, resourceGroup().id) + +resource bingSearch 'Microsoft.Bing/accounts@2020-06-10' = { + name: 'bing-${resourceToken}' + location: 'global' + tags: tags + sku: { name: 'G1' } + kind: 'Bing.Grounding' + properties: { statisticsEnabled: false } +} + +// Project MI: Cognitive Services User +resource bingRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: bingSearch + name: guid(bingSearch.id, projectPrincipalId, 'a97b65f3-24c7-4388-baec-2e87135dc908') + properties: { + principalId: projectPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908') + } +} + +// Connection to Foundry Project +module bingConnection './connection.bicep' = { + name: 'bing-connection' + params: { + aiAccountName: aiAccountName + aiProjectName: aiProjectName + connectionConfig: { + name: connectionName + category: 'GroundingWithBingSearch' + target: bingSearch.properties.endpoint + authType: 'ApiKey' + isSharedToAll: true + metadata: { + Location: 'global' + ResourceId: bingSearch.id + ApiType: 'Azure' + type: 'bing_grounding' + } + } + credentials: { key: bingSearch.listKeys().key1 } + } + dependsOn: [bingRole] +} + +output bingName string = bingSearch.name +output connectionName string = bingConnection.outputs.connectionName +output connectionId string = bingConnection.outputs.connectionId diff --git a/infra/modules/connection.bicep b/infra/modules/connection.bicep new file mode 100644 index 0000000..7b53ade --- /dev/null +++ b/infra/modules/connection.bicep @@ -0,0 +1,38 @@ +targetScope = 'resourceGroup' + +@description('AI Services account name') +param aiAccountName string + +@description('AI project name') +param aiProjectName string + +@description('Connection configuration object') +param connectionConfig object + +@secure() +@description('Credentials for the connection (e.g. { key: "..." } for ApiKey)') +param credentials object = {} + +resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiAccountName + + resource project 'projects' existing = { + name: aiProjectName + } +} + +resource connection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = { + parent: aiAccount::project + name: connectionConfig.name + properties: { + category: connectionConfig.category + target: connectionConfig.target + authType: connectionConfig.authType + isSharedToAll: connectionConfig.?isSharedToAll ?? true + credentials: !empty(credentials) ? credentials : null + metadata: connectionConfig.?metadata + } +} + +output connectionName string = connection.name +output connectionId string = connection.id diff --git a/infra/modules/storage.bicep b/infra/modules/storage.bicep new file mode 100644 index 0000000..d5700fe --- /dev/null +++ b/infra/modules/storage.bicep @@ -0,0 +1,89 @@ +targetScope = 'resourceGroup' + +@description('Location for the storage account') +param location string = resourceGroup().location + +@description('Tags for all resources') +param tags object = {} + +@description('AI Services account name') +param aiAccountName string + +@description('AI project name') +param aiProjectName string + +@description('Managed identity principal ID of the AI project') +param projectPrincipalId string + +@description('Developer principal ID') +param principalId string + +@description('Developer principal type') +param principalType string + +@description('Connection name for the Foundry Project') +param connectionName string + +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { + name: 'st${resourceToken}' + location: location + tags: tags + sku: { name: 'Standard_LRS' } + kind: 'StorageV2' + identity: { type: 'SystemAssigned' } + properties: { + supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + accessTier: 'Hot' + } +} + +// Project MI: Storage Blob Data Contributor +resource projectStorageRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storageAccount + name: guid(storageAccount.id, projectPrincipalId, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + properties: { + principalId: projectPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + } +} + +// Developer: Storage Blob Data Contributor +resource userStorageRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storageAccount + name: guid(storageAccount.id, principalId, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + properties: { + principalId: principalId + principalType: principalType + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + } +} + +// Connection to Foundry Project +module storageConnection './connection.bicep' = { + name: 'storage-connection' + params: { + aiAccountName: aiAccountName + aiProjectName: aiProjectName + connectionConfig: { + name: connectionName + category: 'AzureStorageAccount' + target: storageAccount.properties.primaryEndpoints.blob + authType: 'AAD' + isSharedToAll: true + metadata: { + ApiType: 'Azure' + ResourceId: storageAccount.id + location: storageAccount.location + } + } + } +} + +output accountName string = storageAccount.name +output accountId string = storageAccount.id +output connectionName string = storageConnection.outputs.connectionName From 119a71601e029fac1124c0688ea2944b1d7ed8bb Mon Sep 17 00:00:00 2001 From: John Miller Date: Fri, 1 May 2026 16:52:11 -0400 Subject: [PATCH 2/5] feat: update output variable names in Bicep templates for consistency with hosted agent runtime --- .gitignore | 3 +++ infra/main.bicep | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index ce89292..f960bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -416,3 +416,6 @@ FodyWeavers.xsd *.msix *.msm *.msp + +# Bicep compiled ARM templates +infra/main.json diff --git a/infra/main.bicep b/infra/main.bicep index 4a1fc7d..5b52e9d 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -107,16 +107,16 @@ module acr './modules/acr.bicep' = if (createAcr) { // Resources output AZURE_RESOURCE_GROUP string = resourceGroupName output AZURE_AI_ACCOUNT_ID string = aiProject.outputs.accountId -output AZURE_AI_PROJECT_ID string = aiProject.outputs.projectId -output AZURE_AI_FOUNDRY_PROJECT_ID string = aiProject.outputs.projectId output AZURE_AI_ACCOUNT_NAME string = aiProject.outputs.accountName output AZURE_AI_PROJECT_NAME string = aiProject.outputs.projectName -// Endpoints -output AZURE_AI_PROJECT_ENDPOINT string = aiProject.outputs.projectEndpoint +// Platform-injected variable names (match hosted agent runtime) +// See: https://learn.microsoft.com/azure/foundry/agents/how-to/deploy-hosted-agent#platform-injected-environment-variables +output FOUNDRY_PROJECT_ENDPOINT string = aiProject.outputs.projectEndpoint +output FOUNDRY_PROJECT_ARM_ID string = aiProject.outputs.projectId output AZURE_OPENAI_ENDPOINT string = aiProject.outputs.openAiEndpoint -// Monitoring +// Monitoring (already matches platform-injected name) output APPLICATIONINSIGHTS_CONNECTION_STRING string = aiProject.outputs.appInsightsConnectionString output APPLICATIONINSIGHTS_RESOURCE_ID string = aiProject.outputs.appInsightsResourceId From 6c4e2f27130639ee5f37cd72b7ce881ea7e751f2 Mon Sep 17 00:00:00 2001 From: John Miller Date: Fri, 1 May 2026 17:55:46 -0400 Subject: [PATCH 3/5] feat: add Bicep validation workflow and parameter files for project scenarios --- .github/workflows/bicep-validate.yml | 91 +++++++++++++++++++ .../existing-project-new-acr.parameters.json | 33 +++++++ infra/tests/existing-project.parameters.json | 39 ++++++++ .../new-project-with-model.parameters.json | 21 +++++ infra/tests/new-project.parameters.json | 18 ++++ 5 files changed, 202 insertions(+) create mode 100644 .github/workflows/bicep-validate.yml create mode 100644 infra/tests/existing-project-new-acr.parameters.json create mode 100644 infra/tests/existing-project.parameters.json create mode 100644 infra/tests/new-project-with-model.parameters.json create mode 100644 infra/tests/new-project.parameters.json diff --git a/.github/workflows/bicep-validate.yml b/.github/workflows/bicep-validate.yml new file mode 100644 index 0000000..e2dd5dd --- /dev/null +++ b/.github/workflows/bicep-validate.yml @@ -0,0 +1,91 @@ +name: Bicep Validate + +on: + pull_request: + paths: + - 'infra/**' + push: + branches: [main] + paths: + - 'infra/**' + workflow_dispatch: + +permissions: + id-token: write + contents: read + +env: + # Override with your test subscription/location + AZURE_LOCATION: eastus2 + +jobs: + # ───────────────────────────────────────────── + # Layer 1: Lint & Build (no Azure creds needed) + # ───────────────────────────────────────────── + lint: + name: Lint & Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Bicep CLI + run: az bicep install + + - name: Lint main.bicep + run: az bicep lint --file infra/main.bicep + + - name: Build main.bicep + run: az bicep build --file infra/main.bicep --stdout > /dev/null + + - name: Build all modules independently + run: | + for f in infra/modules/*.bicep; do + echo "::group::Building $(basename $f)" + az bicep build --file "$f" --stdout > /dev/null + echo "::endgroup::" + done + + # ───────────────────────────────────────────── + # Layer 2 & 3: Validate + What-If per scenario + # ───────────────────────────────────────────── + validate: + name: "Validate: ${{ matrix.scenario }}" + runs-on: ubuntu-latest + needs: lint + strategy: + fail-fast: false + matrix: + include: + - scenario: new-project + params: infra/tests/new-project.parameters.json + - scenario: new-project-with-model + params: infra/tests/new-project-with-model.parameters.json + - scenario: existing-project + params: infra/tests/existing-project.parameters.json + - scenario: existing-project-new-acr + params: infra/tests/existing-project-new-acr.parameters.json + + steps: + - uses: actions/checkout@v4 + + - name: Azure Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Validate template + run: | + az deployment sub validate \ + --location "${{ env.AZURE_LOCATION }}" \ + --template-file infra/main.bicep \ + --parameters @${{ matrix.params }} + + - name: What-If preview + run: | + az deployment sub what-if \ + --location "${{ env.AZURE_LOCATION }}" \ + --template-file infra/main.bicep \ + --parameters @${{ matrix.params }} \ + --no-pretty-print diff --git a/infra/tests/existing-project-new-acr.parameters.json b/infra/tests/existing-project-new-acr.parameters.json new file mode 100644 index 0000000..84d5fac --- /dev/null +++ b/infra/tests/existing-project-new-acr.parameters.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "bicep-test-existing-noacr" + }, + "location": { + "value": "eastus2" + }, + "principalId": { + "value": "00000000-0000-0000-0000-000000000000" + }, + "principalType": { + "value": "User" + }, + "aiFoundryResourceName": { + "value": "my-existing-account" + }, + "aiFoundryProjectName": { + "value": "my-existing-project" + }, + "useExistingAiProject": { + "value": true + }, + "existingApplicationInsightsConnectionString": { + "value": "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://eastus2-0.in.applicationinsights.azure.com/" + }, + "existingApplicationInsightsResourceId": { + "value": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Insights/components/appi-test" + } + } +} diff --git a/infra/tests/existing-project.parameters.json b/infra/tests/existing-project.parameters.json new file mode 100644 index 0000000..9202ff4 --- /dev/null +++ b/infra/tests/existing-project.parameters.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "bicep-test-existing" + }, + "location": { + "value": "eastus2" + }, + "principalId": { + "value": "00000000-0000-0000-0000-000000000000" + }, + "principalType": { + "value": "User" + }, + "aiFoundryResourceName": { + "value": "my-existing-account" + }, + "aiFoundryProjectName": { + "value": "my-existing-project" + }, + "useExistingAiProject": { + "value": true + }, + "existingAcrConnectionName": { + "value": "existing-acr-connection" + }, + "existingContainerRegistryEndpoint": { + "value": "myregistry.azurecr.io" + }, + "existingApplicationInsightsConnectionString": { + "value": "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://eastus2-0.in.applicationinsights.azure.com/" + }, + "existingApplicationInsightsResourceId": { + "value": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Insights/components/appi-test" + } + } +} diff --git a/infra/tests/new-project-with-model.parameters.json b/infra/tests/new-project-with-model.parameters.json new file mode 100644 index 0000000..843f5d9 --- /dev/null +++ b/infra/tests/new-project-with-model.parameters.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "bicep-test-deploy" + }, + "location": { + "value": "eastus2" + }, + "principalId": { + "value": "00000000-0000-0000-0000-000000000000" + }, + "principalType": { + "value": "User" + }, + "aiProjectDeploymentsJson": { + "value": "[{\"name\":\"gpt-4.1-mini\",\"model\":{\"name\":\"gpt-4.1-mini\",\"format\":\"OpenAI\",\"version\":\"2025-04-14\"},\"sku\":{\"name\":\"GlobalBatch\",\"capacity\":10}}]" + } + } +} diff --git a/infra/tests/new-project.parameters.json b/infra/tests/new-project.parameters.json new file mode 100644 index 0000000..39c1957 --- /dev/null +++ b/infra/tests/new-project.parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "bicep-test-new" + }, + "location": { + "value": "eastus2" + }, + "principalId": { + "value": "00000000-0000-0000-0000-000000000000" + }, + "principalType": { + "value": "User" + } + } +} From 80a4ad70243b07a902b24b009b6c7abbbe0bce21 Mon Sep 17 00:00:00 2001 From: John Miller Date: Fri, 1 May 2026 18:59:33 -0400 Subject: [PATCH 4/5] feat: enhance Bicep CLI installation step for local testing support --- .github/workflows/bicep-validate.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bicep-validate.yml b/.github/workflows/bicep-validate.yml index e2dd5dd..d060f52 100644 --- a/.github/workflows/bicep-validate.yml +++ b/.github/workflows/bicep-validate.yml @@ -29,7 +29,12 @@ jobs: - uses: actions/checkout@v4 - name: Install Bicep CLI - run: az bicep install + run: | + # Install az CLI if not present (needed for act/local testing) + if ! command -v az &> /dev/null; then + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + fi + az bicep install - name: Lint main.bicep run: az bicep lint --file infra/main.bicep From 1d39fbbe5224063e010827e2aa1b6902521f9d03 Mon Sep 17 00:00:00 2001 From: John Miller Date: Fri, 1 May 2026 19:03:49 -0400 Subject: [PATCH 5/5] feat: add infra/tests directory to .azdignore for improved build process --- .azdignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.azdignore b/.azdignore index 0cdfeeb..b51945e 100644 --- a/.azdignore +++ b/.azdignore @@ -1,6 +1,8 @@ .github/ .gitignore +infra/tests/ + CHANGELOG.md CONTRIBUTING.md LICENSE.md