DEV Community

Frank Boucher ☁ for Microsoft Azure

Posted on • Updated on

Keep your Azure Subscription Clean Automatically

I'm sure it happens to most of us, you create a few things in the cloud and forget about it. Then at the end of the month, you receive a bigger invoice then expected and it's the panic. Even though I was lucky enough to never have had HUGE surprises, I admit to forgetting to delete demo resources after some presentations. Would it be an on-stage talk or video recording, I create content and once the presentation is done, it would be great is the resource could delete themselves the next day.

In this post, I will explain how I build that tool using a very simple PowerShell script and Azure Automation. All the code is available on my GitHub and I'm currently working on a 1-click button to help you deploy it in your subscription. I also created a Glo board so you could see the road map of the incoming features. Feel free to have a look, use it, ask for a feature, add one and share!

The Goal

The idea was to delete resources that are "expired". But Azure's resources don't have an expiration date, so I thought about using tags! The script would get executed on a regular base (ex: every morning), and delete all resources with a tag expireOn where the value is before the current date. Look simple enough, let's get started.

The PowerShell Script

To find all the expired resources I wanted to use Azure Resource Graph queries. It's a very efficient way to query your resources. The query language for the Azure Resource Graph supports several operators and functions, here I use the todatetime to convert the string value to a date. And because we only need the ResourceId to delete a resource, we return a list of IDs.

$expResources= Search-AzGraph -Query 'where todatetime(tags.expireOn) < now() | project id'

foreach ($r in $expResources) {
    Remove-AzResource -ResourceId $ -Force

To get started on how to run your first Resource Graph query visit I also did a 5-minute video Search Like a Boss with Azure Graph Query where I explain how it works.

If you execute this query, you will notice that even if you had Resource Group with a tag expireOn then won't be part of the result. It's because Resource Graph query doesn't see Resource Group. Fortunately for us, it allows us to be a little more granular in our deletion process. By selecting at the resource level we cover a scenario where for example, you would delete all the resources inside a Resource Group except the storage.

However, we may also delete all resources and left and empty Resource Group. Therefore, we need to delete all empty Resource Group. Since there is no simple way to search for an empty group, we need to do a little bit more effort, but nothing dramatic.

$rgs = Get-AzResourceGroup;

foreach($resourceGroup in $rgs){
    $name=  $resourceGroup.ResourceGroupName;
    $count = (Get-AzResource | Where-Object{ $_.ResourceGroupName -match $name }).Count;
    if($count -eq 0){
        Write-Output "==> $name is empty. Deleting it...";
        Remove-AzResourceGroup -Name $name -Force -WhatIf

The Azure Automation

To execute some commands based on a schedule, you have a few options: Time Trigger Azure Functions or Logic Apps and Azure Automation. In this post, we will be using the last one. Open your favourite browser and navigate to Azure portal ( Click the green plus button in the top left corner, and search for Automation. Let's create one.

"Create a Azure Automation"

An important mention, be sure to select yes in the creation form to create an Azure Run account. We need this to give the script enough permission to query the subscription.

"Select Create Run Account"

Adding Modules

The script will need to two modules to be loaded to works:

  • Az.Accounts
  • Az.ResourceGraph

To add them to our Automation Account, open the freshly created Resource and click the Modules from the options panel. This will open a new blade. At the top right of this one, click the Browse gallery button.

"Open the Gallery"

Enter "Az.Accounts" in the search bar (you will need to hit Enter to search), and select the matching module. Click Import and Ok to Import the module to our Automation Account. Once the module is completely imported you will be able to repeat the process for the Az.ResourceGraph module.

Az.Accounts is a dependency of Az.ResourceGraph, this is why the first one needs to be fully imported first.

You can validate that both modules are imported by doing filtering "az."

"Filter Imported modules"

Create a Runbook

Now we will create a Runbook that will hold our PowerShell code. Click the Runbooks option from the left panel, then click the "+ Create a runbook" button.

"Create a Runbook"

From the form on the right, give it a name (ex: SubscriptionCleaner), select PowerShell as the type, and add a meaningful description. (you will thanks me later ;) ). Finally, click the Create button.

Now you can copy-paste our script. Note that a few lines are added in the beginning to Login using the Run As account created previously.

$connectionName = "AzureRunAsConnection"
    # Get the connection "AzureRunAsConnection "
    $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName         

    Connect-AzAccount `
        -ServicePrincipal `
        -Tenant $servicePrincipalConnection.TenantId `
        -ApplicationId $servicePrincipalConnection.ApplicationId `
        -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 
catch {
    if (!$servicePrincipalConnection)
        $ErrorMessage = "Connection $connectionName not found."
        throw $ErrorMessage
    } else{
        Write-Error -Message $_.Exception
        throw $_.Exception

$expResources= Search-AzGraph -Query 'where todatetime(tags.expireOn) < now() | project id'

foreach ($r in $expResources) {
    Remove-AzResource -ResourceId $ -Force -WhatIf

$rgs = Get-AzResourceGroup;

foreach($resourceGroup in $rgs){
    $name=  $resourceGroup.ResourceGroupName;
    $count = (Get-AzResource | Where-Object{ $_.ResourceGroupName -match $name }).Count;
    if($count -eq 0){
        Remove-AzResourceGroup -Name $name -Force -WhatIf

You can now Save, and publish the runbook.

Make it run every morning

You probably guessed it, we now need to create a Schedule so our runbook gets executed every morning (or whenever you want). From the Runbook blade, click on the Link to schedule button on the top of the blade.

From there, expand the "Link a schedule to your runbook" and click the "Create a new schedule" button. Give it a name, description, select your time and recurring options. And voila!


What's next

The Runbook will search and delete all expired resources next time it will get trigger by our schedule, in my case next morning. Of course, you need to have some resources tagged with expiredOn!

The most accessible way to create a Tag is definitely with the portal. From any resource, click the Tags option and enter expireOn as Name, and the date you wish this resource to expire following the format YYYY-MM-dd. It's also possible to add tags with PowerShell, Azure CLI and from Azure Resource Manager (ARM) template.

I also made a video if you prefer.

How to Delete All Expired Resources Automatically

Happy cleaning!

Discussion (0)