diff --git a/.github/workflows/cleanup-dev-new.yml b/.github/workflows/cleanup-dev-new.yml new file mode 100644 index 0000000..caeea2a --- /dev/null +++ b/.github/workflows/cleanup-dev-new.yml @@ -0,0 +1,36 @@ +name: AOE Clean Up (DEV NEW) +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' # This will run the action every day at midnight +permissions: + id-token: write + contents: read +jobs: + AOE-CLEANUP: + environment: devnew + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/dev' + + steps: + - run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!" + - name: Installing modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts,Az.Resources -Force + - name: Check out repository code + uses: actions/checkout@v3 + - name: Login via Az module + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + - name: Testing PowerShell script call + shell: pwsh + run: | + $aoeResource = Get-AzResource -Id "/subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/${{ secrets.AOE_NAMEPREFIX }}-rg/providers/Microsoft.Automation/automationAccounts/${{ secrets.AOE_NAMEPREFIX }}-auto" -ExpandProperties + if ((Get-Date) -gt $aoeResource.Properties.creationTime.AddDays(${{ vars.AOE_MAXAGEDAYS }})) { Write-Host "Deleting Resource Group"; Remove-AzResourceGroup -Name "${{ secrets.AOE_NAMEPREFIX }}-rg" -Force -AsJob } else { Write-Host "Resource Group is not old enough to delete" } + - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/.github/workflows/continuous-deployment-dev-new.yml b/.github/workflows/continuous-deployment-dev-new.yml new file mode 100644 index 0000000..b24d925 --- /dev/null +++ b/.github/workflows/continuous-deployment-dev-new.yml @@ -0,0 +1,52 @@ +name: AOE Continuous Deployment (DEV NEW) +on: + workflow_dispatch: + push: + branches: + - dev +permissions: + id-token: write + contents: read +jobs: + AOE-CD: + environment: devnew + runs-on: ubuntu-latest + env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }} + AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }} + AOE_LOCATION: ${{ secrets.AOE_LOCATION }} + AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }} + steps: + - run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!" + - name: Installing modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts,Az.Resources,Az.Storage,Az.OperationalInsights,Az.Sql,Az.Automation,Microsoft.Graph.Authentication,Microsoft.Graph.Identity.DirectoryManagement -Force + - name: Check out repository code + uses: actions/checkout@v3 + - name: Login via Az module + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + - name: Create Deployment Settings JSON file + run: | + echo '{ + "SubscriptionId": "'"$AZURE_SUBSCRIPTION_ID"'", + "NamePrefix": "'"$AOE_NAMEPREFIX$(date '+%Y%m%d%H')"'", + "WorkspaceReuse": "n", + "DeployWorkbooks": "y", + "SqlAdmin": "'"$AOE_SQL_ADMIN"'", + "SqlPass": "'"$AOE_SQL_PASSWD"'", + "TargetLocation": "'"$AOE_LOCATION"'", + "DeployBenefitsUsageDependencies": "n" + }' > ./deploymentSettings.json + - name: Testing PowerShell script call + shell: pwsh + run: | + ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/dev/azuredeploy.bicep" + - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/.github/workflows/continuous-deployment-dev.yml b/.github/workflows/continuous-deployment-dev.yml new file mode 100644 index 0000000..b31f0c9 --- /dev/null +++ b/.github/workflows/continuous-deployment-dev.yml @@ -0,0 +1,56 @@ +name: AOE Continuous Deployment (DEV) +on: + workflow_dispatch: + push: + branches: + - dev +permissions: + id-token: write + contents: read +jobs: + AOE-CD: + environment: dev + runs-on: ubuntu-latest + env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }} + AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }} + AOE_LOCATION: ${{ secrets.AOE_LOCATION }} + AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }} + AOE_WORKSPACENAME: ${{ secrets.AOE_WORKSPACENAME }} + AOE_WORKSPACERG: ${{ secrets.AOE_WORKSPACERG }} + steps: + - run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!" + - name: Installing modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts,Az.Resources,Az.Storage,Az.OperationalInsights,Az.Sql,Az.Automation,Microsoft.Graph.Authentication,Microsoft.Graph.Identity.DirectoryManagement -Force + - name: Check out repository code + uses: actions/checkout@v3 + - name: Login via Az module + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + - name: Create Deployment Settings JSON file + run: | + echo '{ + "SubscriptionId": "'"$AZURE_SUBSCRIPTION_ID"'", + "NamePrefix": "'"$AOE_NAMEPREFIX"'", + "WorkspaceReuse": "y", + "WorkspaceName": "'"$AOE_WORKSPACENAME"'", + "WorkspaceResourceGroupName": "'"$AOE_WORKSPACERG"'", + "DeployWorkbooks": "y", + "SqlAdmin": "'"$AOE_SQL_ADMIN"'", + "SqlPass": "'"$AOE_SQL_PASSWD"'", + "TargetLocation": "'"$AOE_LOCATION"'", + "DeployBenefitsUsageDependencies": "n" + }' > ./deploymentSettings.json + - name: Testing PowerShell script call + shell: pwsh + run: | + ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/dev/azuredeploy.bicep" -DoPartialUpgrade + - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 4cacd2b..aa3fd8c 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -15,6 +15,8 @@ jobs: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }} AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }} + AOE_LOCATION: ${{ secrets.AOE_LOCATION }} + AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }} steps: - run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!" - name: Installing modules @@ -35,12 +37,12 @@ jobs: run: | echo '{ "SubscriptionId": "'"$AZURE_SUBSCRIPTION_ID"'", - "NamePrefix": "aoeprdgithub", + "NamePrefix": "'"$AOE_NAMEPREFIX"'", "WorkspaceReuse": "n", "DeployWorkbooks": "y", "SqlAdmin": "'"$AOE_SQL_ADMIN"'", "SqlPass": "'"$AOE_SQL_PASSWD"'", - "TargetLocation": "westeurope", + "TargetLocation": "'"$AOE_LOCATION"'", "DeployBenefitsUsageDependencies": "n" }' > ./deploymentSettings.json - name: Testing PowerShell script call diff --git a/Deploy-AzureOptimizationEngine.ps1 b/Deploy-AzureOptimizationEngine.ps1 index 702dc04..ee234ed 100644 --- a/Deploy-AzureOptimizationEngine.ps1 +++ b/Deploy-AzureOptimizationEngine.ps1 @@ -378,8 +378,13 @@ if (-not($deploymentOptions["ResourceGroupName"])) $resourceGroupName = $resourceGroupNameTemplate -f $namePrefix $storageAccountName = $storageAccountNameTemplate -f $namePrefix $automationAccountName = $automationAccountNameTemplate -f $namePrefix - $sqlServerName = $sqlServerNameTemplate -f $namePrefix - $laWorkspaceName = $laWorkspaceNameTemplate -f $namePrefix + $sqlServerName = $sqlServerNameTemplate -f $namePrefix + if ("Y", "y" -contains $workspaceReuse -and $silentDeploy) { + $laWorkspaceName = $deploymentOptions["WorkspaceName"] + } + else { + $laWorkspaceName = $laWorkspaceNameTemplate -f $namePrefix + } $sqlDatabaseName = "azureoptimization" } @@ -393,7 +398,7 @@ if (-not($deploymentOptions["ResourceGroupName"])) else { # With a silent deploy, overrule any custom resource naming if a NamePrefix is provided - if($silentDeploy -and (![string]::IsNullOrEmpty($namePrefix))) + if($silentDeploy -and ![string]::IsNullOrEmpty($namePrefix) -and $namePrefix -ne "EmptyNamePrefix") { $deploymentName = $deploymentNameTemplate -f $namePrefix $resourceGroupName = $resourceGroupNameTemplate -f $namePrefix diff --git a/README.md b/README.md index d8ce568..bfc5d86 100644 --- a/README.md +++ b/README.md @@ -446,4 +446,4 @@ The Reservations Usage Workbook has a couple of "Unused Reservations" tiles that * **Why is AOE recommending to delete a long-deallocated VM that was already deleted?** Due to the fact that Azure consumption exports are collected with a (default) offset of 7 days, the _LongDeallocatedVms_ recommendation might recommend for deletion a long-deallocated VM that was meanwhile deleted. That false positive should normally disappear in the next iteration. -* **How much does running AOE in my subscription cost?** The default AOE deployment requires cheap Azure resources: an Azure Automation account, an Azure SQL Database and a Storage Account. The costs will depend on the size of your environment, but even in customers with thousands of VMs, the costs do not amount to more than 50 EUR/month. In small to medium-sized environments, it will cost less than 20 EUR/month. **These costs do not include VM performance metrics ingestion into Log Analytics**. \ No newline at end of file +* **How much does running AOE in my subscription cost?** The default AOE deployment requires cheap Azure resources: an Azure Automation account, a small Azure SQL Database and a Storage Account. It also requires a Log Analytics Workspace to which it sends all the logs used by Workbooks. The costs, mostly coming from Azure Automation, depend on the size of your environment, but even in customers with hundreds of VMs, AOE costs do not amount to more than 100 EUR/month. In small to medium-sized environments, it will cost less than 20 EUR/month. **These costs do not include agent-based VM performance metrics ingestion into Log Analytics**. \ No newline at end of file diff --git a/views/workbooks/benefits-simulation.json b/views/workbooks/benefits-simulation.json index 71beeb8..eb3c6a1 100644 --- a/views/workbooks/benefits-simulation.json +++ b/views/workbooks/benefits-simulation.json @@ -92,7 +92,7 @@ { "type": 1, "content": { - "json": "If below tabs are reporting query errors, you must set up Pricesheet exports. See more details [here](https://github.com/helderpinto/AzureOptimizationEngine#enabling-the-reservations-and-benefits-usage-workbooks).", + "json": "If below tabs are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).", "style": "warning" }, "name": "text - 6" diff --git a/views/workbooks/benefits-usage.json b/views/workbooks/benefits-usage.json index 11210c3..868536c 100644 --- a/views/workbooks/benefits-usage.json +++ b/views/workbooks/benefits-usage.json @@ -58,7 +58,7 @@ { "type": 1, "content": { - "json": "If below tabs are reporting query errors, you must set up Pricesheet exports. See more details [here](https://github.com/helderpinto/AzureOptimizationEngine#enabling-the-reservations-and-benefits-usage-workbooks).", + "json": "If below tabs are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).", "style": "warning" }, "name": "text - 8" diff --git a/views/workbooks/identities-roles.json b/views/workbooks/identities-roles.json index 4a2cf6d..127c28a 100644 --- a/views/workbooks/identities-roles.json +++ b/views/workbooks/identities-roles.json @@ -41,7 +41,7 @@ { "type": 1, "content": { - "json": "For full functionality, this workbook requires Microsoft Entra ID exports. If you had not assigned the Global Reader role to the AOE managed identity yet, please do so ([see requirements](https://github.com/helderpinto/AzureOptimizationEngine#requirements)).", + "json": "For full functionality, this workbook requires Microsoft Entra ID exports. If you had not assigned the Global Reader role to the AOE managed identity yet, please do so ([see requirements](https://aka.ms/AzureOptimizationEngine/requirements)).", "style": "info" }, "name": "text - 22" diff --git a/views/workbooks/recommendations.json b/views/workbooks/recommendations.json index 5c8977a..a81741a 100644 --- a/views/workbooks/recommendations.json +++ b/views/workbooks/recommendations.json @@ -641,12 +641,26 @@ "version": "KqlParameterItem/1.0", "name": "Currency", "type": 1, + "description": "The currency to display savings amounts. Must be your Azure consumption agreement currency.", "isRequired": true, "timeContext": { "durationMs": 86400000 }, "value": "EUR" }, + { + "id": "1d6e5f51-df51-4989-90ee-0a014253bbf9", + "version": "KqlParameterItem/1.0", + "name": "USDRate", + "label": "USD Rate", + "type": 1, + "description": "Used to convert some of the Azure Advisor-generated USD savings into your currency. If your currency is USD, set the rate to 1.", + "isRequired": true, + "timeContext": { + "durationMs": 86400000 + }, + "value": "0.93" + }, { "id": "941690c0-8e7f-48fe-b8ad-f668b066bae5", "version": "KqlParameterItem/1.0", @@ -736,7 +750,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\r\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g;\r\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceId_g)\r\n| extend SubscriptionGuid_g = InstanceId_g\r\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\r\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g, InstanceName_s);\r\nAzureOptimizationRecommendationsV1_CL\r\n| where TimeGenerated > beginTime\r\n| where Category == 'Cost'\r\n| where \"'*'\" == \"{Subscriptions}\" or SubscriptionName_s in ({Subscriptions})\r\n| where Impact_s in ({Impact})\r\n| where iif('{TagName:label}' == '' or '{TagValue:label}' == '', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\r\n| where FitScore_d >= {CostFitScore}\r\n| where RecommendationDescription_s in ({CostRecommendations})\r\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\r\n| where isempty(RecommendationSubTypeId_g1)\r\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\r\n| where isempty(RecommendationSubTypeId_g2)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g3)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g4)\r\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\r\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\r\n| summarize sum(SavingsAmount) by SubscriptionName_s", + "query": "let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\r\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g;\r\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceId_g)\r\n| extend SubscriptionGuid_g = InstanceId_g\r\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\r\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g, InstanceName_s);\r\nAzureOptimizationRecommendationsV1_CL\r\n| where TimeGenerated > beginTime\r\n| where Category == 'Cost'\r\n| where \"'*'\" == \"{Subscriptions}\" or SubscriptionName_s in ({Subscriptions})\r\n| where Impact_s in ({Impact})\r\n| where iif('{TagName:label}' == '' or '{TagValue:label}' == '', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\r\n| where FitScore_d >= {CostFitScore}\r\n| where RecommendationDescription_s in ({CostRecommendations})\r\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\r\n| where isempty(RecommendationSubTypeId_g1)\r\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\r\n| where isempty(RecommendationSubTypeId_g2)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g3)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g4)\r\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\r\n| extend Currency = iif(isnotempty(parse_json(AdditionalInfo_s).savingsCurrency), parse_json(AdditionalInfo_s).savingsCurrency, '{Currency}')\r\n| extend SavingsAmount = iif(Currency == 'USD', SavingsAmount*{USDRate}, SavingsAmount)\r\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\r\n| summarize sum(SavingsAmount) by SubscriptionName_s", "size": 4, "title": "Potential cost savings by subscription (in {Currency})", "noDataMessage": "No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.", @@ -783,7 +797,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\r\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g;\r\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceId_g)\r\n| extend SubscriptionGuid_g = InstanceId_g\r\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\r\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g, InstanceName_s);\r\nAzureOptimizationRecommendationsV1_CL\r\n| where TimeGenerated > beginTime\r\n| where Category == 'Cost'\r\n| where \"'*'\" == \"{Subscriptions}\" or SubscriptionName_s in ({Subscriptions})\r\n| where Impact_s in ({Impact})\r\n| where iif('{TagName:label}' == '' or '{TagValue:label}' == '', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\r\n| where FitScore_d >= {CostFitScore}\r\n| where RecommendationDescription_s in ({CostRecommendations})\r\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\r\n| where isempty(RecommendationSubTypeId_g1)\r\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\r\n| where isempty(RecommendationSubTypeId_g2)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g3)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g4)\r\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\r\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\r\n| summarize sum(SavingsAmount) by RecommendationSubType_s", + "query": "let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\r\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g;\r\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceId_g)\r\n| extend SubscriptionGuid_g = InstanceId_g\r\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\r\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g, InstanceName_s);\r\nAzureOptimizationRecommendationsV1_CL\r\n| where TimeGenerated > beginTime\r\n| where Category == 'Cost'\r\n| where \"'*'\" == \"{Subscriptions}\" or SubscriptionName_s in ({Subscriptions})\r\n| where Impact_s in ({Impact})\r\n| where iif('{TagName:label}' == '' or '{TagValue:label}' == '', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\r\n| where FitScore_d >= {CostFitScore}\r\n| where RecommendationDescription_s in ({CostRecommendations})\r\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\r\n| where isempty(RecommendationSubTypeId_g1)\r\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\r\n| where isempty(RecommendationSubTypeId_g2)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g3)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g4)\r\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\r\n| extend Currency = iif(isnotempty(parse_json(AdditionalInfo_s).savingsCurrency), parse_json(AdditionalInfo_s).savingsCurrency, '{Currency}')\r\n| extend SavingsAmount = iif(Currency == 'USD', SavingsAmount*{USDRate}, SavingsAmount)\r\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\r\n| summarize sum(SavingsAmount) by RecommendationSubType_s", "size": 4, "title": "Potential savings by type (in {Currency})", "noDataMessage": "No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.", @@ -830,7 +844,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\r\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g;\r\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceId_g)\r\n| extend SubscriptionGuid_g = InstanceId_g\r\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\r\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g, InstanceName_s);\r\nAzureOptimizationRecommendationsV1_CL\r\n| where TimeGenerated > beginTime\r\n| where Category == 'Cost'\r\n| where \"'*'\" == \"{Subscriptions}\" or SubscriptionName_s in ({Subscriptions})\r\n| where Impact_s in ({Impact})\r\n| where iif('{TagName:label}' == '' or '{TagValue:label}' == '', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\r\n| where FitScore_d >= {CostFitScore}\r\n| where RecommendationDescription_s in ({CostRecommendations})\r\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\r\n| where isempty(RecommendationSubTypeId_g1)\r\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\r\n| where isempty(RecommendationSubTypeId_g2)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g3)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g4)\r\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\r\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\r\n| extend Origin = iif(RecommendationSubType_s startswith \"Advisor\", \"Advisor\", \"Custom\")\r\n| summarize sum(SavingsAmount) by Origin", + "query": "let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\r\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g;\r\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceId_g)\r\n| extend SubscriptionGuid_g = InstanceId_g\r\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\r\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g, InstanceName_s);\r\nAzureOptimizationRecommendationsV1_CL\r\n| where TimeGenerated > beginTime\r\n| where Category == 'Cost'\r\n| where \"'*'\" == \"{Subscriptions}\" or SubscriptionName_s in ({Subscriptions})\r\n| where Impact_s in ({Impact})\r\n| where iif('{TagName:label}' == '' or '{TagValue:label}' == '', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\r\n| where FitScore_d >= {CostFitScore}\r\n| where RecommendationDescription_s in ({CostRecommendations})\r\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\r\n| where isempty(RecommendationSubTypeId_g1)\r\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\r\n| where isempty(RecommendationSubTypeId_g2)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g3)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g4)\r\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\r\n| extend Currency = iif(isnotempty(parse_json(AdditionalInfo_s).savingsCurrency), parse_json(AdditionalInfo_s).savingsCurrency, '{Currency}')\r\n| extend SavingsAmount = iif(Currency == 'USD', SavingsAmount*{USDRate}, SavingsAmount)\r\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\r\n| extend Origin = iif(RecommendationSubType_s startswith \"Advisor\", \"Advisor\", \"Custom\")\r\n| summarize sum(SavingsAmount) by Origin", "size": 4, "title": "Potential savings by origin (in {Currency})", "noDataMessage": "No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.", @@ -877,7 +891,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\r\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g;\r\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceId_g)\r\n| extend SubscriptionGuid_g = InstanceId_g\r\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\r\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g, InstanceName_s);\r\nAzureOptimizationRecommendationsV1_CL\r\n| where TimeGenerated > beginTime\r\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\r\n| where isempty(RecommendationSubTypeId_g1)\r\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\r\n| where isempty(RecommendationSubTypeId_g2)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g3)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g4)\r\n| where Category == 'Cost'\r\n| where \"'*'\" == \"{Subscriptions}\" or SubscriptionName_s in ({Subscriptions})\r\n| where Impact_s in ({Impact})\r\n| where iif('{TagName:label}' == '' or '{TagValue:label}' == '', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\r\n| where FitScore_d >= {CostFitScore}\r\n| where RecommendationDescription_s in ({CostRecommendations})\r\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\r\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\r\n| project Recommendation=RecommendationDescription_s, Instance=InstanceName_s, ['Resource Group']=ResourceGroup, Subscription=SubscriptionName_s, Impact=Impact_s, ['Fit Score']=FitScore_d, ['Savings ({Currency})']=SavingsAmount, Tags=parse_json(Tags_s), ['Additional Info']=parse_json(AdditionalInfo_s), Details=DetailsURL_s\r\n| order by ['Savings ({Currency})']", + "query": "let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\r\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g;\r\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceId_g)\r\n| extend SubscriptionGuid_g = InstanceId_g\r\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\r\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \r\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\r\n| where FilterType_s in ('Snooze','Dismiss')\r\n| where isnotempty(InstanceName_s)\r\n| project RecommendationSubTypeId_g, InstanceName_s);\r\nAzureOptimizationRecommendationsV1_CL\r\n| where TimeGenerated > beginTime\r\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\r\n| where isempty(RecommendationSubTypeId_g1)\r\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\r\n| where isempty(RecommendationSubTypeId_g2)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g3)\r\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\r\n| where isempty(RecommendationSubTypeId_g4)\r\n| where Category == 'Cost'\r\n| where \"'*'\" == \"{Subscriptions}\" or SubscriptionName_s in ({Subscriptions})\r\n| where Impact_s in ({Impact})\r\n| where iif('{TagName:label}' == '' or '{TagValue:label}' == '', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\r\n| where FitScore_d >= {CostFitScore}\r\n| where RecommendationDescription_s in ({CostRecommendations})\r\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\r\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\r\n| extend Currency = iif(isnotempty(parse_json(AdditionalInfo_s).savingsCurrency), parse_json(AdditionalInfo_s).savingsCurrency, '{Currency}')\r\n| extend SavingsAmount = iif(Currency == 'USD', SavingsAmount*{USDRate}, SavingsAmount)\r\n| project Recommendation=RecommendationDescription_s, Instance=InstanceName_s, ['Resource Group']=ResourceGroup, Subscription=SubscriptionName_s, Impact=Impact_s, ['Fit Score']=FitScore_d, ['Savings ({Currency})']=SavingsAmount, Tags=parse_json(Tags_s), ['Additional Info']=parse_json(AdditionalInfo_s), Details=DetailsURL_s\r\n| order by ['Savings ({Currency})']", "size": 0, "noDataMessage": "No Cost recommendations available", "showExportToExcel": true, diff --git a/views/workbooks/reservations-potential.json b/views/workbooks/reservations-potential.json index 0323e7c..8464d79 100644 --- a/views/workbooks/reservations-potential.json +++ b/views/workbooks/reservations-potential.json @@ -80,7 +80,7 @@ { "type": 1, "content": { - "json": "If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://github.com/helderpinto/AzureOptimizationEngine#enabling-the-reservations-and-benefits-usage-workbooks).", + "json": "If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).", "style": "warning" }, "name": "text - 5" diff --git a/views/workbooks/reservations-usage.json b/views/workbooks/reservations-usage.json index 1b43b30..0e0e6a6 100644 --- a/views/workbooks/reservations-usage.json +++ b/views/workbooks/reservations-usage.json @@ -137,7 +137,7 @@ { "type": 1, "content": { - "json": "If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://github.com/helderpinto/AzureOptimizationEngine#enabling-the-reservations-and-benefits-usage-workbooks).", + "json": "If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).", "style": "warning" }, "name": "text - 10" diff --git a/views/workbooks/savingsplans-usage.json b/views/workbooks/savingsplans-usage.json index 1b586d8..3e710d2 100644 --- a/views/workbooks/savingsplans-usage.json +++ b/views/workbooks/savingsplans-usage.json @@ -97,7 +97,7 @@ { "type": 1, "content": { - "json": "If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://github.com/helderpinto/AzureOptimizationEngine#enabling-the-reservations-and-benefits-usage-workbooks).", + "json": "If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).", "style": "warning" }, "name": "text - 10"