Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Azure API Management Workspace Terraform Module

Provisions an API Management workspace — a team-scoped boundary — with its version sets, certificates, named values, policies, and policy fragments. Secrets out of band. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Version Type Resources


🧩 Overview

  • 🗂️ The workspace (azurerm_api_management_workspace) — a team-scoped area within an API Management service.
  • 🔢 Version sets, 🔐 certificates, 🔑 named values, 📜 policies, and 🧩 policy fragments — keyed maps.
  • 🕵️ Certificate and named-value secrets are wrapped sensitive() and provisioned out of band.

💡 Why it matters: Workspaces let teams manage their own APIs, policies, and secrets inside a shared API Management service. This module keeps each team's certificate and named-value secrets sensitive by default and lets you source them from Key Vault, so shared-service multi-tenancy doesn't leak credentials.

❤️ 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 LR
  apim["terraform-azurerm-<br/>api-management"]
  ws["terraform-azurerm-<br/>api-management-workspace"]
  ch["version-set / certificate / named-value / policy / fragment"]
  apim -->|"id (api_management_id)"| ws
  ws -->|"id"| ch
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b8c4d0,color:#1b1b1b;
  class ws me;
  class apim target;
  class ch sib;
Loading

🧬 What this module builds

flowchart TD
  core["name · api_management_id · display_name"]
  this["azurerm_api_management_workspace.this"]
  v["api_version_set (for_each)"]
  c["certificate (for_each)"]
  n["named_value (for_each)"]
  p["policy (for_each)"]
  f["policy_fragment (for_each)"]
  core --> this
  this --> v
  this --> c
  this --> n
  this --> p
  this --> f
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef child fill:#eef3f8,stroke:#b8c4d0,color:#1b1b1b;
  class this me;
  class v,c,n,p,f child;
Loading

Resource inventory

Resource Count Role
azurerm_api_management_workspace.this 1 The workspace (keystone).
azurerm_api_management_workspace_api_version_set.version_set 0..N Version sets (for_each).
azurerm_api_management_workspace_certificate.certificate 0..N Certificates (for_each).
azurerm_api_management_workspace_named_value.named_value 0..N Named values (for_each).
azurerm_api_management_workspace_policy.policy 0..N Workspace policies (for_each).
azurerm_api_management_workspace_policy_fragment.policy_fragment 0..N Policy fragments (for_each).

✅ Provider / Versions

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

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

  • 🔒 name and api_management_id are effectively immutable.
  • 🕵️ Certificate / named-value secrets are wrapped sensitive() (not variable-level sensitive, since for_each keys can't be sensitive). Provision them out of band.
  • 🔑 A named value takes an inline value OR a value_from_key_vault reference — not both.
  • 🔢 versioning_scheme is Header / Query / Segment, validated at plan.
  • 🚫 No Azure resource tags on the workspace — the tail is timeouts only.
  • 🔐 The two Key Vault URI fields are NOT validated the same way, on the same provider line. certificates[*].key_vault_secret_id sits behind a version gate: the pinned 4.x path accepts any Key Vault nested item type (/secrets/, /keys/, /certificates/, /storage/) and only the 5.0 path narrows it to secrets — so a /certificates/ URI applies today and breaks on that upgrade. named_values[*].value_from_key_vault.secret_id has no gate and is checked as a secret right now. Both take a data-plane URI, never an ARM Resource ID.
  • 🔄 Version segment is optional on both, and the choice matters. A versionless URI lets a rotated secret or renewed certificate reach the workspace with no Terraform change; a versioned URI pins it. certificate_key_vault_ids_pinned_to_a_version reports which entries are pinned.

🔑 Required Azure RBAC Roles / Permissions

  • Contributor on the API Management service's resource group (or a least-privilege role with write on Microsoft.ApiManagement/service/workspaces/*).

Azure Prerequisites

  • An existing API Management service on a workspace-capable SKU (the api-management module).
  • Key Vault secrets for any Key-Vault-referenced certificate / named value.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription.

📁 Module Structure

terraform-azurerm-api-management-workspace/
├── providers.tf · variables.tf · main.tf · outputs.tf
├── README.md · SCOPE.md · LICENSE · .gitignore

⚙️ Quick Start

provider "azurerm" {
  features {}
}

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

  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
}

🔌 Cross-Module Contract

Consumes

Input Type Source module
api_management_id string terraform-azurerm-api-management (its id)
Key Vault secret IDs (certs / named values) string Key Vault

Emits

Output Description
id Workspace Resource ID (first).
name Workspace name.
version_set_ids / certificate_ids / named_value_ids / policy_ids / policy_fragment_ids Maps of child key → ID.

📚 Example Library

1 · Minimal workspace
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
}
2 · With a description
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  description       = "APIs and policies owned by the Orders team."
}
3 · Version set
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  version_sets = {
    orders = { display_name = "Orders versions", versioning_scheme = "Segment" }
  }
}
4 · Header-based version set
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  version_sets = {
    orders = { display_name = "Orders versions", versioning_scheme = "Header", version_header_name = "api-version" }
  }
}
5 · Certificate from Key Vault
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  certificates = {
    gateway = { key_vault_secret_id = var.gateway_cert_kv_secret_id }
  }
}

🔒 Prefer a Key Vault reference over an inline PFX; either way the secret is wrapped sensitive.

6 · Inline PFX certificate
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  certificates = {
    gateway = { certificate_data_base64 = var.pfx_base64, password = var.pfx_password }
  }
}
7 · Named value (plain)
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  named_values = {
    backend-url = { display_name = "backend-url", value = "https://orders.internal" }
  }
}
8 · Named value (secret from Key Vault)
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  named_values = {
    api-key = {
      display_name         = "api-key"
      secret               = true
      value_from_key_vault = { secret_id = var.api_key_kv_secret_id }
    }
  }
}

🔒 Set secret = true and reference Key Vault for sensitive named values.

9 · Workspace policy
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  policies = {
    main = { xml_content = "<policies><inbound><base /></inbound></policies>" }
  }
}
10 · Policy fragment
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  policy_fragments = {
    cors = { xml_content = "<fragment><cors><allowed-origins><origin>*</origin></allowed-origins></cors></fragment>", description = "CORS" }
  }
}
11 · Custom timeouts
module "ws" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"
  timeouts          = { create = "30m" }
}
12 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

module "apim" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management.git?ref=v1.0.0"
  name                = "apim-contoso-eastus"
  resource_group_name = "rg-apim-eastus"
  location            = "eastus"
  publisher_name      = "Contoso"
  publisher_email     = "api-platform@contoso.com"
  sku_name            = "Premium_1"
}

module "team_workspace" {
  source            = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-workspace.git?ref=v1.0.0"
  name              = "orders-team"
  api_management_id = module.apim.id
  display_name      = "Orders Team"

  version_sets = { orders = { display_name = "Orders versions", versioning_scheme = "Segment" } }
  named_values = { backend-url = { display_name = "backend-url", value = "https://orders.internal" } }
  policies     = { main = { xml_content = "<policies><inbound><base /></inbound></policies>" } }
}

💡 The workspace consumes the APIM service id, so Terraform creates the service first — no depends_on. Workspaces require a workspace-capable SKU (e.g. Premium).

📥 Inputs

Required: name, api_management_id, display_name. Optional: description, version_sets/certificates/named_values/policies/policy_fragments (maps), timeouts.

Full input schemas
variable "certificates" {
  type = map(object({
    certificate_data_base64          = optional(string) # wrapped sensitive
    password                         = optional(string) # wrapped sensitive
    key_vault_secret_id              = optional(string)
    user_assigned_identity_client_id = optional(string)
  }))
  default = {}
}

variable "named_values" {
  type = map(object({
    display_name         = string
    value                = optional(string) # wrapped sensitive
    secret               = optional(bool)
    tags                 = optional(list(string))
    value_from_key_vault = optional(object({ secret_id = string, identity_client_id = optional(string) }))
  }))
  default = {}
}
# version_sets, policies, policy_fragments follow the keyed-map pattern; see variables.tf.

🧾 Outputs

Output Description Kind
id Resource ID of the API Management Workspace (emitted first) Passthrough
name Name of the workspace Passthrough
display_name Human-readable name shown in the portal Passthrough
api_management_id Resource ID of the parent API Management service Passthrough
version_set_ids Map of version_sets key to the created API version set's Resource ID Derived
certificate_ids Map of certificates key to the created certificate's Resource ID Derived
named_value_ids Map of named_values key to the created named value's Resource ID Derived
policy_ids Map of policies key to the created policy's Resource ID Derived
policy_fragment_ids Map of policy_fragments key to the created fragment's Resource ID Derived
certificate_thumbprints Map of certificates key to the certificate's thumbprint, as computed by Azure Derived
certificate_expirations Map of certificates key to the certificate's expiration date, as computed by Azure Derived
child_counts How many of each child this workspace owns, as a map Derived
version_sets_missing_their_scheme_name Keys of version_sets entries whose versioning_scheme is Header or Query but which supply no matching version_header_name or version_query_name Passthrough
named_values_stored_in_state_as_plaintext Keys of named_values entries supplied with an inline value rather than a Key Vault reference Derived
certificates_supplied_inline Keys of certificates entries supplied as an inline PFX rather than a Key Vault reference Derived
requires_premium_tier_service Constant true Constant
policy_is_a_singleton_per_workspace Constant true Constant
child_names_use_five_different_patterns Constant true, and the trap most likely to cost time here Constant
certificate_key_vault_ids_that_are_not_secret_uris Certificates keys whose Key Vault URI is not /secrets/ — accepted on the pinned line, an error after the 5.0 upgrade Derived
certificate_key_vault_ids_pinned_to_a_version Certificates keys whose Key Vault URI carries a version segment, so vault rotation will not reach them Derived
the_two_key_vault_uri_fields_are_validated_differently_by_the_provider Constant true — the gated certificate field versus the ungated named-value field Constant

🧠 Architecture Notes

  • name and api_management_id are the workspace's fixed identity.
  • Secret handling: certificate certificate_data_base64/password and named-value value are wrapped sensitive() (they can't be variable-level sensitive because for_each keys must not be sensitive); nothing secret is emitted.
  • Named value source is exclusive: inline value or value_from_key_vault, not both.
  • for_each keys are stable for every child; all children reference the keystone id.
  • No tags on the workspace — the tail is timeouts only.
  • features {} dependence — configured by the caller. Workspaces require a workspace-capable SKU.

🧱 Design Principles

Concern Secure default Opt-out
Certificate / named-value secrets sensitive, out of band, never emitted —
Key Vault sourcing preferred for secrets inline value (still sensitive)
Input safety versioning_scheme validated at plan —

Workspace secrets are sensitive by default and best sourced from Key Vault.

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin by immutable tag (?ref=v1.0.0). Plan-only; a human applies from CI.

🧪 Testing

init -backend=false / validate / fmt -check confirm the config, object() schemas, and the versioning_scheme validation type-check. Only plan/apply against a live service exercises the workspace-capable SKU requirement, parent APIM existence, and Key Vault access.

💬 Example Output

$ terraform output
id   = "/subscriptions/0000.../service/apim-contoso/workspaces/orders-team"
name = "orders-team"
named_value_ids = { "backend-url" = "/subscriptions/0000.../workspaces/orders-team/namedValues/backend-url" }

🔍 Troubleshooting

Symptom Cause Fix
Workspace creation fails The APIM SKU doesn't support workspaces. Use a workspace-capable SKU (e.g. Premium).
Each version_sets entry's versioning_scheme must be exactly one of: Header, Query, Segment Invalid scheme, or the right value in the wrong case — the comparison is case-sensitive. Use Header, Query, or Segment, spelled exactly.
A named_values entry using value_from_key_vault must also set secret = true A Key Vault–backed named value without secret = true. The provider enforces this in its create function, so without the module's check it plans cleanly and fails at apply. Add secret = true to that entry.
Each certificates entry must set EXACTLY ONE of certificate_data_base64 or key_vault_secret_id Both sources supplied, or neither. Pick one. Prefer the Key Vault reference — an inline PFX and its password are stored in Terraform state in plaintext.
Named value apply fails Both value and value_from_key_vault set. Provide exactly one source.
Terraform wants to replace the workspace name or api_management_id changed. Revert or treat as a new workspace.
provider not initialized / features error Caller missing features {}. Add the features {} block.

🔗 Related Docs


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