Workarounds for creating Azure Hybrid Connections with Terraform
UPDATE: The bug has been fixed and released in 3.98. The workarounds are no longer needed.
Due to some bugs in the current Terraform azurerm provider (v3.80.0), certain workarounds are needed to create a working hybrid connection.
The issue is present for both Function Apps and Web Apps.
Following the official documentation’s examples, the following code will not work and will result in a seemingly successful deployment, but the hybrid connection will not work. No connection will be able to be established, but everything looks fine in the Azure Portal.
I have two workarounds for this issue and a workaround idea.
Workaround 1 - Run a Provisioner after Deployment of Function App Hybrid Connection
resource "azurerm_function_app_hybrid_connection" "example" {
function_app_id = azurerm_linux_function_app.example.id
relay_id = azurerm_relay_hybrid_connection.example.id
hostname = "server.example.local"
port = 443
send_key_name = azurerm_relay_hybrid_connection_authorization_rule.example.name
lifecycle {
ignore_changes = [
relay_id
]
}
provisioner "local-exec" {
command = "az functionapp hybrid-connection add --hybrid-connection ${azurerm_relay_hybrid_connection.example.name} --namespace ${azurerm_relay_namespace.example.name} -n ${azurerm_linux_function_app.example.name} -g ${azurerm_resource_group.example.name} --subscription ${var.subscription_id}"
}
}
The thing to note in this workaround is that the provisioner will change the relay_id
which differs in letter casing from what Terraform expects. This will cause Terraform to see the resource as changed and will recreate the hybrid connection on every run. To prevent this, use the lifecycle meta-argument for ignore_changes
on relay_id
.
Workaround 2 - Use a Null Resource and Provisioners to create the Hybrid Connection with Azure CLI
resource "null_resource" "function_app_hybrid_connection" {
triggers = {
function_app_name = azurerm_linux_function_app.example.name
resource_group_name = azurerm_resource_group.example.name
namespace_name = azurerm_relay_namespace.example.name
hybrid_connection_name = azurerm_relay_hybrid_connection.example.name
subscription_id = var.subscription_id
}
provisioner "local-exec" {
command = "az functionapp hybrid-connection add --hybrid-connection ${self.triggers.hybrid_connection_name} --namespace ${self.triggers.namespace_name} -n ${self.triggers.function_app_name} -g ${self.triggers.resource_group_name} --subscription ${self.triggers.subscription_id}"
}
provisioner "local-exec" {
when = destroy
command = "az functionapp hybrid-connection remove --hybrid-connection ${self.triggers.hybrid_connection_name} --namespace ${self.triggers.namespace_name} -n ${self.triggers.function_app_name} -g ${self.triggers.resource_group_name} --subscription ${self.triggers.subscription_id}"
}
}
This workaround will not use the azurerm_function_app_hybrid_connection
resource at all, but instead use a null_resource
with provisioners to create the hybrid connection with Azure CLI. The method prevents Terraform from seeing changes to the resource and will not recreate the hybrid connection on every run, but nor will it be able to update the hybrid connection if it is changed in the Azure Portal.
Workaround 3 - The Idea
This is but an idea and I have not attempted it. It may be possible to make an ARM template and use the azurerm_resource_group_template_deployment
to deploy the Function App Hybrid Connection.
As with Workaround 2, this would not detect the changes to the resource. The benefit though would be having no dependence on Azure CLI.
Remember the Metadata
When creating the Hybrid Connection in the Azure Relay, remember to add the metadata endpoint
with the value of the hostname and port of the server you want to connect to, otherwise the provisioner will fail and the Azure Portal will not display the information correctly.
resource "azurerm_relay_hybrid_connection" "example" {
name = "example"
resource_group_name = azurerm_resource_group.example.name
relay_namespace_name = azurerm_relay_namespace.example.name
requires_client_authorization = true
user_metadata = jsonencode(
[
{
key = "endpoint",
value = "server.example.local:443"
}
])
}