In the age-old dance of creation and destruction, behold Cloud Reaper for Azure, a guardian wrought in code and cloud. Its purpose noble, it wields the power to vanquish Azure resources marked by time’s decree, ensuring realms of development and testing stand uncluttered, their legacies preserved in the annals of digital lore.
Note
Cloud Reaper for Azure has been completely rewritten for v2.0.0 using .NET 10 and the Azure Functions isolated worker model. If you are interested in the original version, check out the ./archive directory.
Cloud Reaper for Azure automatically deletes tagged Azure resource groups after a specified lifetime -- keeping dev and test subscriptions clean and your cloud bill in check. Tag a resource group, walk away, and let the reaper handle the rest.
Inspired by Jeff Holan's functions-csharp-entities-grimreaper, rebuilt from the ground up with .NET 10, Azure Functions isolated worker model, and Durable Entities.
Cloud Reaper for Azure uses specific tags to manage the lifecycle of Azure resource groups. Tag names are configurable via environment variables (LifetimeTagName, StatusTagName). Below are the default tags and their use cases:
| Tag Name | Description | Example Value | Responsibility | Comments |
|---|---|---|---|---|
| CloudReaperLifetime | This tag is applied when the resource group is created by the engineer. It specifies the lifespan of the resource group in minutes before it should be deleted. The value must be a positive integer (> 0). Values of 0, negative numbers, or non-integer strings are ignored. Configurable via LifetimeTagName. |
60 (for 60 minutes lifetime) | User | |
| CloudReaperStatus | This tag is applied by Cloud Reaper for Azure after successful validation and scheduling of the resource group’s deletion. It indicates that the resource group is confirmed for deletion. Configurable via StatusTagName. |
Confirmed | Cloud Reaper for Azure | Can be used to return comments or error messages from Cloud Reaper for Azure |
| CloudReaperDeletionTime | Applied by Cloud Reaper for Azure after scheduling deletion. Shows the exact UTC timestamp when the resource group will be deleted (ISO 8601 format). Configurable via DeletionTimeTagName. |
2024-05-31T15:30:00.0000000+00:00 | Cloud Reaper for Azure |
Cloud Reaper for Azure currently has the following limitations:
| Description | Status |
|---|---|
| Cloud Reaper for Azure has only been tested with a single subscription. Multi-subscription support is planned. | Planned Improvement |
| Cloud Reaper for Azure operates only at the Azure Resource Group level, not on individual resources | Current Functionality |
Cloud Reaper for Azure uses UTC internally for all scheduling and timestamps (e.g. CloudReaperDeletionTime). There is currently no option to configure a different time zone. |
Current Functionality |
Re-scheduling a deletion by updating the CloudReaperLifetime tag value after the resource group has already been scheduled is not supported. The original schedule remains active. |
Current Functionality |
Cloud Reaper for Azure is under active development and is constantly evolving. The capabilities and performance of the project are continually being improved.
Warning
Cloud Reaper for Azure uses a custom role that grants its managed identity permission to read, modify tags on, and delete any resource group in the target subscription. While this role is scoped to resource group lifecycle operations only, it is still highly privileged. Deployment is recommended for test and development subscriptions only. Do not deploy to production subscriptions without a thorough security review.
Cloud Reaper for Azure is deployed with Azure Developer CLI (azd) and Terraform.
The user running the deployment needs the following permissions on the target Azure subscription:
| Permission | Reason |
|---|---|
| Owner (or Contributor + User Access Administrator) | The deployment creates a custom role definition and role assignments, which requires Microsoft.Authorization/roleAssignments/write and Microsoft.Authorization/roleDefinitions/write. |
| Resource provider registration | The Microsoft.EventGrid resource provider must be registered on the subscription (see tip below). |
During provisioning, the deployment creates a custom Azure Reaper Operator role with least-privilege permissions (read, tag, and delete resource groups only) and assigns it to the Function App's system-assigned managed identity. Storage access uses managed identity authentication — no connection strings or access keys are stored in app settings.
For the current dev/test setup, Terraform also auto-grants the currently authenticated deployer principal Storage Blob Data Contributor on the deployment storage account. This is needed because azd deploy uploads the function package to Blob Storage by using Azure AD data-plane access, and subscription-level Owner/Contributor rights alone are not sufficient for that upload.
- Install the required tooling:
az,azd,dotnet10, and Azure Functions Core Tools 4.x. - Sign in to Azure:
az login azd auth login
- Create or select an
azdenvironment and choose a short environment name such asd1:azd env new d1 azd env set AZURE_LOCATION westeurope - Run the deployment:
azd up
azd up provisions the infrastructure, waits briefly for the deployer's Blob role assignment to propagate, deploys the Function App package, and then runs scripts/postdeploy.sh to create or update the Event Grid subscription.
Tip
If provisioning fails with a MissingSubscriptionRegistration error for Microsoft.EventGrid, register the resource provider first:
az provider register --namespace Microsoft.EventGrid
az provider show --namespace Microsoft.EventGrid --query "registrationState" -o tsvWait until the state shows Registered, then re-run azd up.
Note
The repository includes infra/main.tfvars.json because azd expects that parameter template for Terraform projects. It passes the core AZURE_* values into Terraform, while optional naming overrides still come from azd env set TF_VAR_<name> ....
By default, resource names are generated from AZURE_ENV_NAME and AZURE_LOCATION using the pattern <prefix>-<env>-azreaper-<location>, for example rg-d1-azreaper-westeurope. For generated defaults, Terraform lowercases those inputs and replaces non-alphanumeric characters with -. The storage account uses the same inputs but removes non-alphanumeric characters entirely so the final name stays within Azure's lowercase alphanumeric 3-24 character rule.
Note
If your environment name is too generic or common, the derived storage account name may already be taken globally. In that case, provisioning will fail. Use a more unique environment name or override the storage account name with azd env set TF_VAR_storage_account_name <unique-name>.
You can still bring your own names by setting Terraform override variables in the azd environment before running azd up:
azd env set TF_VAR_resource_group_name rg-custom-reaper
azd env set TF_VAR_storage_account_name std1azreaperweu
azd env set TF_VAR_function_app_name func-custom-reaper
azd env set TF_VAR_app_service_plan_name asp-custom-reaper
azd env set TF_VAR_log_analytics_name log-custom-reaper
azd env set TF_VAR_app_insights_name appi-custom-reaper
azd env set TF_VAR_eventgrid_system_topic_name evgt-custom-reaper
azd env set TF_VAR_eventgrid_event_subscription_name evs-custom-reaperAfter infrastructure provisioning, azd deploys the Function App and runs the postdeploy hook to create or update the Event Grid event subscription. The hook first tries to target the EventGridTrigger function by its Azure resource ID. If that native Function endpoint is not accepted yet on the hosting plan or platform, the hook falls back to the Event Grid webhook endpoint using the Function's Event Grid system key.
If a different user or CI identity later runs azd deploy against the same environment, that identity must also have Storage Blob Data Contributor on the deployment storage account. The current auto-grant behavior is a convenience for this repository's dev/test workflow and should be reviewed before reusing the template in stricter production environments.
To remove all Azure resources created by this deployment:
azd downNote
The Terraform provider is configured with prevent_deletion_if_contains_resources = false. This means azd down will delete the resource group and all its contents, including resources not directly managed by Terraform (e.g., the smart detection alert rule auto-created by Application Insights). This is intentional for this self-contained project but should be reviewed if the template is reused in shared environments.
| Tool | Version | Purpose |
|---|---|---|
| .NET SDK | 10.0+ | Build and run the Functions app |
| Azure Functions Core Tools | 4.x | Local Functions runtime (func start) |
| Azure CLI | latest | Azure authentication and management |
| Azure Developer CLI | 1.x | Streamlined deployment with azd up |
| Terraform | latest | Infrastructure provisioning |
| Docker or Podman | latest | Run Azurite (Azure Storage emulator) |
Tip: A Dev Container is included with all prerequisites pre-installed — just open the project in VS Code or GitHub Codespaces.
1. Clone the repository
git clone https://github.com/lrottach/az-reaper.git
cd az-reaper2. Start Azurite
docker compose up -d azurite # or: podman compose up -d azurite3. Create local settings
cp src/AzureReaper.Functions/local.settings.sample.json \
src/AzureReaper.Functions/local.settings.json4. Build and run
cd src/AzureReaper.Functions
dotnet build
func startThe function app will start at http://localhost:7071.
The function listens for EventGrid events at:
POST http://localhost:7071/runtime/webhooks/EventGrid?functionName=EventGridTrigger
Required header: aeg-event-type: Notification
Using VS Code REST Client: Open rest/eventgrid.http and click "Send Request".
Using curl:
curl -X POST \
-H "Content-Type: application/json" \
-H "aeg-event-type: Notification" \
-d @rest/payload.example.json \
http://localhost:7071/runtime/webhooks/EventGrid?functionName=EventGridTriggerBy default the example payloads use placeholder subscription and resource group values (xxxxxxxx-…). To test tagging, scheduling, and deletion of actual resource groups in Azure, you need to authenticate locally and point the payload at a real resource group.
1. Authenticate with Azure CLI
Sign in with the Azure CLI so the Functions app can obtain a token via DefaultAzureCredential:
az loginIf your account has access to multiple tenants or subscriptions, select the correct one:
az account set --subscription "<subscription-id-or-name>"Verify your session:
az account show2. Rewrite the EventGrid payload
Open rest/eventgrid.http (or rest/payload.example.json) and replace every xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx subscription placeholder with your real subscription ID. Then set the resource group name to the one you want Cloud Reaper for Azure to target.
The two fields that matter most are subject and data.resourceUri — they must contain the full resource ID of the target resource group:
/subscriptions/<your-subscription-id>/resourceGroups/<your-resource-group>
Also update these fields to match:
| Field | Value |
|---|---|
subject |
/subscriptions/<sub-id>/resourceGroups/<rg-name> |
data.authorization.scope |
/subscriptions/<sub-id>/resourceGroups/<rg-name> |
data.resourceUri |
/subscriptions/<sub-id>/resourceGroups/<rg-name> |
data.subscriptionId |
<sub-id> |
topic |
/subscriptions/<sub-id> |
Warning: This will schedule a real deletion. Make sure the target resource group is a throwaway test group and has the
CloudReaperLifetimetag set (value in minutes). Apply a resource lock first if you want to verify scheduling without actually deleting.
3. Send the request
Fire the modified payload against the local function app as described in Testing the EventGrid Trigger. Cloud Reaper for Azure will use your az login session to read tags, apply the CloudReaperStatus tag, and schedule deletion via the Durable Entity.
Configured in src/AzureReaper.Functions/local.settings.json (see local.settings.sample.json for a template):
| Variable | Default | Mandatory | Description |
|---|---|---|---|
AzureWebJobsStorage |
UseDevelopmentStorage=true |
Yes | Storage connection — uses local Azurite |
FUNCTIONS_WORKER_RUNTIME |
dotnet-isolated |
Yes | Required for the isolated worker model |
LifetimeTagName |
CloudReaperLifetime |
No | Custom tag name for resource group lifetime (minutes) |
StatusTagName |
CloudReaperStatus |
No | Custom tag name applied when deletion is scheduled |
DeletionTimeTagName |
CloudReaperDeletionTime |
No | Custom tag name for the scheduled deletion timestamp |
The repository includes a Dev Container (.devcontainer/) that provides a ready-to-go environment with .NET 10, Azure CLI, Azure Functions Core Tools, Docker, and GitHub CLI pre-installed. Open the project in VS Code with the Dev Containers extension or use GitHub Codespaces to get started instantly.


