Azure Event Grid Trigger Functions’ Two-Step Tango: Why One ‘Terraform Apply’ Isn’t Enough
Infrastructure as code (IaC) promises a clean, repeatable, and reliable way to provision cloud resources without having to rely on the state or availability of the actual application code. However, as developers dive into serverless solutions on Azure, there’s a hidden wrinkle that can undermine this idempotent ideal — particularly when using Azure Functions with Event Grid triggers.
Azure Event Grid Subscriptions perform their own validation step when being created. If the Azure Function designated as an event handler (i.e., the function containing the EventGridTrigger) isn’t already deployed and healthy, the subscription creation process simply fails. This introduces friction in the typical one-step deployment flow and leads to what I like to call the “Terraform Apply Sandwich”. In this article, I’ll dig into why this happens, show the exact error messages you’ll run into, and highlight a workaround that, while not perfect, allows you to keep moving forward.
The Idempotent Ideal vs. Reality
In theory, when setting up an Azure solution using IaC (such as Terraform), you can run one command — terraform apply—and your entire environment should spin up from scratch. Everything from Azure Function Apps to Event Grid Topics and Subscriptions should be created in one pass without manual intervention. This approach works for most Azure resources.
However, for Azure Functions with an Event Grid trigger, a circular dependency rears its head:
- You can’t create the Event Grid Subscription until the Azure Function (not the Function App but the Function application code itself) that the subscription points to exists.
- You can’t deploy your Azure Function until the Function App is provisioned.
As soon as you deploy a brand-new environment that includes an Event Grid Topic, a Function App, and an Event Grid Subscription that targets the function, you’ll see the subscription creation fail on the first run, because the Azure Functions code is not yet deployed or recognized. This can be frustrating if you’re expecting a simple one-and-done deployment.
Azure EventGrid’s “Dirty Little Secret”
Azure EventGrid Subscriptions need to validate their attachment to the Azure Function, which means the function code has to be in place before the subscription is created. If the function code or its top-level metadata (like the function name) isn’t found, the subscription fails to validate.
The Error Message
When you first attempt to create the subscription via Terraform in a completely new environment, you’ll likely see an error like the following:
Error: creating/updating Scoped Event Subscription (Scope: “/subscriptions/9db8d5ac-a7c8–4882–9720-d0c3424699d7/resourceGroups/rg-qonq-watch-prod-eastus2/providers/Microsoft.EventGrid/topics/evgt-qonq-watch-prod-eastus2”
157│ Event Subscription Name: “evgs-qonq-watch-prod-eastus2-internal”): polling after CreateOrUpdate: polling failed: the Azure API returned the following error:
158│
159│ Status: “Failed”
160│ Code: “Endpoint validation”
161│ Message: “Destination endpoint not found. Resource details: resourceId: /subscriptions/9db8d5ac-a7c8–4882–9720-d0c3424699d7/resourceGroups/rg-qonq-watch-prod-eastus2/providers/Microsoft.Web/sites/func-qonq-watch-prod-eastus2/functions/InternalSubscriber. Resource should pre-exist before attempting this operation. Activity id:5d3cb017-b2ac-462d-aa38-f08214ea07c4, timestamp: 1/1/2025 1:58:38 PM (UTC).”
162│ Activity Id: “”
163│
164│ — -
165│
166│ API Response:
167│
168│ — — [start] — —
169│ {“id”:”https://management.azure.com/subscriptions/9db8d5ac-a7c8-4882-9720-d0c3424699d7/providers/Microsoft.EventGrid/locations/eastus2/operationsStatus/75388CA4-CE55-42B9-8188-282B7112E4FB?api-version=2022-06-15","name":"75388ca4-ce55-42b9-8188-282b7112e4fb","status":"Failed","error":{"code":"Endpoint validation”,”message”:”Destination endpoint not found. Resource details: resourceId: /subscriptions/9db8d5ac-a7c8–4882–9720-d0c3424699d7/resourceGroups/rg-qonq-watch-prod-eastus2/providers/Microsoft.Web/sites/func-qonq-watch-prod-eastus2/functions/InternalSubscriber. Resource should pre-exist before attempting this operation. Activity id:5d3cb017-b2ac-462d-aa38-f08214ea07c4, timestamp: 1/1/2025 1:58:38 PM (UTC).”}}
170│ — — -[end] — — -
171│
172│
173│ with module.region_stamp_primary.azurerm_eventgrid_event_subscription.internal,
174│ on modules/regional-stamp/eventgrid.tf line 30, in resource “azurerm_eventgrid_event_subscription” “internal”:
175│ 30: resource “azurerm_eventgrid_event_subscription” “internal” {
176│
177╵
178Error: Terraform exited with code 1.
179Error: Process completed with exit code 1.
Terraform then exits with code 1, complaining that the endpoint validation for the subscription failed because the resource (i.e., the function) did not exist at the time Azure Event Grid tried to validate it. This is expected if the function was only just created and hadn’t even been fully deployed with its code.
The Circular Dependency Problem
At this point, you’re left with a situation where your infrastructure can’t fully provision because your application code (the Azure Function) isn’t ready yet. Meanwhile, your application code can’t fully behave as intended because the infrastructure (the Event Grid Subscription) isn’t complete.
This is effectively a circular dependency: Terraform wants to deploy the Event Grid Subscription but can’t until the Azure Function application code is in place. By default, when using IaC we typically expect that we can create all the necessary Azure resources without requiring the code to be there first. There is a natural dependency chain, at least within the boundary of one system, that infrastructure comes first and then the application.
The “Terraform Apply Sandwich” Workaround
Because Azure requires your function code to be present before Event Grid can validate the endpoint, you need a multi-step approach. One straightforward workaround is what I call the “Terraform Apply Sandwich”, which looks like this: First terraform apply – Deploy your Function App, your Event Grid Topic, and other resources, but disable the Event Grid Subscription.
Deploy the Azure Function code — Ensure that your actual code (the .NET assemblies or whatever language you’re using) is uploaded and the Azure Function is up and healthy.
Second terraform apply – Re-enable the Event Grid Subscription resource and let Terraform create it. This time the subscription creation will succeed because the function endpoint passes validation.
Using a count Meta-Argument
To implement this, you can use a Terraform variable to control whether the Event Grid Subscriptions are created. Here’s a small snippet showing how you can conditionally include it:
resource "azurerm_eventgrid_event_subscription" "internal" {
count = var.eventgrid_subscriptions_enabled ? 1 : 0
name = "evgs-${var.name}-${var.location}-internal"
scope = azurerm_eventgrid_topic.main.id
event_delivery_schema = "CloudEventSchemaV1_0"
azure_function_endpoint {
function_id = local.eventgrid_internal_subscriber_endpoint
}
}
When you run your first terraform apply, you set eventgrid_subscriptions_enabled = false, which effectively prevents Terraform from trying to create the subscription (or quite tragically deleting them if they already exist). Then you deploy your Azure Function code to the newly provisioned Function App. Finally, you run a second terraform apply with eventgrid_subscriptions_enabled = true. Now Terraform will create the Event Grid Subscription, succeed in validating the function endpoint, and complete the setup.
Minimizing the Impact
One of the big downsides to disabling and enabling the subscription is that live events won’t be routed to your function during that in-between period. In production scenarios, this can be a moment of downtime or at least delayed processing of events. However, the silver lining here is that you only need this two-step approach the very first time you set things up. Once the Azure Function is in place (and as long as its top-level metadata, such as the function name, remains the same), future deployments can be performed in a single pass.
It’s also worth noting that this friction usually reappears only if you make significant changes to the function’s name or major deployment parameters that break the previous subscription’s validation. In general, if you keep the name and resource identifiers consistent, you won’t have to do the sandwich method again for subsequent releases.
Conclusion
At first glance, the idea of having to perform multiple steps to deploy a single Azure Function with Event Grid triggers can feel very clunky, especially in a world where Infrastructure as Code is supposed to be fully automated and idempotent. Yet, due to the Event Grid validation process, Azure Functions effectively introduce a small — but meaningful — loop into your deployment flow.
Thankfully, this “Terraform Apply Sandwich” approach resolves the immediate validation problem and keeps you from getting stuck. While it’s not ideal, you typically won’t have to repeat this multi-step process once you’ve established your baseline infrastructure and function. Once your function name and signature remain consistent, your Event Grid Subscriptions won’t complain. That said, it’s important to be aware of this “dirty little secret” in order to avoid being blindsided by it in the first place. Having this knowledge (and workaround) in your toolbox will help you streamline your first-time deployments and ensure your Event Grid triggers are always ready to route events to your Azure Functions.