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.
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.
- ☁️ Creates one
azurerm_express_route_port_authorization(the keystonethis) — 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_keyassensitive— a bearer credential — andhas_authorization_keyas 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.
If this module saves you time:
- ⭐ Star the repository — it helps others find it.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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;
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.
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;
ℹ️ 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.
| 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_keyis 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 noupdatetimeout, onlycreate,readanddelete. A four-keytimeoutsobject silently dropsupdate.⚠️ Notagsattribute, sonameis 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.⚠️ lifecycleis not valid inside amoduleblock, so a caller cannot addprevent_destroy.
| 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
Readeron 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.
- 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.
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
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
nameis 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.
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 |
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 itsid. 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_circuitresource 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 wantterraform-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_keycopies 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 = trueredacts 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"
⚠️ Atruehere 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_redeemedcompares 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.
🔒
lifecycleis not valid inside amoduleblock, soprevent_destroyis unavailable. ACanNotDeletemanagement 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_redeemedtells 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-authorizationvalidates 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_bandcarries 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.
| 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
tagsvariable, because the resource has notagsattribute.
| 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_keyis a credential;has_authorization_keyis one bit about its existence, unwrapped withnonsensitive()so a composition can assert an authorization exists without copying the key into its own state.
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.
| 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. |
terraform init -backend=false
terraform validate
terraform fmt -checkPin 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.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.
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
| 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. |
azurerm_express_route_port_authorizationprovider reference- Microsoft Learn — Create ExpressRoute Direct, including port authorizations
- Microsoft Learn — About ExpressRoute Direct
- Parent:
terraform-azurerm-express-route-port— suppliesexpress_route_port_name. - Sibling:
terraform-azurerm-express-route-circuit-authorization— structurally identical, semantically different: it authorises attaching a virtual network gateway to a circuit you own. The two modules share a resource-shape diagram and nothing else. - Sibling:
terraform-azurerm-express-route-circuit— a circuit on this port that you create yourself, needing no authorization. - Sibling:
terraform-azurerm-express-route-gatewayandterraform-azurerm-express-route-connection— the gateway side, for connections you own. - This module's
SCOPE.md.
💙 "Infrastructure as Code should be standardized, consistent, and secure."

