Publishes a workbook template into an Azure Monitor gallery β a starting point colleagues instantiate, not a dashboard they share. Targets
hashicorp/azurerm ~> 4.0.
- βοΈ Creates one
azurerm_application_insights_workbook_template(the keystonethis) β a gallery entry undermicrosoft.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
galleriesas 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 thattemplate_datais 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.
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"]
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;
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.
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;
| 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.
| 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, nosource_idand noidentity; the workbook resource has all three and takesdata_jsonrather thantemplate_data. - π΄
gallerieshas a provider-enforced minimum of one. A template with none is accepted by the type system and rejected by the provider. - π΄
template_datais 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 intemplate_data.β οΈ priorityand a gallery'sorderare different fields, andorderis 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 misspelledtimeoutskey is still discarded silently by object conversion.β οΈ lifecycleis not valid inside amoduleblock, so a caller cannot addprevent_destroy.
| 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
Readeron a resource of the matchingresource_typesees 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_datais configuration; the concern it raises is information disclosure, not secret leakage.
- A resource group to hold the template record.
microsoft.insightsregistered in the subscription.- The workbook content, taken from the Advanced Editor's Gallery Template tab.
- At least one gallery decided: its
category, and theresource_typewhose gallery it belongs to. - The caller configures the
provider "azurerm" { features {} }block, auth, and subscription; this module declares none of these.
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
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.jsonfile on disk keeps it reviewable β and avoids Terraform interpolating a literal${...}that happens to appear inside the document.
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 |
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" }
}
}βΉοΈ
galleriescannot be omitted β the provider requires at least one, andnamedefaults to the map key, so this entry is displayed asfailures.
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_typediffers.
βΉοΈ
gallery_categoriesdeduplicates. Both entries useFailures, 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 unrecognisedtypethrough 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
Workbookthrough 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 emptyitemsarray, an unknown item type or a malformed KQL query all pass. Theitemscheck 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
orderis the documented one for position within a category.priorityis a property of the template. Where both are available, setorder.
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"
}
β οΈ Omittingnamedoes 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_unprovenis alwaystrueβ it is a standing reminder, not a finding. No plan can tell you the JSON renders correctly.
π΄
embeds_idsis the one to actually read. If the exported JSON pinned a workspace, this composition publishes that subscription's identity into a gallery.
| 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
}| 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.
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.
| 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. |
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.
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.
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.
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
| 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. |
azurerm_application_insights_workbook_templateprovider reference- Microsoft Learn β Programmatically manage workbooks
- Microsoft Learn β Azure Workbooks overview
- Sibling:
terraform-azurerm-application-insights-workbookβ a saved workbook instance, takingdata_json,display_nameandsource_id. Choose it when a team should share one view rather than each hold a copy. - Prerequisite:
terraform-azurerm-resource-groupβ suppliesresource_group_name. - Related:
terraform-azurerm-application-insightsandterraform-azurerm-log-analytics-workspaceβ the resource types whose galleries a template is published into. - This module's
SCOPE.md.
π "Infrastructure as Code should be standardized, consistent, and secure."