Manages a Grafana folder and its full permission set — one
grafana_folder_permission_itemresource per principal — as one composite unit. Targetsgrafana/grafana(~> 4.0).
- 📁 Creates and manages a
grafana_folder— title, optional customuid, org scope, optional nested- folder parent, and optionalprevent_destroy_if_not_emptyguard. - 🔐 Renders zero or more
grafana_folder_permission_itembindings,for_each-keyed by a caller-chosen stable principal key — one Terraform resource per principal, never a single whole-list resource. - 🧭 Deliberately does not use
grafana_folder_permission(the whole-list alternative that re-renders the entire permission set, and removes any binding absent from that render, on every apply) — see §6 Schema notes that bite. - 🆔 Emits
uidas the primary output — the identity every sibling module placing content in this folder (terraform-grafana-dashboard,terraform-grafana-alerting-rule-group,terraform-grafana-synthetic-monitoring- check) and a subsequent nested-folder instance of this same module consume by reference.
💡 Why it matters: a folder's access boundary is a security-relevant property in a regulated environment. Managing the folder and its permission bindings as one composite unit means a caller who owns a folder's lifecycle also owns exactly who can access it — and adding or removing a single principal's binding never disturbs any other binding's state, unlike the whole-list alternative.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
flowchart TD
team["terraform-grafana-team\n(team principal)"]:::sibling
user["terraform-grafana-user\n(user principal)"]:::sibling
svcacct["terraform-grafana-service-account\n(service account principal)"]:::sibling
folder["terraform-grafana-folder"]:::this
parentfolder["terraform-grafana-folder\n(parent instance, nested folders)"]:::sibling
dashboard["terraform-grafana-dashboard"]:::sibling
rulegroup["terraform-grafana-alerting-rule-group"]:::sibling
smcheck["terraform-grafana-synthetic-monitoring-check"]:::sibling
team -- "team_id (permission principal)" --> folder
user -- "user_id (permission principal)" --> folder
svcacct -- "id (permission principal)" --> folder
parentfolder -- "uid (parent_folder_uid)" --> folder
folder -- "uid (folder_uid)" --> dashboard
folder -- "uid (folder_uid)" --> rulegroup
folder -- "uid (folder_uid)" --> smcheck
classDef this fill:#F46800,color:#FFFFFF,stroke:#F46800;
classDef sibling fill:#E8E8E8,color:#111217,stroke:#8A8A8A;
Validated via the Mermaid Chart MCP (valid: true) before embedding. terraform-grafana-team,
terraform-grafana-user, and terraform-grafana-service-account feed permission-binding principal identities
into this module; a separately-applied instance of this same module feeds parent_folder_uid for nested
folders; this module's own uid output feeds three content-layer siblings that place dashboards, alert
rule groups, and Synthetic Monitoring checks inside the folder.
flowchart LR
subgraph inputs["Inputs"]
varfolder["var.folder\n(object)"]
varperms["var.permissions\n(map(object), default {})"]
end
keystone["grafana_folder.this\n(keystone)"]:::this
bindings["grafana_folder_permission_item.bindings\n(for_each, keyed map)"]:::this
varfolder -- "title, uid, org_id, parent_folder_uid, prevent_destroy_if_not_empty" --> keystone
varperms -- "permission, role, team, user, org_id (per key)" --> bindings
keystone -- "uid (folder_uid)" --> bindings
keystone --> outuid["uid\n(primary_output)"]
keystone --> outid["id"]
keystone --> outtitle["title"]
keystone --> outurl["url"]
bindings --> outbindings["permission_bindings\n(map, per key)"]
classDef this fill:#F46800,color:#FFFFFF,stroke:#F46800;
Validated via the Mermaid Chart MCP (valid: true) before embedding.
Resource inventory:
| Resource | Cardinality | Role |
|---|---|---|
grafana_folder.this |
1 (keystone) | The folder itself: title, optional custom uid, org scope, optional parent_folder_uid (nested folders), optional prevent_destroy_if_not_empty guard |
grafana_folder_permission_item.bindings |
0..N (for_each, keyed by var.permissions map key) |
One resource per principal binding — role, team, or user, each with its own permission level |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
grafana/grafana provider |
~> 4.0 (confirmed latest at authoring time: 4.40.1, installed and locked during this session's terraform init) |
| Provider block | None — the caller configures provider "grafana" {... } (with whichever alias this module's auth mechanism requires) at the root module and passes it in via providers = { grafana =... } |
Schema notes that bite:
uidis listed as Optional in the live v4.40.1 schema, with no separate "Computed"/"Force new" annotation surfaced in the rendered docs page. This suite's accumulated Known schema gotchas record (cross-checked during prior authoring) confirms: when omitted, Grafana generates auidserver-side; once auidexists (caller-set or generated), changing it forces replacement of the folder — it is this resource's stable identity everywhere else in the API and in every sibling module'sfolder_uidconsumption. Treat as immutable-after-set.parent_folder_uidrequires thenestedFoldersfeature flag enabled on the target Grafana instance — the live schema's own field description states this explicitly. See §8 Grafana Prerequisites.prevent_destroy_if_not_emptyrequires Grafana 10.2 or later per the live schema's own field description; defaults tofalse. See §8 Grafana Prerequisites.grafana_folder.idis a String in the live schema ("The ID of this resource"), not a legacy numeric ID as earlier module planning assumed — this module'sidoutput reflects that String type verbatim and is documented as informational-only; preferuidfor all cross-module wiring.- This module uses
grafana_folder_permission_item(one resource per principal), notgrafana_folder_permission(a single resource holding the folder's entire permission set as a repeatingpermissionsblock). The provider's own docs state the whole-list resource "manages the entire set of permissions for a folder. Permissions that aren't specified when applying this resource will be removed" — it does not compose safely across independentfor_eachiterations or independent callers adding principals over time._itemavoids this: each principal binding gets its own resource identity and lifecycle, and the two resources are documented by the provider as conflicting if both target the same folder. - Each
grafana_folder_permission_itemrequiresfolder_uidandpermission, plus exactly one ofrole/team/user— the live schema only enforces this "conflicts with" constraint at apply time; this module'svariables.tfenforces it atterraform plantime instead (see §12 example 9). - Unresolved identity-type ambiguity for
team: the provider's own example usage forgrafana_folder_permission_itempassesteam = grafana_team.team.id(the team resource's ownidattribute), notteam_uid. Bothgrafana_folder_permissionandgrafana_folder_permission_itemlink to the legacy folder-permissions HTTP API in their own docs (api-legacy/folder_permissions/), which is evidence — not a schema-confirmed fact — that theteamfield expects the string form of the team's legacy numeric ID (tostring(team_id)), not the newerteam_uid. Verify with a liveplanagainst a real team before depending on this in production; see §12 example 6 and §15 Architecture Notes. grafana_folder.idandgrafana_team.id/grafana_user.idare all separately-documentedidattributes on their respective resources — none of this library's sibling modules currently expose a plainid-named output for team, so this module's README recommendstostring(team_id)/tostring(user_id)/ the service account's ownidoutput as the practical wiring path (see §11).
Grafana core auth (see this suite's Authentication model, "Grafana core auth" table) — the auth
provider argument (Service Account token). least-privilege convention for this module is an
Editor/Admin-scoped Service Account token: Editor is sufficient to create and rename the folder
itself; Admin is required if this module's permission bindings grant Admin-level access to any other
principal, since a token cannot grant a permission level higher than its own. Choose the narrower Editor
scope when this module's calling root module only ever grants Viewer/Editor bindings; step up to Admin
only when an Admin-level binding is actually rendered. Configured once by the caller on whichever aliased
grafana provider instance is passed into this module via providers = { grafana =... }; this module
declares no alias or credential of its own.
parent_folder_uidrequires thenestedFoldersfeature flag enabled on the target Grafana instance. Only relevant when the caller sets this field — treat as feature-gated, not universally available.prevent_destroy_if_not_emptyrequires Grafana 10.2 or later. Version-gate this field's use; do not assume every target instance is new enough to support it.- Folder identity (
uid) is scoped per org/stack, not global — confirm the target org/stack context (e.g. via anorg_id-scoping convention, where the calling root module wires one fromterraform-grafana-organization) before applying.
terraform-grafana-folder/
├── providers.tf # required_providers: grafana/grafana ~> 4.0, required_version >= 1.12.0. No provider {} block.
├── variables.tf # var.folder (keystone object) + var.permissions (for_each-keyed permission bindings map)
├── main.tf # grafana_folder.this (keystone) + grafana_folder_permission_item.bindings (for_each)
├── outputs.tf # uid (primary), id, title, url, permission_bindings
├── README.md # this file
├── SCOPE.md # cross-module contract, auth/scope, prerequisites, provider gotchas
└── examples/ # runnable example(s)
module "platform_ops_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Platform Ops"
}
}The caller configures the grafana provider (with the alias this module's Editor/Admin-scoped Service
Account token auth requires) once at the root module — this module never accepts its own url, auth,
or org_id default.
Consumes
| Input | Type | Source module |
|---|---|---|
team (permission principal) |
string |
terraform-grafana-team — recommend tostring(team_id); see §6 Schema notes that bite for the unresolved team_id vs. team_uid ambiguity |
user (permission principal) |
string |
terraform-grafana-user's user_id output (tostring(...), since it is a Number) |
user (permission principal, service account) |
string |
terraform-grafana-service-account's id output (already a string) |
parent_folder_uid (optional, nested folders) |
string |
terraform-grafana-folder (this module's own uid output, from a separately-applied parent instance) |
Emits
| Output | Description | Consumed by |
|---|---|---|
uid |
Primary output. Folder's stable UID | terraform-grafana-dashboard (folder_uid input), terraform-grafana-alerting-rule-group (folder_uid input), terraform-grafana-synthetic-monitoring-check (folder_uid input, where applicable), a subsequent instance of this module itself (as parent_folder_uid) |
id |
The provider's own resource id (String) | Informational only — prefer uid for all cross-module wiring |
title |
Folder display title | Informational |
url |
Full URL of the folder in the Grafana UI | Informational / linking |
permission_bindings |
Map, keyed like var.permissions, of each binding's provider-assigned id |
Informational / drift-detection only |
1 · Minimal folder
module "folder_minimal" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Observability"
}
}💡 The empty-call default: no explicit
uid(Grafana generates one), no permission bindings beyond Grafana's own default folder ACL, no nesting, no destroy guard.
2 · Folder with an explicit, caller-chosen uid
module "folder_explicit_uid" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Shared Dashboards"
uid = "shared-dashboards"
}
}
⚠️ Treatuidas immutable once applied — changing it on a later apply forces replacement of the folder (see §6 Schema notes that bite).
3 · Folder with an explicit org scope
module "folder_scoped_org" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Tenant B Reporting"
org_id = "3"
}
}ℹ️
org_idis exposed explicitly here rather than left to whatever org the aliasedgrafanaprovider instance defaults to — this suite's narrowest-explicit-scope convention.
4 · Nested folder (requires nestedFolders feature flag)
module "parent_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Engineering"
}
}
module "child_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Engineering / Platform"
parent_folder_uid = module.parent_folder.uid
}
}🔒 Requires the
nestedFoldersfeature flag enabled on the target Grafana instance — see §8 Grafana Prerequisites. Do not assume this is available on every instance.
5 · Folder with a Viewer role binding (least-privilege)
module "folder_least_privilege" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Read-Only Metrics"
}
permissions = {
"role:viewer" = {
role = "Viewer"
permission = "View"
}
}
}💡 The narrowest binding this module can render: the built-in
Viewerbasic role, granted onlyView. Prefer this shape whenever a folder just needs to be readable org-wide without individually enumerating principals.
6 · Folder with a team permission binding
module "analytics_team_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Analytics"
}
permissions = {
"team:analytics" = {
team = tostring(module.analytics_team.team_id)
permission = "Edit"
}
}
}
⚠️ Wired here astostring(module.analytics_team.team_id), notteam_uid— see §6 Schema notes that bite for the evidence behind this choice, and verify against a liveplanbefore depending on it in production.terraform-grafana-teamis the source module; see its own README forteam_id's exact type.
7 · Folder with a user permission binding
module "user_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Personal Sandbox"
}
permissions = {
"user:analyst01" = {
user = tostring(module.analyst_user.user_id)
permission = "Admin"
}
}
}ℹ️
terraform-grafana-user's primary outputuser_idis a Number —tostring(...)is required sincegrafana_folder_permission_item.useris String-typed.
8 · Folder with a service account permission binding
module "automation_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "CI/CD Provisioned Dashboards"
}
permissions = {
"svcacct:ci-bot" = {
user = module.ci_service_account.id
permission = "Edit"
}
}
}💡
terraform-grafana-service-account's primary outputidis already a string and is that module's own documented principal-reference output — no conversion needed.
9 · ⚠️ What NOT to do — multiple principal fields on one binding
module "folder_bad_binding" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Conflicting Binding"
}
permissions = {
"bad:entry" = {
role = "Viewer"
team = tostring(module.analytics_team.team_id) # fails validation — do not do this
permission = "Edit"
}
}
}
⚠️ This module'svariable "permissions"carries avalidation {}block that failsterraform planwith an actionable error when a binding sets more than one (or none) ofrole/team/user, rather than letting the live API reject it at apply time.
10 · Multiple bindings at once, mixed principal types
module "mixed_principal_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Shared Ops Dashboards"
}
permissions = {
"role:viewer" = { role = "Viewer", permission = "View" }
"team:ops" = { team = tostring(module.ops_team.team_id), permission = "Edit" }
"user:oncall" = { user = tostring(module.oncall_user.user_id), permission = "Admin" }
}
}💡 Because bindings are
for_each-keyed, adding a fourth entry to this map — or removing any one of these three — never disturbs the Terraform resource identity of the other two.
11 · Permission bindings at scale via caller-side locals
locals {
reporting_team_ids = {
finance = "12"
treasury = "18"
audit = "27"
}
}
module "reporting_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Regulatory Reporting"
}
permissions = {
for name, team_id in local.reporting_team_ids :
"team:${name}" => {
team = team_id
permission = "View"
}
}
}💡 A caller-side
forexpression over a stable map produces a stablefor_eachkey set for this module's ownpermissionsvariable — the module itself does not impose any ceiling on binding count, but very large permission sets are still oneterraform plandiff per changed principal, not per whole folder.
12 · Folder with the Grafana 10.2+ destroy guard
module "protected_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Production Dashboards"
prevent_destroy_if_not_empty = true
}
}🔒 Requires Grafana 10.2 or later on the target instance — see §8 Grafana Prerequisites. Confirm the target version before enabling; the provider will otherwise silently accept the field with no effect on older instances (behavior for unsupported versions was not independently verified this session — treat as an inference from the schema's own documented minimum-version note).
13 · Reporting-only folder with no permission bindings
module "folder_reporting_only" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Audit Trail"
}
}🔒 No bindings rendered at all — the narrowest possible folder, relying entirely on Grafana's own default folder ACL until a sibling module or a later apply adds explicit bindings.
14 · Consuming `uid` vs. `id` downstream
module "network_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Network Engineering"
}
}
output "network_folder_uid" {
value = module.network_folder.uid
}
output "network_folder_id" {
value = module.network_folder.id
}
⚠️ Always wireuid, notid, into any sibling module'sfolder_uid-shaped input —idis documented here only as an informational pass-through of the provider's own resource id.
15 · 🏗️ End-to-end composition — team + user + service account → folder → downstream content
provider "grafana" {
alias = "stack_primary"
url = "https://casey-primary.grafana.net"
auth = var.grafana_stack_service_account_token
}
module "analytics_team" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-team.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
team = {
name = "Analytics"
}
}
module "analyst_user" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-user.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
# See that module's own README for its real variable names/schema.
# email = "[email protected]" / login = "analyst01" / password = var.analyst_password
}
module "ci_service_account" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-service-account.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
# See that module's own README for its real variable names/schema.
# service_account = { name = "ci-bot", role = "Editor" }
}
module "analytics_folder" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-folder.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
folder = {
title = "Analytics"
}
permissions = {
"team:analytics" = {
team = tostring(module.analytics_team.team_id)
permission = "Edit"
}
"user:analyst01" = {
user = tostring(module.analyst_user.user_id)
permission = "View"
}
"svcacct:ci-bot" = {
user = module.ci_service_account.id
permission = "Edit"
}
}
}
module "analytics_dashboard" {
source = "git::https://github.com/microsoftexpert/terraform-grafana-dashboard.git?ref=v1.0.0"
providers = { grafana = grafana.stack_primary }
# Illustrative only -- see that module's own README for its real variable names/schema.
# folder_uid = module.analytics_folder.uid
# config_json = file("${path.module}/dashboards/analytics-overview.json")
}💡 This composition shows the full intended chain: a team, a human user, and a machine identity are each provisioned by their own owning module; this folder module grants each one a differentiated permission level on a shared folder; and a downstream content-layer module (illustrated, not asserted as verified — confirm
terraform-grafana-dashboard's own README for its exact variable names) places a dashboard inside that folder viafolder_uid = module.analytics_folder.uid.
| Variable | Type (summary) | Default | Required |
|---|---|---|---|
folder |
object — keystone fields (see below) | n/a | Yes (title required inside) |
permissions |
map(object(...)) — permission bindings |
{} |
No |
Full object schemas
variable "folder" {
type = object({
title = string
uid = optional(string)
org_id = optional(string)
parent_folder_uid = optional(string)
prevent_destroy_if_not_empty = optional(bool, false)
})
}
variable "permissions" {
type = map(object({
permission = string # "View" | "Edit" | "Admin"
role = optional(string) # "Viewer" | "Editor" | "Admin" -- mutually exclusive with team/user
team = optional(string) # mutually exclusive with role/user
user = optional(string) # mutually exclusive with role/team
org_id = optional(string) # per-binding override; falls back to var.folder.org_id
}))
default = {}
}| Output | Description | Sensitive / Conditional |
|---|---|---|
uid |
Primary output. Folder's stable UID | No |
id |
Provider's own resource id (String) | No |
title |
Pass-through of configured folder title | No |
url |
Full URL of the folder in the Grafana UI | No |
permission_bindings |
Map, keyed like var.permissions, of each binding's provider-assigned id |
No — empty map {} when var.permissions is empty |
_itemover the whole-list resource, deliberately.grafana_folder_permission_item.bindingsisfor_each-keyed overvar.permissions, giving every principal binding independent Terraform resource identity. The rejected alternative,grafana_folder_permission, renders the entire permission set as one resource and removes any binding not present in that render on every apply — it does not compose safely when different callers orfor_eachiterations add/remove principals independently over time.uidis immutable-after-set. Whether caller-supplied or Grafana-generated, changing an existinguidforces replacement of the folder — every sibling module'sfolder_uidconsumption depends on this value staying stable across applies.- Per-binding
org_idfalls back to the folder's ownorg_id.main.tfrendersorg_id = each.value.org_id != null ? each.value.org_id: try(var.folder.org_id, null)— a ternary, nottry, since a null per-bindingorg_idis a valid (non-erroring) value andtryonly falls through on evaluation errors, not onnull. This keeps a binding in the same org/stack scope as the folder it protects unless the caller explicitly overrides it. - The
team/useridentity-type question is genuinely open. The provider's own example usage forgrafana_folder_permission_itempassesgrafana_team.team.id/grafana_user.user.id, and both resources' docs reference the legacy folder-permissions HTTP API. This module's Example Library (§12 examples 6–8) wirestostring(team_id)/tostring(user_id)/ the service account's ownidoutput as the practical, evidence-based recommendation — but this was not independently confirmed with a liveplanagainst a real Grafana instance this session. Verify before depending on it in production. for_each, nevercount, for permission bindings — this suite's mandate, followed here so adding or removing one principal never re-indexes or disturbs any other binding's state.- Cloud vs. OSS applicability:
grafana_folderandgrafana_folder_permission_itemboth apply to Cloud-hosted and self-managed OSS/Enterprise Grafana instances alike;parent_folder_uid(nested folders) andprevent_destroy_if_not_emptyare each separately feature/version-gated regardless of Cloud-vs-OSS (see §8 Grafana Prerequisites).
| Concern | Safe default (this module) | Opt-out |
|---|---|---|
| Folder/dashboard visibility | No public sharing rendered by this module at all — folder visibility is private to the org by default per Grafana's own platform behavior | N/A — public sharing is a separate resource (grafana_dashboard_public) this module never renders |
| Team/role permissions | Empty permissions map ({}) — no explicit non-owner bindings rendered unless the caller supplies them; every binding's permission level must be stated explicitly, none defaults to Admin |
Caller supplies var.permissions entries with explicit permission values |
| Nested folders | Disabled by default — parent_folder_uid defaults to null (root-level folder) |
Caller sets var.folder.parent_folder_uid, and the target instance must have nestedFolders enabled |
| Destroy guard | Disabled by default — prevent_destroy_if_not_empty defaults to false, matching the provider's own documented default |
Caller sets var.folder.prevent_destroy_if_not_empty = true (Grafana 10.2+ only) |
| Org/stack scope | org_id defaults to null (provider's own default org) but is always an explicit, visible field on var.folder rather than an implicit dependency on the aliased provider's own defaults; per-binding org_id falls back to the folder's own scope, never a different implicit default |
Caller sets var.folder.org_id and/or a per-binding org_id override |
| Credentials | None accepted or produced — this module has no credential-bearing inputs or outputs | N/A |
cd C:\GitHubCode\newgrafanamodules\terraform-grafana-folder
terraform init -backend=false
terraform validate
terraform fmt -checkPin consumers at ?ref=v1.0.0, never a branch. This module is plan-only in this authoring pipeline —
no apply is ever run here; a human applies from CI against a real Grafana target.
terraform validate + terraform fmt -check together confirm: every field in var.folder and
var.permissions type-checks against the schemas declared in variables.tf; the three validation {}
blocks on permissions (permission enum, role enum, exactly-one-of role/team/user) are themselves
syntactically valid; and the whole file set is canonically formatted. These checks run entirely offline —
no GRAFANA_URL or Service Account token is required.
What this harness cannot catch: whether a uid value collides with an existing folder on a real
instance, whether nestedFolders or Grafana 10.2+ are actually available on the target instance (both are
platform-side checks, not Terraform-side ones), or whether team/user bindings actually expect the
legacy numeric ID vs. team_uid as this README's Architecture Notes flags as unresolved — those surface
only at plan/apply against a live Grafana instance.
$ terraform output
id = "3"
permission_bindings = {
"team:analytics" = "12:team:12"
"user:analyst01" = "12:user:47"
}
title = "Analytics"
uid = "adfhmxue45gnkb"
url = "/dashboards/f/adfhmxue45gnkb/analytics"
| Symptom | Cause | Fix |
|---|---|---|
plan fails: "Each permissions entry must set EXACTLY ONE of role, team, or user" |
A binding set two (or zero) of role/team/user |
Set exactly one mutually exclusive principal field per binding |
plan fails: "permissions[*].permission must be one of..." |
An invalid/mistyped permission value |
Use exactly "View", "Edit", or "Admin" |
plan fails: "permissions[*].role, when set, must be one of..." |
An invalid/mistyped role value |
Use exactly "Viewer", "Editor", or "Admin" |
apply fails referencing parent_folder_uid with a feature-flag error |
nestedFolders is not enabled on the target Grafana instance |
Enable the feature flag, or omit parent_folder_uid |
apply fails or silently no-ops referencing prevent_destroy_if_not_empty |
Target Grafana instance predates 10.2 | Upgrade the instance, or omit the field |
Changing folder.uid triggers an unexpected destroy/recreate |
uid is immutable-after-set; changing it forces replacement |
Treat uid as write-once; provision a new folder instead of renaming an existing uid |
A team/user permission binding is rejected by the live API despite passing plan |
The team/user field may expect the legacy numeric ID rather than team_uid, or vice versa — this was not independently confirmed this session |
Try tostring(team_id) / tostring(user_id) first (see §6, §15); confirm with a live plan against the target instance |
terraform init can't find provider version ~> 4.0 |
Local provider cache is stale or offline | Re-run terraform init -backend=false -upgrade, or confirm network access to the Terraform Registry |
- Provider resource docs:
grafana_folder,grafana_folder_permission_item,grafana_folder_permission(grafana/grafanaprovider, Terraform Registry) - This module's
SCOPE.md— cross-module contract, required auth/scope, Grafana prerequisites, provider gotchas, design decisions - Sibling modules referenced above:
terraform-grafana-team,terraform-grafana-user,terraform-grafana-service-account,terraform-grafana-dashboard,terraform-grafana-alerting-rule-group,terraform-grafana-synthetic-monitoring-check(dashboard/alerting-rule-group/synthetic-monitoring-check not yet authored as of this session — treat their example call shapes above as illustrative)