Skip to content
Merged
6 changes: 4 additions & 2 deletions docs-mslearn/toolkit/hubs/configure-scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Configure scopes for FinOps hubs
description: Connect FinOps hubs to billing accounts and subscriptions by configuring Cost Management exports manually or give FinOps hubs access to manage exports for you.
author: flanakin
ms.author: micflan
ms.date: 02/24/2026
ms.date: 02/28/2026
ms.topic: how-to
ms.service: finops
ms.subservice: finops-toolkit
Expand Down Expand Up @@ -224,7 +224,9 @@ Managed exports use a managed identity (MI) to configure the exports automatical
- Use the following guides to assign access to each scope you want to monitor:
- EA enrollments – [Assign enrollment reader role permission](/azure/cost-management-billing/manage/assign-roles-azure-service-principals#assign-enrollment-account-role-permission-to-the-spn).
- EA departments – [Assign department reader role permission](/azure/cost-management-billing/manage/assign-roles-azure-service-principals#assign-enrollment-account-role-permission-to-the-spn).
- Subscriptions and resource groups – [Assign Azure roles using the Azure portal](/azure/role-based-access-control/role-assignments-portal).
- Subscriptions and resource groups – [Assign Azure roles using the Azure portal](/azure/role-based-access-control/role-assignments-portal). Assign the following roles to the hub managed identity on each scope:
Comment thread
flanakin marked this conversation as resolved.
Outdated
- **Cost Management Contributor** – create and manage exports.
- **RBAC Administrator** – required by Cost Management to grant itself access to write export data to the hub storage account.
Comment thread
flanakin marked this conversation as resolved.
Outdated

<!--
### Enterprise agreement billing accounts and departments
Expand Down
100 changes: 99 additions & 1 deletion src/scripts/Deploy-Hub.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@
.PARAMETER Location
Optional. Azure location. Default: westus.

.PARAMETER PR
Use PR naming convention. Sets initials to "pr" so resources are named "pr-{name}".
Comment thread
flanakin marked this conversation as resolved.
Outdated

.PARAMETER Scope
Optional. Azure scope ID for cost data exports (e.g., "/subscriptions/{id}"). When specified with -ManagedExports, enables managed exports and grants the hub identity access. When specified without -ManagedExports, creates exports manually via New-FinOpsCostExport after deployment.

.PARAMETER ManagedExports
Optional. Use managed exports instead of manual exports. Requires -Scope. Grants the hub managed identity the required roles on the scope and passes scopesToMonitor to the template.

.PARAMETER Build
Optional. Build the template before deploying.

Expand All @@ -94,6 +103,9 @@ param(
[string]$Fabric,
[switch]$StorageOnly,
[switch]$Remove,
[switch]$PR,
[string]$Scope,
[switch]$ManagedExports,
[string]$Location,
[switch]$Build,
[switch]$WhatIf
Expand Down Expand Up @@ -123,7 +135,14 @@ function Get-Initials()
return $u.Substring(0, [Math]::Min(2, $u.Length))
}

$initials = Get-Initials
if ($PR)
{
$initials = "pr"
}
else
{
$initials = Get-Initials
}

# Default name to "adx" when not specified
if (-not $Name)
Expand Down Expand Up @@ -188,6 +207,12 @@ if ($StorageOnly -and $Fabric)
return
}

if ($ManagedExports -and -not $Scope)
{
Write-Error "-ManagedExports requires -Scope. Provide an Azure scope ID (e.g., '/subscriptions/{id}')."
return
}

# Build parameters
$params = @{}

Expand Down Expand Up @@ -215,6 +240,18 @@ else

Write-Host " Hub: $($params.hubName)"

# Managed exports via template parameters
if ($ManagedExports -and $Scope)
{
$params.enableManagedExports = $true
$params.scopesToMonitor = @($Scope)
Write-Host " Managed exports: $Scope"
}
elseif ($Scope)
{
Comment thread
flanakin marked this conversation as resolved.
Write-Host " Manual exports: $Scope"
}

# Resource group
if (-not $ResourceGroup)
{
Expand All @@ -232,3 +269,64 @@ $deployArgs.Build = $Build
$deployArgs.WhatIf = $WhatIf

& "$PSScriptRoot/Deploy-Toolkit" @deployArgs

#------------------------------------------------------------------------------
# Post-deployment: configure exports
#------------------------------------------------------------------------------

if ($Scope -and -not $WhatIf -and $global:ftkDeployment)
{
$outputs = $global:ftkDeployment.Outputs

if ($ManagedExports)
{
# Grant hub managed identity the required roles on the scope
$managedIdentityId = $outputs["managedIdentityId"].Value
if ($managedIdentityId)
{
Write-Host "Granting hub identity access to $Scope..."
$roles = @("Cost Management Contributor", "RBAC Administrator")
foreach ($role in $roles)
{
$existing = Get-AzRoleAssignment -ObjectId $managedIdentityId -RoleDefinitionName $role -Scope $Scope -ErrorAction SilentlyContinue
if (-not $existing)
{
New-AzRoleAssignment -ObjectId $managedIdentityId -RoleDefinitionName $role -Scope $Scope -ErrorAction SilentlyContinue | Out-Null
Write-Host " Granted: $role"
Comment thread
flanakin marked this conversation as resolved.
Outdated
}
}
}
else
{
Write-Warning "Could not retrieve managedIdentityId from deployment outputs. Grant access manually."
}
}
else
{
# Create exports manually via New-FinOpsCostExport
$storageAccountId = $outputs["storageAccountId"].Value
if ($storageAccountId)
{
Write-Host "Creating manual exports for $Scope..."

# Build the FinOps toolkit PowerShell module if not already loaded
Comment thread
flanakin marked this conversation as resolved.
Outdated
if (-not (Get-Command New-FinOpsCostExport -ErrorAction SilentlyContinue))
{
Import-Module "$PSScriptRoot/../powershell/FinOpsToolkit.psm1" -Force
}

New-FinOpsCostExport -Name "ftk-focuscost" `
-Scope $Scope `
-Dataset "FocusCost" `
-StorageAccountId $storageAccountId `
-StorageContainer "msexports" `
-DoNotOverwrite `
-Execute
Write-Host " Created FocusCost export and triggered initial run."
}
else
{
Write-Warning "Could not retrieve storageAccountId from deployment outputs. Create exports manually."
}
}
}
45 changes: 33 additions & 12 deletions src/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,18 +148,21 @@ By default, deploys with Azure Data Explorer (dev SKU). Use `-StorageOnly` for s

All resources use an `{initials}-{name}` naming convention where initials are pulled from `git config user.name` and name defaults to `adx`. Pass a name as the first positional parameter to use a custom value (e.g., `216` for Feb 16).

| Parameter | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `‑Name` | Optional. First positional parameter. Suffix for `{initials}-{name}` convention. Default: `adx`. |
| `‑HubName` | Optional. Name of the hub instance. Default: `hub`. |
| `‑ADX` | Optional. Name of the Azure Data Explorer cluster. Overrides the `{initials}-{name}` convention. |
| `‑ResourceGroup` | Optional. Name of the resource group. Overrides the `{initials}-{name}` convention. |
| `‑Fabric` | Optional. Deploy with Microsoft Fabric. Provide the eventhouse query URI. |
| `‑StorageOnly` | Optional. Deploy a storage-only hub (no Azure Data Explorer or Fabric). |
| `‑Remove` | Optional. Remove test environments. With a name, deletes the target RG. Alone, lists all `{initials}-*`. |
| `‑Location` | Optional. Azure location. Default: `westus`. |
| `‑Build` | Optional. Build the template before deploying. |
| `‑WhatIf` | Optional. Validate the deployment without making changes. |
| Parameter | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `‑Name` | Optional. First positional parameter. Suffix for `{initials}-{name}` convention. Default: `adx`. |
| `‑HubName` | Optional. Name of the hub instance. Default: `hub`. |
| `‑ADX` | Optional. Name of the Azure Data Explorer cluster. Overrides the `{initials}-{name}` convention. |
| `‑ResourceGroup` | Optional. Name of the resource group. Overrides the `{initials}-{name}` convention. |
| `‑Fabric` | Optional. Deploy with Microsoft Fabric. Provide the eventhouse query URI. |
| `‑StorageOnly` | Optional. Deploy a storage-only hub (no Azure Data Explorer or Fabric). |
| `‑Remove` | Optional. Remove test environments. With a name, deletes the target RG. Alone, lists all `{initials}-*`. |
| `‑PR` | Optional. Use PR naming convention. Sets initials to `pr` so resources are named `pr-{name}`. |
| `‑Scope` | Optional. Azure scope ID for cost data exports (e.g., `/subscriptions/{id}`). With `-ManagedExports`, enables managed exports. Without it, creates exports manually. |
| `‑ManagedExports` | Optional. Use managed exports instead of manual exports. Requires `-Scope`. Passes `scopesToMonitor` to the template and grants the hub identity required roles. |
| `‑Location` | Optional. Azure location. Default: `westus`. |
| `‑Build` | Optional. Build the template before deploying. |
| `‑WhatIf` | Optional. Validate the deployment without making changes. |

Examples:

Expand Down Expand Up @@ -205,6 +208,24 @@ Examples:
./Deploy-Hub -Remove
```

- Deploy with PR naming convention (e.g., RG `pr-123`, ADX `pr-123`):

```powershell
./Deploy-Hub -PR 123
```
Comment thread
flanakin marked this conversation as resolved.

- Deploy with managed exports:

```powershell
./Deploy-Hub -PR 123 -Scope "/subscriptions/{id}" -ManagedExports -Build
```

- Deploy storage-only with manual exports:

```powershell
./Deploy-Hub -StorageOnly -Scope "/subscriptions/{id}" -Build
```

<br>

## 🚀 Deploy-Toolkit
Expand Down