Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Application Insights Workbook Template Terraform Module

Publishes a workbook template into an Azure Monitor gallery β€” a starting point colleagues instantiate, not a dashboard they share. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


🧩 Overview

  • ☁️ Creates one azurerm_application_insights_workbook_template (the keystone this) β€” a gallery entry under microsoft.insights/workbooktemplates.
  • πŸ”΄ Draws the line that makes this resource easy to reach for by mistake: a template is instantiated and copied, a workbook is shared. Editing a template never changes copies already taken from it.
  • πŸ—‚οΈ Takes galleries as a keyed map β€” at least one is required, because a template with no gallery is published and invisible.
  • πŸ” Reports embedded Resource IDs in the template content, which Microsoft documents as a common side effect of exporting workbook JSON β€” surfaced as a list, not rejected.
  • ⚠️ States plainly that template_data is checked for JSON parseability and nothing more; the workbook content schema is not published in an enforceable form.

πŸ’‘ Why it matters: a template is the low-risk way to share a diagnostic view β€” republishing it cannot break a dashboard someone is relying on. The trade-off is that a fix reaches only future users, and nothing tells you who already holds a copy.


❀️ Support this project

If this module saves you time:


πŸ—ΊοΈ Where this fits in the family

flowchart TB
  rg["terraform-azurerm-resource-group"]
  mod["terraform-azurerm-application-insights-workbook-template"]
  entry["A gallery ENTRY, browsable by anyone with read access"]
  user["A colleague opens it and saves their OWN copy"]
  copy["An independent workbook, no longer linked to the template"]
  sib["terraform-azurerm-application-insights-workbook  a saved instance, takes data_json and source_id"]
  ai["Application Insights components  gallery resource_type microsoft.insights/components"]
  law["Log Analytics workspaces  gallery resource_type microsoft.operationalinsights/workspaces"]

  rg -->|"name and location"| mod
  mod -->|"publishes"| entry
  entry -->|"offered against"| ai
  entry -->|"offered against"| law
  entry -->|"instantiated by"| user
  user -->|"produces"| copy
  copy -.->|"editing the template does NOT update this"| mod

  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 entry keystone;
  class rg,user,copy,sib,ai,law neutral;
Loading

The dotted edge is the one worth studying: a copy someone has saved is not linked back to the template. That is the whole difference between this module and terraform-azurerm-application-insights-workbook, and it is a property of the service rather than of the module.


🧬 What this module builds

flowchart TB
  inputs["name, resource_group_name, location  all force-new"]
  td["template_data  the Gallery Template JSON, parsed but never schema-checked"]
  gal["galleries  a keyed MAP, at least one required"]
  meta["author, localized, priority  optional"]
  tags["tags  in-place"]

  res["azurerm_application_insights_workbook_template.this"]

  oid["id  microsoft.insights/workbooktemplates"]
  ogal["gallery_count, gallery_names, gallery_categories  derived"]
  odisc["template_data_embeds_resource_ids  plus the subscription and resource group lists"]
  onote["a_template_is_a_gallery_entry_not_a_workbook"]

  inputs --> res
  td --> res
  gal --> res
  meta --> res
  tags --> res
  res --> oid
  res --> ogal
  td -->|"scanned for embedded resource IDs"| odisc
  res --> onote

  classDef this fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef neutral fill:#F3F2F1,stroke:#8A8886,color:#201F1E;
  class res this;
  class inputs,td,gal,meta,tags,oid,ogal,odisc,onote neutral;
Loading
Resource Cardinality Purpose
azurerm_application_insights_workbook_template.this single One gallery entry, published into one or more galleries.

Nine arguments plus timeouts. The galleries block is a list in the provider's schema and is exposed here as a keyed map.


βœ… 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.
ARM type microsoft.insights/workbooktemplates, API 2019-10-17-preview

Schema notes that bite:

  • πŸ”΄ A template is not a workbook. Different ARM type, different schema, different lifecycle. This resource has no display_name, no source_id and no identity; the workbook resource has all three and takes data_json rather than template_data.
  • πŸ”΄ galleries has a provider-enforced minimum of one. A template with none is accepted by the type system and rejected by the provider.
  • πŸ”΄ template_data is an opaque string to Azure. It is not compiled or schema-checked on the way in, so a document that applies cleanly can still render as an empty workbook.
  • πŸ”΄ Exported workbook JSON commonly embeds subscription IDs and resource group names. Microsoft documents this. It makes a template non-portable and discloses more than its author usually intends.
  • ⚠️ Use the Advanced Editor's Gallery Template tab, not its ARM Template tab. The two emit different JSON, and only the first belongs in template_data.
  • ⚠️ priority and a gallery's order are different fields, and order is the documented one for position within a category. Lower is earlier in both.
  • ⚠️ Force-new: name, resource_group_name, location.
  • ⚠️ All four timeouts exist. A misspelled timeouts key is still discarded silently by object conversion.
  • ⚠️ lifecycle is not valid inside a module block, so a caller cannot add prevent_destroy.

πŸ”‘ Required Azure RBAC Roles / Permissions

Operation Role Scope
Create, update or delete the template Monitoring Contributor or Contributor the resource group
Read the template Reader the template
Browse and instantiate it from the gallery Reader the resource the gallery belongs to
Save an instantiated copy as a workbook Monitoring Contributor wherever the copy is saved

ℹ️ The audience for a published template is wider than the identity that published it. Anyone with Reader on a resource of the matching resource_type sees the gallery entry β€” which is what makes the embedded-Resource-ID question in Architecture Notes a disclosure question rather than a tidiness one.

πŸ”’ Nothing here is a credential, and nothing is marked sensitive. template_data is configuration; the concern it raises is information disclosure, not secret leakage.


Azure Prerequisites

  • A resource group to hold the template record.
  • microsoft.insights registered in the subscription.
  • The workbook content, taken from the Advanced Editor's Gallery Template tab.
  • At least one gallery decided: its category, and the resource_type whose gallery it belongs to.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription; this module declares none of these.

πŸ“ Module Structure

terraform-azurerm-application-insights-workbook-template/
β”œβ”€β”€ providers.tf     # required_version + pinned azurerm; no provider block
β”œβ”€β”€ variables.tf     # 10 inputs, 19 validations
β”œβ”€β”€ main.tf          # the keystone `this` + the content-scanning locals
β”œβ”€β”€ outputs.tf       # id first, then what the module cannot verify
β”œβ”€β”€ README.md        # this file
β”œβ”€β”€ SCOPE.md         # the cross-module contract
β”œβ”€β”€ LICENSE          # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "workbook_template" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-application-insights-workbook-template.git?ref=v1.0.0"

  name                = "wbt-latency-triage"
  resource_group_name = module.rg.name
  location            = "eastus2"

  template_data = file("${path.module}/templates/latency-triage.json")

  galleries = {
    failures = {
      category      = "Failures"
      name          = "Latency Triage"
      resource_type = "microsoft.insights/components"
      type          = "tsg"
    }
  }

  tags = { environment = "prod" }
}

πŸ’‘ file(...) rather than a heredoc. Workbook JSON is long, and keeping it as a .json file on disk keeps it reviewable β€” and avoids Terraform interpolating a literal ${...} that happens to appear inside the document.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
name string caller β€” force-new
resource_group_name string terraform-azurerm-resource-group output name β€” force-new
location string caller β€” force-new, lowercase short name
template_data string a .json file on disk, or jsonencode(...)
galleries map(object) caller β€” at least one
author / localized / priority optional caller
tags map(string) caller
timeouts object(...) caller β€” all four operations

Emits

Output Description Consumed by
id The template's Resource ID. auditing and imports β€” nothing in this library
name / resource_group_name / location All force-new. review
tags The effective tags. review
gallery_count / gallery_names / gallery_categories Derived from the map. review
template_data_embeds_resource_ids Derived, known at plan time. Reported, not rejected. security review
template_data_embedded_subscription_ids Derived list. security review
template_data_embedded_resource_group_names Derived list. security review
template_data_is_not_validated_against_the_workbook_schema Always true. pre-apply review
a_template_is_a_gallery_entry_not_a_workbook Always true. design review
editing_this_template_does_not_change_workbooks_already_created_from_it Always true. change review

πŸ“š Example Library

1 Β· Minimal call
module "workbook_template" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-application-insights-workbook-template.git?ref=v1.0.0"

  name                = "wbt-basic"
  resource_group_name = module.rg.name
  location            = "eastus2"

  template_data = jsonencode({
    version = "Notebook/1.0"
    items = [{
      type    = 1
      content = { json = "## Latency triage\n\nStart here." }
      name    = "text - 0"
    }]
    styleSettings = {}
  })

  galleries = {
    failures = { category = "Failures" }
  }
}

ℹ️ galleries cannot be omitted β€” the provider requires at least one, and name defaults to the map key, so this entry is displayed as failures.

2 Β· Content from a file, which is the maintainable form
module "workbook_template" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-application-insights-workbook-template.git?ref=v1.0.0"

  name                = "wbt-latency-triage"
  resource_group_name = module.rg.name
  location            = "eastus2"

  template_data = file("${path.module}/templates/latency-triage.json")

  galleries = {
    failures = { category = "Failures", name = "Latency Triage" }
  }
}

πŸ”΄ A heredoc is the wrong container for this JSON. Terraform interpolates ${...} inside a heredoc, and workbook content legitimately contains such sequences β€” so a document that is valid on disk becomes a parse error or, worse, silently altered when inlined. file(...) reads it verbatim.

3 Β· Publishing into several galleries at once
galleries = {
  ai_tsg = {
    category      = "Failures"
    name          = "Latency Triage"
    order         = 100
    resource_type = "microsoft.insights/components"
    type          = "tsg"
  }
  law_workbook = {
    category      = "Failures"
    name          = "Latency Triage"
    order         = 100
    resource_type = "microsoft.operationalinsights/workspaces"
    type          = "workbook"
  }
}

πŸ’‘ One template, two audiences. The same content is offered to people browsing an Application Insights component and to people browsing a Log Analytics workspace, because resource_type differs.

ℹ️ gallery_categories deduplicates. Both entries use Failures, so that output lists it once β€” the template still appears in both galleries.

4 Β· Which gallery is which
type Where it appears
workbook The default Workbooks gallery.
tsg The Troubleshooting Guides gallery.
usage The More gallery under Usage.
galleries = {
  # A diagnostic runbook belongs under Troubleshooting Guides.
  tsg = { category = "Failures", type = "tsg" }
}

⚠️ Documented as examples, not as a closed set, so this module lets an unrecognised type through and rejects only a near miss. "Workbook" with a capital W is rejected; "somethingnew" is not.

5 Β· The near-miss rule, both ways
# Rejected -- differs from a documented value only by case.
galleries = { g = { category = "Failures", type = "Workbook" } }

# Rejected -- same reason.
galleries = { g = { category = "Failures", type = "TSG" } }

# ACCEPTED -- unrecognised, so possibly a value Microsoft has added since.
galleries = { g = { category = "Failures", type = "somethingnew" } }

ℹ️ This is deliberate asymmetry. Enforcing a closed set would reject a legal value the day Microsoft adds one; accepting everything would let Workbook through to fail in the portal. Rejecting only the near miss catches the mistake people actually make.

6 Β· Embedded Resource IDs β€” the disclosure this reports
output "review_before_publishing" {
  value = {
    embeds          = module.workbook_template.template_data_embeds_resource_ids
    subscriptions   = module.workbook_template.template_data_embedded_subscription_ids
    resource_groups = module.workbook_template.template_data_embedded_resource_group_names
  }
}
review_before_publishing = {
  "embeds" = true
  "resource_groups" = ["rg-prod-obs", "rg-dev-obs"]
  "subscriptions" = [
    "11111111-1111-1111-1111-111111111111",
    "22222222-2222-2222-2222-222222222222",
  ]
}

πŸ”΄ Microsoft documents that exported workbook JSON often carries fixed resource links containing subscription IDs, resource group names and other Resource IDs. A gallery entry is visible to everyone with read access on the matching resource type, so those names reach a wider audience than the author usually pictures.

ℹ️ Reported, not rejected. Pinning a template to a known workspace is a legitimate choice, so refusing it would reject correct input. Two different subscriptions in one template β€” as above β€” usually means it was assembled from exports taken in different places, which is worth catching before publishing.

7 Β· The portable alternative β€” parameterise instead of pinning
module "workbook_template" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-application-insights-workbook-template.git?ref=v1.0.0"

  name                = "wbt-portable"
  resource_group_name = module.rg.name
  location            = "eastus2"

  # A resource-picker parameter instead of a hard-coded workspace.
  template_data = jsonencode({
    version = "Notebook/1.0"
    items = [
      { type = 9, content = { parameters = [{ name = "Workspace", type = 5 }] }, name = "parameters - 0" },
      { type = 3, content = { query = "requests | summarize count()" }, name = "query - 1" },
    ]
    styleSettings = {}
  })

  galleries = {
    failures = { category = "Failures", name = "Portable Triage" }
  }
}
template_data_embeds_resource_ids = false

βœ… A parameterised template works in every subscription and discloses nothing. Item type 9 is a parameters block and type 5 is a resource picker, so the person opening the template chooses the workspace.

8 Β· What the JSON checks catch, and what they do not
# Rejected -- not JSON at all.
template_data = "{not json"

# Rejected -- valid JSON, but the ARM Template tab's shape: no top-level `items`.
template_data = "{\"resources\":[{\"type\":\"microsoft.insights/workbooktemplates\"}]}"

# ACCEPTED -- and may still render as an empty workbook.
template_data = "{\"items\":[]}"

⚠️ The third case is the honest limit. There is no offline schema for workbook content, so an empty items array, an unknown item type or a malformed KQL query all pass. The items check is a heuristic whose own error message says so, and it exists to catch exactly one mistake: exporting from the wrong tab.

πŸ’‘ The only real proof is opening the template from the gallery after apply.

9 Β· Ordering within a category
module "workbook_template" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-application-insights-workbook-template.git?ref=v1.0.0"

  name                = "wbt-start-here"
  resource_group_name = module.rg.name
  location            = "eastus2"
  template_data       = file("${path.module}/templates/start-here.json")

  # Lower sorts earlier, so this one leads the category.
  priority = 1

  galleries = {
    failures = { category = "Failures", order = 1 }
  }
}

ℹ️ Two fields, and order is the documented one for position within a category. priority is a property of the template. Where both are available, set order.

10 Β· Naming and the map-key default
galleries = {
  # `name` supplied -- shown as "Latency Triage".
  failures = { category = "Failures", name = "Latency Triage" }

  # `name` omitted -- shown as "usage_view", which is probably not what you want.
  usage_view = { category = "Usage", type = "usage" }
}
output "check_what_colleagues_will_see" {
  value = module.workbook_template.gallery_names
}
check_what_colleagues_will_see = {
  "failures"   = "Latency Triage"
  "usage_view" = "usage_view"
}

⚠️ Omitting name does not leave the entry unnamed β€” it names it after the map key. That key is usually a short internal identifier, so this output exists to make the difference visible before anyone else sees it.

11 Β· A template is not a workbook β€” choosing between them
output "which_resource_did_i_want" {
  value = module.workbook_template.a_template_is_a_gallery_entry_not_a_workbook
}
You want Use Key inputs
A view a team opens and all see the same instance of terraform-azurerm-application-insights-workbook data_json, display_name, source_id
A starting point colleagues each take their own copy of this module template_data, galleries

πŸ”΄ Editing a template does not change copies already taken from it, and nothing in Azure reports how many copies exist or who holds them. A fix published here reaches only future users β€” announce it out of band.

βœ… The upside of the same property: republishing a template cannot break a dashboard somebody is relying on, which is the opposite of the risk a shared workbook carries.

12 Β· πŸ—οΈ End-to-end composition β€” observability with a published triage template
provider "azurerm" {
  features {}
}

variable "location" {
  type    = string
  default = "eastus2"
}

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

  name     = "rg-observability"
  location = var.location

  tags = { environment = "prod" }
}

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

  name                = "law-prod"
  resource_group_name = module.rg.name
  location            = var.location
}

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

  name                = "ai-prod"
  resource_group_name = module.rg.name
  location            = var.location
  workspace_id        = module.law.id
}

# A shared, editable dashboard -- one instance everybody opens.
module "live_dashboard" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-application-insights-workbook.git?ref=v1.0.0"

  name                = "11111111-2222-3333-4444-555555555555"
  resource_group_name = module.rg.name
  location            = var.location
  display_name        = "Production Service Health"
  data_json           = file("${path.module}/templates/service-health.json")
  source_id           = module.app_insights.id
}

# THIS MODULE: a starting point colleagues each copy. Parameterised, so it is
# portable and discloses no subscription IDs.
module "triage_template" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-application-insights-workbook-template.git?ref=v1.0.0"

  name                = "wbt-latency-triage"
  resource_group_name = module.rg.name
  location            = var.location
  template_data       = file("${path.module}/templates/latency-triage.json")

  author   = "Platform Observability"
  priority = 1

  galleries = {
    ai_tsg = {
      category      = "Failures"
      name          = "Latency Triage"
      order         = 1
      resource_type = "microsoft.insights/components"
      type          = "tsg"
    }
    law_workbook = {
      category      = "Failures"
      name          = "Latency Triage"
      order         = 1
      resource_type = "microsoft.operationalinsights/workspaces"
      type          = "workbook"
    }
  }

  tags = { environment = "prod" }
}

output "publish_review" {
  description = "Check these before anyone browses the gallery."
  value = {
    galleries       = module.triage_template.gallery_names
    embeds_ids      = module.triage_template.template_data_embeds_resource_ids
    subscriptions   = module.triage_template.template_data_embedded_subscription_ids
    content_unproven = module.triage_template.template_data_is_not_validated_against_the_workbook_schema
  }
}

πŸ’‘ The composition shows both resources on purpose, because choosing between them is the real decision. The workbook is one shared instance wired to a specific Application Insights component; the template is a gallery entry with no owning resource at all.

⚠️ content_unproven is always true β€” it is a standing reminder, not a finding. No plan can tell you the JSON renders correctly.

πŸ”΄ embeds_ids is the one to actually read. If the exported JSON pinned a workspace, this composition publishes that subscription's identity into a gallery.


πŸ“₯ Inputs

Input Type Default Notes
name string β€” Required. Force-new.
resource_group_name string β€” Required. Force-new.
location string β€” Required. Force-new. Lowercase, no spaces.
template_data string β€” Required. Must parse as JSON and carry items.
galleries map(object(...)) β€” Required, at least one. Keyed map.
author string null Visible in the gallery.
localized string null JSON; shape undocumented.
priority number null Non-negative whole number. Lower is higher priority.
tags map(string) {} In-place.
timeouts object(...) null All four operations.
Full schemas
variable "name" { type = string }
variable "resource_group_name" { type = string }
variable "location" { type = string }

# Checked for JSON parseability and a top-level `items` array. Nothing more is
# checkable -- the workbook content schema is not published.
variable "template_data" { type = string }

variable "galleries" {
  type = map(object({
    category      = string
    name          = optional(string) # defaults to the map key
    order         = optional(number)
    resource_type = optional(string)
    type          = optional(string) # workbook | tsg | usage, near-miss rejected
  }))
}

variable "author" {
  type    = string
  default = null
}

variable "localized" {
  type    = string
  default = null
}

variable "priority" {
  type    = number
  default = null
}

variable "tags" {
  type    = map(string)
  default = {}
}

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

🧾 Outputs

Output Type Notes
id string The Resource ID. Nothing in this library consumes it.
name / resource_group_name / location string All force-new.
tags map(string) In-place.
gallery_count number At least 1.
gallery_names map(string) Effective display name per key.
gallery_categories list(string) Distinct categories.
template_data_embeds_resource_ids bool Derived. Reported, not rejected.
template_data_embedded_subscription_ids list(string) Derived. Empty when none.
template_data_embedded_resource_group_names list(string) Derived. A text scan, not a parse.
template_data_is_not_validated_against_the_workbook_schema bool Always true.
a_template_is_a_gallery_entry_not_a_workbook bool Always true.
editing_this_template_does_not_change_workbooks_already_created_from_it bool Always true.

πŸ”’ Nothing is sensitive. Template content is configuration. The concern it raises is disclosure of resource names, not secrets.


🧠 Architecture Notes

A workbook template is a gallery entry, and that single fact drives everything else here. Publishing a template does not create something anyone is looking at β€” it adds an entry under a category, and a colleague who opens it receives their own independent copy to edit and save. So the ARM type is microsoft.insights/workbooktemplates rather than microsoft.insights/workbooks, there is no source_id naming an owning resource, and the template is offered against every resource of the matching resource_type instead of hanging off one of them.

The lifecycle consequence cuts both ways and neither direction is visible to Terraform. A template is copied at instantiation, not linked: changing template_data updates the gallery entry and leaves every existing copy untouched. That makes a template safe to iterate on β€” republishing cannot break a dashboard someone depends on, which is the opposite of a shared workbook's risk profile. It also means a fix reaches only future users, and Azure will not tell you how many copies exist or who holds them. If a template shipped a wrong query, correcting it here corrects nobody else's copy, and that has to be announced out of band.

template_data is checked for parseability and nothing more, and saying so precisely matters. The module confirms the string is JSON and carries a top-level items array. There is no offline schema for workbook content: Azure stores the document opaquely and does not compile it on the way in, and the item types, query syntax and parameter bindings inside it are not published in a form Terraform can check. So a template that parses, applies cleanly and renders as an empty workbook is entirely possible. The items check is a heuristic β€” its own error message says so β€” and it exists to catch one specific mistake: exporting from the Advanced Editor's ARM Template tab rather than its Gallery Template tab, which wraps the content in a resources array. It catches nothing else, and the only real proof is opening the template from the gallery after apply.

Embedded Resource IDs are reported rather than rejected, and the reasoning is a judgement about who can see the gallery. Microsoft documents that exported workbook JSON often contains fixed resource links carrying subscription IDs, resource group names and full Resource IDs. Pinning a template to a known Log Analytics workspace is sometimes deliberate, so refusing it would reject legitimate input β€” this module scans for /subscriptions/<guid> and emits both a flag and the actual lists. What makes it worth surfacing is that a gallery entry is visible to everyone with read access on the matching resource type, a wider audience than the workbook's author typically pictures, and an embedded ID also makes the template silently non-portable. This is information disclosure, not secret leakage: a Resource ID is not a credential, nothing is marked sensitive, and the question it raises is whether the template should be parameterised with a resource picker instead. Two distinct subscriptions in one document usually means it was assembled from exports taken in different places.

galleries is a keyed map where the provider has a list, and here that costs nothing. A map makes a duplicate key unrepresentable rather than merely discouraged, and adding or removing one gallery never re-indexes the rest. Normally that trades away ordering; in this case it does not, because display order is carried explicitly by each entry's order field, so the collection's own sequence never mattered. One wrinkle is worth watching: omitting a gallery's name does not leave it unnamed β€” it defaults to the map key, which is usually a short internal identifier rather than something you want a colleague to read, which is why gallery_names is emitted.

Two different fields control position. A gallery's order decides placement within a category and is the one Microsoft documents; the template's own priority is a separate property. Lower means earlier in both. Where both are available, set order.


🧱 Design Principles

Concern This module's position Why
Template vs workbook Named in a constant output and in the DAG. The two are easy to confuse and behave differently.
Copy-not-link semantics Named in a constant output. Neither direction produces a Terraform diff.
template_data validity Parseability enforced; content explicitly not. No offline schema exists; claiming otherwise would mislead.
The items check A heuristic, labelled as one in its own message. It catches the wrong-tab export and nothing else.
Embedded Resource IDs Reported as a flag plus two lists. Pinning is sometimes deliberate; rejecting it would be inventing a rule.
Malformed JSON Rejected, not normalised. A recoverable-but-malformed input should fail loudly.
galleries A keyed map, minimum one. Duplicates unrepresentable; order removes the ordering trade-off.
Gallery type Near miss rejected, unknown allowed. Documented as examples, not a closed set.
resource_type A Resource ID is rejected by name. An ID is the plausible wrong value for a type.
Secrets None. Nothing marked sensitive. Content is configuration.
tags Carried as the universal tail. The resource exposes tags β€” confirmed against the schema.

πŸš€ 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.

After the apply β€” the check no plan can make:

# Confirm the template exists and see its galleries.
az resource show --ids "<template id>" --query "properties.galleries"

Then open Azure Monitor β†’ Workbooks (or the Troubleshooting Guides gallery for tsg), find the entry under its category, and open it. That is the only way to know the content renders.


πŸ§ͺ 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 all 19 validations fire on the input each targets. None of them references another variable, so validation suppression is not a factor here and a single group would have sufficed; they were split by intent instead.

Proved with a failing value each: a Resource ID in name and in resource_group_name; both of those empty; location empty, and "East US 2" producing two errors for the space and the casing independently; template_data empty (which fires both the empty check and the JSON check), malformed, and valid-but-without-items; an empty galleries map; a gallery with a blank category, one with a blank name, one with the near-miss type = "Workbook", and one with a Resource ID in resource_type; a whitespace author; a non-JSON localized; and priority = -1.5 producing two errors for the sign and the fraction.

The near-miss rule was proved in both directions: "Workbook" rejected, "somethingnew" accepted.

Every derived value was printed rather than reasoned about, against two shapes of content. With a template embedding two subscriptions: template_data_embeds_resource_ids = true, both GUIDs and both resource group names extracted, gallery_categories correctly deduplicating Failures across two galleries, gallery_count = 3, and gallery_names showing the map key used where name was omitted. With a parameterised template: false and two empty lists.

What only an apply exercises: whether the resource group exists, and whether Azure accepts the document.

What no Terraform run exercises at all: whether the workbook renders, whether its queries return anything, and who has already taken a copy.


πŸ’¬ Example Output

Outputs:

a_template_is_a_gallery_entry_not_a_workbook = true
editing_this_template_does_not_change_workbooks_already_created_from_it = true
gallery_categories = [
  "Failures",
]
gallery_count = 2
gallery_names = {
  "ai_tsg" = "Latency Triage"
  "law_workbook" = "Latency Triage"
}
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-observability/providers/microsoft.insights/workbooktemplates/wbt-latency-triage"
location = "eastus2"
name = "wbt-latency-triage"
resource_group_name = "rg-observability"
tags = {
  "environment" = "prod"
}
template_data_embedded_resource_group_names = []
template_data_embedded_subscription_ids = []
template_data_embeds_resource_ids = false
template_data_is_not_validated_against_the_workbook_schema = true

πŸ” Troubleshooting

Symptom Cause Fix
template_data is not valid JSON Malformed, or interpolated by a heredoc. Use file(...) or jsonencode(...).
template_data parses as JSON but has no top-level items array Exported from the ARM Template tab. Re-export from the Gallery Template tab.
galleries must contain at least one entry The map is empty. Add one with a category.
a gallery's type differs from a documented value only by case "Workbook" or "TSG". Use workbook / tsg / usage, lower case.
a gallery's resource_type looks like a Resource ID An instance where a type belongs. Use microsoft.insights/components.
location contains a space / must be lower case A portal display name. Use eastus2.
The template applies but is nowhere in the gallery Wrong resource_type or type for the gallery you are browsing. Check both; a tsg entry is under Troubleshooting Guides.
The gallery entry has an odd name name was omitted, so the map key is used. Set name, or read gallery_names first.
The template opens empty or errors The content is wrong, and nothing offline can see that. Open it from the gallery; fix the JSON.
A colleague's copy still has the old bug Templates are copied, not linked. Republish and tell them; their copy is independent.
The template only works in one subscription It embeds absolute Resource IDs. Read template_data_embedded_subscription_ids; parameterise with a resource picker.
A timeouts key is ignored Object conversion discards undeclared keys. Check the spelling; all four are valid.

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."