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