Protego
HomeBlogToolsAboutContact

Protego

Expert insights on cloud security, cybersecurity, zero trust, and AI technologies.

Quick Links

  • Blog
  • Tools
  • About
  • Contact

Categories

  • Cloud Security
  • Zero Trust
  • Networking
  • Cybersecurity
Privacy Policy·Terms of Service

© 2026 Protego. All rights reserved.

Home/Blog/Cloud Engineering
Cloud Engineering8 min read

Auto-Tagging Azure Resources at Creation Time: An Event-Driven Governance Solution

Azure doesn't stamp resources with a CreatedBy tag — but it can. This guide wires Event Grid, an Azure Function with Managed Identity, and Bicep to automatically tag every resource the moment it's created, across the entire tenant.

I
Idan Ohayon
Microsoft Cloud Solution Architect
February 22, 2026
AzureCloud GovernanceAzure FunctionsEvent GridBicepInfrastructure as CodeFinOpsDevOps

Table of Contents

  1. The Problem
  2. The Architecture
  3. The Function (Python)
  4. Infrastructure as Code (Bicep)
  5. Deployment
  6. What You End Up With
  7. Lessons Learned
  8. The Code

The "who created this?" problem is older than the cloud itself — here's how to solve it for good.

The Problem

  • A VM appears in production. No one owns it.
  • An expensive resource sits idle. You don't know who provisioned it or when.
  • A security team asks "who created this storage account?" — and the audit trail is buried in Activity Logs that are already rolling off retention.

Azure doesn't natively stamp resources with a CreatedBy tag. You can enforce tagging policies with Azure Policy, but those require the user to provide the tag — and people forget, skip, or guess wrong.

The real fix is to make it automatic, invisible, and accurate.

The Architecture

The solution uses three Azure-native services wired together in an event-driven pipeline:

Loading diagram...

Why Event Grid? It's push-based (no polling), near-real-time, and fires for every write operation across every subscription. You register one event subscription per Azure subscription and it covers everything inside it.

Why a Function App with Managed Identity? No credentials to rotate. The function authenticates to Azure Resource Manager using its own system-assigned identity, scoped with Tag Contributor — which is the least-privilege role needed to write tags without touching anything else.

The Function (Python)

The core logic is intentionally compact:

import logging
from datetime import datetime, timezone

import azure.functions as func
import requests
from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential()

def main(event: func.EventGridEvent):
    data = event.get_json()
    resource_id = data.get("resourceUri")

    # Extract caller: UPN for users, appId for service principals
    claims = data.get("claims", {})
    caller = claims.get(
        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"
    ) or claims.get("appid")

    if not caller or not resource_id:
        logging.warning("Missing caller or resource ID, skipping.")
        return

    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    token = credential.get_token("https://management.azure.com/.default").token
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }

    tags_url = (
        f"https://management.azure.com{resource_id}"
        f"/providers/Microsoft.Resources/tags/default?api-version=2024-03-01"
    )
    response = requests.patch(tags_url, headers=headers, json={
        "operation": "Merge",
        "properties": {
            "tags": {
                "CreatedBy": caller,
                "CreatedDate": now,
            }
        },
    })

    if response.status_code in (200, 201):
        logging.info("Tagged %s as CreatedBy=%s", resource_id, caller)
    else:
        logging.error("Failed: %s %s", response.status_code, response.text)

A few design decisions worth noting:

operation: "Merge" — This is critical. Using Merge on the Tags API means we only add our two tags without touching any existing tags on the resource. A Replace would wipe everything already there.

UPN vs appId fallback — When a human creates a resource, the Event Grid payload includes their UPN in the claims. When a service principal or automation creates it, the UPN claim is absent but appid is present. This two-step lookup handles both cases cleanly.

DefaultAzureCredential — In production on Azure Functions, this resolves automatically to the managed identity. Locally during development, it falls back to your Azure CLI session. No code changes needed between environments.

Infrastructure as Code (Bicep)

The Function App is deployed with a minimal Bicep template that provisions the storage account, a Linux Consumption plan, and the function app itself — with system-assigned managed identity enabled from the start:

resource app 'Microsoft.Web/sites@2022-03-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp,linux'
  identity: { type: 'SystemAssigned' }   // <-- this is the key
  properties: {
    serverFarmId: plan.id
    siteConfig: {
      linuxFxVersion: 'Python|3.11'
      ...
    }
  }
}

output principalId string = app.identity.principalId

The principalId output is used by the deployment script to immediately assign the Tag Contributor role across every enabled subscription in the tenant.

The Event Grid subscription is deployed at subscription scope (not resource group scope) so it captures everything:

targetScope = 'subscription'

resource eventSub 'Microsoft.EventGrid/eventSubscriptions@2022-06-15' = {
  name: eventSubName
  properties: {
    destination: {
      endpointType: 'WebHook'
      properties: {
        endpointUrl: 'https://${functionAppName}.azurewebsites.net/api/auto-tag-createdby'
      }
    }
    filter: {
      includedEventTypes: [ 'Microsoft.Resources.ResourceWriteSuccess' ]
    }
  }
}

Deployment

The PowerShell deployment script runs six steps in sequence:

  1. Connect to the tenant and set context
  2. Deploy Function App infrastructure via Bicep
  3. Retrieve the managed identity principalId from deployment output
  4. Assign Tag Contributor on every enabled subscription
  5. Wait for the function code to be deployed (zip deploy or VS Code publish)
  6. Deploy the Event Grid subscription to every subscription

Multi-subscription coverage is what makes this operationally useful. One function app handles the entire tenant — you just need one Event Grid subscription registered per Azure subscription pointing at the same webhook endpoint.

What You End Up With

After deployment, every resource created by anyone — via the portal, CLI, SDK, Terraform, Bicep, ARM, a pipeline — gets tagged within seconds:

CreatedBy:   john.doe@company.com
CreatedDate: 2026-01-15T09:42:11Z

For service principal actions (pipelines, automation):

CreatedBy:   a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx  (appId)
CreatedDate: 2026-01-15T09:42:11Z

This data then powers cost management, security investigations, resource ownership reports, and compliance audits — all from a tag that nobody had to remember to set.

Lessons Learned

Event Grid validates your webhook endpoint. When you first create the Event Grid subscription, Azure sends a validation handshake to your function URL. Your function must respond to this before it starts receiving real events. Azure Functions with the Event Grid trigger binding handles this automatically — but your function must be deployed and running before you create the Event Grid subscription. The deployment script pauses at step 5 for exactly this reason.

Not all resources support tags. Some Azure resource types simply don't support the Tags API. The function will get a 4xx error on these. Log it, don't throw — you don't want Event Grid to retry aggressively on resources that will never be taggable.

The Consumption plan is fine for tagging. Cold starts don't matter here — a few extra seconds of latency on tagging is irrelevant. Save the Premium plan budget for latency-sensitive workloads.

Be careful with the Tags API vs the resource PATCH. There are two ways to update tags on a resource: patch the resource itself or use the dedicated /providers/Microsoft.Resources/tags/default endpoint. The dedicated endpoint supports the Merge operation cleanly. Patching the resource requires you to read the full resource body first — fragile and provider-specific.

The Code

The full solution (function, Bicep templates, deployment script) is available on my blog at protego.me.

If you found this useful or have a different approach to the same problem, I'd like to hear about it in the comments.

I

Idan Ohayon

Microsoft Cloud Solution Architect

Cloud Solution Architect with deep expertise in Microsoft Azure and a strong background in systems and IT infrastructure. Passionate about cloud technologies, security best practices, and helping organizations modernize their infrastructure.

Share this article

TwitterLinkedIn

Questions & Answers

Need Help with Your Security?

Our team of security experts can help you implement the strategies discussed in this article.

Contact Us