From 4da5f79353e517b2ebb4d59bf046920d9ebe23ed Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Wed, 1 Oct 2025 09:04:01 +0530 Subject: [PATCH 1/6] post deployment script changes --- docs/AVMPostDeploymentGuide.md | 30 +++++++ infra/main.bicep | 4 + infra/scripts/process_sample_data.sh | 128 ++++++++++++++++++--------- 3 files changed, 118 insertions(+), 44 deletions(-) create mode 100644 docs/AVMPostDeploymentGuide.md diff --git a/docs/AVMPostDeploymentGuide.md b/docs/AVMPostDeploymentGuide.md new file mode 100644 index 00000000..0ba7a63b --- /dev/null +++ b/docs/AVMPostDeploymentGuide.md @@ -0,0 +1,30 @@ +# AVM Post Deployment Guide +This document provides guidance on post-deployment steps after deploying the Build Your Own Copilot Accelerator from the [AVM (Azure Verified Modules) repository](https://github.com/Azure/bicep-registry-modules/tree/main/avm/ptn/sa/build-your-own-copilot). + +## Post Deployment Steps +1. Clone the Repository + First, clone this repository to access the post-deployment scripts: + ```bash + git clone https://github.com/microsoft/Build-your-own-copilot-Solution-Accelerator.git + ``` + ```bash + cd Build-your-own-copilot-Solution-Accelerator + ``` + +2. Import Sample Data -Run bash command printed in the terminal. The bash command will look like the following: + + ```bash + ./infra/scripts/process_sample_data.sh + ``` + If the deployment does not exist or has been deleted – The script will prompt you to manually enter the required values + +3. Add Authentication Provider + + Follow steps in [App Authentication](https://github.com/microsoft/Build-your-own-copilot-Solution-Accelerator/blob/main/docs/AppAuthentication.md) to configure authentication in app service. + >Note that Authentication changes can take up to 10 minutes. + +4. Deleting Resources After a Failed Deployment + + Follow steps in [Delete Resource Group](https://github.com/microsoft/Build-your-own-copilot-Solution-Accelerator/blob/main/docs/DeleteResourceGroup.md) if your deployment fails and/or you need to clean up the resources. + +By following these steps, you’ll ensure a smooth transition from deployment to hands-on usage. \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep index 1fb0f6a5..7634defe 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -287,6 +287,7 @@ resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { TemplateName: 'Client Advisor' Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' CreatedBy: createdBy + DeploymentName: deployment().name } } } @@ -1367,3 +1368,6 @@ output USE_AI_PROJECT_CLIENT string = useAIProjectClientFlag @description('Indicates whether the internal stream should be used.') output USE_INTERNAL_STREAM string = useInternalStream + +@description('The subscription ID where the resources are deployed.') +output AZURE_SUBSCRIPTION_ID string = subscription().subscriptionId diff --git a/infra/scripts/process_sample_data.sh b/infra/scripts/process_sample_data.sh index c0b9f8a3..224290b1 100644 --- a/infra/scripts/process_sample_data.sh +++ b/infra/scripts/process_sample_data.sh @@ -2,16 +2,6 @@ # Variables resourceGroupName="$1" - cosmosDbAccountName="$2" - storageAccount="$3" - fileSystem="$4" - keyvaultName="$5" - sqlServerName="$6" - SqlDatabaseName="$7" - webAppManagedIdentityClientId="$8" - webAppManagedIdentityDisplayName="$9" - aiSearchName="${10}" - aif_resource_id="${11}" # Global variables to track original network access states original_storage_public_access="" @@ -20,6 +10,7 @@ aif_resource_group="" aif_account_resource_id="" # Add global variable for SQL Server public access + original_sql_public_access="" created_sql_allow_all_firewall_rule="false" original_full_range_rule_present="false" @@ -286,53 +277,102 @@ # Set up trap to ensure cleanup happens on exit trap cleanup_on_exit EXIT INT TERM - # get parameters from azd env, if not provided - if [ -z "$resourceGroupName" ]; then - resourceGroupName=$(azd env get-value RESOURCE_GROUP_NAME) + if az account show &> /dev/null; then + echo "Already authenticated with Azure." + else + echo "Authenticating with Azure CLI..." + az login + echo "Authenticated with Azure CLI." fi + # fetch all variables from deployment outputs - if [ -z "$cosmosDbAccountName" ]; then - cosmosDbAccountName=$(azd env get-value COSMOSDB_ACCOUNT_NAME) - fi + deploymentName=$(az group show --name "$resourceGroupName" --query "tags.DeploymentName" -o tsv) + echo "Deployment Name (from tag): $deploymentName" - if [ -z "$storageAccount" ]; then - storageAccount=$(azd env get-value STORAGE_ACCOUNT_NAME) - fi +if az deployment group show --resource-group "$resourceGroupName" --name "$deploymentName" &>/dev/null; then + cosmosDbAccountName=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.cosmosdB_ACCOUNT_NAME.value" -o tsv) + echo "Cosmos DB Account Name (from outputs): $cosmosDbAccountName" - if [ -z "$fileSystem" ]; then - fileSystem=$(azd env get-value STORAGE_CONTAINER_NAME) - fi + storageAccount=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.storagE_ACCOUNT_NAME.value" -o tsv) + echo "Storage Account Name (from outputs): $storageAccount" - if [ -z "$keyvaultName" ]; then - keyvaultName=$(azd env get-value KEY_VAULT_NAME) - fi + fileSystem=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.storagE_CONTAINER_NAME.value" -o tsv) + echo "Storage Container Name (from outputs): $fileSystem" - if [ -z "$sqlServerName" ]; then - sqlServerName=$(azd env get-value SQLDB_SERVER_NAME) - fi + keyvaultName=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.keY_VAULT_NAME.value" -o tsv) + echo "Key Vault Name (from outputs): $keyvaultName" - if [ -z "$SqlDatabaseName" ]; then - SqlDatabaseName=$(azd env get-value SQLDB_DATABASE) - fi + sqlServerName=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.sqldB_SERVER_NAME.value" -o tsv) + echo "SQL Server Name (from outputs): $sqlServerName" - if [ -z "$webAppManagedIdentityClientId" ]; then - webAppManagedIdentityClientId=$(azd env get-value MANAGEDIDENTITY_WEBAPP_CLIENTID) - fi + SqlDatabaseName=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.sqldB_DATABASE.value" -o tsv) + echo "SQL Database Name (from outputs): $SqlDatabaseName" - if [ -z "$webAppManagedIdentityDisplayName" ]; then - webAppManagedIdentityDisplayName=$(azd env get-value MANAGEDIDENTITY_WEBAPP_NAME) - fi + webAppManagedIdentityClientId=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.managedidentitY_WEBAPP_CLIENTID.value" -o tsv) + echo "Web App Managed Identity Client ID (from outputs): $webAppManagedIdentityClientId" - if [ -z "$aiSearchName" ]; then - aiSearchName=$(azd env get-value AI_SEARCH_SERVICE_NAME) - fi + webAppManagedIdentityDisplayName=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.managedidentitY_WEBAPP_NAME.value" -o tsv) + echo "Web App Managed Identity Display Name (from outputs): $webAppManagedIdentityDisplayName" - if [ -z "$aif_resource_id" ]; then - aif_resource_id=$(azd env get-value AI_FOUNDRY_RESOURCE_ID) - fi + aiSearchName=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.aI_SEARCH_SERVICE_NAME.value" -o tsv) + echo "AI Search Service Name (from outputs): $aiSearchName" - azSubscriptionId=$(azd env get-value AZURE_SUBSCRIPTION_ID) + aif_resource_id=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.aI_FOUNDRY_RESOURCE_ID.value" -o tsv) + echo "AI Foundry Resource ID (from outputs): $aif_resource_id" + + azSubscriptionId=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.azurE_SUBSCRIPTION_ID.value" -o tsv) + + echo "Azure Subscription ID (from outputs): $azSubscriptionId" +else + echo "Deployment does NOT exist in resource group $resourceGroupName." + echo "Please enter required values manually." + + read -rp "Enter Cosmos DB Account Name: " cosmosDbAccountName + read -rp "Enter Storage Account Name: " storageAccount + read -rp "Enter Storage Container/File System Name: " fileSystem + read -rp "Enter SQL Server Name: " sqlServerName + read -rp "Enter SQL Database Name: " SqlDatabaseName + read -rp "Enter Key Vault Name: " keyvaultName + read -rp "Enter Web App Managed Identity Display Name: " webAppManagedIdentityDisplayName + read -rp "Enter Web App Managed Identity Client ID: " webAppManagedIdentityClientId + read -rp "Enter AI Search Service Name: " aiSearchName + read -rp "Enter AI Foundry Resource ID: " aif_resource_id + read -rp "Enter Azure Subscription ID: " azSubscriptionId +fi # Check if all required arguments are provided if [ -z "$resourceGroupName" ] || [ -z "$cosmosDbAccountName" ] || [ -z "$storageAccount" ] || [ -z "$fileSystem" ] || [ -z "$keyvaultName" ] || [ -z "$sqlServerName" ] || [ -z "$SqlDatabaseName" ] || [ -z "$webAppManagedIdentityClientId" ] || [ -z "$webAppManagedIdentityDisplayName" ] || [ -z "$aiSearchName" ] || [ -z "$aif_resource_id" ]; then From 0f69898fdc8a17f3c9e172de92c2df7f48386813 Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Thu, 9 Oct 2025 14:17:15 +0530 Subject: [PATCH 2/6] Updated the network module --- infra/main.bicep | 125 ++++++- infra/modules/network.bicep | 251 ------------- infra/modules/network/bastionHost.bicep | 104 ------ infra/modules/network/jumpbox.bicep | 155 -------- infra/modules/network/network-resources.bicep | 104 ------ infra/modules/network/virtualNetwork.bicep | 157 -------- infra/modules/virtualNetwork.bicep | 346 ++++++++++++++++++ 7 files changed, 452 insertions(+), 790 deletions(-) delete mode 100644 infra/modules/network.bicep delete mode 100644 infra/modules/network/bastionHost.bicep delete mode 100644 infra/modules/network/jumpbox.bicep delete mode 100644 infra/modules/network/network-resources.bicep delete mode 100644 infra/modules/network/virtualNetwork.bicep create mode 100644 infra/modules/virtualNetwork.bicep diff --git a/infra/main.bicep b/infra/main.bicep index aab96329..60eefd36 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -387,22 +387,109 @@ module sqlUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned } } -// ========== Network Module ========== // -module network 'modules/network.bicep' = if (enablePrivateNetworking) { - name: take('network-${solutionSuffix}-deployment', 64) +// ========== Virtual Network and Networking Components ========== // + +// Virtual Network with NSGs and Subnets +module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { + name: take('module.virtualNetwork.${solutionSuffix}', 64) params: { - resourcesName: solutionSuffix - // logAnalyticsWorkSpaceResourceId: logAnalyticsWorkspace.outputs.resourceId - logAnalyticsWorkSpaceResourceId: logAnalyticsWorkspaceResourceId - vmAdminUsername: vmAdminUsername ?? 'JumpboxAdminUser' - vmAdminPassword: vmAdminPassword ?? 'JumpboxAdminP@ssw0rd1234!' - vmSize: vmSize ?? 'Standard_DS2_v2' // Default VM size +name: 'vnet-${solutionSuffix}' + addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) location: solutionLocation tags: allTags + logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId + resourceSuffix: solutionSuffix + enableTelemetry: enableTelemetry + } +} +// Azure Bastion Host +var bastionHostName = 'bas-${solutionSuffix}' +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: solutionLocation + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + diagnosticSettings: [ + { + name: 'bastionDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceResourceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + } + ] + tags: tags 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: solutionLocation + adminUsername: vmAdminUsername ?? 'JumpboxAdminUser' + adminPassword: vmAdminPassword ?? 'JumpboxAdminP@ssw0rd1234!' + tags: tags + 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: [ + { + name: 'jumpboxDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceResourceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metricCategories: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } + ] + } + ] + enableTelemetry: enableTelemetry + } +} // ========== Private DNS Zones ========== // var privateDnsZones = [ 'privatelink.cognitiveservices.azure.com' @@ -456,8 +543,8 @@ module avmPrivateDnsZones 'br/public:avm/res/network/private-dns-zone:0.7.1' = [ enableTelemetry: enableTelemetry virtualNetworkLinks: [ { - name: take('vnetlink-${network!.outputs.vnetName}-${split(zone, '.')[1]}', 80) - virtualNetworkResourceId: network!.outputs.vnetResourceId + name: take('vnetlink-${virtualNetwork!.outputs.name}-${split(zone, '.')[1]}', 80) + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId } ] } @@ -497,7 +584,7 @@ module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = { ] } service: 'vault' - subnetResourceId: network!.outputs.subnetPrivateEndpointsResourceId + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId } ] : [] @@ -638,7 +725,7 @@ module aiFoundryAiServices 'modules/ai-services.bicep' = if (aiFoundryAIservices { name: 'pep-${aiFoundryAiServicesResourceName}' customNetworkInterfaceName: 'nic-${aiFoundryAiServicesResourceName}' - subnetResourceId: network!.outputs.subnetPrivateEndpointsResourceId + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId privateDnsZoneGroup: { privateDnsZoneGroupConfigs: [ { @@ -745,7 +832,7 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.15.0' = { ] } service: 'Sql' - subnetResourceId: network!.outputs.subnetPrivateEndpointsResourceId + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId } ] : [] @@ -818,7 +905,7 @@ module avmStorageAccount 'br/public:avm/res/storage/storage-account:0.20.0' = { } ] } - subnetResourceId: network!.outputs.subnetPrivateEndpointsResourceId + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId service: 'blob' } { @@ -831,7 +918,7 @@ module avmStorageAccount 'br/public:avm/res/storage/storage-account:0.20.0' = { } ] } - subnetResourceId: network!.outputs.subnetPrivateEndpointsResourceId + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId service: 'queue' } ] @@ -940,7 +1027,7 @@ module sqlDBModule 'br/public:avm/res/sql/server:0.20.1' = { ] } service: 'sqlServer' - subnetResourceId: network!.outputs.subnetPrivateEndpointsResourceId + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId tags: tags } ] @@ -1063,7 +1150,7 @@ module webSite 'modules/web-sites.bicep' = { // WAF aligned configuration for Private Networking vnetRouteAllEnabled: enablePrivateNetworking ? true : false vnetImagePullEnabled: enablePrivateNetworking ? true : false - virtualNetworkSubnetId: enablePrivateNetworking ? network!.outputs.subnetWebResourceId : null + virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null publicNetworkAccess: 'Enabled' } } @@ -1145,7 +1232,7 @@ module searchService 'br/public:avm/res/search/search-service:0.11.1' = { ] } service: 'searchService' - subnetResourceId: network!.outputs.subnetPrivateEndpointsResourceId + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId } ] : [] diff --git a/infra/modules/network.bicep b/infra/modules/network.bicep deleted file mode 100644 index ace0139f..00000000 --- a/infra/modules/network.bicep +++ /dev/null @@ -1,251 +0,0 @@ -@description('Required. Named used for all resource naming.') -param resourcesName string - -@description('Required. Resource ID of the Log Analytics Workspace for monitoring and diagnostics.') -param logAnalyticsWorkSpaceResourceId string - -@minLength(3) -@description('Required. Azure region for all services.') -param location string - -@description('Optional. Tags to be applied to the resources.') -param tags object = {} - -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -@description('Required. Admin username for the VM.') -@secure() -param vmAdminUsername string - -@description('Required. Admin password for the VM.') -@secure() -param vmAdminPassword string - -@description('Required. VM size for the Jumpbox VM.') -param vmSize 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. - -module network 'network/network-resources.bicep' = { - name: take('network-${resourcesName}-create', 64) - params: { - resourcesName: resourcesName - location: location - logAnalyticsWorkSpaceResourceId: logAnalyticsWorkSpaceResourceId - tags: tags - addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) - subnets: [ - // Only one delegation per subnet is supported by the AVM module as of June 2025. - // For subnets that do not require delegation, leave the value empty. - { - name: 'web' - addressPrefixes: ['10.0.0.0/23'] // /23 (10.0.0.0 - 10.0.1.255), 512 addresses - networkSecurityGroup: { - name: 'nsg-web' - securityRules: [ - { - name: 'AllowHttpsInbound' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 - protocol: 'Tcp' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefixes: ['0.0.0.0/0'] - destinationAddressPrefixes: ['10.0.0.0/23'] - } - } - { - name: 'AllowIntraSubnetTraffic' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 200 - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefixes: ['10.0.0.0/23'] // From same subnet - destinationAddressPrefixes: ['10.0.0.0/23'] // To same subnet - } - } - { - name: 'AllowAzureLoadBalancer' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 300 - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: 'AzureLoadBalancer' - destinationAddressPrefix: '10.0.0.0/23' - } - } - ] - } - delegation: 'Microsoft.Web/serverFarms' - } - { - name: 'peps' - addressPrefixes: ['10.0.2.0/23'] // /23 (10.0.2.0 - 10.0.3.255), 512 addresses - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Disabled' - networkSecurityGroup: { - name: 'nsg-peps' - securityRules: [] - } - } - ] - bastionConfiguration: { - name: 'bas-${resourcesName}' - subnet: { - name: 'AzureBastionSubnet' - addressPrefixes: ['10.0.10.0/26'] - networkSecurityGroup: { - name: 'nsg-AzureBastionSubnet' - 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' - } - } - ] - } - } - } - jumpboxConfiguration: { - name: 'vm-jumpbox-${resourcesName}' - size: vmSize - username: vmAdminUsername - password: vmAdminPassword - subnet: { - name: 'jumpbox' - addressPrefixes: ['10.0.12.0/23'] // /23 (10.0.12.0 - 10.0.13.255), 512 addresses - networkSecurityGroup: { - name: 'nsg-jumbox' - 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'] - } - } - ] - } - } - } - enableTelemetry: enableTelemetry - } -} - -@description('Name of the Virtual Network resource.') -output vnetName string = network.outputs.vnetName - -@description('Resource ID of the Virtual Network.') -output vnetResourceId string = network.outputs.vnetResourceId - -@description('Resource ID of the "web" subnet.') -output subnetWebResourceId string = first(filter(network.outputs.subnets, s => s.name == 'web')).?resourceId ?? '' - -@description('Resource ID of the "peps" subnet for Private Endpoints.') -output subnetPrivateEndpointsResourceId string = first(filter(network.outputs.subnets, s => s.name == 'peps')).?resourceId ?? '' - -@description('Resource ID of the Bastion Host.') -output bastionResourceId string = network.outputs.bastionHostId - -@description('Resource ID of the Jumpbox VM.') -output jumpboxResourceId string = network.outputs.jumpboxResourceId diff --git a/infra/modules/network/bastionHost.bicep b/infra/modules/network/bastionHost.bicep deleted file mode 100644 index cc1987e5..00000000 --- a/infra/modules/network/bastionHost.bicep +++ /dev/null @@ -1,104 +0,0 @@ -// /****************************************************************************************************************************/ -// Create Azure Bastion Subnet and Azure Bastion Host -// /****************************************************************************************************************************/ - -@description('Name of the Azure Bastion Host resource.') -param name string - -@description('Azure region to deploy resources.') -param location string = resourceGroup().location - -@description('Resource ID of the Virtual Network where the Azure Bastion Host will be deployed.') -param vnetId string - -@description('Name of the Virtual Network where the Azure Bastion Host will be deployed.') -param vnetName string - -@description('Resource ID of the Log Analytics Workspace for monitoring and diagnostics.') -param logAnalyticsWorkspaceId string - -@description('Optional. Tags to apply to the resources.') -param tags object = {} - -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -import { subnetType } from 'virtualNetwork.bicep' -@description('Optional. Subnet configuration for the Jumpbox VM.') -param subnet subnetType? - -// 1. Create AzureBastionSubnet NSG -// using AVM Network Security Group module -// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/network-security-group -module nsg 'br/public:avm/res/network/network-security-group:0.5.1' = if (!empty(subnet)) { - name: '${vnetName}-${subnet.?networkSecurityGroup.name}' - params: { - name: '${subnet.?networkSecurityGroup.name}-${vnetName}' - location: location - securityRules: subnet.?networkSecurityGroup.securityRules - tags: tags - enableTelemetry: enableTelemetry - } -} - -// 2. Create Azure Bastion Host using AVM Subnet Module with special config for Azure Bastion Subnet -// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/virtual-network/subnet -module bastionSubnet 'br/public:avm/res/network/virtual-network/subnet:0.1.2' = if (!empty(subnet)) { - name: take('bastionSubnet-${vnetName}', 64) - params: { - virtualNetworkName: vnetName - name: 'AzureBastionSubnet' // this name required as is for Azure Bastion Host subnet - addressPrefixes: subnet.?addressPrefixes - networkSecurityGroupResourceId: nsg.outputs.resourceId - enableTelemetry: enableTelemetry - } -} - -// 3. Create Azure Bastion Host in AzureBastionsubnetSubnet using AVM Bastion Host module -// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/bastion-host - -module bastionHost 'br/public:avm/res/network/bastion-host:0.6.1' = { - name: take('bastionHost-${vnetName}-${name}', 64) - params: { - name: name - skuName: 'Standard' - location: location - virtualNetworkResourceId: vnetId - diagnosticSettings: [ - { - name: 'bastionDiagnostics' - workspaceResourceId: logAnalyticsWorkspaceId - logCategoriesAndGroups: [ - { - categoryGroup: 'allLogs' - enabled: true - } - ] - } - ] - tags: tags - enableTelemetry: enableTelemetry - publicIPAddressObject: { - name: 'pip-${name}' - zones: [] - } - } - dependsOn: [ - bastionSubnet - ] -} - -output resourceId string = bastionHost.outputs.resourceId -output name string = bastionHost.outputs.name -output subnetId string = bastionSubnet.outputs.resourceId -output subnetName string = bastionSubnet.outputs.name - -@export() -@description('Custom type definition for establishing Bastion Host for remote connection.') -type bastionHostConfigurationType = { - @description('The name of the Bastion Host resource.') - name: string - - @description('Optional. Subnet configuration for the Jumpbox VM.') - subnet: subnetType? -} diff --git a/infra/modules/network/jumpbox.bicep b/infra/modules/network/jumpbox.bicep deleted file mode 100644 index 29f7d3e2..00000000 --- a/infra/modules/network/jumpbox.bicep +++ /dev/null @@ -1,155 +0,0 @@ -// /****************************************************************************************************************************/ -// Create Jumpbox NSG and Jumpbox Subnet, then create Jumpbox VM -// /****************************************************************************************************************************/ - -@description('Name of the Jumpbox Virtual Machine.') -param name string - -@description('Azure region to deploy resources.') -param location string = resourceGroup().location - -@description('Name of the Virtual Network where the Jumpbox VM will be deployed.') -param vnetName string - -@description('Size of the Jumpbox Virtual Machine.') -param size string - -import { subnetType } from 'virtualNetwork.bicep' -@description('Optional. Subnet configuration for the Jumpbox VM.') -param subnet subnetType? - -@description('Username to access the Jumpbox VM.') -param username string - -@secure() -@description('Password to access the Jumpbox VM.') -param password string - -@description('Optional. Tags to apply to the resources.') -param tags object = {} - -@description('Log Analytics Workspace Resource ID for VM diagnostics.') -param logAnalyticsWorkspaceId string - -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -// 1. Create Jumpbox NSG -// using AVM Network Security Group module -// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/network-security-group -module nsg 'br/public:avm/res/network/network-security-group:0.5.1' = if (!empty(subnet)) { - name: '${vnetName}-${subnet.?networkSecurityGroup.name}' - params: { - name: '${subnet.?networkSecurityGroup.name}-${vnetName}' - location: location - securityRules: subnet.?networkSecurityGroup.securityRules - tags: tags - enableTelemetry: enableTelemetry - } -} - -// 2. Create Jumpbox subnet as part of the existing VNet -// using AVM Virtual Network Subnet module -// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/virtual-network/subnet -module subnetResource 'br/public:avm/res/network/virtual-network/subnet:0.1.2' = if (!empty(subnet)) { - name: subnet.?name ?? '${vnetName}-jumpbox-subnet' - params: { - virtualNetworkName: vnetName - name: subnet.?name ?? '' - addressPrefixes: subnet.?addressPrefixes - networkSecurityGroupResourceId: nsg.outputs.resourceId - enableTelemetry: enableTelemetry - } -} - -// 3. Create Jumpbox VM -// using AVM Virtual Machine module -// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/compute/virtual-machine -var vmName = take(name, 15) // Shorten VM name to 15 characters to avoid Azure limits - -module vm 'br/public:avm/res/compute/virtual-machine:0.15.0' = { - name: take('${vmName}-jumpbox', 64) - params: { - name: vmName - vmSize: size - location: location - adminUsername: username - adminPassword: password - tags: tags - zone: 0 - imageReference: { - offer: 'WindowsServer' - publisher: 'MicrosoftWindowsServer' - sku: '2019-datacenter' - version: 'latest' - } - osType: 'Windows' - osDisk: { - name: 'osdisk-${vmName}' - managedDisk: { - storageAccountType: 'Standard_LRS' - } - } - encryptionAtHost: false // Some Azure subscriptions do not support encryption at host - nicConfigurations: [ - { - name: 'nic-${vmName}' - ipConfigurations: [ - { - name: 'ipconfig1' - subnetResourceId: subnetResource.outputs.resourceId - } - ] - networkSecurityGroupResourceId: nsg.outputs.resourceId - diagnosticSettings: [ - { - name: 'jumpboxDiagnostics' - workspaceResourceId: logAnalyticsWorkspaceId - logCategoriesAndGroups: [ - { - categoryGroup: 'allLogs' - enabled: true - } - ] - metricCategories: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } - ] - } - ] - enableTelemetry: enableTelemetry - } -} - -output resourceId string = vm.outputs.resourceId -output name string = vm.outputs.name -output location string = vm.outputs.location - -output subnetId string = subnetResource.outputs.resourceId -output subnetName string = subnetResource.outputs.name -output nsgId string = nsg.outputs.resourceId -output nsgName string = nsg.outputs.name - -@export() -@description('Custom type definition for establishing Jumpbox Virtual Machine and its associated resources.') -type jumpBoxConfigurationType = { - @description('The name of the Virtual Machine.') - name: string - - @description('The size of the VM.') - size: string? - - @description('Username to access VM.') - username: string - - @secure() - @description('Password to access VM.') - password: string - - @description('Optional. Subnet configuration for the Jumpbox VM.') - subnet: subnetType? -} diff --git a/infra/modules/network/network-resources.bicep b/infra/modules/network/network-resources.bicep deleted file mode 100644 index 7e2e8658..00000000 --- a/infra/modules/network/network-resources.bicep +++ /dev/null @@ -1,104 +0,0 @@ -@minLength(6) -@maxLength(25) -@description('Name used for naming all network resources.') -param resourcesName string - -@minLength(3) -@description('Azure region for all services.') -param location string - -@description('Resource ID of the Log Analytics Workspace for monitoring and diagnostics.') -param logAnalyticsWorkSpaceResourceId string - -@description('Networking address prefix for the VNET.') -param addressPrefixes array - -import { subnetType } from 'virtualNetwork.bicep' -@description('Array of subnets to be created within the VNET.') -param subnets subnetType[] - -import { jumpBoxConfigurationType } from 'jumpbox.bicep' -@description('Optional. Configuration for the Jumpbox VM. Leave null to omit Jumpbox creation.') -param jumpboxConfiguration jumpBoxConfigurationType? - -import { bastionHostConfigurationType } from 'bastionHost.bicep' -@description('Optional. Configuration for the Azure Bastion Host. Leave null to omit Bastion creation.') -param bastionConfiguration bastionHostConfigurationType? - -@description('Optional. Tags to be applied to the resources.') -param tags object = {} - -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -// /****************************************************************************************************************************/ -// Networking - NSGs, VNET and Subnets. Each subnet has its own NSG -// /****************************************************************************************************************************/ - -module virtualNetwork 'virtualNetwork.bicep' = { - name: '${resourcesName}-virtualNetwork' - params: { - name: 'vnet-${resourcesName}' - addressPrefixes: addressPrefixes - subnets: subnets - location: location - tags: tags - logAnalyticsWorkspaceId: logAnalyticsWorkSpaceResourceId - enableTelemetry: enableTelemetry - } -} - -// /****************************************************************************************************************************/ -// // Create Azure Bastion Subnet and Azure Bastion Host -// /****************************************************************************************************************************/ - -module bastionHost 'bastionHost.bicep' = if (!empty(bastionConfiguration)) { - name: '${resourcesName}-bastionHost' - params: { - name: bastionConfiguration.?name ?? 'bas-${resourcesName}' - vnetId: virtualNetwork.outputs.resourceId - vnetName: virtualNetwork.outputs.name - location: location - logAnalyticsWorkspaceId: logAnalyticsWorkSpaceResourceId - subnet: bastionConfiguration.?subnet - tags: tags - enableTelemetry: enableTelemetry - } -} - -// /****************************************************************************************************************************/ -// // create Jumpbox NSG and Jumpbox Subnet, then create Jumpbox VM -// /****************************************************************************************************************************/ - -module jumpbox 'jumpbox.bicep' = if (!empty(jumpboxConfiguration)) { - name: '${resourcesName}-jumpbox' - params: { - name: jumpboxConfiguration.?name ?? 'vm-jumpbox-${resourcesName}' - vnetName: virtualNetwork.outputs.name - size: jumpboxConfiguration.?size ?? 'Standard_D2s_v3' - logAnalyticsWorkspaceId: logAnalyticsWorkSpaceResourceId - location: location - subnet: jumpboxConfiguration.?subnet - username: jumpboxConfiguration.?username ?? '' // required - password: jumpboxConfiguration.?password ?? '' // required - enableTelemetry: enableTelemetry - tags: tags - } -} - -output vnetName string = virtualNetwork.outputs.name -output vnetResourceId string = virtualNetwork.outputs.resourceId - -import { subnetOutputType } from 'virtualNetwork.bicep' -output subnets subnetOutputType[] = virtualNetwork.outputs.subnets // This one holds critical info for subnets, including NSGs - -output bastionSubnetId string = bastionHost.outputs.subnetId -output bastionSubnetName string = bastionHost.outputs.subnetName -output bastionHostId string = bastionHost.outputs.resourceId -output bastionHostName string = bastionHost.outputs.name - -output jumpboxSubnetName string = jumpbox.outputs.subnetName -output jumpboxSubnetId string = jumpbox.outputs.subnetId -output jumpboxName string = jumpbox.outputs.name -output jumpboxResourceId string = jumpbox.outputs.resourceId - diff --git a/infra/modules/network/virtualNetwork.bicep b/infra/modules/network/virtualNetwork.bicep deleted file mode 100644 index 6b502974..00000000 --- a/infra/modules/network/virtualNetwork.bicep +++ /dev/null @@ -1,157 +0,0 @@ -/****************************************************************************************************************************/ -// 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 OR the resource ID of the IPAM pool to be used for the Virtual Network. When specifying an IPAM pool resource ID you must also set a value for the parameter called `ipamPoolNumberOfIpAddresses`.') -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[] - -@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 - -// 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('${name}-${subnet.?networkSecurityGroup.name}-networksecuritygroup', 64) - params: { - name: '${subnet.?networkSecurityGroup.name}-${name}' - 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('${name}-virtualNetwork', 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 - } -] - -@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[] -} diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep new file mode 100644 index 00000000..996b4a9c --- /dev/null +++ b/infra/modules/virtualNetwork.bicep @@ -0,0 +1,346 @@ +/****************************************************************************************************************************/ +// 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: 'web' + addressPrefixes: ['10.0.0.0/23'] // /23 (10.0.0.0 - 10.0.1.255), 512 addresses + delegation: 'Microsoft.Web/serverFarms' + networkSecurityGroup: { + name: 'nsg-web' + securityRules: [ + { + name: 'AllowHttpsInbound' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 100 + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefixes: ['0.0.0.0/0'] + destinationAddressPrefixes: ['10.0.0.0/23'] + } + } + { + name: 'AllowIntraSubnetTraffic' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 200 + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefixes: ['10.0.0.0/23'] + destinationAddressPrefixes: ['10.0.0.0/23'] + } + } + { + name: 'AllowAzureLoadBalancer' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 300 + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: 'AzureLoadBalancer' + destinationAddressPrefix: '10.0.0.0/23' + } + } + ] + } + } + { + name: 'peps' + addressPrefixes: ['10.0.2.0/23'] // /23 (10.0.2.0 - 10.0.3.255), 512 addresses + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + networkSecurityGroup: { + name: 'nsg-peps' + securityRules: [] + } + } + { + 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 webSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'web') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'web')] : '' +output pepsSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'peps') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'peps')] : '' +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[] +} From 536d6c612171f7640bff186214282a2e701a097c Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Thu, 9 Oct 2025 20:21:04 +0530 Subject: [PATCH 3/6] script fix --- azure.yaml | 4 ++-- docs/AVMPostDeploymentGuide.md | 2 +- infra/main.bicep | 2 ++ infra/scripts/process_sample_data.sh | 18 ++++++++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/azure.yaml b/azure.yaml index f8a7f360..c9bcc1d3 100644 --- a/azure.yaml +++ b/azure.yaml @@ -16,7 +16,7 @@ hooks: Write-Host "Web app URL: " Write-Host "$env:WEB_APP_URL" -ForegroundColor Cyan Write-Host "`nRun the following command in your Bash terminal. It will grant the necessary permissions between resources and your user account, and also process and load the sample data into the application." - Write-Host "bash ./infra/scripts/process_sample_data.sh" -ForegroundColor Cyan + Write-Host "bash ./infra/scripts/process_sample_data.sh $env:AZURE_RESOURCE_GROUP_NAME" -ForegroundColor Cyan shell: pwsh continueOnError: false interactive: true @@ -26,7 +26,7 @@ hooks: echo $WEB_APP_URL echo "" echo "Run the following command in your Bash terminal. It will grant the necessary permissions between resources and your user account, and also process and load the sample data into the application." - echo "bash ./infra/scripts/process_sample_data.sh" + echo "bash ./infra/scripts/process_sample_data.sh $env:AZURE_RESOURCE_GROUP_NAME" shell: sh continueOnError: false interactive: true diff --git a/docs/AVMPostDeploymentGuide.md b/docs/AVMPostDeploymentGuide.md index 0ba7a63b..3fb0dc79 100644 --- a/docs/AVMPostDeploymentGuide.md +++ b/docs/AVMPostDeploymentGuide.md @@ -14,7 +14,7 @@ This document provides guidance on post-deployment steps after deploying the Bui 2. Import Sample Data -Run bash command printed in the terminal. The bash command will look like the following: ```bash - ./infra/scripts/process_sample_data.sh + bash ./infra/scripts/process_sample_data.sh ``` If the deployment does not exist or has been deleted – The script will prompt you to manually enter the required values diff --git a/infra/main.bicep b/infra/main.bicep index aab96329..061f55d8 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1382,3 +1382,5 @@ output USE_AI_PROJECT_CLIENT string = useAIProjectClientFlag @description('Indicates whether the internal stream should be used.') output USE_INTERNAL_STREAM string = useInternalStream +@description('The Azure Subscription ID where the resources are deployed.') +output AZURE_SUBSCRIPTION_ID string = subscription().subscriptionId diff --git a/infra/scripts/process_sample_data.sh b/infra/scripts/process_sample_data.sh index 546da9e7..7711e4ec 100644 --- a/infra/scripts/process_sample_data.sh +++ b/infra/scripts/process_sample_data.sh @@ -330,6 +330,24 @@ if az deployment group show --resource-group "$resourceGroupName" --name "$deplo --query "properties.outputs.managedidentitY_WEBAPP_NAME.value" -o tsv) echo "Web App Managed Identity Display Name (from outputs): $webAppManagedIdentityDisplayName" + SqlDatabaseName=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.sqldB_DATABASE.value" -o tsv) + echo "SQL Database Name (from outputs): $SqlDatabaseName" + + sqlManagedIdentityClientId=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.managedidentitY_SQL_CLIENTID.value" -o tsv) + echo "SQL Managed Identity Client ID (from outputs): $sqlManagedIdentityClientId" + + sqlManagedIdentityDisplayName=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.managedidentitY_SQL_NAME.value" -o tsv) + echo "SQL Managed Identity Display Name (from outputs): $sqlManagedIdentityDisplayName" + aiSearchName=$(az deployment group show \ --name "$deploymentName" \ --resource-group "$resourceGroupName" \ From 568181aefa58956c1342442aa04fd3240eb70507 Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Thu, 9 Oct 2025 21:27:22 +0530 Subject: [PATCH 4/6] update azure.yml --- azure.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure.yaml b/azure.yaml index c9bcc1d3..4c3ca3b0 100644 --- a/azure.yaml +++ b/azure.yaml @@ -16,7 +16,7 @@ hooks: Write-Host "Web app URL: " Write-Host "$env:WEB_APP_URL" -ForegroundColor Cyan Write-Host "`nRun the following command in your Bash terminal. It will grant the necessary permissions between resources and your user account, and also process and load the sample data into the application." - Write-Host "bash ./infra/scripts/process_sample_data.sh $env:AZURE_RESOURCE_GROUP_NAME" -ForegroundColor Cyan + Write-Host "bash ./infra/scripts/process_sample_data.sh $env:AZURE_RESOURCE_GROUP" -ForegroundColor Cyan shell: pwsh continueOnError: false interactive: true @@ -26,7 +26,7 @@ hooks: echo $WEB_APP_URL echo "" echo "Run the following command in your Bash terminal. It will grant the necessary permissions between resources and your user account, and also process and load the sample data into the application." - echo "bash ./infra/scripts/process_sample_data.sh $env:AZURE_RESOURCE_GROUP_NAME" + echo "bash ./infra/scripts/process_sample_data.sh $env:AZURE_RESOURCE_GROUP" shell: sh continueOnError: false interactive: true From 090534e1bbb589439c4580823c7c4137a36027a6 Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Thu, 9 Oct 2025 21:53:48 +0530 Subject: [PATCH 5/6] fix script 1 --- infra/scripts/process_sample_data.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infra/scripts/process_sample_data.sh b/infra/scripts/process_sample_data.sh index 7711e4ec..4f055ea7 100644 --- a/infra/scripts/process_sample_data.sh +++ b/infra/scripts/process_sample_data.sh @@ -330,6 +330,12 @@ if az deployment group show --resource-group "$resourceGroupName" --name "$deplo --query "properties.outputs.managedidentitY_WEBAPP_NAME.value" -o tsv) echo "Web App Managed Identity Display Name (from outputs): $webAppManagedIdentityDisplayName" + webAppManagedIdentityClientId=$(az deployment group show \ + --name "$deploymentName" \ + --resource-group "$resourceGroupName" \ + --query "properties.outputs.managedidentitY_WEBAPP_CLIENTID.value" -o tsv) + echo "Web App Managed Identity Client ID (from outputs): $webAppManagedIdentityClientId" + SqlDatabaseName=$(az deployment group show \ --name "$deploymentName" \ --resource-group "$resourceGroupName" \ @@ -378,6 +384,8 @@ else read -rp "Enter Key Vault Name: " keyvaultName read -rp "Enter Web App Managed Identity Display Name: " webAppManagedIdentityDisplayName read -rp "Enter Web App Managed Identity Client ID: " webAppManagedIdentityClientId + read -rp "Enter SQL Managed Identity Display Name: " sqlManagedIdentityDisplayName + read -rp "Enter SQL Managed Identity Client ID: " sqlManagedIdentityClientId read -rp "Enter AI Search Service Name: " aiSearchName read -rp "Enter AI Foundry Resource ID: " aif_resource_id read -rp "Enter Azure Subscription ID: " azSubscriptionId From 6678b91ab7a2727cec71a3e24b4c34af308dbab6 Mon Sep 17 00:00:00 2001 From: NirajC-Microsoft Date: Fri, 10 Oct 2025 10:44:23 +0530 Subject: [PATCH 6/6] Quotacheck fix (#701) --- infra/scripts/checkquota.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index 136d1902..146dc6bd 100755 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -56,6 +56,7 @@ for REGION in "${REGIONS[@]}"; do if [ -z "$MODEL_INFO" ]; then echo "⚠️ WARNING: No quota information found for model: $MODEL in $REGION. Skipping." + INSUFFICIENT_QUOTA=true continue fi