diff --git a/docs/deploy/finops-hub-latest.ui.json b/docs/deploy/finops-hub-latest.ui.json index da1f029b8..3dc7a4834 100644 --- a/docs/deploy/finops-hub-latest.ui.json +++ b/docs/deploy/finops-hub-latest.ui.json @@ -890,33 +890,52 @@ "label": "Networking", "elements": [ { - "name": "infraEncryptionIntro", + "name": "networkingIntro", "type": "Microsoft.Common.TextBlock", "visible": true, "options": { - "text": "For enhanced security, consider blocking external access to your FinOps hub instance to enable secure, private connectivity to your cloud resources by eliminating exposure to the public internet. Private endpoints can incur significant additional cost. Please understand the cost impact before selecting this option." + "text": "Select the network isolation mode. Private vNet uses private endpoints to block all public access and incurs significant additional cost (private endpoint hours, data processing, plus a NAT Gateway with a public IP for outbound dependencies). Please understand the cost impact before selecting Private vNet." } }, { - "name": "enablePublicAccess", - "type": "Microsoft.Common.DropDown", - "label": "Access", - "toolTip": "Indicate whether FinOps hubs resources should be accessible by the internet or not. If set to private, all resources will be hidden behind a virtual network private endpoint. This will incur additional costs for data crossing virtual network boundaries and for virtual network peering, if configured.", - "defaultValue": "Public", - "multiLine": true, + "name": "networkMode", + "type": "Microsoft.Common.OptionsGroup", + "label": "Network access mode", + "defaultValue": "Public (no vNet)", + "toolTip": "Choose how FinOps hubs resources are exposed on the network. Public: no virtual network, all resources reachable from the internet. vNet: deploy resources inside a virtual network with a NAT Gateway for outbound traffic, but keep public endpoints (segmentation without private-endpoint cost). Private vNet: all resources are hidden behind private endpoints; public access is blocked. This will incur additional costs for private endpoints, data processing across virtual network boundaries, and for virtual network peering if configured.", "constraints": { "allowedValues": [ { - "label": "Public", - "description": "Allow external access to storage and Data Explorer.", - "value": true + "label": "Public (no vNet)", + "value": "public" + }, + { + "label": "vNet (public endpoints)", + "value": "vnet" }, { - "label": "Private", - "description": "Block all external access. This will incur additional charges.", - "value": false + "label": "Private vNet (private endpoints only)", + "value": "private" } - ] + ], + "required": true + }, + "visible": true + }, + { + "name": "vnetModeNote", + "type": "Microsoft.Common.TextBlock", + "visible": "[equals(steps('advanced').networking.networkMode, 'vnet')]", + "options": { + "text": "A virtual network + NSG + 3 subnets are deployed as a scaffold for future expansion, but public endpoints remain. No NAT Gateway and no private endpoints are created, so this mode adds no Azure cost beyond the free VNet primitives. Useful if you want a VNet ready to flip to Private vNet later without recreating resources." + } + }, + { + "name": "privateModeNote", + "type": "Microsoft.Common.TextBlock", + "visible": "[equals(steps('advanced').networking.networkMode, 'private')]", + "options": { + "text": "Private endpoints and private DNS zones will be created for storage, Key Vault, and Data Explorer. A NAT Gateway with a static public IP is also deployed so deployment scripts and Data Explorer can reach Microsoft Container Registry and the FinOps toolkit open data on GitHub. This incurs charges for private endpoint hours, data processing, NAT Gateway hours, and the public IP. Switching back to vNet or Public later does NOT delete these resources — clean them up manually if you downgrade." } }, { @@ -933,7 +952,7 @@ } ] }, - "visible": "[equals(steps('advanced').networking.enablePublicAccess, false)]" + "visible": "[not(equals(steps('advanced').networking.networkMode, 'public'))]" } ], "visible": true @@ -954,9 +973,13 @@ "Microsoft.KeyVault/vaults", "Microsoft.Kusto/clusters", "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.Network/natGateways", + "Microsoft.Network/networkSecurityGroups", "Microsoft.Network/privateDnsZones", "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "Microsoft.Network/privateEndpoints", + "Microsoft.Network/publicIPAddresses", + "Microsoft.Network/virtualNetworks", "Microsoft.Resources/deploymentScripts", "Microsoft.Storage/storageAccounts" ] @@ -976,7 +999,8 @@ "enableRecommendations": "[steps('recommendations').enableRecommendations]", "enableAHBRecommendations": "[steps('recommendations').optional.enableAHBRecommendations]", "enableSpotRecommendations": "[steps('recommendations').optional.enableSpotRecommendations]", - "enablePublicAccess": "[steps('advanced').networking.enablePublicAccess]", + "enablePublicAccess": "[not(equals(steps('advanced').networking.networkMode, 'private'))]", + "networkMode": "[steps('advanced').networking.networkMode]", "virtualNetworkAddressPrefix": "[steps('advanced').networking.virtualNetworkAddressPrefix]", "dataExplorerSku": "[steps('pricing').dataExplorer.dataExplorerSku]", "exportRetentionInDays": "[steps('retention').storage.msexportsDays]", diff --git a/src/templates/finops-hub/createUiDefinition.json b/src/templates/finops-hub/createUiDefinition.json index 726974856..533f15fb0 100644 --- a/src/templates/finops-hub/createUiDefinition.json +++ b/src/templates/finops-hub/createUiDefinition.json @@ -906,33 +906,52 @@ "label": "Networking", "elements": [ { - "name": "infraEncryptionIntro", + "name": "networkingIntro", "type": "Microsoft.Common.TextBlock", "visible": true, "options": { - "text": "For enhanced security, consider blocking external access to your FinOps hub instance to enable secure, private connectivity to your cloud resources by eliminating exposure to the public internet. Private endpoints can incur significant additional cost. Please understand the cost impact before selecting this option." + "text": "Select the network isolation mode. Private vNet uses private endpoints to block all public access and incurs significant additional cost (private endpoint hours, data processing, plus a NAT Gateway with a public IP for outbound dependencies). Please understand the cost impact before selecting Private vNet." } }, { - "name": "enablePublicAccess", - "type": "Microsoft.Common.DropDown", - "label": "Access", - "toolTip": "Indicate whether FinOps hubs resources should be accessible by the internet or not. If set to private, all resources will be hidden behind a virtual network private endpoint. This will incur additional costs for data crossing virtual network boundaries and for virtual network peering, if configured.", - "defaultValue": "Public", - "multiLine": true, + "name": "networkMode", + "type": "Microsoft.Common.OptionsGroup", + "label": "Network access mode", + "defaultValue": "Public (no vNet)", + "toolTip": "Choose how FinOps hubs resources are exposed on the network. Public: no virtual network, all resources reachable from the internet. vNet: deploy resources inside a virtual network with a NAT Gateway for outbound traffic, but keep public endpoints (segmentation without private-endpoint cost). Private vNet: all resources are hidden behind private endpoints; public access is blocked. This will incur additional costs for private endpoints, data processing across virtual network boundaries, and for virtual network peering if configured.", "constraints": { "allowedValues": [ { - "label": "Public", - "description": "Allow external access to storage and Data Explorer.", - "value": true + "label": "Public (no vNet)", + "value": "public" + }, + { + "label": "vNet (public endpoints)", + "value": "vnet" }, { - "label": "Private", - "description": "Block all external access. This will incur additional charges.", - "value": false + "label": "Private vNet (private endpoints only)", + "value": "private" } - ] + ], + "required": true + }, + "visible": true + }, + { + "name": "vnetModeNote", + "type": "Microsoft.Common.TextBlock", + "visible": "[equals(steps('advanced').networking.networkMode, 'vnet')]", + "options": { + "text": "A virtual network + NSG + 3 subnets are deployed as a scaffold for future expansion, but public endpoints remain. No NAT Gateway and no private endpoints are created, so this mode adds no Azure cost beyond the free VNet primitives. Useful if you want a VNet ready to flip to Private vNet later without recreating resources." + } + }, + { + "name": "privateModeNote", + "type": "Microsoft.Common.TextBlock", + "visible": "[equals(steps('advanced').networking.networkMode, 'private')]", + "options": { + "text": "Private endpoints and private DNS zones will be created for storage, Key Vault, and Data Explorer. A NAT Gateway with a static public IP is also deployed so deployment scripts and Data Explorer can reach Microsoft Container Registry and the FinOps toolkit open data on GitHub. This incurs charges for private endpoint hours, data processing, NAT Gateway hours, and the public IP. Switching back to vNet or Public later does NOT delete these resources — clean them up manually if you downgrade." } }, { @@ -949,7 +968,7 @@ } ] }, - "visible": "[equals(steps('advanced').networking.enablePublicAccess, false)]" + "visible": "[not(equals(steps('advanced').networking.networkMode, 'public'))]" } ], "visible": true @@ -970,9 +989,13 @@ "Microsoft.KeyVault/vaults", "Microsoft.Kusto/clusters", "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.Network/natGateways", + "Microsoft.Network/networkSecurityGroups", "Microsoft.Network/privateDnsZones", "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "Microsoft.Network/privateEndpoints", + "Microsoft.Network/publicIPAddresses", + "Microsoft.Network/virtualNetworks", "Microsoft.Resources/deploymentScripts", "Microsoft.Storage/storageAccounts" ] @@ -992,7 +1015,8 @@ "enableRecommendations": "[steps('recommendations').enableRecommendations]", "enableAHBRecommendations": "[steps('recommendations').optional.enableAHBRecommendations]", "enableSpotRecommendations": "[steps('recommendations').optional.enableSpotRecommendations]", - "enablePublicAccess": "[steps('advanced').networking.enablePublicAccess]", + "enablePublicAccess": "[not(equals(steps('advanced').networking.networkMode, 'private'))]", + "networkMode": "[steps('advanced').networking.networkMode]", "virtualNetworkAddressPrefix": "[steps('advanced').networking.virtualNetworkAddressPrefix]", "dataExplorerSku": "[steps('pricing').dataExplorer.dataExplorerSku]", "exportRetentionInDays": "[steps('retention').storage.msexportsDays]", diff --git a/src/templates/finops-hub/main.bicep b/src/templates/finops-hub/main.bicep index 4e058fcba..0320d5fcd 100644 --- a/src/templates/finops-hub/main.bicep +++ b/src/templates/finops-hub/main.bicep @@ -156,13 +156,31 @@ param dataExplorerRawRetentionInDays int = 0 @description('Optional. Number of months of data to retain in the Data Explorer *_final_v* tables. Default: 13.') param dataExplorerFinalRetentionInMonths int = 13 -@description('Optional. Enable public access to FinOps hubs resources. Default: true.') +@description('Optional. DEPRECATED — use networkMode instead. Enable public access to FinOps hubs resources. Default: true. Honored only when networkMode is omitted: false maps to "private", true maps to "public". When networkMode is supplied this parameter is ignored.') param enablePublicAccess bool = true +@description('Optional. Network access mode. "public" (default): no virtual network, all resources publicly reachable. "vnet": virtual network + NSG scaffold deployed for future use, but public endpoints remain; no NAT Gateway, no private endpoints, no extra cost beyond the (free) VNet/NSG. "private": full virtual network with private endpoints, NAT Gateway + static public IP for outbound egress (deployment scripts to MCR and ADX open-data to GitHub), public access blocked. When not specified, derived from enablePublicAccess. NOTE: switching modes does not delete resources from previous modes (incremental deployment); to clean up, manually remove orphaned VNet/NAT/PIP/PE/DNS resources after switching to a lower mode.') +@allowed([ + 'public' + 'vnet' + 'private' +]) +param networkMode string? + @description('Optional. Address space for the workload. Minimum /26 subnet size is required for the workload. Default: "10.20.30.0/26".') param virtualNetworkAddressPrefix string = '10.20.30.0/26' +//============================================================================== +// Variables +//============================================================================== + +// Resolve networkMode: explicit value wins; otherwise derive from legacy enablePublicAccess. +// !empty() guards against both null AND empty-string inputs (the latter could theoretically arrive +// via templates that pre-existed the @allowed change). +var effectiveNetworkMode = !empty(networkMode) ? networkMode! : (enablePublicAccess ? 'public' : 'private') + + //============================================================================== // Resources //============================================================================== @@ -194,7 +212,7 @@ module hub 'modules/hub.bicep' = { dataExplorerFinalRetentionInMonths: dataExplorerFinalRetentionInMonths remoteHubStorageUri: remoteHubStorageUri remoteHubStorageKey: remoteHubStorageKey - enablePublicAccess: enablePublicAccess + networkMode: effectiveNetworkMode virtualNetworkAddressPrefix: virtualNetworkAddressPrefix } } diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep index 0cf23a808..305b9b71e 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep @@ -17,17 +17,20 @@ param hub HubProperties //============================================================================== var nsgName = '${hub.routing.networkName}-nsg' +var natGatewayName = '${hub.routing.networkName}-natgw' +var natGatewayPipName = '${hub.routing.networkName}-natgw-pip' // Workaround https://github.com/Azure/bicep/issues/1853 var finopsHubSubnetName = 'private-endpoint-subnet' var scriptSubnetName = 'script-subnet' var dataExplorerSubnetName = 'dataExplorer-subnet' -var subnets = !hub.options.privateRouting ? [] : [ +var subnets = !hub.options.vnetIntegration ? [] : [ { name: finopsHubSubnetName properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 28, 0) + defaultOutboundAccess: false networkSecurityGroup: { id: nsg.id } @@ -40,8 +43,9 @@ var subnets = !hub.options.privateRouting ? [] : [ } { name: scriptSubnetName - properties: { + properties: union({ addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 28, 1) + defaultOutboundAccess: false networkSecurityGroup: { id: nsg.id } @@ -58,16 +62,25 @@ var subnets = !hub.options.privateRouting ? [] : [ service: 'Microsoft.Storage' } ] - } + }, hub.options.privateRouting ? { + natGateway: { + id: resourceId('Microsoft.Network/natGateways', natGatewayName) + } + } : {}) } { name: dataExplorerSubnetName - properties: { + properties: union({ addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 27, 1) + defaultOutboundAccess: false networkSecurityGroup: { id: nsg.id } - } + }, hub.options.privateRouting ? { + natGateway: { + id: resourceId('Microsoft.Network/natGateways', natGatewayName) + } + } : {}) } ] @@ -80,10 +93,10 @@ var subnets = !hub.options.privateRouting ? [] : [ // Network //------------------------------------------------------------------------------ -resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = if (hub.options.privateRouting) { +resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = if (hub.options.vnetIntegration) { name: nsgName location: hub.location - tags: getHubTags(hub, 'Microsoft.Storage/networkSecurityGroups') + tags: getHubTags(hub, 'Microsoft.Network/networkSecurityGroups') properties: { securityRules: [ { @@ -168,10 +181,13 @@ resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = if (hub.opti } } -resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.privateRouting) { +resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.vnetIntegration) { name: hub.routing.networkName location: hub.location - tags: getHubTags(hub, 'Microsoft.Storage/virtualNetworks') + tags: getHubTags(hub, 'Microsoft.Network/virtualNetworks') + dependsOn: hub.options.privateRouting ? [ + natGateway + ] : [] properties: { addressSpace: { addressPrefixes: [hub.options.networkAddressPrefix] @@ -192,6 +208,40 @@ resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.p } } +//------------------------------------------------------------------------------ +// NAT Gateway (for outbound from script + dataExplorer subnets when defaultOutboundAccess is disabled) +//------------------------------------------------------------------------------ + +resource natGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = if (hub.options.privateRouting) { + name: natGatewayPipName + location: hub.location + tags: getHubTags(hub, 'Microsoft.Network/publicIPAddresses') + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + } +} + +resource natGateway 'Microsoft.Network/natGateways@2023-11-01' = if (hub.options.privateRouting) { + name: natGatewayName + location: hub.location + tags: getHubTags(hub, 'Microsoft.Network/natGateways') + sku: { + name: 'Standard' + } + properties: { + idleTimeoutInMinutes: 4 + publicIpAddresses: [ + { + id: natGatewayPublicIp.id + } + ] + } +} + //------------------------------------------------------------------------------ // Storage DNS zones //------------------------------------------------------------------------------ @@ -203,7 +253,7 @@ resource blobPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = if vNet ] location: 'global' - tags: getHubTags(hub, 'Microsoft.Storage/privateDnsZones') + tags: getHubTags(hub, 'Microsoft.Network/privateDnsZones') properties: {} resource blobPrivateDnsZoneLink 'virtualNetworkLinks' = { @@ -226,7 +276,7 @@ resource dfsPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = if ( vNet ] location: 'global' - tags: getHubTags(hub, 'Microsoft.Storage/privateDnsZones') + tags: getHubTags(hub, 'Microsoft.Network/privateDnsZones') properties: {} resource dfsPrivateDnsZoneLink 'virtualNetworkLinks' = { @@ -249,7 +299,7 @@ resource queuePrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = if vNet ] location: 'global' - tags: getHubTags(hub, 'Microsoft.Storage/privateDnsZones') + tags: getHubTags(hub, 'Microsoft.Network/privateDnsZones') properties: {} resource queuePrivateDnsZoneLink 'virtualNetworkLinks' = { @@ -272,7 +322,7 @@ resource tablePrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = if vNet ] location: 'global' - tags: getHubTags(hub, 'Microsoft.Storage/privateDnsZones') + tags: getHubTags(hub, 'Microsoft.Network/privateDnsZones') properties: {} resource tablePrivateDnsZoneLink 'virtualNetworkLinks' = { @@ -369,21 +419,21 @@ resource scriptEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (hu output config HubProperties = hub @description('Resource ID of the virtual network.') -output vNetId string = !hub.options.privateRouting ? '' : vNet.id +output vNetId string = !hub.options.vnetIntegration ? '' : vNet.id @description('Virtual network address prefixes.') #disable-next-line BCP318 // Null safety warning for conditional resource access -output vNetAddressSpace array = !hub.options.privateRouting ? [] : vNet.properties.addressSpace.addressPrefixes +output vNetAddressSpace array = !hub.options.vnetIntegration ? [] : vNet.properties.addressSpace.addressPrefixes @description('Virtual network subnets.') #disable-next-line BCP318 // Null safety warning for conditional resource access -output vNetSubnets array = !hub.options.privateRouting ? [] : vNet.properties.subnets +output vNetSubnets array = !hub.options.vnetIntegration ? [] : vNet.properties.subnets @description('Resource ID of the FinOps hub network subnet.') -output finopsHubSubnetId string = !hub.options.privateRouting ? '' : vNet::finopsHubSubnet.id +output finopsHubSubnetId string = !hub.options.vnetIntegration ? '' : vNet::finopsHubSubnet.id @description('Resource ID of the script storage account network subnet.') -output scriptSubnetId string = !hub.options.privateRouting ? '' : vNet::scriptSubnet.id +output scriptSubnetId string = !hub.options.vnetIntegration ? '' : vNet::scriptSubnet.id @description('Resource ID of the Data Explorer network subnet.') -output dataExplorerSubnetId string = !hub.options.privateRouting ? '' : vNet::dataExplorerSubnet.id +output dataExplorerSubnetId string = !hub.options.vnetIntegration ? '' : vNet::dataExplorerSubnet.id diff --git a/src/templates/finops-hub/modules/fx/hub-types.bicep b/src/templates/finops-hub/modules/fx/hub-types.bicep index 2cb8e050e..1ff460b98 100644 --- a/src/templates/finops-hub/modules/fx/hub-types.bicep +++ b/src/templates/finops-hub/modules/fx/hub-types.bicep @@ -72,7 +72,8 @@ type HubRoutingProperties = { keyVaultSku: 'KeyVault SKU. Allowed values: "standard", "premium".' keyVaultEnablePurgeProtection: 'Indicates whether purge protection is enabled for the Key Vault. When enabled, deleted Key Vault and its secrets cannot be permanently deleted until the retention period expires, which is required for compliance in some environments.' networkAddressPrefix: 'Address prefix for the FinOps hub isolated virtual network, if private network routing is enabled.' - privateRouting: 'Indicates whether private network routing is enabled.' + vnetIntegration: 'Indicates whether the FinOps hub is deployed inside a virtual network. True for "vnet" and "private" network modes.' + privateRouting: 'Indicates whether private network routing is enabled (private endpoints, no public access). True for the "private" network mode only.' publisherIsolation: 'Indicates whether FinOps hub resources should be separated by publisher for advanced security.' storageInfrastructureEncryption: 'Indicates whether infrastructure encryption is enabled for the storage account.' storageSku: 'Storage account SKU. Allowed values: "Premium_LRS", "Premium_ZRS".' @@ -96,6 +97,7 @@ type HubProperties = { keyVaultSku: string keyVaultEnablePurgeProtection: bool networkAddressPrefix: string + vnetIntegration: bool privateRouting: bool publisherIsolation: bool storageInfrastructureEncryption: bool @@ -191,7 +193,7 @@ func newHubInternal( keyVaultSku string, keyVaultEnablePurgeProtection bool, enableInfrastructureEncryption bool, - enablePublicAccess bool, + networkMode string, networkName string, networkAddressPrefix string, isTelemetryEnabled bool, @@ -211,27 +213,28 @@ func newHubInternal( keyVaultSku: keyVaultSku keyVaultEnablePurgeProtection: keyVaultEnablePurgeProtection networkAddressPrefix: networkAddressPrefix - privateRouting: !enablePublicAccess + vnetIntegration: networkMode != 'public' + privateRouting: networkMode == 'private' publisherIsolation: false // TODO: Expose publisher isolation option storageInfrastructureEncryption: enableInfrastructureEncryption storageSku: storageSku } routing: { - networkId: enablePublicAccess ? '' : resourceId('Microsoft.Network/virtualNetworks', networkName) - networkName: enablePublicAccess ? '' : networkName - scriptStorage: enablePublicAccess ? '' : '${take(safeStorageName(name), 16 - length(suffix))}script${suffix}' + networkId: networkMode == 'public' ? '' : resourceId('Microsoft.Network/virtualNetworks', networkName) + networkName: networkMode == 'public' ? '' : networkName + scriptStorage: networkMode != 'private' ? '' : '${take(safeStorageName(name), 16 - length(suffix))}script${suffix}' dnsZones: { - blob: enablePublicAccess ? { id:'', name:'' } : dnsZoneIdName('blob') - dfs: enablePublicAccess ? { id:'', name:'' } : dnsZoneIdName('dfs') - queue: enablePublicAccess ? { id:'', name:'' } : dnsZoneIdName('queue') - table: enablePublicAccess ? { id:'', name:'' } : dnsZoneIdName('table') + blob: networkMode != 'private' ? { id:'', name:'' } : dnsZoneIdName('blob') + dfs: networkMode != 'private' ? { id:'', name:'' } : dnsZoneIdName('dfs') + queue: networkMode != 'private' ? { id:'', name:'' } : dnsZoneIdName('queue') + table: networkMode != 'private' ? { id:'', name:'' } : dnsZoneIdName('table') } subnets: { - dataExplorer: enablePublicAccess ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'dataExplorer-subnet')! - dataFactory: enablePublicAccess ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'private-endpoint-subnet')! - keyVault: enablePublicAccess ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'private-endpoint-subnet')! - scripts: enablePublicAccess ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'script-subnet')! - storage: enablePublicAccess ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'private-endpoint-subnet')! + dataExplorer: networkMode == 'public' ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'dataExplorer-subnet')! + dataFactory: networkMode == 'public' ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'private-endpoint-subnet')! + keyVault: networkMode == 'public' ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'private-endpoint-subnet')! + scripts: networkMode == 'public' ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'script-subnet')! + storage: networkMode == 'public' ? '' : resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, 'private-endpoint-subnet')! } } core: { @@ -250,7 +253,7 @@ func newHub( keyVaultSku string, keyVaultEnablePurgeProtection bool, enableInfrastructureEncryption bool, - enablePublicAccess bool, + networkMode string, networkAddressPrefix string, isTelemetryEnabled bool, ) HubProperties => newHubInternal( @@ -264,7 +267,7 @@ func newHub( keyVaultSku, keyVaultEnablePurgeProtection, enableInfrastructureEncryption, - enablePublicAccess, + networkMode, '${safeStorageName(name)}-vnet-${location}', // networkName, cSpell:ignore vnet networkAddressPrefix, isTelemetryEnabled ?? true diff --git a/src/templates/finops-hub/modules/hub.bicep b/src/templates/finops-hub/modules/hub.bicep index 2b5603372..7778bf2b5 100644 --- a/src/templates/finops-hub/modules/hub.bicep +++ b/src/templates/finops-hub/modules/hub.bicep @@ -167,8 +167,13 @@ param dataExplorerRawRetentionInDays int = 0 @description('Optional. Number of months of data to retain in the Data Explorer *_final_v* tables. Default: 13.') param dataExplorerFinalRetentionInMonths int = 13 -@description('Optional. Enable public access to the data lake. Default: true.') -param enablePublicAccess bool = true +@description('Optional. Network access mode. "public" (default): no virtual network. "vnet": resources are deployed inside a virtual network with NAT Gateway for outbound, but public endpoints are kept. "private": full virtual network with private endpoints, public access blocked.') +@allowed([ + 'public' + 'vnet' + 'private' +]) +param networkMode string = 'public' @description('Optional. Address space for the workload. Minimum /26 subnet size is required for the workload. Default: "10.20.30.0/26".') param virtualNetworkAddressPrefix string = '10.20.30.0/26' @@ -193,7 +198,7 @@ var hub = newHub( keyVaultSku, enablePurgeProtection, enableInfrastructureEncryption, - enablePublicAccess, + networkMode, virtualNetworkAddressPrefix, enableDefaultTelemetry ) @@ -217,8 +222,8 @@ var telemetryString = join([ !useAzureDataExplorer ? '' : replace(replace(replace(replace(replace(replace(replace(replace(split(split(dataExplorerSku, 'Standard_')[1], '_')[0], 'C', ''), 'D', ''), 'E', ''), 'L', ''), 'a', ''), 'd', ''), 'i', ''), 's', '') // Number of nodes in the cluster !useAzureDataExplorer || dataExplorerCapacity == 1 ? '' : 'x${dataExplorerCapacity}' - // P = private endpoints enabled - enablePublicAccess ? '' : 'P' + // V = vnet-only, P = private endpoints enabled + networkMode == 'private' ? 'P' : (networkMode == 'vnet' ? 'V' : '') ], '')