Schedule clean up of your sandbox tenant with Azure Automation
Tags: azure, automation, powershell, sandbox
Published: 2023-12-05 16:03:17

---
title: Schedule clean up of your sandbox tenant with Azure Automation
published: true
date: 2023-09-07 15:22:00 UTC
tags: Azure,Automation,PowerShell,Sandbox
canonical_url: https://rios.engineer/schedule-azure-sandbox-cleanup/
---

## Introduction

If you’ve got a sandbox tenant which you and your teams are using to simulate certain deployments that don’t fit into the typical subscription scope (E.g. full blown Landing Zones for example) you may have a test or sandbox Azure tenant for this. In this case, you may also want to clear down Azure resources on that tenant on a daily basis to keep costs low and tenant clean for the next deployment.

I’ll detail how you can implement this with an Azure Automation runbook on a schedule with a PowerShell script.

**!!! Please be extra careful implementing as this is destructive with NO WAY to retrieve the deletions !!! Proceed at your own risk, and be sure to triple check permissions and target scopes. This is to act as an example, your scenario may differ but the concept will be similar to implement.**

### Prerequisites

- Azure Key Vault deployed in your main Azure tenant
- Azure Automation Account deployed in your main Azure tenant 

## Authentication Setup

1. Create a [SPN (Service Principal Name)](https://learn.microsoft.com/en-us/purview/create-service-principal-azure) in the _ **Azure Sandbox Tenant** _ with a secret (take note of the secret!)
2. Check that you have [enabled Manage Identity on the Azure Automation Account](https://learn.microsoft.com/en-us/azure/automation/enable-managed-identity-for-automation#enable-using-the-azure-portal) 
3. Grant the Managed Identity RBAC access to the Key Vault as ‘Key Vault Secrets User’ role

### Key Vault Secrets

In the Key Vault ensure the following secret values are added:

1. Application Id of the Service Principal (not to be confused with the App registration Id that is associated to it)
2. Service Principal secret
3. Tenant Id of the Azure Sandbox

![](https://rios.engineer/wp-content/uploads/2023/09/kv-secrets-destroy-az-1024x211.png)

### Sandbox RBAC

For the runbook to successfully clear down every resource group in the tenant, we must add the SPN onto the root management group where we want the script to find the subscriptions for. In your case, this may be more granular that that.

In my example I have added the Service Principal object ‘Owner’ role onto Tenant Root Group so it will inherit down to everything beneath.

## Azure Automation Runbook

We’ll be using a PowerShell runbook on a linked schedule to run daily in my example, but you can amend to your needs. We don’t need to import any specific Azure modules here, as they are already baked in for use.

1. Create a new Azure Automation runbook: 

- Runbook type: PowerShell
- Runtime version: 7.2 (preview)

### Runbook script

Once created, go to the runbook and edit in the Portal (or VSCode) and paste the script in, saving & publishing it.

Adjust the Key Vault name and secret names according to yours.

[pwsh/AzureAutomationRunBooks/azure-sandbox-destroy.ps1 at main · riosengineer/pwsh (github.com)](https://github.com/riosengineer/pwsh/blob/main/AzureAutomationRunBooks/azure-sandbox-destroy.ps1)

<svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewbox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewbox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg>
```
# Connect to Key Vault with MI and get secret values
Connect-AzAccount -Identity
$tenantId = Get-AzKeyVaultSecret -VaultName "kv-rios-example" -Name "tenantId" -AsPlainText
$appId = Get-AzKeyVaultSecret -VaultName "kv-rios-example" -Name "appId" -AsPlainText
$spnsecret = Get-AzKeyVaultSecret -VaultName "kv-rios-example" -Name "secret" -AsPlainText

# Login to Sandbox Tenant via SPN
$secret = ConvertTo-SecureString -String $spnsecret -AsPlainText -Force
$pscredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $appId, $secret
Connect-AzAccount -ServicePrincipal -Credential $pscredential -Tenant $tenantId -Verbose

# Get subscriptions
$subscriptions = Get-AzSubscription

# Loop through each subscription and delete resource groups as background job
foreach ($subscription in $subscriptions) {
    Select-AzSubscription -SubscriptionId $subscription.Id
    $resourceGroups = Get-AzResourceGroup
    foreach ($resourceGroup in $resourceGroups) {
        $lock = Get-AzResourceLock -ResourceGroupName $resourceGroup.ResourceGroupName
        if ($lock -eq $null) {
            Remove-AzResourceGroup -Name $resourceGroup.ResourceGroupName -Force -AsJob
            Write-Output "Sending delete job for Resource Group: $($resourceGroup.ResourceGroupName)."
        }
        else {
            Write-Output "Resource group $($resourceGroup.ResourceGroupName) has a resource lock present so cannot be deleted."
        }
    }
}
```
PowerShell

The script runs the delete request as a job, this is to combat long runtimes from certain Azure resources that can take a while to remove (or timeout). This way, the runbook won’t wait about for the return and will execute quickly.

Additionally, in the script I’m writing the outputs to the logs so we can see what the script picks up during run, this helps aid troubleshooting and auditing from the runs.

### Creating the automation schedule

Now the runbook is published, we can create and associate a schedule for it to run with. Locate the Automation Account > Schedules > Add a schedule, for example:

![Azure sandbox tenant automation](https://rios.engineer/wp-content/uploads/2023/09/aa-schedule-559x1024.png)

Lastly, back to the runbook select ‘Link to a schedule’ and select your newly created schedule to complete.

## Test run

Now we’re set to go, we can see after I’ve created some dummy resource groups over in the sandbox tenant that the script detects these and outputs the information to the logs on the runbook for us! Awesome.

![](https://rios.engineer/wp-content/uploads/2023/09/example-aa-runbook-output-1024x744.png)

### What if I want to exclude certain resource groups?

Adding a resource lock to the resource group (or sub-level if needed) will prevent the script from deleting the resources within. This is easier than adding more complexity logic into the script.

### Source control?

I recommend implementing [Azure automation source control](https://learn.microsoft.com/en-us/azure/automation/source-control-integration#configure-source-control-in-azure-portal). When making updates not only will it auto-sync the changes to the Azure runbook from the repository but is good practice for a robust CI/CD lifecycle with relevant tests before going live. Out of scope for this post but worth a mention.

## Conclusion

The script is quite rudimentary in my example, but can be expanded on to include specific requirements for your needs. For example, running the deletions as jobs could pose issues where they timeout with no way to retry in the current script, so that may be something I may look to add in at a later stage.

That’s all there is to it, I hope others find the post useful!

The post [Schedule clean up of your sandbox tenant with Azure Automation](https://rios.engineer/schedule-azure-sandbox-cleanup/) appeared first on [Rios Engineer](https://rios.engineer).