AzureSaveMoney.psm1
# Contributors: # Chad Schultz # # PowerShell module to List on and delete unused Azure resources and save money. # # This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. # Class to hold alert resource groups and names as script has to get RGs from different command let than alert. Class MyAlert { [String]$RG [String]$Name } function global:Get-AzSMUnusedNICs { <# .SYNOPSIS Lists unused NICs in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE Get-AzSMUnusedNICs -Subscription 00000000-0000-0000-0000-000000000000 .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null|Out-Null Write-Debug "Subscription: $SubscriptionID" $nics=Get-AzureRmNetworkInterface|Where-Object{!$_.VirtualMachine} #$nics|fl Name,ResourceGroupName,Location Return $nics } function global:Get-AzSMUnusedNSGs { <# .SYNOPSIS Lists unused NSGs in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $nsg=Get-AzureRmNetworkSecurityGroup|Where-Object{!$_.NetworkInterfaces} #$nsg|fl Name,ResourceGroupName,Location if($nsg.count -gt 0) { Return $nsg } else { Return "No unused network security groups" } } function global:Get-AzSMUnusedPIPs { <# .SYNOPSIS Lists unused Public IPs in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $pip=Get-AzureRmPublicIpAddress|Where-Object{!$_.IpConfiguration} if($pip.count -gt 0) { Return $pip } else { Return "No unused public IP addresses" } } function global:Get-AzSMDisabledAlerts { <# .SYNOPSIS Lists disabled "classic" alerts in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $alerts = New-Object System.Collections.ArrayList $rgs=Get-AzureRmResourceGroup foreach ($r in $rgs) { $a=Get-AzureRmAlertRule -ResourceGroupName $r.ResourceGroupName -WarningAction Ignore|Where-Object{$_.IsEnabled -eq $false} if ($a.IsEnabled -eq $false){ $al=New-Object MyAlert $al.RG=$r.ResourceGroupName $al.Name=$a.Name $alerts.Add($al)|Out-Null #Write-Host ($al.RG+"-"+$al.Name ) -ForegroundColor DarkYellow } } Return $alerts } function global:Get-AzSMDisabledLogAlerts{ <# .SYNOPSIS List disabled Activity Log alerts in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $logalerts = New-Object System.Collections.ArrayList $rg=Get-AzureRmResourceGroup foreach ($r in $rg) { $a=Get-AzureRmActivityLogAlert -ResourceGroupName $r.ResourceGroupName -WarningAction Ignore -ErrorAction Ignore|Where-Object{$_.Enabled -eq $false} if ($a.Enabled -eq $false){ $al=New-Object MyAlert $al.RG=$r.ResourceGroupName $al.Name=$a.Name $logalerts.Add($al)|Out-Null #Write-Host ($al.RG+"-"+$al.Name ) -ForegroundColor DarkYellow } } Return $logalerts } function global:Get-AzSMEmptyResourceGroups { <# .SYNOPSIS Lists empty resource groups in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $emptyrgs = New-Object System.Collections.ArrayList $rgs=Get-AzureRmResourceGroup $rgs|ForEach-Object { $rgd=Get-AzureRMResource -ResourceGroupNameEquals $_.ResourceGroupName -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable $rgerr if (!$rgd -and $rgerr -eq $null) { $emptyrgs.add($_)|Out-Null #Write-Host $_.ResourceGroupName } } Return $emptyrgs } function global:Get-AzSMUnusedAlertActionGroups { <# .SYNOPSIS Lists unused Alert Action Groups in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $actiongroups = New-Object System.Collections.ArrayList $rgs=Get-AzureRmResourceGroup foreach ($rg in $rgs) { $ala=Get-AzureRmActivityLogAlert -ResourceGroupName $rg.ResourceGroupName -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable $alerr if ($ala -and $alerr.count -lt 1) { $a=$ala.Actions.ActionGroups[0].ActionGroupId.Split("/") $actiongroups.add($a.GetValue($a.Count -1))|Out-Null } } $actiongroups2 = New-Object System.Collections.ArrayList $ags=Get-AzureRmResource -ResourceType microsoft.insights/actionGroups foreach ($g in $ags) { $actiongroups2.add($g.ResourceName)|Out-Null } $unusedactiongroups=$actiongroups2|Where-Object{$actiongroups -notcontains $_} Return $unusedactiongroups } function global:Get-AzSMUnusedRouteTables { <# .SYNOPSIS List unused route tables in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $routelist = New-Object System.Collections.ArrayList $routes=Get-AzureRmRouteTable $routes|ForEach-Object { if ($route.Subnets.Count -eq 0) { $routelist.add($_)|Out-Null } } Return $routelist } function global:Get-AzSMVNetsWithoutSubnets { <# .SYNOPSIS List VNets without any subnets defined in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $emptysubnets=New-Object System.Collections.ArrayList $vnets=Get-AzureRmVirtualNetwork foreach ($vnet in $vnets) { if ($vnet.Subnets.Count -eq 0) { $emptysubnets.add($vnet)|Out-Null #Write-Host "VNet without subnets defined found: " + $vnet.Name } } Return $emptysubnets } function global:Get-AzSMOldDeployments{ <# .SYNOPSIS List deployments older than 365 days in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .PARAMETER Days Set to the number of days to scan back for old deployments. Default is 365 days old. .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID, [Parameter(Mandatory=$false)][int] $Days = 365 ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $rgd=Get-AzureRMResourceGroup|Get-AzureRmResourceGroupDeployment|Where-Object{$_.Timestamp -lt (Get-Date).AddDays(-$Days)} Return $rgd } function global:Get-AzSMUnusedDisks { <# .SYNOPSIS List unused managed disks in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription: $SubscriptionID" $disks = Get-AzureRmDisk|Where-Object{$_.ManagedBy.Length -lt 1} Return $disks } function global:Get-AzSMEmptyAADGroups { <# .SYNOPSIS List empty AAD groups in a tenant. .DESCRIPTION .PARAMETER TenantID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="TenantID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $TenantID ) Connect-AzureAD -TenantId $TenantID Write-Debug "Tenant ID: $TenantID" $emptygroups=New-Object System.Collections.ArrayList Get-AzureADGroup|ForEach-Object { $aadgmem=Get-AzureADGroupMember -ObjectId $_.ObjectId if($aadgmem.Count -lt 1) { $emptygroups.add($_)|Out-Null } } Return $emptygroups } function global:Get-AzSMDisabledLogicApps { <# .SYNOPSIS List disabled Logic Apps in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription ID: $SubscriptionID" $disabledlapps = New-Object System.Collections.ArrayList $lapps=Get-AzureRMResource -ResourceType "Microsoft.Logic/workflows" $lapps|ForEach-Object { $lapp=Get-AzureRmLogicApp -ResourceGroupName $_.ResourceGroupName -Name $_.Name|Where-Object{$_.State -eq "Disabled"} $disabledlapps.add($lapp)|Out-Null } Return $disabledlapps } function global:Get-AzSMOldSnapshots{ <# .SYNOPSIS List snapshots older than 365 days in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .PARAMETER Days Set to the number of days to scan back for old deployments. Default is 365 days old. .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID, [Parameter(Mandatory=$false)][int] $Days = 365 ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription ID: $SubscriptionID" $snap=Get-AzureRmSnapshot|Where-Object{$_.TimeCreated -lt (Get-Date).AddDays(-$Days)} Return $snap } function global:Get-AzSMIlbNoBackendPool { <# .SYNOPSIS List Internal load balancers that have no backend pool in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription ID: $SubscriptionID" $ilbsnopool=Get-AzureRmLoadBalancer|Where-Object{$_.BackendAddressPools.Count -lt 1} Return $ilbsnopool } function global:Get-AzSMDisabledTrafficManagerProfiles { <# .SYNOPSIS List disabled TrafficManager Profiles in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription ID: $SubscriptionID" $dtmpro=Get-AzureRmTrafficManagerProfile|Where-Object{$_.ProfileStatus -eq "Disabled"} Return $dtmpro } function global:Get-AzSMTrafficManagerProfilesWithNoEndpoints { <# .SYNOPSIS List TrafficManager Profiles without endpoints in a subscription. .DESCRIPTION .PARAMETER SubscriptionID .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID ) Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Subscription ID: $SubscriptionID" $dtmpro=Get-AzureRmTrafficManagerProfile|Where-Object{$_.Endpoints.Count -lt 1} Return $dtmpro } function global:Get-AzSMAllResources { <# .SYNOPSIS List all unused resources that this module implements in a tenant and subscription. .DESCRIPTION .PARAMETER SubscriptionID .PARAMETER TenantID .PARAMETER Days .OUTPUTS .EXAMPLE .NOTES .LINK #> [CmdletBinding( DefaultParameterSetName="SubscriptionID", SupportsShouldProcess=$false, ConfirmImpact='Low' )] param( [Parameter(Mandatory=$true)][string] $SubscriptionID, [Parameter(Mandatory=$true)][string] $TenantID, [Parameter(Mandatory=$false)][int] $Days = 365 ) Connect-AzureAD -TenantId $TenantID|Out-Null Select-AzureRmSubscription -Subscription $SubscriptionID|Out-Null Write-Debug "Tenant ID: $TenantID" Write-Debug "Subscription ID: $SubscriptionID" Write-Debug "Days: $Days" Write-Host "Ununsed NICs:" -ForegroundColor Cyan Get-AzSMUnusedNICs -Subscription $SubscriptionID Write-Host "Ununsed NSGs:" -ForegroundColor Cyan Get-AzSMUnusedNSGs -Subscription $SubscriptionID Write-Host "Ununsed PIPs:" -ForegroundColor Cyan Get-AzSMUnusedPIPs -Subscription $SubscriptionID Write-Host "Disabled Alerts(Classic):" -ForegroundColor Cyan Get-AzSMDisabledAlerts -Subscription $SubscriptionID Write-Host "Disabled Log Alerts:" -ForegroundColor Cyan Get-AzSMDisabledLogAlerts -Subscription $SubscriptionID Write-Host "Empty Resource Groups:" -ForegroundColor Cyan Get-AzSMEmptyResourceGroups -Subscription $SubscriptionID Write-Host "Ununsed Alert Groups:" -ForegroundColor Cyan Get-AzSMUnusedAlertActionGroups -Subscription $SubscriptionID Write-Host "Ununsed Route Tables:" -ForegroundColor Cyan Get-AzSMUnusedRouteTables -Subscription $SubscriptionID Write-Host "VNets without Subnets:" -ForegroundColor Cyan Get-AzSMVNetsWithoutSubnets -Subscription $SubscriptionID Write-Host "Old Deployments older than $Days days:" -ForegroundColor Cyan Get-AzSMOldDeployments -Subscription $SubscriptionID Write-Host "Ununsed Disks:" -ForegroundColor Cyan Get-AzSMUnusedDisks -Subscription $SubscriptionID Write-Host "Empty AAD Groups:" -ForegroundColor Cyan Get-AzSMEmptyAADGroups -Subscription -TenantId $TenantID Write-Host "Disabled Logic Apps:" -ForegroundColor Cyan Get-AzSMDisabledLogicApps -Subscription $SubscriptionID Write-Host "Old Snapshots older than $Days days:" -ForegroundColor Cyan Get-AzSMOldSnapshots -Subscription $SubscriptionID Write-Host "ILBs with no backend pools:" -ForegroundColor Cyan Get-AzSMIlbNoBackendPool -Subscription $SubscriptionID Write-Host "Disabled TrafficManager Profiles:" -ForegroundColor Cyan Get-AzSMDisabledTrafficManagerProfiles -Subscription $SubscriptionID Write-Host "TrafficManager Profiles With No Endpoints:" -ForegroundColor Cyan Get-AzSMTrafficManagerProfilesWithNoEndpoints -Subscription $SubscriptionID } |