Auto delete expired Azure test resources

This blog post documents something that I’ve been meaning to implement for a while and have now finally got around to doing so. In my day job, I tend to create a lot of temporary Azure resources. This can be for a number of reasons like testing new features, trying out different solution architectures or just trying to recreate a customer problem and figure out how to solve it in a sandbox environment.

Naturally, these resources will cost money or consume credit but as they are often very short lived, i.e. deleted when no longer required, then it usually isn’t too much of an issue.

On occasion however (and we’ve all done this), I forget to delete these resources and sometimes this goes unnoticed for some time meaning additional costs that could have been avoided.

Scope

This solution is specific to my particular setup and how I deploy my temporary resources but hopefully if you find it useful you can modify the solution for your own needs.

I always deploy my temporary resources into their own resource groups so that I can keep them organised and then usually it’s then just a matter of deleting the entire resource group when I am finished. Sometimes resources will have dependencies in place, so attempting deletion of a resource group is not always guaranteed to delete every single resource but generally for my needs this is sufficient. Therefore this solution will just focus on deleting tagged resource groups but the occasional failure due to a resource dependency is expected.

Tagging resource groups

This is the easy part. I need a way to identify which resource groups are to be deleted and when. I just decided to use a very basic name/value pair for this purpose and will use a tag with the name: delete-me-on and with a value of the date when it is to be deleted in my local regional date format of DD/MM/YYYY so for example 04/10/2021. All that needs to be tagged here is the resource group itself, I am going to delete all resources within that resource group. This fits my requirement but with some modification I could easily only delete individually tagged resources if required.

Of course, you can always remove or change the date value on the tag but just remember to do so before the automation tasks runs!

Automation

The next decision was which automation service to use to delete these tagged resource groups. Generally the options for this in Azure are: Azure Automation accounts, Logic Apps or Azure Functions. I ruled out using Logic Apps as they are more suited to workflows based on other events or triggers. Azure Automation would be the traditional choice here and would have worked fine but I opted to go with an Azure Function app here. Mainly because the use case is to run a very basic routine task on a scheduled basis and also because I wanted to get more familiar and comfortable with using Azure Functions generally.

I deployed a consumption (serverless) based function app service using the PowerShell Core runtime stack. Microsoft provide a decent free allocation of 1 million executions and 400,000 GB-s execution time per month which should more than enough not to cost me anything.

Setting up the function app

I will be the using the Az PowerShell module for my script so I need to make sure this module is loaded into my function app. The easiest way to achieve this is to go to App files under the functions section of the function app. Then select the requirements.psd1 file and uncomment the ‘Az’ line shown on line 7 below. Make sure to click Save. This will download and install the Az dependency module the first time the function app is executed.

We will need to authenticate to Azure Resource Manager when our function runs. There are a few ways to do this such as using a service principal but arguably the best method here is to use a system assigned managed identity. This way we can give the function app direct access to Azure Resource Manager without have to create or manage any identity credentials at all.

To set this up, click on Identity under the settings section of the function app and set the status to On.

It’s important to assign this managed identity with Contributor level RBAC access to each of the Azure subscriptions where you intend to use this function app. This level of access is required in order to be able to delete all of the various resource types. This can be done by assigning access via the Azure role assignments button shown above.

I am going to use a time based trigger to execute my function, so it’s very important to note that the default time zone used for the CRON expression in the next step will be UTC time. If you want use a different time zone then you must add an application setting to the function app called WEBSITE_TIME_ZONE. Remember to click the Save button to restart the app service.

A list of time zones can be found here: https://docs.microsoft.com/en-gb/previous-versions/windows/it-pro/windows-vista/cc749073(v=ws.10)

Build the function

Now we can create the actual function that will perform the task required. Go to Functions and click the Create button.

I want my function to run on a scheduled basis so the trigger will be time based, therefore select the Timer trigger option. Give the function a name and provide the trigger schedule in CRON format. I decided that I want my function to run every night at 9pm therefore the expression I need here is: 0 0 21 * * *

There are some free CRON expression generator tools available online to help you to generate the required expression such as this tool: https://en.rakko.tools/tools/88/

The below is the code that I used for my function which will iterate through any Azure subscriptions that the managed identity has access to. For each subscription it will fetch and delete a list of resource groups tagged to be deleted on today’s date.

Remember, this runs daily at 9pm and there is no come back from deleting Azure resources so use the below only if you fully understand the implications.

# Input bindings are passed in via param block.
param($Timer)

# Get the current universal time in the default string format.
$currentUTCtime = (Get-Date).ToUniversalTime()

# The 'IsPastDue' property is 'true' when the current function invocation is later than scheduled.
if ($Timer.IsPastDue) {
    Write-Host "PowerShell timer is running late!"
}

# Write an information log with the current time.
Write-Host "PowerShell timer trigger function ran! TIME: $currentUTCtime"

$subscriptions = Get-AzSubscription

Foreach ($subscription in $subscriptions)
{
    Set-AzContext -Subscription $subscription
    (Get-AzResourceGroup -Tag @{ "delete-me-on"= Get-Date -Format "dd/MM/yyyy" }) | Remove-AzResourceGroup -AsJob -Force
}

Write-Host "Task Finished"

At this point, you should be good to go. I would recommend doing a test run without any tagged resource groups just to make sure the authentication is working correctly and that the code executes correctly. It will take several minutes on the first execution anyway as remember it will need to download and install the Az PowerShell module dependency.

If you have tagged any resource groups for the current date then these should be deleted and show up in the monitor logs as below.

If you deployed the function app with Application Insights integration you can always check the Monitor section shown below to review the execution logs of your function app afterwards.

That’s it. A very simple function app that I will personally find very useful and will hopefully save my employer some money for when I forget to delete those temporary test environments. I hope you might find it useful also.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.