Automating SignalR and API Management with Terraform
When building real-time features in a distributed web application, SignalR over WebSockets is often a natural choice. With Azure API Management (APIM) as a central API gateway, it’s tempting to integrate everything — HTTP and WebSocket-based traffic alike — into a single Terraform-managed pipeline.
This article walks through my attempt to automate the provisioning of SignalR and its WebSocket connectivity using APIM and Terraform. I’ll outline my initial strategy, the architectural decisions I made, and ultimately, the roadblock I hit when trying to define WebSocket operations in APIM.
Defining the WebSocket API in APIM
My approach began with creating a WebSocket API in Azure API Management using Terraform. I intended to define a WebSocket-specific API with a connection route that would upgrade incoming HTTP requests to WebSocket protocol (via the standard 101 Switching Protocols response). My assumption was that, like with standard HTTP operations, I could define an operation in APIM that would route WebSocket traffic to the appropriate backend service—in this case, an Azure Container App running my SignalR server.
I created the operation using azurerm_api_management_api_operation and added a policy using azurerm_api_management_api_operation_policy, expecting to define the backend routing logic just as I do for HTTP APIs. Here’s the policy resource:
resource "azurerm_api_management_api_operation_policy" "realtime_ws_connect_policy" {
api_name = azurerm_api_management_api.realtime_ws.name
api_management_name = var.api_management.name
resource_group_name = var.api_management.resource_group
operation_id = azurerm_api_management_api_operation.realtime_ws_connect.operation_id
xml_content = local.api_policy_xml
}
The policy’s purpose is twofold: it routes requests to the correct regional backend and handles failover in case of service failure. The backend selection is critical to my regional failover strategy, which is why the policy is dynamically generated based on the available backends:
locals {
primary_backend = try(var.backends[0], "")
secondary_backend = try(var.backends[1], "")
secondary_policy = local.secondary_backend == "" ? "" : <<XML
<set-backend-service backend-id="${local.secondary_backend}" />
<retry condition="@(true)" count="1" interval="0" />
XML
api_policy_xml = <<XML
<policies>
<inbound>
<set-backend-service backend-id="${local.primary_backend}" />
<base />
</inbound>
<backend>
<forward-request timeout="30" />
</backend>
<outbound><base /></outbound>
<on-error>
${local.secondary_policy}
<base />
</on-error>
</policies>
XML
}
This setup works well for standard HTTP APIs. My application is stateless at the API layer, and I use CosmosDB for shared state, so active-active deployments across regions are feasible. However, when it comes to WebSocket connections — particularly with SignalR — statefulness becomes a major consideration. WebSockets are long-lived connections, and routing a client to a different region mid-session isn’t viable without sticky sessions or connection replication. A React client can scatter HTTP requests across regions without issue, but its SignalR connection is bound to a specific server for the duration of the session.
The Roadblock: WebSocket Operation Creation Fails
While applying this configuration in Terraform, I encountered the following error:
BadRequest: ValidationError, "Operation entity cannot be defined by user for web socket api type."
This error stopped my automation efforts in their tracks. I assumed that defining an operation — like a connection route for the WebSocket upgrade — was required, just as it is for HTTP APIs. However, APIM treats WebSocket APIs quite differently.
Why APIM Rejects WebSocket Operations
The key issue is that APIM does not support custom operations for WebSocket APIs in the same way it does for HTTP-based APIs. When you define a WebSocket API in APIM, you are only configuring a single route — the connection endpoint. Unlike REST APIs, which can have multiple operations (GET, POST, etc.), a WebSocket API is treated as a single logical unit: the upgrade to a persistent connection.
Because of that, Azure API Management does not allow users to explicitly define operations for WebSocket APIs. The platform handles the upgrade route implicitly. Any attempt to define custom operations, as I tried to do via Terraform, will trigger a validation error like the one above.
This behavior is not immediately obvious from the documentation, but it’s a hard restriction within the APIM resource provider.
What You Can Do Instead
If your goal is to route WebSocket traffic through APIM to a backend (such as a Container App running a SignalR service), you should:
Define the WebSocket API without operations — Simply create the API resource of type websocket and specify the route (e.g., /ws) for the upgrade.
Attach a global policy to the API itself (not to individual operations), which sets the backend and manages routing or failover.
This means adjusting your Terraform setup to apply the routing policy at the API level:
resource "azurerm_api_management_api_policy" "realtime_ws_policy" {
api_name = azurerm_api_management_api.realtime_ws.name
api_management_name = var.api_management.name
resource_group_name = var.api_management.resource_group
xml_content = local.api_policy_xml
}
And skipping the operation and operation policy resources entirely. The same api_policy_xml structure can still be used to define your primary and secondary backend routing logic—it just needs to be applied at the API level.
Conclusion
My attempt to automate WebSocket support in Azure API Management via Terraform revealed an important limitation: WebSocket APIs in APIM do not support user-defined operations. This makes sense when you consider the connection-oriented nature of WebSockets. Unlike REST APIs, there are no stateless, distinct endpoints to manage — just a single connection route.
The takeaway is that while APIM supports WebSocket APIs, it expects a different configuration model than HTTP APIs. Understanding this distinction will save time and confusion when building infrastructure as code for real-time applications. By applying policies at the API level instead of defining operations, we can still manage backend routing and regional failover — just within the constraints that APIM enforces for WebSockets.