diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 763de5b..9387c1e 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -17,6 +17,8 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_AI_MODEL_NAME` | string | `o3` | Specifies the `o` model name. | | `AZURE_AI_MODEL_VERSION` | string | `2025-04-16` | Specifies the `o` model version. | | `AZURE_AI_MODEL_CAPACITY` | integer | `200` | Sets the model capacity (choose based on your subscription's available `o` capacity). | +| `AZURE_ENV_VM_ADMIN_USERNAME` | string | `` | The administrator username for the virtual machine. | +| `AZURE_ENV_VM_ADMIN_PASSWORD` | string | `` | The administrator password for the virtual machine. | ## How to Set a Parameter diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 1cb736c..f02c8c9 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -34,6 +34,40 @@ This will allow the scripts to run for the current session without permanently c ## Deployment Options & Steps +### Sandbox or WAF Aligned Deployment Options + +The [`infra`](../infra) folder of the Multi Agent Solution Accelerator contains the [`main.bicep`](../infra/main.bicep) Bicep script, which defines all Azure infrastructure components for this solution. + +By default, the `azd up` command uses the [`main.parameters.json`](../infra/main.parameters.json) file to deploy the solution. This file is pre-configured for a **sandbox environment** — ideal for development and proof-of-concept scenarios, with minimal security and cost controls for rapid iteration. + +For **production deployments**, the repository also provides [`main.waf.parameters.json`](../infra/main.waf.parameters.json), which applies a [Well-Architected Framework (WAF) aligned](https://learn.microsoft.com/en-us/azure/well-architected/) configuration. This option enables additional Azure best practices for reliability, security, cost optimization, operational excellence, and performance efficiency, such as: + +**How to choose your deployment configuration:** + +* Use the default `main.parameters.json` file for a **sandbox/dev environment** +* For a **WAF-aligned, production-ready deployment**, copy the contents of `main.waf.parameters.json` into `main.parameters.json` before running `azd up` + +--- + +### VM Credentials Configuration + +By default, the solution sets the VM administrator username and password from environment variables. +If you do not configure these values, a randomly generated GUID will be used for both the username and password. + +To set your own VM credentials before deployment, use: + +```sh +azd env set AZURE_ENV_VM_ADMIN_USERNAME +azd env set AZURE_ENV_VM_ADMIN_PASSWORD +``` + +> [!TIP] +> Always review and adjust parameter values (such as region, capacity, security settings and log analytics workspace configuration) to match your organization’s requirements before deploying. For production, ensure you have sufficient quota and follow the principle of least privilege for all identities and role assignments. + + +> [!IMPORTANT] +> The WAF-aligned configuration is under active development. More Azure Well-Architected recommendations will be added in future updates. + Pick from the options below to see step-by-step instructions for GitHub Codespaces, VS Code Dev Containers, and Local Environments. | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/microsoft/Container-Migration-Solution-Accelerator) | [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/Container-Migration-Solution-Accelerator) | @@ -126,12 +160,28 @@ By default, the **o3 model capacity** in deployment is set to **200k tokens**. > **We recommend increasing the capacity to 500k tokens, if available, for optimal performance.** -To adjust quota settings, follow these [steps](./AzureAIModelQuotaSettings.md.md). +To adjust quota settings, follow these [steps](./AzureAIModelQuotaSettings.md). **⚠️ Warning:** Insufficient quota can cause deployment errors. Please ensure you have the recommended capacity or request additional capacity before deploying this solution. +
+ + Reusing an Existing Log Analytics Workspace + + Guide to get your [Existing Workspace ID](/docs/re-use-log-analytics.md) + +
+ +
+ + Reusing an Existing Azure AI Foundry Project + + Guide to get your [Existing Project ID](/docs/re-use-foundry-project.md) + +
+ ### Deploying with AZD Once you've opened the project in [Codespaces](#github-codespaces), [Dev Containers](#vs-code-dev-containers), or [locally](#local-environment), you can deploy it to Azure by following these steps: diff --git a/docs/images/re_use_foundry_project/azure_ai_foundry_list.png b/docs/images/re_use_foundry_project/azure_ai_foundry_list.png new file mode 100644 index 0000000..784bc85 Binary files /dev/null and b/docs/images/re_use_foundry_project/azure_ai_foundry_list.png differ diff --git a/docs/images/re_use_foundry_project/navigate_to_projects.png b/docs/images/re_use_foundry_project/navigate_to_projects.png new file mode 100644 index 0000000..11082c1 Binary files /dev/null and b/docs/images/re_use_foundry_project/navigate_to_projects.png differ diff --git a/docs/images/re_use_foundry_project/project_resource_id.png b/docs/images/re_use_foundry_project/project_resource_id.png new file mode 100644 index 0000000..7835ea9 Binary files /dev/null and b/docs/images/re_use_foundry_project/project_resource_id.png differ diff --git a/docs/images/re_use_log/logAnalytics.png b/docs/images/re_use_log/logAnalytics.png new file mode 100644 index 0000000..95402f8 Binary files /dev/null and b/docs/images/re_use_log/logAnalytics.png differ diff --git a/docs/images/re_use_log/logAnalyticsJson.png b/docs/images/re_use_log/logAnalyticsJson.png new file mode 100644 index 0000000..3a4093b Binary files /dev/null and b/docs/images/re_use_log/logAnalyticsJson.png differ diff --git a/docs/images/re_use_log/logAnalyticsList.png b/docs/images/re_use_log/logAnalyticsList.png new file mode 100644 index 0000000..6dcf464 Binary files /dev/null and b/docs/images/re_use_log/logAnalyticsList.png differ diff --git a/docs/re-use-foundry-project.md b/docs/re-use-foundry-project.md new file mode 100644 index 0000000..7d33dfb --- /dev/null +++ b/docs/re-use-foundry-project.md @@ -0,0 +1,44 @@ +[← Back to *DEPLOYMENT* guide](/docs/DeploymentGuide.md#deployment-steps) + +# Reusing an Existing Azure AI Foundry Project +To configure your environment to use an existing Azure AI Foundry Project, follow these steps: +--- +### 1. Go to Azure Portal +Go to https://portal.azure.com + +### 2. Search for Azure AI Foundry +In the search bar at the top, type "Azure AI Foundry" and click on it. Then select the Foundry service instance where your project exists. + +![alt text](../docs/images/re_use_foundry_project/azure_ai_foundry_list.png) + +### 3. Navigate to Projects under Resource Management +On the left sidebar of the Foundry service blade: + +- Expand the Resource Management section +- Click on Projects (this refers to the active Foundry project tied to the service) + +### 4. Click on the Project +From the Projects view: Click on the project name to open its details + + Note: You will see only one project listed here, as each Foundry service maps to a single project in this accelerator + +![alt text](../docs/images/re_use_foundry_project/navigate_to_projects.png) + +### 5. Copy Resource ID +In the left-hand menu of the project blade: + +- Click on Properties under Resource Management +- Locate the Resource ID field +- Click on the copy icon next to the Resource ID value + +![alt text](../docs/images/re_use_foundry_project/project_resource_id.png) + +### 6. Set the Foundry Project Resource ID in Your Environment +Run the following command in your terminal +```bash +azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID '' +``` +Replace `` with the value obtained from Step 5. + +### 7. Continue Deployment +Proceed with the next steps in the [deployment guide](/docs/DeploymentGuide.md#deployment-steps). diff --git a/docs/re-use-log-analytics.md b/docs/re-use-log-analytics.md new file mode 100644 index 0000000..1fa7a35 --- /dev/null +++ b/docs/re-use-log-analytics.md @@ -0,0 +1,31 @@ +[← Back to *DEPLOYMENT* guide](/docs/DeploymentGuide.md#deployment-steps) + +# Reusing an Existing Log Analytics Workspace +To configure your environment to use an existing Log Analytics Workspace, follow these steps: +--- +### 1. Go to Azure Portal +Go to https://portal.azure.com + +### 2. Search for Log Analytics +In the search bar at the top, type "Log Analytics workspaces" and click on it and click on the workspace you want to use. + +![alt text](../docs/images/re_use_log/logAnalyticsList.png) + +### 3. Copy Resource ID +In the Overview pane, Click on JSON View + +![alt text](../docs/images/re_use_log/logAnalytics.png) + +Copy Resource ID that is your Workspace ID + +![alt text](../docs/images/re_use_log/logAnalyticsJson.png) + +### 4. Set the Workspace ID in Your Environment +Run the following command in your terminal +```bash +azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID '' +``` +Replace `` with the value obtained from Step 3. + +### 5. Continue Deployment +Proceed with the next steps in the [deployment guide](/docs/DeploymentGuide.md#deployment-steps). diff --git a/infra/main.bicep b/infra/main.bicep index 707018a..5dee828 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -12,7 +12,8 @@ param solutionUniqueText string = substring(uniqueString(subscription().id, reso @minLength(3) @metadata({ azd: { type: 'location' } }) @description('Optional. Azure region for all services. Defaults to the resource group location.') -param location string = resourceGroup().location +param location string +var solutionLocation = empty(location) ? resourceGroup().location : location @allowed([ 'australiaeast' @@ -28,13 +29,15 @@ param location string = resourceGroup().location 'westus3' ]) @metadata({ - azd : { + azd: { type: 'location' - usageName : [ + usageName: [ 'OpenAI.GlobalStandard.o3, 500' ] } }) +@description('Required. Location for AI Foundry deployment. This is the location where the AI Foundry resources will be deployed.') +param azureAiServiceLocation string @description('Optional. Location for all AI service resources. This location can be different from the resource group location.') param aiDeploymentLocation string @@ -54,32 +57,70 @@ param aiModelName string = 'o3' @description('Optional. Version of AI model. Review available version numbers per model before setting. Defaults to 2025-04-16.') param aiModelVersion string = '2025-04-16' -@description('Optional. AI model deployment token capacity. Defaults to 500K tokens per minute.') -param aiModelCapacity int = 500 +@description('Optional. AI model deployment token capacity. Lower this if initial provisioning fails due to capacity. Defaults to 50K tokens per minute to improve regional success rate.') +param aiModelCapacity int = 1 -@description('Optional. Specifies the resource tags for all the resources. Tag "azd-env-name" is automatically added to all resources.') -param tags object = {} - -@description('Optional. Enable monitoring for the resources. This will enable Application Insights and Log Analytics. Defaults to false.') -param enableMonitoring bool = false - -@description('Optional. Enable scaling for the container apps. Defaults to false.') -param enableScaling bool = false +@description('Optional. The tags to apply to all deployed Azure resources.') +param tags resourceInput<'Microsoft.Resources/resourceGroups@2025-04-01'>.tags = {} @description('Optional. Enable redundancy for applicable resources. Defaults to false.') param enableRedundancy bool = false -@metadata({ azd: { type: 'location' } }) -@description('Optional. The secondary location for the Cosmos DB account if redundancy is enabled.') -param secondaryLocation string? +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true -@description('Optional. Enable private networking for the resources. Defaults to false.') +@description('Optional. Enable private networking for applicable resources, aligned with the Well Architected Framework recommendations. Defaults to false.') param enablePrivateNetworking bool = false -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true +@description('Optional. Enable monitoring applicable resources, aligned with the Well Architected Framework recommendations. This setting enables Application Insights and Log Analytics and configures all the resources applicable resources to send logs. Defaults to false.') +param enableMonitoring bool = false + +@description('Optional. Enable scalability for applicable resources, aligned with the Well Architected Framework recommendations. Defaults to false.') +param enableScalability bool = false + +@description('Optional. CosmosDB Location') +param cosmosLocation string = 'eastus2' + +@description('Optional. Existing Log Analytics Workspace Resource ID') +param existingLogAnalyticsWorkspaceId string = '' + +@description('Tag, Created by user name') +param createdBy string = contains(deployer(), 'userPrincipalName') + ? split(deployer().userPrincipalName, '@')[0] + : deployer().objectId + +@description('Optional. Resource ID of an existing Foundry project') +param existingFoundryProjectResourceId string = '' + +@description('Optional. Admin username for the Jumpbox Virtual Machine. Set to custom value if enablePrivateNetworking is true.') +@secure() +//param vmAdminUsername string = take(newGuid(), 20) +param vmAdminUsername string? -var resourcesName = toLower(trim(replace( +@description('Optional. Admin password for the Jumpbox Virtual Machine. Set to custom value if enablePrivateNetworking is true.') +@secure() +//param vmAdminPassword string = newGuid() +param vmAdminPassword string? + +@description('Optional. Size of the Jumpbox Virtual Machine when created. Set to custom value if enablePrivateNetworking is true.') +param vmSize string? + +// Extracts subscription, resource group, and workspace name from the resource ID when using an existing Log Analytics workspace +var useExistingLogAnalytics = !empty(existingLogAnalyticsWorkspaceId) +var existingLawSubscription = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[2] : '' +var existingLawResourceGroup = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[4] : '' +var existingLawName = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[8] : '' + +resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-08-01' existing = if (useExistingLogAnalytics) { + name: existingLawName + scope: resourceGroup(existingLawSubscription, existingLawResourceGroup) +} + +var logAnalyticsWorkspaceResourceId = useExistingLogAnalytics + ? existingLogAnalyticsWorkspaceId + : logAnalyticsWorkspace!.outputs.resourceId + +var solutionSuffix = toLower(trim(replace( replace( replace(replace(replace(replace('${solutionName}${solutionUniqueText}', '-', ''), '_', ''), '.', ''), '/', ''), ' ', @@ -100,148 +141,621 @@ var allTags = union( resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { name: 'default' properties: { - tags: union( - reference(resourceGroup().id, '2021-04-01', 'Full').?tags ?? {}, - allTags - ) + tags: { + ...resourceGroup().tags + ...tags + TemplateName: 'Container Migration' + Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' + CreatedBy: createdBy + } } } -module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.12.0' = if (enableMonitoring || enablePrivateNetworking) { - name: take('avm.res.operational-insights.workspace.${resourcesName}', 64) +// Replica regions list based on article in [Azure regions list](https://learn.microsoft.com/azure/reliability/regions-list) and [Enhance resilience by replicating your Log Analytics workspace across regions](https://learn.microsoft.com/azure/azure-monitor/logs/workspace-replication#supported-regions) for supported regions for Log Analytics Workspace. +var replicaRegionPairs = { + australiaeast: 'australiasoutheast' + centralus: 'westus' + eastasia: 'japaneast' + eastus: 'centralus' + eastus2: 'centralus' + japaneast: 'eastasia' + northeurope: 'westeurope' + southeastasia: 'eastasia' + uksouth: 'westeurope' + westeurope: 'northeurope' +} +var replicaLocation = replicaRegionPairs[resourceGroup().location] + +// ========== User Assigned Identity ========== // +// WAF best practices for identity and access management: https://learn.microsoft.com/en-us/azure/well-architected/security/identity-access +var userAssignedIdentityResourceName = 'id-${solutionSuffix}' +module appIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = { + name: take('avm.res.managed-identity.user-assigned-identity.${userAssignedIdentityResourceName}', 64) params: { - name: 'log-${resourcesName}' - location: location + name: userAssignedIdentityResourceName + location: solutionLocation + tags: allTags + enableTelemetry: enableTelemetry + } +} + +// ========== Log Analytics Workspace ========== // +// WAF best practices for Log Analytics: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-log-analytics +// WAF PSRules for Log Analytics: https://azure.github.io/PSRule.Rules.Azure/en/rules/resource/#azure-monitor-logs +var logAnalyticsWorkspaceResourceName = 'log-${solutionSuffix}' +module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.12.0' = if ((enableMonitoring || enablePrivateNetworking) && !useExistingLogAnalytics) { + name: take('avm.res.operational-insights.workspace.${logAnalyticsWorkspaceResourceName}', 64) + params: { + name: logAnalyticsWorkspaceResourceName + location: solutionLocation skuName: 'PerGB2018' dataRetention: 30 diagnosticSettings: [{ useThisWorkspace: true }] tags: allTags enableTelemetry: enableTelemetry + features: { enableLogAccessUsingOnlyResourcePermissions: true } + // WAF aligned configuration for Redundancy + dailyQuotaGb: enableRedundancy ? 10 : null //WAF recommendation: 10 GB per day is a good starting point for most workloads + replication: enableRedundancy + ? { + enabled: true + location: replicaLocation + } + : null + // WAF aligned configuration for Private Networking + publicNetworkAccessForIngestion: enablePrivateNetworking ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: enablePrivateNetworking ? 'Disabled' : 'Enabled' + dataSources: enablePrivateNetworking + ? [ + { + tags: allTags + eventLogName: 'Application' + eventTypes: [ + { + eventType: 'Error' + } + { + eventType: 'Warning' + } + { + eventType: 'Information' + } + ] + kind: 'WindowsEvent' + name: 'applicationEvent' + } + { + counterName: '% Processor Time' + instanceName: '*' + intervalSeconds: 60 + kind: 'WindowsPerformanceCounter' + name: 'windowsPerfCounter1' + objectName: 'Processor' + } + { + kind: 'IISLogs' + name: 'sampleIISLog1' + state: 'OnPremiseEnabled' + } + ] + : null } } +// ========== Application Insights ========== // +// WAF best practices for Application Insights: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/application-insights +// WAF PSRules for Application Insights: https://azure.github.io/PSRule.Rules.Azure/en/rules/resource/#application-insights +var applicationInsightsResourceName = 'appi-${solutionSuffix}' module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (enableMonitoring) { - name: take('avm.res.insights.component.${resourcesName}', 64) + name: take('avm.res.insights.component.${applicationInsightsResourceName}', 64) #disable-next-line no-unnecessary-dependson - dependsOn: [logAnalyticsWorkspace] + //dependsOn: [logAnalyticsWorkspace] params: { - name: 'appi-${resourcesName}' + name: applicationInsightsResourceName + location: solutionLocation + tags: allTags + enableTelemetry: enableTelemetry + retentionInDays: 365 + kind: 'web' + disableIpMasking: false + flowType: 'Bluefield' + // WAF aligned configuration for Monitoring + workspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : '' + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + } +} + +// ========== Virtual Network ========== // +module virtualNetwork './modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { + name: take('module.virtual-network.${solutionSuffix}', 64) + params: { + name: 'vnet-${solutionSuffix}' + addressPrefixes: ['10.0.0.0/20'] location: location - workspaceResourceId: logAnalyticsWorkspace!.outputs!.resourceId - diagnosticSettings: [{ workspaceResourceId: logAnalyticsWorkspace!.outputs!.resourceId }] tags: allTags + logAnalyticsWorkspaceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : '' + resourceSuffix: solutionSuffix enableTelemetry: enableTelemetry } } -resource appIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { - name: 'id-${resourcesName}' - location: location - tags: allTags +// Azure Bastion Host +var bastionHostName = 'bas-${solutionSuffix}' // Bastion host name must be between 3 and 15 characters in length and use numbers and lower-case letters only. +module bastionHost 'br/public:avm/res/network/bastion-host:0.6.1' = if (enablePrivateNetworking) { + name: take('avm.res.network.bastion-host.${bastionHostName}', 64) + params: { + name: bastionHostName + skuName: 'Standard' + location: location + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + diagnosticSettings: enableMonitoring + ? [ + { + name: 'bastionDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceResourceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + } + ] + : null + tags: allTags + enableTelemetry: enableTelemetry + publicIPAddressObject: { + name: 'pip-${bastionHostName}' + zones: [] + } + } +} +// Jumpbox Virtual Machine +var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) +module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.15.0' = if (enablePrivateNetworking) { + name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) + params: { + name: take(jumpboxVmName, 15) // Shorten VM name to 15 characters to avoid Azure limits + vmSize: vmSize ?? 'Standard_DS2_v2' + location: location + adminUsername: vmAdminUsername ?? 'JumpboxAdminUser' + adminPassword: vmAdminPassword ?? 'JumpboxAdminP@ssw0rd1234!' + tags: allTags + zone: 0 + imageReference: { + offer: 'WindowsServer' + publisher: 'MicrosoftWindowsServer' + sku: '2019-datacenter' + version: 'latest' + } + osType: 'Windows' + osDisk: { + name: 'osdisk-${jumpboxVmName}' + managedDisk: { + storageAccountType: 'Standard_LRS' + } + } + encryptionAtHost: false // Some Azure subscriptions do not support encryption at host + nicConfigurations: [ + { + name: 'nic-${jumpboxVmName}' + ipConfigurations: [ + { + name: 'ipconfig1' + subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId + } + ] + diagnosticSettings: enableMonitoring + ? [ + { + name: 'jumpboxDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceResourceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metricCategories: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } + ] + : null + } + ] + enableTelemetry: enableTelemetry + } } var processBlobContainerName = 'processes' var processQueueName = 'processes-queue' -module storageAccount 'modules/storageAccount.bicep' = { - name: take('module.storageAccount.${resourcesName}', 64) - #disable-next-line no-unnecessary-dependson - dependsOn: [logAnalyticsWorkspace] +// ========== Private DNS Zones ========== // +var privateDnsZones = [ + 'privatelink.cognitiveservices.azure.com' + 'privatelink.openai.azure.com' + 'privatelink.services.ai.azure.com' + 'privatelink.documents.azure.com' + 'privatelink.blob.${environment().suffixes.storage}' + 'privatelink.queue.${environment().suffixes.storage}' + 'privatelink.azconfig.io' +] + +// DNS Zone Index Constants +var dnsZoneIndex = { + cognitiveServices: 0 + openAI: 1 + aiServices: 2 + cosmosDB: 3 + storageBlob: 4 + storageQueue: 5 + appConfig: 6 +} + +// List of DNS zone indices that correspond to AI-related services. +var aiRelatedDnsZoneIndices = [ + dnsZoneIndex.cognitiveServices + dnsZoneIndex.openAI + dnsZoneIndex.aiServices +] + +// =================================================== +// DEPLOY PRIVATE DNS ZONES +// - Deploys all zones if no existing Foundry project is used +// - Excludes AI-related zones when using with an existing Foundry project +// =================================================== +@batchSize(5) +module avmPrivateDnsZones 'br/public:avm/res/network/private-dns-zone:0.7.1' = [ + for (zone, i) in privateDnsZones: if (enablePrivateNetworking && (empty(existingFoundryProjectResourceId) || !contains( + aiRelatedDnsZoneIndices, + i + ))) { + name: 'dns-zone-${i}' + params: { + name: zone + tags: allTags + enableTelemetry: enableTelemetry + virtualNetworkLinks: [ + { + name: take('vnetlink-${virtualNetwork!.outputs.name}-${split(zone, '.')[1]}', 80) + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + } + ] + } + } +] + +// ========== AVM WAF ========== // +// ========== Storage account module ========== // +var storageAccountName = 'st${solutionSuffix}' // Storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only. +module storageAccount 'br/public:avm/res/storage/storage-account:0.20.0' = { + name: take('avm.res.storage.storage-account.${storageAccountName}', 64) params: { - name: take('sa${resourcesName}', 24) - location: location - skuName: enableRedundancy ? 'Standard_GZRS' : 'Standard_LRS' - // TODO - private networking - // privateEndpointSubnetResourceId: privateEndpointSubnetResourceId - // blobPrivateDnsZoneResourceId: blobPrivateDnsZoneResourceId - // queuePrivateDnsZoneResourceId: queuePrivateDnsZoneResourceId - containers: [processBlobContainerName] - queues: [processQueueName, '${processQueueName}-dead-letter'] - logAnalyticsWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspace!.outputs!.resourceId : '' + name: storageAccountName + location: solutionLocation + managedIdentities: { systemAssigned: true } + minimumTlsVersion: 'TLS1_2' + enableTelemetry: enableTelemetry + tags: allTags + accessTier: 'Hot' + supportsHttpsTrafficOnly: true roleAssignments: [ { roleDefinitionIdOrName: 'Storage Blob Data Contributor' - principalId: appIdentity.properties.principalId + principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' } { roleDefinitionIdOrName: 'Storage Queue Data Contributor' - principalId: appIdentity.properties.principalId + principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' } ] - enableTelemetry: enableTelemetry - tags: allTags + // WAF aligned networking + networkAcls: { + bypass: 'AzureServices' + defaultAction: enablePrivateNetworking ? 'Deny' : 'Allow' + } + allowBlobPublicAccess: enablePrivateNetworking ? true : false + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + // Private endpoints for blob and queue + privateEndpoints: enablePrivateNetworking + ? [ + { + name: 'pep-storage-${storageAccountName}' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { + name: 'storage-dns-zone-group-blob' + privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.storageBlob]!.outputs.resourceId + } + ] + } + subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId + service: 'blob' + } + { + name: 'pep-queue-${solutionSuffix}' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { + name: 'storage-dns-zone-group-queue' + privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.storageQueue]!.outputs.resourceId + } + ] + } + subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId + service: 'queue' + } + ] + : [] + blobServices: { + corsRules: [] + deleteRetentionPolicyEnabled: false + containers: [ + { + name: 'data' + publicAccess: 'None' + denyEncryptionScopeOverride: false + defaultEncryptionScope: '$account-encryption-key' + } + ] + } + queueServices: { + deleteRetentionPolicyEnabled: true + deleteRetentionPolicyDays: 7 + queues: [ + for queue in ([processQueueName, '${processQueueName}-dead-letter'] ?? []): { + name: queue + } + ] + } } } +//========== AVM WAF ========== // +//========== Cosmos DB module ========== // +var cosmosDbResourceName = 'cosmos-${solutionSuffix}' +var cosmosDbZoneRedundantHaRegionPairs = { + australiaeast: 'uksouth' //'southeastasia' + centralus: 'eastus2' + eastasia: 'southeastasia' + eastus: 'centralus' + eastus2: 'centralus' + japaneast: 'australiaeast' + northeurope: 'westeurope' + southeastasia: 'eastasia' + uksouth: 'westeurope' + westeurope: 'northeurope' +} +var cosmosDbHaLocation = cosmosDbZoneRedundantHaRegionPairs[resourceGroup().location] + var cosmosDatabaseName = 'migration_db' var processCosmosContainerName = 'processes' var agentTelemetryCosmosContainerName = 'agent_telemetry' - -module cosmosDb 'modules/cosmosDb.bicep' = { - name: take('module.cosmosdb.${resourcesName}', 64) - #disable-next-line no-unnecessary-dependson - dependsOn: [logAnalyticsWorkspace] +module cosmosDb 'br/public:avm/res/document-db/database-account:0.15.0' = { + name: take('avm.res.document-db.database-account.${cosmosDbResourceName}', 64) params: { - name: take('cosmos-${resourcesName}', 44) - location: location - zoneRedundant: enableRedundancy - secondaryLocation: enableRedundancy && !empty(secondaryLocation) ? secondaryLocation : '' - databaseName: cosmosDatabaseName - containers: [ - processCosmosContainerName - agentTelemetryCosmosContainerName - 'files' - 'process_statuses' - ] - // TODO - private networking - // privateEndpointSubnetResourceId: privateEndpointSubnetResourceId - // sqlPrivateDnsZoneResourceId: sqlPrivateDnsZoneResourceId - dataAccessIdentityPrincipalId: appIdentity.properties.principalId - logAnalyticsWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspace!.outputs!.resourceId : '' - enableTelemetry: enableTelemetry + name: cosmosDbResourceName + location: cosmosLocation tags: allTags + enableTelemetry: enableTelemetry + sqlDatabases: [ + { + name: cosmosDatabaseName + containers: [ + { + name: processCosmosContainerName + paths: [ + '/_partitionKey' + ] + } + { + name: agentTelemetryCosmosContainerName + paths: [ + '/_partitionKey' + ] + } + { + name: 'files' + paths: [ + '/_partitionKey' + ] + } + { + name: 'process_statuses' + paths: [ + '/_partitionKey' + ] + } + ] + } + ] + + diagnosticSettings: enableMonitoring + ? [ + { + workspaceResourceId: logAnalyticsWorkspaceResourceId + } + ] + : null + + networkRestrictions: { + networkAclBypass: 'None' + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + } + + privateEndpoints: enablePrivateNetworking + ? [ + { + name: 'pep-${cosmosDbResourceName}' + customNetworkInterfaceName: 'nic-${cosmosDbResourceName}' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cosmosDB]!.outputs.resourceId } + ] + } + service: 'Sql' + subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId + } + ] + : [] + + zoneRedundant: enableRedundancy ? true : false + capabilitiesToAdd: enableRedundancy + ? null + : [ + 'EnableServerless' + ] + automaticFailover: enableRedundancy ? true : false + failoverLocations: enableRedundancy + ? [ + { + failoverPriority: 0 + isZoneRedundant: true + locationName: solutionLocation + } + { + failoverPriority: 1 + isZoneRedundant: true + locationName: cosmosDbHaLocation + } + ] + : [ + { + locationName: solutionLocation + failoverPriority: 0 + isZoneRedundant: enableRedundancy + } + ] + // Use built-in Cosmos DB roles for RBAC access + roleAssignments: [ + { + principalId: appIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'DocumentDB Account Contributor' + } + ] + // Create custom data plane role definition and assignment + dataPlaneRoleDefinitions: [ + { + roleName: 'CosmosDB Data Contributor Custom' + dataActions: [ + 'Microsoft.DocumentDB/databaseAccounts/readMetadata' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' + ] + assignments: [ + { principalId: appIdentity.outputs.principalId } + ] + } + ] } + dependsOn: [storageAccount] } var aiModelDeploymentName = aiModelName -module aiFoundry 'br/public:avm/ptn/ai-ml/ai-foundry:0.4.0' = { - name: take('avm.ptn.ai-ml.ai-foundry.${resourcesName}', 64) +var useExistingAiFoundryAiProject = !empty(existingFoundryProjectResourceId) +var aiFoundryAiServicesResourceGroupName = useExistingAiFoundryAiProject + ? split(existingFoundryProjectResourceId, '/')[4] + : 'rg-${solutionSuffix}' +var aiFoundryAiServicesSubscriptionId = useExistingAiFoundryAiProject + ? split(existingFoundryProjectResourceId, '/')[2] + : subscription().id +var aiFoundryAiServicesResourceName = useExistingAiFoundryAiProject + ? split(existingFoundryProjectResourceId, '/')[8] + : 'aif-${solutionSuffix}' + + +resource existingAiFoundryAiServices 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = if (useExistingAiFoundryAiProject) { + name: aiFoundryAiServicesResourceName + scope: resourceGroup(aiFoundryAiServicesSubscriptionId, aiFoundryAiServicesResourceGroupName) +} + +module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.bicep' = if (useExistingAiFoundryAiProject) { + name: take('module.ai-services-model-deployments.${existingAiFoundryAiServices.name}', 64) + scope: resourceGroup(aiFoundryAiServicesSubscriptionId, aiFoundryAiServicesResourceGroupName) + params: { + name: existingAiFoundryAiServices.name + deployments: [ + { + name: aiModelDeploymentName + model: { + format: 'OpenAI' + name: aiModelName + version: aiModelVersion + } + sku: { + name: aiDeploymentType + capacity: aiModelCapacity + } + } + ] + roleAssignments: [ + { + principalId: appIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor' + } + { + principalId: appIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer + } + { + principalId: appIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User + } + ] + } +} + +// Temporarily disabled AI Foundry due to AML workspace creation issues +module aiFoundry 'br/public:avm/ptn/ai-ml/ai-foundry:0.4.0' = if(!useExistingAiFoundryAiProject) { + name: take('avm.ptn.ai-ml.ai-foundry.${solutionSuffix}', 64) params: { #disable-next-line BCP334 - baseName: take(resourcesName, 12) + baseName: take(aiFoundryAiServicesResourceName, 12) baseUniqueName: null - location: empty(aiDeploymentLocation) ? location : aiDeploymentLocation + location: azureAiServiceLocation aiFoundryConfiguration: { + accountName:aiFoundryAiServicesResourceName allowProjectManagement: true roleAssignments: [ { - principalId: appIdentity.properties.principalId + principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor' } { - principalId: appIdentity.properties.principalId + principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer } { - principalId: appIdentity.properties.principalId + principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User } ] - // TODO - private networking - // networking: { - // aiServicesPrivateDnsZoneId: '' - // openAiPrivateDnsZoneId: '' - // cognitiveServicesPrivateDnsZoneId: '' - // } + // Remove networking configuration to avoid AML workspace creation issues + networking: enablePrivateNetworking? { + aiServicesPrivateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.aiServices]!.outputs.resourceId + openAiPrivateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.openAI]!.outputs.resourceId + cognitiveServicesPrivateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cognitiveServices]!.outputs.resourceId + } : null } - // TODO - private networking - //privateEndpointSubnetId: + // Disable private endpoints temporarily to fix AML workspace issue + privateEndpointSubnetResourceId: enablePrivateNetworking ? virtualNetwork!.outputs.backendSubnetResourceId : null + // Only attempt model deployment when explicitly enabled to avoid AccountIsNotSucceeded failures due to quota or model availability. aiModelDeployments: [ { name: aiModelDeploymentName @@ -261,16 +775,15 @@ module aiFoundry 'br/public:avm/ptn/ai-ml/ai-foundry:0.4.0' = { } } +var aiServicesName = useExistingAiFoundryAiProject ? existingAiFoundryAiServices.name : aiFoundry.outputs.aiServicesName module appConfiguration 'br/public:avm/res/app-configuration/configuration-store:0.9.1' = { - name: take('avm.res.app-config.store.${resourcesName}', 64) + name: take('avm.res.app-config.store.${solutionSuffix}', 64) params: { - location: location - name: 'appcs-${resourcesName}' + location: solutionLocation + name: 'appcs-${solutionSuffix}' disableLocalAuth: false // needed to allow setting app config key values from this module - enablePurgeProtection: false - // TODO - private networking - //privateEndpoints: tags: allTags + // Always set key values during deployment since Container Apps will be in private network keyValues: [ { name: 'APP_LOGGING_ENABLE' @@ -298,11 +811,11 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store } { name: 'AZURE_OPENAI_ENDPOINT' - value: 'https://${aiFoundry.outputs.aiServicesName}.cognitiveservices.azure.com/' + value: 'https://${aiServicesName}.cognitiveservices.azure.com/' } { name: 'AZURE_OPENAI_ENDPOINT_BASE' - value: 'https://${aiFoundry.outputs.aiServicesName}.cognitiveservices.azure.com/' + value: 'https://${aiServicesName}.cognitiveservices.azure.com/' } { name: 'AZURE_TRACING_ENABLED' @@ -310,7 +823,7 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store } { name: 'STORAGE_ACCOUNT_BLOB_URL' - value: storageAccount.outputs.blobEndpoint + value: 'https://${storageAccountName}.blob.${environment().suffixes.storage}' } { name: 'STORAGE_ACCOUNT_NAME' @@ -326,7 +839,7 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store } { name: 'STORAGE_ACCOUNT_QUEUE_URL' - value: storageAccount.outputs.queueEndpoint + value: 'https://${storageAccountName}.queue.${environment().suffixes.storage}' } { name: 'COSMOS_DB_CONTAINER_NAME' @@ -345,7 +858,7 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store value: processCosmosContainerName } { - name: 'COSMOS_DB_PROCESS_LOG_CONTAINER' // TODO - is this being used? + name: 'COSMOS_DB_PROCESS_LOG_CONTAINER' value: agentTelemetryCosmosContainerName } { @@ -353,75 +866,113 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store value: 'AzureOpenAI' } { - name: 'STORAGE_QUEUE_ACCOUNT' // TODO - is this being used? + name: 'STORAGE_QUEUE_ACCOUNT' value: storageAccount.outputs.name } ] roleAssignments: [ { - principalId: appIdentity.properties.principalId + principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' roleDefinitionIdOrName: 'App Configuration Data Reader' } ] enableTelemetry: enableTelemetry + managedIdentities: { systemAssigned: true } + sku: 'Standard' + publicNetworkAccess: 'Enabled' } } -var containerAppsEnvironmentName = 'cae-${resourcesName}' -module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.3' = { - name: take('avm.res.app.managed-environment.${containerAppsEnvironmentName}', 64) - #disable-next-line no-unnecessary-dependson - dependsOn: [logAnalyticsWorkspace, applicationInsights] // required due to optional flags that could change dependency +module avmAppConfigUpdated 'br/public:avm/res/app-configuration/configuration-store:0.6.3' = if (enablePrivateNetworking) { + name: take('avm.res.app-configuration.configuration-store-update.${solutionSuffix}', 64) params: { - name: containerAppsEnvironmentName - infrastructureResourceGroupName: '${resourceGroup().name}-ME-${containerAppsEnvironmentName}' - location: location - zoneRedundant: enableRedundancy && enablePrivateNetworking - publicNetworkAccess: 'Enabled' // public access required for frontend - // TODO - private networking: - //infrastructureSubnetResourceId: enablePrivateNetworking ? network.outputs.subnetWebResourceId : null - managedIdentities: { - userAssignedResourceIds: [ - appIdentity.id - ] - } - appInsightsConnectionString: enableMonitoring ? applicationInsights!.outputs.connectionString : null - appLogsConfiguration: enableMonitoring ? { - destination: 'log-analytics' - logAnalyticsConfiguration: { - customerId: logAnalyticsWorkspace!.outputs!.logAnalyticsWorkspaceId - sharedKey: logAnalyticsWorkspace!.outputs!.primarySharedKey - } - } : {} - // TODO - private networking: - // workloadProfiles: enablePrivateNetworking - // ? [ - // // NOTE: workload profiles are required for private networking - // { - // name: 'Consumption' - // workloadProfileType: 'Consumption' - // } - // ] - // : [] + name: 'appcs-${solutionSuffix}' + location: solutionLocation + managedIdentities: { systemAssigned: true } + sku: 'Standard' + enableTelemetry: enableTelemetry tags: allTags + disableLocalAuth: true + // Keep public access enabled for Container Apps access (Container Apps not in private network due to capacity constraints) + publicNetworkAccess: 'Enabled' + privateEndpoints: enablePrivateNetworking + ? [ + { + name: 'pep-appconfig-${solutionSuffix}' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { + name: 'appconfig-dns-zone-group' + privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.appConfig]!.outputs.resourceId + } + ] + } + subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId + } + ] + : [] + } + dependsOn: [ + appConfiguration + ] +} + +var logAnalyticsPrimarySharedKey = useExistingLogAnalytics + ? existingLogAnalyticsWorkspace!.listKeys().primarySharedKey + : logAnalyticsWorkspace!.outputs!.primarySharedKey +var logAnalyticsWorkspaceId = useExistingLogAnalytics + ? existingLogAnalyticsWorkspace!.properties.customerId + : logAnalyticsWorkspace!.outputs.logAnalyticsWorkspaceId +// ========== Container App Environment ========== // +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.2' = { + name: take('avm.res.app.managed-environment.${solutionSuffix}', 64) + params: { + name: 'cae-${solutionSuffix}' + location: location + managedIdentities: { systemAssigned: true } + appLogsConfiguration: enableMonitoring + ? { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspaceId + sharedKey: logAnalyticsPrimarySharedKey + } + } + : null + workloadProfiles: [ + { + name: 'Consumption' + workloadProfileType: 'Consumption' + } + ] enableTelemetry: enableTelemetry + publicNetworkAccess: 'Enabled' // Always enabled for Container Apps Environment + + // <========== WAF related parameters + + platformReservedCidr: '172.17.17.0/24' + platformReservedDnsIP: '172.17.17.17' + zoneRedundant: (enablePrivateNetworking) ? true : false // Enable zone redundancy if private networking is enabled + infrastructureSubnetResourceId: (enablePrivateNetworking) + ? virtualNetwork.outputs.containersSubnetResourceId // Use the container app subnet + : null // Use the container app subnet } } var backendContainerPort = 80 -var backendContainerAppName = take('ca-backend-api-${resourcesName}', 32) +var backendContainerAppName = take('ca-backend-api-${solutionSuffix}', 32) module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { name: take('avm.res.app.container-app.${backendContainerAppName}', 64) #disable-next-line no-unnecessary-dependson dependsOn: [applicationInsights] params: { name: backendContainerAppName - location: location + location: solutionLocation environmentResourceId: containerAppsEnvironment.outputs.resourceId managedIdentities: { userAssignedResourceIds: [ - appIdentity.id + appIdentity.outputs.resourceId ] } containers: [ @@ -436,7 +987,7 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { } { name: 'AZURE_CLIENT_ID' - value: appIdentity.properties.clientId + value: appIdentity.outputs.clientId } ], enableMonitoring @@ -457,18 +1008,20 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { ingressTargetPort: backendContainerPort ingressExternal: true scaleSettings: { - maxReplicas: enableScaling ? 3 : 1 + maxReplicas: enableScalability ? 3 : 1 minReplicas: 1 - rules: enableScaling ? [ - { - name: 'http-scaler' - http: { - metadata: { - concurrentRequests: 100 + rules: enableScalability + ? [ + { + name: 'http-scaler' + http: { + metadata: { + concurrentRequests: 100 + } + } } - } - } - ] : [] + ] + : [] } corsPolicy: { allowedOrigins: [ @@ -492,16 +1045,16 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { } } -var frontEndContainerAppName = take('ca-frontend-${resourcesName}', 32) +var frontEndContainerAppName = take('ca-frontend-${solutionSuffix}', 32) module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { name: take('avm.res.app.container-app.${frontEndContainerAppName}', 64) params: { name: frontEndContainerAppName - location: location + location: solutionLocation environmentResourceId: containerAppsEnvironment.outputs.resourceId managedIdentities: { userAssignedResourceIds: [ - appIdentity.id + appIdentity.outputs.resourceId ] } containers: [ @@ -527,36 +1080,38 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { ingressTargetPort: 3000 ingressExternal: true scaleSettings: { - maxReplicas: enableScaling ? 3 : 1 + maxReplicas: enableScalability ? 3 : 1 minReplicas: 1 - rules: enableScaling ? [ - { - name: 'http-scaler' - http: { - metadata: { - concurrentRequests: 100 + rules: enableScalability + ? [ + { + name: 'http-scaler' + http: { + metadata: { + concurrentRequests: 100 + } + } } - } - } - ] : [] + ] + : [] } tags: allTags enableTelemetry: enableTelemetry } } -var processorContainerAppName = take('ca-processor-${resourcesName}', 32) +var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32) module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { name: take('avm.res.app.container-app.${processorContainerAppName}', 64) #disable-next-line no-unnecessary-dependson dependsOn: [applicationInsights] params: { name: processorContainerAppName - location: location + location: solutionLocation environmentResourceId: containerAppsEnvironment.outputs.resourceId managedIdentities: { userAssignedResourceIds: [ - appIdentity.id + appIdentity.outputs.resourceId ] } containers: [ @@ -571,7 +1126,7 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { } { name: 'AZURE_CLIENT_ID' - value: appIdentity.properties.clientId + value: appIdentity.outputs.clientId } { name: 'AZURE_STORAGE_ACCOUNT_NAME' // TODO - verify name and if needed or if pulled from app config service @@ -591,7 +1146,8 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { ] : [] ) - resources: { // TODO - assess increasing resource limits + resources: { + // TODO - assess increasing resource limits cpu: 2 memory: '4.0Gi' } @@ -601,7 +1157,7 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { disableIngress: true ingressExternal: false scaleSettings: { - maxReplicas: enableScaling ? 3 : 1 + maxReplicas: enableScalability ? 3 : 1 minReplicas: 1 //rules: [] - TODO - what scaling rules to use here? } diff --git a/infra/main.parameters.json b/infra/main.parameters.json index e9929f3..77530a0 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -31,6 +31,18 @@ }, "enableTelemetry": { "value": true + }, + "existingLogAnalyticsWorkspaceId": { + "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" + }, + "existingFoundryProjectResourceId":{ + "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" + }, + "vmAdminUsername": { + "value": "${AZURE_ENV_VM_ADMIN_USERNAME}" + }, + "vmAdminPassword": { + "value": "${AZURE_ENV_VM_ADMIN_PASSWORD}" } } } diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json new file mode 100644 index 0000000..c268d6e --- /dev/null +++ b/infra/main.waf.parameters.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "solutionName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "secondaryLocation": { + "value": "${AZURE_SECONDARY_LOCATION}" + }, + "containerRegistryHost": { + "value": "${AZURE_CONTAINER_REGISTRY_HOST}" + }, + "aiDeploymentLocation": { + "value": "${AZURE_AI_DEPLOYMENT_LOCATION}" + }, + "aiDeploymentType": { + "value": "${AZURE_AI_DEPLOYMENT_TYPE}" + }, + "aiModelName": { + "value": "${AZURE_AI_MODEL_NAME}" + }, + "aiModelVersion": { + "value": "${AZURE_AI_MODEL_VERSION}" + }, + "aiModelCapacity": { + "value": "${AZURE_AI_MODEL_CAPACITY}" + }, + "enableTelemetry": { + "value": true + }, + "enableMonitoring": { + "value": true + }, + "enablePrivateNetworking": { + "value": true + }, + "enableScalability": { + "value": true + }, + "vmAdminUsername": { + "value": "${AZURE_ENV_VM_ADMIN_USERNAME}" + }, + "vmAdminPassword": { + "value": "${AZURE_ENV_VM_ADMIN_PASSWORD}" + }, + "existingLogAnalyticsWorkspaceId": { + "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" + }, + "existingFoundryProjectResourceId":{ + "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" + } + } +} diff --git a/infra/modules/ai-services-deployments.bicep b/infra/modules/ai-services-deployments.bicep new file mode 100644 index 0000000..8c32a0e --- /dev/null +++ b/infra/modules/ai-services-deployments.bicep @@ -0,0 +1,197 @@ +@description('Required. The name of Cognitive Services account.') +param name string + +@description('Optional. SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@allowed([ + 'C2' + 'C3' + 'C4' + 'F0' + 'F1' + 'S' + 'S0' + 'S1' + 'S10' + 'S2' + 'S3' + 'S4' + 'S5' + 'S6' + 'S7' + 'S8' + 'S9' +]) +param sku string = 'S0' + +import { deploymentType } from 'br:mcr.microsoft.com/bicep/avm/res/cognitive-services/account:0.13.2' +@description('Optional. Array of deployments about cognitive service accounts to create.') +param deployments deploymentType[]? + +import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Array of role assignments to create.') +param roleAssignments roleAssignmentType[]? + +var builtInRoleNames = { + 'Cognitive Services Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' + ) + 'Cognitive Services Custom Vision Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'c1ff6cc2-c111-46fe-8896-e0ef812ad9f3' + ) + 'Cognitive Services Custom Vision Deployment': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5c4089e1-6d96-4d2f-b296-c1bc7137275f' + ) + 'Cognitive Services Custom Vision Labeler': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '88424f51-ebe7-446f-bc41-7fa16989e96c' + ) + 'Cognitive Services Custom Vision Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '93586559-c37d-4a6b-ba08-b9f0940c2d73' + ) + 'Cognitive Services Custom Vision Trainer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '0a5ae4ab-0d65-4eeb-be61-29fc9b54394b' + ) + 'Cognitive Services Data Reader (Preview)': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b59867f0-fa02-499b-be73-45a86b5b3e1c' + ) + 'Cognitive Services Face Recognizer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '9894cab4-e18a-44aa-828b-cb588cd6f2d7' + ) + 'Cognitive Services Immersive Reader User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b2de6794-95db-4659-8781-7e080d3f2b9d' + ) + 'Cognitive Services Language Owner': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f07febfe-79bc-46b1-8b37-790e26e6e498' + ) + 'Cognitive Services Language Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '7628b7b8-a8b2-4cdc-b46f-e9b35248918e' + ) + 'Cognitive Services Language Writer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f2310ca1-dc64-4889-bb49-c8e0fa3d47a8' + ) + 'Cognitive Services LUIS Owner': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f72c8140-2111-481c-87ff-72b910f6e3f8' + ) + 'Cognitive Services LUIS Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18e81cdc-4e98-4e29-a639-e7d10c5a6226' + ) + 'Cognitive Services LUIS Writer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '6322a993-d5c9-4bed-b113-e49bbea25b27' + ) + 'Cognitive Services Metrics Advisor Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'cb43c632-a144-4ec5-977c-e80c4affc34a' + ) + 'Cognitive Services Metrics Advisor User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '3b20f47b-3825-43cb-8114-4bd2201156a8' + ) + 'Cognitive Services OpenAI Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a001fd3d-188f-4b5d-821b-7da978bf7442' + ) + 'Cognitive Services OpenAI User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + ) + 'Cognitive Services QnA Maker Editor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f4cc2bf9-21be-47a1-bdf1-5c5804381025' + ) + 'Cognitive Services QnA Maker Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '466ccd10-b268-4a11-b098-b4849f024126' + ) + 'Cognitive Services Speech Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '0e75ca1e-0464-4b4d-8b93-68208a576181' + ) + 'Cognitive Services Speech User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f2dc8367-1007-4938-bd23-fe263f013447' + ) + 'Cognitive Services User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a97b65f3-24c7-4388-baec-2e87135dc908' + ) + 'Azure AI Developer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '64702f94-c441-49e6-a78b-ef80e0188fee' + ) + Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') + Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') + 'Role Based Access Control Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f58310d9-a9f6-439a-9e8d-f62e7b41a168' + ) + 'User Access Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' + ) +} + +var formattedRoleAssignments = [ + for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, { + roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains( + roleAssignment.roleDefinitionIdOrName, + '/providers/Microsoft.Authorization/roleDefinitions/' + ) + ? roleAssignment.roleDefinitionIdOrName + : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName)) + }) +] + +resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = { + name: name +} + +@batchSize(1) +resource cognitiveService_deployments 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = [ + for (deployment, index) in (deployments ?? []): { + parent: cognitiveService + name: deployment.?name ?? '${name}-deployments' + properties: { + model: deployment.model + raiPolicyName: deployment.?raiPolicyName + versionUpgradeOption: deployment.?versionUpgradeOption + } + sku: deployment.?sku ?? { + name: sku + capacity: sku.?capacity + tier: sku.?tier + size: sku.?size + family: sku.?family + } + } +] + +resource cognitiveService_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for (roleAssignment, index) in (formattedRoleAssignments ?? []): { + name: roleAssignment.?name ?? guid(cognitiveService.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) + properties: { + roleDefinitionId: roleAssignment.roleDefinitionId + principalId: roleAssignment.principalId + description: roleAssignment.?description + principalType: roleAssignment.?principalType + condition: roleAssignment.?condition + conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set + delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId + } + scope: cognitiveService + } +] diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep new file mode 100644 index 0000000..84a2f25 --- /dev/null +++ b/infra/modules/virtualNetwork.bicep @@ -0,0 +1,395 @@ +/****************************************************************************************************************************/ +// Networking - NSGs, VNET and Subnets. Each subnet has its own NSG +/****************************************************************************************************************************/ +@description('Name of the virtual network.') +param name string + +@description('Azure region to deploy resources.') +param location string = resourceGroup().location + +@description('Required. An Array of 1 or more IP Address Prefixes for the Virtual Network.') +param addressPrefixes array + +@description('An array of subnets to be created within the virtual network. Each subnet can have its own configuration and associated Network Security Group (NSG).') +param subnets subnetType[] = [ + { + name: 'containers' + addressPrefixes: ['10.0.2.0/24'] // /24 (10.0.2.0 - 10.0.2.255), 256 addresses + delegation: 'Microsoft.App/environments' + networkSecurityGroup: { + name: 'nsg-containers' + securityRules: [ + //Inbound Rules + { + name: 'AllowHttpsInbound' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 100 + protocol: 'Tcp' + sourceAddressPrefix: 'Internet' + sourcePortRange: '*' + destinationPortRanges: ['443', '80'] + destinationAddressPrefixes: ['10.0.2.0/24'] + } + } + { + name: 'AllowAzureLoadBalancerInbound' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 102 + protocol: '*' + sourceAddressPrefix: 'AzureLoadBalancer' + sourcePortRange: '*' + destinationPortRanges: ['30000-32767'] + destinationAddressPrefixes: ['10.0.2.0/24'] + } + } + { + name: 'AllowSideCarsInbound' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 103 + protocol: '*' + sourcePortRange: '*' + sourceAddressPrefixes: ['10.0.2.0/24'] + destinationPortRange: '*' + destinationAddressPrefix: '*' + } + } + //Outbound Rules + { + name: 'AllowOutboundToAzureServices' + properties: { + access: 'Allow' + direction: 'Outbound' + priority: 200 + protocol: '*' + sourceAddressPrefixes: ['10.0.2.0/24'] + sourcePortRange: '*' + destinationPortRange: '*' + destinationAddressPrefix: '*' + } + } + { + name: 'deny-hop-outbound' + properties: { + access: 'Deny' + direction: 'Outbound' + priority: 100 + protocol: '*' + sourcePortRange: '*' + destinationPortRanges: ['3389', '22'] + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: '*' + } + } + ] + } + } + { + name: 'backend' + addressPrefixes: ['10.0.0.0/24'] // /24 (10.0.0.0 - 10.0.0.255), 256 addresses + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + networkSecurityGroup: { + name: 'nsg-backend' + securityRules: [ + { + name: 'Deny-hop-outbound' + properties: { + access: 'Deny' + direction: 'Outbound' + priority: 200 + protocol: '*' + sourcePortRange: '*' + destinationPortRanges: ['3389', '22'] + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: '*' + } + } + ] + } + } + { + name: 'AzureBastionSubnet' // Required name for Azure Bastion + addressPrefixes: ['10.0.10.0/26'] + networkSecurityGroup: { + name: 'nsg-bastion' + securityRules: [ + { + name: 'AllowGatewayManager' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 2702 + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'GatewayManager' + destinationAddressPrefix: '*' + } + } + { + name: 'AllowHttpsInBound' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 2703 + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'Internet' + destinationAddressPrefix: '*' + } + } + { + name: 'AllowSshRdpOutbound' + properties: { + access: 'Allow' + direction: 'Outbound' + priority: 100 + protocol: '*' + sourcePortRange: '*' + destinationPortRanges: ['22', '3389'] + sourceAddressPrefix: '*' + destinationAddressPrefix: 'VirtualNetwork' + } + } + { + name: 'AllowAzureCloudOutbound' + properties: { + access: 'Allow' + direction: 'Outbound' + priority: 110 + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: '*' + destinationAddressPrefix: 'AzureCloud' + } + } + ] + } + } + { + name: 'jumpbox' + addressPrefixes: ['10.0.12.0/23'] // /23 (10.0.12.0 - 10.0.13.255), 512 addresses + networkSecurityGroup: { + name: 'nsg-jumpbox' + securityRules: [ + { + name: 'AllowRdpFromBastion' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 100 + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '3389' + sourceAddressPrefixes: ['10.0.10.0/26'] // Azure Bastion subnet + destinationAddressPrefixes: ['10.0.12.0/23'] + } + } + ] + } + } +] + +@description('Optional. Tags to be applied to the resources.') +param tags object = {} + +@description('Optional. The resource ID of the Log Analytics Workspace to send diagnostic logs to.') +param logAnalyticsWorkspaceId string + +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +@description('Required. Suffix for resource naming.') +param resourceSuffix string + +// VM Size Notes: +// 1 B-series VMs (like Standard_B2ms) do not support accelerated networking. +// 2 Pick a VM size that does support accelerated networking (the usual jump-box candidates): +// Standard_DS2_v2 (2 vCPU, 7 GiB RAM, Premium SSD) // The most broadly available (it’s a legacy SKU supported in virtually every region). +// Standard_D2s_v3 (2 vCPU, 8 GiB RAM, Premium SSD) // next most common +// Standard_D2s_v4 (2 vCPU, 8 GiB RAM, Premium SSD) // Newest, so fewer regions availabl + +// Subnet Classless Inter-Doman Routing (CIDR) Sizing Reference Table (Best Practices) +// | CIDR | # of Addresses | # of /24s | Notes | +// |-----------|---------------|-----------|----------------------------------------| +// | /24 | 256 | 1 | Smallest recommended for Azure subnets | +// | /23 | 512 | 2 | Good for 1-2 workloads per subnet | +// | /22 | 1024 | 4 | Good for 2-4 workloads per subnet | +// | /21 | 2048 | 8 | | +// | /20 | 4096 | 16 | Used for default VNet in this solution | +// | /19 | 8192 | 32 | | +// | /18 | 16384 | 64 | | +// | /17 | 32768 | 128 | | +// | /16 | 65536 | 256 | | +// | /15 | 131072 | 512 | | +// | /14 | 262144 | 1024 | | +// | /13 | 524288 | 2048 | | +// | /12 | 1048576 | 4096 | | +// | /11 | 2097152 | 8192 | | +// | /10 | 4194304 | 16384 | | +// | /9 | 8388608 | 32768 | | +// | /8 | 16777216 | 65536 | | +// +// Best Practice Notes: +// - Use /24 as the minimum subnet size for Azure (smaller subnets are not supported for most services). +// - Plan for future growth: allocate larger address spaces (e.g., /20 or /21 for VNets) to allow for new subnets. +// - Avoid overlapping address spaces with on-premises or other VNets. +// - Use contiguous, non-overlapping ranges for subnets. +// - Document subnet usage and purpose in code comments. +// - For AVM modules, ensure only one delegation per subnet and leave delegations empty if not required. + +// 1. Create NSGs for subnets +// using AVM Network Security Group module +// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/network-security-group + +@batchSize(1) +module nsgs 'br/public:avm/res/network/network-security-group:0.5.1' = [ + for (subnet, i) in subnets: if (!empty(subnet.?networkSecurityGroup)) { + name: take('avm.res.network.network-security-group.${subnet.?networkSecurityGroup.name}.${resourceSuffix}', 64) + params: { + name: '${subnet.?networkSecurityGroup.name}-${resourceSuffix}' + location: location + securityRules: subnet.?networkSecurityGroup.securityRules + tags: tags + enableTelemetry: enableTelemetry + } + } +] + +// 2. Create VNet and subnets, with subnets associated with corresponding NSGs +// using AVM Virtual Network module +// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/virtual-network + +module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.0' = { + name: take('avm.res.network.virtual-network.${name}', 64) + params: { + name: name + location: location + addressPrefixes: addressPrefixes + subnets: [ + for (subnet, i) in subnets: { + name: subnet.name + addressPrefixes: subnet.?addressPrefixes + networkSecurityGroupResourceId: !empty(subnet.?networkSecurityGroup) ? nsgs[i]!.outputs.resourceId : null + privateEndpointNetworkPolicies: subnet.?privateEndpointNetworkPolicies + privateLinkServiceNetworkPolicies: subnet.?privateLinkServiceNetworkPolicies + delegation: subnet.?delegation + } + ] + diagnosticSettings: [ + { + name: 'vnetDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metricCategories: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } + ] + tags: tags + enableTelemetry: enableTelemetry + } +} + +output name string = virtualNetwork.outputs.name +output resourceId string = virtualNetwork.outputs.resourceId + +// combined output array that holds subnet details along with NSG information +output subnets subnetOutputType[] = [ + for (subnet, i) in subnets: { + name: subnet.name + resourceId: virtualNetwork.outputs.subnetResourceIds[i] + nsgName: !empty(subnet.?networkSecurityGroup) ? subnet.?networkSecurityGroup.name : null + nsgResourceId: !empty(subnet.?networkSecurityGroup) ? nsgs[i]!.outputs.resourceId : null + } +] + +// Dynamic outputs for individual subnets for backward compatibility +output containersSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'containers') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'containers')] + : '' +output backendSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'backend') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'backend')] + : '' +output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] + : '' +output jumpboxSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'jumpbox') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'jumpbox')] + : '' + +@export() +@description('Custom type definition for subnet resource information as output') +type subnetOutputType = { + @description('The name of the subnet.') + name: string + + @description('The resource ID of the subnet.') + resourceId: string + + @description('The name of the associated network security group, if any.') + nsgName: string? + + @description('The resource ID of the associated network security group, if any.') + nsgResourceId: string? +} + +@export() +@description('Custom type definition for subnet configuration') +type subnetType = { + @description('Required. The Name of the subnet resource.') + name: string + + @description('Required. Prefixes for the subnet.') // Required to ensure at least one prefix is provided + addressPrefixes: string[] + + @description('Optional. The delegation to enable on the subnet.') + delegation: string? + + @description('Optional. enable or disable apply network policies on private endpoint in the subnet.') + privateEndpointNetworkPolicies: ('Disabled' | 'Enabled' | 'NetworkSecurityGroupEnabled' | 'RouteTableEnabled')? + + @description('Optional. Enable or disable apply network policies on private link service in the subnet.') + privateLinkServiceNetworkPolicies: ('Disabled' | 'Enabled')? + + @description('Optional. Network Security Group configuration for the subnet.') + networkSecurityGroup: networkSecurityGroupType? + + @description('Optional. The resource ID of the route table to assign to the subnet.') + routeTableResourceId: string? + + @description('Optional. An array of service endpoint policies.') + serviceEndpointPolicies: object[]? + + @description('Optional. The service endpoints to enable on the subnet.') + serviceEndpoints: string[]? + + @description('Optional. Set this property to false to disable default outbound connectivity for all VMs in the subnet. This property can only be set at the time of subnet creation and cannot be updated for an existing subnet.') + defaultOutboundAccess: bool? +} + +@export() +@description('Custom type definition for network security group configuration') +type networkSecurityGroupType = { + @description('Required. The name of the network security group.') + name: string + + @description('Required. The security rules for the network security group.') + securityRules: object[] +}