Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: FinOps toolkit changelog
description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more.
author: MSBrett
ms.author: brettwil
ms.date: 06/22/2026
ms.date: 07/07/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
Expand All @@ -25,6 +25,11 @@ This article summarizes the features and enhancements in each release of the Fin
The following section lists features and enhancements that are currently in development.
### [FinOps hubs](hubs/finops-hubs-overview.md)
- **Added**
Comment thread
MSBrett marked this conversation as resolved.
- Added VNet and private network modes, including opt-in NAT Gateway support for private mode; NAT Gateway incurs additional cost when enabled ([#2163](https://github.com/microsoft/finops-toolkit/pull/2163)).
-->

<br><a name="latest"></a>
Expand Down
31 changes: 28 additions & 3 deletions src/powershell/Public/Deploy-FinOpsHub.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,11 @@
.PARAMETER DataExplorerFinalRetentionInMonths
Optional. Number of months of data to retain in the Data Explorer *_final_v* tables. Default: 13.

.PARAMETER EnablePublicAccess
Optional. Enable public access to the data lake. Default: true.
.PARAMETER NetworkMode
Optional. Network mode for the hub: 'public' (default), 'vnet' (private endpoints, default outbound), or 'private' (private endpoints + NAT Gateway, subnets locked down with defaultOutboundAccess=false - required when the 'Subnets should be private' policy is enforced).

.PARAMETER DisablePublicAccess
Optional. Deprecated. Use -NetworkMode 'vnet' or -NetworkMode 'private' instead. When set without -NetworkMode, behaves as -NetworkMode 'vnet'. Ignored when -NetworkMode is supplied.

Comment thread
MSBrett marked this conversation as resolved.
.PARAMETER VirtualNetworkAddressPrefix
Optional. Address space for the workload. A /26 is required for the workload. Default: "10.20.30.0/26".
Expand Down Expand Up @@ -187,6 +190,11 @@ function Deploy-FinOpsHub
[int]
$DataExplorerFinalRetentionInMonths = 13,

[Parameter()]
[ValidateSet('public', 'vnet', 'private')]
[string]
$NetworkMode,

[Parameter()]
[switch]
$DisablePublicAccess,
Expand Down Expand Up @@ -249,6 +257,16 @@ function Deploy-FinOpsHub
# Init deployment (register providers)
Initialize-FinOpsHubDeployment -WhatIf:$WhatIfPreference

# Resolve network mode. -NetworkMode wins; -DisablePublicAccess (deprecated) maps to 'vnet'.
$effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' }
if (-not $NetworkMode -and $DisablePublicAccess) { Write-Warning "-DisablePublicAccess is deprecated; use -NetworkMode 'vnet' or -NetworkMode 'private'." }

# Private network mode (NAT Gateway) requires hub template version 15.0 or later
if ($effectiveNetworkMode -eq 'private' -and $Version -ne 'latest' -and [version]$Version -lt '15.0')
{
throw "NAT Gateway / private network mode requires hub template version 15.0 or later. Current version: $Version"
}

# Download template
if (Test-ShouldProcess $PSCmdlet $Version 'DownloadTemplate')
{
Expand Down Expand Up @@ -281,7 +299,7 @@ function Deploy-FinOpsHub
$parameterSplat.TemplateParameterObject.Add('dataExplorerCapacity', $DataExplorerCapacity)
$parameterSplat.TemplateParameterObject.Add('dataExplorerRawRetentionInDays', $DataExplorerRawRetentionInDays)
$parameterSplat.TemplateParameterObject.Add('dataExplorerFinalRetentionInMonths', $DataExplorerFinalRetentionInMonths)
$parameterSplat.TemplateParameterObject.Add('enablePublicAccess', -not $DisablePublicAccess)
$parameterSplat.TemplateParameterObject.Add('enablePublicAccess', ($effectiveNetworkMode -eq 'public'))
$parameterSplat.TemplateParameterObject.Add('virtualNetworkAddressPrefix', $VirtualNetworkAddressPrefix)
$parameterSplat.TemplateParameterObject.Add('exportRetentionInDays', $ExportRetentionInDays)
$parameterSplat.TemplateParameterObject.Add('ingestionRetentionInMonths', $IngestionRetentionInMonths)
Expand All @@ -304,6 +322,13 @@ function Deploy-FinOpsHub
$parameterSplat.TemplateParameterObject.Add('enablePurgeProtection', $EnablePurgeProtection.IsPresent)
}

# Only pass enableNatGateway when private mode is requested. This keeps public/vnet
# deployments compatible with template versions that predate the parameter.
if ($effectiveNetworkMode -eq 'private' -and ($Version -eq 'latest' -or [version]$Version -ge '15.0'))
{
$parameterSplat.TemplateParameterObject.Add('enableNatGateway', $true)
}

if ($Tags -and $Tags.Keys.Count -gt 0)
{
$parameterSplat.TemplateParameterObject.Add('tags', $Tags)
Expand Down
1 change: 1 addition & 0 deletions src/powershell/Tests/Initialize-Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

Remove-Module FinOpsToolkit -ErrorAction SilentlyContinue
Import-Module -FullyQualifiedName "$PSScriptRoot/../FinOpsToolkit.psm1"
Import-Module Pester -Global -ErrorAction Stop

BeforeAll {
# Bring the Monitor functions in to simplify debugging
Expand Down
2 changes: 1 addition & 1 deletion src/powershell/Tests/Integration/CostExports.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ Describe 'CostExports' {
-Backfill 12

# Assert
Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Write-Progress' -Times 4
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Write-Progress' -Times 4
$success | Should -Be $true
((Get-Date) - $testStartTime).TotalSeconds | Should -BeGreaterThan 60
}
Expand Down
108 changes: 87 additions & 21 deletions src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ InModuleScope 'FinOpsToolkit' {
Deploy-FinOpsHub -WhatIf -Name $hubName -ResourceGroupName $rgName -Location $location

# Assert
Assert-MockCalled -CommandName 'Initialize-FinOpsHubDeployment' -Times 1 -ParameterFilter { $WhatIf -eq $true }
Should -Invoke -CommandName 'Initialize-FinOpsHubDeployment' -Times 1 -ParameterFilter { $WhatIf -eq $true }
@('CreateResourceGroup', 'CreateTempDirectory', 'DownloadTemplate', 'DeployFinOpsHub') | ForEach-Object {
Assert-MockCalled -CommandName 'Test-ShouldProcess' -Times 1 -ParameterFilter { $Action -eq $_ }
Should -Invoke -CommandName 'Test-ShouldProcess' -Times 1 -ParameterFilter { $Action -eq $_ }
}
}
}
Expand All @@ -55,8 +55,8 @@ InModuleScope 'FinOpsToolkit' {
Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location

# Assert
Assert-MockCalled -CommandName 'Get-AzResourceGroup' -Times 1
Assert-MockCalled -CommandName 'New-AzResourceGroup' -Times 1
Should -Invoke -CommandName 'Get-AzResourceGroup' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroup' -Times 1
}

It 'Should use RG if it exists' {
Expand All @@ -69,8 +69,8 @@ InModuleScope 'FinOpsToolkit' {
Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location

# Assert
Assert-MockCalled -CommandName 'Get-AzResourceGroup' -Times 1
Assert-MockCalled -CommandName 'New-AzResourceGroup' -Times 0
Should -Invoke -CommandName 'Get-AzResourceGroup' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroup' -Times 0
}
}

Expand All @@ -85,7 +85,7 @@ InModuleScope 'FinOpsToolkit' {
Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location

# Assert
Assert-MockCalled -CommandName 'Initialize-FinOpsHubDeployment' -Times 1
Should -Invoke -CommandName 'Initialize-FinOpsHubDeployment' -Times 1
}
}

Expand All @@ -111,7 +111,7 @@ InModuleScope 'FinOpsToolkit' {
Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location

# Assert
Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -Times 1
}
It 'Should add tags to the deployment' -Skip {
}
Expand All @@ -130,7 +130,7 @@ InModuleScope 'FinOpsToolkit' {
It 'Should throw if template file is not found' {
Mock -CommandName 'Get-ChildItem'
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Throw
Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1
Should -Invoke -CommandName 'Get-ChildItem' -Times 1
}

Context 'More' {
Expand All @@ -142,14 +142,14 @@ InModuleScope 'FinOpsToolkit' {

It 'Should deploy the template without throwing' {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Not -Throw
Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1
Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { @{ TemplateFile = $templateFile } } -Times 1
Should -Invoke -CommandName 'Get-ChildItem' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { @{ TemplateFile = $templateFile } } -Times 1
}

It 'Should deploy the template with tags' {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Tags @{ Test = 'Tag' } -Version 'latest' } | Should -Not -Throw
Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1
Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
Should -Invoke -CommandName 'Get-ChildItem' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
@{
TemplateParameterObject = @{
tags = @{
Expand All @@ -163,8 +163,8 @@ InModuleScope 'FinOpsToolkit' {
It 'Should deploy the template with StorageSku' {
$storageSku = 'Premium_ZRS'
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -StorageSku $storageSku -Version 'latest' } | Should -Not -Throw
Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1
Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
Should -Invoke -CommandName 'Get-ChildItem' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
@{
TemplateParameterObject = @{
storageSku = $storageSku
Expand All @@ -176,17 +176,17 @@ InModuleScope 'FinOpsToolkit' {
It 'Should deploy the template with RemoteHubStorageUri' {
$remoteHubStorageUri = 'https://primaryhub.dfs.core.windows.net/'
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -RemoteHubStorageUri $remoteHubStorageUri -Version 'latest' } | Should -Not -Throw
Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1
Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
Should -Invoke -CommandName 'Get-ChildItem' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.remoteHubStorageUri -eq $remoteHubStorageUri
} -Times 1
}

It 'Should deploy the template with RemoteHubStorageKey' {
$remoteHubStorageKey = 'abc123...xyz789=='
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -RemoteHubStorageKey $remoteHubStorageKey -Version 'latest' } | Should -Not -Throw
Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1
Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
Should -Invoke -CommandName 'Get-ChildItem' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.remoteHubStorageKey -eq $remoteHubStorageKey
} -Times 1
}
Expand All @@ -195,12 +195,78 @@ InModuleScope 'FinOpsToolkit' {
$remoteHubStorageUri = 'https://primaryhub.dfs.core.windows.net/'
$remoteHubStorageKey = 'abc123...xyz789=='
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -RemoteHubStorageUri $remoteHubStorageUri -RemoteHubStorageKey $remoteHubStorageKey -Version 'latest' } | Should -Not -Throw
Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1
Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
Should -Invoke -CommandName 'Get-ChildItem' -Times 1
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.remoteHubStorageUri -eq $remoteHubStorageUri -and $TemplateParameterObject.remoteHubStorageKey -eq $remoteHubStorageKey
} -Times 1
}
}
}

Context 'Network mode' {
BeforeAll {
Mock -CommandName 'Get-AzResourceGroup' -MockWith { return @{ ResourceGroupName = $rgName } }
Mock -CommandName 'New-AzResourceGroup'
Mock -CommandName 'Save-FinOpsHubTemplate'
Mock -CommandName 'Initialize-FinOpsHubDeployment'
$templateFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'FinOps/finops-hub-v1.0.0/main.bicep'
Mock -CommandName 'Get-ChildItem' -MockWith { return @{ FullName = $templateFile } }
Mock -CommandName 'New-AzResourceGroupDeployment'
}

It 'Should default to public access without a NAT Gateway' {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.enablePublicAccess -eq $true -and -not $TemplateParameterObject.ContainsKey('enableNatGateway')
} -Times 1
}

It "Should map -NetworkMode 'public' to enablePublicAccess and no NAT Gateway" {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'public' -Version 'latest' } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.enablePublicAccess -eq $true -and -not $TemplateParameterObject.ContainsKey('enableNatGateway')
} -Times 1
}

It "Should map -NetworkMode 'vnet' to private access without a NAT Gateway" {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'vnet' -Version 'latest' } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.enablePublicAccess -eq $false -and -not $TemplateParameterObject.ContainsKey('enableNatGateway')
} -Times 1
}

It "Should map -NetworkMode 'private' to private access with a NAT Gateway" {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version 'latest' } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.enablePublicAccess -eq $false -and $TemplateParameterObject.enableNatGateway -eq $true
} -Times 1
}

It 'Should map deprecated -DisablePublicAccess to vnet (no NAT Gateway)' {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -DisablePublicAccess -Version 'latest' -WarningAction 'SilentlyContinue' } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.enablePublicAccess -eq $false -and -not $TemplateParameterObject.ContainsKey('enableNatGateway')
} -Times 1
}

It 'Should let -NetworkMode win over deprecated -DisablePublicAccess' {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -DisablePublicAccess -Version 'latest' } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.enablePublicAccess -eq $false -and $TemplateParameterObject.enableNatGateway -eq $true
} -Times 1
}

It 'Should throw when private mode targets a version older than 15.0' {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version '14.0' } | Should -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -Times 0
}

It 'Should pass enableNatGateway when private mode targets version 15.0 or later' {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version '15.0' } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.enableNatGateway -eq $true
} -Times 1
}
}
}
}
Loading
Loading