Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure ExpressRoute Port Authorization Terraform Module

Issues an authorization on an ExpressRoute Direct port — a redeemable key that lets another subscription, or another Microsoft Entra tenant, create an ExpressRoute circuit on your port. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


⚠️ Not the same as a circuit authorization

This resource and azurerm_express_route_circuit_authorization are structurally identical and semantically different. Same three force-new arguments, same computed authorization_key and authorization_use_status, same absent tags — differing in one argument name. What they authorise is not the same thing:

Circuit authorization Port authorization (this module)
Parent An ExpressRoute circuit An ExpressRoute Direct port
The redeemer may Attach a virtual network gateway to a circuit you own Create an ExpressRoute circuit on your port
They end up owning A connection A circuit resource, in their subscription
Cross-tenant Not offered Supported
Scope of the grant One virtual network per key, max 10 per circuit A share of your port's capacity

This is the broader grant of the two. The redeemer does not borrow your circuit; they get a circuit of their own, on your hardware, and cross-tenant redemption is explicitly supported.

ℹ️ The two modules share a resource-shape diagram because the shape genuinely is identical — inventing two would imply a difference a reader would then have to verify. They share nothing else, and reading one module's documentation as though it described the other is the mistake this section exists to prevent.


🧩 Overview

  • ☁️ Creates one azurerm_express_route_port_authorization (the keystone this) — a delegation of ExpressRoute Direct capacity.
  • 🔴 Emits this_authorises_creating_a_circuit_not_connecting_a_virtual_network, because the identically-shaped sibling does something else.
  • 🔒 Emits authorization_key as sensitive — a bearer credential — and has_authorization_key as presence-only so a composition need not copy it.
  • ⚠️ Records that reading this resource returns the key, so plan access is credential access.
  • 📉 States that port capacity is shared and billed to the port owner, and that nothing here updates in place.

💡 Why it matters: ExpressRoute Direct is dedicated hardware you pay for. This resource hands someone else the right to carve a circuit out of it — in their subscription, possibly in another tenant — and the plan for it is three lines long.


❤️ Support this project

If this module saves you time:


🗺️ Where this fits in the family

flowchart TB
  rg["terraform-azurerm-resource-group"]
  port["terraform-azurerm-express-route-port  ExpressRoute Direct hardware"]
  mod["terraform-azurerm-express-route-port-authorization"]
  key["authorization_key  a bearer credential, handed over OUT OF BAND"]
  redeemer["Another subscription OR another Entra tenant"]
  newcirc["An ExpressRoute circuit THEY own, on YOUR port"]
  owncirc["terraform-azurerm-express-route-circuit  a circuit you create on the port yourself"]
  sib["terraform-azurerm-express-route-circuit-authorization  same shape, different meaning"]

  rg -->|"name"| port
  port -->|"name as express_route_port_name"| mod
  port -->|"no authorization needed for your own"| owncirc
  mod -->|"issues"| key
  key -->|"redeemed to CREATE a circuit"| redeemer
  redeemer -->|"owns"| newcirc
  newcirc -->|"draws from your port bandwidth"| port
  mod -.->|"do not confuse with"| sib

  classDef this fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef keystone fill:#004578,stroke:#00243c,color:#ffffff;
  classDef neutral fill:#F3F2F1,stroke:#8A8886,color:#201F1E;
  class mod this;
  class port keystone;
  class rg,key,redeemer,newcirc,owncirc,sib neutral;
Loading

Everything past the key is outside this configuration — another subscription or tenant, and a circuit resource they own that draws from your port. Note the branch on the left: a circuit you create on your own port needs no authorization at all. The dashed edge marks the sibling that shares this resource's exact shape.


🧬 What this module builds

flowchart TB
  subgraph inputs["Inputs  three arguments, ALL force-new"]
    n["name  the only record of who a key went to"]
    rgn["resource_group_name"]
    pn["parent name  express_route_circuit_name OR express_route_port_name"]
  end

  res["azurerm_express_route_circuit_authorization.this  OR  azurerm_express_route_port_authorization.this"]

  subgraph outputs["Outputs"]
    oid["id  the redeemer needs this too, and it is not a secret"]
    okey["authorization_key  SENSITIVE, a bearer credential"]
    ohas["has_authorization_key  presence only, via nonsensitive"]
    ost["authorization_use_status  Available or InUse"]
    ored["is_redeemed  derived"]
    ocred["reading_this_resource_is_reading_a_credential"]
    ofn["nothing_updates_in_place_every_argument_is_force_new"]
  end

  n --> res
  rgn --> res
  pn --> res
  res --> oid
  res --> okey
  res --> ohas
  res --> ost
  res --> ored
  res --> ocred
  res --> ofn

  classDef this fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef neutral fill:#F3F2F1,stroke:#8A8886,color:#201F1E;
  class res this;
  class n,rgn,pn,oid,okey,ohas,ost,ored,ocred,ofn neutral;
Loading

ℹ️ Shared with terraform-azurerm-express-route-circuit-authorization, deliberately — see Not the same as a circuit authorization above.

Resource Cardinality Purpose
azurerm_express_route_port_authorization.this single One authorization on a Direct port: a name, the port, and a generated key.

Three arguments, all three force-new. Both interesting attributes are computed.


✅ Provider / Versions

Item Value
Terraform >= 1.12.0
Provider hashicorp/azurerm ~> 4.0
Provider block None in this module. The caller configures provider "azurerm" { features {} }, auth and subscription.
Module type Standalone — one resource, no children.

Schema notes that bite:

  • 🔴 This authorises creating a circuit, not connecting a virtual network — unlike its identically-shaped sibling.
  • 🔴 Cross-tenant redemption is supported, so the redeemer need not be in your Entra tenant at all.
  • 🔴 Every argument is force-new, so there is no way to adjust an authorization — only to destroy and reissue, which produces a different key.
  • 🔴 authorization_key is returned by a read, so anyone who can plan or refresh can obtain a redeemable credential.
  • 🔴 Destroying it revokes a delegation a circuit in another subscription may already depend on.
  • ⚠️ There is no update timeout, only create, read and delete. A four-key timeouts object silently drops update.
  • ⚠️ No tags attribute, so name is the only record of who a key was issued to.
  • ⚠️ The parent is addressed by name plus resource group, not by ID. Two arguments must agree and nothing here can confirm it.
  • ⚠️ Port capacity is shared and billed to the port owner, so issuing an authorization gives away hardware you pay for.
  • ⚠️ lifecycle is not valid inside a module block, so a caller cannot add prevent_destroy.

🔑 Required Azure RBAC Roles / Permissions

Operation Role Scope
Create or delete the authorization Contributor, Owner or Network Contributor the ExpressRoute Direct port
Read it — which returns the key Reader is sufficient the authorization
Redeem the key and create the circuit the redeemer's own permissions their subscription or tenant
Create a circuit on the port yourself Contributor the port — no authorization needed

🔴 Plan access is credential access. The key is returned by a read, so Reader on this resource is enough to obtain something redeemable. Grant refresh or plan rights on this configuration accordingly.

🔴 No built-in role separates issuing from managing. As with the circuit form, the ability to create and revoke authorizations travels with Contributor-class access to the parent, and splitting them requires a custom role definition.

🔒 Nothing else here is sensitive. The authorization ID, the port name and the use status are all identifiers or status strings.


Azure Prerequisites

  • An existing ExpressRoute Direct port, and its name plus resource group.
  • A redeemer to give the key to — a subscription or Entra tenant that should own a circuit on your port.
  • A way to hand the key over out of band, so it does not have to travel through Terraform outputs.
  • Capacity to spare on the port, because the redeemer's circuit draws from the same bandwidth you are billed for.
  • Acceptance that the resulting circuit is theirs, not yours — you own the port, they own the circuit.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription; this module declares none of these.

📁 Module Structure

terraform-azurerm-express-route-port-authorization/
├── providers.tf     # required_version + pinned azurerm; no provider block
├── variables.tf     # 4 inputs: name, resource_group_name, express_route_port_name, timeouts
├── main.tf          # the keystone `this` + the redemption and key-presence locals
├── outputs.tf       # id first, then the sensitive key, then the facts that produce no error
├── README.md        # this file
├── SCOPE.md         # the cross-module contract
├── LICENSE          # MIT
└── .gitignore

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "partner_port_authorization" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port-authorization.git?ref=v1.0.0"

  name                    = "auth-partner-tenant"
  resource_group_name     = "rg-expressroute-direct"
  express_route_port_name = "er-direct-port-prod"
}

output "key_was_issued" {
  # Presence only -- the key itself is deliberately not routed through an output here.
  value = module.partner_port_authorization.has_authorization_key
}

💡 Name it for who redeems it. There are no tags on this resource, so name is the only record of which key went where.

🔒 Read the key out of band with az network express-route port authorization list, rather than through a Terraform output that copies it into another state file.


🔌 Cross-Module Contract

Consumes

Input Type Source
name string caller — force-new; the only record of the recipient
resource_group_name string terraform-azurerm-resource-group output name — force-new
express_route_port_name string terraform-azurerm-express-route-port output name — force-new
timeouts object(...) caller — three keys only

Emits

Output Description Consumed by
id The authorization's Resource ID. Not secret. the redeemer, out of band
name / resource_group_name / express_route_port_name Identity. All force-new. review
authorization_key sensitive. A bearer credential. the redeemer — prefer out of band
has_authorization_key Derived, not sensitive. Presence only. compositions
authorization_use_status Available or InUse. review
is_redeemed Derived. A true means someone owns a circuit on your hardware. change review
this_authorises_creating_a_circuit_not_connecting_a_virtual_network Always true. design review
destroying_this_revokes_a_delegation_someone_may_have_built_on Always true. change review
reading_this_resource_is_reading_a_credential Always true. permissions review
port_capacity_is_shared_and_billed_to_the_port_owner Always true. cost review
nothing_updates_in_place_every_argument_is_force_new Always true. change review
this_resource_carries_no_tags Always true. tagging review

📚 Example Library

1 · Minimal call
module "authorization" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port-authorization.git?ref=v1.0.0"

  name                    = "auth-partner-tenant"
  resource_group_name     = "rg-expressroute-direct"
  express_route_port_name = "er-direct-port-prod"
}

ℹ️ Three arguments is the whole resource. Everything else about it is computed, and all three are force-new.

2 · Wired to the port module
module "port" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port.git?ref=v1.0.0"

  name                = "er-direct-port-prod"
  resource_group_name = module.rg.name
  location            = "eastus2"
  peering_location    = "Washington DC"
  bandwidth_in_gbps   = 10
  encapsulation       = "Dot1Q"
}

module "authorization" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port-authorization.git?ref=v1.0.0"

  name                    = "auth-partner-tenant"
  resource_group_name     = module.rg.name
  express_route_port_name = module.port.name
}

💡 Pass the port's name, not its id. This resource addresses its parent by name plus resource group — the older azurerm pattern — so wiring the output is what gives Terraform an ordering edge and keeps the two arguments consistent.

3 · What this actually grants
output "read_this_before_issuing" {
  value = module.authorization.this_authorises_creating_a_circuit_not_connecting_a_virtual_network
}

🔴 The redeemer ends up owning a circuit. Not a connection to your circuit — an azurerm_express_route_circuit resource in their subscription, provisioned on your ExpressRoute Direct port, and Microsoft supports redeeming it from another Entra tenant.

⚠️ This is a heavier grant than a circuit authorization, where the redeemer only attaches a gateway to a circuit you continue to own. If what you meant was the lighter one, you want terraform-azurerm-express-route-circuit-authorization.

4 · Your own circuit needs no authorization
module "port" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port.git?ref=v1.0.0"

  name                = "er-direct-port-prod"
  resource_group_name = module.rg.name
  location            = "eastus2"
  peering_location    = "Washington DC"
  bandwidth_in_gbps   = 10
  encapsulation       = "Dot1Q"
}

# No authorization involved -- same subscription, so the circuit is created directly
# on the port.
module "own_circuit" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-circuit.git?ref=v1.0.0"

  name                = "er-circuit-platform"
  resource_group_name = module.rg.name
  location            = "eastus2"
}

ℹ️ Authorizations exist only to cross a boundary. If the circuit is yours and in the same subscription as the port, this module has nothing to do — which is worth stating, because reaching for an authorization by default is a common misreading.

5 · Handing the key over without routing it through Terraform
output "authorization_id_for_the_redeemer" {
  description = "Not a secret."
  value       = module.authorization.id
}

output "key_was_issued" {
  description = "Presence only, so no credential is copied into a consuming state file."
  value       = module.authorization.has_authorization_key
}

Then read the key once, out of band:

az network express-route port authorization list \
  --port-name "er-direct-port-prod" -g "rg-expressroute-direct" \
  --query "[?name=='auth-partner-tenant'].authorizationKey" -o tsv

🔒 This is the recommended pattern. Every configuration that reads authorization_key copies the credential into its own state as well.

6 · Reading the key through Terraform, if you must
output "authorization_key" {
  description = "Bearer credential. Redacted in plan output; NOT encrypted in state."
  value       = module.authorization.authorization_key
  sensitive   = true
}

⚠️ sensitive = true redacts plan and CLI output. It does not encrypt state. The key is written to the state file in plaintext, so the control that actually matters is an encrypted, access-controlled remote backend.

🔒 The consuming output must itself be sensitive, or Terraform refuses the configuration.

7 · Checking whether a key has been redeemed
output "someone_owns_a_circuit_on_our_port" {
  value = module.authorization.is_redeemed
}

output "raw_status" {
  value = module.authorization.authorization_use_status
}
someone_owns_a_circuit_on_our_port = false
raw_status                          = "Available"

⚠️ A true here is a bigger deal than on the circuit form, because what exists on the other side is a circuit resource rather than a connection.

ℹ️ is_redeemed compares case-insensitively, because the value is ARM's string passed through unchanged.

8 · Before you destroy one
output "read_this_before_destroying" {
  value = {
    revokes_a_live_delegation = module.authorization.destroying_this_revokes_a_delegation_someone_may_have_built_on
    currently_in_use          = module.authorization.is_redeemed
  }
}

⚠️ The plan will look trivial — one small authorization record being destroyed, reading like the removal of an unused credential. On the other side of it may be a circuit in a subscription this configuration cannot see.

🔒 lifecycle is not valid inside a module block, so prevent_destroy is unavailable. A CanNotDelete management lock on the port is the guard that works, bearing in mind a lock prevents deletion rather than replacement.

9 · There is no rotation, only reissue
module "authorization" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port-authorization.git?ref=v1.0.0"

  # Changing ANY of these three replaces the resource -- there is no in-place edit.
  name                    = "auth-partner-tenant-2026"
  resource_group_name     = module.rg.name
  express_route_port_name = module.port.name
}

output "nothing_is_editable" {
  value = module.authorization.nothing_updates_in_place_every_argument_is_force_new
}

⚠️ A leaked key cannot be rotated in place. Every argument is force-new, so replacing it means a destroy and a create that issues a different key the redeemer must use again — a coordinated change, not a routine apply.

💡 If nobody has redeemed yet, reissuing is painless. is_redeemed tells you which situation you are in.

10 · What the parent-name validation rejects
# Rejected — a Resource ID where a name belongs.
express_route_port_name = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"

# Rejected — this names an ExpressRoute CIRCUIT, not a Direct port. The sibling
# resource is structurally identical, so the mix-up is easy and detectable.
express_route_port_name = "expressRouteCircuits/er-circuit-prod"

# Accepted.
express_route_port_name = "er-direct-port-prod"

ℹ️ This is a mirror-image guard. terraform-azurerm-express-route-circuit-authorization validates the reverse — a port name where a circuit belongs — so the two modules guard each other and each message names where the value actually belongs, and what the other resource authorises.

11 · Several redeemers with for_each
variable "port_redeemers" {
  type    = set(string)
  default = []
}

module "authorization" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port-authorization.git?ref=v1.0.0"
  for_each = var.port_redeemers

  name                    = "auth-${each.key}"
  resource_group_name     = module.rg.name
  express_route_port_name = module.port.name
}

output "delegations_issued" {
  description = "Name to recipient, since there are no tags to record it."
  value = {
    for key, mod in module.authorization : key => {
      authorization_id = mod.id
      key_was_issued   = mod.has_authorization_key
      redeemed         = mod.is_redeemed
    }
  }
}

💡 An empty default is the right one here. Delegating your ExpressRoute Direct hardware should be an explicit act, so a fresh composition starts with none — and the output doubles as a register of who holds capacity on your port.

12 · 🏗️ End-to-end composition — a Direct port, your own circuit, and a delegated one
provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-expressroute-direct"
  location = "eastus2"

  tags = { environment = "prod" }
}

# 1 · The ExpressRoute Direct port -- dedicated hardware, billed to you. Tag here;
#     authorizations have no tags.
module "port" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port.git?ref=v1.0.0"

  name                = "er-direct-port-prod"
  resource_group_name = module.rg.name
  location            = "eastus2"
  peering_location    = "Washington DC"
  bandwidth_in_gbps   = 10
  encapsulation       = "Dot1Q"

  tags = { environment = "prod", cost_owner = "network-platform" }
}

# 2 · A circuit YOU own on that port. No authorization involved -- same subscription.
module "own_circuit" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-circuit.git?ref=v1.0.0"

  name                = "er-circuit-platform"
  resource_group_name = module.rg.name
  location            = "eastus2"

  tags = { environment = "prod" }
}

# 3 · This module. Delegates the right to create a circuit on the same port to a
#     partner tenant. What they create is theirs, not yours.
module "partner_authorization" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-express-route-port-authorization.git?ref=v1.0.0"

  name                    = "auth-partner-tenant"
  resource_group_name     = module.rg.name
  express_route_port_name = module.port.name
}

output "hand_this_over_out_of_band" {
  description = "The ID is not secret. Read the key with `az network express-route port authorization list`."
  value = {
    authorization_id = module.partner_authorization.id
    key_was_issued   = module.partner_authorization.has_authorization_key
    redeemed         = module.partner_authorization.is_redeemed
  }
}

output "capacity_and_cost_review" {
  value = {
    you_pay_for_the_port  = module.partner_authorization.port_capacity_is_shared_and_billed_to_the_port_owner
    they_get_a_circuit    = module.partner_authorization.this_authorises_creating_a_circuit_not_connecting_a_virtual_network
    before_any_destroy    = module.partner_authorization.destroying_this_revokes_a_delegation_someone_may_have_built_on
  }
}

💡 Steps 2 and 3 side by side are the point. The same port carries a circuit you own — created directly, no authorization — and a delegation letting a partner create one of their own. That contrast is the clearest way to see what an authorization is for.

⚠️ The partner's circuit is deliberately absent from this composition, because it lives in their subscription or tenant. Modelling it here would imply this configuration manages it; it does not, which is exactly why destroying step 3 is dangerous.

⚠️ hand_this_over_out_of_band carries no key on purpose. It reports that a key exists and whether it has been used, leaving the credential itself to be read once from the CLI.


📥 Inputs

Input Type Default Notes
name string — Required. Force-new. The only record of the recipient.
resource_group_name string — Required. Force-new.
express_route_port_name string — Required. Force-new. Rejects a circuit name.
timeouts object(...) null Three keys only — no update. A fourth is silently dropped.
Full schemas
variable "name" { type = string }

variable "resource_group_name" { type = string }

# Rejects a Resource ID, and rejects a value naming an ExpressRoute CIRCUIT.
variable "express_route_port_name" { type = string }

# NOTE three keys, not four -- this resource has no update.
variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    delete = optional(string)
  })
  default = null
}

ℹ️ There is no tags variable, because the resource has no tags attribute.


🧾 Outputs

Output Type Notes
id string The Resource ID. The redeemer needs it; not a secret.
name string Force-new.
resource_group_name string Force-new.
express_route_port_name string Force-new.
authorization_key string sensitive. A bearer credential. Not encrypted in state.
has_authorization_key bool Derived, not sensitive. Presence only.
authorization_use_status string Available or InUse.
is_redeemed bool Derived.
this_authorises_creating_a_circuit_not_connecting_a_virtual_network bool Always true. Read this one.
destroying_this_revokes_a_delegation_someone_may_have_built_on bool Always true.
reading_this_resource_is_reading_a_credential bool Always true.
port_capacity_is_shared_and_billed_to_the_port_owner bool Always true.
nothing_updates_in_place_every_argument_is_force_new bool Always true.
this_resource_carries_no_tags bool Always true.

🔒 One output is sensitive and one is deliberately not. authorization_key is a credential; has_authorization_key is one bit about its existence, unwrapped with nonsensitive() so a composition can assert an authorization exists without copying the key into its own state.


🧠 Architecture Notes

The resemblance to the circuit authorization is exact and the meaning is not, which is the single most important thing about this module. Both resources take a name, a resource group and a parent name; both are entirely force-new; both emit a sensitive authorization_key and an authorization_use_status; neither has tags. But a circuit authorization lets a circuit user attach a virtual network gateway to a circuit you own, one per virtual network and at most ten per circuit. A port authorization lets another subscription — or another Microsoft Entra tenant — create an ExpressRoute circuit on your ExpressRoute Direct port. The redeemer does not borrow anything; they end up owning a circuit resource, in their subscription, drawing on your hardware. This module shares a resource-shape diagram with its sibling because the shape genuinely is the same, and states the difference in prose and in a dedicated output rather than inventing a second diagram.

Which makes this the broader of the two grants. Cross-tenant redemption is explicitly supported here and is not offered on the circuit form, and what the redeemer creates is a first-class resource of their own rather than a connection into yours.

Authorizations exist only to cross a boundary. A circuit you create yourself on your own port needs no authorization at all — it is created directly against the port in the same subscription. Reaching for this module by default is a common misreading, which is why an example exists that uses none of it.

Every argument is force-new, so there is no such thing as adjusting an authorization. Any change is a destroy and create, and the create issues a different key that the redeemer must use again. A leaked key therefore cannot be rotated in place. The absence of an update timeout follows from that, and it creates the one input this module accepts silently and wrongly: a four-key timeouts object drops update without an error, because object-type conversion discards attributes the type does not declare.

The key is a bearer credential and reading the resource returns it. authorization_key is marked sensitive, which redacts plan and CLI output and does not encrypt state — the key is in the state file in plaintext, so the control that matters is an encrypted, access-controlled backend. Reader on the authorization is enough to obtain the key, so plan access is credential access and belongs in a permissions decision rather than a discovery afterwards. This module therefore also emits has_authorization_key, a presence-only boolean produced with nonsensitive(), so a composition can assert a key was issued without copying it. The unwrap is required rather than stylistic: comparing a sensitive value yields a sensitive boolean, which Terraform refuses to emit as an output.

Destroying it revokes a delegation someone may already have built on, and the plan understates that as thoroughly as it does on the circuit form. What sits on the other side is a circuit in a subscription this configuration cannot see. is_redeemed distinguishes a free destroy from an expensive one, and because lifecycle is unavailable inside a module block, a CanNotDelete management lock on the port is the only real guard.

Capacity and cost travel with the delegation and appear nowhere in a plan. An ExpressRoute Direct port is dedicated hardware billed to whoever owns it, and circuits created on it draw from its bandwidth. The redeeming subscription is billed for its own circuit charges while the port underneath remains your cost and your finite resource — so issuing a port authorization is a commercial and capacity decision as much as a networking one.

There are no tags, so name is the only durable record of who a key was issued to. Given that a redeemer ends up owning a circuit on your hardware, a naming convention here is doing real work.


🧱 Design Principles

Concern This module's position Why
The resemblance to its sibling Shape diagram shared; meaning stated in prose and an output. The shape is genuinely identical; inventing a second diagram would imply a difference to verify.
The difference in meaning A dedicated output and a table at the top of the README. Reading the wrong module's docs is the mistake most likely to be made here.
The key sensitive = true, with state's limits stated plainly. It is a bearer credential; implying sensitive encrypts state is worse than not marking it.
Routing the key through Terraform Discouraged, with has_authorization_key offered instead. Every consuming configuration that reads it copies the credential into its own state.
Presence flag nonsensitive() at the point of use. A comparison against a sensitive value is itself sensitive and cannot be emitted.
Destroy Named in an output, with the other subscription spelled out. The plan understates it, and no Terraform signal exists.
Plan permissions Named as a non-role row in the RBAC table. Reader returns the key, so plan access is credential access.
Force-new everywhere Named in an output, with the reissue sequence explained. lifecycle is unavailable to a module caller.
The wrong parent Rejected, with the sibling module and its meaning named. The circuit resource is structurally identical; the mix-up is plausible and catchable.
Cost and capacity Reported, never chosen. Delegating hardware you pay for is a commercial decision, not a module's.
tags Absent, and the absence is stated. The schema has no tags; name carries the record instead.

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module with ?ref=v1.0.0 — never a branch. Plan-only; a human applies from CI.

Then, outside Terraform:

# 1 · Read the key once and hand it over securely.
az network express-route port authorization list \
  --port-name "er-direct-port-prod" -g "rg-expressroute-direct" \
  --query "[].{name:name, status:authorizationUseStatus}" -o table

# 2 · Before any destroy, confirm nothing is redeemed.
terraform output -json | jq '.hand_this_over_out_of_band'

# 3 · The redeemer creates the circuit in THEIR subscription or tenant, passing
#     the key and a reference to your port.

🧪 Testing

What the offline gate proves: that the configuration parses against the pinned provider, that formatting is canonical, and — via terraform console with variables files — that every validation {} fires on the input it targets. Proved with a failing value each: a Resource ID in name, resource_group_name and express_route_port_name; an empty string in each of those three; and a value naming an ExpressRoute circuit in express_route_port_name. Proved as accepted: a complete, realistic call.

The sensitivity handling was verified rather than assumed, because nonsensitive() errors if its argument is not actually sensitive. Evaluating the pattern in terraform console confirmed that a comparison against a sensitive value is sensitive, that nonsensitive() unwraps it successfully, and that the result is no longer sensitive — so has_authorization_key is a genuine presence flag rather than a construct that would fail at plan time.

authorization_key, authorization_use_status, has_authorization_key and is_redeemed are computed by Azure, so their values cannot be evaluated offline — only the expressions that derive them.

What only an apply exercises: whether the port exists in that resource group, and what key Azure generates.

What no Terraform run exercises at all: whether the key reached the right party, whether they redeemed it, what circuit they created, and how much of your port's bandwidth it consumes.


💬 Example Output

Outputs:

authorization_key = <sensitive>
authorization_use_status = "Available"
destroying_this_revokes_a_delegation_someone_may_have_built_on = true
express_route_port_name = "er-direct-port-prod"
has_authorization_key = true
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-expressroute-direct/providers/Microsoft.Network/expressRoutePorts/er-direct-port-prod/authorizations/auth-partner-tenant"
is_redeemed = false
name = "auth-partner-tenant"
nothing_updates_in_place_every_argument_is_force_new = true
port_capacity_is_shared_and_billed_to_the_port_owner = true
reading_this_resource_is_reading_a_credential = true
resource_group_name = "rg-expressroute-direct"
this_authorises_creating_a_circuit_not_connecting_a_virtual_network = true
this_resource_carries_no_tags = true

🔍 Troubleshooting

Symptom Cause Fix
express_route_port_name looks like it names an ExpressRoute CIRCUIT The sibling resource's parent was passed. Use terraform-azurerm-express-route-circuit-authorization for a circuit.
looks like a Resource ID on any argument An ID where a name belongs. This resource addresses its parent by name plus resource group.
A timeouts key is ignored update was passed; this resource has only three timeouts. Remove it. Object conversion drops it silently.
Apply fails: port not found The name and resource group do not agree. Wire both from terraform-azurerm-express-route-port.
Terraform refuses the output authorization_key was re-emitted without sensitive = true. Mark the consuming output sensitive, or use has_authorization_key.
The redeemer says the key does not work It was reissued, or already redeemed. Check is_redeemed; issue a fresh authorization if needed.
You expected to connect a virtual network This authorises creating a circuit, not connecting a VNet. Use the circuit authorization module instead.
An authorization seems unnecessary The circuit is yours, in the same subscription as the port. It is unnecessary — create the circuit directly on the port.
A destroy broke a partner's circuit Revoking withdraws a delegation they built on. Expected. Check is_redeemed first; use a lock to prevent a repeat.
The partner's traffic degrades yours Circuits on the port share its bandwidth, which you pay for. Nothing in this resource limits it; resize the port or separate the workloads.
No tags argument accepted The resource has no tags attribute. Tag the port; use name to record the recipient.

🔗 Related Docs


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