diff --git a/docs/_reporting/hubs/template.md b/docs/_reporting/hubs/template.md
index 2304b666f..912b7927f 100644
--- a/docs/_reporting/hubs/template.md
+++ b/docs/_reporting/hubs/template.md
@@ -101,6 +101,8 @@ Please ensure the following prerequisites are met before deploying this template
| **dataExplorerFinalRetentionInMonths** | Int | Optional. Number of months of data to retain in the Data Explorer \*_final_v\* tables. | 13 |
| **remoteHubStorageUri** | String | Optional. Storage account to push data to for ingestion into a remote hub. | |
| **remoteHubStorageKey** | String | Optional. Storage account key to use when pushing data to a remote hub. | |
+| **enablePublicAccess** | string | Optional. Disable public access to the datalake (storage firewall). | False |
+| **virtualNetworkAddressPrefix**| string | Optional. IP Address range for the private vNet used by the toolkit. /27 recommended to avoid wasting IP's as the deployment will split it into 2 x /28 subnets. | '10.20.30.0/27' |
@@ -128,6 +130,7 @@ Resources use the following naming convention: `--script` storage account (Data Lake Storage Gen2) for deployment scripts.
- `-engine-` Data Factory instance
- Pipelines:
- `config_InitializeHub` – Initializes (or updates) the FinOps hub instance after deployment.
@@ -147,6 +150,9 @@ Resources use the following naming convention: `--store` - Managed private endpoint for storage account.
+ - `-vault-` - Managed private endpoint for Azure Key Vault.
- `-vault-` Key Vault instance
- Secrets:
- Data Factory system managed identity
diff --git a/docs/_resources/changelog.md b/docs/_resources/changelog.md
index 229fb5e21..96d7d3ee0 100644
--- a/docs/_resources/changelog.md
+++ b/docs/_resources/changelog.md
@@ -65,6 +65,12 @@ Legend:
> 2. Auto-backfill – Backfill historical data from Microsoft Cost Management.
> 3. Retention – Configure how long you want to keep Cost Management exports and normalized data in storage.
> 4. ETL pipelile – Add support for parquet files created by Cost Management exports.
+> 5. Private endpoints support.
+> - Added private endpoints for storage account & Keyvault.
+> - Added managed virtual network & storage endpoint for Azure Data Factory Runtime.
+> - All data processing now happens within a vNet.
+> - Added param to disable external access to data lake
+> - Added param to specify subnet range of vnet - minumum size = /27
📊 Power BI reports
{: .fs-5 .fw-500 .mt-4 mb-0 }
diff --git a/src/templates/finops-hub/main.bicep b/src/templates/finops-hub/main.bicep
index b6ba6860f..47a14b15b 100644
--- a/src/templates/finops-hub/main.bicep
+++ b/src/templates/finops-hub/main.bicep
@@ -129,6 +129,12 @@ param remoteHubStorageUri string = ''
@secure()
param remoteHubStorageKey string = ''
+@description('Optional. Enable public access to the data lake. Default: true.')
+param enablePublicAccess bool = true
+
+@description('Optional. Address space for the workload. A /27 is required for the workload. Default: "10.20.30.0/27".')
+param virtualNetworkAddressPrefix string = '10.20.30.0/27'
+
//==============================================================================
// Resources
//==============================================================================
@@ -152,6 +158,8 @@ module hub 'modules/hub.bicep' = {
dataExplorerFinalRetentionInMonths: dataExplorerFinalRetentionInMonths
remoteHubStorageUri: remoteHubStorageUri
remoteHubStorageKey: remoteHubStorageKey
+ enablePublicAccess: enablePublicAccess
+ virtualNetworkAddressPrefix: virtualNetworkAddressPrefix
}
}
diff --git a/src/templates/finops-hub/modules/dataFactory.bicep b/src/templates/finops-hub/modules/dataFactory.bicep
index 380a72654..b2ba79c64 100644
--- a/src/templates/finops-hub/modules/dataFactory.bicep
+++ b/src/templates/finops-hub/modules/dataFactory.bicep
@@ -74,6 +74,8 @@ var datasetPropsDefault = {
var safeExportContainerName = replace('${exportContainerName}', '-', '_')
var safeIngestionContainerName = replace('${ingestionContainerName}', '-', '_')
var safeConfigContainerName = replace('${configContainerName}', '-', '_')
+var managedVnetName = 'default'
+var managedIntegrationRuntimeName = 'AutoResolveIntegrationRuntime'
// Separator used to separate ingestion ID from file name for ingested files
var ingestionIdFileNameSeparator = '__'
@@ -133,6 +135,106 @@ module azuretimezones 'azuretimezones.bicep' = {
}
}
+resource managedVirtualNetwork 'Microsoft.DataFactory/factories/managedVirtualNetworks@2018-06-01' = {
+ name: managedVnetName
+ parent: dataFactory
+ properties: {}
+}
+
+resource managedIntegrationRuntime 'Microsoft.DataFactory/factories/integrationRuntimes@2018-06-01' = {
+ name: managedIntegrationRuntimeName
+ parent: dataFactory
+ properties: {
+ type: 'Managed'
+ managedVirtualNetwork: {
+ referenceName: managedVnetName
+ type: 'ManagedVirtualNetworkReference'
+ }
+ typeProperties: {
+ computeProperties: {
+ location: 'AutoResolve'
+ }
+ }
+ }
+ dependsOn: [
+ managedVirtualNetwork
+ ]
+}
+
+resource storageManagedPrivateEndpoint 'Microsoft.DataFactory/factories/managedVirtualNetworks/managedPrivateEndpoints@2018-06-01' = {
+ name: storageAccount.name
+ parent: managedVirtualNetwork
+ dependsOn: [
+ identityRoleAssignments
+ ]
+ properties: {
+ name: storageAccount.name
+ groupId: 'dfs'
+ privateLinkResourceId: storageAccount.id
+ fqdns: [
+ storageAccount.properties.primaryEndpoints.dfs
+ ]
+ }
+}
+
+module getStoragePrivateEndpointConnections 'storageEndpoints.bicep' = {
+ name: 'GetStoragePrivateEndpointConnections'
+ dependsOn: [
+ storageManagedPrivateEndpoint
+ ]
+ params: {
+ storageAccountName: storageAccount.name
+ }
+}
+
+module approveStoragePrivateEndpointConnections 'storageEndpoints.bicep' = {
+ name: 'ApproveStoragePrivateEndpointConnections'
+ dependsOn: [
+ getStoragePrivateEndpointConnections
+ ]
+ params: {
+ storageAccountName: storageAccount.name
+ privateEndpointConnections: getStoragePrivateEndpointConnections.outputs.privateEndpointConnections
+ }
+}
+
+resource keyVaultManagedPrivateEndpoint 'Microsoft.DataFactory/factories/managedVirtualNetworks/managedPrivateEndpoints@2018-06-01' = {
+ name: keyVault.name
+ parent: managedVirtualNetwork
+ dependsOn: [
+ identityRoleAssignments
+ ]
+ properties: {
+ name: keyVault.name
+ groupId: 'vault'
+ privateLinkResourceId: keyVault.id
+ fqdns: [
+ keyVault.properties.vaultUri
+ ]
+ }
+}
+
+module getKeyVaultPrivateEndpointConnections 'keyVaultEndpoints.bicep' = {
+ name: 'GetKeyVaultPrivateEndpointConnections'
+ dependsOn: [
+ keyVaultManagedPrivateEndpoint
+ ]
+ params: {
+ keyVaultName: keyVault.name
+ }
+}
+
+module approveKeyVaultPrivateEndpointConnections 'keyVaultEndpoints.bicep' = {
+ name: 'ApproveKeyVaultPrivateEndpointConnections'
+ dependsOn: [
+ getKeyVaultPrivateEndpointConnections
+ ]
+ params: {
+ keyVaultName: keyVault.name
+ privateEndpointConnections: getKeyVaultPrivateEndpointConnections.outputs.privateEndpointConnections
+ }
+}
+
//------------------------------------------------------------------------------
// Identities and RBAC
//------------------------------------------------------------------------------
@@ -277,6 +379,10 @@ resource linkedService_storageAccount 'Microsoft.DataFactory/factories/linkedser
typeProperties: {
url: reference('Microsoft.Storage/storageAccounts/${storageAccount.name}', '2021-08-01').primaryEndpoints.dfs
}
+ connectVia: {
+ referenceName: managedIntegrationRuntime.name
+ type: 'IntegrationRuntimeReference'
+ }
}
}
diff --git a/src/templates/finops-hub/modules/hub.bicep b/src/templates/finops-hub/modules/hub.bicep
index d82b2039c..08d4c8a0f 100644
--- a/src/templates/finops-hub/modules/hub.bicep
+++ b/src/templates/finops-hub/modules/hub.bicep
@@ -127,6 +127,12 @@ param remoteHubStorageUri string = ''
@secure()
param remoteHubStorageKey string = ''
+@description('Optional. Address space for the workload. A /27 is required for the workload. Default: "10.20.30.0/27".')
+param virtualNetworkAddressPrefix string = '10.20.30.0/27'
+
+@description('Optional. Enable public access to the data lake. Default: true.')
+param enablePublicAccess bool = true
+
@description('Optional. Enable telemetry to track anonymous module usage trends, monitor for bugs, and improve future releases.')
param enableDefaultTelemetry bool = true
@@ -211,6 +217,19 @@ resource defaultTelemetry 'Microsoft.Resources/deployments@2022-09-01' = if (ena
}
}
+//------------------------------------------------------------------------------
+// Virtual network
+//------------------------------------------------------------------------------
+
+module vnet 'vnet.bicep' = {
+ name: 'vnet'
+ params: {
+ hubName: hubName
+ location: location
+ virtualNetworkAddressPrefix: virtualNetworkAddressPrefix
+ }
+}
+
//------------------------------------------------------------------------------
// ADLSv2 storage account for staging and archive
//------------------------------------------------------------------------------
@@ -229,6 +248,10 @@ module storage 'storage.bicep' = {
ingestionRetentionInMonths: ingestionRetentionInMonths
rawRetentionInDays: dataExplorerRawRetentionInDays
finalRetentionInMonths: dataExplorerFinalRetentionInMonths
+ virtualNetworkId: vnet.outputs.vNetId
+ privateEndpointSubnetId: vnet.outputs.finopsHubSubnetId
+ scriptSubnetId: vnet.outputs.scriptSubnetId
+ enablePublicAccess: enablePublicAccess
}
}
@@ -306,6 +329,8 @@ module keyVault 'keyVault.bicep' = {
tags: resourceTags
tagsByResource: tagsByResource
storageAccountKey: remoteHubStorageKey
+ virtualNetworkId: vnet.outputs.vNetId
+ privateEndpointSubnetId: vnet.outputs.finopsHubSubnetId
accessPolicies: [
{
objectId: dataFactory.identity.principalId
diff --git a/src/templates/finops-hub/modules/keyVault.bicep b/src/templates/finops-hub/modules/keyVault.bicep
index 2b3a69e20..1e15a18c4 100644
--- a/src/templates/finops-hub/modules/keyVault.bicep
+++ b/src/templates/finops-hub/modules/keyVault.bicep
@@ -34,6 +34,12 @@ param tags object = {}
@description('Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources.')
param tagsByResource object = {}
+@description('Required. Id of the virtual network for private endpoints.')
+param virtualNetworkId string
+
+@description('Required. Id of the subnet for private endpoints.')
+param privateEndpointSubnetId string
+
//------------------------------------------------------------------------------
// Variables
//------------------------------------------------------------------------------
@@ -43,6 +49,7 @@ var keyVaultPrefix = '${replace(hubName, '_', '-')}-vault'
var keyVaultSuffix = '-${uniqueSuffix}'
var keyVaultName = replace('${take(keyVaultPrefix, 24 - length(keyVaultSuffix))}${keyVaultSuffix}', '--', '-')
var keyVaultSecretName = '${toLower(hubName)}-storage-key'
+var keyVaultPrivateDnsZoneName = 'privatelink${replace(environment().suffixes.keyvaultDns, 'vault', 'vaultcore')}'
var formattedAccessPolicies = [for accessPolicy in accessPolicies: {
applicationId: contains(accessPolicy, 'applicationId') ? accessPolicy.applicationId : ''
@@ -74,6 +81,10 @@ resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: startsWith(location, 'china') ? 'standard' : sku
family: 'A'
}
+ networkAcls: {
+ bypass: 'AzureServices'
+ defaultAction: 'Deny'
+ }
}
}
@@ -98,6 +109,59 @@ resource keyVault_secret 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = if (!e
}
}
+resource keyVaultPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = {
+ name: keyVaultPrivateDnsZoneName
+ location: 'global'
+ properties: {}
+}
+
+resource keyVaultPrivateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = {
+ name: '${replace(keyVaultPrivateDnsZone.name, '.', '-')}-link'
+ location: 'global'
+ parent: keyVaultPrivateDnsZone
+ properties: {
+ virtualNetwork: {
+ id: virtualNetworkId
+ }
+ registrationEnabled: false
+ }
+}
+
+resource keyVaultEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = {
+ name: '${keyVault.name}-ep'
+ location: location
+ properties: {
+ subnet: {
+ id: privateEndpointSubnetId
+ }
+ privateLinkServiceConnections: [
+ {
+ name: 'keyVaultLink'
+ properties: {
+ privateLinkServiceId: keyVault.id
+ groupIds: ['vault']
+ }
+ }
+ ]
+ }
+}
+
+resource keyVaultPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = {
+ name: 'keyvault-endpoint-zone'
+ parent: keyVaultEndpoint
+ properties: {
+ privateDnsZoneConfigs: [
+ {
+ name: keyVaultPrivateDnsZone.name
+ properties: {
+ privateDnsZoneId: keyVaultPrivateDnsZone.id
+ }
+ }
+ ]
+ }
+}
+
+
//==============================================================================
// Outputs
//==============================================================================
diff --git a/src/templates/finops-hub/modules/keyVaultEndpoints.bicep b/src/templates/finops-hub/modules/keyVaultEndpoints.bicep
new file mode 100644
index 000000000..4dab785e2
--- /dev/null
+++ b/src/templates/finops-hub/modules/keyVaultEndpoints.bicep
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//==============================================================================
+// Parameters
+//==============================================================================
+
+@description('Optional. Array of private endpoint connections. Pending ones will be approved.')
+param privateEndpointConnections array = []
+
+@description('Required. Name of the KeyVault.')
+param keyVaultName string
+
+//==============================================================================
+// Resources
+//==============================================================================
+
+resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
+ name: keyVaultName
+}
+
+resource privateEndpointConnection 'Microsoft.KeyVault/vaults/privateEndpointConnections@2023-07-01' = [ for privateEndpointConnection in privateEndpointConnections : if (privateEndpointConnection.properties.privateLinkServiceConnectionState.status == 'Pending') {
+ name: last(array(split(privateEndpointConnection.id, '/')))
+ parent: keyVault
+ properties: {
+ privateLinkServiceConnectionState: {
+ status: 'Approved'
+ description: 'Approved-by-pipeline'
+ }
+ }
+}]
+
+//==============================================================================
+// Outputs
+//==============================================================================
+
+output privateEndpointConnections array = keyVault.properties.privateEndpointConnections
diff --git a/src/templates/finops-hub/modules/storage.bicep b/src/templates/finops-hub/modules/storage.bicep
index e48630776..8448d3340 100644
--- a/src/templates/finops-hub/modules/storage.bicep
+++ b/src/templates/finops-hub/modules/storage.bicep
@@ -42,6 +42,18 @@ param rawRetentionInDays int = 0
@description('Optional. Number of months of data to retain in the Data Explorer *_final_v* tables. Default: 13.')
param finalRetentionInMonths int = 13
+@description('Required. Id of the virtual network for private endpoints.')
+param virtualNetworkId string
+
+@description('Required. Id of the subnet for private endpoints.')
+param privateEndpointSubnetId string
+
+@description('Required. Id of the virtual network for running deployment scripts.')
+param scriptSubnetId string
+
+@description('Optional. Enable public access to the data lake. Default: false.')
+param enablePublicAccess bool
+
//------------------------------------------------------------------------------
// Variables
//------------------------------------------------------------------------------
@@ -50,6 +62,7 @@ param finalRetentionInMonths int = 13
var safeHubName = replace(replace(toLower(hubName), '-', ''), '_', '')
var storageAccountSuffix = uniqueSuffix
var storageAccountName = '${take(safeHubName, 24 - length(storageAccountSuffix))}${storageAccountSuffix}'
+var scriptStorageAccountName = '${take(safeHubName, 16 - length(storageAccountSuffix))}script${storageAccountSuffix}'
var schemaFiles = {
'focuscost_1.0': loadTextContent('../schemas/focuscost_1.0.json')
'focuscost_1.0-preview(v1)': loadTextContent('../schemas/focuscost_1.0-preview(v1).json')
@@ -63,9 +76,13 @@ var schemaFiles = {
}
// Roles needed to auto-start triggers
+// Storage Blob Data Contributor - used by deployment scripts to write data to blob storage
+// Storage File Data Privileged Contributor - used by deployment scripts to write data to blob storage
+// https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/deployment-script-template#use-existing-storage-account
var blobUploadRbacRoles = [
'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor - https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#storage-blob-data-contributor
'e40ec5ca-96e0-45a2-b4ff-59039f2c2b59' // Managed Identity Contributor - https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#managed-identity-contributor
+ '69566ab7-960f-475b-8e7c-b3118f30c6bd' // Storage File Data Privileged Contributor - https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/storage#storage-file-data-privileged-contributor
]
//==============================================================================
@@ -82,9 +99,181 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
tags: union(tags, contains(tagsByResource, 'Microsoft.Storage/storageAccounts') ? tagsByResource['Microsoft.Storage/storageAccounts'] : {})
properties: {
supportsHttpsTrafficOnly: true
+ allowSharedKeyAccess: true
isHnsEnabled: true
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
+ publicNetworkAccess: 'Enabled'
+ networkAcls: {
+ bypass: 'AzureServices'
+ defaultAction: enablePublicAccess ? 'Allow' : 'Deny'
+ }
+}
+}
+
+resource scriptStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
+ name: scriptStorageAccountName
+ location: location
+ sku: {
+ name: 'Standard_LRS' //sku
+ }
+ kind: 'StorageV2'// 'BlockBlobStorage'
+ tags: union(tags, contains(tagsByResource, 'Microsoft.Storage/storageAccounts') ? tagsByResource['Microsoft.Storage/storageAccounts'] : {})
+ properties: {
+ supportsHttpsTrafficOnly: true
+ allowSharedKeyAccess: true
+ isHnsEnabled: false
+ minimumTlsVersion: 'TLS1_2'
+ allowBlobPublicAccess: false
+ publicNetworkAccess: 'Enabled'
+ networkAcls: {
+ bypass: 'AzureServices'
+ defaultAction: 'Deny'
+ virtualNetworkRules: [
+ {
+ id: scriptSubnetId
+ action: 'Allow'
+ }
+ ]
+ }
+ }
+}
+
+resource blobPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = {
+ name: 'privatelink.blob.${environment().suffixes.storage}'
+ location: 'global'
+ properties: {}
+}
+
+resource dfsPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = {
+ name: 'privatelink.dfs.${environment().suffixes.storage}'
+ location: 'global'
+ properties: {}
+}
+
+resource blobPrivateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = {
+ parent: blobPrivateDnsZone
+ name: '${replace(blobPrivateDnsZone.name, '.', '-')}-link'
+ location: 'global'
+ properties: {
+ registrationEnabled: false
+ virtualNetwork: {
+ id: virtualNetworkId
+ }
+ }
+}
+
+resource dfsPrivateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = {
+ parent: dfsPrivateDnsZone
+ name: '${replace(dfsPrivateDnsZone.name, '.', '-')}-link'
+ location: 'global'
+ properties: {
+ registrationEnabled: false
+ virtualNetwork: {
+ id: virtualNetworkId
+ }
+ }
+}
+
+resource blobEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = {
+ name: '${storageAccount.name}-blob-ep'
+ location: location
+ properties: {
+ subnet: {
+ id: privateEndpointSubnetId
+ }
+ privateLinkServiceConnections: [
+ {
+ name: 'blobLink'
+ properties: {
+ privateLinkServiceId: storageAccount.id
+ groupIds: ['blob']
+ }
+ }
+ ]
+ }
+}
+
+resource scriptEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = {
+ name: '${scriptStorageAccount.name}-blob-ep'
+ location: location
+ properties: {
+ subnet: {
+ id: privateEndpointSubnetId
+ }
+ privateLinkServiceConnections: [
+ {
+ name: 'scriptLink'
+ properties: {
+ privateLinkServiceId: scriptStorageAccount.id
+ groupIds: ['blob']
+ }
+ }
+ ]
+ }
+}
+
+resource dfsEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = {
+ name: '${storageAccount.name}-dfs-ep'
+ location: location
+ properties: {
+ subnet: {
+ id: privateEndpointSubnetId
+ }
+ privateLinkServiceConnections: [
+ {
+ name: 'dfsLink'
+ properties: {
+ privateLinkServiceId: storageAccount.id
+ groupIds: ['dfs']
+ }
+ }
+ ]
+ }
+}
+
+resource blobPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = {
+ name: 'blob-endpoint-zone'
+ parent: blobEndpoint
+ properties: {
+ privateDnsZoneConfigs: [
+ {
+ name: blobPrivateDnsZone.name
+ properties: {
+ privateDnsZoneId: blobPrivateDnsZone.id
+ }
+ }
+ ]
+ }
+}
+
+resource scriptPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = {
+ name: 'blob-endpoint-zone'
+ parent: scriptEndpoint
+ properties: {
+ privateDnsZoneConfigs: [
+ {
+ name: blobPrivateDnsZone.name
+ properties: {
+ privateDnsZoneId: blobPrivateDnsZone.id
+ }
+ }
+ ]
+ }
+}
+
+resource dfsPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = {
+ name: 'dfs-endpoint-zone'
+ parent: dfsEndpoint
+ properties: {
+ privateDnsZoneConfigs: [
+ {
+ name: dfsPrivateDnsZone.name
+ properties: {
+ privateDnsZoneId: dfsPrivateDnsZone.id
+ }
+ }
+ ]
}
}
@@ -146,7 +335,7 @@ resource identityRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-0
}
}]
-resource uploadSettings 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
+resource uploadSettings 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: '${storageAccountName}_uploadSettings'
kind: 'AzurePowerShell'
// chinaeast2 is the only region in China that supports deployment scripts
@@ -161,9 +350,13 @@ resource uploadSettings 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
dependsOn: [
configContainer
identityRoleAssignments
+ blobEndpoint
+ blobPrivateDnsZoneGroup
+ scriptEndpoint
+ scriptPrivateDnsZoneGroup
]
properties: {
- azPowerShellVersion: '8.0'
+ azPowerShellVersion: '9.0'
retentionInterval: 'PT1H'
environmentVariables: [
{
@@ -204,6 +397,18 @@ resource uploadSettings 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
}
]
scriptContent: loadTextContent('./scripts/Copy-FileToAzureBlob.ps1')
+ storageAccountSettings: {
+ storageAccountName: scriptStorageAccount.name
+ //storageAccountKey: storageAccount.listKeys().keys[0].value
+ }
+ containerSettings: {
+ containerGroupName: '${scriptStorageAccount.name}cg'
+ subnetIds: [
+ {
+ id: scriptSubnetId
+}
+ ]
+ }
}
}
@@ -217,6 +422,12 @@ output resourceId string = storageAccount.id
@description('The name of the storage account.')
output name string = storageAccount.name
+@description('The resource ID of the storage account.')
+output scriptStorageAccountResourceId string = scriptStorageAccount.id
+
+@description('The name of the storage account.')
+output scriptStorageAccountName string = scriptStorageAccount.name
+
@description('The name of the container used for configuration settings.')
output configContainer string = configContainer.name
diff --git a/src/templates/finops-hub/modules/storageEndpoints.bicep b/src/templates/finops-hub/modules/storageEndpoints.bicep
new file mode 100644
index 000000000..60ef5d052
--- /dev/null
+++ b/src/templates/finops-hub/modules/storageEndpoints.bicep
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//==============================================================================
+// Parameters
+//==============================================================================
+
+@description('Optional. Array of private endpoint connections. Pending ones will be approved.')
+param privateEndpointConnections array = []
+
+@description('Required. Name of the storage account.')
+param storageAccountName string
+
+//==============================================================================
+// Resources
+//==============================================================================
+
+resource storageAccount 'Microsoft.Storage/storageAccounts@2023-04-01' existing = {
+ name: storageAccountName
+}
+
+resource privateEndpointConnection 'Microsoft.Storage/storageAccounts/privateEndpointConnections@2023-04-01' = [ for privateEndpointConnection in privateEndpointConnections : if (privateEndpointConnection.properties.privateLinkServiceConnectionState.status == 'Pending') {
+ name: last(array(split(privateEndpointConnection.id, '/')))
+ parent: storageAccount
+ properties: {
+ privateLinkServiceConnectionState: {
+ status: 'Approved'
+ description: 'Approved-by-pipeline'
+ actionRequired: 'None'
+ }
+ }
+}]
+
+//==============================================================================
+// Outputs
+//==============================================================================
+
+output privateEndpointConnections array = storageAccount.properties.privateEndpointConnections
diff --git a/src/templates/finops-hub/modules/vnet.bicep b/src/templates/finops-hub/modules/vnet.bicep
new file mode 100644
index 000000000..389bf6278
--- /dev/null
+++ b/src/templates/finops-hub/modules/vnet.bicep
@@ -0,0 +1,175 @@
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//==============================================================================
+// Parameters
+//==============================================================================
+
+@description('Required. Name of the hub. Used to ensure unique resource names.')
+param hubName string
+
+@description('Address space for the workload. A /27 is required for the workload.')
+param virtualNetworkAddressPrefix string = '10.20.30.0/27'
+
+@description('Optional. Azure location where all resources should be created. See https://aka.ms/azureregions. Default: (resource group location).')
+param location string = resourceGroup().location
+
+//------------------------------------------------------------------------------
+// Variables
+//------------------------------------------------------------------------------
+
+var safeHubName = replace(replace(toLower(hubName), '-', ''), '_', '')
+var vNetName = '${safeHubName}-vnet-${location}'
+var nsgName = '${vNetName}-nsg'
+var subnets = [
+ {
+ name: 'finops-hub-subnet'
+ properties: {
+ addressPrefix: cidrSubnet(virtualNetworkAddressPrefix, 28, 0)
+ networkSecurityGroup: {
+ id: nsg.id
+ }
+ serviceEndpoints: [
+ {
+ service: 'Microsoft.Storage'
+ }
+ ]
+ }
+ }
+ {
+ name: 'script-subnet'
+ properties: {
+ addressPrefix: cidrSubnet(virtualNetworkAddressPrefix, 28, 1)
+ networkSecurityGroup: {
+ id: nsg.id
+ }
+ delegations: [
+ {
+ name: 'Microsoft.ContainerInstance/containerGroups'
+ properties: {
+ serviceName: 'Microsoft.ContainerInstance/containerGroups'
+ }
+ }
+ ]
+ serviceEndpoints: [
+ {
+ service: 'Microsoft.Storage'
+ }
+ ]
+ }
+ }
+]
+
+//------------------------------------------------------------------------------
+// Resources
+//------------------------------------------------------------------------------
+
+resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
+ name: nsgName
+ location: location
+ properties: {
+ securityRules: [
+ {
+ name: 'AllowVnetInBound'
+ properties: {
+ priority: 100
+ direction: 'Inbound'
+ access: 'Allow'
+ protocol: '*'
+ sourcePortRange: '*'
+ destinationPortRange: '*'
+ sourceAddressPrefix: 'VirtualNetwork'
+ destinationAddressPrefix: 'VirtualNetwork'
+ }
+ }
+ {
+ name: 'AllowAzureLoadBalancerInBound'
+ properties: {
+ priority: 200
+ direction: 'Inbound'
+ access: 'Allow'
+ protocol: '*'
+ sourcePortRange: '*'
+ destinationPortRange: '*'
+ sourceAddressPrefix: 'AzureLoadBalancer'
+ destinationAddressPrefix: '*'
+ }
+ }
+ {
+ name: 'DenyAllInBound'
+ properties: {
+ priority: 4096
+ direction: 'Inbound'
+ access: 'Deny'
+ protocol: '*'
+ sourcePortRange: '*'
+ destinationPortRange: '*'
+ sourceAddressPrefix: '*'
+ destinationAddressPrefix: '*'
+ }
+ }
+ {
+ name: 'AllowVnetOutBound'
+ properties: {
+ priority: 100
+ direction: 'Outbound'
+ access: 'Allow'
+ protocol: '*'
+ sourcePortRange: '*'
+ destinationPortRange: '*'
+ sourceAddressPrefix: 'VirtualNetwork'
+ destinationAddressPrefix: 'VirtualNetwork'
+ }
+ }
+ {
+ name: 'AllowInternetOutBound'
+ properties: {
+ priority: 200
+ direction: 'Outbound'
+ access: 'Allow'
+ protocol: '*'
+ sourcePortRange: '*'
+ destinationPortRange: '*'
+ sourceAddressPrefix: '*'
+ destinationAddressPrefix: 'Internet'
+ }
+ }
+ {
+ name: 'DenyAllOutBound'
+ properties: {
+ priority: 4096
+ direction: 'Outbound'
+ access: 'Deny'
+ protocol: '*'
+ sourcePortRange: '*'
+ destinationPortRange: '*'
+ sourceAddressPrefix: '*'
+ destinationAddressPrefix: '*'
+ }
+ }
+ ]
+ }
+}
+
+resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
+ name: vNetName
+ location: location
+ properties: {
+ addressSpace: {
+ addressPrefixes: [virtualNetworkAddressPrefix]
+ }
+ subnets: subnets
+ }
+}
+
+//------------------------------------------------------------------------------
+// Outputs
+//------------------------------------------------------------------------------
+
+output vNetId string = vNet.id
+output vNetName string = vNet.name
+output vNetAddressSpace array = vNet.properties.addressSpace.addressPrefixes
+output vNetSubnets array = vNet.properties.subnets
+output finopsHubSubnetId string = vNet.properties.subnets[0].id
+output scriptSubnetId string = vNet.properties.subnets[1].id