Skip to main content

Using EventGrid with Storage Accounts and Azure Functions

This is a step-by-step guide to configure Blob Triggers in an Azure Function App using EventGrid for efficient event-driven processing.

warning

Using EventGrid is strongly recommended when your storage account contains more than 10,000 blobs. Blob triggers use polling which becomes inefficient at scale.

Why EventGrid?

MethodBlob TriggerEventGrid
DeliveryPolling (~10s delay)Push (sub-second)
ScaleSlow for >10K blobsInstant
RetriesFunction-level onlyBuilt-in (24h) + dead-letter
Latency~10s polling delaySub-second push

References:

Implementation Steps

Step 1: Configure Dead Letter Storage

Create a storage account for EventGrid dead-letter messages:

module "deadletter_storage" {
source = "pagopa-dx/azure-storage-account/azurerm"
version = "~> 2.0"

environment = {
prefix = "dx"
env_short = "d"
location = "West Europe"
domain = "core"
app_name = "deadletter"
instance_number = "01"
}

resource_group_name = azurerm_resource_group.main.name

containers = [{
name = "eventgrid-deadletter"
access_type = "private"
}]
}

Step 2: Configure RBAC Permissions

Grant the Function App permission to read blobs:

module "function_storage_role" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~> 1.0"

principal_id = module.function_app.function_app.principal_id
subscription_id = data.azurerm_subscription.current.subscription_id

storage_blob = [{
storage_account_name = module.storage_account.name
resource_group_name = azurerm_resource_group.main.name
container_name = "*"
role = "reader"
description = "Allow function app to read blobs"
}]
}

Step 3: Create EventGrid Subscription

Configure EventGrid to trigger your Function App on blob events:

resource "azurerm_eventgrid_event_subscription" "blob_events" {
# Required fields
name = "blob-processing-events"
scope = module.storage_account.id

# Required: endpoint configuration
azure_function_endpoint {
function_id = "${module.function_app.function_app.id}/functions/onBlobEvent"
max_events_per_batch = 1
preferred_batch_size_in_kilobytes = 64
}

# Optional: filter specific event types
included_event_types = ["Microsoft.Storage.BlobCreated"]

# Optional: filter events by subject pattern
subject_filter {
subject_begins_with = "/blobServices/default/containers/uploads/"
subject_ends_with = ".json"
}

# Optional: configure retry behavior
retry_policy {
max_delivery_attempts = 30
event_time_to_live = 1440 # 24 hours
}

# Optional: dead-letter destination for failed events
storage_blob_dead_letter_destination {
storage_account_id = module.deadletter_storage.id
storage_blob_container_name = "eventgrid-deadletter"
}
}

Step 4: Configure Function Code

Create your EventGrid trigger function using the Azure Functions v4 programming model:

import { app, InvocationContext, EventGridEvent } from "@azure/functions";

export async function onBlobEvent(
event: EventGridEvent,
context: InvocationContext,
): Promise<void> {
context.log("EventGrid event received:", event);

// Extract blob information from the event
const blobUrl = event.data.url;
const blobName = blobUrl.split("/").pop();

context.log(`Processing blob: ${blobName}`);

// Your blob processing logic here
// e.g., download blob, process content, store results
}

app.eventGrid("onBlobEvent", {
handler: onBlobEvent,
});
info

The v4 programming model uses decorators and doesn't require function.json files. The function name in app.eventGrid() must match the function name in the EventGrid subscription's function_id.

Step 5: Deploy and Test

  1. Deploy infrastructure: terraform apply
  2. Deploy function code: Deploy your Function App
  3. Test with sample blob:
    az storage blob upload \
    --account-name <storage-account> \
    --container-name uploads \
    --name test.json \
    --file test.json
  4. Verify function execution: Check Azure Portal for function logs

Step 6: Monitor and Validate

Monitor the following metrics:

  • Latency: EventGrid should deliver events in < 1s
  • Success rate: Verify all events are processed successfully
  • Event count: Ensure all blob creation events trigger the function
  • Dead-letter queue: Monitor for failed events

Complete Example

Click to expand full Terraform configuration
module "storage_account" {
source = "pagopa-dx/azure-storage-account/azurerm"
version = "~> 2.0"

environment = {
prefix = "dx"
env_short = "d"
location = "West Europe"
domain = "core"
app_name = "events"
instance_number = "01"
}

resource_group_name = azurerm_resource_group.main.name

containers = [
{ name = "uploads", access_type = "private" },
{ name = "archive", access_type = "private" }
]
}

module "function_app" {
source = "pagopa-dx/azure-function-app/azurerm"
version = "~> 4.0"

environment = {
prefix = "dx"
env_short = "d"
location = "West Europe"
domain = "core"
app_name = "processor"
instance_number = "01"
}

resource_group_name = azurerm_resource_group.main.name
subnet_pep_id = data.azurerm_subnet.pep.id

virtual_network = {
name = "vnet-core"
resource_group_name = "rg-network"
}

health_check_path = "/api/health"
stack = "node"
node_version = 20

app_settings = {
FUNCTIONS_WORKER_RUNTIME = "node"
STORAGE_ACCOUNT_NAME = module.storage_account.name
UPLOADS_CONTAINER = "uploads"
}
}

module "deadletter_storage" {
source = "pagopa-dx/azure-storage-account/azurerm"
version = "~> 2.0"

environment = {
prefix = "dx"
env_short = "d"
location = "West Europe"
domain = "core"
app_name = "deadletter"
instance_number = "01"
}

resource_group_name = azurerm_resource_group.main.name

containers = [{
name = "eventgrid-deadletter"
access_type = "private"
}]
}

module "function_storage_role" {
source = "pagopa-dx/azure-role-assignments/azurerm"
version = "~> 1.0"

principal_id = module.function_app.function_app.principal_id
subscription_id = data.azurerm_subscription.current.subscription_id

storage_blob = [{
storage_account_name = module.storage_account.name
resource_group_name = azurerm_resource_group.main.name
container_name = "*"
role = "reader"
description = "Allow function app to read/write blobs"
}]
}

resource "azurerm_eventgrid_event_subscription" "blob_events" {
name = "blob-processing-events"
scope = module.storage_account.id

azure_function_endpoint {
function_id = "${module.function_app.function_app.id}/functions/onBlobEvent"
max_events_per_batch = 1
preferred_batch_size_in_kilobytes = 64
}

included_event_types = ["Microsoft.Storage.BlobCreated"]

subject_filter {
subject_begins_with = "/blobServices/default/containers/uploads/"
subject_ends_with = ".json"
}

retry_policy {
max_delivery_attempts = 30
event_time_to_live = 1440
}

storage_blob_dead_letter_destination {
storage_account_id = module.deadletter_storage.id
storage_blob_container_name = "eventgrid-deadletter"
}
}

Best Practices

  • Idempotency: Handle duplicate events (EventGrid may deliver same event multiple times)
  • Error handling: Let exceptions bubble up for EventGrid retry logic
  • Monitoring: Track EventGrid delivery metrics and dead-letter queue
  • Filtering: Use subject filters to reduce unnecessary invocations
  • Dead-letter monitoring: Set up alerts when events land in dead-letter storage