Skip to content
Open
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
138 changes: 138 additions & 0 deletions src/templates/finops-hub/createUiDefinition.json
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,141 @@
}
]
},
{
"name": "recommendations",
"label": "πŸ†• Recommendations",
"elements": [
{
"name": "recommendationsIntro",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "Uncover hidden savings with FinOps hubs recommendations. FinOps hubs can automatically scan your environment using Azure Resource Graph to surface cost optimization opportunities like idle resources and missed discounts that aren't available in Microsoft Cost Management or Azure Advisor."
}
},
{
"name": "enableRecommendations",
"type": "Microsoft.Common.CheckBox",
"label": "Enable hubs recommendations (preview)"
},
{
"name": "included",
"type": "Microsoft.Common.Section",
"label": "Included recommendations",
"visible": "[steps('recommendations').enableRecommendations]",
"elements": [
{
"name": "includedIntro",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "The following recommendations are always included when enabled:"
}
},
{
"name": "advisorCost",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "βœ… Azure Advisor cost recommendations"
}
},
{
"name": "stoppedVMs",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "βœ… Stopped (not deallocated) VMs"
}
},
{
"name": "unattachedDisks",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "βœ… Unattached managed disks"
}
},
{
"name": "unattachedPublicIPs",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "βœ… Unattached static public IPs"
}
},
{
"name": "emptySQLElasticPools",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "βœ… Empty SQL elastic pools"
}
},
{
"name": "backendlessAppGateways",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "βœ… Application Gateways with empty backend pools"
}
},
{
"name": "backendlessLoadBalancers",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "βœ… Load Balancers without backends"
}
}
]
},
{
"name": "optional",
"type": "Microsoft.Common.Section",
"label": "Optional recommendations",
"visible": "[steps('recommendations').enableRecommendations]",
"elements": [
{
"name": "optionalIntro",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "The following recommendations do not apply in all cases and are optional:"
}
},
{
"name": "enableAHBRecommendations",
"type": "Microsoft.Common.CheckBox",
"label": "βž• Azure Hybrid Benefit",
"toolTip": "Flag VMs and SQL VMs without Azure Hybrid Benefit enabled. May generate noise if your organization does not have on-premises licenses."
},
{
"name": "enableSpotRecommendations",
"type": "Microsoft.Common.CheckBox",
"label": "βž• Non-Spot AKS cluster",
"toolTip": "Flag AKS clusters with autoscaling but not using Spot VMs. May generate noise since Spot VMs are only appropriate for interruptible workloads."
}
]
},
{
"name": "permissions",
"type": "Microsoft.Common.Section",
"label": "Required permissions",
"visible": "[steps('recommendations').enableRecommendations]",
"elements": [
{
"name": "permissionsNote",
"type": "Microsoft.Common.TextBlock",
"visible": true,
"options": {
"text": "The Data Factory managed identity requires Reader role on the management groups or subscriptions you want to scan. After deployment, grant the hub's managed identity Reader access to the desired scopes."
}
}
]
}
]
},
{
"name": "advanced",
"label": "Advanced",
Expand Down Expand Up @@ -777,6 +912,9 @@
"fabricCapacity": "[basics('fabric').fabricCapacity]",
"enableInfrastructureEncryption": "[steps('advanced').storage.enableInfrastructureEncryption]",
"enableManagedExports": "[steps('advanced').managedExports.enableManagedExports]",
"enableRecommendations": "[steps('recommendations').enableRecommendations]",
"enableAHBRecommendations": "[steps('recommendations').optional.enableAHBRecommendations]",
"enableSpotRecommendations": "[steps('recommendations').optional.enableSpotRecommendations]",
"enablePublicAccess": "[steps('advanced').networking.enablePublicAccess]",
"virtualNetworkAddressPrefix": "[steps('advanced').networking.virtualNetworkAddressPrefix]",
"dataExplorerSku": "[steps('pricing').dataExplorer.dataExplorerSku]",
Expand Down
12 changes: 12 additions & 0 deletions src/templates/finops-hub/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ param remoteHubStorageKey string = ''
@description('Optional. Enable managed exports where your FinOps hub instance will create and run Cost Management exports on your behalf. Not supported for Microsoft Customer Agreement (MCA) billing profiles. Requires the ability to grant User Access Administrator role to FinOps hubs, which is required to create Cost Management exports. Default: true.')
param enableManagedExports bool = true

@description('Optional. Enable recommendations ingested from Azure Resource Graph based on configurable queries. The Data Factory managed identity requires Reader role on management groups or subscriptions to execute Resource Graph queries. Default: false.')
param enableRecommendations bool = false

@description('Optional. Enable Azure Hybrid Benefit recommendations that flag VMs and SQL VMs without Azure Hybrid Benefit enabled. May generate noise if your organization does not have on-premises licenses. Requires enableRecommendations. Default: false.')
param enableAHBRecommendations bool = false

@description('Optional. Enable non-Spot AKS cluster recommendations that flag AKS clusters with autoscaling but not using Spot VMs. May generate noise since Spot VMs are only appropriate for interruptible workloads. Requires enableRecommendations. Default: false.')
param enableSpotRecommendations bool = false

@description('Optional. Name of the Azure Data Explorer cluster to use for advanced analytics. If empty, Azure Data Explorer will not be deployed. Required to use with Power BI if you have more than $2-5M/mo in costs being monitored. Default: "" (do not use).')
param dataExplorerName string = ''

Expand Down Expand Up @@ -168,6 +177,9 @@ module hub 'modules/hub.bicep' = {
enableInfrastructureEncryption: enableInfrastructureEncryption
enablePurgeProtection: enablePurgeProtection
enableManagedExports: enableManagedExports
enableRecommendations: enableRecommendations
enableAHBRecommendations: enableAHBRecommendations
enableSpotRecommendations: enableSpotRecommendations
dataExplorerName: dataExplorerName
dataExplorerSku: dataExplorerSku
dataExplorerCapacity: dataExplorerCapacity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ param configContainerName string = 'config'
@description('Optional. Name of the ingestion container. Default: ingestion.')
param ingestionContainerName string = 'ingestion'

@description('Optional. Whether to enable Azure Hybrid Benefit recommendations. These recommendations flag VMs and SQL VMs without Azure Hybrid Benefit enabled, which may generate noise if your organization does not have on-premises licenses. Default: false.')
param enableAHBRecommendations bool = false

@description('Optional. Whether to enable non-Spot AKS cluster recommendations. These recommendations flag AKS clusters that use autoscaling without Spot VMs, which may generate noise since Spot VMs are only appropriate for interruptible workloads. Default: false.')
param enableSpotRecommendations bool = false


//==============================================================================
// Variables
Expand All @@ -27,20 +33,29 @@ var QUERIES = 'queries'
// Separator used to separate ingestion ID from file name for ingested files
var ingestionIdFileNameSeparator = '__'

// Load query files
var queryFiles = {
'HubsRecommendations-AdvisorCost': loadTextContent('queries/HubsRecommendations-AdvisorCost.json')
'HubsRecommendations-BackendlessAppGateways': loadTextContent('queries/HubsRecommendations-BackendlessAppGateways.json')
'HubsRecommendations-BackendlessLoadBalancers': loadTextContent('queries/HubsRecommendations-BackendlessLoadBalancers.json')
'HubsRecommendations-EmptySQLElasticPools': loadTextContent('queries/HubsRecommendations-EmptySQLElasticPools.json')
'HubsRecommendations-NonSpotAKSClusters': loadTextContent('queries/HubsRecommendations-NonSpotAKSClusters.json')
'HubsRecommendations-SQLVMsWithoutAHB': loadTextContent('queries/HubsRecommendations-SQLVMsWithoutAHB.json')
'HubsRecommendations-StoppedVMs': loadTextContent('queries/HubsRecommendations-StoppedVMs.json')
'HubsRecommendations-UnattachedDisks': loadTextContent('queries/HubsRecommendations-UnattachedDisks.json')
'HubsRecommendations-UnattachedPublicIPs': loadTextContent('queries/HubsRecommendations-UnattachedPublicIPs.json')
'HubsRecommendations-VMsWithoutAHB': loadTextContent('queries/HubsRecommendations-VMsWithoutAHB.json')
// Load query files -- core recommendations are always included
var coreQueryFiles = {
'Recommendations-Microsoft-AdvisorCost': loadTextContent('queries/Recommendations-Microsoft-AdvisorCost.json')
'Recommendations-Microsoft-BackendlessAppGateways': loadTextContent('queries/Recommendations-Microsoft-BackendlessAppGateways.json')
'Recommendations-Microsoft-BackendlessLoadBalancers': loadTextContent('queries/Recommendations-Microsoft-BackendlessLoadBalancers.json')
'Recommendations-Microsoft-EmptySQLElasticPools': loadTextContent('queries/Recommendations-Microsoft-EmptySQLElasticPools.json')
'Recommendations-Microsoft-StoppedVMs': loadTextContent('queries/Recommendations-Microsoft-StoppedVMs.json')
'Recommendations-Microsoft-UnattachedDisks': loadTextContent('queries/Recommendations-Microsoft-UnattachedDisks.json')
'Recommendations-Microsoft-UnattachedPublicIPs': loadTextContent('queries/Recommendations-Microsoft-UnattachedPublicIPs.json')
}

// Optional recommendations that require opt-in due to potential noise
var ahbQueryFiles = enableAHBRecommendations ? {
'Recommendations-Microsoft-SQLVMsWithoutAHB': loadTextContent('queries/Recommendations-Microsoft-SQLVMsWithoutAHB.json')
'Recommendations-Microsoft-VMsWithoutAHB': loadTextContent('queries/Recommendations-Microsoft-VMsWithoutAHB.json')
} : {}

var spotQueryFiles = enableSpotRecommendations ? {
'Recommendations-Microsoft-NonSpotAKSClusters': loadTextContent('queries/Recommendations-Microsoft-NonSpotAKSClusters.json')
} : {}

var queryFiles = union(coreQueryFiles, ahbQueryFiles, spotQueryFiles)

// Load schema files
var schemaFiles = {
'recommendations_1.0': loadTextContent('schemas/recommendations_1.0.json')
Expand Down Expand Up @@ -95,12 +110,6 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
dependsOn: [appRegistration]
}

// Get storage account instance
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = {
name: app.storage
dependsOn: [appRegistration]
}

//------------------------------------------------------------------------------
// Linked Services
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -133,7 +142,7 @@ resource linkedService_arm 'Microsoft.DataFactory/factories/linkedservices@2018-
//------------------------------------------------------------------------------

// Resource Graph dataset
resource dataset_resourcegraph 'Microsoft.DataFactory/factories/datasets@2018-06-01' = {
resource dataset_resourceGraph 'Microsoft.DataFactory/factories/datasets@2018-06-01' = {
name: 'resourceGraph'
parent: dataFactory
properties: {
Expand Down Expand Up @@ -688,12 +697,12 @@ resource pipeline_ExecuteQueries_query 'Microsoft.DataFactory/factories/pipeline
userProperties: []
typeProperties: {
on: {
value: '@pipeline().parameters.inputDataset'
value: '@toLower(pipeline().parameters.inputDataset)'
type: 'Expression'
}
cases: [
{
value: dataset_resourcegraph.name
value: toLower(dataset_resourceGraph.name) // Case-insensitive: match against toLower() of the input
activities: [
{
name: 'Execute ARG Query'
Expand Down Expand Up @@ -738,7 +747,7 @@ resource pipeline_ExecuteQueries_query 'Microsoft.DataFactory/factories/pipeline
}
inputs: [
{
referenceName: dataset_resourcegraph.name
referenceName: dataset_resourceGraph.name
type: 'DatasetReference'
parameters: {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"dataset": "Recommendations",
"provider": "Microsoft",
"query": "advisorresources | where type == 'microsoft.advisor/recommendations' | where properties.category == 'Cost' | extend x_RecommendationDetails = bag_pack('RecommendationImpact', tostring(properties.impact), 'x_RecommendationProvider', 'Azure Advisor', 'x_RecommendationSolution', tostring(properties.shortDescription.solution), 'x_RecommendationTypeId', tostring(properties.recommendationTypeId), 'x_ResourceType', tolower(properties.impactedField)) | extend x_RecommendationDetails = bag_merge(x_RecommendationDetails, properties.extendedProperties) | project x_RecommendationId=id, x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory=tostring(properties.category), x_RecommendationDescription=tostring(properties.shortDescription.problem), ResourceId=tolower(properties.resourceMetadata.resourceId), ResourceName=tolower(properties.impactedValue), x_RecommendationDetails, x_RecommendationDate=tostring(properties.lastUpdated) | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1",
"queryEngine": "resourceGraph",
"scope": "tenant",
"queryEngine": "ResourceGraph",
"scope": "Tenant",
"source": "Azure Advisor",
"type": "HubsRecommendations-AzureAdvisorCost",
"type": "Microsoft-AdvisorCost",
"version": "1.0"
}
Loading