Skip to content

microsoftexpert/tf-mod-fabric-folder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📊 Microsoft Fabric Folder Terraform Module

Manages the lifecycle of a single Fabric folder (fabric_folder) nested under a workspace or another folder, targeting the microsoft/fabric provider ~> 1.12.0.

Terraform Provider Module Version Module Type Resources Posture Preview


🧩 Overview

  • Creates and manages exactly one Fabric folder (fabric_folder.this) nested under a workspace, or under another folder via parent_folder_id.
  • Defaults to workspace-root placement (parent_folder_id = null) — the secure/least-surprise default.
  • Changing parent_folder_id on an existing folder moves it in place rather than replacing it.
  • Emits the folder id that downstream item modules (lakehouse, notebook, warehouse, variable-library, etc.) consume as folder_id, or that another fabric_folder instance consumes as parent_folder_id to build nested hierarchies.

💡 Why it matters: since this provider has no tags argument anywhere on fabric_folder, folder hierarchy is the primary governance grouping mechanism this library has for Fabric items — a raw/ + curated/ split under a medallion-architecture workspace is a real access-boundary and discovery aid, not just cosmetic organization.

⚠️ fabric_folder is a PREVIEW resource in the live v1.12.0 schema (confirmed via the terraform-registry MCP, the live provider schema, provider_doc_id 12766471). The caller's root provider "fabric" { preview = true } must be set, or every apply of this module fails/no-ops against a provider that silently doesn't expose this resource. This module never sets preview itself — provider-level toggles are strictly the caller's concern per this module suite's code-standard convention.


❤️ Support this project

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

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


🗺️ Where this fits

flowchart TB
 Workspace["tf-mod-fabric-workspace"]:::keystone
 Folder["tf-mod-fabric-folder\n(this module)"]:::thisModule
 Lakehouse["tf-mod-fabric-lakehouse"]:::sibling
 Notebook["tf-mod-fabric-notebook"]:::sibling
 Warehouse["tf-mod-fabric-warehouse"]:::sibling
 VarLib["tf-mod-fabric-variable-library"]:::sibling

 Workspace -->|"workspace_id"| Folder
 Folder -.->|"parent_folder_id (nested subfolder)"| Folder
 Folder -->|"folder_id"| Lakehouse
 Folder -->|"folder_id"| Notebook
 Folder -->|"folder_id"| Warehouse
 Folder -->|"folder_id"| VarLib

 classDef thisModule fill:#0F6CBD,color:#FFFFFF,stroke:#0F6CBD
 classDef keystone fill:#143551,color:#FFFFFF,stroke:#143551
 classDef sibling fill:#E8EAED,color:#1A1A1A,stroke:#B0B7BF
Loading

This module requires workspace_id from tf-mod-fabric-workspace and can reference another instance of itself via parent_folder_id to build nested folder hierarchies (up to the provider's 10-level nesting limit). Item-type modules that accept an optional folder_id consume this module's id output.


🧬 What this builds

flowchart TB
 subgraph Inputs["Inputs"]
 WorkspaceId["workspace_id"]
 DisplayName["display_name"]
 ParentFolderId["parent_folder_id"]
 Timeouts["timeouts"]
 end

 Keystone["fabric_folder.this"]:::thisModule

 WorkspaceId --> Keystone
 DisplayName --> Keystone
 ParentFolderId --> Keystone
 Timeouts --> Keystone

 Keystone -->|"id"| OutId["id output"]
 Keystone -->|"echoed"| OutWs["workspace_id output"]
 Keystone -->|"echoed"| OutParent["parent_folder_id output"]

 classDef thisModule fill:#0F6CBD,color:#FFFFFF,stroke:#0F6CBD
Loading

Resource inventory: 1 resource — fabric_folder.this (single instance, the sole keystone).


✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
Provider microsoft/fabric ~> 1.12.0
Provider configuration None in this module — the caller configures provider "fabric" {} (auth, tenant, preview = true) at the root

Schema notes that bite (confirmed against the live v1.12.0 schema via the terraform-registry MCP, the live provider schema, provider_doc_id 12766471):

  • The fabric_folder resource itself is in preview, not just a recursive-listing data source. Every apply requires the caller's root provider block to set preview = true; a caller running under SPN-only auth without that flag will see this resource fail or silently no-op, not a clean permission error. <VERIFY: reconfirm GA/preview status against the live schema before every authoring session — the provider ships roughly every two weeks and this resource could have reached GA since this pass>.
  • Folder names must meet Fabric's folder name requirements: no leading/trailing spaces, no C0/C1 control codes, excludes the characters ~"#.&*:<>?/{|}, must not be one of the reserved names $recycle.bin, recycled, or recycler (case-insensitive), and must be unique among sibling folders under the same parent (that uniqueness constraint is enforced by the Fabric API at apply time, not by this module's variable validation).
  • parent_folder_id is not ForceNew — changing it on an existing folder moves the folder to a different parent in place. This is explicitly demonstrated as a supported update in the provider's own Example Usage, confirmed directly from main.tf/the live schema captured during authoring — do not treat it as immutable.
  • workspace_id is ForceNew — changing it replaces the folder.
  • timeouts is a Terraform Plugin Framework single nested Attributes object (consistent with the pattern confirmed on fabric_workspace), assigned directly with = in main.tf, never a dynamic "timeouts" block.
  • The provider enforces a maximum folder-nesting depth (10 levels) at apply time — this module has no variable-level guard against exceeding it.

🔑 Required Fabric / Entra Permissions

(sourced from this module's lightweight SCOPE.md — do not let this drift independently)

  • Grounded via Microsoft Learn ("Create folders in workspaces" → Permission model): folders inherit workspace permissions. Admin, Member, and Contributor workspace roles can create, modify, and delete folders; Viewer can only view the folder hierarchy — Viewer cannot call this module's apply successfully.
  • Entra: the calling Service Principal (or Managed Identity) must be included in the tenant's allowed security group under the "Service principals can call Fabric public APIs" Developer setting (see Microsoft Fabric Prerequisites below) — this is universal across every Fabric module in this library.

Microsoft Fabric Prerequisites

  • Preview mode must be enabled on the caller's root provider "fabric" { preview = true } block — this is a provider-level, caller-only concern this module can never set for the caller.
  • The tenant-level "Service principals can call Fabric public APIs" Developer setting must be enabled and scoped to include this module's caller SPN/MSI — a Fabric Admin Portal setting entirely outside Terraform's control, flipped once per tenant, before any non-interactive apply will succeed.
  • No Fabric trial/license prerequisite beyond the workspace itself already existing and being reachable by the calling principal at the role level described above.

📁 Module Structure

tf-mod-fabric-folder/
├── providers.tf # required_providers (fabric ~> 1.12.0), no provider {} block
├── variables.tf # display_name, workspace_id, parent_folder_id, timeouts
├── main.tf # fabric_folder.this — the sole keystone
├── outputs.tf # id, display_name, workspace_id, parent_folder_id
├── README.md # this file
├── SCOPE.md # lightweight cross-module contract (standalone)
└── examples/
 ├── basic/ # workspace-root folder (parent_folder_id = null)
 └── complete/ # medallion raw/ + curated/ split with a nested bronze/ subfolder

⚙️ Quick Start

# Caller's root module configures the provider — never this module. `preview = true` is REQUIRED for
# fabric_folder or every apply fails/no-ops.
provider "fabric" {
  preview       = true
  tenant_id     = var.tenant_id
  client_id     = var.client_id
  client_secret = var.client_secret # sourced from Key Vault / pipeline secret, never literal
}

module "folder_raw" {
  source = "git::https://github.com/microsoftexpert/tf-mod-fabric-folder.git?ref=v1.0.0"

  workspace_id = module.workspace.id # tf-mod-fabric-workspace's `id` output
  display_name = "raw"
}

🔌 Cross-Module Contract

Consumes

Input Type Source module
workspace_id string tf-mod-fabric-workspace's id output
parent_folder_id string (optional) A sibling fabric_folder instance's id output (this same module, invoked again)

Emits

Output Description Consumed by
id Folder GUID Downstream item modules' folder_id input; another fabric_folder instance's parent_folder_id
display_name Folder display name, echoed Informational / cross-referencing
workspace_id Workspace GUID, echoed Downstream chaining
parent_folder_id Parent folder GUID, echoed (null at workspace root) Informational

📚 Example Library

⚠️ Every example below assumes the caller's root provider "fabric" { preview = true } is already set — fabric_folder is a preview-only resource and every apply fails/no-ops without it.

1 · Workspace-root folder

The secure default shape — parent_folder_id omitted, so the folder lands at the workspace root.

module "folder_raw" {
  source = "../../"

  workspace_id = module.workspace.id
  display_name = "raw"
}
2 · One level of nesting
module "folder_raw" {
  source = "../../"

  workspace_id = module.workspace.id
  display_name = "raw"
}

module "folder_raw_bronze" {
  source = "../../"

  workspace_id     = module.workspace.id
  display_name     = "bronze"
  parent_folder_id = module.folder_raw.id
}
3 · Full medallion raw/ + curated/ split

💡 Folder hierarchy is the primary governance grouping mechanism here — this provider has no tags argument on fabric_folder.

module "folder_raw" {
  source = "../../"

  workspace_id = module.workspace.id
  display_name = "raw"
}

module "folder_curated" {
  source = "../../"

  workspace_id = module.workspace.id
  display_name = "curated"
}
4 · Nested subfolders under both raw/ and curated/
module "folder_raw_bronze" {
  source = "../../"

  workspace_id     = module.workspace.id
  display_name     = "bronze"
  parent_folder_id = module.folder_raw.id
}

module "folder_curated_gold" {
  source = "../../"

  workspace_id     = module.workspace.id
  display_name     = "gold"
  parent_folder_id = module.folder_curated.id
}
5 · Moving a folder by changing parent_folder_id

ℹ️ This is an in-place move, not a ForceNew replacement — confirmed against the live schema's Example Usage. The folder's id stays stable across the move.

# Before: nested under "raw"
module "folder_staging" {
  source = "../../"

  workspace_id     = module.workspace.id
  display_name     = "staging"
  parent_folder_id = module.folder_raw.id
}

# After: re-point parent_folder_id to "curated" — Terraform moves the folder in place, no replacement.
# module "folder_staging" {
# source = "../../"
#
# workspace_id = module.workspace.id
# display_name = "staging"
# parent_folder_id = module.folder_curated.id
# }
6 · Environment-qualified folder naming
module "folder_raw_dev" {
  source = "../../"

  workspace_id = module.workspace_dev.id
  display_name = "raw-dev"
}
7 · Naming edge case — maximum length (255 chars)
module "folder_long_name" {
  source = "../../"

  workspace_id = module.workspace.id
  display_name = "curated-finance-quarterly-close-reporting-extract-staging-area-for-downstream-consumption-by-the-analytics-team-and-external-auditors-pending-final-sign-off-from-the-controller-group-before-publication-to-the-executive-dashboard-suite-v2"
}
8 · Naming edge case — reserved-name avoidance

🔒 display_name values of $recycle.bin, recycled, or recycler (case-insensitive) are rejected at terraform validate time by this module's own validation {} block — before any call ever reaches the Fabric API.

module "folder_archive" {
  source = "../../"

  workspace_id = module.workspace.id
  display_name = "archive" # NOT "recycled" — that value fails validation intentionally
}
9 · Custom timeouts
module "folder_raw" {
  source = "../../"

  workspace_id = module.workspace.id
  display_name = "raw"

  timeouts = {
    create = "10m"
  }
}
10 · Multiple sibling folders via caller-side for_each
locals {
  domain_folders = ["raw", "curated", "sandbox"]
}

module "domain_folders" {
  source   = "../../"
  for_each = toset(local.domain_folders)

  workspace_id = module.workspace.id
  display_name = each.value
}
11 · Three-level nesting (raw / bronze / vendor-a)
module "folder_raw" {
  source       = "../../"
  workspace_id = module.workspace.id
  display_name = "raw"
}

module "folder_raw_bronze" {
  source           = "../../"
  workspace_id     = module.workspace.id
  display_name     = "bronze"
  parent_folder_id = module.folder_raw.id
}

module "folder_raw_bronze_vendor_a" {
  source           = "../../"
  workspace_id     = module.workspace.id
  display_name     = "vendor-a"
  parent_folder_id = module.folder_raw_bronze.id
}
12 · Sandbox folder isolated from governed hierarchy
module "folder_sandbox" {
  source = "../../"

  workspace_id = module.workspace.id
  display_name = "sandbox-experiments"
  # parent_folder_id intentionally omitted — kept flat at workspace root, distinct from the governed
  # raw/curated hierarchy.
}
13 · Per-team subfolder under a shared curated/ folder
module "folder_curated_finance" {
  source           = "../../"
  workspace_id     = module.workspace.id
  display_name     = "finance"
  parent_folder_id = module.folder_curated.id
}

module "folder_curated_claims" {
  source           = "../../"
  workspace_id     = module.workspace.id
  display_name     = "claims"
  parent_folder_id = module.folder_curated.id
}
14 · Renaming a folder in place

ℹ️ display_name is not ForceNew per the live schema — changing it renames the folder without replacement, subject to the same name-uniqueness-among-siblings constraint enforced by the Fabric API.

module "folder_raw" {
  source       = "../../"
  workspace_id = module.workspace.id
  display_name = "raw-v2" # renamed from "raw"
}
15 · 🏗️ End-to-end composition

Wires tf-mod-fabric-workspace into this module's workspace_id, builds a medallion raw/ folder, and feeds this module's id into tf-mod-fabric-lakehouse's folder_id.

provider "fabric" {
  preview       = true
  tenant_id     = var.tenant_id
  client_id     = var.client_id
  client_secret = var.client_secret
}

module "workspace" {
  source = "git::https://github.com/microsoftexpert/tf-mod-fabric-workspace.git?ref=v1.0.0"

  display_name = "ws-core-bronze-prod"
  description  = "Bronze-layer medallion workspace, core domain — prod."
}

module "folder_raw" {
  source = "git::https://github.com/microsoftexpert/tf-mod-fabric-folder.git?ref=v1.0.0"

  workspace_id = module.workspace.id
  display_name = "raw"
}

module "lakehouse_raw" {
  source = "git::https://github.com/microsoftexpert/tf-mod-fabric-lakehouse.git?ref=v1.0.0"

  workspace_id = module.workspace.id
  folder_id    = module.folder_raw.id
  display_name = "lh-claims-raw-prod"
}

📥 Inputs

Variable Type Default Notes
display_name string — (required) Max 255 chars; excluded characters and reserved names enforced via validation {}
workspace_id string — (required) ForceNew; ties this folder to a workspace
parent_folder_id string null Not ForceNew — changing it moves the folder
timeouts object({...}) null Applies to fabric_folder.this
Full object schema
variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

🧾 Outputs

Output Description Sensitive
id Folder GUID
display_name Folder display name, echoed
workspace_id Workspace GUID, echoed
parent_folder_id Parent folder GUID, echoed (null at workspace root)

🧠 Architecture Notes

  • fabric_folder is gated entirely behind the caller's root provider preview = true toggle — this module has no way to detect or enforce that at plan time; a caller who forgets it will see either an apply-time failure or a silent no-op, not a clean, actionable error.
  • parent_folder_id changes are in-place moves, not replacements — this is a deliberate divergence from how many other hierarchical resources behave, confirmed directly against the live provider schema's own Example Usage rather than assumed from convention.
  • workspace_id is ForceNew — moving a folder to a different workspace is not supported; it must be destroyed and recreated under the new workspace.
  • The provider enforces a maximum folder-nesting depth of 10 levels and folder-name uniqueness among siblings at apply time — neither constraint is (or can be) fully expressed in this module's validation {} blocks, which catch only the syntactic name-format rules (length, excluded characters, reserved names, leading/trailing whitespace).
  • timeouts is assigned directly with = (Plugin Framework Attributes object), consistent with the same pattern confirmed on fabric_workspace.

🧱 Design Principles

Concern Safe default Opt-out
Folder placement parent_folder_id = null — new folders land at the workspace root Caller supplies parent_folder_id to nest under a governed folder hierarchy
Preview-only resource Module never assumes preview mode is enabled — states the requirement prominently rather than silently failing N/A — caller's root provider block must set preview = true

🚀 Runbook

cd C:\GitHubCode\newfabricmodules\tf-mod-fabric-folder
terraform init -backend=false
terraform validate
terraform fmt -check

Pin consumers to ?ref=v1.0.0 — never a branch. This module is plan-only; a human applies from a reviewed, approved CI pipeline.


🧪 Testing

  • terraform validate catches: a display_name that's empty, too long, has leading/trailing whitespace, contains an excluded character, or matches a reserved name; a malformed timeouts type.
  • terraform fmt -check catches formatting drift without silently rewriting files.
  • Not caught offline: whether the caller's provider block actually has preview = true set, whether the calling principal has Admin/Member/Contributor role on the target workspace, whether the tenant's "Service principals can call Fabric public APIs" setting is enabled, folder-name uniqueness among siblings, and the 10-level nesting-depth limit. These require a live plan/apply against a real tenant with preview mode enabled.

💬 Example Output

module.folder_raw.id = "3c9e1f2a-7b4d-4e6a-9f1c-2d5e8a7b9c0d"
module.folder_raw.display_name = "raw"
module.folder_raw.workspace_id = "8f4e2c1a-9b3d-4a7e-b2f1-6c5d8e9f0a1b"
module.folder_raw.parent_folder_id = null

🔍 Troubleshooting

Symptom Cause Fix
apply fails / resource not found Preview mode not enabled on caller's root provider block Set provider "fabric" { preview = true } at the root
apply fails with a 403/permission error Calling principal has only Viewer role on the workspace Grant Admin, Member, or Contributor role on the workspace
apply fails with a naming conflict Two sibling folders under the same parent share a display_name Rename one; the Fabric API enforces uniqueness among siblings, not this module
terraform validate rejects display_name Value contains an excluded character, a reserved name, or leading/trailing whitespace Adjust the name per the folder name requirements documented in Provider/Versions
Folder unexpectedly moved on apply parent_folder_id was changed intentionally or by drift Confirm this is expected — it's an in-place move, not a bug; revert parent_folder_id if unintended
Entire module fails on every resource, every environment Tenant-level "Service principals can call Fabric public APIs" setting not enabled Have a Fabric administrator flip the Developer setting in the Fabric Admin Portal

🔗 Related Docs

About

No description or website provided.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages