Showing preview only (1,987K chars total). Download the full file or copy to clipboard to get everything.
Repository: helderpinto/AzureOptimizationEngine
Branch: master
Commit: 143b27eac1ea
Files: 107
Total size: 1.9 MB
Directory structure:
gitextract_09u_p9cv/
├── .github/
│ └── workflows/
│ ├── continuous-deployment-dev-new.yml
│ ├── continuous-deployment-dev.yml
│ └── continuous-deployment.yml
├── .gitignore
├── Deploy-AzureOptimizationEngine.ps1
├── LICENSE
├── README.md
├── Reset-AutomationSchedules.ps1
├── Setup-BenefitsUsageDependencies.ps1
├── Setup-DataCollectionRules.ps1
├── Setup-LogAnalyticsWorkspaces.ps1
├── Suppress-Recommendation.ps1
├── azuredeploy-nested.bicep
├── azuredeploy.bicep
├── custom-recommendations-types.json
├── docs/
│ ├── configuring-workspaces.md
│ ├── customizing-aoe.md
│ └── suppressing-recommendations.md
├── model/
│ ├── filters-table.sql
│ ├── loganalyticsingestcontrol-initialize.sql
│ ├── loganalyticsingestcontrol-table.sql
│ ├── loganalyticsingestcontrol-upgrade.sql
│ ├── recommendations-sp.sql
│ ├── recommendations-table.sql
│ ├── sqlserveringestcontrol-initialize.sql
│ └── sqlserveringestcontrol-table.sql
├── perfcounters.json
├── queries/
│ ├── rbac-spns-keys-expiring.kql
│ ├── rbac-spns-roles-aad-all.kql
│ ├── rbac-spns-roles-arm-all.kql
│ ├── rbac-spns-roles-arm-privileged.kql
│ ├── rbac-users-roles-aad-all.kql
│ ├── rbac-users-roles-arm-privileged.kql
│ ├── rbac-users-roles-guests-privileged.kql
│ └── rbac-users-roles-guests.kql
├── runbooks/
│ ├── data-collection/
│ │ ├── Export-AADObjectsToBlobStorage.ps1
│ │ ├── Export-ARGAppGatewayPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGAppServicePlanPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGLoadBalancerPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGManagedDisksPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGNICPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGNSGPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGPublicIpPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGResourceContainersPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGSqlDatabasePropertiesToBlobStorage.ps1
│ │ ├── Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGVMSSPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGVNetPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1
│ │ ├── Export-AdvisorRecommendationsToBlobStorage.ps1
│ │ ├── Export-AzMonitorMetricsToBlobStorage.ps1
│ │ ├── Export-ConsumptionToBlobStorage.ps1
│ │ ├── Export-PolicyComplianceToBlobStorage.ps1
│ │ ├── Export-PriceSheetToBlobStorage.ps1
│ │ ├── Export-RBACAssignmentsToBlobStorage.ps1
│ │ ├── Export-ReservationsPriceToBlobStorage.ps1
│ │ ├── Export-ReservationsUsageToBlobStorage.ps1
│ │ ├── Export-SavingsPlansUsageToBlobStorage.ps1
│ │ └── Ingest-OptimizationCSVExportsToLogAnalytics.ps1
│ ├── maintenance/
│ │ └── CleanUp-OlderRecommendationsFromSqlServer.ps1
│ ├── recommendations/
│ │ ├── Ingest-RecommendationsToLogAnalytics.ps1
│ │ ├── Ingest-RecommendationsToSQLServer.ps1
│ │ ├── Ingest-SuppressionsToLogAnalytics.ps1
│ │ ├── Recommend-AADExpiringCredentialsToBlobStorage.ps1
│ │ ├── Recommend-ARMOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-AdvisorAsIsToBlobStorage.ps1
│ │ ├── Recommend-AdvisorCostAugmentedToBlobStorage.ps1
│ │ ├── Recommend-AppServiceOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-DiskOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-SqlDbOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-StorageAccountOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-UnattachedDisksToBlobStorage.ps1
│ │ ├── Recommend-UnusedAppGWsToBlobStorage.ps1
│ │ ├── Recommend-UnusedLoadBalancersToBlobStorage.ps1
│ │ ├── Recommend-VMOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-VMSSOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-VMsHighAvailabilityToBlobStorage.ps1
│ │ └── Recommend-VNetOptimizationsToBlobStorage.ps1
│ └── remediations/
│ ├── Remediate-AdvisorRightSizeFiltered.ps1
│ ├── Remediate-LongDeallocatedVMsFiltered.ps1
│ └── Remediate-UnattachedDisksFiltered.ps1
├── upgrade-manifest.json
└── views/
├── AzureOptimizationEngine.pbix
├── powerbi-query.m
└── workbooks/
├── benefits-simulation.bicep
├── benefits-simulation.json
├── benefits-usage.bicep
├── benefits-usage.json
├── blockblobstorage-usage.bicep
├── blockblobstorage-usage.json
├── costs-growing.bicep
├── costs-growing.json
├── identities-roles.bicep
├── identities-roles.json
├── policy-compliance.bicep
├── policy-compliance.json
├── recommendations.bicep
├── recommendations.json
├── reservations-potential.bicep
├── reservations-potential.json
├── reservations-usage.bicep
├── reservations-usage.json
├── resources-inventory.bicep
├── resources-inventory.json
├── savingsplans-usage.bicep
└── savingsplans-usage.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/continuous-deployment-dev-new.yml
================================================
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@hf_447_release
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 }}."
================================================
FILE: .github/workflows/continuous-deployment-dev.yml
================================================
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@hf_447_release
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 }}."
================================================
FILE: .github/workflows/continuous-deployment.yml
================================================
name: AOE Continuous Deployment (PROD)
on:
workflow_dispatch:
push:
branches:
- master
permissions:
id-token: write
contents: read
jobs:
AOE-CD:
environment: prod
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@hf_447_release
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": "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
- run: echo "🍏 This job's status is ${{ job.status }}."
================================================
FILE: .gitignore
================================================
# Deployment state file
last-deployment-state.json
# Database connection settings (for the Suppress-Recommendation.ps1 helper script)
database-connection-settings.json
# Silent deployment settings file
deployment-settings-*.json
================================================
FILE: Deploy-AzureOptimizationEngine.ps1
================================================
param (
[Parameter(Mandatory = $false)]
[string] $TemplateUri,
[Parameter(Mandatory = $false)]
[string] $AzureEnvironment = "AzureCloud",
[Parameter(Mandatory = $false)]
[switch] $DoPartialUpgrade,
[Parameter(Mandatory = $false)]
[switch] $IgnoreNamingAvailabilityErrors,
[Parameter(Mandatory = $false)]
[string] $SilentDeploymentSettingsPath,
[Parameter(Mandatory = $false)]
[hashtable] $ResourceTags = @{}
)
function ConvertTo-Hashtable {
[CmdletBinding()]
[OutputType('hashtable')]
param (
[Parameter(ValueFromPipeline)]
$InputObject
)
process {
if ($null -eq $InputObject) {
return $null
}
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
$collection = @(
foreach ($object in $InputObject) {
ConvertTo-Hashtable -InputObject $object
}
)
Write-Output -NoEnumerate $collection
} elseif ($InputObject -is [psobject]) {
$hash = @{}
foreach ($property in $InputObject.PSObject.Properties) {
$hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value
}
$hash
} else {
$InputObject
}
}
}
function Test-SqlPasswordComplexity {
param (
[string]$Username,
[string]$Password
)
# Check if the username is present in the password
if ($Password -match $Username) {
throw "SQL password cannot contain the SQL username."
return $false
}
# Password must be minimum 8 characters, contains at least one uppercase, lowercase letter, contains at least one digit, contains at least one special character
$regex = '^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).{8,}$'
if ($Password -match $regex) {
Write-Host "SQL password is valid." -ForegroundColor Green
return $true
} else {
throw "Password does not meet the complexity requirements."
return $false
}
}
$ErrorActionPreference = "Stop"
#region Deployment environment settings
$lastDeploymentStatePath = ".\last-deployment-state.json"
$deploymentOptions = @{}
$silentDeploy = $false
# Check if silent deployment settings file exists
if(-not([string]::IsNullOrEmpty($SilentDeploymentSettingsPath)) -and (Test-Path -Path $SilentDeploymentSettingsPath))
{
$silentDeploy = $true
# Get the deployment details from the silent deployment settings file
$silentDepOptions = Get-Content -Path $SilentDeploymentSettingsPath | ConvertFrom-Json
Write-Host "Silent deployment options found." -ForegroundColor Green
$silentDepOptions = ConvertTo-Hashtable -InputObject $silentDepOptions
$silentDepOptions.Keys | ForEach-Object {
$deploymentOptions[$_] = $silentDepOptions[$_]
}
# Validate the silent deployment settings
if (-not($deploymentOptions["SubscriptionId"]))
{
throw "SubscriptionId is required for silent deployment."
}
if (-not($deploymentOptions["NamePrefix"]))
{
throw "NamePrefix is required for silent deployment. Set to 'EmptyNamePrefix' to use own naming convention and specify the needed resource names."
}
if ($deploymentOptions["NamePrefix"].Length -gt 21) {
throw "Name prefix length is larger than the 21 characters limit ($($deploymentOptions["NamePrefix"]))"
}
if ($deploymentOptions["NamePrefix"] -eq "EmptyNamePrefix")
{
if (-not($deploymentOptions["ResourceGroupName"]))
{
throw "ResourceGroupName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
}
if (-not($deploymentOptions["StorageAccountName"]))
{
throw "StorageAccountName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
}
if (-not($deploymentOptions["AutomationAccountName"]))
{
throw "AutomationAccountName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
}
if (-not($deploymentOptions["SqlServerName"]))
{
throw "SqlServerName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
}
if (-not($deploymentOptions["SqlDatabaseName"]))
{
throw "SqlDatabaseName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
}
}
if (-not($deploymentOptions["WorkspaceReuse"]) -or ($deploymentOptions["WorkspaceReuse"] -ne "y" -and $deploymentOptions["WorkspaceReuse"] -ne "n"))
{
throw "WorkspaceReuse set to 'y' or 'n' is required for silent deployment."
}
if ($deploymentOptions["WorkspaceReuse"] -eq "y")
{
if (-not($deploymentOptions["WorkspaceName"]))
{
throw "WorkspaceName is required for silent deployment when WorkspaceReuse is set to 'y'."
}
if (-not($deploymentOptions["WorkspaceResourceGroupName"]))
{
throw "WorkspaceResourceGroupName is required for silent deployment when WorkspaceReuse is set to 'y'."
}
}
if (-not($deploymentOptions["DeployWorkbooks"]) -or ($deploymentOptions["DeployWorkbooks"] -ne "y" -and $deploymentOptions["DeployWorkbooks"] -ne "n"))
{
throw "DeployWorkbooks set to 'y' or 'n' is required for silent deployment."
}
if (-not($deploymentOptions["SqlAdmin"]))
{
throw "SqlAdmin is required for silent deployment."
}
if (-not($deploymentOptions["SqlPass"]))
{
throw "SqlPass is required for silent deployment."
}
if (-not($deploymentOptions["TargetLocation"]))
{
throw "TargetLocation is required for silent deployment."
}
if (-not($deploymentOptions["DeployBenefitsUsageDependencies"]))
{
throw "DeployBenefitsUsageDependencies is required for silent deployment."
}
if ($deploymentOptions["DeployBenefitsUsageDependencies"] -eq "y")
{
if (-not($deploymentOptions["CustomerType"]))
{
throw "CustomerType is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'."
}
if (-not($deploymentOptions["BillingAccountId"]))
{
throw "BillingAccountId is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'."
}
if (-not($deploymentOptions["CurrencyCode"]))
{
throw "CurrencyCode is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'."
}
if ($deploymentOptions["CustomerType"] -eq "MCA")
{
if (-not($deploymentOptions["BillingProfileId"]))
{
throw "BillingProfileId is required for silent deployment when CustomerType is set to 'MCA'."
}
}
}
}
if ((Test-Path -Path $lastDeploymentStatePath) -and !$silentDeploy)
{
$depOptions = Get-Content -Path $lastDeploymentStatePath | ConvertFrom-Json
Write-Host $depOptions -ForegroundColor Green
$depOptionsReuse = Read-Host "Found last deployment options above. Do you want to repeat/upgrade last deployment (Y/N)?"
if ("Y", "y" -contains $depOptionsReuse)
{
foreach ($property in $depOptions.PSObject.Properties)
{
$deploymentOptions[$property.Name] = $property.Value
}
}
}
$GitHubOriginalUri = "https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/master/azuredeploy.bicep"
if ([string]::IsNullOrEmpty($TemplateUri)) {
$TemplateUri = $GitHubOriginalUri
}
$isTemplateAvailable = $false
try {
Invoke-WebRequest -Uri $TemplateUri | Out-Null
$isTemplateAvailable = $true
}
catch {
Write-Host "The template URL ($TemplateUri) is not available. Please, put it in a publicly accessible HTTPS location." -ForegroundColor Red
}
if (!$isTemplateAvailable) {
throw "Terminating due to template unavailability."
}
if (-not((Test-Path -Path "./azuredeploy.bicep") -and (Test-Path -Path "./azuredeploy-nested.bicep"))) {
throw "Terminating due to template unavailability. Please, change directory to where azuredeploy.bicep and azuredeploy-nested.bicep are located."
}
$cloudDetails = Get-AzEnvironment -Name $AzureEnvironment
$ctx = Get-AzContext
if (-not($ctx)) {
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
else {
if ($ctx.Environment.Name -ne $AzureEnvironment) {
Disconnect-AzAccount -ContextName $ctx.Name
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
}
#endregion
#region Azure subscription choice
Write-Host "Getting Azure subscriptions..." -ForegroundColor Yellow
$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" -and $_.SubscriptionPolicies.QuotaId -notlike "AAD*" }
if ($subscriptions.Count -gt 1) {
$selectedSubscription = -1
for ($i = 0; $i -lt $subscriptions.Count; $i++)
{
if (-not($deploymentOptions["SubscriptionId"]))
{
Write-Output "[$i] $($subscriptions[$i].Name)"
}
else
{
if ($subscriptions[$i].Id -eq $deploymentOptions["SubscriptionId"])
{
$selectedSubscription = $i
break
}
}
}
if (-not($deploymentOptions["SubscriptionId"]))
{
$lastSubscriptionIndex = $subscriptions.Count - 1
while ($selectedSubscription -lt 0 -or $selectedSubscription -gt $lastSubscriptionIndex) {
Write-Output "---"
$selectedSubscription = [int] (Read-Host "Please, select the target subscription for this deployment [0..$lastSubscriptionIndex]")
}
}
if ($selectedSubscription -eq -1)
{
throw "The selected subscription does not exist. Check if you are logged in with the right Microsoft Entra ID user."
}
}
else
{
if ($subscriptions.Count -ne 0)
{
$selectedSubscription = 0
}
else
{
throw "No valid subscriptions found. Only EA, MCA, PAYG or MSDN subscriptions are supported currently."
}
}
if ($subscriptions.Count -eq 0) {
throw "No subscriptions found. Check if you are logged in with the right Microsoft Entra ID account."
}
$subscriptionId = $subscriptions[$selectedSubscription].Id
if (-not($deploymentOptions["SubscriptionId"]))
{
$deploymentOptions["SubscriptionId"] = $subscriptionId
}
if ($ctx.Subscription.Id -ne $subscriptionId) {
$ctx = Select-AzSubscription -SubscriptionId $subscriptionId
}
#endregion
#region Resource naming options
if($silentDeploy)
{
$workspaceReuse = $deploymentOptions["WorkspaceReuse"]
}
else {
$workspaceReuse = $null
}
$deploymentNameTemplate = "{0}" + (Get-Date).ToString("yyMMddHHmmss")
$resourceGroupNameTemplate = "{0}-rg"
$storageAccountNameTemplate = "{0}sa"
$laWorkspaceNameTemplate = "{0}-la"
$automationAccountNameTemplate = "{0}-auto"
$sqlServerNameTemplate = "{0}-sql"
$nameAvailable = $true
if (-not($deploymentOptions["NamePrefix"]))
{
do
{
$namePrefix = Read-Host "Please, enter a unique name prefix for the deployment (max. 21 chars) or existing prefix if updating deployment. If you want instead to individually name all resources, just press ENTER"
if (-not($namePrefix))
{
$namePrefix = "EmptyNamePrefix"
}
}
while ($namePrefix.Length -gt 21)
$deploymentOptions["NamePrefix"] = $namePrefix
}
else {
if ($deploymentOptions["NamePrefix"] -eq "EmptyNamePrefix")
{
$namePrefix = $null
}
else
{
$namePrefix = $deploymentOptions["NamePrefix"]
}
}
if (-not($deploymentOptions["WorkspaceReuse"]))
{
if ($null -eq $workspaceReuse) {
$workspaceReuse = Read-Host "Are you going to reuse an existing Log Analytics workspace (Y/N)?"
}
$deploymentOptions["WorkspaceReuse"] = $workspaceReuse
}
else
{
$workspaceReuse = $deploymentOptions["WorkspaceReuse"]
}
if (-not($deploymentOptions["ResourceGroupName"]))
{
if ([string]::IsNullOrEmpty($namePrefix) -or $namePrefix -eq "EmptyNamePrefix") {
$resourceGroupName = Read-Host "Please, enter the new or existing Resource Group for this deployment"
$deploymentName = $deploymentNameTemplate -f $resourceGroupName
$storageAccountName = Read-Host "Enter the Storage Account name"
$automationAccountName = Read-Host "Automation Account name"
$sqlServerName = Read-Host "Azure SQL Server name"
$sqlDatabaseName = Read-Host "Azure SQL Database name"
if ("N", "n" -contains $workspaceReuse) {
$laWorkspaceName = Read-Host "Log Analytics Workspace"
}
}
else {
$deploymentName = $deploymentNameTemplate -f $namePrefix
$resourceGroupName = $resourceGroupNameTemplate -f $namePrefix
$storageAccountName = $storageAccountNameTemplate -f $namePrefix
$automationAccountName = $automationAccountNameTemplate -f $namePrefix
$sqlServerName = $sqlServerNameTemplate -f $namePrefix
if ("Y", "y" -contains $workspaceReuse -and $silentDeploy) {
$laWorkspaceName = $deploymentOptions["WorkspaceName"]
}
else {
$laWorkspaceName = $laWorkspaceNameTemplate -f $namePrefix
}
$sqlDatabaseName = "azureoptimization"
}
$deploymentOptions["ResourceGroupName"] = $resourceGroupName
$deploymentOptions["StorageAccountName"] = $storageAccountName
$deploymentOptions["AutomationAccountName"] = $automationAccountName
$deploymentOptions["SqlServerName"] = $sqlServerName
$deploymentOptions["SqlDatabaseName"] = $sqlDatabaseName
$deploymentOptions["WorkspaceName"] = $laWorkspaceName
}
else
{
# With a silent deploy, overrule any custom resource naming if a NamePrefix is provided
if($silentDeploy -and ![string]::IsNullOrEmpty($namePrefix) -and $namePrefix -ne "EmptyNamePrefix")
{
$deploymentName = $deploymentNameTemplate -f $namePrefix
$resourceGroupName = $resourceGroupNameTemplate -f $namePrefix
$storageAccountName = $storageAccountNameTemplate -f $namePrefix
$automationAccountName = $automationAccountNameTemplate -f $namePrefix
$sqlServerName = $sqlServerNameTemplate -f $namePrefix
if ("Y", "y" -contains $workspaceReuse) {
$laWorkspaceName = $deploymentOptions["WorkspaceName"]
}
else {
$laWorkspaceName = $laWorkspaceNameTemplate -f $namePrefix
}
$sqlDatabaseName = "azureoptimization"
}
else {
$resourceGroupName = $deploymentOptions["ResourceGroupName"]
$storageAccountName = $deploymentOptions["StorageAccountName"]
$automationAccountName = $deploymentOptions["AutomationAccountName"]
$sqlServerName = $deploymentOptions["SqlServerName"]
$sqlDatabaseName = $deploymentOptions["SqlDatabaseName"]
$laWorkspaceName = $deploymentOptions["WorkspaceName"]
$deploymentName = $deploymentNameTemplate -f $resourceGroupName
}
}
#endregion
#region Resource naming availability checks
Write-Host "Checking name prefix availability..." -ForegroundColor Green
Write-Host "...for the Storage Account..." -ForegroundColor Green
$sa = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName -ErrorAction SilentlyContinue
if ($null -eq $sa) {
$saNameResult = Get-AzStorageAccountNameAvailability -Name $storageAccountName
if (-not($saNameResult.NameAvailable)) {
$nameAvailable = $false
Write-Host "$($saNameResult.Message)" -ForegroundColor Red
}
}
else {
Write-Host "(The Storage Account was already deployed)" -ForegroundColor Green
}
if ("N", "n" -contains $workspaceReuse) {
Write-Host "...for the Log Analytics workspace..." -ForegroundColor Green
$logAnalyticsReuse = $false
$laWorkspaceResourceGroup = $resourceGroupName
$la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $resourceGroupName -Name $laWorkspaceName -ErrorAction SilentlyContinue
if ($null -eq $la) {
$laNameResult = Invoke-WebRequest -Uri "https://portal.loganalytics.io/api/workspaces/IsWorkspaceExists?name=$laWorkspaceName"
if ($laNameResult.Content -eq "true") {
$nameAvailable = $false
Write-Host "The Log Analytics workspace $laWorkspaceName is already taken." -ForegroundColor Red
}
}
else {
Write-Host "(The Log Analytics Workspace was already deployed)" -ForegroundColor Green
}
}
else {
$logAnalyticsReuse = $true
}
Write-Host "...for the Azure SQL Server..." -ForegroundColor Green
$sql = Get-AzSqlServer -ResourceGroupName $resourceGroupName -Name $sqlServerName -ErrorAction SilentlyContinue
if ($null -eq $sql -and -not($sqlServerName -like "*.database.*") -and -not($IgnoreNamingAvailabilityErrors)) {
$SqlServerNameAvailabilityUriPath = "/subscriptions/$subscriptionId/providers/Microsoft.Sql/checkNameAvailability?api-version=2014-04-01"
$body = "{`"name`": `"$sqlServerName`", `"type`": `"Microsoft.Sql/servers`"}"
$sqlNameResult = (Invoke-AzRestMethod -Path $SqlServerNameAvailabilityUriPath -Method POST -Payload $body).Content | ConvertFrom-Json
if (-not($sqlNameResult.available)) {
$nameAvailable = $false
Write-Host "$($sqlNameResult.message) ($sqlServerName)" -ForegroundColor Red
}
}
else {
Write-Host "(The SQL Server was already deployed)" -ForegroundColor Green
}
if (-not($nameAvailable) -and -not($IgnoreNamingAvailabilityErrors))
{
throw "Please, fix naming issues. Terminating execution."
}
Write-Host "Chosen resource names are available for all services" -ForegroundColor Green
#endregion
#region Additional resource options (LA reused, region, SQL user)
if (-not($deploymentOptions["WorkspaceResourceGroupName"]))
{
if ("Y", "y" -contains $workspaceReuse) {
$laWorkspaceName = Read-Host "Please, enter the name of the Log Analytics workspace to be reused"
$laWorkspaceResourceGroup = Read-Host "Please, enter the name of the resource group containing Log Analytics $laWorkspaceName"
$la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $laWorkspaceResourceGroup -Name $laWorkspaceName -ErrorAction SilentlyContinue
if (-not($la)) {
throw "Could not find $laWorkspaceName in resource group $laWorkspaceResourceGroup for the chosen subscription. Aborting."
}
$deploymentOptions["WorkspaceName"] = $laWorkspaceName
$deploymentOptions["WorkspaceResourceGroupName"] = $laWorkspaceResourceGroup
}
}
else
{
$laWorkspaceResourceGroup = $deploymentOptions["WorkspaceResourceGroupName"]
}
$rg = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue
if (-not($deploymentOptions["TargetLocation"]))
{
if (-not($rg.Location)) {
Write-Host "Getting Azure locations..." -ForegroundColor Green
$locations = Get-AzLocation | Where-Object { $_.Providers -contains "Microsoft.Automation" -and $_.Providers -contains "Microsoft.Sql" `
-and $_.Providers -contains "Microsoft.OperationalInsights" `
-and $_.Providers -contains "Microsoft.Storage"} | Sort-Object -Property Location
for ($i = 0; $i -lt $locations.Count; $i++) {
Write-Output "[$i] $($locations[$i].location)"
}
$selectedLocation = -1
$lastLocationIndex = $locations.Count - 1
while ($selectedLocation -lt 0 -or $selectedLocation -gt $lastLocationIndex) {
Write-Output "---"
$selectedLocation = [int] (Read-Host "Please, select the target location for this deployment [0..$lastLocationIndex]")
}
$targetLocation = $locations[$selectedLocation].location
}
else {
$targetLocation = $rg.Location
}
$deploymentOptions["TargetLocation"] = $targetLocation
}
else
{
$targetLocation = $deploymentOptions["TargetLocation"]
}
if (-not($deploymentOptions["SqlAdmin"]))
{
$sqlAdmin = Read-Host "Please, input the SQL Admin username"
$deploymentOptions["SqlAdmin"] = $sqlAdmin
}
else
{
$sqlAdmin = $deploymentOptions["SqlAdmin"]
}
if (-not($deploymentOptions["SqlPass"]))
{
$sqlPass = Read-Host "Please, input the SQL Admin ($sqlAdmin) password" -AsSecureString
}
else
{
$sqlPass = $deploymentOptions["SqlPass"]
if(Test-SqlPasswordComplexity -Username $sqlAdmin -Password $sqlPass -ErrorAction SilentlyContinue)
{
Write-Host "Password complexity check passed" -ForegroundColor Green
$sqlPass = ConvertTo-SecureString -AsPlainText $sqlPass -Force
}
else
{
throw "SQL password complexity check failed. Please, fix the password and try again."
}
}
#endregion
#region Partial upgrade dependent resource checks
if (-not($DoPartialUpgrade))
{
$upgrading = $false
}
else
{
$upgrading = $true
if ($null -ne $rg)
{
if ($upgrading -and $null -ne $sa)
{
$containers = Get-AzStorageContainer -Context $sa.Context
}
else
{
$upgrading = $false
Write-Host "Did not find the $storageAccountName Storage Account." -ForegroundColor Yellow
}
if ($upgrading -and $null -ne $sql)
{
$databases = Get-AzSqlDatabase -ServerName $sql.ServerName -ResourceGroupName $resourceGroupName
if (-not($databases | Where-Object { $_.DatabaseName -eq $sqlDatabaseName}))
{
$upgrading = $false
Write-Host "Did not find the $sqlDatabaseName database." -ForegroundColor Yellow
}
}
else
{
if (-not($IgnoreNamingAvailabilityErrors))
{
$upgrading = $false
Write-Host "Did not find the $sqlServerName SQL Server." -ForegroundColor Yellow
}
}
$auto = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName -Name $automationAccountName -ErrorAction SilentlyContinue
if ($null -ne $auto)
{
$runbooks = Get-AzAutomationRunbook -ResourceGroupName $resourceGroupName `
-AutomationAccountName $auto.AutomationAccountName | Where-Object { $_.Name.StartsWith('Export') }
if ($runbooks.Count -lt 3)
{
$upgrading = $false
Write-Host "Did not find existing runbooks in the $automationAccountName Automation Account." -ForegroundColor Yellow
}
}
else
{
$upgrading = $false
Write-Host "Did not find the $automationAccountName Automation Account." -ForegroundColor Yellow
}
}
else
{
$upgrading = $false
}
}
#endregion
$deploymentMessage = "Deploying Azure Optimization Engine to subscription"
if ($upgrading)
{
Write-Host "Looks like this deployment was already done in the past. We will only upgrade runbooks, modules, schedules, variables, storage and the database." -ForegroundColor Yellow
$deploymentMessage = "Upgrading Azure Optimization Engine in subscription"
}
if ($silentDeploy)
{
$continueInput = "Y"
}
else
{
$continueInput = Read-Host "$deploymentMessage $($subscriptions[$selectedSubscription].Name). Continue (Y/N)?"
}
if ("Y", "y" -contains $continueInput) {
# If we deploy silently, be sure to strip the SQL password from the output
if ($silentDeploy)
{
$deploymentOptions.Remove("SqlPass")
}
$deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force
#region Computing schedules base time
$baseTime = (Get-Date).ToUniversalTime().ToString("u")
$upgradingSchedules = $false
$schedules = Get-AzAutomationSchedule -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -ErrorAction SilentlyContinue
if ($schedules.Count -gt 0) {
$upgradingSchedules = $true
$originalBaseTime = ($schedules | Where-Object { $_.Name.EndsWith("Weekly") } | Sort-Object -Property StartTime | Select-Object -First 1).StartTime.AddHours(-1.25).DateTime
$now = (Get-Date).ToUniversalTime()
$diff = $now.AddHours(-1.25) - $originalBaseTime
$nextWeekDays = [Math]::Ceiling($diff.TotalDays / 7) * 7
$baseTime = $now.AddHours(-1.25).AddDays($nextWeekDays - $diff.TotalDays).ToString("u")
Write-Host "Existing schedules found. Keeping original base time: $baseTime." -ForegroundColor Green
}
else {
Write-Host "Automation schedules base time automatically set to $baseTime." -ForegroundColor Green
}
#endregion
if (-not($upgrading))
{
#region Template-based deployment
$jobSchedules = Get-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -ErrorAction SilentlyContinue
if ($jobSchedules.Count -gt 0) {
Write-Host "Unregistering previous runbook schedules associations from $automationAccountName..." -ForegroundColor Green
foreach ($jobSchedule in $jobSchedules) {
if ($jobSchedule.ScheduleName.StartsWith("AzureOptimization")) {
Unregister-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-JobScheduleId $jobSchedule.JobScheduleId -Force
}
}
}
Write-Host "Deploying Azure Optimization Engine resources..." -ForegroundColor Green
$deploymentTries = 0
$maxDeploymentTries = 2
$deploymentSucceeded = $false
do {
$deploymentTries++
try {
$deployment = New-AzDeployment -TemplateFile ".\azuredeploy.bicep" -templateLocation $TemplateUri.Replace("azuredeploy.bicep", "") -Location $targetLocation -rgName $resourceGroupName -Name $deploymentName `
-projectLocation $targetlocation -logAnalyticsReuse $logAnalyticsReuse -baseTime $baseTime `
-logAnalyticsWorkspaceName $laWorkspaceName -logAnalyticsWorkspaceRG $laWorkspaceResourceGroup `
-storageAccountName $storageAccountName -automationAccountName $automationAccountName `
-sqlServerName $sqlServerName -sqlDatabaseName $sqlDatabaseName -cloudEnvironment $AzureEnvironment `
-sqlAdminLogin $sqlAdmin -sqlAdminPassword $sqlPass -resourceTags $ResourceTags -WarningAction SilentlyContinue
$deploymentSucceeded = $true
}
catch {
if ($deploymentTries -ge $maxDeploymentTries) {
Write-Host "Failed deployment. Stop trying." -ForegroundColor Yellow
throw $_
}
Write-Host "Failed deployment. Trying once more..." -ForegroundColor Yellow
}
} while (-not($deploymentSucceeded) -and $deploymentTries -lt $maxDeploymentTries)
$spnId = $deployment.Outputs['automationPrincipalId'].Value
#endregion
}
else
{
#region Partial upgrade deployment
$upgradeManifest = Get-Content -Path "./upgrade-manifest.json" | ConvertFrom-Json
Write-Host "Creating missing storage account containers..." -ForegroundColor Green
$upgradeContainers = $upgradeManifest.dataCollection.container
foreach ($container in $upgradeContainers)
{
if (-not($container -in $containers.Name))
{
New-AzStorageContainer -Name $container -Context $sa.Context -Permission Off | Out-Null
Write-Host "$container container created."
}
}
Write-Host "Importing runbooks..." -ForegroundColor Green
$allRunbooks = $upgradeManifest.baseIngest.runbook + $upgradeManifest.dataCollection.runbook + $upgradeManifest.recommendations.runbook + $upgradeManifest.remediations.runbook
$runbookBaseUri = $TemplateUri.Replace("azuredeploy.bicep", "")
$topTemplateJson = "{ `"`$schema`": `"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#`", " + `
"`"contentVersion`": `"1.0.0.0`", `"resources`": ["
$bottomTemplateJson = "] }"
$runbookDeploymentTemplateJson = $topTemplateJson
for ($i = 0; $i -lt $allRunbooks.Count; $i++)
{
try {
Invoke-WebRequest -Uri ($runbookBaseUri + $allRunbooks[$i].name) | Out-Null
$runbookName = [System.IO.Path]::GetFilenameWithoutExtension($allRunbooks[$i].name)
$runbookJson = "{ `"name`": `"$automationAccountName/$runbookName`", `"type`": `"Microsoft.Automation/automationAccounts/runbooks`", " + `
"`"apiVersion`": `"2018-06-30`", `"location`": `"$targetLocation`", `"tags`": $($ResourceTags | ConvertTo-Json), `"properties`": { " + `
"`"runbookType`": `"PowerShell`", `"logProgress`": false, `"logVerbose`": false, " + `
"`"publishContentLink`": { `"uri`": `"$runbookBaseUri$($allRunbooks[$i].name)`", `"version`": `"$($allRunbooks[$i].version)`" } } }"
$runbookDeploymentTemplateJson += $runbookJson
if ($i -lt $allRunbooks.Count - 1)
{
$runbookDeploymentTemplateJson += ", "
}
Write-Host "$($allRunbooks[$i].name) imported."
}
catch {
Write-Host "$($allRunbooks[$i].name) not imported (not found)." -ForegroundColor Yellow
}
}
$runbookDeploymentTemplateJson += $bottomTemplateJson
$runbooksTemplatePath = "./aoe-runbooks-deployment.json"
$runbookDeploymentTemplateJson | Out-File -FilePath $runbooksTemplatePath -Force
Write-Host "Executing runbooks deployment..." -ForegroundColor Green
New-AzResourceGroupDeployment -TemplateFile $runbooksTemplatePath -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f "runbooks") | Out-Null
Remove-Item -Path $runbooksTemplatePath -Force
Write-Host "Runbooks update deployed."
Write-Host "Importing modules..." -ForegroundColor Green
$allModules = $upgradeManifest.modules
$modulesDeploymentTemplateJson = $topTemplateJson
for ($i = 0; $i -lt $allModules.Count; $i++)
{
$moduleJson = "{ `"name`": `"$automationAccountName/$($allModules[$i].name)`", `"type`": `"Microsoft.Automation/automationAccounts/modules`", " + `
"`"apiVersion`": `"2018-06-30`", `"location`": `"$targetLocation`", `"tags`": $($ResourceTags | ConvertTo-Json), `"properties`": { " + `
"`"contentLink`": { `"uri`": `"$($allModules[$i].url)`" } } "
if ($allModules[$i].name -ne "Az.Accounts" -and $allModules[$i].name -ne "Microsoft.Graph.Authentication")
{
$moduleJson += ", `"dependsOn`": [ `"Az.Accounts`", `"Microsoft.Graph.Authentication`" ]"
}
$moduleJson += "}"
$modulesDeploymentTemplateJson += $moduleJson
if ($i -lt $allModules.Count - 1)
{
$modulesDeploymentTemplateJson += ", "
}
Write-Host "$($allModules[$i].name) imported."
}
$modulesDeploymentTemplateJson += $bottomTemplateJson
$modulesTemplatePath = "./aoe-modules-deployment.json"
$modulesDeploymentTemplateJson | Out-File -FilePath $modulesTemplatePath -Force
Write-Host "Executing modules deployment..." -ForegroundColor Green
New-AzResourceGroupDeployment -TemplateFile $modulesTemplatePath -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f "modules") | Out-Null
Remove-Item -Path $modulesTemplatePath -Force
Write-Host "Modules update deployed."
Write-Host "Updating schedules..." -ForegroundColor Green
$allSchedules = $upgradeManifest.schedules
$allScheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName
$exportHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Export") })[0].HybridWorker
$ingestHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Ingest") })[0].HybridWorker
$recommendHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Recommend") })[0].HybridWorker
if ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Remediate") })
{
$remediateHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Remediate") })[0].HybridWorker
}
$hybridWorkerOption = "None"
if ($exportHybridWorkerOption -or $ingestHybridWorkerOption -or $recommendHybridWorkerOption -or $remediateHybridWorkerOption) {
$hybridWorkerOption = "Export: $exportHybridWorkerOption; Ingest: $ingestHybridWorkerOption; Recommend: $recommendHybridWorkerOption; Remediate: $remediateHybridWorkerOption"
}
Write-Host "Current Hybrid Worker option: $hybridWorkerOption" -ForegroundColor Green
$dataIngestRunbookName = [System.IO.Path]::GetFileNameWithoutExtension(($upgradeManifest.baseIngest | Where-Object { $_.source -eq "dataCollection"}).runbook.name)
$dataExportsToMultiSchedule = $upgradeManifest.dataCollection | Where-Object { $_.exportSchedules.Count -gt 0 }
$recommendationsProcessingRunbooks = $upgradeManifest.baseIngest | Where-Object { $_.source -eq "recommendations" -or $_.source -eq "maintenance"}
foreach ($schedule in $allSchedules)
{
if (-not($schedules | Where-Object { $_.Name -eq $schedule.name }))
{
$scheduleStartTime = (Get-Date $baseTime).Add([System.Xml.XmlConvert]::ToTimeSpan($schedule.offset))
$scheduleNow = (Get-Date).ToUniversalTime()
if ($schedule.frequency -eq "Hour")
{
if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)
{
$hoursDiff = ($scheduleNow - $scheduleStartTime).Hours + 1
$scheduleStartTime = $scheduleStartTime.AddHours($hoursDiff)
}
New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
-StartTime $scheduleStartTime -HourInterval 1 | Out-Null
}
if ($schedule.frequency -eq "Day")
{
if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)
{
$scheduleStartTime = $scheduleStartTime.AddDays(1)
}
New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
-StartTime $scheduleStartTime -DayInterval 1 | Out-Null
}
if ($schedule.frequency -eq "Week")
{
if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)
{
$scheduleStartTime = $scheduleStartTime.AddDays(7)
}
New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
-StartTime $scheduleStartTime -WeekInterval 1 | Out-Null
}
Write-Host "$($schedule.name) schedule created."
}
$scheduledRunbooks = Get-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-ScheduleName $schedule.name
$dataExportsToSchedule = ($upgradeManifest.dataCollection + $upgradeManifest.recommendations) | Where-Object { $_.exportSchedule -eq $schedule.name }
foreach ($dataExport in $dataExportsToSchedule)
{
$runbookName = [System.IO.Path]::GetFileNameWithoutExtension($dataExport.runbook.name)
$runbookType = $runbookName.Split("-")[0]
switch ($runbookType)
{
"Export" {
$hybridWorkerName = $exportHybridWorkerOption
}
"Recommend" {
$hybridWorkerName = $recommendHybridWorkerOption
}
"Ingest" {
$hybridWorkerName = $ingestHybridWorkerOption
}
"Remediate" {
$hybridWorkerName = $remediateHybridWorkerOption
}
Default {
$hybridWorkerName = $null
}
}
if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName}))
{
if ($hybridWorkerName)
{
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName | Out-Null
}
else
{
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $runbookName -ScheduleName $schedule.name | Out-Null
}
Write-Host "Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook"
}
}
foreach ($dataExport in $dataExportsToMultiSchedule)
{
$exportSchedule = $dataExport.exportSchedules | Where-Object { $_.schedule -eq $schedule.name }
if ($exportSchedule)
{
$runbookName = [System.IO.Path]::GetFileNameWithoutExtension($dataExport.runbook.name)
$runbookType = $runbookName.Split("-")[0]
switch ($runbookType)
{
"Export" {
$hybridWorkerName = $exportHybridWorkerOption
}
"Recommend" {
$hybridWorkerName = $recommendHybridWorkerOption
}
"Ingest" {
$hybridWorkerName = $ingestHybridWorkerOption
}
"Remediate" {
$hybridWorkerName = $remediateHybridWorkerOption
}
Default {
$hybridWorkerName = $null
}
}
if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName -and $_.ScheduleName -eq $schedule.name}))
{
$params = @{}
$exportSchedule.parameters.PSObject.Properties | ForEach-Object {
$params[$_.Name] = $_.Value
}
if ($hybridWorkerName)
{
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName -Parameters $params | Out-Null
}
else
{
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $runbookName -ScheduleName $schedule.name -Parameters $params | Out-Null
}
Write-Host "Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook."
}
}
}
$dataIngestToSchedule = $upgradeManifest.dataCollection | Where-Object { $_.ingestSchedule -eq $schedule.name }
foreach ($dataIngest in $dataIngestToSchedule)
{
$hybridWorkerName = $ingestHybridWorkerOption
if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $dataIngestRunbookName}))
{
$params = @{"StorageSinkContainer"=$dataIngest.container}
if ($hybridWorkerName)
{
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $dataIngestRunbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName -Parameters $params | Out-Null
}
else
{
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $dataIngestRunbookName -ScheduleName $schedule.name -Parameters $params | Out-Null
}
Write-Host "Added $($schedule.name) schedule to $hybridWorkerName $dataIngestRunbookName runbook."
}
}
foreach ($recommendationsProcessingRunbook in $recommendationsProcessingRunbooks)
{
$runbookName = [System.IO.Path]::GetFileNameWithoutExtension($recommendationsProcessingRunbook.runbook.name)
$hybridWorkerName = $ingestHybridWorkerOption
if ($recommendationsProcessingRunbook.schedule -eq $schedule.name -and -not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName}))
{
if ($hybridWorkerName)
{
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName | Out-Null
}
else
{
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $runbookName -ScheduleName $schedule.name | Out-Null
}
Write-Host "Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook."
}
}
}
Write-Host "Updating variables..." -ForegroundColor Green
$allVariables = $upgradeManifest.dataCollection.requiredVariables + $upgradeManifest.recommendations.requiredVariables + $upgradeManifest.remediations.requiredVariables
foreach ($variable in $allVariables)
{
$existingVariables = Get-AzAutomationVariable -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName
if (-not($existingVariables | Where-Object { $_.Name -eq $variable.name }))
{
New-AzAutomationVariable -Name $variable.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
-Value $variable.defaultValue -Encrypted $false | Out-Null
Write-Host "$($variable.name) variable created."
}
}
Write-Host "Force-updating variables..." -ForegroundColor Green
$forceUpdateVariables = $upgradeManifest.overwriteVariables
foreach ($variable in $forceUpdateVariables)
{
Set-AzAutomationVariable -Name $variable.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
-Value $variable.value -Encrypted $false | Out-Null
Write-Host "$($variable.name) variable updated."
}
Write-Host "Removing deprecated runbooks..." -ForegroundColor Green
$deprecatedRunbooks = $upgradeManifest.deprecatedRunbooks
foreach ($deprecatedRunbook in $deprecatedRunbooks)
{
Remove-AzAutomationRunbook -AutomationAccountName $automationAccountName -Name $deprecatedRunbook -ResourceGroupName $resourceGroupName -Force -ErrorAction SilentlyContinue
}
#endregion
}
#region Schedules reset
if ($upgradingSchedules) {
$schedules = Get-AzAutomationSchedule -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName
$dailySchedules = $schedules | Where-Object { $_.Frequency -eq "Day" -or $_.Frequency -eq "Hour" }
Write-Host "Fixing daily schedules after upgrade..." -ForegroundColor Green
foreach ($schedule in $dailySchedules) {
$now = (Get-Date).ToUniversalTime()
$newStartTime = [System.DateTimeOffset]::Parse($now.ToString("yyyy-MM-ddT00:00:00Z"))
$newStartTime = $newStartTime.AddHours($schedule.StartTime.Hour).AddMinutes($schedule.StartTime.Minute)
if ($newStartTime.AddMinutes(-5) -lt $now) {
$newStartTime = $newStartTime.AddDays(1)
}
$expiryTime = $schedule.ExpiryTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
$startTime = $newStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
$automationPath = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/schedules/$($schedule.Name)?api-version=2015-10-31"
$body = "{
`"name`": `"$($schedule.Name)`",
`"properties`": {
`"description`": `"$($schedule.Description)`",
`"startTime`": `"$startTime`",
`"expiryTime`": `"$expiryTime`",
`"interval`": 1,
`"frequency`": `"$($schedule.Frequency.ToString())`",
`"advancedSchedule`": {}
}
}"
Invoke-AzRestMethod -Path $automationPath -Method PUT -Payload $body | Out-Null
}
}
#endregion
#region Deployment date Automation variable
Write-Host "Checking Azure Automation variable referring to the initial Azure Optimization Engine deployment date..." -ForegroundColor Green
$deploymentDateVariableName = "AzureOptimization_DeploymentDate"
$deploymentDateVariable = Get-AzAutomationVariable -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name $deploymentDateVariableName -ErrorAction SilentlyContinue
if ($null -eq $deploymentDateVariable) {
$deploymentDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd")
Write-Host "Setting initial deployment date ($deploymentDate)..." -ForegroundColor Green
New-AzAutomationVariable -Name $deploymentDateVariableName -Description "The date of the initial engine deployment" `
-ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Value $deploymentDate -Encrypted $false
}
#endregion
#region Open SQL Server firewall rule
if (-not($sqlServerName -like "*.database.*"))
{
$myPublicIp = (Invoke-WebRequest -uri "https://ifconfig.me/ip").Content.Trim()
if (-not($myPublicIp -like "*.*.*.*"))
{
$myPublicIp = (Invoke-WebRequest -uri "https://ipv4.icanhazip.com").Content.Trim()
if (-not($myPublicIp -like "*.*.*.*"))
{
$myPublicIp = (Invoke-WebRequest -uri "https://ipinfo.io/ip").Content.Trim()
}
}
Write-Host "Opening SQL Server firewall temporarily to your public IP ($myPublicIp)..." -ForegroundColor Green
$tempFirewallRuleName = "InitialDeployment"
New-AzSqlServerFirewallRule -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -FirewallRuleName $tempFirewallRuleName -StartIpAddress $myPublicIp -EndIpAddress $myPublicIp -ErrorAction Continue | Out-Null
}
#endregion
#region SQL Database model deployment
Write-Host "Deploying SQL Database model..." -ForegroundColor Green
$sqlPassPlain = (New-Object PSCredential "user", $sqlPass).GetNetworkCredential().Password
if (-not($sqlServerName -like "*.database.*"))
{
$sqlServerEndpoint = "$sqlServerName$($cloudDetails.SqlDatabaseDnsSuffix)"
}
else
{
$sqlServerEndpoint = $sqlServerName
}
$databaseName = $sqlDatabaseName
$SqlTimeout = 60
$tries = 0
$connectionSuccess = $false
do {
$tries++
try {
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$createTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-table.sql"
$Cmd = new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = $createTableQuery
$Cmd.ExecuteReader()
$Conn.Close()
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$initTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-initialize.sql"
$Cmd = new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = $initTableQuery
$Cmd.ExecuteReader()
$Conn.Close()
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$upgradeTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-upgrade.sql"
$Cmd = new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = $upgradeTableQuery
$Cmd.ExecuteReader()
$Conn.Close()
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$createTableQuery = Get-Content -Path "./model/sqlserveringestcontrol-table.sql"
$Cmd = new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = $createTableQuery
$Cmd.ExecuteReader()
$Conn.Close()
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$initTableQuery = Get-Content -Path "./model/sqlserveringestcontrol-initialize.sql"
$Cmd = new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = $initTableQuery
$Cmd.ExecuteReader()
$Conn.Close()
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$createTableQuery = Get-Content -Path "./model/recommendations-table.sql"
$Cmd = new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = $createTableQuery
$Cmd.ExecuteReader()
$Conn.Close()
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$createTableQuery = Get-Content -Path "./model/recommendations-sp.sql"
$Cmd = new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = $createTableQuery
$Cmd.ExecuteReader()
$Conn.Close()
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$createTableQuery = Get-Content -Path "./model/filters-table.sql"
$Cmd = new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = $createTableQuery
$Cmd.ExecuteReader()
$Conn.Close()
$connectionSuccess = $true
}
catch {
Write-Host "Failed to contact SQL at try $tries." -ForegroundColor Yellow
Write-Host $Error[0] -ForegroundColor Yellow
Start-Sleep -Seconds ($tries * 20)
}
} while (-not($connectionSuccess) -and $tries -lt 3)
if (-not($connectionSuccess)) {
if (-not($sqlServerName -like "*.database.*"))
{
Write-Host "Deleting temporary SQL Server firewall rule..." -ForegroundColor Green
Remove-AzSqlServerFirewallRule -FirewallRuleName $tempFirewallRuleName -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -ErrorAction Continue | Out-Null
}
throw "Could not establish connection to SQL."
}
#endregion
#region Close SQL Server firewall rule
if (-not($sqlServerName -like "*.database.*"))
{
Write-Host "Deleting temporary SQL Server firewall rule..." -ForegroundColor Green
Remove-AzSqlServerFirewallRule -FirewallRuleName $tempFirewallRuleName -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -ErrorAction Continue | Out-Null
}
#endregion
#region Workbooks deployment
if (-not($deploymentOptions["DeployWorkbooks"]))
{
$deployWorkbooks = Read-Host "Do you want to deploy the workbooks with additional insights (recommended)? (Y/N)"
}
else
{
$deployWorkbooks = $deploymentOptions["DeployWorkbooks"]
}
if ("Y", "y" -contains $deployWorkbooks) {
$deploymentOptions["DeployWorkbooks"] = "Y"
$deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force
Write-Host "Publishing workbooks..." -ForegroundColor Green
$workbooks = Get-ChildItem -Path "./views/workbooks/" | Where-Object { $_.Name.EndsWith(".bicep") }
$la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $laWorkspaceResourceGroup -Name $laWorkspaceName
foreach ($workbook in $workbooks)
{
$workbookFileName = [System.IO.Path]::GetFileNameWithoutExtension($workbook.Name)
Write-Host "Deploying $workbookFileName workbook..."
try {
New-AzResourceGroupDeployment -TemplateFile $workbook.FullName -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f $workbookFileName) `
-workbookSourceId $la.ResourceId -resourceTags $ResourceTags -WarningAction SilentlyContinue | Out-Null
}
catch {
Write-Host "Failed to deploy the workbook. If you are upgrading AOE, please remove first the $workbookFileName workbook from the $laWorkspaceName Log Analytics workspace and then re-deploy." -ForegroundColor Yellow
}
}
}
#endregion
if (!$silentDeploy)
{
#region Grant Microsoft Entra ID role to AOE principal
if ($null -eq $spnId)
{
$auto = Get-AzAutomationAccount -Name $automationAccountName -ResourceGroupName $resourceGroupName
$spnId = $auto.Identity.PrincipalId
if ($null -eq $spnId)
{
$runAsConnection = Get-AzAutomationConnection -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name AzureRunAsConnection -ErrorAction SilentlyContinue
$runAsAppId = $runAsConnection.FieldDefinitionValues.ApplicationId
if ($runAsAppId)
{
$runAsServicePrincipal = Get-AzADServicePrincipal -ApplicationId $runAsAppId
$spnId = $runAsServicePrincipal.Id
}
}
}
try
{
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Identity.DirectoryManagement
Write-Host "Granting Microsoft Entra ID Global Reader role to the Automation Account (requires administrative permissions in Microsoft Entra and MS Graph PowerShell SDK >= 2.4.0)..." -ForegroundColor Green
#workaround for https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/888
$localPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)
if (-not(get-item "$localPath\.graph\" -ErrorAction SilentlyContinue))
{
New-Item -Type Directory "$localPath\.graph"
}
switch ($cloudEnvironment) {
"AzureUSGovernment" {
$graphEnvironment = "USGov"
break
}
"AzureChinaCloud" {
$graphEnvironment = "China"
break
}
"AzureGermanCloud" {
$graphEnvironment = "Germany"
break
}
Default {
$graphEnvironment = "Global"
}
}
Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Directory","Directory.Read.All" -UseDeviceAuthentication -Environment $graphEnvironment -NoWelcome
$globalReaderRole = Get-MgDirectoryRole -ExpandProperty Members -Property Id,Members,DisplayName,RoleTemplateId `
| Where-Object { $_.RoleTemplateId -eq "f2ef992c-3afb-46b9-b7cf-a126ee74c451" }
$globalReaders = $globalReaderRole.Members.Id
if (-not($globalReaders -contains $spnId))
{
New-MgDirectoryRoleMemberByRef -DirectoryRoleId $globalReaderRole.Id -BodyParameter @{"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$spnId"}
Start-Sleep -Seconds 5
$globalReaderRole = Get-MgDirectoryRole -ExpandProperty Members -Property Id,Members,DisplayName,RoleTemplateId `
| Where-Object { $_.RoleTemplateId -eq "f2ef992c-3afb-46b9-b7cf-a126ee74c451" }
$globalReaders = $globalReaderRole.Members.Id
if ($globalReaders -contains $spnId)
{
Write-Host "Role granted." -ForegroundColor Green
}
else
{
throw "Error when trying to grant Global Reader role"
}
}
else
{
Write-Host "Role was already granted before." -ForegroundColor Green
}
}
catch
{
Write-Host $Error[0] -ForegroundColor Yellow
Write-Host "Could not grant role. If you want Microsoft Entra-based recommendations, please grant the Global Reader role manually to the $automationAccountName managed identity or, for previous versions of AOE, to the Run As Account principal." -ForegroundColor Red
}
#endregion
}
else
{
Write-Host "Could not grant role. If you want Microsoft Entra-based recommendations, please grant the Global Reader role manually to the $automationAccountName managed identity or, for previous versions of AOE, to the Run As Account principal." -ForegroundColor Red
}
Write-Host "Azure Optimization Engine deployment completed! We're almost there..." -ForegroundColor Green
#region Benefits Usage dependencies
if (-not($deploymentOptions["DeployBenefitsUsageDependencies"]))
{
$benefitsUsageDependenciesOption = Read-Host "Do you also want to deploy the dependencies for the Azure Benefits usage workbooks (EA/MCA customers only + agreement administrator role required)? (Y/N)"
}
else
{
$benefitsUsageDependenciesOption = $deploymentOptions["DeployBenefitsUsageDependencies"]
}
if ("Y", "y" -contains $benefitsUsageDependenciesOption)
{
$deploymentOptions["DeployBenefitsUsageDependencies"] = $benefitsUsageDependenciesOption
$automationAccount = Get-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName
$principalId = $automationAccount.Identity.PrincipalId
$tenantId = $automationAccount.Identity.TenantId
$mcaBillingAccountIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}"
$mcaBillingProfileIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+)"
if (-not($deploymentOptions["CustomerType"]))
{
$customerType = Read-Host "Are you an Enterprise Agreement (EA) or Microsoft Customer Agreement (MCA) customer? Please, type EA or MCA"
$deploymentOptions["CustomerType"] = $customerType
}
else
{
$customerType = $deploymentOptions["CustomerType"]
}
switch ($customerType) {
"EA" {
if (-not($deploymentOptions["BillingAccountId"]))
{
$billingAccountId = Read-Host "Please, enter your Enterprise Agreement Billing Account ID (e.g. 12345678)"
$deploymentOptions["BillingAccountId"] = $billingAccountId
}
else
{
$billingAccountId = $deploymentOptions["BillingAccountId"]
}
try
{
[int32]::Parse($billingAccountId) | Out-Null
}
catch
{
throw "The Enterprise Agreement Billing Account ID must be a number (e.g. 12345678)."
}
Write-Host "Granting the Enterprise Enrollment Reader role to the AOE Managed Identity..." -ForegroundColor Green
$uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments?api-version=2019-10-01-preview"
$roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri
if (-not($roleAssignmentResponse.StatusCode -eq 200))
{
throw "The Enterprise Enrollment Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
}
$roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value
if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq "/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e" }))
{
$billingRoleAssignmentName = ([System.Guid]::NewGuid()).Guid
$uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments/$($billingRoleAssignmentName)?api-version=2019-10-01-preview"
$body = "{`"properties`": {`"principalId`":`"$principalId`",`"principalTenantId`":`"$tenantId`",`"roleDefinitionId`":`"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e`"}}"
$roleAssignmentResponse = Invoke-AzRestMethod -Method PUT -Uri $uri -Payload $body
if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))
{
throw "The Enterprise Enrollment Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
}
}
else
{
Write-Host "Role was already granted before." -ForegroundColor Green
}
break
}
"MCA" {
if (-not($deploymentOptions["BillingAccountId"]))
{
$billingAccountId = Read-Host "Please, enter your Microsoft Customer Agreement Billing Account ID (e.g. <guid>:<guid>_YYYY-MM-DD)"
$deploymentOptions["BillingAccountId"] = $billingAccountId
}
else
{
$billingAccountId = $deploymentOptions["BillingAccountId"]
}
if (-not($billingAccountId -match $mcaBillingAccountIdRegex))
{
throw "The Microsoft Customer Agreement Billing Account ID must be in the format <guid>:<guid>_YYYY-MM-DD."
}
if (-not($deploymentOptions["BillingProfileId"]))
{
$billingProfileId = Read-Host "Please, enter your Billing Profile ID (e.g. ABCD-DEF-GHI-JKL)"
$deploymentOptions["BillingProfileId"] = $billingProfileId
}
else
{
$billingProfileId = $deploymentOptions["BillingProfileId"]
}
if (-not($billingProfileId -match $mcaBillingProfileIdRegex))
{
throw "The Microsoft Customer Agreement Billing Profile ID must be in the format ABCD-DEF-GHI-JKL."
}
Write-Host "Granting the Billing Profile Reader role to the AOE Managed Identity..." -ForegroundColor Green
$uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleAssignments?api-version=2019-10-01-preview"
$roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri
if (-not($roleAssignmentResponse.StatusCode -eq 200))
{
throw "The Billing Profile Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
}
$roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value
if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq "/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002" }))
{
$uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/createBillingRoleAssignment?api-version=2020-12-15-privatepreview"
$body = "{`"principalId`":`"$principalId`",`"principalTenantId`":`"$tenantId`",`"roleDefinitionId`":`"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002`"}"
$roleAssignmentResponse = Invoke-AzRestMethod -Method POST -Uri $uri -Payload $body
if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))
{
throw "The Billing Profile Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
}
}
else
{
Write-Host "Role was already granted before." -ForegroundColor Green
}
break
}
Default {
throw "Only EA and MCA customers are supported at this time."
}
}
Write-Output "Setting up the Billing Account ID variable..."
$billingAccountIdVarName = "AzureOptimization_BillingAccountID"
$billingAccountIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -ErrorAction SilentlyContinue
if (-not($billingAccountIdVar))
{
New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null
}
else
{
Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null
}
if ($billingProfileId)
{
Write-Output "Setting up the Billing Profile ID variable..."
$billingProfileIdVarName = "AzureOptimization_BillingProfileID"
$billingProfileIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -ErrorAction SilentlyContinue
if (-not($billingProfileIdVar))
{
New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null
}
else
{
Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null
}
}
if (-not $deploymentOptions["CurrencyCode"])
{
$currencyCode = Read-Host "Please, enter your consumption currency code (e.g. EUR, USD, etc.)"
$deploymentOptions["CurrencyCode"] = $currencyCode
}
else
{
$currencyCode = $deploymentOptions["CurrencyCode"]
}
$deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force
Write-Output "Setting up the consumption currency code variable..."
$currencyCodeVarName = "AzureOptimization_RetailPricesCurrencyCode"
$currencyCodeVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -ErrorAction SilentlyContinue
if (-not($currencyCodeVar))
{
New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null
}
else
{
Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null
}
}
#endregion
Write-Host "Deployment fully completed!" -ForegroundColor Green
}
else {
Write-Host "Deployment cancelled." -ForegroundColor Red
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Hélder Pinto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Welcome to the Azure Optimization Engine - now a FinOps Toolkit tool! 🔍
👋 Thank you for your interest in the Azure Optimization Engine! We've migrated the Azure Optimization Engine repository to a new home - the Microsoft [FinOps Toolkit](https://aka.ms/AzureOptimizationEngine). For historical reasons, the older code will remain here, but the most recent version and new releases will be made in the FinOps Toolkit repository going forward. Here's what you need to know:
1. **We are collecting feedback in preparation for AOE v2**: Answer this [anonymous feedback form](https://forms.office.com/r/fLeJS8Rd2E) and contribute to the [discussion about the evolution of AOE](https://github.com/microsoft/finops-toolkit/discussions/753).
1. **Start Using the FinOps Toolkit**: If you were using the Azure Optimization Engine, it's time to switch! The [FinOps Toolkit](https://aka.ms/AzureOptimizationEngine) provides advanced solutions, automation scripts, and learning resources to accelerate your FinOps journey.
2. **Open Issues Here**: Have questions, feedback, or need assistance? Open an issue in the [FinOps Toolkit repository](https://github.com/microsoft/finops-toolkit/issues), and our community and maintainers will be happy to help. 🙌
3. **Explore Our Tools**: Besides the Azure Optimization Engine, check out all the other available tools, including FinOps hubs, Power BI reports, cost optimization workbooks, and more. [Explore the FinOps Toolkit](https://aka.ms/finops/toolkit)
4. **Stay Updated**: Follow our [FinOps blog](https://techcommunity.microsoft.com/t5/finops-blog/bg-p/FinOpsBlog) for the latest news and announcements.
Let's make cloud cost management easier together! 🌟
================================================
FILE: Reset-AutomationSchedules.ps1
================================================
param(
[Parameter(Mandatory = $false)]
[String] $AzureEnvironment = "AzureCloud",
[Parameter(Mandatory = $true)]
[String] $AutomationAccountName,
[Parameter(Mandatory = $true)]
[String] $ResourceGroupName
)
$ErrorActionPreference = "Stop"
$ctx = Get-AzContext
if (-not($ctx)) {
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
else {
if ($ctx.Environment.Name -ne $AzureEnvironment) {
Disconnect-AzAccount -ContextName $ctx.Name
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
}
try {
$scheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName
}
catch {
throw "$AutomationAccountName Automation Account not found in Resource Group $ResourceGroupName in Subscription $($ctx.Subscription.Name). If we are not in the right subscription, use Set-AzContext to switch to the correct one."
}
if (-not($scheduledRunbooks)) {
throw "The $AutomationAccountName Automation Account does not contain any scheduled runbook. It might not be associated to the Azure Optimization Engine."
}
$subscriptionId = (Get-AzContext).Subscription.Id
$schedules = Get-AzAutomationSchedule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName
$weeklySchedules = $schedules | Where-Object { $_.Name.StartsWith("AzureOptimization") -and $_.Name.EndsWith("Weekly") }
if ($weeklySchedules.Count -gt 0) {
$originalBaseTime = ($weeklySchedules | Sort-Object -Property StartTime | Select-Object -First 1).StartTime.AddHours(-1.25).DateTime
$now = (Get-Date).ToUniversalTime()
$diff = $now.AddHours(-1.25) - $originalBaseTime
$nextWeekDays = [Math]::Ceiling($diff.TotalDays / 7) * 7
$baseDateTime = $now.AddHours(-1.25).AddDays($nextWeekDays - $diff.TotalDays)
$baseTimeStr = $baseDateTime.ToString("u")
Write-Host "Existing schedules found. Weekly base time is $($baseDateTime.DayOfWeek) at $($baseDateTime.ToString('T')) (UTC)." -ForegroundColor Green
}
else {
throw "The $AutomationAccountName Automation Account does not contain Azure Optimization Engine schedules."
}
$newBaseTimeStr = Read-Host "Please, enter a new base time for the *weekly* schedules in UTC (YYYY-MM-dd HH:mm:ss). If you want to keep the current one, just press ENTER"
if (-not($newBaseTimeStr)) {
$newBaseTimeStr = $baseTimeStr
}
else {
try {
$newBaseTimeStr += "Z"
$newBaseTime = [DateTime]::Parse($newBaseTimeStr)
}
catch {
throw "$newBaseTimeStr is an invalid base time. Use the following format: YYYY-MM-dd HH:mm:ss. For example: 1977-09-08 06:14:15"
}
if ($newBaseTime -lt (Get-Date).ToUniversalTime().AddHours(-1)) {
throw "$newBaseTimeStr is an invalid base time. It can't be sooner than $((Get-Date).ToUniversalTime().AddHours(-1).ToString('u'))"
}
}
$baseTimeUtc = [DateTime]::Parse($newBaseTimeStr).ToUniversalTime()
if ($newBaseTimeStr -ne $baseTimeStr) {
Write-Host "Updating current base schedule to every $($baseTimeUtc.DayOfWeek) at $($baseTimeUtc.TimeOfDay.ToString()) UTC..." -ForegroundColor Green
$continueInput = Read-Host "Continue (Y/N)?"
if ("Y", "y" -contains $continueInput) {
$upgradeManifest = Get-Content -Path "./upgrade-manifest.json" | ConvertFrom-Json
$manifestSchedules = $upgradeManifest.schedules
foreach ($schedule in $schedules) {
$manifestSchedule = $manifestSchedules | Where-Object { $_.name -eq $schedule.Name }
if ($manifestSchedule) {
if ($schedule.Frequency -eq "Week") {
$newStartTime = $baseTimeUtc.Add([System.Xml.XmlConvert]::ToTimeSpan($manifestSchedule.offset))
}
else {
$now = (Get-Date).ToUniversalTime()
$newStartTime = [System.DateTimeOffset]::Parse($now.ToString("yyyy-MM-ddT00:00:00Z"))
$newStartTime = $newStartTime.AddHours($baseTimeUtc.Hour).AddMinutes($baseTimeUtc.Minute).AddSeconds($baseTimeUtc.Second)
if ($newStartTime -lt $now.AddMinutes(15))
{
$newStartTime = $newStartTime.AddDays(1)
}
$newStartTime = $newStartTime.Add([System.Xml.XmlConvert]::ToTimeSpan($manifestSchedule.offset))
}
$expiryTime = $schedule.ExpiryTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
$startTime = $newStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
$automationPath = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/schedules/$($schedule.Name)?api-version=2015-10-31"
$body = "{
`"name`": `"$($schedule.Name)`",
`"properties`": {
`"description`": `"$($schedule.Description)`",
`"startTime`": `"$startTime`",
`"expiryTime`": `"$expiryTime`",
`"interval`": $($schedule.Interval),
`"frequency`": `"$($schedule.Frequency)`",
`"advancedSchedule`": {}
}
}"
Invoke-AzRestMethod -Path $automationPath -Method PUT -Payload $body | Out-Null
Write-Host "Re-scheduled $($schedule.Name)." -ForegroundColor Blue
}
else {
Write-Host "$($schedule.Name) not found in schedules manifest." -ForegroundColor Yellow
}
}
}
else
{
throw "Interrupting schedules reset due to user input."
}
}
else {
Write-Host "Kept current base schedule (every $($baseTimeUtc.DayOfWeek) at $($baseTimeUtc.TimeOfDay.ToString()) UTC)." -ForegroundColor Green
}
$exportHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Export") })[0].HybridWorker
$ingestHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Ingest") })[0].HybridWorker
$recommendHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Recommend") })[0].HybridWorker
if ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Remediate") })
{
$remediateHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Remediate") })[0].HybridWorker
}
$hybridWorkerOption = "None"
if ($exportHybridWorkerOption -or $ingestHybridWorkerOption -or $recommendHybridWorkerOption -or $remediateHybridWorkerOption) {
$hybridWorkerOption = "Export: $exportHybridWorkerOption; Ingest: $ingestHybridWorkerOption; Recommend: $recommendHybridWorkerOption; Remediate: $remediateHybridWorkerOption"
}
Write-Host "Current Hybrid Worker option: $hybridWorkerOption" -ForegroundColor Green
$newHybridWorker = Read-Host "If you want all schedules to use the same Hybrid Worker, please enter the Hybrid Worker Group name (if you want to keep the current option, just press ENTER)"
if ($newHybridWorker)
{
$hybridWorker = Get-AzAutomationHybridWorkerGroup -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name $newHybridWorker -ErrorAction SilentlyContinue
if (-not($hybridWorker))
{
throw "Hybrid Worker $newHybridWorker was not found in Automation Account $automationAccountName."
}
Write-Host "Updating Hybrid Worker Group in every runbook schedule to $newHybridWorker..."
$continueInput = Read-Host "Continue (Y/N)?"
if ("Y", "y" -contains $continueInput)
{
Write-Host "Re-registering previous runbook schedules associations from $automationAccountName..." -ForegroundColor Green
foreach ($jobSchedule in $scheduledRunbooks) {
if ($jobSchedule.ScheduleName.StartsWith("AzureOptimization")) {
$automationPath = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/jobSchedules/$($jobSchedule.JobScheduleId)?api-version=2015-10-31"
$jobSchedule = (Invoke-AzRestMethod -Path $automationPath -Method GET).Content | ConvertFrom-Json
Unregister-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-JobScheduleId $jobSchedule.id.Split('/')[10] -Force
$params = @{}
$jobSchedule.properties.parameters.PSObject.Properties | ForEach-Object {
$params[$_.Name] = $_.Value
}
Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
-RunbookName $jobSchedule.properties.runbook.name -ScheduleName $jobSchedule.properties.schedule.name -RunOn $newHybridWorker -Parameters $params | Out-Null
Write-Host "Re-registered $($jobSchedule.properties.runbook.name) for schedule $($jobSchedule.properties.schedule.name)." -ForegroundColor Blue
}
}
}
else
{
throw "Interrupting schedules reset due to user input."
}
}
else
{
Write-Host "Kept current Hybrid Worker option: $hybridWorkerOption" -ForegroundColor Green
}
Write-Host "DONE" -ForegroundColor Green
================================================
FILE: Setup-BenefitsUsageDependencies.ps1
================================================
param(
[Parameter(Mandatory = $false)]
[String] $AzureEnvironment = "AzureCloud",
[Parameter(Mandatory = $true)]
[String] $AutomationAccountName,
[Parameter(Mandatory = $true)]
[String] $ResourceGroupName
)
$ErrorActionPreference = "Stop"
$ctx = Get-AzContext
if (-not($ctx)) {
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
else {
if ($ctx.Environment.Name -ne $AzureEnvironment) {
Disconnect-AzAccount -ContextName $ctx.Name
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
}
try {
$scheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName
}
catch {
throw "$AutomationAccountName Automation Account not found in Resource Group $ResourceGroupName in Subscription $($ctx.Subscription.Name). If we are not in the right subscription, use Set-AzContext to switch to the correct one."
}
if (-not($scheduledRunbooks)) {
throw "The $AutomationAccountName Automation Account does not contain any scheduled runbook. It might not be associated to the Azure Optimization Engine."
}
$automationAccount = Get-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName
$principalId = $automationAccount.Identity.PrincipalId
$tenantId = $automationAccount.Identity.TenantId
if (-not($principalId))
{
throw "The $AutomationAccountName Automation Account does not have a managed identity and probably is not associated to the latest version of Azure Optimization Engine. Please, upgrade it before setting up benefits usage dependencies (more details: https://github.com/helderpinto/AzureOptimizationEngine#upgrade)."
}
$pricesheetSchedule = Get-AzAutomationSchedule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name "AzureOptimization_ExportPricesWeekly" -ErrorAction SilentlyContinue
if (-not($pricesheetSchedule)) {
throw "The Azure Optimization Engine is in an older version. Please, upgrade it before setting up benefits usage dependencies (more details: https://github.com/helderpinto/AzureOptimizationEngine#upgrade)."
}
$mcaBillingAccountIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}"
$mcaBillingProfileIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+)"
$customerType = Read-Host "Are you an Enterprise Agreement (EA), Microsoft Customer Agreement (MCA), or other type (Other) of customer? Please, type EA, MCA, or Other"
switch ($customerType) {
"EA" {
$billingAccountId = Read-Host "Please, enter your Enterprise Agreement Billing Account ID (e.g. 12345678)"
try
{
[int32]::Parse($billingAccountId) | Out-Null
}
catch
{
throw "The Enterprise Agreement Billing Account ID must be a number (e.g. 12345678)."
}
Write-Host "Granting the Enterprise Enrollment Reader role to the AOE Managed Identity..." -ForegroundColor Green
$uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments?api-version=2019-10-01-preview"
$roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri
if (-not($roleAssignmentResponse.StatusCode -eq 200))
{
throw "The Enterprise Enrollment Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
}
$roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value
if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq "/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e" }))
{
$billingRoleAssignmentName = ([System.Guid]::NewGuid()).Guid
$uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments/$($billingRoleAssignmentName)?api-version=2019-10-01-preview"
$body = "{`"properties`": {`"principalId`":`"$principalId`",`"principalTenantId`":`"$tenantId`",`"roleDefinitionId`":`"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e`"}}"
$roleAssignmentResponse = Invoke-AzRestMethod -Method PUT -Uri $uri -Payload $body
if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))
{
throw "The Enterprise Enrollment Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
}
}
else
{
Write-Host "Role was already granted before." -ForegroundColor Green
}
break
}
"MCA" {
$billingAccountId = Read-Host "Please, enter your Microsoft Customer Agreement Billing Account ID (e.g. <guid>:<guid>_YYYY-MM-DD)"
if (-not($billingAccountId -match $mcaBillingAccountIdRegex))
{
throw "The Microsoft Customer Agreement Billing Account ID must be in the format <guid>:<guid>_YYYY-MM-DD."
}
$billingProfileId = Read-Host "Please, enter your Billing Profile ID (e.g. ABCD-DEF-GHI-JKL)"
if (-not($billingProfileId -match $mcaBillingProfileIdRegex))
{
throw "The Microsoft Customer Agreement Billing Profile ID must be in the format ABCD-DEF-GHI-JKL."
}
Write-Host "Granting the Billing Profile Reader role to the AOE Managed Identity..." -ForegroundColor Green
$uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleAssignments?api-version=2019-10-01-preview"
$roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri
if (-not($roleAssignmentResponse.StatusCode -eq 200))
{
throw "The Billing Profile Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
}
$roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value
if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq "/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002" }))
{
$uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/createBillingRoleAssignment?api-version=2020-12-15-privatepreview"
$body = "{`"principalId`":`"$principalId`",`"principalTenantId`":`"$tenantId`",`"roleDefinitionId`":`"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002`"}"
$roleAssignmentResponse = Invoke-AzRestMethod -Method POST -Uri $uri -Payload $body
if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))
{
throw "The Billing Profile Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
}
}
else
{
Write-Host "Role was already granted before." -ForegroundColor Green
}
break
}
Default {
throw "Only EA and MCA customers are supported at this time."
}
}
Write-Output "Setting up the Billing Account ID variable..."
$billingAccountIdVarName = "AzureOptimization_BillingAccountID"
$billingAccountIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -ErrorAction SilentlyContinue
if (-not($billingAccountIdVar))
{
New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null
}
else
{
Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null
}
if ($billingProfileId)
{
Write-Output "Setting up the Billing Profile ID variable..."
$billingProfileIdVarName = "AzureOptimization_BillingProfileID"
$billingProfileIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -ErrorAction SilentlyContinue
if (-not($billingProfileIdVar))
{
New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null
}
else
{
Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null
}
}
$currencyCode = Read-Host "Please, enter your consumption currency code (e.g. EUR, USD, etc.)"
Write-Output "Setting up the consumption currency code variable..."
$currencyCodeVarName = "AzureOptimization_RetailPricesCurrencyCode"
$currencyCodeVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -ErrorAction SilentlyContinue
if (-not($currencyCodeVar))
{
New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null
}
else
{
Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null
}
Write-Host "DONE" -ForegroundColor Green
================================================
FILE: Setup-DataCollectionRules.ps1
================================================
param(
[Parameter(Mandatory = $false)]
[String] $AzureEnvironment = "AzureCloud",
[Parameter(Mandatory = $true)]
[String] $DestinationWorkspaceResourceId,
[Parameter(Mandatory = $false)]
[int] $IntervalSeconds = 60,
[Parameter(Mandatory = $false)]
[hashtable] $ResourceTags = @{}
)
$ErrorActionPreference = "Stop"
$ctx = Get-AzContext
if (-not($ctx)) {
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
else {
if ($ctx.Environment.Name -ne $AzureEnvironment) {
Disconnect-AzAccount -ContextName $ctx.Name
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
}
$lastDeploymentStatePath = ".\last-deployment-state.json"
$deploymentOptions = @{}
$perfCounters = Get-Content -Path ".\perfcounters.json" | ConvertFrom-Json
if ((Test-Path -Path $lastDeploymentStatePath))
{
$depOptions = Get-Content -Path $lastDeploymentStatePath | ConvertFrom-Json
Write-Host $depOptions -ForegroundColor Green
$depOptionsReuse = Read-Host "Found last deployment options above. Do you want to create Data Collection Rules (DCRs) reusing the last deployment options (Y/N)?"
if ("Y", "y" -contains $depOptionsReuse)
{
foreach ($property in $depOptions.PSObject.Properties)
{
$deploymentOptions[$property.Name] = $property.Value
}
}
}
Write-Host "Getting Azure subscriptions..." -ForegroundColor Yellow
$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" -and $_.SubscriptionPolicies.QuotaId -notlike "AAD*" }
if ($subscriptions.Count -gt 1) {
$selectedSubscription = -1
for ($i = 0; $i -lt $subscriptions.Count; $i++)
{
if (-not($deploymentOptions["SubscriptionId"]))
{
Write-Output "[$i] $($subscriptions[$i].Name)"
}
else
{
if ($subscriptions[$i].Id -eq $deploymentOptions["SubscriptionId"])
{
$selectedSubscription = $i
break
}
}
}
if (-not($deploymentOptions["SubscriptionId"]))
{
$lastSubscriptionIndex = $subscriptions.Count - 1
while ($selectedSubscription -lt 0 -or $selectedSubscription -gt $lastSubscriptionIndex) {
Write-Output "---"
$selectedSubscription = [int] (Read-Host "Please, select the target subscription for this deployment [0..$lastSubscriptionIndex]")
}
}
if ($selectedSubscription -eq -1)
{
throw "The selected subscription does not exist. Check if you are logged in with the right Microsoft Entra ID user."
}
}
else
{
if ($subscriptions.Count -ne 0)
{
$selectedSubscription = 0
}
else
{
throw "No valid subscriptions found. Only EA, MCA, PAYG or MSDN subscriptions are supported currently."
}
}
if ($subscriptions.Count -eq 0) {
throw "No subscriptions found. Check if you are logged in with the right Microsoft Entra ID account."
}
$subscriptionId = $subscriptions[$selectedSubscription].Id
if ($ctx.Subscription.SubscriptionId -ne $DestinationWorkspaceResourceId.Split('/')[2])
{
$ctx = Set-AzContext -SubscriptionId $DestinationWorkspaceResourceId.Split('/')[2]
}
$la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $DestinationWorkspaceResourceId.Split('/')[4] -Name $DestinationWorkspaceResourceId.Split('/')[8] -ErrorAction SilentlyContinue
if (-not($la))
{
throw "The destination workspace ($DestinationWorkspaceResourceId) does not exist. Check if you are logged in with the right Microsoft Entra ID user."
}
if (-not($deploymentOptions["NamePrefix"]))
{
do
{
$namePrefix = Read-Host "Please, enter a unique name prefix for the DCRs or existing prefix if updating deployment. If you want instead to individually name all DCRs, just press ENTER"
if (-not($namePrefix))
{
$namePrefix = "EmptyNamePrefix"
}
}
while ($namePrefix.Length -gt 21)
}
else {
if ($deploymentOptions["NamePrefix"] -eq "EmptyNamePrefix")
{
$namePrefix = $null
}
else
{
$namePrefix = $deploymentOptions["NamePrefix"]
}
}
$windowsDcrNameTemplate = "{0}-windows-dcr"
$linuxDcrNameTemplate = "{0}-linux-dcr"
if (-not($deploymentOptions["ResourceGroupName"]))
{
$resourceGroupName = Read-Host "Please, enter the new or existing Resource Group for this deployment"
}
else
{
$resourceGroupName = $deploymentOptions["ResourceGroupName"]
}
if ($ctx.Subscription.SubscriptionId -ne $subscriptionId)
{
$ctx = Set-AzContext -SubscriptionId $subscriptionId
}
$rg = Get-AzResourceGroup -Name $resourceGroupName
if ([string]::IsNullOrEmpty($namePrefix) -or $namePrefix -eq "EmptyNamePrefix") {
$windowsDcrName = Read-Host "Enter the Windows DCR name"
$linuxDcrName = Read-Host "Enter the Linux DCR name"
}
else {
$windowsDcrName = $windowsDcrNameTemplate -f $namePrefix
$linuxDcrName = $linuxDcrNameTemplate -f $namePrefix
}
if (-not($deploymentOptions["TargetLocation"]))
{
if (-not($rg.Location)) {
Write-Host "Getting Azure locations..." -ForegroundColor Green
$locations = Get-AzLocation | Where-Object { $_.Providers -contains "Microsoft.Insights" } | Sort-Object -Property Location
for ($i = 0; $i -lt $locations.Count; $i++) {
Write-Output "[$i] $($locations[$i].location)"
}
$selectedLocation = -1
$lastLocationIndex = $locations.Count - 1
while ($selectedLocation -lt 0 -or $selectedLocation -gt $lastLocationIndex) {
Write-Output "---"
$selectedLocation = [int] (Read-Host "Please, select the target location for this deployment [0..$lastLocationIndex]")
}
$targetLocation = $locations[$selectedLocation].location
}
else {
$targetLocation = $rg.Location
}
}
else
{
$targetLocation = $deploymentOptions["TargetLocation"]
}
$windowsPerfCounters = @()
foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq "Windows"})) {
$windowsPerfCounters += $ExecutionContext.InvokeCommand.ExpandString('"\\$($perfCounter.objectName)($($perfCounter.instance))\\$($perfCounter.counterName)"')
}
$linuxPerfCounters = @()
foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq "Linux"})) {
$linuxPerfCounters += $ExecutionContext.InvokeCommand.ExpandString('"\\$($perfCounter.objectName)($($perfCounter.instance))\\$($perfCounter.counterName)"')
}
$windowsDcrBody = @'
{
"dataSources": {
"performanceCounters": [
{
"streams": [
"Microsoft-Perf"
],
"samplingFrequencyInSeconds": $IntervalSeconds,
"counterSpecifiers": [
$($windowsPerfCounters -join ",")
],
"name": "perfCounterDataSource$IntervalSeconds"
}
]
},
"destinations": {
"logAnalytics": [
{
"workspaceResourceId": "$destinationWorkspaceResourceId",
"workspaceId": "$($la.Properties.CustomerId)",
"name": "la--1138206996"
}
]
},
"dataFlows": [
{
"streams": [
"Microsoft-Perf"
],
"destinations": [
"la--1138206996"
]
}
]
}
'@
Write-Output "Creating Windows DCR..."
$windowsDcrBody = $ExecutionContext.InvokeCommand.ExpandString($windowsDcrBody) | ConvertFrom-Json
New-AzResource -ResourceType "Microsoft.Insights/dataCollectionRules" -ResourceGroupName $resourceGroupName -Location $targetLocation -Name $windowsDcrName -PropertyObject $windowsDcrBody -ApiVersion "2021-04-01" -Tag $ResourceTags -Kind "Windows" -Force | Out-Null
$linuxDcrBody = @'
{
"dataSources": {
"performanceCounters": [
{
"streams": [
"Microsoft-Perf"
],
"samplingFrequencyInSeconds": $IntervalSeconds,
"counterSpecifiers": [
$($linuxPerfCounters -join ",")
],
"name": "perfCounterDataSource$IntervalSeconds"
}
]
},
"destinations": {
"logAnalytics": [
{
"workspaceResourceId": "$destinationWorkspaceResourceId",
"workspaceId": "$($la.Properties.CustomerId)",
"name": "la--1138206996"
}
]
},
"dataFlows": [
{
"streams": [
"Microsoft-Perf"
],
"destinations": [
"la--1138206996"
]
}
]
}
'@
Write-Output "Creating Linux DCR..."
$linuxDcrBody = $ExecutionContext.InvokeCommand.ExpandString($linuxDcrBody) | ConvertFrom-Json
New-AzResource -ResourceType "Microsoft.Insights/dataCollectionRules" -ResourceGroupName $resourceGroupName -Location $targetLocation -Name $linuxDcrName -PropertyObject $linuxDcrBody -ApiVersion "2021-04-01" -Tag $ResourceTags -Kind "Linux" -Force | Out-Null
Write-Host -ForegroundColor Green "Deployment completed successfully"
================================================
FILE: Setup-LogAnalyticsWorkspaces.ps1
================================================
param(
[Parameter(Mandatory = $false)]
[String] $AzureEnvironment = "AzureCloud",
[Parameter(Mandatory = $false)]
[String[]] $WorkspaceIds,
[Parameter(Mandatory = $false)]
[switch] $AutoFix,
[Parameter(Mandatory = $false)]
[int] $IntervalSeconds = 60
)
$ErrorActionPreference = "Stop"
$ctx = Get-AzContext
if (-not($ctx)) {
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
else {
if ($ctx.Environment.Name -ne $AzureEnvironment) {
Disconnect-AzAccount -ContextName $ctx.Name
Connect-AzAccount -Environment $AzureEnvironment
$ctx = Get-AzContext
}
}
$wsIds = foreach ($workspaceId in $WorkspaceIds)
{
"'$workspaceId'"
}
if ($wsIds)
{
$wsIds = $wsIds -join ","
$whereWsIds = " and properties.customerId in ($wsIds)"
}
$perfCounters = Get-Content -Path ".\perfcounters.json" | ConvertFrom-Json
$ARGPageSize = 1000
$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"}
$argQuery = "resources | where type =~ 'microsoft.operationalinsights/workspaces'$whereWsIds | order by id"
$workspaces = (Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions).data
Write-Output "Found $($workspaces.Count) workspaces."
$laQuery = "Heartbeat | where TimeGenerated > ago(1d) and ComputerEnvironment == 'Azure' | distinct Computer | summarize AzureComputersCount = count()"
foreach ($workspace in $workspaces) {
$laQueryResults = $null
$results = $null
$laQueryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspace.properties.customerId -Query $laQuery -Timespan (New-TimeSpan -Days 1) -ErrorAction Continue
if ($laQueryResults)
{
$results = [System.Linq.Enumerable]::ToArray($laQueryResults.Results)
Write-Output "$($workspace.name) ($($workspace.properties.customerId)): $($results.AzureComputersCount) Azure computers connected."
}
else
{
Write-Output "$($workspace.name) ($($workspace.properties.customerId)): could not validate connected computers."
}
if ($results.AzureComputersCount -gt 0)
{
if ($ctx.Subscription.SubscriptionId -ne $workspace.subscriptionId)
{
$ctx = Set-AzContext -SubscriptionId $workspace.subscriptionId
}
$dsWindows = Get-AzOperationalInsightsDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name -Kind WindowsPerformanceCounter
foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq "Windows"})) {
if (-not($dsWindows | Where-Object { $_.Properties.ObjectName -eq $perfCounter.objectName -and $_.Properties.InstanceName -eq $perfCounter.instance `
-and $_.Properties.CounterName -eq $perfCounter.counterName}))
{
Write-Output "Missing $($perfCounter.objectName)($($perfCounter.instance))\$($perfCounter.counterName)"
if ($AutoFix)
{
Write-Output "Fixing..."
$dsName = "DataSource_WindowsPerformanceCounter_$(New-Guid)"
New-AzOperationalInsightsWindowsPerformanceCounterDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name `
-Name $dsName -ObjectName $perfCounter.objectName -CounterName $perfCounter.counterName -InstanceName $perfCounter.instance `
-IntervalSeconds $IntervalSeconds -Force | Out-Null
}
}
}
$missingLinuxCounters = @()
$dsLinux = Get-AzOperationalInsightsDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name -Kind LinuxPerformanceObject
foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq "Linux"})) {
if (-not($dsLinux | Where-Object { $_.Properties.ObjectName -eq $perfCounter.objectName -and $_.Properties.InstanceName -eq $perfCounter.instance `
-and ($_.Properties.PerformanceCounters | Where-Object { $_.CounterName -eq $perfCounter.counterName }) }))
{
Write-Output "Missing $($perfCounter.objectName)($($perfCounter.instance))\$($perfCounter.counterName)"
if ($AutoFix)
{
$missingLinuxCounters += $perfCounter
}
}
}
if ($AutoFix)
{
$fixedLinuxCounters = @()
$existingLinuxObjects = ($dsLinux | Select-Object -ExpandProperty Properties | Select-Object -Property ObjectName).ObjectName
foreach ($linuxObject in $existingLinuxObjects) {
$missingObjectCounters = $missingLinuxCounters | Where-Object { $_.objectName -eq $linuxObject }
$originalDataSource = $dsLinux | Where-Object { $_.Properties.ObjectName -eq $linuxObject }
foreach ($perfCounter in $missingObjectCounters) {
$fixedLinuxCounters += $perfCounter
$newCounterName = New-Object -TypeName Microsoft.Azure.Commands.OperationalInsights.Models.PerformanceCounterIdentifier -Property @{CounterName = $perfCounter.counterName}
$originalDataSource.Properties.PerformanceCounters.Add($newCounterName)
}
if ($missingObjectCounters)
{
Write-Output "Fixing $linuxObject object..."
Set-AzOperationalInsightsDataSource -DataSource $originalDataSource | Out-Null
}
}
$missingObjects = ($missingLinuxCounters | Select-Object -Property objectName -Unique).objectName
$fixedObjects = ($fixedLinuxCounters | Select-Object -Property objectName -Unique).objectName
$missingObjects = $missingObjects | Where-Object { -not($_ -in $fixedObjects) }
foreach ($linuxObject in $missingObjects) {
$missingObjectCounters = $missingLinuxCounters | Where-Object { $_.objectName -eq $linuxObject }
$missingInstance = ($missingObjectCounters | Select-Object -Property instance -Unique -First 1).instance
$missingCounterNames = ($missingObjectCounters).counterName
Write-Output "Adding $linuxObject object..."
New-AzOperationalInsightsLinuxPerformanceObjectDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name `
-Name "DataSource_LinuxPerformanceObject_$(New-Guid)" -ObjectName $linuxObject -InstanceName $missingInstance -IntervalSeconds $IntervalSeconds `
-CounterNames $missingCounterNames -Force | Out-Null
}
}
}
}
================================================
FILE: Suppress-Recommendation.ps1
================================================
param(
[Parameter(Mandatory = $true)]
[String] $RecommendationId
)
$ErrorActionPreference = "Stop"
function Test-IsGuid
{
[OutputType([bool])]
param
(
[Parameter(Mandatory = $true)]
[string]$ObjectGuid
)
# Define verification regex
[regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$'
# Check guid against regex
return $ObjectGuid -match $guidRegex
}
if (-not(Test-IsGuid -ObjectGuid $RecommendationId))
{
Write-Host "The provided recommendation Id is invalid. Must be a valid GUID." -ForegroundColor Red
Exit
}
$databaseConnectionSettingsPath = ".\database-connection-settings.json"
$dbConnectionSettings = @{}
if (Test-Path -Path $databaseConnectionSettingsPath)
{
$dbSettings = Get-Content -Path $databaseConnectionSettingsPath | ConvertFrom-Json
Write-Host $dbSettings -ForegroundColor Green
$dbSettingsReuse = Read-Host "Found existing database connection settings. Do you want to reuse them (Y/N)?"
if ("Y", "y" -contains $dbSettingsReuse)
{
foreach ($property in $dbSettings.PSObject.Properties)
{
$dbConnectionSettings[$property.Name] = $property.Value
}
}
}
if (-not($dbConnectionSettings["DatabaseServer"]))
{
$databaseServer = Read-Host "Please, enter the AOE Azure SQL server hostname (e.g., xpto.database.windows.net)"
$dbConnectionSettings["DatabaseServer"] = $databaseServer
}
else
{
$databaseServer = $dbConnectionSettings["DatabaseServer"]
}
if (-not($dbConnectionSettings["DatabaseName"]))
{
$databaseName = Read-Host "Please, enter the AOE Azure SQL Database name (e.g., azureoptimization)"
$dbConnectionSettings["DatabaseName"] = $databaseName
}
else
{
$databaseName = $dbConnectionSettings["DatabaseName"]
}
if (-not($dbConnectionSettings["DatabaseUser"]))
{
$databaseUser = Read-Host "Please, enter the AOE database user name"
$dbConnectionSettings["DatabaseUser"] = $databaseUser
}
else
{
$databaseUser = $dbConnectionSettings["DatabaseUser"]
}
$sqlPass = Read-Host "Please, input the password for the $databaseUser SQL user" -AsSecureString
$sqlPassPlain = (New-Object PSCredential "user", $sqlPass).GetNetworkCredential().Password
$sqlPassPlain = $sqlPassPlain.Replace("'", "''")
$SqlTimeout = 120
$recommendationsTable = "Recommendations"
$suppressionsTable = "Filters"
Write-Host "Opening connection to the database..." -ForegroundColor Green
$tries = 0
$connectionSuccess = $false
do {
$tries++
try {
$Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$databaseServer,1433;Database=$databaseName;User ID=$databaseUser;Password='$sqlPassPlain';Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn.Open()
$Cmd=new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn
$Cmd.CommandTimeout = $SqlTimeout
$Cmd.CommandText = "SELECT * FROM [dbo].[$recommendationsTable] WHERE RecommendationId = '$RecommendationId'"
$sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$sqlAdapter.SelectCommand = $Cmd
$controlRows = New-Object System.Data.DataTable
$sqlAdapter.Fill($controlRows) | Out-Null
$connectionSuccess = $true
}
catch {
Write-Host "Failed to contact SQL at try $tries." -ForegroundColor Yellow
Write-Host $Error[0] -ForegroundColor Yellow
Write-Output "Waiting $($tries * 20) seconds..."
Start-Sleep -Seconds ($tries * 20)
}
} while (-not($connectionSuccess) -and $tries -lt 3)
if (-not($connectionSuccess))
{
throw "Could not establish connection to SQL."
}
$Conn.Close()
$Conn.Dispose()
if (-not($controlRows.RecommendationId))
{
Write-Host "The provided recommendation Id was not found. Please, try again with a valid GUID." -ForegroundColor Red
Exit
}
Write-Host "You are suppressing the recommendation with the below details" -ForegroundColor Green
Write-Host "Recommendation: $($controlRows.RecommendationDescription)" -ForegroundColor Blue
Write-Host "Recommendation sub-type id: $($controlRows.RecommendationSubTypeId)" -ForegroundColor Blue
Write-Host "Category: $($controlRows.Category)" -ForegroundColor Blue
Write-Host "Instance Name: $($controlRows.InstanceName)" -ForegroundColor Blue
Write-Host "Resource Group: $($controlRows.ResourceGroup)" -ForegroundColor Blue
Write-Host "Subscription Id: $($controlRows.SubscriptionGuid)" -ForegroundColor Blue
Write-Host "Please, choose the suppression type" -ForegroundColor Green
Write-Host "[E]xclude - this recommendation type will be completely excluded from the engine and will no longer be generated for any resource" -ForegroundColor Green
Write-Host "[D]ismiss - this recommendation will be dismissed for the scope to be chosen next (instance, resource group or subscription)" -ForegroundColor Green
Write-Host "[S]nooze - this recommendation will be postponed for the duration (in days) and scope to be chosen next (instance, resource group or subscription)" -ForegroundColor Green
Write-Host "[C]ancel - no action will be taken" -ForegroundColor Green
$suppOption = Read-Host "Enter your choice (E, D, S or C)"
if ("E", "e" -contains $suppOption)
{
$suppressionType = "Exclude"
}
elseif ("D", "d" -contains $suppOption)
{
$suppressionType = "Dismiss"
}
elseif ("S", "s" -contains $suppOption)
{
$suppressionType = "Snooze"
}
else
{
Write-Host "Cancelling.. No action will be taken." -ForegroundColor Green
Exit
}
if ($suppressionType -in ("Dismiss", "Snooze"))
{
Write-Host "Please, choose the scope for the suppression" -ForegroundColor Green
Write-Host "[S]ubscription ($($controlRows.SubscriptionGuid))" -ForegroundColor Green
Write-Host "[R]esource Group ($($controlRows.ResourceGroup))" -ForegroundColor Green
Write-Host "[I]nstance ($($controlRows.InstanceName))" -ForegroundColor Green
$scopeOption = Read-Host "Enter your choice (S, R, or I)"
if ("S", "s" -contains $scopeOption)
{
$scope = $controlRows.SubscriptionGuid
}
elseif ("R", "r" -contains $scopeOption)
{
$scope = $controlRows.ResourceGroup
}
elseif ("I", "i" -contains $scopeOption)
{
$scope = $controlRows.InstanceId
}
else
{
Write-Host "Wrong input. No action will be taken." -ForegroundColor Red
Exit
}
}
$snoozeDays = 0
if ($suppressionType -eq "Snooze")
{
Write-Host "Please, enter the number of days the recommendation will be snoozed" -ForegroundColor Green
$snoozeDays = Read-Host "Number of days (min. 14)"
if (-not($snoozeDays -ge 14))
{
Write-Host "Wrong snooze days. No action will be taken." -ForegroundColor Red
Exit
}
}
$author = Read-Host "Please enter your name"
$notes = Read-Host "Please enter a reason for this suppression"
Write-Host "You are about to suppress this recommendation" -ForegroundColor Yellow
Write-Host "Recommendation: $($controlRows.RecommendationDescription)" -ForegroundColor Blue
Write-Host "Suppression type: $suppressionType" -ForegroundColor Blue
if ($suppressionType -in ("Dismiss", "Snooze"))
{
Write-Host "Scope: $scope" -ForegroundColor Blue
}
if ($suppressionType -eq "Snooze")
{
Write-Host "Snooze days: $snoozeDays" -ForegroundColor Blue
}
Write-Host "Author: $author" -ForegroundColor Blue
Write-Host "Reason: $notes" -ForegroundColor Blue
$continueInput = Read-Host "Do you want to continue (Y/N)?"
if ("Y", "y" -contains $continueInput)
{
if ($scope)
{
$scope = "'$scope'"
}
else
{
$scope = "NULL"
}
if ($snoozeDays -ge 14)
{
$now = (Get-Date).ToUniversalTime()
$endDate = "'$($now.Add($snoozeDays).ToString("yyyy-MM-ddTHH:mm:00Z"))'"
}
else {
$endDate = "NULL"
}
$sqlStatement = "INSERT INTO [$suppressionsTable] VALUES (NEWID(), '$($controlRows.RecommendationSubTypeId)', '$suppressionType', $scope, GETDATE(), $endDate, '$author', '$notes', 1)"
$Conn2 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$databaseServer,1433;Database=$databaseName;User ID=$databaseUser;Password='$sqlPassPlain';Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;")
$Conn2.Open()
$Cmd=new-object system.Data.SqlClient.SqlCommand
$Cmd.Connection = $Conn2
$Cmd.CommandText = $sqlStatement
$Cmd.CommandTimeout=120
try
{
$Cmd.ExecuteReader()
}
catch
{
Write-Output "Failed statement: $sqlStatement"
throw
}
$Conn2.Close()
Write-Host "Suppression sucessfully added." -ForegroundColor Green
}
else
{
Write-Host "No action was taken." -ForegroundColor Green
}
$dbConnectionSettings | ConvertTo-Json | Out-File -FilePath $databaseConnectionSettingsPath -Force
================================================
FILE: azuredeploy-nested.bicep
================================================
param projectLocation string
param templateLocation string
param storageAccountName string
param automationAccountName string
param sqlServerName string
param sqlDatabaseName string
param logAnalyticsReuse bool
param logAnalyticsWorkspaceName string
param logAnalyticsWorkspaceRG string
param logAnalyticsRetentionDays int
param sqlBackupRetentionDays int
param sqlAdminLogin string
@secure()
param sqlAdminPassword string
param cloudEnvironment string
param authenticationOption string
param baseTime string
param resourceTags object
param contributorRoleAssignmentGuid string
param argDiskExportJobId string = newGuid()
param argVhdExportJobId string = newGuid()
param argVmExportJobId string = newGuid()
param argVmssExportJobId string = newGuid()
param argAvailSetExportJobId string = newGuid()
param advisorExportJobId string = newGuid()
param consumptionExportJobId string = newGuid()
param aadObjectsExportJobId string = newGuid()
param argLoadBalancersExportJobId string = newGuid()
param argAppGWsExportJobId string = newGuid()
param rbacExportJobId string = newGuid()
param argResContainersExportJobId string = newGuid()
param argNICExportJobId string = newGuid()
param argNSGExportJobId string = newGuid()
param argPublicIPExportJobId string = newGuid()
param argVNetExportJobId string = newGuid()
param argSqlDbExportJobId string = newGuid()
param policyStateExportJobId string = newGuid()
param monitorVmssCpuMaxExportJobId string = newGuid()
param monitorVmssCpuAvgExportJobId string = newGuid()
param monitorVmssMemoryMinExportJobId string = newGuid()
param monitorSqlDbDtuMaxExportJobId string = newGuid()
param monitorSqlDbDtuAvgExportJobId string = newGuid()
param monitorAppServiceCpuMaxExportJobId string = newGuid()
param monitorAppServiceCpuAvgExportJobId string = newGuid()
param monitorAppServiceMemoryMaxExportJobId string = newGuid()
param monitorAppServiceMemoryAvgExportJobId string = newGuid()
param monitorDiskIOPSAvgExportJobId string = newGuid()
param monitorDiskMBPsAvgExportJobId string = newGuid()
param argAppServicePlanExportJobId string = newGuid()
param pricesheetExportJobId string = newGuid()
param reservationPricesExportJobId string = newGuid()
param reservationUsageExportJobId string = newGuid()
param savingsPlansUsageExportJobId string = newGuid()
param argDiskIngestJobId string = newGuid()
param argVhdIngestJobId string = newGuid()
param argVmIngestJobId string = newGuid()
param argVmssIngestJobId string = newGuid()
param argAvailSetIngestJobId string = newGuid()
param advisorIngestJobId string = newGuid()
param remediationLogsIngestJobId string = newGuid()
param consumptionIngestJobId string = newGuid()
param aadObjectsIngestJobId string = newGuid()
param argLoadBalancersIngestJobId string = newGuid()
param argAppGWsIngestJobId string = newGuid()
param argResContainersIngestJobId string = newGuid()
param rbacIngestJobId string = newGuid()
param argNICIngestJobId string = newGuid()
param argNSGIngestJobId string = newGuid()
param argPublicIPIngestJobId string = newGuid()
param argVNetIngestJobId string = newGuid()
param argSqlDbIngestJobId string = newGuid()
param policyStateIngestJobId string = newGuid()
param monitorIngestJobId string = newGuid()
param argAppServicePlanIngestJobId string = newGuid()
param pricesheetIngestJobId string = newGuid()
param reservationPricesIngestJobId string = newGuid()
param reservationUsageIngestJobId string = newGuid()
param savingsPlansUsageIngestJobId string = newGuid()
param unattachedDisksRecommendationJobId string = newGuid()
param advisorCostAugmentedRecommendationJobId string = newGuid()
param advisorAsIsRecommendationJobId string = newGuid()
param vmsHaRecommendationJobId string = newGuid()
param vmOptimizationsRecommendationJobId string = newGuid()
param aadExpiringCredsRecommendationJobId string = newGuid()
param unusedLoadBalancersRecommendationJobId string = newGuid()
param unusedAppGWsRecommendationJobId string = newGuid()
param armOptimizationsRecommendationJobId string = newGuid()
param vnetOptimizationsRecommendationJobId string = newGuid()
param vmssOptimizationsRecommendationJobId string = newGuid()
param sqldbOptimizationsRecommendationJobId string = newGuid()
param storageOptimizationsRecommendationJobId string = newGuid()
param appServiceOptimizationsRecommendationJobId string = newGuid()
param diskOptimizationsRecommendationJobId string = newGuid()
param recommendationsIngestJobId string = newGuid()
param recommendationsLogAnalyticsIngestJobId string = newGuid()
param suppressionsLogAnalyticsIngestJobId string = newGuid()
param recommendationsCleanUpJobId string = newGuid()
param roleContributor string = '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
var advisorExportsRunbookName = 'Export-AdvisorRecommendationsToBlobStorage'
var argVmExportsRunbookName = 'Export-ARGVirtualMachinesPropertiesToBlobStorage'
var argVmssExportsRunbookName = 'Export-ARGVMSSPropertiesToBlobStorage'
var argDisksExportsRunbookName = 'Export-ARGManagedDisksPropertiesToBlobStorage'
var argVhdExportsRunbookName = 'Export-ARGUnmanagedDisksPropertiesToBlobStorage'
var argAvailSetExportsRunbookName = 'Export-ARGAvailabilitySetPropertiesToBlobStorage'
var consumptionExportsRunbookName = 'Export-ConsumptionToBlobStorage'
var aadObjectsExportsRunbookName = 'Export-AADObjectsToBlobStorage'
var argLoadBalancersExportsRunbookName = 'Export-ARGLoadBalancerPropertiesToBlobStorage'
var argAppGWsExportsRunbookName = 'Export-ARGAppGatewayPropertiesToBlobStorage'
var argResContainersExportsRunbookName = 'Export-ARGResourceContainersPropertiesToBlobStorage'
var rbacExportsRunbookName = 'Export-RBACAssignmentsToBlobStorage'
var argNICExportsRunbookName = 'Export-ARGNICPropertiesToBlobStorage'
var argNSGExportsRunbookName = 'Export-ARGNSGPropertiesToBlobStorage'
var argVNetExportsRunbookName = 'Export-ARGVNetPropertiesToBlobStorage'
var argPublicIpExportsRunbookName = 'Export-ARGPublicIpPropertiesToBlobStorage'
var argSqlDbExportsRunbookName = 'Export-ARGSqlDatabasePropertiesToBlobStorage'
var policyStateExportsRunbookName = 'Export-PolicyComplianceToBlobStorage'
var monitorExportsRunbookName = 'Export-AzMonitorMetricsToBlobStorage'
var argAppServicePlanExportsRunbookName = 'Export-ARGAppServicePlanPropertiesToBlobStorage'
var reservationsExportsRunbookName = 'Export-ReservationsUsageToBlobStorage'
var reservationsPriceExportsRunbookName = 'Export-ReservationsPriceToBlobStorage'
var priceSheetExportsRunbookName = 'Export-PriceSheetToBlobStorage'
var savingsPlansExportsRunbookName = 'Export-SavingsPlansUsageToBlobStorage'
var advisorExportsScheduleName = 'AzureOptimization_ExportAdvisorWeekly'
var argExportsScheduleName = 'AzureOptimization_ExportARGDaily'
var consumptionExportsScheduleName = 'AzureOptimization_ExportConsumptionDaily'
var aadObjectsExportsScheduleName = 'AzureOptimization_ExportAADObjectsDaily'
var rbacExportsScheduleName = 'AzureOptimization_ExportRBACDaily'
var policyStateExportsScheduleName = 'AzureOptimization_ExportPolicyStateDaily'
var monitorVmssCpuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorVmssCpuMaxHourly'
var monitorVmssCpuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorVmssCpuAvgHourly'
var monitorVmssMemoryMinExportsScheduleName = 'AzureOptimization_ExportMonitorVmssMemoryMinHourly'
var monitorSqlDbDtuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorSqlDbDtuMaxHourly'
var monitorSqlDbDtuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorSqlDbDtuAvgHourly'
var monitorAppServiceCpuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceCpuMaxHourly'
var monitorAppServiceCpuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceCpuAvgHourly'
var monitorAppServiceMemoryMaxExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceMemoryMaxHourly'
var monitorAppServiceMemoryAvgExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceMemoryAvgHourly'
var monitorDiskIOPSAvgExportsScheduleName = 'AzureOptimization_ExportMonitorDiskIOPSHourly'
var monitorDiskMBPsAvgExportsScheduleName = 'AzureOptimization_ExportMonitorDiskMBPsHourly'
var priceExportsScheduleName = 'AzureOptimization_ExportPricesWeekly'
var reservationsUsageExportsScheduleName = 'AzureOptimization_ExportReservationsDaily'
var savingsPlansUsageExportsScheduleName = 'AzureOptimization_ExportSavingsPlansDaily'
var csvExportsSchedules = [
{
exportSchedule: argExportsScheduleName
exportDescription: 'Daily Azure Resource Graph exports'
exportTimeOffset: 'PT1H05M'
exportFrequency: 'Day'
}
{
exportSchedule: advisorExportsScheduleName
exportDescription: 'Weekly Azure Advisor exports'
exportTimeOffset: 'PT1H15M'
exportFrequency: 'Week'
}
{
exportSchedule: consumptionExportsScheduleName
exportDescription: 'Daily Azure Consumption exports'
exportTimeOffset: 'PT1H'
exportFrequency: 'Day'
}
{
exportSchedule: aadObjectsExportsScheduleName
exportDescription: 'Daily Microsoft Entra Objects exports'
exportTimeOffset: 'PT1H'
exportFrequency: 'Day'
}
{
exportSchedule: rbacExportsScheduleName
exportDescription: 'Daily Azure RBAC exports'
exportTimeOffset: 'PT1H02M'
exportFrequency: 'Day'
}
{
exportSchedule: policyStateExportsScheduleName
exportDescription: 'Daily Azure Policy State exports'
exportTimeOffset: 'PT1H'
exportFrequency: 'Day'
}
{
exportSchedule: monitorVmssCpuAvgExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Percentage CPU (Avg.)'
exportTimeOffset: 'PT1H15M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorVmssCpuMaxExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Percentage CPU (Max.)'
exportTimeOffset: 'PT1H15M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorVmssMemoryMinExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Available Memory (Min.)'
exportTimeOffset: 'PT1H15M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorSqlDbDtuMaxExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for SQL Database Percentage DTU (Max.)'
exportTimeOffset: 'PT1H15M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorSqlDbDtuAvgExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for SQL Database Percentage DTU (Avg.)'
exportTimeOffset: 'PT1H16M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorAppServiceCpuAvgExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage CPU (Avg.)'
exportTimeOffset: 'PT1H16M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorAppServiceCpuMaxExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage CPU (Max.)'
exportTimeOffset: 'PT1H16M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorAppServiceMemoryAvgExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage RAM (Avg.)'
exportTimeOffset: 'PT1H16M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorAppServiceMemoryMaxExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage RAM (Max.)'
exportTimeOffset: 'PT1H17M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorDiskIOPSAvgExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for Disk IOPS (Avg.)'
exportTimeOffset: 'PT1H17M'
exportFrequency: 'Hour'
}
{
exportSchedule: monitorDiskMBPsAvgExportsScheduleName
exportDescription: 'Hourly Azure Monitor metrics exports for Disk MBPs (Avg.)'
exportTimeOffset: 'PT1H17M'
exportFrequency: 'Hour'
}
{
exportSchedule: priceExportsScheduleName
exportDescription: 'Weekly Pricesheet and Reservation Prices exports'
exportTimeOffset: 'PT1H35M'
exportFrequency: 'Week'
}
{
exportSchedule: reservationsUsageExportsScheduleName
exportDescription: 'Daily Reservation Usage exports'
exportTimeOffset: 'PT2H'
exportFrequency: 'Day'
}
{
exportSchedule: savingsPlansUsageExportsScheduleName
exportDescription: 'Daily Savings Plans Usage exports'
exportTimeOffset: 'PT2H05M'
exportFrequency: 'Day'
}
]
var csvExports = [
{
runbookName: advisorExportsRunbookName
isOneToMany: false
containerName: 'advisorexports'
variableName: 'AzureOptimization_AdvisorContainer'
variableDescription: 'The Storage Account container where Azure Advisor exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestAdvisorWeekly'
ingestDescription: 'Weekly Azure Advisor recommendations ingests'
ingestTimeOffset: 'PT1H45M'
ingestFrequency: 'Week'
ingestJobId: advisorIngestJobId
exportSchedule: advisorExportsScheduleName
exportJobId: advisorExportJobId
}
{
runbookName: argVmExportsRunbookName
isOneToMany: false
containerName: 'argvmexports'
variableName: 'AzureOptimization_ARGVMContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph Virtual Machine exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGVMsDaily'
ingestDescription: 'Daily Azure Resource Graph Virtual Machines ingests'
ingestTimeOffset: 'PT1H30M'
ingestFrequency: 'Day'
ingestJobId: argVmIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argVmExportJobId
}
{
runbookName: argVmssExportsRunbookName
isOneToMany: false
containerName: 'argvmssexports'
variableName: 'AzureOptimization_ARGVMSSContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph VMSS exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGVMSSDaily'
ingestDescription: 'Daily Azure Resource Graph VMSS ingests'
ingestTimeOffset: 'PT1H30M'
ingestFrequency: 'Day'
ingestJobId: argVmssIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argVmssExportJobId
}
{
runbookName: argDisksExportsRunbookName
isOneToMany: false
containerName: 'argdiskexports'
variableName: 'AzureOptimization_ARGDiskContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph Managed Disks exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGDisksDaily'
ingestDescription: 'Daily Azure Resource Graph Managed Disks ingests'
ingestTimeOffset: 'PT1H30M'
ingestFrequency: 'Day'
ingestJobId: argDiskIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argDiskExportJobId
}
{
runbookName: argVhdExportsRunbookName
isOneToMany: false
containerName: 'argvhdexports'
variableName: 'AzureOptimization_ARGVhdContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph Unmanaged Disks exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGVHDsDaily'
ingestDescription: 'Daily Azure Resource Graph Unmanaged Disks ingests'
ingestTimeOffset: 'PT1H30M'
ingestFrequency: 'Day'
ingestJobId: argVhdIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argVhdExportJobId
}
{
runbookName: argAvailSetExportsRunbookName
isOneToMany: false
containerName: 'argavailsetexports'
variableName: 'AzureOptimization_ARGAvailabilitySetContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph Availability Set exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGAvailSetsDaily'
ingestDescription: 'Daily Azure Resource Graph Availability Sets ingests'
ingestTimeOffset: 'PT1H31M'
ingestFrequency: 'Day'
ingestJobId: argAvailSetIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argAvailSetExportJobId
}
{
runbookName: consumptionExportsRunbookName
isOneToMany: false
containerName: 'consumptionexports'
variableName: 'AzureOptimization_ConsumptionContainer'
variableDescription: 'The Storage Account container where Azure Consumption exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestConsumptionDaily'
ingestDescription: 'Daily Azure Consumption ingests'
ingestTimeOffset: 'PT2H'
ingestFrequency: 'Day'
ingestJobId: consumptionIngestJobId
exportSchedule: consumptionExportsScheduleName
exportJobId: consumptionExportJobId
}
{
runbookName: aadObjectsExportsRunbookName
isOneToMany: false
containerName: 'aadobjectsexports'
variableName: 'AzureOptimization_AADObjectsContainer'
variableDescription: 'The Storage Account container where Microsoft Entra Objects exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestAADObjectsDaily'
ingestDescription: 'Daily Microsoft Entra Objects ingests'
ingestTimeOffset: 'PT2H'
ingestFrequency: 'Day'
ingestJobId: aadObjectsIngestJobId
exportSchedule: aadObjectsExportsScheduleName
exportJobId: aadObjectsExportJobId
}
{
runbookName: argLoadBalancersExportsRunbookName
isOneToMany: false
containerName: 'arglbexports'
variableName: 'AzureOptimization_ARGLoadBalancerContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph Load Balancer exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGLoadBalancersDaily'
ingestDescription: 'Daily Azure Resource Graph Load Balancers ingests'
ingestTimeOffset: 'PT1H31M'
ingestFrequency: 'Day'
ingestJobId: argLoadBalancersIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argLoadBalancersExportJobId
}
{
runbookName: argAppGWsExportsRunbookName
isOneToMany: false
containerName: 'argappgwexports'
variableName: 'AzureOptimization_ARGAppGatewayContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph Application Gateway exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGAppGWsDaily'
ingestDescription: 'Daily Azure Resource Graph Application Gateways ingests'
ingestTimeOffset: 'PT1H31M'
ingestFrequency: 'Day'
ingestJobId: argAppGWsIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argAppGWsExportJobId
}
{
runbookName: argResContainersExportsRunbookName
isOneToMany: false
containerName: 'argrescontainersexports'
variableName: 'AzureOptimization_ARGResourceContainersContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph Resource Containers exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGResourceContainersDaily'
ingestDescription: 'Daily Azure Resource Graph Resource Containers ingests'
ingestTimeOffset: 'PT1H32M'
ingestFrequency: 'Day'
ingestJobId: argResContainersIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argResContainersExportJobId
}
{
runbookName: rbacExportsRunbookName
isOneToMany: false
containerName: 'rbacexports'
variableName: 'AzureOptimization_RBACAssignmentsContainer'
variableDescription: 'The Storage Account container where RBAC Assignments exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestRBACDaily'
ingestDescription: 'Daily Azure RBAC ingests'
ingestTimeOffset: 'PT1H32M'
ingestFrequency: 'Day'
ingestJobId: rbacIngestJobId
exportSchedule: rbacExportsScheduleName
exportJobId: rbacExportJobId
}
{
runbookName: argNICExportsRunbookName
isOneToMany: false
containerName: 'argnicexports'
variableName: 'AzureOptimization_ARGNICContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph NIC exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGNICsDaily'
ingestDescription: 'Daily Azure Resource Graph NIC ingests'
ingestTimeOffset: 'PT1H32M'
ingestFrequency: 'Day'
ingestJobId: argNICIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argNICExportJobId
}
{
runbookName: argNSGExportsRunbookName
isOneToMany: false
containerName: 'argnsgexports'
variableName: 'AzureOptimization_ARGNSGContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph NSG exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGNSGsDaily'
ingestDescription: 'Daily Azure Resource Graph NSG ingests'
ingestTimeOffset: 'PT1H32M'
ingestFrequency: 'Day'
ingestJobId: argNSGIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argNSGExportJobId
}
{
runbookName: argVNetExportsRunbookName
isOneToMany: false
containerName: 'argvnetexports'
variableName: 'AzureOptimization_ARGVNetContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph VNet exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGVNetsDaily'
ingestDescription: 'Daily Azure Resource Graph Virtual Network ingests'
ingestTimeOffset: 'PT1H33M'
ingestFrequency: 'Day'
ingestJobId: argVNetIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argVNetExportJobId
}
{
runbookName: argPublicIpExportsRunbookName
isOneToMany: false
containerName: 'argpublicipexports'
variableName: 'AzureOptimization_ARGPublicIpContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph Public IP exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGPublicIPsDaily'
ingestDescription: 'Daily Azure Resource Graph Public IP ingests'
ingestTimeOffset: 'PT1H33M'
ingestFrequency: 'Day'
ingestJobId: argPublicIPIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argPublicIPExportJobId
}
{
runbookName: argSqlDbExportsRunbookName
isOneToMany: false
containerName: 'argsqldbexports'
variableName: 'AzureOptimization_ARGSqlDatabaseContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph SQL DB exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGSqlDbDaily'
ingestDescription: 'Daily Azure Resource Graph SQL DB ingests'
ingestTimeOffset: 'PT1H33M'
ingestFrequency: 'Day'
ingestJobId: argSqlDbIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argSqlDbExportJobId
}
{
runbookName: policyStateExportsRunbookName
isOneToMany: false
containerName: 'policystateexports'
variableName: 'AzureOptimization_PolicyStatesContainer'
variableDescription: 'The Storage Account container where Azure Policy State exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestPolicyStateDaily'
ingestDescription: 'Daily Azure Policy State ingests'
ingestTimeOffset: 'PT1H33M'
ingestFrequency: 'Day'
ingestJobId: policyStateIngestJobId
exportSchedule: policyStateExportsScheduleName
exportJobId: policyStateExportJobId
}
{
runbookName: monitorExportsRunbookName
isOneToMany: true
containerName: 'azmonitorexports'
variableName: 'AzureOptimization_AzMonitorContainer'
variableDescription: 'The Storage Account container where Azure Monitor metrics exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestAzMonitorMetricsHourly'
ingestDescription: 'Hourly Azure Monitor metrics ingests'
ingestTimeOffset: 'PT2H'
ingestFrequency: 'Hour'
ingestJobId: monitorIngestJobId
exportSchedule: null
exportJobId: 'dummy'
}
{
runbookName: argAppServicePlanExportsRunbookName
isOneToMany: false
containerName: 'argappserviceplanexports'
variableName: 'AzureOptimization_ARGAppServicePlanContainer'
variableDescription: 'The Storage Account container where Azure Resource Graph App Service Plan exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestARGAppServicePlanDaily'
ingestDescription: 'Daily Azure Resource Graph App Service Plan ingests'
ingestTimeOffset: 'PT1H34M'
ingestFrequency: 'Day'
ingestJobId: argAppServicePlanIngestJobId
exportSchedule: argExportsScheduleName
exportJobId: argAppServicePlanExportJobId
}
{
runbookName: priceSheetExportsRunbookName
isOneToMany: false
containerName: 'pricesheetexports'
variableName: 'AzureOptimization_PriceSheetContainer'
variableDescription: 'The Storage Account container where Pricesheet exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestPricesheetWeekly'
ingestDescription: 'Weekly Pricesheet ingests'
ingestTimeOffset: 'PT2H'
ingestFrequency: 'Week'
ingestJobId: pricesheetIngestJobId
exportSchedule: priceExportsScheduleName
exportJobId: pricesheetExportJobId
}
{
runbookName: reservationsPriceExportsRunbookName
isOneToMany: false
containerName: 'reservationspriceexports'
variableName: 'AzureOptimization_ReservationsPriceContainer'
variableDescription: 'The Storage Account container where Reservations Prices exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestReservationsPriceWeekly'
ingestDescription: 'Weekly Reservations Prices ingests'
ingestTimeOffset: 'PT2H'
ingestFrequency: 'Week'
ingestJobId: reservationPricesIngestJobId
exportSchedule: priceExportsScheduleName
exportJobId: reservationPricesExportJobId
}
{
runbookName: reservationsExportsRunbookName
isOneToMany: false
containerName: 'reservationsexports'
variableName: 'AzureOptimization_ReservationsContainer'
variableDescription: 'The Storage Account container where Reservations Usage exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestReservationsUsageDaily'
ingestDescription: 'Daily Reservations Usage ingests'
ingestTimeOffset: 'PT2H30M'
ingestFrequency: 'Day'
ingestJobId: reservationUsageIngestJobId
exportSchedule: reservationsUsageExportsScheduleName
exportJobId: reservationUsageExportJobId
}
{
runbookName: savingsPlansExportsRunbookName
isOneToMany: false
containerName: 'savingsplansexports'
variableName: 'AzureOptimization_SavingsPlansContainer'
variableDescription: 'The Storage Account container where Savings Plans Usage exports are dumped to'
ingestSchedule: 'AzureOptimization_IngestSavingsPlansUsageDaily'
ingestDescription: 'Daily Savings Plans Usage ingests'
ingestTimeOffset: 'PT2H35M'
ingestFrequency: 'Day'
ingestJobId: savingsPlansUsageIngestJobId
exportSchedule: savingsPlansUsageExportsScheduleName
exportJobId: savingsPlansUsageExportJobId
}
]
var csvParameterizedExports = [
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorVmssCpuMaxExportsScheduleName
exportJobId: monitorVmssCpuMaxExportJobId
parameters: {
ResourceType: 'microsoft.compute/virtualmachinescalesets'
TimeSpan: '01:00:00'
aggregationType: 'Maximum'
MetricNames: 'Percentage CPU'
TimeGrain: '01:00:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorVmssCpuAvgExportsScheduleName
exportJobId: monitorVmssCpuAvgExportJobId
parameters: {
ResourceType: 'microsoft.compute/virtualmachinescalesets'
TimeSpan: '01:00:00'
aggregationType: 'Average'
MetricNames: 'Percentage CPU'
TimeGrain: '01:00:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorVmssMemoryMinExportsScheduleName
exportJobId: monitorVmssMemoryMinExportJobId
parameters: {
ResourceType: 'microsoft.compute/virtualmachinescalesets'
TimeSpan: '01:00:00'
aggregationType: 'Minimum'
MetricNames: 'Available Memory Bytes'
TimeGrain: '01:00:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorSqlDbDtuMaxExportsScheduleName
exportJobId: monitorSqlDbDtuMaxExportJobId
parameters: {
ResourceType: 'microsoft.sql/servers/databases'
ARGFilter: 'sku.tier in (\'Standard\',\'Premium\')'
TimeSpan: '01:00:00'
aggregationType: 'Maximum'
MetricNames: 'dtu_consumption_percent'
TimeGrain: '01:00:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorSqlDbDtuAvgExportsScheduleName
exportJobId: monitorSqlDbDtuAvgExportJobId
parameters: {
ResourceType: 'microsoft.sql/servers/databases'
ARGFilter: 'sku.tier in (\'Standard\',\'Premium\')'
TimeSpan: '01:00:00'
aggregationType: 'Average'
AggregationOfType: 'Maximum'
MetricNames: 'dtu_consumption_percent'
TimeGrain: '00:01:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorAppServiceCpuMaxExportsScheduleName
exportJobId: monitorAppServiceCpuMaxExportJobId
parameters: {
ResourceType: 'microsoft.web/serverfarms'
ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\''
TimeSpan: '01:00:00'
aggregationType: 'Maximum'
MetricNames: 'CpuPercentage'
TimeGrain: '01:00:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorAppServiceCpuAvgExportsScheduleName
exportJobId: monitorAppServiceCpuAvgExportJobId
parameters: {
ResourceType: 'microsoft.web/serverfarms'
ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\''
TimeSpan: '01:00:00'
aggregationType: 'Average'
AggregationOfType: 'Maximum'
MetricNames: 'CpuPercentage'
TimeGrain: '00:01:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorAppServiceMemoryMaxExportsScheduleName
exportJobId: monitorAppServiceMemoryMaxExportJobId
parameters: {
ResourceType: 'microsoft.web/serverfarms'
ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\''
TimeSpan: '01:00:00'
aggregationType: 'Maximum'
MetricNames: 'MemoryPercentage'
TimeGrain: '01:00:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorAppServiceMemoryAvgExportsScheduleName
exportJobId: monitorAppServiceMemoryAvgExportJobId
parameters: {
ResourceType: 'microsoft.web/serverfarms'
ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\''
TimeSpan: '01:00:00'
aggregationType: 'Average'
AggregationOfType: 'Maximum'
MetricNames: 'MemoryPercentage'
TimeGrain: '00:01:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorDiskIOPSAvgExportsScheduleName
exportJobId: monitorDiskIOPSAvgExportJobId
parameters: {
ResourceType: 'microsoft.compute/disks'
ARGFilter: 'sku.name =~ \'Premium_LRS\' and properties.diskState != \'Unattached\''
TimeSpan: '01:00:00'
aggregationType: 'Average'
AggregationOfType: 'Maximum'
MetricNames: 'Composite Disk Read Operations/sec,Composite Disk Write Operations/sec'
TimeGrain: '00:01:00'
}
}
{
runbookName: monitorExportsRunbookName
exportSchedule: monitorDiskMBPsAvgExportsScheduleName
exportJobId: monitorDiskMBPsAvgExportJobId
parameters: {
ResourceType: 'microsoft.compute/disks'
ARGFilter: 'sku.name =~ \'Premium_LRS\' and properties.diskState != \'Unattached\''
TimeSpan: '01:00:00'
aggregationType: 'Average'
AggregationOfType: 'Maximum'
MetricNames: 'Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec'
TimeGrain: '00:01:00'
}
}
]
var unattachedDisksRecommendationsRunbookName = 'Recommend-UnattachedDisksToBlobStorage'
var advisorCostAugmentedRecommendationsRunbookName = 'Recommend-AdvisorCostAugmentedToBlobStorage'
var advisorAsIsRecommendationsRunbookName = 'Recommend-AdvisorAsIsToBlobStorage'
var vmsHARecommendationsRunbookName = 'Recommend-VMsHighAvailabilityToBlobStorage'
var vmOptimizationsRecommendationsRunbookName = 'Recommend-VMOptimizationsToBlobStorage'
var aadExpiringCredsRecommendationsRunbookName = 'Recommend-AADExpiringCredentialsToBlobStorage'
var unusedLBsRecommendationsRunbookName = 'Recommend-UnusedLoadBalancersToBlobStorage'
var unusedAppGWsRecommendationsRunbookName = 'Recommend-UnusedAppGWsToBlobStorage'
var armOptimizationsRecommendationsRunbookName = 'Recommend-ARMOptimizationsToBlobStorage'
var vnetOptimizationsRecommendationsRunbookName = 'Recommend-VNetOptimizationsToBlobStorage'
var vmssOptimizationsRecommendationsRunbookName = 'Recommend-VMSSOptimizationsToBlobStorage'
var sqldbOptimizationsRecommendationsRunbookName = 'Recommend-SqlDbOptimizationsToBlobStorage'
var storageOptimizationsRecommendationsRunbookName = 'Recommend-StorageAccountOptimizationsToBlobStorage'
var appServiceOptimizationsRecommendationsRunbookName = 'Recommend-AppServiceOptimizationsToBlobStorage'
var diskOptimizationsRecommendationsRunbookName = 'Recommend-DiskOptimizationsToBlobStorage'
var cleanUpOlderRecommendationsRunbookName = 'CleanUp-OlderRecommendationsFromSqlServer'
var recommendations = [
{
recommendationJobId: unattachedDisksRecommendationJobId
runbookName: unattachedDisksRecommendationsRunbookName
}
{
recommendationJobId: advisorCostAugmentedRecommendationJobId
runbookName: advisorCostAugmentedRecommendationsRunbookName
}
{
recommendationJobId: advisorAsIsRecommendationJobId
runbookName: advisorAsIsRecommendationsRunbookName
}
{
recommendationJobId: vmsHaRecommendationJobId
runbookName: vmsHARecommendationsRunbookName
}
{
recommendationJobId: vmOptimizationsRecommendationJobId
runbookName: vmOptimizationsRecommendationsRunbookName
}
{
recommendationJobId: aadExpiringCredsRecommendationJobId
runbookName: aadExpiringCredsRecommendationsRunbookName
}
{
recommendationJobId: unusedLoadBalancersRecommendationJobId
runbookName: unusedLBsRecommendationsRunbookName
}
{
recommendationJobId: unusedAppGWsRecommendationJobId
runbookName: unusedAppGWsRecommendationsRunbookName
}
{
recommendationJobId: armOptimizationsRecommendationJobId
runbookName: armOptimizationsRecommendationsRunbookName
}
{
recommendationJobId: vnetOptimizationsRecommendationJobId
runbookName: vnetOptimizationsRecommendationsRunbookName
}
{
recommendationJobId: vmssOptimizationsRecommendationJobId
runbookName: vmssOptimizationsRecommendationsRunbookName
}
{
recommendationJobId: sqldbOptimizationsRecommendationJobId
runbookName: sqldbOptimizationsRecommendationsRunbookName
}
{
recommendationJobId: storageOptimizationsRecommendationJobId
runbookName: storageOptimizationsRecommendationsRunbookName
}
{
recommendationJobId: appServiceOptimizationsRecommendationJobId
runbookName: appServiceOptimizationsRecommendationsRunbookName
}
{
recommendationJobId: diskOptimizationsRecommendationJobId
runbookName: diskOptimizationsRecommendationsRunbookName
}
]
var remediationLogsContainerName = 'remediationlogs'
var recommendationsContainerName = 'recommendationsexports'
var csvIngestRunbookName = 'Ingest-OptimizationCSVExportsToLogAnalytics'
var recommendationsIngestRunbookName = 'Ingest-RecommendationsToSQLServer'
var recommendationsLogAnalyticsIngestRunbookName = 'Ingest-RecommendationsToLogAnalytics'
var suppressionsLogAnalyticsIngestRunbookName = 'Ingest-SuppressionsToLogAnalytics'
var advisorRightSizeFilteredRemediationRunbookName = 'Remediate-AdvisorRightSizeFiltered'
var longDeallocatedVMsFilteredRemediationRunbookName = 'Remediate-LongDeallocatedVMsFiltered'
var unattachedDisksFilteredRemediationRunbookName = 'Remediate-UnattachedDisksFiltered'
var remediationLogsIngestScheduleName = 'AzureOptimization_IngestRemediationLogsDaily'
var recommendationsScheduleName = 'AzureOptimization_RecommendationsWeekly'
var recommendationsIngestScheduleName = 'AzureOptimization_IngestRecommendationsWeekly'
var suppressionsIngestScheduleName = 'AzureOptimization_IngestSuppressionsWeekly'
var recommendationsCleanUpScheduleName = 'AzureOptimization_CleanUpRecommendationsWeekly'
var Az_Accounts = {
name: 'Az.Accounts'
url: 'https://www.powershellgallery.com/api/v2/package/Az.Accounts/2.12.1'
}
var Microsoft_Graph_Authentication = {
name: 'Microsoft.Graph.Authentication'
url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Authentication/2.4.0'
}
var psModules = [
{
name: 'Az.Compute'
url: 'https://www.powershellgallery.com/api/v2/package/Az.Compute/5.7.0'
}
{
name: 'Az.OperationalInsights'
url: 'https://www.powershellgallery.com/api/v2/package/Az.OperationalInsights/3.2.0'
}
{
name: 'Az.ResourceGraph'
url: 'https://www.powershellgallery.com/api/v2/package/Az.ResourceGraph/0.13.0'
}
{
name: 'Az.Storage'
url: 'https://www.powershellgallery.com/api/v2/package/Az.Storage/5.5.0'
}
{
name: 'Az.Resources'
url: 'https://www.powershellgallery.com/api/v2/package/Az.Resources/6.6.0'
}
{
name: 'Az.Monitor'
url: 'https://www.powershellgallery.com/api/v2/package/Az.Monitor/4.4.1'
}
{
name: 'Az.PolicyInsights'
url: 'https://www.powershellgallery.com/api/v2/package/Az.PolicyInsights/1.6.0'
}
{
name: 'Microsoft.Graph.Users'
url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Users/2.4.0'
}
{
name: 'Microsoft.Graph.Groups'
url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Groups/2.4.0'
}
{
name: 'Microsoft.Graph.Applications'
url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Applications/2.4.0'
}
{
name: 'Microsoft.Graph.Identity.DirectoryManagement'
url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Identity.DirectoryManagement/2.4.0'
}
]
var runbooks = [
{
name: advisorExportsRunbookName
version: '1.4.2.1'
description: 'Exports Azure Advisor recommendations to Blob Storage using the Advisor API'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${advisorExportsRunbookName}.ps1')
}
{
name: argDisksExportsRunbookName
version: '1.3.4.1'
description: 'Exports Managed Disks properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argDisksExportsRunbookName}.ps1')
}
{
name: argVhdExportsRunbookName
version: '1.1.4.1'
description: 'Exports Unmanaged Disks (owned by a VM) properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVhdExportsRunbookName}.ps1')
}
{
name: argVmExportsRunbookName
version: '1.4.4.1'
description: 'Exports Virtual Machine properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVmExportsRunbookName}.ps1')
}
{
name: argVmssExportsRunbookName
version: '1.0.2.1'
description: 'Exports VMSS properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVmssExportsRunbookName}.ps1')
}
{
name: argAvailSetExportsRunbookName
version: '1.1.4.1'
description: 'Exports Availability Set properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAvailSetExportsRunbookName}.ps1')
}
{
name: consumptionExportsRunbookName
version: '2.0.4.1'
description: 'Exports Azure Consumption events to Blob Storage using Azure Consumption API'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${consumptionExportsRunbookName}.ps1')
}
{
name: aadObjectsExportsRunbookName
version: '1.2.2.1'
description: 'Exports Azure AAD Objects to Blob Storage using Azure ARM API'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${aadObjectsExportsRunbookName}.ps1')
}
{
name: argLoadBalancersExportsRunbookName
version: '1.1.4.1'
description: 'Exports Load Balancer properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argLoadBalancersExportsRunbookName}.ps1')
}
{
name: argAppGWsExportsRunbookName
version: '1.1.4.1'
description: 'Exports Application Gateway properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAppGWsExportsRunbookName}.ps1')
}
{
name: argResContainersExportsRunbookName
version: '1.0.5.1'
description: 'Exports Resource Containers properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argResContainersExportsRunbookName}.ps1')
}
{
name: rbacExportsRunbookName
version: '1.0.4.1'
description: 'Exports RBAC assignments to Blob Storage using ARM and Microsoft Entra'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${rbacExportsRunbookName}.ps1')
}
{
name: argNICExportsRunbookName
version: '1.0.2.1'
description: 'Exports NIC properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argNICExportsRunbookName}.ps1')
}
{
name: argNSGExportsRunbookName
version: '1.0.2.1'
description: 'Exports NSG properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argNSGExportsRunbookName}.ps1')
}
{
name: argPublicIpExportsRunbookName
version: '1.0.2.1'
description: 'Exports Public IP properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argPublicIpExportsRunbookName}.ps1')
}
{
name: argVNetExportsRunbookName
version: '1.0.2.1'
description: 'Exports VNet properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVNetExportsRunbookName}.ps1')
}
{
name: argSqlDbExportsRunbookName
version: '1.0.2.1'
description: 'Exports SQL DB properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argSqlDbExportsRunbookName}.ps1')
}
{
name: policyStateExportsRunbookName
version: '1.0.3.1'
description: 'Exports Azure Policy State to Blob Storage'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${policyStateExportsRunbookName}.ps1')
}
{
name: monitorExportsRunbookName
version: '1.0.2.1'
description: 'Exports Azure Monitor metrics to Blob Storage'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${monitorExportsRunbookName}.ps1')
}
{
name: argAppServicePlanExportsRunbookName
version: '1.0.1.1'
description: 'Exports App Service Plan properties to Blob Storage using Azure Resource Graph'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAppServicePlanExportsRunbookName}.ps1')
}
{
name: reservationsExportsRunbookName
version: '1.1.2.1'
description: 'Exports Reservations Usage to Blob Storage using the EA or MCA APIs'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${reservationsExportsRunbookName}.ps1')
}
{
name: reservationsPriceExportsRunbookName
version: '1.0.1.1'
description: 'Exports Reservations Prices to Blob Storage using the Retail Prices API'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${reservationsPriceExportsRunbookName}.ps1')
}
{
name: priceSheetExportsRunbookName
version: '1.1.1.1'
description: 'Exports Price Sheet to Blob Storage using the EA or MCA APIs'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${priceSheetExportsRunbookName}.ps1')
}
{
name: savingsPlansExportsRunbookName
version: '1.0.0.0'
description: 'Exports Savings Plans Usage to Blob Storage using the EA or MCA APIs'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${savingsPlansExportsRunbookName}.ps1')
}
{
name: csvIngestRunbookName
version: '1.5.0.0'
description: 'Ingests CSV blobs as custom logs to Log Analytics'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/data-collection/${csvIngestRunbookName}.ps1')
}
{
name: unattachedDisksRecommendationsRunbookName
version: '2.4.8.0'
description: 'Generates unattached disks recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${unattachedDisksRecommendationsRunbookName}.ps1')
}
{
name: advisorCostAugmentedRecommendationsRunbookName
version: '2.9.1.0'
description: 'Generates augmented Advisor Cost recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorCostAugmentedRecommendationsRunbookName}.ps1')
}
{
name: advisorAsIsRecommendationsRunbookName
version: '1.5.5.0'
description: 'Generates all types of Advisor recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorAsIsRecommendationsRunbookName}.ps1')
}
{
name: vmsHARecommendationsRunbookName
version: '1.0.3.0'
description: 'Generates VMs High Availability recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmsHARecommendationsRunbookName}.ps1')
}
{
name: vmOptimizationsRecommendationsRunbookName
version: '1.0.0.0'
description: 'Generates VM optimizations recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmOptimizationsRecommendationsRunbookName}.ps1')
}
{
name: aadExpiringCredsRecommendationsRunbookName
version: '1.1.10.0'
description: 'Generates AAD Objects with expiring credentials recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${aadExpiringCredsRecommendationsRunbookName}.ps1')
}
{
name: unusedLBsRecommendationsRunbookName
version: '1.2.9.0'
description: 'Generates unused Load Balancers recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedLBsRecommendationsRunbookName}.ps1')
}
{
name: unusedAppGWsRecommendationsRunbookName
version: '1.2.9.0'
description: 'Generates unused Application Gateways recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedAppGWsRecommendationsRunbookName}.ps1')
}
{
name: armOptimizationsRecommendationsRunbookName
version: '1.0.3.0'
description: 'Generates ARM optimizations recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${armOptimizationsRecommendationsRunbookName}.ps1')
}
{
name: vnetOptimizationsRecommendationsRunbookName
version: '1.0.4.0'
description: 'Generates Virtual Network optimizations recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${vnetOptimizationsRecommendationsRunbookName}.ps1')
}
{
name: vmssOptimizationsRecommendationsRunbookName
version: '1.1.1.0'
description: 'Generates VM Scale Set optimizations recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmssOptimizationsRecommendationsRunbookName}.ps1')
}
{
name: sqldbOptimizationsRecommendationsRunbookName
version: '1.1.2.0'
description: 'Generates SQL DB optimizations recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${sqldbOptimizationsRecommendationsRunbookName}.ps1')
}
{
name: storageOptimizationsRecommendationsRunbookName
version: '1.0.3.0'
description: 'Generates Storage Account optimizations recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${storageOptimizationsRecommendationsRunbookName}.ps1')
}
{
name: appServiceOptimizationsRecommendationsRunbookName
version: '1.0.3.0'
description: 'Generates App Service optimizations recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${appServiceOptimizationsRecommendationsRunbookName}.ps1')
}
{
name: diskOptimizationsRecommendationsRunbookName
version: '1.1.1.0'
description: 'Generates Disk optimizations recommendations'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${diskOptimizationsRecommendationsRunbookName}.ps1')
}
{
name: recommendationsIngestRunbookName
version: '1.6.5.0'
description: 'Ingests JSON-based recommendations into an Azure SQL Database'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${recommendationsIngestRunbookName}.ps1')
}
{
name: recommendationsLogAnalyticsIngestRunbookName
version: '1.0.2.0'
description: 'Ingests JSON-based recommendations into Log Analytics'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${recommendationsLogAnalyticsIngestRunbookName}.ps1')
}
{
name: suppressionsLogAnalyticsIngestRunbookName
version: '1.0.0.0'
description: 'Ingests suppressions into Log Analytics'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/recommendations/${suppressionsLogAnalyticsIngestRunbookName}.ps1')
}
{
name: advisorRightSizeFilteredRemediationRunbookName
version: '1.2.4.0'
description: 'Remediates Azure Advisor right-size recommendations given fit and tag filters'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/remediations/${advisorRightSizeFilteredRemediationRunbookName}.ps1')
}
{
name: longDeallocatedVMsFilteredRemediationRunbookName
version: '1.0.3.0'
description: 'Remediates long-deallocated VMs recommendations given fit and tag filters'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/remediations/${longDeallocatedVMsFilteredRemediationRunbookName}.ps1')
}
{
name: unattachedDisksFilteredRemediationRunbookName
version: '1.0.3.0'
description: 'Remediates unattached disks recommendations given fit and tag filters'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/remediations/${unattachedDisksFilteredRemediationRunbookName}.ps1')
}
{
name: cleanUpOlderRecommendationsRunbookName
version: '1.0.0.0'
description: 'Cleans up older recommendations from SQL Database'
type: 'PowerShell'
scriptUri: uri(templateLocation, 'runbooks/maintenance/${cleanUpOlderRecommendationsRunbookName}.ps1')
}
]
var automationVariables = [
{
name: 'AzureOptimization_CloudEnvironment'
description: 'Azure Cloud environment (e.g., AzureCloud, AzureChinaCloud, etc.)'
value: '"${cloudEnvironment}"'
}
{
name: 'AzureOptimization_AuthenticationOption'
description: 'Runbook authentication type (RunAsAccount or ManagedIdentity)'
value: '"${authenticationOption}"'
}
{
name: 'AzureOptimization_StorageSink'
description: 'The Azure Storage Account where data source exports are dumped to'
value: '"${storageAccountName}"'
}
{
name: 'AzureOptimization_StorageSinkRG'
description: 'The resource group for the Azure Storage Account sink'
value: '"${resourceGroup().name}"'
}
{
name: 'AzureOptimization_StorageSinkSubId'
description: 'The subscription Id for the Azure Storage Account sink'
value: '"${subscription().subscriptionId}"'
}
{
name: 'AzureOptimization_ConsumptionOffsetDays'
description: 'The offset (in days) for querying for consumption data'
value: 3
}
{
name: 'AzureOptimization_AdvisorFilter'
description: 'The category filter to use for Azure Advisor (non-Cost) recommendations exports'
value: '"HighAvailability,Security,Performance,OperationalExcellence"'
}
{
name: 'AzureOptimization_ReferenceRegion'
description: 'The Azure region used as a reference for getting details about Azure VM sizes available'
value: '"${projectLocation}"'
}
{
name: 'AzureOptimization_SQLServerDatabase'
description: 'The Azure SQL Database name for the ingestion control and recommendations tables'
value: '"${sqlDatabaseName}"'
}
{
name: 'AzureOptimization_LogAnalyticsChunkSize'
description: 'The size (in rows) for each chunk of Log Analytics ingestion request'
value: 6000
}
{
name: 'AzureOptimization_StorageBlobsPageSize'
description: 'The size (in blobs count) for each page of Storage Account container blob listing'
value: 1000
}
{
name: 'AzureOptimization_SQLServerInsertSize'
description: 'The size (in inserted lines) for each page of recommendations ingestion into the SQL Database'
value: 900
}
{
name: 'AzureOptimization_LogAnalyticsLogPrefix'
description: 'The prefix for all Azure Optimization custom log tables in Log Analytics'
value: '"AzureOptimization"'
}
{
name: 'AzureOptimization_LogAnalyticsWorkspaceName'
description: 'The Log Analytics Workspace Name where optimization data will be ingested'
value: '"${logAnalyticsWorkspaceName}"'
}
{
name: 'AzureOptimization_LogAnalyticsWorkspaceRG'
description: 'The resource group for the Log Analytics Workspace where optimization data will be ingested'
value: '"${((!logAnalyticsReuse) ? resourceGroup().name : logAnalyticsWorkspaceRG)}"'
}
{
name: 'AzureOptimization_LogAnalyticsWorkspaceSubId'
description: 'The Azure subscription for the Log Analytics Workspace where optimization data will be ingested'
value: '"${subscription().subscriptionId}"'
}
{
name: 'AzureOptimization_LogAnalyticsWorkspaceTenantId'
description: 'The Microsoft Entra tenant for the Log Analytics Workspace where optimization data will be ingested'
value: '"${subscription().tenantId}"'
}
{
name: 'AzureOptimization_PriceSheetMeterCategories'
description: 'Comma-separated meter categories to be included in the Price Sheet (remove variable to include all categories)'
value: '"Virtual Machines,Storage"'
}
{
name: 'AzureOptimization_RetailPricesCurrencyCode'
description: 'The currency code to be used for the retail prices exports (used for Reservations prices)'
value: '"EUR"'
}
{
name: 'AzureOptimization_RecommendAdvisorPeriodInDays'
description: 'The period (in days) to look back for Advisor exported recommendations'
value: 7
}
{
name: 'AzureOptimization_RecommendationLongDeallocatedVmsIntervalDays'
description: 'The period (in days) for considering a VM long deallocated'
value: 30
}
{
name: 'AzureOptimization_PerfPercentileCpu'
description: 'The percentile to be used for processor metrics'
value: 99
}
{
name: 'AzureOptimization_PerfPercentileMemory'
description: 'The percentile to be used for memory metrics'
value: 99
}
{
name: 'AzureOptimization_PerfPercentileNetwork'
description: 'The percentile to be used for network metrics'
value: 99
}
{
name: 'AzureOptimization_PerfPercentileDisk'
description: 'The percentile to be used for disk metrics'
value: 99
}
{
name: 'AzureOptimization_PerfPercentileSqlDtu'
description: 'The percentile to be used for SQL DB DTU metrics'
value: 99
}
{
name: 'AzureOptimization_PerfThresholdCpuPercentage'
description: 'The processor usage percentage threshold above which the fit score is decreased or below which the instance is considered underutilized'
value: 30
}
{
name: 'AzureOptimization_PerfThresholdMemoryPercentage'
description: 'The memory usage percentage threshold above which the fit score is decreased or below which the instance is considered underutilized'
value: 50
}
{
name: 'AzureOptimization_PerfThresholdCpuDegradedMaxPercentage'
description: 'The maximum processor usage percentage threshold above which the instance is considered degraded'
value: 95
}
{
name: 'AzureOptimization_PerfThresholdCpuDegradedAvgPercentage'
description: 'The average processor usage percentage threshold above which the instance is considered degraded'
value: 75
}
{
name: 'AzureOptimization_PerfThresholdMemoryDegradedPercentage'
description: 'The memory usage percentage threshold above which the instance is considered degraded'
value: 90
}
{
name: 'AzureOptimization_PerfThresholdNetworkMbps'
description: 'The network usage threshold (in Mbps) above which the fit score is decreased'
value: 750
}
{
name: 'AzureOptimization_PerfThresholdCpuShutdownPercentage'
description: 'The processor usage percentage threshold above which the fit score is decreased (shutdown scenarios)'
value: 5
}
{
name: 'AzureOptimization_PerfThresholdMemoryShutdownPercentage'
description: 'The memory usage percentage threshold above which the fit score is decreased (shutdown scenarios)'
value: 100
}
{
name: 'AzureOptimization_PerfThresholdNetworkShutdownMbps'
description: 'The network usage threshold (in Mbps) above which the fit score is decreased (shutdown scenarios)'
value: 10
}
{
name: 'AzureOptimization_PerfThresholdDtuPercentage'
description: 'The DTU usage percentage threshold below which a SQL Database instance is considered underutilized'
value: 40
}
{
name: 'AzureOptimization_PerfThresholdDtuDegradedPercentage'
description: 'The DTU usage percentage threshold above which a SQL Database instance is considered performance degraded'
value: 75
}
{
name: 'AzureOptimization_PerfThresholdDiskIOPSPercentage'
description: 'The IOPS usage percentage threshold below which a Disk is considered underutilized'
value: 5
}
{
name: 'AzureOptimization_PerfThresholdDiskMBsPercentage'
description: 'The throughput (MBps) usage percentage threshold below which a Disk is considered underutilized'
value: 5
}
{
name: 'AzureOptimization_RemediateRightSizeMinFitScore'
description: 'The minimum fit score for right-size remediation'
value: '"5.0"'
}
{
name: 'AzureOptimization_RemediateRightSizeMinWeeksInARow'
description: 'The minimum number of weeks in a row required for a right-size recommendation to be remediated'
value: 4
}
{
name: 'AzureOptimization_RecommendationAdvisorCostRightSizeId'
description: 'The Azure Advisor VM right-size recommendation ID'
value: '"e10b1381-5f0a-47ff-8c7b-37bd13d7c974"'
}
{
name: 'AzureOptimization_RemediateLongDeallocatedVMsMinFitScore'
description: 'The minimum fit score for long-deallocated VM remediation'
value: '"5.0"'
}
{
name: 'AzureOptimization_RemediateLongDeallocatedVMsMinWeeksInARow'
description: 'The minimum number of weeks in a row required for a long-deallocated VM recommendation to be remediated'
value: 4
}
{
name: 'AzureOptimization_RecommendationLongDeallocatedVMsId'
description: 'The long deallocated VM recommendation ID'
value: '"c320b790-2e58-452a-aa63-7b62c383ad8a"'
}
{
name: 'AzureOptimization_RemediateUnattachedDisksMinFitScore'
description: 'The minimum fit score for unattached disk remediation'
value: '"5.0"'
}
{
name: 'AzureOptimization_RemediateUnattachedDisksMinWeeksInARow'
description: 'The minimum number of weeks in a row required for a unattached disk recommendation to be remediated'
value: 4
}
{
name: 'AzureOptimization_RemediateUnattachedDisksAction'
description: 'The action for the unattached disk recommendation to be remediated (Delete or Downsize)'
value: '"Delete"'
}
{
name: 'AzureOptimization_RecommendationUnattachedDisksId'
description: 'The unattached disk recommendation ID'
value: '"c84d5e86-e2d6-4d62-be7c-cecfbd73b0db"'
}
{
name: 'AzureOptimization_RecommendationAADMinCredValidityDays'
description: 'The minimum validity of an AAD Object credential in days'
value: 30
}
{
name: 'AzureOptimization_RecommendationAADMaxCredValidityYears'
description: 'The maximum validity of an AAD Object credential in years'
value: 2
}
{
name: 'AzureOptimization_AADObjectsFilter'
description: 'The Microsoft Entra object types to export'
value: '"Application,ServicePrincipal,User,Group"'
}
{
name: 'AzureOptimization_RecommendationRBACAssignmentsPercentageThreshold'
description: 'The percentage threshold (used to trigger recommendations) for total RBAC assignments limits'
value: 80
}
{
name: 'AzureOptimization_RecommendationResourceGroupsPerSubPercentageThreshold'
description: 'The percentage threshold (used to trigger recommendations) for resource group count limits'
value: 80
}
{
name: 'AzureOptimization_RecommendationVNetSubnetMaxUsedPercentageThreshold'
description: 'The percentage threshold (used to trigger recommendations) for maximum subnet address space usage'
value: 80
}
{
name: 'AzureOptimization_RecommendationVNetSubnetMinUsedPercentageThreshold'
description: 'The percentage threshold (used to trigger recommendations) for minimum subnet address space usage'
value: 5
}
{
name: 'AzureOptimization_RecommendationVNetSubnetEmptyMinAgeInDays'
description: 'The minimum age (in days) for an empty subnet to trigger an NSG rule recommendation'
value: 30
}
{
name: 'AzureOptimization_RecommendationsMaxAgeInDays'
description: 'The maximum age (in days) for a recommendation to be kept in the SQL database'
value: 365
}
{
name: 'AzureOptimization_RecommendationStorageAcountGrowthThresholdPercentage'
description: 'The minimum Storage Account growth percentage required to flag Storage as not having a retention policy in place'
value: 5
}
{
name: 'AzureOptimization_RecommendationStorageAcountGrowthMonthlyCostThreshold'
description: 'The minimum monthly cost (in your EA/MCA currency) required to flag Storage as not having a retention policy in place'
value: 50
}
{
name: 'AzureOptimization_RecommendationStorageAcountGrowthLookbackDays'
description: 'The lookback period (in days) for analyzing Storage Account growth'
value: 30
}
]
resource logAnalyticsWorkspace 'microsoft.operationalinsights/workspaces@2020-08-01' = if (!logAnalyticsReuse) {
name: logAnalyticsWorkspaceName
location: projectLocation
tags: resourceTags
properties: {
sku: {
name: 'pergb2018'
}
retentionInDays: logAnalyticsRetentionDays
}
}
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
name: storageAccountName
location: projectLocation
tags: resourceTags
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: false
networkAcls: {
bypass: 'AzureServices'
virtualNetworkRules: []
ipRules: []
defaultAction: 'Allow'
}
supportsHttpsTrafficOnly: true
encryption: {
services: {
file: {
enabled: true
}
blob: {
enabled: true
}
}
keySource: 'Microsoft.Storage'
}
minimumTlsVersion: 'TLS1_2'
accessTier: 'Cool'
}
}
resource storageBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = {
parent: storageAccount
name: 'default'
properties: {
cors: {
corsRules: []
}
deleteRetentionPolicy: {
enabled: false
}
}
}
resource storageCsvExportsContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = [for item in csvExports: {
name: '${storageAccountName}/default/${item.containerName}'
properties: {
publicAccess: 'None'
}
dependsOn: [
storageBlobServices
storageAccount
]
}]
resource storageRecommendationsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = {
name: '${storageAccountName}/default/${recommendationsContainerName}'
properties: {
publicAccess: 'None'
}
dependsOn: [
storageBlobServices
storageAccount
]
}
resource storageRemediationLogsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = {
name: '${storageAccountName}/default/${remediationLogsContainerName}'
properties: {
publicAccess: 'None'
}
dependsOn: [
storageBlobServices
storageAccount
]
}
resource storageLifecycleManagementPolicy 'Microsoft.Storage/storageAccounts/managementPolicies@2021-02-01' = {
parent: storageAccount
name: 'default'
properties: {
policy: {
rules: [
{
enabled: true
name: 'Clean6MonthsOldBlobs'
type: 'Lifecycle'
definition: {
actions: {
baseBlob: {
delete: {
daysAfterModificationGreaterThan: 180
}
}
snapshot: {
delete: {
daysAfterCreationGreaterThan: 180
}
}
version: {
delete: {
daysAfterCreationGreaterThan: 180
}
}
}
filters: {
blobTypes: [
'blockBlob'
]
}
}
}
]
}
}
dependsOn: [
storageBlobServices
]
}
resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
name: sqlServerName
location: projectLocation
tags: resourceTags
properties: {
administratorLogin: sqlAdminLogin
administratorLoginPassword: sqlAdminPassword
version: '12.0'
publicNetworkAccess: 'Enabled'
minimalTlsVersion: '1.2'
}
}
resource sqlServerFirewall 'Microsoft.Sql/servers/firewallRules@2022-05-01-preview' = {
parent: sqlServer
name: 'AllowAllWindowsAzureIps'
properties: {
endIpAddress: '0.0.0.0'
startIpAddress: '0.0.0.0'
}
}
resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-05-01-preview' = {
parent: sqlServer
name: sqlDatabaseName
location: projectLocation
tags: resourceTags
sku: {
name: 'Basic'
tier: 'Basic'
capacity: 5
}
properties: {
collation: 'SQL_Latin1_General_CP1_CI_AS'
maxSizeBytes: 2147483648
catalogCollation: 'SQL_Latin1_General_CP1_CI_AS'
zoneRedundant: false
readScale: 'Disabled'
autoPauseDelay: 60
requestedBackupStorageRedundancy: 'Geo'
}
}
resource sqlServerName_sqlDatabaseName_default 'Microsoft.Sql/servers/databases/backupShortTermRetentionPolicies@2022-05-01-preview' = {
name: '${sqlServerName}/${sqlDatabaseName}/default'
properties: {
retentionDays: sqlBackupRetentionDays
}
dependsOn: [
sqlDatabase
sqlServer
]
}
resource automationAccount 'Microsoft.Automation/automationAccounts@2020-01-13-preview' = {
name: automationAccountName
location: projectLocation
tags: resourceTags
identity: {
type: 'SystemAssigned'
}
properties: {
sku: {
name: 'Basic'
}
}
}
resource automationModule_Az_Accounts 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = {
parent: automationAccount
name: Az_Accounts.name
tags: resourceTags
properties: {
contentLink: {
uri: Az_Accounts.url
}
}
}
resource automationModule_Microsoft_Graph_Authentication 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = {
parent: automationAccount
name: Microsoft_Graph_Authentication.name
tags: resourceTags
properties: {
contentLink: {
uri: Microsoft_Graph_Authentication.url
}
}
}
resource automationModule_All 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = [for item in psModules: {
parent: automationAccount
name: item.name
tags: resourceTags
properties: {
contentLink: {
uri: item.url
}
}
dependsOn: [
automationModule_Az_Accounts
automationModule_Microsoft_Graph_Authentication
]
}]
resource automationRunbooks 'Microsoft.Automation/automationAccounts/runbooks@2020-01-13-preview' = [for item in runbooks: {
parent: automationAccount
name: item.name
tags: resourceTags
location: projectLocation
properties: {
runbookType: item.type
logProgress: false
logVerbose: false
description: item.description
publishContentLink: {
uri: item.scriptUri
version: item.version
}
}
dependsOn: [
automationModule_All
]
}]
resource automationVariablesAll 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = [for item in automationVariables: {
parent: automationAccount
name: item.name
properties: {
description: item.description
value: item.value
}
}]
resource automationVariables_csvExports 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = [for item in csvExports: {
parent: automationAccount
name: item.variableName
properties: {
description: item.variableDescription
value: '"${item.containerName}"'
}
}]
resource auto
gitextract_09u_p9cv/
├── .github/
│ └── workflows/
│ ├── continuous-deployment-dev-new.yml
│ ├── continuous-deployment-dev.yml
│ └── continuous-deployment.yml
├── .gitignore
├── Deploy-AzureOptimizationEngine.ps1
├── LICENSE
├── README.md
├── Reset-AutomationSchedules.ps1
├── Setup-BenefitsUsageDependencies.ps1
├── Setup-DataCollectionRules.ps1
├── Setup-LogAnalyticsWorkspaces.ps1
├── Suppress-Recommendation.ps1
├── azuredeploy-nested.bicep
├── azuredeploy.bicep
├── custom-recommendations-types.json
├── docs/
│ ├── configuring-workspaces.md
│ ├── customizing-aoe.md
│ └── suppressing-recommendations.md
├── model/
│ ├── filters-table.sql
│ ├── loganalyticsingestcontrol-initialize.sql
│ ├── loganalyticsingestcontrol-table.sql
│ ├── loganalyticsingestcontrol-upgrade.sql
│ ├── recommendations-sp.sql
│ ├── recommendations-table.sql
│ ├── sqlserveringestcontrol-initialize.sql
│ └── sqlserveringestcontrol-table.sql
├── perfcounters.json
├── queries/
│ ├── rbac-spns-keys-expiring.kql
│ ├── rbac-spns-roles-aad-all.kql
│ ├── rbac-spns-roles-arm-all.kql
│ ├── rbac-spns-roles-arm-privileged.kql
│ ├── rbac-users-roles-aad-all.kql
│ ├── rbac-users-roles-arm-privileged.kql
│ ├── rbac-users-roles-guests-privileged.kql
│ └── rbac-users-roles-guests.kql
├── runbooks/
│ ├── data-collection/
│ │ ├── Export-AADObjectsToBlobStorage.ps1
│ │ ├── Export-ARGAppGatewayPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGAppServicePlanPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGLoadBalancerPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGManagedDisksPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGNICPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGNSGPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGPublicIpPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGResourceContainersPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGSqlDatabasePropertiesToBlobStorage.ps1
│ │ ├── Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGVMSSPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGVNetPropertiesToBlobStorage.ps1
│ │ ├── Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1
│ │ ├── Export-AdvisorRecommendationsToBlobStorage.ps1
│ │ ├── Export-AzMonitorMetricsToBlobStorage.ps1
│ │ ├── Export-ConsumptionToBlobStorage.ps1
│ │ ├── Export-PolicyComplianceToBlobStorage.ps1
│ │ ├── Export-PriceSheetToBlobStorage.ps1
│ │ ├── Export-RBACAssignmentsToBlobStorage.ps1
│ │ ├── Export-ReservationsPriceToBlobStorage.ps1
│ │ ├── Export-ReservationsUsageToBlobStorage.ps1
│ │ ├── Export-SavingsPlansUsageToBlobStorage.ps1
│ │ └── Ingest-OptimizationCSVExportsToLogAnalytics.ps1
│ ├── maintenance/
│ │ └── CleanUp-OlderRecommendationsFromSqlServer.ps1
│ ├── recommendations/
│ │ ├── Ingest-RecommendationsToLogAnalytics.ps1
│ │ ├── Ingest-RecommendationsToSQLServer.ps1
│ │ ├── Ingest-SuppressionsToLogAnalytics.ps1
│ │ ├── Recommend-AADExpiringCredentialsToBlobStorage.ps1
│ │ ├── Recommend-ARMOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-AdvisorAsIsToBlobStorage.ps1
│ │ ├── Recommend-AdvisorCostAugmentedToBlobStorage.ps1
│ │ ├── Recommend-AppServiceOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-DiskOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-SqlDbOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-StorageAccountOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-UnattachedDisksToBlobStorage.ps1
│ │ ├── Recommend-UnusedAppGWsToBlobStorage.ps1
│ │ ├── Recommend-UnusedLoadBalancersToBlobStorage.ps1
│ │ ├── Recommend-VMOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-VMSSOptimizationsToBlobStorage.ps1
│ │ ├── Recommend-VMsHighAvailabilityToBlobStorage.ps1
│ │ └── Recommend-VNetOptimizationsToBlobStorage.ps1
│ └── remediations/
│ ├── Remediate-AdvisorRightSizeFiltered.ps1
│ ├── Remediate-LongDeallocatedVMsFiltered.ps1
│ └── Remediate-UnattachedDisksFiltered.ps1
├── upgrade-manifest.json
└── views/
├── AzureOptimizationEngine.pbix
├── powerbi-query.m
└── workbooks/
├── benefits-simulation.bicep
├── benefits-simulation.json
├── benefits-usage.bicep
├── benefits-usage.json
├── blockblobstorage-usage.bicep
├── blockblobstorage-usage.json
├── costs-growing.bicep
├── costs-growing.json
├── identities-roles.bicep
├── identities-roles.json
├── policy-compliance.bicep
├── policy-compliance.json
├── recommendations.bicep
├── recommendations.json
├── reservations-potential.bicep
├── reservations-potential.json
├── reservations-usage.bicep
├── reservations-usage.json
├── resources-inventory.bicep
├── resources-inventory.json
├── savingsplans-usage.bicep
└── savingsplans-usage.json
Condensed preview — 107 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,066K chars).
[
{
"path": ".github/workflows/continuous-deployment-dev-new.yml",
"chars": 2260,
"preview": "name: AOE Continuous Deployment (DEV NEW)\non: \n workflow_dispatch:\n push:\n branches:\n - dev\npermissions:\n "
},
{
"path": ".github/workflows/continuous-deployment-dev.yml",
"chars": 2485,
"preview": "name: AOE Continuous Deployment (DEV)\non: \n workflow_dispatch:\n push:\n branches:\n - dev\npermissions:\n id-"
},
{
"path": ".github/workflows/continuous-deployment.yml",
"chars": 2132,
"preview": "name: AOE Continuous Deployment (PROD)\non: \n workflow_dispatch:\n push:\n branches:\n - master\npermissions:\n "
},
{
"path": ".gitignore",
"chars": 228,
"preview": "# Deployment state file\nlast-deployment-state.json\n# Database connection settings (for the Suppress-Recommendation.ps1 h"
},
{
"path": "Deploy-AzureOptimizationEngine.ps1",
"chars": 73567,
"preview": "param (\n [Parameter(Mandatory = $false)]\n [string] $TemplateUri,\n\n [Parameter(Mandatory = $false)]\n [string]"
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2020 Hélder Pinto\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 1704,
"preview": "# Welcome to the Azure Optimization Engine - now a FinOps Toolkit tool! 🔍\n\n👋 Thank you for your interest in the Azure Op"
},
{
"path": "Reset-AutomationSchedules.ps1",
"chars": 9491,
"preview": "param(\n [Parameter(Mandatory = $false)] \n [String] $AzureEnvironment = \"AzureCloud\",\n\n [Parameter(Mandatory = $"
},
{
"path": "Setup-BenefitsUsageDependencies.ps1",
"chars": 10239,
"preview": "param(\n [Parameter(Mandatory = $false)] \n [String] $AzureEnvironment = \"AzureCloud\",\n\n [Parameter(Mandatory = $"
},
{
"path": "Setup-DataCollectionRules.ps1",
"chars": 9301,
"preview": "param(\n [Parameter(Mandatory = $false)] \n [String] $AzureEnvironment = \"AzureCloud\",\n\n [Parameter(Mandatory = $"
},
{
"path": "Setup-LogAnalyticsWorkspaces.ps1",
"chars": 6768,
"preview": "param(\n [Parameter(Mandatory = $false)] \n [String] $AzureEnvironment = \"AzureCloud\",\n\n [Parameter(Mandatory = $"
},
{
"path": "Suppress-Recommendation.ps1",
"chars": 8955,
"preview": "param(\n [Parameter(Mandatory = $true)] \n [String] $RecommendationId\n)\n\n$ErrorActionPreference = \"Stop\"\n\nfunction T"
},
{
"path": "azuredeploy-nested.bicep",
"chars": 79576,
"preview": "param projectLocation string\nparam templateLocation string\n\nparam storageAccountName string\nparam automationAccountName "
},
{
"path": "azuredeploy.bicep",
"chars": 2600,
"preview": "targetScope = 'subscription'\nparam rgName string\nparam readerRoleAssignmentGuid string = guid(subscription().subscriptio"
},
{
"path": "custom-recommendations-types.json",
"chars": 10015,
"preview": "{\n \"recommendations\": [\n {\n \"category\": \"Cost\",\n \"typeId\": \"c320b790-2e58-452a-aa63-7b62"
},
{
"path": "docs/configuring-workspaces.md",
"chars": 6777,
"preview": "# Configuring Log Analytics workspaces\n\n## Validating/configuring performance counters collection\n\nIf you want to fully "
},
{
"path": "docs/customizing-aoe.md",
"chars": 10857,
"preview": "# Customizing the Azure Optimization Engine\n\nThere are many customization options available in AOE, in the form of Azure"
},
{
"path": "docs/suppressing-recommendations.md",
"chars": 2604,
"preview": "# Suppressing recommendations\n\nWhen working on the recommendations provided by AOE, you may find some cases where the re"
},
{
"path": "model/filters-table.sql",
"chars": 752,
"preview": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[Filters"
},
{
"path": "model/loganalyticsingestcontrol-initialize.sql",
"chars": 7007,
"preview": "IF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argvmexports')\nBEGIN\n IN"
},
{
"path": "model/loganalyticsingestcontrol-table.sql",
"chars": 919,
"preview": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[LogAnal"
},
{
"path": "model/loganalyticsingestcontrol-upgrade.sql",
"chars": 626,
"preview": "UPDATE [dbo].[LogAnalyticsIngestControl] SET CollectedType = 'ARGVirtualMachine' WHERE StorageContainerName = 'argvmexpo"
},
{
"path": "model/recommendations-sp.sql",
"chars": 772,
"preview": "IF OBJECT_ID ( N'[dbo].[GetRecommendations]', 'P' ) IS NOT NULL\nBEGIN\n DROP PROCEDURE dbo.GetRecommendations\nEND\nEXEC"
},
{
"path": "model/recommendations-table.sql",
"chars": 3076,
"preview": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[Recomme"
},
{
"path": "model/sqlserveringestcontrol-initialize.sql",
"chars": 266,
"preview": "IF NOT EXISTS (SELECT * FROM [dbo].[SqlServerIngestControl] WHERE StorageContainerName = 'recommendationsexports')\nBEGIN"
},
{
"path": "model/sqlserveringestcontrol-table.sql",
"chars": 597,
"preview": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[SqlServ"
},
{
"path": "perfcounters.json",
"chars": 2021,
"preview": "[\n {\n \"objectName\": \"LogicalDisk\",\n \"instance\": \"*\",\n \"counterName\": \"Disk Read Bytes/sec\",\n "
},
{
"path": "queries/rbac-spns-keys-expiring.kql",
"chars": 3154,
"preview": "let expiryInterval = 30d;\nlet AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago"
},
{
"path": "queries/rbac-spns-roles-aad-all.kql",
"chars": 2522,
"preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
},
{
"path": "queries/rbac-spns-roles-arm-all.kql",
"chars": 2522,
"preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
},
{
"path": "queries/rbac-spns-roles-arm-privileged.kql",
"chars": 2848,
"preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
},
{
"path": "queries/rbac-users-roles-aad-all.kql",
"chars": 1903,
"preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
},
{
"path": "queries/rbac-users-roles-arm-privileged.kql",
"chars": 2119,
"preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
},
{
"path": "queries/rbac-users-roles-guests-privileged.kql",
"chars": 2217,
"preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
},
{
"path": "queries/rbac-users-roles-guests.kql",
"chars": 1891,
"preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
},
{
"path": "runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1",
"chars": 18818,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $externalCloudEnvironment,\n\n [Parameter(Mandatory = $false)]\n"
},
{
"path": "runbooks/data-collection/Export-ARGAppGatewayPropertiesToBlobStorage.ps1",
"chars": 8863,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGAppServicePlanPropertiesToBlobStorage.ps1",
"chars": 7490,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1",
"chars": 6774,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGLoadBalancerPropertiesToBlobStorage.ps1",
"chars": 8137,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGManagedDisksPropertiesToBlobStorage.ps1",
"chars": 8031,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGNICPropertiesToBlobStorage.ps1",
"chars": 8846,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGNSGPropertiesToBlobStorage.ps1",
"chars": 8551,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGPublicIpPropertiesToBlobStorage.ps1",
"chars": 9188,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGResourceContainersPropertiesToBlobStorage.ps1",
"chars": 9314,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGSqlDatabasePropertiesToBlobStorage.ps1",
"chars": 7177,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1",
"chars": 8354,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGVMSSPropertiesToBlobStorage.ps1",
"chars": 9687,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGVNetPropertiesToBlobStorage.ps1",
"chars": 11762,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1",
"chars": 12593,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-AdvisorRecommendationsToBlobStorage.ps1",
"chars": 8944,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $targetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1",
"chars": 11204,
"preview": "Param (\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $true)]\n [s"
},
{
"path": "runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1",
"chars": 35680,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-PolicyComplianceToBlobStorage.ps1",
"chars": 24368,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetSubscription,\n\n [Parameter(Mandatory = $false)]\n [s"
},
{
"path": "runbooks/data-collection/Export-PriceSheetToBlobStorage.ps1",
"chars": 17191,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $BillingAccountID,\n\n [Parameter(Mandatory = $false)]\n [str"
},
{
"path": "runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1",
"chars": 10069,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $externalCloudEnvironment,\n\n [Parameter(Mandatory = $false)]\n"
},
{
"path": "runbooks/data-collection/Export-ReservationsPriceToBlobStorage.ps1",
"chars": 5814,
"preview": "param(\n [Parameter(Mandatory = $false)] \n [string] $Filter = \"serviceName eq 'Virtual Machines' and priceType eq '"
},
{
"path": "runbooks/data-collection/Export-ReservationsUsageToBlobStorage.ps1",
"chars": 12415,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetScope,\n\n [Parameter(Mandatory = $false)]\n [string] "
},
{
"path": "runbooks/data-collection/Export-SavingsPlansUsageToBlobStorage.ps1",
"chars": 10118,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [string] $TargetScope,\n\n [Parameter(Mandatory = $false)]\n [string] "
},
{
"path": "runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1",
"chars": 13414,
"preview": "param(\n [Parameter(Mandatory = $true)]\n [string] $StorageSinkContainer\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudE"
},
{
"path": "runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1",
"chars": 1962,
"preview": "$ErrorActionPreference = \"Stop\"\n\n$sqlserver = Get-AutomationVariable -Name \"AzureOptimization_SQLServerHostname\"\n$sqlse"
},
{
"path": "runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1",
"chars": 13548,
"preview": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -"
},
{
"path": "runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1",
"chars": 13080,
"preview": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -"
},
{
"path": "runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1",
"chars": 8114,
"preview": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -"
},
{
"path": "runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1",
"chars": 14072,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1",
"chars": 19951,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1",
"chars": 12451,
"preview": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -"
},
{
"path": "runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1",
"chars": 43671,
"preview": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-SkuHourlyPrice {\n param (\n [object[]] $SKUPriceSheet,\n ["
},
{
"path": "runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1",
"chars": 32547,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1",
"chars": 25143,
"preview": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-DiskMonthlyPrice {\n param (\n [object[]] $SKUPriceSheet,\n "
},
{
"path": "runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1",
"chars": 19669,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1",
"chars": 14757,
"preview": "function ConvertTo-Hashtable {\n [CmdletBinding()]\n [OutputType('hashtable')]\n param (\n [Parameter(ValueF"
},
{
"path": "runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1",
"chars": 11987,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1",
"chars": 12155,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1",
"chars": 17113,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1",
"chars": 20132,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1",
"chars": 38136,
"preview": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-SkuHourlyPrice {\n param (\n [object[]] $SKUPriceSheet,\n ["
},
{
"path": "runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1",
"chars": 57129,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1",
"chars": 55548,
"preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
},
{
"path": "runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1",
"chars": 8356,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnviro"
},
{
"path": "runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1",
"chars": 10943,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnviro"
},
{
"path": "runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1",
"chars": 9777,
"preview": "param(\n [Parameter(Mandatory = $false)]\n [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnviro"
},
{
"path": "upgrade-manifest.json",
"chars": 39722,
"preview": "{\n \"modules\": [\n {\n \"name\": \"Az.Accounts\",\n \"url\": \"https://www.powershellgallery.com/ap"
},
{
"path": "views/powerbi-query.m",
"chars": 8093,
"preview": "let\n Source = Sql.Database(\"aoedevgithub-sql.database.windows.net\", \"azureoptimization\", [Query=\"EXEC GetRecommendati"
},
{
"path": "views/workbooks/benefits-simulation.bicep",
"chars": 1158,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/benefits-simulation.json",
"chars": 66232,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/benefits-usage.bicep",
"chars": 1148,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/benefits-usage.json",
"chars": 40295,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/blockblobstorage-usage.bicep",
"chars": 1166,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/blockblobstorage-usage.json",
"chars": 51655,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/costs-growing.bicep",
"chars": 1146,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/costs-growing.json",
"chars": 27158,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 1,\n \"content\": {\n \"json\": \"### Outliers/grow"
},
{
"path": "views/workbooks/identities-roles.bicep",
"chars": 1156,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/identities-roles.json",
"chars": 52373,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/policy-compliance.bicep",
"chars": 1154,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/policy-compliance.json",
"chars": 90787,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/recommendations.bicep",
"chars": 1150,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/recommendations.json",
"chars": 177739,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/reservations-potential.bicep",
"chars": 1164,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/reservations-potential.json",
"chars": 45096,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/reservations-usage.bicep",
"chars": 1156,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/reservations-usage.json",
"chars": 92177,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/resources-inventory.bicep",
"chars": 1158,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/resources-inventory.json",
"chars": 187909,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
},
{
"path": "views/workbooks/savingsplans-usage.bicep",
"chars": 1157,
"preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique wi"
},
{
"path": "views/workbooks/savingsplans-usage.json",
"chars": 43684,
"preview": "{\n \"version\": \"Notebook/1.0\",\n \"items\": [\n {\n \"type\": 9,\n \"content\": {\n \"version\": \"KqlParameterIt"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the helderpinto/AzureOptimizationEngine GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 107 files (1.9 MB), approximately 472.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.