diff --git a/src/templates/finops-hub/main.bicep b/src/templates/finops-hub/main.bicep index 4e058fcba..d583a1eff 100644 --- a/src/templates/finops-hub/main.bicep +++ b/src/templates/finops-hub/main.bicep @@ -162,6 +162,21 @@ param enablePublicAccess bool = true @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' +@description('Optional. Name of an existing VNet to deploy private endpoints into (BYO-VNet for hub-and-spoke). Leave empty to create a new VNet. Default: "".') +param existingVNetName string = '' + +@description('Optional. Resource group of the existing VNet. Leave empty if same as deployment RG. Default: "".') +param existingVNetResourceGroupName string = '' + +@description('Optional. Subnet name for private endpoints. Default: "snet-finops-pe-01".') +param peSubnetName string = 'snet-finops-pe-01' + +@description('Optional. Subnet name for deployment scripts (delegated to Microsoft.ContainerInstance/containerGroups). Default: "snet-finops-script-01".') +param scriptSubnetName string = 'snet-finops-script-01' + +@description('Optional. Subnet name for Azure Data Explorer. Default: "snet-finops-adx-01".') +param dataExplorerSubnetName string = 'snet-finops-adx-01' + //============================================================================== // Resources @@ -196,6 +211,11 @@ module hub 'modules/hub.bicep' = { remoteHubStorageKey: remoteHubStorageKey enablePublicAccess: enablePublicAccess virtualNetworkAddressPrefix: virtualNetworkAddressPrefix + existingVNetName: existingVNetName + existingVNetResourceGroupName: existingVNetResourceGroupName + peSubnetName: peSubnetName + scriptSubnetName: scriptSubnetName + dataExplorerSubnetName: dataExplorerSubnetName } } 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..55471c0d1 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep @@ -16,14 +16,19 @@ param hub HubProperties // Variables //============================================================================== -var nsgName = '${hub.routing.networkName}-nsg' +var nsgName = 'nsg-finops-${hub.name}' -// Workaround https://github.com/Azure/bicep/issues/1853 -var finopsHubSubnetName = 'private-endpoint-subnet' -var scriptSubnetName = 'script-subnet' -var dataExplorerSubnetName = 'dataExplorer-subnet' +// True when the user supplies their own VNet (BYO / hub-and-spoke). +// In BYO mode subnets must already exist in the existing VNet and we skip creating NSG/VNet here. +var bringYourOwnNetwork = !empty(hub.routing.existingVNetResourceGroupName) || (!empty(hub.routing.networkName) && !startsWith(hub.routing.networkName, 'vnet-finops-')) +var createNetwork = hub.options.privateRouting && !bringYourOwnNetwork -var subnets = !hub.options.privateRouting ? [] : [ +// Use the subnet names from hub.routing so they always match what was passed to newHub. +var finopsHubSubnetName = hub.routing.peSubnetName +var scriptSubnetName = hub.routing.scriptSubnetName +var dataExplorerSubnetName = hub.routing.dataExplorerSubnetName + +var subnets = !createNetwork ? [] : [ { name: finopsHubSubnetName properties: { @@ -80,7 +85,7 @@ 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 (createNetwork) { name: nsgName location: hub.location tags: getHubTags(hub, 'Microsoft.Storage/networkSecurityGroups') @@ -168,7 +173,7 @@ 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 (createNetwork) { name: hub.routing.networkName location: hub.location tags: getHubTags(hub, 'Microsoft.Storage/virtualNetworks') @@ -295,7 +300,7 @@ resource tablePrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = if resource scriptStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = if (hub.options.privateRouting) { name: hub.routing.scriptStorage dependsOn: [ - vNet::scriptSubnet + vNet ] location: hub.location sku: { @@ -324,9 +329,9 @@ resource scriptStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = i } resource scriptEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (hub.options.privateRouting) { - name: '${scriptStorageAccount.name}-blob-ep' + name: 'pep-finops-stgblob-${replace(hub.name, '-01', '-02')}' dependsOn: [ - vNet::scriptSubnet + vNet ] location: hub.location tags: getHubTags(hub, 'Microsoft.Network/privateEndpoints') @@ -369,21 +374,20 @@ 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.privateRouting ? '' : hub.routing.networkId @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.privateRouting ? [] : [hub.options.networkAddressPrefix] @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 = !createNetwork ? [] : 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.privateRouting ? '' : hub.routing.subnets.dataFactory @description('Resource ID of the script storage account network subnet.') -output scriptSubnetId string = !hub.options.privateRouting ? '' : vNet::scriptSubnet.id +output scriptSubnetId string = !hub.options.privateRouting ? '' : hub.routing.subnets.scripts @description('Resource ID of the Data Explorer network subnet.') -output dataExplorerSubnetId string = !hub.options.privateRouting ? '' : vNet::dataExplorerSubnet.id +output dataExplorerSubnetId string = !hub.options.privateRouting ? '' : hub.routing.subnets.dataExplorer diff --git a/src/templates/finops-hub/modules/fx/hub-app.bicep b/src/templates/finops-hub/modules/fx/hub-app.bicep index 6bdd0cf21..112aff155 100644 --- a/src/templates/finops-hub/modules/fx/hub-app.bicep +++ b/src/templates/finops-hub/modules/fx/hub-app.bicep @@ -317,7 +317,7 @@ resource factorySelfRoleAssignments 'Microsoft.Authorization/roleAssignments@202 // Create managed identity to start/stop triggers resource triggerManagerIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = if (usesDataFactory) { - name: '${dataFactory.name}_triggerManager' + name: 'id-finops-${app.hub.name}' location: app.hub.location tags: union(app.tags, app.hub.tagsByResource[?'Microsoft.ManagedIdentity/userAssignedIdentities'] ?? {}) } @@ -391,7 +391,7 @@ resource blobPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' exist } resource blobEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (usesStorage && app.hub.options.privateRouting) { - name: '${storageAccount.name}-blob-ep' + name: 'pep-finops-stgblob-${app.hub.name}' location: app.hub.location tags: getAppPublisherTags(app, 'Microsoft.Network/privateEndpoints') properties: { @@ -430,7 +430,7 @@ resource dfsPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' existi } resource dfsEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (usesStorage && app.hub.options.privateRouting) { - name: '${storageAccount.name}-dfs-ep' + name: 'pep-finops-stgdfs-${app.hub.name}' location: app.hub.location tags: getAppPublisherTags(app, 'Microsoft.Network/privateEndpoints') properties: { @@ -515,7 +515,7 @@ resource keyVaultPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = } resource keyVaultEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (usesKeyVault && app.hub.options.privateRouting) { - name: '${keyVault.name}-ep' + name: 'pep-finops-kv-${app.hub.name}' location: app.hub.location tags: getAppPublisherTags(app, 'Microsoft.Network/privateEndpoints') properties: { @@ -550,6 +550,21 @@ resource keyVaultEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if ( } +//============================================================================== +// Event Grid System Topic (explicit name so it doesn't get an auto-generated GUID) +//============================================================================== + +resource eventGridSystemTopic 'Microsoft.EventGrid/systemTopics@2024-06-01-preview' = if (usesStorage) { + name: 'evg-finops-${app.hub.name}' + location: app.hub.location + tags: getAppPublisherTags(app, 'Microsoft.EventGrid/systemTopics') + properties: { + #disable-next-line BCP318 + source: storageAccount.id + topicType: 'Microsoft.Storage.StorageAccounts' + } +} + //============================================================================== // Outputs //============================================================================== diff --git a/src/templates/finops-hub/modules/fx/hub-types.bicep b/src/templates/finops-hub/modules/fx/hub-types.bicep index 2cb8e050e..272d16a21 100644 --- a/src/templates/finops-hub/modules/fx/hub-types.bicep +++ b/src/templates/finops-hub/modules/fx/hub-types.bicep @@ -42,6 +42,10 @@ type IdNameObject = { id: string, name: string } type HubRoutingProperties = { networkId: string networkName: string + existingVNetResourceGroupName: string + peSubnetName: string + scriptSubnetName: string + dataExplorerSubnetName: string scriptStorage: string dnsZones: { blob: IdNameObject @@ -195,6 +199,10 @@ func newHubInternal( networkName string, networkAddressPrefix string, isTelemetryEnabled bool, + existingVNetResourceGroupName string, + peSubnetName string, + scriptSubnetName string, + dataExplorerSubnetName string ) HubProperties => { id: id name: name @@ -217,9 +225,13 @@ func newHubInternal( storageSku: storageSku } routing: { - networkId: enablePublicAccess ? '' : resourceId('Microsoft.Network/virtualNetworks', networkName) + networkId: enablePublicAccess ? '' : (empty(existingVNetResourceGroupName) ? resourceId('Microsoft.Network/virtualNetworks', networkName) : resourceId(existingVNetResourceGroupName, 'Microsoft.Network/virtualNetworks', networkName)) networkName: enablePublicAccess ? '' : networkName - scriptStorage: enablePublicAccess ? '' : '${take(safeStorageName(name), 16 - length(suffix))}script${suffix}' + existingVNetResourceGroupName: existingVNetResourceGroupName + peSubnetName: peSubnetName + scriptSubnetName: scriptSubnetName + dataExplorerSubnetName: dataExplorerSubnetName + scriptStorage: enablePublicAccess ? '' : take('stgfinops${replace(safeStorageName(name), '-', '')}02', 24) dnsZones: { blob: enablePublicAccess ? { id:'', name:'' } : dnsZoneIdName('blob') dfs: enablePublicAccess ? { id:'', name:'' } : dnsZoneIdName('dfs') @@ -227,11 +239,11 @@ func newHubInternal( table: enablePublicAccess ? { 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: enablePublicAccess ? '' : (empty(existingVNetResourceGroupName) ? resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, dataExplorerSubnetName)! : resourceId(existingVNetResourceGroupName, 'Microsoft.Network/virtualNetworks/subnets', networkName, dataExplorerSubnetName)!) + dataFactory: enablePublicAccess ? '' : (empty(existingVNetResourceGroupName) ? resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, peSubnetName)! : resourceId(existingVNetResourceGroupName, 'Microsoft.Network/virtualNetworks/subnets', networkName, peSubnetName)!) + keyVault: enablePublicAccess ? '' : (empty(existingVNetResourceGroupName) ? resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, peSubnetName)! : resourceId(existingVNetResourceGroupName, 'Microsoft.Network/virtualNetworks/subnets', networkName, peSubnetName)!) + scripts: enablePublicAccess ? '' : (empty(existingVNetResourceGroupName) ? resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, scriptSubnetName)! : resourceId(existingVNetResourceGroupName, 'Microsoft.Network/virtualNetworks/subnets', networkName, scriptSubnetName)!) + storage: enablePublicAccess ? '' : (empty(existingVNetResourceGroupName) ? resourceId('Microsoft.Network/virtualNetworks/subnets', networkName, peSubnetName)! : resourceId(existingVNetResourceGroupName, 'Microsoft.Network/virtualNetworks/subnets', networkName, peSubnetName)!) } } core: { @@ -253,6 +265,11 @@ func newHub( enablePublicAccess bool, networkAddressPrefix string, isTelemetryEnabled bool, + existingVNetName string, + existingVNetResourceGroupName string, + peSubnetName string, + scriptSubnetName string, + dataExplorerSubnetName string ) HubProperties => newHubInternal( '${resourceGroup().id}/providers/Microsoft.Cloud/hubs/${name}', // id name, @@ -265,9 +282,13 @@ func newHub( keyVaultEnablePurgeProtection, enableInfrastructureEncryption, enablePublicAccess, - '${safeStorageName(name)}-vnet-${location}', // networkName, cSpell:ignore vnet + empty(existingVNetName) ? 'vnet-finops-${name}' : existingVNetName, // networkName, cSpell:ignore vnet networkAddressPrefix, - isTelemetryEnabled ?? true + isTelemetryEnabled ?? true, + existingVNetResourceGroupName, + empty(peSubnetName) ? 'snet-finops-pe-01' : peSubnetName, + empty(scriptSubnetName) ? 'snet-finops-script-01' : scriptSubnetName, + empty(dataExplorerSubnetName) ? 'snet-finops-adx-01' : dataExplorerSubnetName ) //------------------------------------------------------------------------------ @@ -298,13 +319,16 @@ func newAppInternal( hub: hub // Globally unique Data Factory name: 3-63 chars; letters, numbers, non-repeating dashes - dataFactory: replace('${take('${replace(hub.name, '_', '-')}-engine', 63 - length(suffix) - 1)}-${suffix}', '--', '-') + // Custom naming: adf-finops- + dataFactory: take('adf-finops-${replace(hub.name, '_', '-')}', 63) // Globally unique KeyVault name: 3-24 chars; letters, numbers, dashes - keyVault: replace('${take('${replace(hub.name, '_', '-')}-vault', 24 - length(suffix) - 1)}-${suffix}', '--', '-') + // Custom naming: kv-finops- + keyVault: take('kv-finops-${replace(hub.name, '_', '-')}', 24) // Globally unique storage account name: 3-24 chars; lowercase letters/numbers only - storage: '${take(safeStorageName(hub.name), 24 - length(suffix))}${suffix}' + // Custom naming: stgfinops01 (no dashes, lowercase) + storage: take('stgfinops${replace(safeStorageName(hub.name), '-', '')}01', 24) } @export() diff --git a/src/templates/finops-hub/modules/hub.bicep b/src/templates/finops-hub/modules/hub.bicep index 2b5603372..ab20daf08 100644 --- a/src/templates/finops-hub/modules/hub.bicep +++ b/src/templates/finops-hub/modules/hub.bicep @@ -173,6 +173,21 @@ param enablePublicAccess bool = true @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' +@description('Optional. Name of an existing VNet to deploy the FinOps hub private endpoints into. Leave empty to create a new VNet. Default: "".') +param existingVNetName string = '' + +@description('Optional. Resource group name of the existing VNet. Leave empty if same resource group as the deployment. Default: "".') +param existingVNetResourceGroupName string = '' + +@description('Optional. Name of the subnet for private endpoints (in either the new or existing VNet). Default: "snet-finops-pe-01".') +param peSubnetName string = 'snet-finops-pe-01' + +@description('Optional. Name of the subnet for deployment scripts (must be delegated to Microsoft.ContainerInstance/containerGroups). Default: "snet-finops-script-01".') +param scriptSubnetName string = 'snet-finops-script-01' + +@description('Optional. Name of the subnet for Azure Data Explorer (only used when ADX is enabled). Default: "snet-finops-adx-01".') +param dataExplorerSubnetName string = 'snet-finops-adx-01' + @description('Optional. Enable telemetry to track anonymous module usage trends, monitor for bugs, and improve future releases.') param enableDefaultTelemetry bool = true @@ -195,7 +210,12 @@ var hub = newHub( enableInfrastructureEncryption, enablePublicAccess, virtualNetworkAddressPrefix, - enableDefaultTelemetry + enableDefaultTelemetry, + existingVNetName, + existingVNetResourceGroupName, + peSubnetName, + scriptSubnetName, + dataExplorerSubnetName ) var useFabric = !empty(fabricQueryUri)