diff --git a/docs/deploymentguide.md b/docs/deploymentguide.md index 2c4eb96..322a4fe 100644 --- a/docs/deploymentguide.md +++ b/docs/deploymentguide.md @@ -240,6 +240,7 @@ After setting these variables, run `azd up` normally. The deployment will attach | `useExistingVNet` | Reuse an existing VNet | `false` | | `existingVnetResourceId` | Existing VNet resource ID (when `useExistingVNet=true`) | `` | | `existingLogAnalyticsWorkspaceResourceId` | Existing Log Analytics workspace to receive PostgreSQL diagnostics. May live in another subscription within the same tenant. | `` | +| `existingAiProjectResourceId` | Existing Microsoft Foundry **project** resource ID to reuse instead of creating a new Foundry account + project. When set, `deployAiFoundry` and `deployAfProject` are auto-disabled. Read from `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID`. | `` | | `vmUserName` | Jump box VM admin username | `VM_ADMIN_USERNAME` env var or `testvmuser` | | `vmAdminPassword` | Jump box VM admin password | `VM_ADMIN_PASSWORD` env var | @@ -283,6 +284,26 @@ When set, the deployment will: The workspace may live in a different resource group or subscription within the same tenant. The identity running `azd up` needs **`Microsoft.Insights/diagnosticSettings/write`** on the workspace itself (covered by the built-in **Log Analytics Contributor** role scoped to the workspace or its resource group — subscription-wide rights are not required). See the **Observability — Bring Your Own Log Analytics Workspace** section in the [Parameter Guide](./parameter_guide.md) for the full output reference (including App Insights values when that component is deployed) and notes on deployment-history exposure of those values. +**Microsoft Foundry Project (BYO AI Project):** + +By default the deployment creates a new Foundry account and project. To attach to an existing Foundry project instead, set: + +```powershell +azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/" +``` + +When set, the wrapper auto-disables `deployAiFoundry` and `deployAfProject`, parses the account/project/RG/subscription from the resource ID, and publishes them to azd env (`aiFoundryName`, `aiFoundryProjectName`, `aiFoundryResourceGroup`, `aiFoundrySubscriptionId`) so post-provision RBAC and Search wiring target the existing project. The existing Foundry account may live in a different resource group or subscription within the same tenant — the identity running `azd up` needs RBAC owner/contributor rights on that account to assign roles. See the **AI Foundry — Bring Your Own Existing Project** section in the [Parameter Guide](./parameter_guide.md) for details. + +**Existing Resource Group:** + +azd natively supports deploying into an existing resource group. Set `AZURE_RESOURCE_GROUP` before `azd up`; the wrapper template uses `targetScope = 'resourceGroup'` and performs incremental deployments, so existing resources in the RG are left untouched: + +```powershell +azd env set AZURE_RESOURCE_GROUP "" +``` + +If unset, the deployment defaults to `rg-`. + ### Step 4: Deploy diff --git a/docs/parameter_guide.md b/docs/parameter_guide.md index 38f3811..cea45c9 100644 --- a/docs/parameter_guide.md +++ b/docs/parameter_guide.md @@ -130,6 +130,46 @@ The identity running the deployment needs permission to attach diagnostic settin --- +## AI Foundry — Bring Your Own Existing Project + +By default the wrapper provisions a new Azure AI Foundry account and project. If you already have an AI Foundry project you want to reuse (for example, a shared project that is centrally governed, or one that lives in a different subscription), you can point the deployment at it. + +### How it works + +When `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID` is set (and surfaced through the bicepparam as `existingAiProjectResourceId`): + +1. The wrapper flips `deployAiFoundry`, `deployAfProject`, and `deployAAfAgentSvc` to `false`, so the AI Landing Zone submodule **skips creating a new AI Foundry account, project, and agent capability hosts**. +2. The preprovision script parses the supplied resource ID into its subscription / resource group / account / project segments and publishes them to `azd env` (`aiFoundryName`, `aiFoundryProjectName`, `aiFoundryResourceGroup`, `aiFoundrySubscriptionId`). +3. Downstream post-provision automation (OneLake indexing, AI Foundry-to-Search RBAC, AI Foundry connection setup) targets the existing project instead of running discovery in the deployment resource group. +4. The wrapper outputs `aiFoundryAccountName`, `aiFoundryProjectName`, `aiFoundryResourceGroup`, `aiFoundrySubscriptionId`, `existingAiProjectResourceIdOut`, and `useExistingAiProject` so external automation can pick them up. + +> **Note:** Cross-subscription IDs are supported. The identity running `azd` must have at least `Reader` on the existing AI Foundry account, plus the role-assignment rights needed by the post-provision RBAC scripts (typically `Cognitive Services Contributor` on the project and `Search Service Contributor`/`Search Index Data Contributor` on the AI Search resource). + +### Setting it via azd env + +```powershell +azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/" +``` + +Or set it directly in `infra/main.bicepparam` (it is already read from the env variable by default): + +```bicep +param existingAiProjectResourceId = '/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/' +``` + +### Outputs + +| Output | Description | +|--------|-------------| +| `useExistingAiProject` | `true` when BYO mode is active | +| `existingAiProjectResourceIdOut` | Echo of the supplied AI Foundry project resource ID | +| `aiFoundryAccountName` | Existing AI Foundry account name (parsed from the resource ID) | +| `aiFoundryProjectName` | Existing AI Foundry project name (parsed from the resource ID) | +| `aiFoundryResourceGroup` | Resource group of the existing AI Foundry account | +| `aiFoundrySubscriptionId` | Subscription ID hosting the existing AI Foundry account | + +--- + ## Table of Contents 1. [Basic Parameters](#basic-parameters) 2. [Deployment Toggles](#deployment-toggles) diff --git a/infra/main.bicep b/infra/main.bicep index c6ed8ac..57cb262 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -111,6 +111,9 @@ param aiFoundryStorageAccountResourceId string = '' param aiFoundryCosmosDBAccountResourceId string = '' param keyVaultResourceId string = '' +@description('Optional. Full ARM resource ID of an existing Azure AI Foundry project to reuse. When provided, the wrapper and AI Landing Zone submodule will skip creating a new AI Foundry account/project, and downstream automation (RBAC, OneLake indexing, AI Foundry connections) will target the existing project. Cross-subscription resource IDs are supported. Format: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{account}/projects/{project}.') +param existingAiProjectResourceId string = '' + @description('Optional. Full ARM resource ID of an existing Log Analytics workspace to use for observability of the deployed Foundry application and wrapper-managed PostgreSQL. When provided, an Application Insights component is created in the deployment resource group and linked to this workspace, and diagnostic settings on the wrapper-managed PostgreSQL flexible server are routed to it. Leave empty to skip BYO behavior. Format: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{name}.') param existingLogAnalyticsWorkspaceResourceId string = '' @@ -524,10 +527,33 @@ var effectiveAiSearchResourceId = !empty(aiSearchResourceId) var effectiveStorageAccountResourceId = resourceId('Microsoft.Storage/storageAccounts', storageAccountName) +// ---------------------------------------------------------------------- +// BYO existing AI Foundry Project parsing. +// When existingAiProjectResourceId is provided, parse it into its +// subscription / resource group / account / project segments so downstream +// automation can target the existing project instead of a wrapper-created +// one. Cross-subscription resource IDs are supported. +// ---------------------------------------------------------------------- +var byoAiProjectEnabled = !empty(existingAiProjectResourceId) +var byoAiProjectIdSegments = byoAiProjectEnabled ? split(existingAiProjectResourceId, '/') : [] +var byoAiProjectSubscriptionId = length(byoAiProjectIdSegments) >= 3 ? byoAiProjectIdSegments[2] : '' +var byoAiProjectResourceGroupName = length(byoAiProjectIdSegments) >= 5 ? byoAiProjectIdSegments[4] : '' +var byoAiFoundryAccountName = length(byoAiProjectIdSegments) >= 9 ? byoAiProjectIdSegments[8] : '' +var byoAiFoundryProjectName = length(byoAiProjectIdSegments) >= 11 ? byoAiProjectIdSegments[10] : '' +var effectiveAiFoundryAccountName = byoAiProjectEnabled ? byoAiFoundryAccountName : aiFoundryAccountName +var effectiveAiFoundryProjectName = byoAiProjectEnabled ? byoAiFoundryProjectName : aiFoundryProjectName +var effectiveAiFoundryResourceGroup = byoAiProjectEnabled ? byoAiProjectResourceGroupName : resourceGroup().name +var effectiveAiFoundrySubscriptionId = byoAiProjectEnabled ? byoAiProjectSubscriptionId : subscription().subscriptionId + output virtualNetworkResourceId string = effectiveVnetResourceId output keyVaultResourceId string = effectiveKeyVaultResourceId output storageAccountResourceId string = effectiveStorageAccountResourceId -output aiFoundryProjectName string = aiFoundryProjectName +output aiFoundryProjectName string = effectiveAiFoundryProjectName +output aiFoundryAccountName string = effectiveAiFoundryAccountName +output aiFoundryResourceGroup string = effectiveAiFoundryResourceGroup +output aiFoundrySubscriptionId string = effectiveAiFoundrySubscriptionId +output existingAiProjectResourceIdOut string = existingAiProjectResourceId +output useExistingAiProject bool = byoAiProjectEnabled output aiSearchResourceId string = effectiveAiSearchResourceId output aiSearchName string = searchServiceName output aiSearchAdditionalAccessObjectIds array = aiSearchAdditionalAccessObjectIds diff --git a/infra/main.bicepparam b/infra/main.bicepparam index 9f8ac0e..1e726e1 100644 --- a/infra/main.bicepparam +++ b/infra/main.bicepparam @@ -23,6 +23,16 @@ param keyVaultResourceId = '' param useExistingVNet = false param existingVnetResourceId = readEnvironmentVariable('EXISTING_VNET_RESOURCE_ID', '') +// BYO existing Azure AI Foundry Project (cross-subscription supported). +// When AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is set, the wrapper and the +// AI Landing Zone submodule will skip creating a new AI Foundry account and +// project; downstream automation (RBAC, OneLake indexing, AI Foundry +// connections) will target the existing project instead. +// Format: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{aiFoundryAccount}/projects/{aiFoundryProject} +var existingAiProjectResourceIdVar = readEnvironmentVariable('AZURE_EXISTING_AI_PROJECT_RESOURCE_ID', '') +param existingAiProjectResourceId = existingAiProjectResourceIdVar +var useExistingAiProjectVar = !empty(existingAiProjectResourceId) + // BYO Log Analytics Workspace for observability of the deployed Foundry // application and wrapper-managed PostgreSQL resources. // When provided, diagnostic settings on the wrapper-managed PostgreSQL @@ -91,7 +101,9 @@ param postgreSqlStorageSizeGB = 32 // ======================================== param deployGroundingWithBing = false -param deployAiFoundry = true +// When AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is set, skip creating a new +// AI Foundry account/project (BYO mode). +param deployAiFoundry = !useExistingAiProjectVar param deployAiFoundrySubnet = false param deployAppConfig = true param deployKeyVault = true @@ -110,7 +122,9 @@ param deployNsgs = true param sideBySideDeploy = readEnvironmentVariable('SIDE_BY_SIDE', 'true') == 'true' param deploySoftware = false param deployApim = false -param deployAfProject = true +// When AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is set, skip creating a new +// AI Foundry project (BYO mode). +param deployAfProject = !useExistingAiProjectVar param deployAAfAgentSvc = false param enableAgenticRetrieval = readEnvironmentVariable('ENABLE_AGENTIC_RETRIEVAL', 'false') == 'true' diff --git a/scripts/preprovision-integrated.ps1 b/scripts/preprovision-integrated.ps1 index 2b6d209..03f1c0f 100644 --- a/scripts/preprovision-integrated.ps1 +++ b/scripts/preprovision-integrated.ps1 @@ -449,27 +449,64 @@ if ([string]::IsNullOrWhiteSpace($aiSearchName)) { try { $aiSearchName = (az search service list --resource-group $ResourceGroup --query "[0].name" -o tsv 2>$null).Trim() } catch { } } +# BYO existing AI Foundry project: when AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is +# set (and surfaced through the bicepparam as existingAiProjectResourceId), parse +# its segments and use them for the downstream azd env values so postprovision +# automation targets the existing account/project instead of attempting RG +# discovery in this deployment's resource group. +$existingAiProjectResourceId = $null +try { $existingAiProjectResourceId = [string]$parentJson.parameters.existingAiProjectResourceId.value } catch { } +if ([string]::IsNullOrWhiteSpace($existingAiProjectResourceId)) { + $existingAiProjectResourceId = $env:AZURE_EXISTING_AI_PROJECT_RESOURCE_ID +} + $aiFoundryName = $null -try { $aiFoundryName = [string]$parentJson.parameters.aiFoundryAccountName.value } catch { } +$aiFoundryProjectName = $null +$aiFoundryResourceGroupForEnv = $ResourceGroup +$aiFoundrySubscriptionForEnv = $SubscriptionId + +if (-not [string]::IsNullOrWhiteSpace($existingAiProjectResourceId)) { + Write-Host " [i] BYO AI Foundry project detected: $existingAiProjectResourceId" -ForegroundColor Cyan + $segments = $existingAiProjectResourceId.Trim('/').Split('/') + # Expected: subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{account}/projects/{project} + if ($segments.Length -ge 10 -and $segments[0] -ieq 'subscriptions' -and $segments[2] -ieq 'resourceGroups' -and $segments[6] -ieq 'accounts' -and $segments[8] -ieq 'projects') { + $aiFoundrySubscriptionForEnv = $segments[1] + $aiFoundryResourceGroupForEnv = $segments[3] + $aiFoundryName = $segments[7] + $aiFoundryProjectName = $segments[9] + Write-Host " Account: $aiFoundryName" -ForegroundColor Green + Write-Host " Project: $aiFoundryProjectName" -ForegroundColor Green + Write-Host " ResourceGrp: $aiFoundryResourceGroupForEnv" -ForegroundColor Green + Write-Host " Subscription: $aiFoundrySubscriptionForEnv" -ForegroundColor Green + } else { + Write-Host " [!] AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is set but the resource ID format is not recognized. Falling back to discovery." -ForegroundColor Yellow + $existingAiProjectResourceId = $null + } +} + if ([string]::IsNullOrWhiteSpace($aiFoundryName)) { - try { - $aiFoundryName = (az cognitiveservices account list --resource-group $ResourceGroup --query "[?kind=='AIServices']|[0].name" -o tsv 2>$null).Trim() - } catch { } + try { $aiFoundryName = [string]$parentJson.parameters.aiFoundryAccountName.value } catch { } + if ([string]::IsNullOrWhiteSpace($aiFoundryName)) { + try { + $aiFoundryName = (az cognitiveservices account list --resource-group $ResourceGroup --query "[?kind=='AIServices']|[0].name" -o tsv 2>$null).Trim() + } catch { } + } } -$aiFoundryProjectName = $null -try { $aiFoundryProjectName = [string]$parentJson.parameters.aiFoundryProjectName.value } catch { } -if ([string]::IsNullOrWhiteSpace($aiFoundryProjectName) -and -not [string]::IsNullOrWhiteSpace($aiFoundryName)) { - try { - $projectCandidatesRaw = az resource list --resource-group $ResourceGroup --resource-type "Microsoft.CognitiveServices/accounts/projects" --query "[?contains(id, '/accounts/$aiFoundryName/')].name" -o tsv 2>$null - if ($projectCandidatesRaw) { - [string[]]$projectCandidates = ($projectCandidatesRaw -split "\r?\n") | Where-Object { $_ -and $_.Trim() } | ForEach-Object { $_.Trim() } - if ($projectCandidates.Length -ge 1) { - $aiFoundryProjectName = $projectCandidates[0] +if ([string]::IsNullOrWhiteSpace($aiFoundryProjectName)) { + try { $aiFoundryProjectName = [string]$parentJson.parameters.aiFoundryProjectName.value } catch { } + if ([string]::IsNullOrWhiteSpace($aiFoundryProjectName) -and -not [string]::IsNullOrWhiteSpace($aiFoundryName)) { + try { + $projectCandidatesRaw = az resource list --resource-group $aiFoundryResourceGroupForEnv --subscription $aiFoundrySubscriptionForEnv --resource-type "Microsoft.CognitiveServices/accounts/projects" --query "[?contains(id, '/accounts/$aiFoundryName/')].name" -o tsv 2>$null + if ($projectCandidatesRaw) { + [string[]]$projectCandidates = ($projectCandidatesRaw -split "\r?\n") | Where-Object { $_ -and $_.Trim() } | ForEach-Object { $_.Trim() } + if ($projectCandidates.Length -ge 1) { + $aiFoundryProjectName = $projectCandidates[0] + } } + } catch { + # Ignore discovery failures and continue. } - } catch { - # Ignore discovery failures and continue. } } @@ -484,8 +521,14 @@ Set-AzdEnvValue -Name 'aiSearchResourceId' -Value $aiSearchResourceId Set-AzdEnvValue -Name 'aiSearchResourceGroup' -Value $ResourceGroup Set-AzdEnvValue -Name 'aiSearchSubscriptionId' -Value $SubscriptionId Set-AzdEnvValue -Name 'aiFoundryName' -Value $aiFoundryName -Set-AzdEnvValue -Name 'aiFoundryResourceGroup' -Value $ResourceGroup +Set-AzdEnvValue -Name 'aiFoundryResourceGroup' -Value $aiFoundryResourceGroupForEnv +Set-AzdEnvValue -Name 'aiFoundrySubscriptionId' -Value $aiFoundrySubscriptionForEnv Set-AzdEnvValue -Name 'aiFoundryProjectName' -Value $aiFoundryProjectName +if (-not [string]::IsNullOrWhiteSpace($existingAiProjectResourceId)) { + Set-AzdEnvValue -Name 'AZURE_EXISTING_AI_PROJECT_RESOURCE_ID' -Value $existingAiProjectResourceId + Set-AzdEnvValue -Name 'existingAiProjectResourceId' -Value $existingAiProjectResourceId + Set-AzdEnvValue -Name 'useExistingAiProject' -Value 'true' +} Write-Host ""