Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Azure Function App Hybrid Connection Terraform Module

Gives a function app a TCP tunnel to one on-premises host and port through Azure Relay (azurerm_function_app_hybrid_connection), with no inbound firewall port opened. Targets hashicorp/azurerm ~> 4.0.

Terraform Provider Module Type Resources


🧩 Overview

  • 🔌 Connects a function app to one on-premises host and port through Azure Relay, as a keystone resource named this.
  • 🚪 No inbound firewall port is opened — the tunnel is established outbound by an on-premises agent, which is why this beats exposing an internal endpoint.
  • 🔑 Pushes toward a dedicated Send-only Relay authorization rule rather than the namespace-wide root key the service defaults to.
  • 🔐 Emits send_key_value as sensitive — the provider reads that key into state whether or not the module emits it.
  • ⚠️ Documents the hosting constraint up front: hybrid connections are unavailable on the Consumption plan, and the per-app count is capped by plan tier.
  • 🧰 States plainly that the Hybrid Connection Manager is an out-of-band install Terraform never provisions.

💡 Why it matters: the tunnel's runtime authorization is a Relay shared access key, not Azure RBAC — so what the app can actually reach is decided by the authorization rule's scope, not by a role assignment. Left at the default, that rule is the namespace root key with Manage, Send, and Listen over everything.

❤️ Support this project

If this module saves you time, please consider supporting its continued development:


🗺️ Where this fits in the family

flowchart LR
  app["terraform-azurerm-function-app-linux or -windows"]
  ns["terraform-azurerm-relay-namespace"]
  rel["terraform-azurerm-relay-hybrid-connection"]
  rule["a Send-only Relay authorization rule"]
  this["terraform-azurerm-function-app-hybrid-connection"]
  hc["azurerm_function_app_hybrid_connection"]
  hcm["Hybrid Connection Manager, installed on-premises"]
  host["one on-premises host and port"]

  app -->|"function_app_id"| this
  ns -->|"owns"| rel
  rel -->|"relay_id"| this
  rule -->|"send_key_name"| this
  this -->|"creates"| hc
  hc -->|"tunnel completed by"| hcm
  hcm -->|"reaches"| host

  classDef me fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
  classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
  class this me;
  class hc keystone;
  class app,ns,rel,rule,hcm,host sib;
Loading

🧬 What this module builds

flowchart TB
  appid["function_app_id: force-new"]
  relid["relay_id: force-new"]
  host["hostname: resolved privately, not by public DNS"]
  port["port: validated 1 to 65535, one per connection"]
  skn["send_key_name: null accepts the namespace root rule"]
  plan["unavailable on the Consumption plan, count capped by tier"]
  this["terraform-azurerm-function-app-hybrid-connection"]
  hc["azurerm_function_app_hybrid_connection.this"]
  skv["send_key_value: computed and sensitive, in state regardless"]
  out["outputs: id, hostname, port, relay_name, send_key_name"]

  appid -->|"consuming app"| this
  relid -->|"tunnel carrier"| this
  host -->|"endpoint"| this
  port -->|"endpoint"| this
  skn -->|"tunnel authorization"| this
  plan -->|"apply-time constraint"| this
  this -->|"creates"| hc
  hc -->|"provider reads the key into state"| skv
  hc -->|"emits"| out

  classDef me fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
  classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
  class this me;
  class hc keystone;
  class appid,relid,host,port,skn,plan,skv,out sib;
Loading

Resource inventory

Resource Count Role
azurerm_function_app_hybrid_connection.this 1 The keystone hybrid connection, with its timeouts block.

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module — the caller configures provider "azurerm" { features {} }, auth, and subscription.

Schema notes that bite (verified against the live provider schema):

  • function_app_id and relay_id are force-new; hostname, port, and send_key_name update in place.
  • One host and one port per connection. Reaching a second port on the same host needs a second hybrid connection — there is no port-range form.
  • hostname is resolved by the Hybrid Connection Manager on the private network, not by public DNS, so an internal name is expected and a publicly resolvable one is usually a mistake.
  • send_key_value is computed and sensitive: the provider reads the Relay authorization rule's primary key into state whether or not the module emits it. Rotating that key on the Relay side therefore produces state drift here.
  • Leaving send_key_name null accepts RootManageSharedAccessKey — the namespace-wide root rule carrying Manage, Send, and Listen over the whole Relay namespace, far more than a tunnel needs.
  • Hybrid connections are unavailable on the Consumption plan, and the per-app count is capped by plan tier. Both are apply-time failures, because the plan is not an input to this module.
  • ⚠️ The provider's service-plan check runs on terraform import and nowhere else. It lives in the resource's CustomImporter; Create, Update, Read and Delete contain no plan check at all. A connection created by a normal apply against a Consumption plan is not refused by Terraform — Azure decides, and nothing in the plan warns you.
  • 🔀 That import guard rejects a narrower SKU set than the web-app sibling's. For a function app it rejects the Consumption SKU Y1 only, so Elastic Premium (EP1/EP2/EP3) is permitted here — while the web-app resource rejects those same Elastic SKUs. A genuine capability difference between two near-identical resources; do not carry the rule across.
  • The connection can exist, apply cleanly, and carry no traffic if the Hybrid Connection Manager is not installed or cannot reach the endpoint. Nothing in Terraform surfaces that.
  • This resource type has no tags surface.

🔑 Required Azure RBAC Roles / Permissions

  • Website Contributor or Contributor on the function app, covering Microsoft.Web/sites/hybridConnectionNamespaces/relays/write.
  • Read access to the Relay hybrid connection and its authorization rules, because the provider reads the rule's primary key in order to configure the app.
  • Note that the tunnel's runtime authorization is the Relay shared access key, not Azure RBAC — so the effective control over what the app can reach is the authorization rule's scope and permissions, not a role assignment.

Azure Prerequisites

  • An existing function app on a plan that supports hybrid connections. The Consumption plan does not, and the number of connections a function app may hold is capped by its plan tier.
  • An existing Relay namespace and hybrid connection.
  • Preferably a dedicated Send-only authorization rule on that hybrid connection, created before this module runs.
  • The Hybrid Connection Manager installed and registered on the private network, able to reach the on-premises host and port.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription.

📁 Module Structure

terraform-azurerm-function-app-hybrid-connection/
├── providers.tf   # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf   # ids, hostname, validated port, send_key_name, timeouts tail
├── main.tf        # keystone azurerm_function_app_hybrid_connection.this; dynamic timeouts
├── outputs.tf     # id, hostname, port, relay/namespace names, send_key_name, send_key_value (sensitive)
├── README.md      # this document
├── SCOPE.md       # cross-module contract
├── LICENSE        # MIT
└── .gitignore     # canonical library ignore set

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "sql_tunnel" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-hybrid-connection.git?ref=v1.0.0"

  function_app_id = module.orders_app.id
  relay_id        = module.sql_relay.id

  hostname = "sql01.corp.internal"
  port     = 1433

  # A dedicated Send-only rule, rather than the namespace root key.
  send_key_name = "functions-send"
}

ℹ️ The caller owns the provider, its authentication, and the mandatory features {} block. This module never declares them.


🔌 Cross-Module Contract

Consumes

Input Type Source module
function_app_id string terraform-azurerm-function-app-linux (id) or terraform-azurerm-function-app-windows (id)
relay_id string terraform-azurerm-relay-hybrid-connection (id)
send_key_name string a Relay authorization rule created alongside the hybrid connection

Emits

Output Description Consumed by
id Hybrid connection Resource ID (first) audit inventories
hostname The on-premises hostname reached connectivity review
port The on-premises TCP port reached connectivity review
relay_name The Relay hybrid connection in use composition wiring
namespace_name The Relay namespace that owns it composition wiring
service_bus_namespace The Service Bus namespace backing the Relay composition wiring
service_bus_suffix The DNS suffix for the Relay endpoint composition wiring
send_key_name The authorization rule the app uses governance review — RootManageSharedAccessKey is the over-privileged default
send_key_value Sensitive. The rule's primary access key Hybrid Connection Manager configuration

📚 Example Library

The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:

variable "orders_storage_name" {
  description = "name of an existing orders storage that these examples reference but do not create."
  type        = string
}

variable "sql_mirror_relay_id" {
  description = "id of an existing sql mirror relay that these examples reference but do not create."
  type        = string
}
1 · A SQL Server tunnel
module "sql_tunnel" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-hybrid-connection.git?ref=v1.0.0"

  function_app_id = module.orders_app.id
  relay_id        = module.sql_relay.id
  hostname        = "sql01.corp.internal"
  port            = 1433
}

⚠️ With send_key_name unset this uses the namespace root key. Read example 3 before shipping it.

2 · An internal hostname, not a public one
hostname = "sql01.corp.internal" # ✅ resolved by the on-prem agent
# hostname = "sql.example.com"   # ❌ usually a mistake — that resolves publicly

ℹ️ The Hybrid Connection Manager resolves this name on the private network. A publicly resolvable hostname suggests the endpoint does not actually need a tunnel — or that the tunnel will reach the wrong host.

3 · A dedicated Send-only authorization rule
# Create a Send-only rule on the Relay hybrid connection first...
resource "azurerm_relay_hybrid_connection_authorization_rule" "functions_send" {
  name                   = "functions-send"
  resource_group_name    = module.integration_rg.name
  namespace_name         = module.relay_namespace.name
  hybrid_connection_name = module.sql_relay.name

  listen = false
  send   = true
  manage = false
}

# ...then name it here.
send_key_name = azurerm_relay_hybrid_connection_authorization_rule.functions_send.name

🔒 This is the hardening step. The default RootManageSharedAccessKey carries Manage, Send, and Listen over the entire Relay namespace; a tunnel needs Send on one hybrid connection. The module cannot default to this rule because it does not exist until you create it, so least privilege is one line of configuration plus the rule.

4 · One host and one port per connection
# Two ports on the same host means two hybrid connections.
module "sql_tunnel" {
  source          = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-hybrid-connection.git?ref=v1.0.0"
  function_app_id = module.orders_app.id
  relay_id        = module.sql_relay.id
  hostname        = "sql01.corp.internal"
  port            = 1433
}

module "sql_mirror_tunnel" {
  source          = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-hybrid-connection.git?ref=v1.0.0"
  function_app_id = module.orders_app.id
  relay_id        = var.sql_mirror_relay_id
  hostname        = "sql01.corp.internal"
  port            = 5022
}

⚠️ There is no port-range form. Note each connection needs its own Relay hybrid connection, not just its own module instance — and each one counts against the plan's cap.

5 · Port validated at plan
port = 70000 # ❌
Error: Invalid value for variable

  port must be between 1 and 65535.
6 · The Consumption-plan constraint
# ❌ A Consumption-hosted function app cannot use hybrid connections at all.

⚠️ This fails at apply, not at plan, because the hosting plan is not an input to this module. Hybrid connections need a dedicated or Elastic Premium plan, and the number allowed per app is capped by tier — a fifth connection on a plan permitting four fails the same way.

7 · The Hybrid Connection Manager is not Terraform's job
# This module creates the Azure side. The tunnel carries no traffic until the
# Hybrid Connection Manager is installed on the private network and registered
# against this Relay hybrid connection.

⚠️ A clean apply proves the Azure-side configuration exists, not that anything is connected. The agent install is an out-of-band step, and its absence produces a function app that times out reaching the endpoint with nothing wrong in Terraform.

8 · Handing the key to the agent
output "hcm_send_key" {
  description = "Primary key for the Hybrid Connection Manager registration. Treat as a credential."
  value       = module.sql_tunnel.send_key_value
  sensitive   = true
}

🔒 The provider reads this key into state whether or not it is emitted, so the module surfaces it as a sensitive output rather than pretending it is absent. Prefer reading it from state when configuring the agent over passing it between modules.

9 · Key rotation shows as drift
# Rotating the Relay authorization rule's primary key outside Terraform:
~ send_key_value = (sensitive value)

ℹ️ Expected, not a problem. send_key_value is computed, so a rotation on the Relay side appears as a refresh diff here. Re-register the Hybrid Connection Manager with the new key when you rotate.

10 · A PostgreSQL tunnel
hostname = "pg01.corp.internal"
port     = 5432

💡 The pattern is the same for any TCP endpoint — a database, an LDAP directory, a legacy SOAP service. Hybrid connections are TCP-level, so nothing about the protocol above matters.

11 · Updating the endpoint in place
# hostname, port, and send_key_name update in place — no replacement.
hostname = "sql02.corp.internal" # failing over to a second host

ℹ️ Only function_app_id and relay_id are force-new. Repointing the tunnel at a different host is an in-place update, which makes a planned failover a small, reviewable diff.

12 · Several tunnels from a keyed map
locals {
  tunnels = {
    sql = { host = "sql01.corp.internal", port = 1433, relay = "sql" }
    ldap = { host = "dc01.corp.internal", port = 636, relay = "ldap" }
  }
}

module "tunnels" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-hybrid-connection.git?ref=v1.0.0"
  for_each = local.tunnels

  function_app_id = module.orders_app.id
  relay_id        = module.relays[each.value.relay].id
  hostname        = each.value.host
  port            = each.value.port
  send_key_name   = "functions-send"
}

⚠️ Count these against the plan's cap before applying — the limit is per app and per tier, and exceeding it fails at apply.

13 · Reviewing tunnel authorization across apps
output "tunnel_review" {
  description = "Per-tunnel endpoint and authorization rule. A RootManageSharedAccessKey here is over-privileged."
  value = {
    for k, m in module.tunnels : k => {
      endpoint = "${m.hostname}:${m.port}"
      rule     = m.send_key_name
      relay    = m.relay_name
    }
  }
}

💡 send_key_name is emitted precisely so this table can be built from state. Any row showing RootManageSharedAccessKey is a hardening candidate.

14 · Custom timeouts
timeouts = {
  create = "30m"
  read   = "5m"
  update = "30m"
  delete = "30m"
}

ℹ️ The defaults are ample — this resource writes a small configuration record. It does not wait for the tunnel to become healthy, which is why a clean apply says nothing about connectivity.

15 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

# 1 · The resource group.
module "integration_rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-integration-eastus2"
  location = "eastus2"
}

# 2 · A plan that supports hybrid connections — not Consumption.
module "orders_plan" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-service-plan.git?ref=v1.0.0"

  name                = "asp-orders-eastus2"
  resource_group_name = module.integration_rg.name
  location            = module.integration_rg.location
  os_type             = "Linux"
  sku_name            = "P1v3"
}

# 3 · The function app that needs the on-premises database.
module "orders_app" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-linux.git?ref=v1.0.0"

  name                 = "func-orders"
  resource_group_name  = module.integration_rg.name
  location             = module.integration_rg.location
  service_plan_id      = module.orders_plan.id
  storage_account_name = var.orders_storage_name
}

# 4 · The Relay namespace and hybrid connection that carry the tunnel.
module "relay_namespace" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-relay-namespace.git?ref=v1.0.0"

  name                = "relay-integration-eastus2"
  resource_group_name = module.integration_rg.name
  location            = module.integration_rg.location
  sku_name            = "Standard"
}

module "sql_relay" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-relay-hybrid-connection.git?ref=v1.0.0"

  name                          = "sql01-1433"
  resource_group_name           = module.integration_rg.name
  relay_namespace_name          = module.relay_namespace.name
  requires_client_authorization = true
}

# 5 · A Send-only rule — the least-privilege alternative to the namespace root key.
resource "azurerm_relay_hybrid_connection_authorization_rule" "functions_send" {
  name                   = "functions-send"
  resource_group_name    = module.integration_rg.name
  namespace_name         = module.relay_namespace.name
  hybrid_connection_name = module.sql_relay.name

  listen = false
  send   = true
  manage = false
}

# 6 · The tunnel — this module.
module "sql_tunnel" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-hybrid-connection.git?ref=v1.0.0"

  function_app_id = module.orders_app.id
  relay_id        = module.sql_relay.id

  hostname = "sql01.corp.internal" # resolved by the on-prem agent, not public DNS
  port     = 1433

  send_key_name = azurerm_relay_hybrid_connection_authorization_rule.functions_send.name
}

# 7 · The key the on-premises agent needs, as a sensitive output.
output "hcm_send_key" {
  description = "Primary key for registering the Hybrid Connection Manager. Treat as a credential."
  value       = module.sql_tunnel.send_key_value
  sensitive   = true
}

💡 This wiring shows the full path: resource group → a slot-capable, hybrid-connection-capable plan → the function app → Relay namespace and hybrid connection → a Send-only rule → the tunnel. Step 5 is the one usually skipped, and it is the whole difference between least privilege and handing the app a namespace-wide root key. Remember that step 7's key still has to reach a Hybrid Connection Manager install on the private network — Terraform does not provision that, and without it the tunnel carries no traffic. Output names on sibling modules are illustrative; match them to the versions you pin.


📥 Inputs

Required: function_app_id, relay_id, hostname, port.

Relay authorization: send_key_name.

Universal tail: timeouts. This resource type does not support tags.

Full object() schemas
variable "function_app_id" { type = string } # force-new
variable "relay_id"        { type = string } # force-new

variable "hostname" {
  type = string # an INTERNAL name, resolved by the Hybrid Connection Manager
}

variable "port" {
  type = number # validated 1-65535. One host + one port per connection.
}

variable "send_key_name" {
  type    = string # null accepts RootManageSharedAccessKey — the namespace-wide root rule
  default = null
}

variable "timeouts" {
  type    = object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) })
  default = null
}

🧾 Outputs

Output Description Kind
id Resource ID of the Function App Hybrid Connection (emitted first) Passthrough
function_app_id Resource ID of the Function App this connection belongs to Passthrough
relay_id Resource ID of the Relay hybrid connection in use Passthrough
hostname Hostname of the endpoint reached through the relay, as resolved on the REMOTE side Passthrough
port TCP port of the remote endpoint Passthrough
relay_name Name of the Relay in use, as reported by Azure Passthrough
namespace_name Name of the Relay namespace, as reported by Azure Passthrough
service_bus_namespace The Service Bus namespace backing the relay Passthrough
service_bus_suffix DNS suffix for the endpoint, which differs by cloud (for example servicebus.windows.net in public Azure) Passthrough
send_key_name Name of the Relay authorization rule whose Send key this connection uses Passthrough
send_key_value Primary access key for send_key_name Passthrough
uses_relay_root_key True when this connection uses the Relay namespace's built-in RootManageSharedAccessKey rule, which carries MANAGE rights over the whole namespace Derived
plan_type_is_only_checked_on_import Constant true, and the most consequential thing to know about this resource Constant
unsupported_plan_skus_on_import The service-plan SKUs the provider's import guard rejects for a FUNCTION APP hybrid connection: the Consumption SKU Y1, and nothing else Derived
elastic_premium_is_unsupported_here Constant FALSE for a FUNCTION APP, and true on the web-app sibling Constant
requires_hybrid_connection_manager_on_premises Constant true Constant
hostname_is_resolved_remotely Constant true Constant
binds_one_relay_to_one_app Constant true Constant
endpoint_changes_apply_in_place Constant true Constant

send_key_value is read into Terraform state by the provider regardless, so the module surfaces it as a sensitive output rather than pretending it is absent. No other secret is accepted or emitted.

🧠 Architecture Notes

  • The tunnel's authorization is a Relay shared key, not Azure RBAC. What the app can reach is decided by the authorization rule's scope and permissions — so send_key_name is the real access-control decision in this module, and leaving it null hands the app RootManageSharedAccessKey: Manage, Send, and Listen over the whole Relay namespace.
  • send_key_name is left at the provider default, against this suite's secure-by-default convention. The least-privilege alternative is a Relay authorization rule that does not exist until the caller creates it, so a module default cannot point at it. The hardening is documented in the variable description, the schema notes, an example, and here, and send_key_name is emitted so an over-privileged connection is visible in review. This mirrors the identical decision in terraform-azurerm-web-app-hybrid-connection, so the two behave the same way.
  • send_key_value is in state regardless. The provider reads the rule's primary key to configure the app, so withholding the output would add friction without adding protection — hence a sensitive output. A rotation on the Relay side appears here as a refresh diff.
  • One host, one port, one connection. There is no port-range form, and each additional connection needs its own Relay hybrid connection as well as its own module instance — and counts against the plan's cap.
  • The plan tier is invisible to this module. Hybrid connections do not exist on Consumption, and the per-app count is tier-capped, but the plan is not an input — so both failures arrive at apply.
  • A clean apply is not a working tunnel. The Azure side is a small configuration record; the traffic path requires a Hybrid Connection Manager install on the private network, which Terraform never provisions. A missing or misconfigured agent produces timeouts with nothing wrong in Terraform.
  • hostname is resolved privately. It goes to the on-premises agent's resolver, not public DNS, so an internal name is expected. A publicly resolvable name usually means either the tunnel is unnecessary or it will reach the wrong host.
  • Endpoint changes are in-place. Only the two IDs are force-new, so repointing hostname or port — a planned failover, say — is a small reviewable diff rather than a replacement.
  • features {} dependence. The module carries no provider {} block. If it appears not to initialize in isolation, the cause is a missing caller-side provider "azurerm" { features {} }.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Relay authorization not defaulted to least privilege — the rule does not exist yet; documented and emitted instead leave send_key_name null and use the namespace root key
Authorization visibility send_key_name emitted as an output — (no opt-out; that is the point)
Key handling send_key_value emitted sensitive, since it is in state regardless —
Inbound exposure none — the tunnel is established outbound by the on-prem agent — (structural to hybrid connections)
Endpoint scope one host and one port per connection add a second connection
Port correctness validated 1-65535 at plan — (no opt-out)
Hostname documented as an internal, privately-resolved name use a public name and reach the wrong host

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 — never a branch.
  • This library is plan-only during authoring; a human runs terraform plan / apply from CI against real credentials.
  • After apply, verify the tunnel from the app — the Azure-side record existing proves nothing about connectivity. Confirm the Hybrid Connection Manager shows the connection as Connected.

🧪 Testing

  • terraform validate proves the configuration is internally consistent and type-correct against the pinned provider schema, and exercises the port range check.
  • terraform fmt -check enforces canonical formatting.
  • Neither command calls Azure. Only terraform plan (run by a human, from CI) exercises the ARM API — the module ships without any cloud apply. Whether the plan tier supports hybrid connections, whether the per-app cap is exceeded, whether the named authorization rule exists, and whether the Hybrid Connection Manager can reach the endpoint are all apply-time or operational facts.

💬 Example Output

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

id                    = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-integration-eastus2/providers/Microsoft.Web/sites/func-orders/hybridConnectionNamespaces/relay-integration-eastus2/relays/sql01-1433"
hostname              = "sql01.corp.internal"
port                  = 1433
relay_name            = "sql01-1433"
namespace_name        = "relay-integration-eastus2"
service_bus_namespace = "relay-integration-eastus2"
service_bus_suffix    = ".servicebus.windows.net"
send_key_name         = "functions-send"
send_key_value        = <sensitive>

🔍 Troubleshooting

Symptom Cause Fix
Provider configuration not present / features error No caller-side provider "azurerm" { features {} }. Add the provider block with features {} in the root module.
Apply fails: hybrid connections not supported The function app is on a Consumption plan. Move to a dedicated or Elastic Premium plan.
Apply fails: too many hybrid connections The per-app count is capped by plan tier. Raise the tier or consolidate connections.
Plan error: port must be between 1 and 65535 An out-of-range port. Correct the port.
Apply succeeds but the app cannot reach the endpoint The Hybrid Connection Manager is not installed, not registered, or cannot resolve the hostname. Install and register the agent; confirm it shows Connected.
The app reaches the wrong host A publicly resolvable hostname was used instead of an internal one. Use the name the on-premises resolver knows.
send_key_value shows a diff on refresh The Relay authorization rule's key was rotated outside Terraform. Expected; re-register the agent with the new key.
Apply fails: authorization rule not found send_key_name names a rule that does not exist on the Relay hybrid connection. Create the rule first, then reference its name.
The app has broader Relay access than intended send_key_name was left null, so RootManageSharedAccessKey is in use. Create a Send-only rule and name it; check the send_key_name output.
A second port on the same host does not work One host and one port per connection. Add a second Relay hybrid connection and a second module instance.

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."