Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Azure Synapse Self-Hosted Integration Runtime Terraform Module

Registers one self-hosted integration runtime on a Synapse workspace — the registration for compute you run yourself, to reach data Azure cannot. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Secret-aware


🧩 Overview

  • 🧩 Creates one azurerm_synapse_integration_runtime_self_hosted — a registration for a runtime you install and operate.
  • 🔴 Leads with the fact that decides who may run a plan: the provider reads this runtime's two authorization keys on every refresh. Plan access is key access.
  • 🔓 States that the provider marks neither key sensitive — both are plain Computed strings, so anything that prints them prints them in the clear.
  • 🔐 Never emits either key. SHA-256 fingerprints and a distinctness flag give the rotation-comparison power with none of the exposure.
  • 🖥️ Explains that this record runs nothing until a node is installed on a machine and registered with one of those keys — and that Terraform cannot see whether one is.
  • 🕳️ Documents a failure mode with no obvious cause: a not-found on the key listing drops the runtime from state, even though the runtime itself was found.
  • 🚩 Reports a defect in the provider's own name message — it claims a three-character minimum its regular expression does not enforce, the mirror image of the Azure sibling's.
  • 🏷️ Carries no tags and no location — the resource has neither. The universal tail is timeouts only.

💡 Why it matters: this is a three-argument resource that looks trivial and is the most credential-exposed one in the Synapse family. Its keys are read on every plan, redacted by nothing, and written to state in plaintext whether or not anything uses them — and the runtime they protect is the one with a route into your on-premises network.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

flowchart TB
  rg["terraform-azurerm-resource-group"]
  ws["terraform-azurerm-synapse-workspace"]
  this["terraform-azurerm-synapse-integration-runtime-self-hosted"]
  azir["terraform-azurerm-synapse-integration-runtime-azure"]
  node["a machine you own, running the runtime software"]
  src["a data source Azure cannot reach"]

  rg -->|"name"| ws
  ws -->|"id"| this
  ws -->|"id"| azir
  this -->|"an authorization key, carried to the node by hand"| node
  node -->|"reads"| src
  azir -->|"the other runtime type, Microsoft-hosted compute"| ws

  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef keystone fill:#004578,stroke:#002b4d,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b9c7d6,color:#1b2a3a;
  class this me;
  class ws keystone;
  class rg,azir,node,src sib;
Loading

The two nodes on the right are the point of the resource and neither is in Azure. A machine you own runs the runtime software, registers with an authorization key carried to it out of band, and reaches a data source that Azure has no route to. Nothing in Terraform can see either of them.


🧬 What this module builds

flowchart TB
  subgraph inputs["Inputs"]
    ident["name plus synapse_workspace_id, both force-new"]
    desc["description, the only field that updates in place"]
  end

  this["azurerm_synapse_integration_runtime_self_hosted.this"]

  subgraph keys["Computed by Azure, read on every refresh"]
    k1["authorization_key_primary"]
    k2["authorization_key_secondary"]
  end

  subgraph outputs["Outputs"]
    oid["id, name, synapse_workspace_id, synapse_workspace_name, resource_group_name"]
    ofp["authorization_key_primary_fingerprint, authorization_key_secondary_fingerprint, authorization_keys_are_distinct"]
    osec["reading_this_resource_reads_its_credentials, the_provider_does_not_redact_these_keys, the_keys_are_in_state_in_plaintext"]
    ofact["this_registration_runs_nothing_until_a_node_is_installed, a_missing_key_response_removes_this_from_state, name_is_shorter_than_three_characters"]
  end

  ident --> this
  desc --> this
  this --> k1
  this --> k2
  k1 -->|"hashed, never emitted"| ofp
  k2 -->|"hashed, never emitted"| ofp
  this --> oid
  this --> osec
  this --> ofact

  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b9c7d6,color:#1b2a3a;
  class this me;
  class ident,desc,k1,k2,oid,ofp,osec,ofact sib;
Loading

Resource inventory

Resource Count Notes
azurerm_synapse_integration_runtime_self_hosted 1 The keystone this. No children — the nodes are not Azure resources.

✅ Provider / Versions

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

Schema notes that bite

  • 🔴 The read path calls a second client and lists the authorization keys on every refresh. That is what makes plan access credential access, and it is why read covers two API calls inside one timeout.
  • 🔴 Neither authorization key is marked Sensitive. Both are plain Computed strings, so nothing redacts them anywhere by default.
  • 🔴 If the key-listing call returns not-found, the provider clears the resource ID and drops the record from state even though the runtime itself was found. The next plan proposes a create, which then fails against the existing runtime.
  • 🔴 The provider's name error message claims a three-character minimum its regular expression does not enforce — a one-character name is accepted. The consecutive-dash rule the same message claims is enforced here.
  • 🔴 The Azure sibling carries the same message and a different pattern, wrong about the opposite half.
  • Two force-new fields, both declared on the resource itself: name and synapse_workspace_id. Only description updates in place — and a replacement issues new keys, so every node must be re-registered.
  • There is no location and no compute size. The machine you install the runtime on is the compute.
  • This resource does have an existence check on create — an apply over an existing runtime fails and tells you to import.
  • No tags.

🔑 Required Azure RBAC Roles / Permissions

Least-privilege, at the smallest scope that works:

  • Microsoft.Synapse/workspaces/integrationRuntimes/write and .../integrationRuntimes/delete on the target workspace. A custom role scoped to the workspace is enough; Contributor on the workspace, or on its resource group, covers this more broadly than necessary.
  • Microsoft.Synapse/workspaces/integrationRuntimes/read on the workspace, for the existence check before creating and for every refresh.
  • 🔴 Microsoft.Synapse/workspaces/integrationRuntimes/listAuthKeys/action on the workspace — required by every refresh, not only by an explicit key read.

🔴 Plan access is credential access on this resource. A terraform plan reads this runtime's two registration keys. Grant refresh rights only to identities you would hand those keys to — and note that the provider marks neither key sensitive, so they are not redacted anywhere by default.

⚠️ The key's own blast radius is not bounded by any Azure role. An authorization key registers a new node against this runtime, and a registered node executes pipeline activities with whatever access those activities have — including to the on-premises systems the runtime exists to reach.


Azure Prerequisites

  • An existing Synapse workspace (the synapse-workspace module).
  • One or more machines you control to install the runtime software on, with outbound connectivity to Azure and a route to whatever data the runtime is meant to reach.
  • A way to get an authorization key to those machines out of band. This module fingerprints the keys and never emits them.
  • A pipeline or linked service that selects this runtime by name, if it is to do anything.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription.

📁 Module Structure

terraform-azurerm-synapse-integration-runtime-self-hosted/
├── providers.tf    # required_version and the pinned azurerm; no provider block
├── variables.tf    # the workspace, the name, the description, timeouts
├── main.tf         # the single keystone azurerm_synapse_integration_runtime_self_hosted.this
├── outputs.tf      # id first, then identity, then key fingerprints and the quiet facts
├── README.md       # this file
├── SCOPE.md        # the cross-module contract
├── LICENSE         # MIT
└── .gitignore

⚙️ Quick Start

module "onprem_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"

  name                 = "ir-onprem-sql"
  synapse_workspace_id = var.synapse_workspace_id
}

⚠️ The caller configures provider "azurerm" { features {} }, its authentication, and its subscription. This module declares no provider block.

🚨 This registration runs nothing. Install the runtime software on a machine and register it with one of the authorization keys, or the runtime exists, looks healthy in Studio, and executes nothing. See example 2.


🔌 Cross-Module Contract

Consumes

Input Type Source
synapse_workspace_id string terraform-azurerm-synapse-workspace output id

Emits

Output Consumed by
id management locks, role assignments, review
name the pipeline activity that selects this runtime, and the node that reports against it
authorization_key_primary_fingerprint rotation review
reading_this_resource_reads_its_credentials permissions review
this_registration_runs_nothing_until_a_node_is_installed design review

📚 Example Library

1 · The smallest real call
module "onprem_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"

  name                 = "ir-onprem-sql"
  synapse_workspace_id = var.synapse_workspace_id
}

💡 The resource takes three arguments and only description is optional. There is no region and no compute size — the machine you install the runtime on is the compute.

2 · What this registration does not do
module "onprem_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"

  name                 = "ir-onprem-sql"
  synapse_workspace_id = var.synapse_workspace_id
}

output "still_needs_a_node" {
  value = module.onprem_runtime.this_registration_runs_nothing_until_a_node_is_installed
}

🚨 A self-hosted runtime is a registration, not compute. Until the runtime software is installed on a machine and registered with one of the authorization keys, this record exists, appears in Synapse Studio, and executes nothing. An apply proves only that the registration was created.

⚠️ Nothing in Terraform can see whether a node is attached, how many there are, or whether they are healthy. That is what the description field is for — see example 6.

3 · Why plan access is key access
output "a_plan_reads_the_registration_keys" {
  value = module.onprem_runtime.reading_this_resource_reads_its_credentials
}

🔴 The provider's read path calls a second client and lists this runtime's two authorization keys on every refresh — not only when something references them. So a terraform plan is a credential read, and the identity running it needs Microsoft.Synapse/workspaces/integrationRuntimes/listAuthKeys/action.

⚠️ Grant refresh rights on this resource only to identities you would hand the registration keys to. An authorization key registers a new node, and a registered node runs pipeline activities with whatever access those activities have — including into the network the runtime exists to reach.

4 · The keys, and why this module will not give them to you
output "primary_key_fingerprint" {
  value = module.onprem_runtime.authorization_key_primary_fingerprint
}

output "keys_differ" {
  value = module.onprem_runtime.authorization_keys_are_distinct
}

output "the_provider_redacts_nothing" {
  value = module.onprem_runtime.the_provider_does_not_redact_these_keys
}

🔒 Neither key is emitted, in any form but a SHA-256 hash. The provider marks neither of them Sensitive, which makes re-emitting worse than usual: the value would be copied into every consuming configuration's state and printed in the clear in any plan that shows it.

🚨 sensitive = true would redact plan output and would NOT encrypt state. These keys are read into state on every refresh whether or not anything uses them, so the state file holds live registration credentials. The control that matters is an encrypted, access-controlled backend that is never a local file in a repository.

ℹ️ To register a node, read the key from Azure at the moment you need it — the portal or the CLI — rather than routing it through Terraform outputs.

5 · Rotating a key without printing one
output "key_fingerprints" {
  value = {
    primary   = module.onprem_runtime.authorization_key_primary_fingerprint
    secondary = module.onprem_runtime.authorization_key_secondary_fingerprint
    distinct  = module.onprem_runtime.authorization_keys_are_distinct
  }
}

💡 Two keys exist so that one can be rotated while nodes are still registering with the other: point new registrations at the secondary, regenerate the primary, then swap back. Comparing the fingerprints across runs is how you confirm that sequence actually happened — without either key appearing in a log.

⚠️ A distinct = false is worth investigating rather than ignoring. Two identical keys defeat the point of having two.

6 · Recording what Terraform cannot see
module "onprem_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"

  name                 = "ir-onprem-sql"
  synapse_workspace_id = var.synapse_workspace_id

  description = "Nodes: dc1-etl01 and dc1-etl02, owned by the on-premises platform team. Reaches the finance SQL Server cluster. Key rotation: quarterly, secondary first."
}

💡 description is the only place on this record to write down which machines are registered against it and who owns them. The nodes are not Azure resources, so nothing in the portal, the state file or this module can tell you what is attached — and there is no tags argument here either.

7 · The failure mode with no obvious cause
output "why_it_vanished_from_state" {
  value = module.onprem_runtime.a_missing_key_response_removes_this_from_state
}

🕳️ The provider's read fetches the runtime and then lists its authorization keys. If that second call returns not-found, the provider clears the resource ID and drops the record from state — even though the runtime itself was found a moment earlier.

🚨 The next plan then proposes to create it, and that create fails against the existing runtime, because this resource does have an existence check. Re-import rather than fighting the plan:

terraform import 'module.onprem_runtime.azurerm_synapse_integration_runtime_self_hosted.this' \
  '/subscriptions/SUB/resourceGroups/RG/providers/Microsoft.Synapse/workspaces/WORKSPACE/integrationRuntimes/ir-onprem-sql'
8 · The name rule the provider misdocuments
module "onprem_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"

  # ACCEPTED here, even though the provider's own error message claims a
  # three-character minimum -- its regular expression does not enforce one.
  name                 = "i"
  synapse_workspace_id = var.synapse_workspace_id
}

output "relying_on_a_provider_gap" {
  value = module.onprem_runtime.name_is_shorter_than_three_characters
}

🚩 This module mirrors the provider's pattern, not its message. A rule the provider does not enforce may be one Azure does not enforce either, and a validation {} failure would block terraform destroy as well as apply — so the condition is reported rather than refused.

⚠️ A true here means you are relying on that gap. A one-character runtime name is also a poor thing to ask an operator to type into a node registration dialog.

9 · Two runtimes, two different name rules
# THIS resource: consecutive dashes REJECTED, one-character name ACCEPTED.
#   "ir--onprem"    rejected
#   "i"             accepted

# The AZURE runtime: minimum 3 characters ENFORCED, consecutive dashes PERMITTED.
#   "ir--dataflow"  accepted
#   "ab"            rejected

module "onprem_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"

  name                 = "ir-onprem-sql" # legal on both
  synapse_workspace_id = var.synapse_workspace_id
}

output "do_not_assume_one_convention" {
  value = module.onprem_runtime.the_two_runtime_types_do_not_share_a_name_rule
}

ℹ️ Both resources carry the same error message text and different regular expressions, and each message is wrong about the opposite half. A name of at least three characters using only single dashes is legal on both, which is the convention worth adopting.

10 · What a replacement costs you
output "what_replaces_this_runtime" {
  value = module.onprem_runtime.force_new_fields
}
# Both force-new fields are ordinary-looking:
#   name                 -- and pipelines name the runtime by this string
#   synapse_workspace_id -- a runtime belongs to one workspace
#
# Only `description` updates in place.

🚨 A replacement issues new authorization keys. Every machine registered against the old runtime stops working and has to be re-registered by hand, on each node, with a key someone has to fetch and carry. Renaming this resource is an operations task, not a Terraform edit.

11 · This is not the Azure runtime
# THIS module -> azurerm_synapse_integration_runtime_self_hosted
#   A REGISTRATION for compute you run yourself. No location, no core count,
#   two authorization keys the provider reads on every refresh.
module "onprem_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"

  name                 = "ir-onprem-sql"
  synapse_workspace_id = var.synapse_workspace_id
}

# The SIBLING -> azurerm_synapse_integration_runtime_azure
#   Microsoft-hosted compute, with a region and a core count. No credentials.
module "dataflow_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-azure.git?ref=v1.0.0"

  name                 = "ir-dataflow-prod"
  synapse_workspace_id = var.synapse_workspace_id
  location             = "eastus2"
}

🔒 The difference that matters operationally: plan access on this resource is credential access, and on the Azure one it is not. Reach for this module only when the data is somewhere Azure cannot reach — a self-hosted runtime is a machine to patch, monitor and re-register, and the managed one is not.

12 · Several runtimes, one per site
locals {
  sites = {
    "ir-onprem-london"   = "Nodes: lon-etl01, lon-etl02. Reaches the London finance SQL cluster."
    "ir-onprem-frankfurt" = "Nodes: fra-etl01. Reaches the Frankfurt warehouse."
  }
}

module "site_runtimes" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"
  for_each = local.sites

  name                 = each.key
  synapse_workspace_id = var.synapse_workspace_id
  description          = each.value
}

output "key_fingerprints_by_site" {
  value = { for k, m in module.site_runtimes : k => m.authorization_key_primary_fingerprint }
}

💡 One runtime per site rather than one shared runtime keeps each site's nodes registering with their own keys, so a key rotation at one site does not touch another. The fingerprint map is how you confirm a rotation landed where it was meant to.

⚠️ The map key becomes the name, which is force-new — and a replacement issues new keys for that site. Treat these keys as an interface, not as labels.

13 · 🏗️ End-to-end composition
module "data_rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-data-platform"
  location = "eastus2"

  tags = {
    environment = "prod"
    owner       = "data-platform-team"
  }
}

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

  name                                 = "syn-platform-prod"
  resource_group_name                  = module.data_rg.name
  location                             = module.data_rg.location
  storage_data_lake_gen2_filesystem_id = var.filesystem_id

  azuread_authentication_only   = true
  public_network_access_enabled = false

  tags = module.data_rg.tags
}

module "workspace_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-workspace-aad-admin.git?ref=v1.0.0"

  synapse_workspace_id = module.synapse.id

  login     = "Synapse Platform Admins"
  object_id = var.platform_admins_group_object_id
  tenant_id = var.tenant_id
}

# Managed compute for data flows inside Azure.
module "dataflow_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-azure.git?ref=v1.0.0"

  name                 = "ir-dataflow-prod"
  synapse_workspace_id = module.synapse.id
  location             = module.data_rg.location

  compute_type     = "MemoryOptimized"
  core_count       = 32
  time_to_live_min = 20
}

# Self-hosted compute for the systems Azure cannot reach.
module "onprem_runtime" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-synapse-integration-runtime-self-hosted.git?ref=v1.0.0"

  name                 = "ir-onprem-sql"
  synapse_workspace_id = module.synapse.id

  description = "Nodes: dc1-etl01 and dc1-etl02, owned by the on-premises platform team. Reaches the finance SQL Server cluster."
}

output "runtime_names_for_pipeline_definitions" {
  value = {
    managed     = module.dataflow_runtime.name
    self_hosted = module.onprem_runtime.name
  }
}

output "self_hosted_key_fingerprint" {
  value = module.onprem_runtime.authorization_key_primary_fingerprint
}

output "remaining_manual_step" {
  value = module.onprem_runtime.this_registration_runs_nothing_until_a_node_is_installed
}

🚨 This composition is not finished when it applies cleanly. The self-hosted runtime is registered and has no nodes; someone has to install the software on dc1-etl01 and dc1-etl02 and register each with an authorization key. Treat remaining_manual_step as the reminder that it is owed.

🔴 Note what the two runtimes cost you in access terms: planning this configuration reads the self-hosted runtime's keys. If the pipeline that plans it is not one you would trust with a route into the finance network, split the self-hosted runtime into its own state and its own pipeline.


📥 Inputs

Identity — name and synapse_workspace_id, both required and both force-new.

Documentation — description, optional and the only field that updates in place.

Universal tail — timeouts only. This resource has no tags and no location.

Full input schema
Name Type Default Required Notes
name string — yes Letters, numbers and single dashes, first and last alphanumeric. Force-new. A one-character name is accepted by the provider despite its message.
synapse_workspace_id string — yes Full Synapse workspace Resource ID, anchored at both ends. Force-new.
description string null no Free text. Must be non-empty when supplied. The only place to record which machines are registered.
timeouts object({ create, read, update, delete }) null no Go duration strings. Provider defaults: create 30m, read 5m, update 30m, delete 30m.

⚠️ A key not declared in the timeouts object type is silently discarded by Terraform's type conversion rather than reported — a misspelled key produces no error and no timeout.

ℹ️ read covers two API calls: the runtime, and then its authorization keys through a separate client. Both happen inside that single timeout.

There are no key inputs. Both authorization keys are Computed — Azure generates them, and the provider reads them back.


🧾 Outputs

Output Description Notes
id Resource ID of the runtime Emitted first
name The string a pipeline names, and a node reports against Force-new
synapse_workspace_id The parent workspace, as Azure holds it Rebuilt from the record's own ID
synapse_workspace_name Workspace name, parsed from that ID Derived locally
resource_group_name Resource group, parsed from that ID Derived locally
description The description as Azure holds it null when unset
authorization_key_primary_fingerprint SHA-256 of the primary key The key itself is never emitted
authorization_key_secondary_fingerprint SHA-256 of the secondary key The key itself is never emitted
authorization_keys_are_distinct True when the two keys differ A false is worth investigating
reading_this_resource_reads_its_credentials Constant true Read this one first
the_provider_does_not_redact_these_keys Constant true Control review
the_keys_are_in_state_in_plaintext Constant true Secret handling
this_registration_runs_nothing_until_a_node_is_installed Constant true Design review
a_missing_key_response_removes_this_from_state Constant true Troubleshooting
name_is_shorter_than_three_characters True when the name relies on a provider gap Naming review
the_two_runtime_types_do_not_share_a_name_rule Constant true Naming review
there_is_no_location_on_this_resource Constant true Design review
force_new_fields ["name", "synapse_workspace_id"] A replacement issues new keys
fields_azure_returns_on_read The fields in which drift is detectable at all Drift review
this_resource_supports_no_azure_resource_tags Constant true Tagging policy

🔒 Neither authorization key is emitted, in any form but a hash. The provider does not mark them sensitive, which makes re-emitting worse than usual — the value would be copied into every consuming configuration's state and printed in the clear in any plan that shows it.


🧠 Architecture Notes

Reading this resource reads its credentials, and that is a permissions decision rather than a footnote. The provider's read path fetches the runtime and then calls a second client to list its two authorization keys — on every refresh, whether or not anything references them. So terraform plan needs listAuthKeys/action, and anyone who can plan can obtain the keys that register a node against this runtime. Where the runtime's whole purpose is a route into a network Azure cannot otherwise reach, that is the most consequential fact about the resource, and it belongs in the conversation before plan rights are granted.

The provider redacts nothing, and state is not encrypted by any marking. Both keys are plain Computed strings in the schema. This module therefore emits fingerprints and a distinctness flag instead of the values — enough to detect a rotation, useless to an attacker — but it cannot stop a caller from referencing the resource attribute directly, and nothing protects that reference. The keys are also written into state on every refresh, so the state file holds live registration credentials; sensitive = true would redact plan output and would not encrypt state, which is why the honest control is an encrypted, access-controlled backend rather than a marking.

The record is a registration, and the compute is somewhere else. Until the runtime software is installed on a machine and registered with one of these keys, the runtime exists, appears healthy in Studio, and runs nothing. Terraform cannot see whether a node is attached, how many there are, or whether they are patched — the nodes are not Azure resources. That is why description carries more weight here than on most resources: it is the only field on the record where the node inventory can live, and there is no tags argument either.

A not-found on the key listing drops the runtime from state. The read fetches the runtime successfully and then, if the key-listing call returns not-found, clears the resource ID anyway. The record disappears from state, the next plan proposes a create, and that create fails against the runtime that is still there — this resource does have an existence check. The recovery is an import, and the symptom gives no hint of the cause.

A replacement is an operations task. name and synapse_workspace_id are both force-new, and a replacement issues new authorization keys. Every registered machine stops working and has to be re-registered by hand with a key someone fetches and carries. Only description updates in place.

The two runtime types share an error message and not a name rule. This resource forbids consecutive dashes and accepts a one-character name; the Azure runtime enforces a three-character minimum and permits consecutive dashes. Each provider message is wrong about the half the other enforces. This module mirrors its own pattern and reports the gap through name_is_shorter_than_three_characters rather than inventing the missing check — a rule the provider does not enforce may be one Azure does not enforce either, and a validation {} failure blocks terraform destroy as well as apply.


🧱 Design Principles

Concern Default in this module Opt-out
The authorization keys Never emitted, in any form but a SHA-256 hash none — read them from Azure at the moment you need one
The state exposure Stated plainly: the keys are in state in plaintext, and no marking changes that —
The refresh-time permission Listed in the RBAC table as a requirement of every plan, not of an explicit key read —
The name pattern Mirrors the provider's regex, not its message. The minimum-length rule the message claims is reported, never enforced —
The missing node Reported through a constant output — Terraform cannot see one, so it will not pretend to —
The state-drop behaviour Reported, with the import command given in the examples —
tags / location Not offered — the resource has neither. Tag the workspace —

🔒 The secure-by-default rule cannot apply to the keys: they are Computed, so there is no input to make safe and no risky option to hide behind extra typing. The module compensates the three ways this suite prescribes — it never emits them, it names the exposure in the outputs and the permissions table, and it publishes the one derived bit worth reviewing (authorization_keys_are_distinct) instead of the values.


🚀 Runbook

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

Pin the module at a tag — ?ref=v1.0.0 — never a branch. Everything above is plan-only static analysis; a human applies from CI.


🧪 Testing

What terraform validate covers, offline, with no credentials:

  • Every validation {} block in this module — the name pattern and its Resource-ID lookalike check, the anchored synapse_workspace_id shape, the description non-empty check, and the timeouts duration shapes.
  • Type conversion of the timeouts object — with the caveat that undeclared keys are discarded silently rather than reported.

What only terraform plan reaches (credentials required):

  • That the workspace exists and that its ID parses.
  • That the caller's identity holds Microsoft.Synapse/workspaces/integrationRuntimes/write on it.
  • That no runtime of this name already exists — this resource does have an existence check.
  • The authorization keys themselves. A plan reads them; that is not a side effect of anything you asked for.

What neither reaches, at any stage:

  • Whether any node is installed, registered, patched or running.
  • Whether any pipeline or linked service selects this runtime. The reference is a string Terraform cannot see.
  • Whether the machine the runtime is meant to run on can actually reach the data source.

💬 Example Output

Outputs:

id                                          = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data-platform/providers/Microsoft.Synapse/workspaces/syn-platform-prod/integrationRuntimes/ir-onprem-sql"
name                                        = "ir-onprem-sql"
synapse_workspace_id                        = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data-platform/providers/Microsoft.Synapse/workspaces/syn-platform-prod"
synapse_workspace_name                      = "syn-platform-prod"
resource_group_name                         = "rg-data-platform"
description                                 = "Nodes: dc1-etl01 and dc1-etl02, owned by the on-premises platform team."
authorization_key_primary_fingerprint       = "42f1591a058c9632545904caea09ddd890ba552d17a506a0f28dba9400eb3bf1"
authorization_key_secondary_fingerprint     = "b30d62604732d1e818387a86684973343356ef877f400613d67fb5d4789c1324"
authorization_keys_are_distinct             = true
reading_this_resource_reads_its_credentials = true
name_is_shorter_than_three_characters       = false
force_new_fields                            = [
  "name",
  "synapse_workspace_id",
]

🔍 Troubleshooting

Symptom Cause Fix
The runtime shows as unavailable in Synapse Studio No node is installed and registered against it Install the runtime software on a machine and register it with an authorization key. See this_registration_runs_nothing_until_a_node_is_installed
terraform plan fails on a permissions error mentioning auth keys Every refresh lists the authorization keys, which needs listAuthKeys/action Grant it — or accept that this identity cannot plan this resource
A plan printed the authorization keys The provider marks neither key sensitive Expected, and it is the reason this module never emits them. Treat the log as containing credentials
The runtime disappeared from state and the next apply fails to create it A not-found on the key listing clears the resource ID even though the runtime was found Re-import. See a_missing_key_response_removes_this_from_state
Nodes stopped working after a rename name is force-new, and a replacement issues new authorization keys Re-register every node. Renaming is an operations task, not an edit
A key rotation cannot be confirmed The keys are never printed, by design Compare authorization_key_primary_fingerprint before and after
Both fingerprints are identical The two authorization keys are the same value, which defeats having two Regenerate one of them in Azure
A one-character name was accepted here and rejected on the Azure runtime The two resources have different regular expressions and the same error message Use at least 3 characters and single dashes — legal on both
A resource with the ID … already exists This resource has a real existence check, and a runtime of that name is already there terraform import it rather than creating
Error: Missing required argument: features The caller has no provider "azurerm" { features {} } block Add one to the root module. This module declares no provider block

🔗 Related Docs


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