Standalone vSphere module — manages a single vCenter inventory folder (
vsphere_folder) of any of the five folder types, with secure-by-default tagging and fail-fast placement validation.
Why it matters: Inventory folders are how vCenter keeps a growing estate legible. They group VMs, hosts, datastores, and networks into a navigable hierarchy and — just as importantly — they are the natural anchor points for permissions and tag-based policy. A flat inventory is unmanageable and unauditable; a folder tree lets you delegate access at the branch level and apply governance consistently. This module creates exactly one folder of any of the five vCenter types. The empty, valid call places the folder correctly or fails at plan time with an actionable message — it never guesses a datacenter, and it never silently applies tags.
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!
Render the source below via the Mermaid Chart MCP (validated
flowchart). This module is highlighted in#607078. The folder consumes adatacenter_id(MOID) resolved by the caller andtagsfromtf-mod-vsphere-tag; itsid/pathflows out to sibling modules that place inventory objects (VMs and other folders) inside it.
graph LR
dc["caller data vsphere_datacenter<br/>datacenter_id"]
tag["tf-mod-vsphere-tag<br/>tags"]
fold["tf-mod-vsphere-folder"]
vm["tf-mod-vsphere-virtual-machine<br/>folder placement"]
sib["sibling inventory objects placed<br/>in this folder (host / datastore /<br/>network / datacenter folders)"]
dc -->|datacenter_id| fold
tag -->|tags| fold
fold -->|id / path| vm
fold -->|id / path| sib
style fold fill:#607078,color:#fff,stroke:#333,stroke-width:2px
Keystone node in
#00A1E0; the universal tail in#607078. This module wraps exactly one resource —vsphere_folder.this— withtagsandcustom_attributesapplied to it.pathandtypeare immutable (force-new).
graph TD
this["vsphere_folder.this<br/>(keystone — one folder)<br/>path + type immutable (force-new)"]
tail["universal tail<br/>tags (list) + custom_attributes (map)"]
this --> tail
style this fill:#00A1E0,color:#fff,stroke:#333,stroke-width:2px
style tail fill:#607078,color:#fff
Grant the Terraform integration account the minimum set below on the target datacenter (or on the parent folder you are building under — Folder.Create is checked on the parent). Do not use the built-in Administrator account; validate the set with vCenter's Check Privileges feature.
| Privilege ID | vCenter UI name | Why this module needs it |
|---|---|---|
Folder.Create |
Create folder | Create the folder. |
Folder.Delete |
Delete folder | Remove the folder on destroy. |
Folder.Move |
Move folder | Re-parent the folder when the leading portion of path changes. |
Folder.Rename |
Rename folder | Rename the folder when the final segment of path changes. |
InventoryService.Tagging.AttachTag |
Assign or Unassign vSphere Tag | Only if tags is non-empty — attach vsphere_tag MOIDs. |
Global.SetCustomField |
Set custom attribute | Only if custom_attributes is non-empty — set attribute values. |
All operations also require baseline read access (the built-in Read-only role:
System.Anonymous,System.View,System.Read) on the inventory path so Terraform can refresh state.The last two rows are conditional — omit them from the role if the caller never sets
tagsorcustom_attributes. Defining a custom attribute (as opposed to setting its value) additionally requiresGlobal.ManageCustomFields, which is out of scope for this module.
| Requirement | Detail |
|---|---|
| vCenter Server | ≥ 7.0. Folders are a vCenter inventory construct — a direct ESXi connection cannot manage them, and custom_attributes is unsupported outside vCenter. |
| License | Any vSphere edition that includes vCenter Server. Inventory folders are not a separately licensed feature. |
| Datacenter exists | For types host, vm, datastore, and network, the target datacenter must already exist and be resolved by the caller to a MOID (datacenter_id). |
| Parent folder exists | For nested paths (foo/bar), the parent foo must already exist — the provider does not create intermediate folders. |
| Tags / attributes exist | Any vsphere_tag MOID (from tf-mod-vsphere-tag) or custom-attribute definition must already exist before it is referenced. |
| Provider | vmware/vsphere ~> 2.0, Terraform ≥ 1.12.0. The provider is configured by the caller (see Quick Start). |
# ---- caller's root module ----
# The provider is configured by the CALLER — never inside the module.
# Prefer environment variables: VSPHERE_SERVER, VSPHERE_USER, VSPHERE_PASSWORD.
provider "vsphere" {
allow_unverified_ssl = false # true is acceptable only for lab/self-signed certs — never in production
}
# Resolve the pre-existing datacenter MOID (consumed by ID — this module never looks it up).
data "vsphere_datacenter" "dc" {
name = "dc-01"
}
# Create the VM inventory folder /dc-01/vm/Production
module "production_vm_folder" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Production"
type = "vm"
datacenter_id = data.vsphere_datacenter.dc.id
}
output "production_vm_folder_id" {
value = module.production_vm_folder.id
}Nested folder (parent must already exist):
module "webtier_vm_folder" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Production/WebTier" # nests under the existing "Production" folder
type = "vm"
datacenter_id = data.vsphere_datacenter.dc.id
tags = [module.tf_mod_vsphere_tag.tag_ids["environment"]] # vsphere_tag MOIDs, not names
}Datacenter-type folder (no datacenter_id):
module "research_dc_folder" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Research"
type = "datacenter" # datacenter_id is omitted only for this type
}Plan-only. Validate with
terraform init -backend=false && terraform validate. This module system never runsterraform apply— a human reviews the plan and applies it in the controlled pipeline. See the Runbook.
1 · Minimal VM folder
module "vm_folder_prod" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Production"
type = "vm"
datacenter_id = data.vsphere_datacenter.dc.id
}2 · Nested VM folder (two levels)
module "vm_folder_webtier" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Production/WebTier" # "Production" must already exist
type = "vm"
datacenter_id = module.datacenter.id
}3 · Host / cluster folder
module "host_folder_prod" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "ProdClusters"
type = "host"
datacenter_id = module.datacenter.id
}4 · Datastore folder
module "ds_folder_prod" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "prod/nfs"
type = "datastore"
datacenter_id = module.datacenter.id
}5 · Network folder
module "net_folder" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "DVS-PortGroups"
type = "network"
datacenter_id = module.datacenter.id
}6 · Datacenter-type folder (no datacenter_id required)
module "dc_folder_regions" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "regions"
type = "datacenter"
# datacenter_id is NOT required for type = "datacenter"
}7 · With environment tags
module "vm_folder_staging" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Staging"
type = "vm"
datacenter_id = module.datacenter.id
tags = [
module.tf_mod_vsphere_tag.tag_ids["environment"], # "staging"
module.tf_mod_vsphere_tag.tag_ids["managed-by"], # "terraform"
]
}8 · With custom attributes for ownership
module "vm_folder_platform" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Platform"
type = "vm"
datacenter_id = module.datacenter.id
custom_attributes = {
(data.vsphere_custom_attribute.owner.id) = "platform-team"
(data.vsphere_custom_attribute.cost_center.id) = "INFRA-PLAT"
}
}9 · Multiple VM folders with for_each
locals {
vm_folders = {
production = "Production"
staging = "Staging"
dev = "Development"
lab = "Lab"
}
}
module "vm_folders" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
for_each = local.vm_folders
path = each.value
type = "vm"
datacenter_id = module.datacenter.id
tags = [module.tf_mod_vsphere_tag.tag_ids[each.key]]
}10 · Three-tier app folder structure
# Creates Production/WebTier, Production/AppTier, Production/DbTier
locals {
app_folders = {
web = "Production/WebTier"
app = "Production/AppTier"
db = "Production/DbTier"
}
}
module "app_vm_folders" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
for_each = local.app_folders
path = each.value # "Production" must already exist
type = "vm"
datacenter_id = module.datacenter.id
}11 · Moving a folder (rename path segment)
# Before: path = "OldName" → After: path = "NewName"
# The provider moves the folder in place; no destroy/recreate needed.
module "vm_folder_renamed" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "NewName" # was "OldName" — this rename is safe (mutable)
type = "vm"
datacenter_id = module.datacenter.id
}12 · Import an existing folder into Terraform state
import {
to = module.vm_folder_existing.vsphere_folder.this
id = "tf-dc-01/vm/Existing" # format: <datacenter_name>/<type>/<path>
}
module "vm_folder_existing" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Existing"
type = "vm"
datacenter_id = module.datacenter.id
}13 · Tags + custom attributes — full governance metadata
module "vm_folder_regulated" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Regulated/PII"
type = "vm"
datacenter_id = module.datacenter.id
tags = [
module.tf_mod_vsphere_tag.tag_ids["data-class"], # "npi"
module.tf_mod_vsphere_tag.tag_ids["compliance"], # "fca-regulated"
]
custom_attributes = {
(data.vsphere_custom_attribute.owner.id) = "cloud-platform"
(data.vsphere_custom_attribute.data_class.id) = "PII"
(data.vsphere_custom_attribute.review_date.id) = "2027-01-01"
}
}14 · All folder types for a complete datacenter org structure
# One module call per folder type, all in the same datacenter
module "vm_folder" { source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"; path = "Production"; type = "vm"; datacenter_id = module.datacenter.id }
module "host_folder" { source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"; path = "ProdHosts"; type = "host"; datacenter_id = module.datacenter.id }
module "datastore_folder" { source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"; path = "ProdDS"; type = "datastore"; datacenter_id = module.datacenter.id }
module "network_folder" { source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"; path = "DVS"; type = "network"; datacenter_id = module.datacenter.id }15 · End-to-end: datacenter → folder → VM placement
module "datacenter" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-prod-01"
}
module "vm_folder_prod" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-folder?ref=v1.0.0"
path = "Production/WebTier"
type = "vm"
datacenter_id = module.datacenter.id # ← from datacenter module
}
# A downstream VM module consumes the folder path:
# folder = "${module.vm_folder_prod.path}"
output "prod_webtier_folder_path" {
value = module.vm_folder_prod.path
}| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string |
✅ Yes | — | Inventory path relative to the chosen type root in the datacenter. Do not prefix with the datacenter or type segment. "Production" → /dc-01/vm/Production; "Production/WebTier" nests under the existing Production. Mutable — changing the leading portion moves the folder; changing the final segment renames it. Validated non-empty after trimming whitespace. |
type |
string |
✅ Yes | — | Folder hierarchy. One of datacenter, host, vm, datastore, network. Immutable — changing forces destroy/recreate. |
datacenter_id |
string |
null |
MOID of the containing datacenter (consumed by ID; the caller resolves it via data "vsphere_datacenter"). Required for types host/vm/datastore/network; omit only when type = "datacenter". Immutable — changing forces destroy/recreate. |
|
tags |
list(string) |
No | [] |
List of vsphere_tag MOIDs (not display names) to attach. Wire from tf-mod-vsphere-tag, e.g. [module.tf_mod_vsphere_tag.tag_ids["environment"]]. The provider argument is a set; a list is converted automatically. |
custom_attributes |
map(string) |
No | {} |
Map of custom-attribute MOID → value string. Requires vCenter (unsupported on direct ESXi). |
Validation rules enforced in variables.tf:
pathmust be non-empty after trimming whitespace.typemust be one of:datacenter,host,vm,datastore,network.datacenter_idmust be set (non-null, non-empty) for every type exceptdatacenter.
| Output | Type | Description |
|---|---|---|
id |
string (MOID) |
Primary identifier. Managed object ID of the folder. Wire into sibling modules' folder-placement inputs (e.g. tf-mod-vsphere-virtual-machine's folder placement). |
name |
string |
Display name — the final segment of the inventory path. |
path |
string |
The folder's inventory path, relative to the datacenter/type root. |
type |
string |
The folder type: datacenter, host, vm, datastore, or network. |
The empty call produces the safe, correct resource; the caller types extra characters to opt into anything broader. Folders carry no network or encryption attack surface, so the posture here is correctness guards plus the secure tagging tail.
| Principle | Secure default | Opt-out / override | Why |
|---|---|---|---|
| Tags off by default | tags = [] |
Pass a list of vsphere_tag MOIDs |
Governance tags are applied deliberately by the caller, never silently inherited. |
| Custom attributes off by default | custom_attributes = {} |
Pass a MOID→value map | No surprise metadata written to the inventory. |
| Placement is explicit | datacenter_id = null + validation {} |
Set datacenter_id |
A folder cannot land in the wrong (or a default) datacenter by accident — non-datacenter types fail fast without it. |
| Type is a closed set | validation {} on type |
— | A typo ("vms") is a plan-time error, not a misplaced folder. |
| Path cannot be empty | validation {} on path |
— | Prevents accidentally targeting the type root. |
| No credentials in the module | provider configured by caller | — | Auth (vsphere_server/user/password/allow_unverified_ssl) lives in the caller's provider block. Use a least-privilege role — never Administrator. |
| Immutable fields documented | type, datacenter_id flagged force-new |
— | Operators are warned before a config edit silently turns into a destroy/recreate. |
Plan-only. This module system never runs terraform apply — a human reviews the plan and applies it after review.
terraform init -backend=false # no remote state during validation
terraform validate # type-checks variables and the resource graph
terraform fmt -check # style gate (run `terraform fmt` to fix)- Pin the module by tag (
?ref=v1.0.0), never a branch. - The controlled pipeline runs
terraform planfor human review;terraform applyis performed by an operator after approval — never by this module system.
| Symptom | Cause | Fix |
|---|---|---|
Error: datacenter_id is required for folder types host, vm, datastore, and network… at plan time |
type is not datacenter but datacenter_id was left null. |
Resolve the datacenter MOID in the caller (data "vsphere_datacenter") and pass it as datacenter_id. |
| Apply fails: parent/ancestor folder not found | Nested path (foo/bar) where the parent foo does not exist — the provider does not create intermediate folders. |
Create the parent first (a separate module call), and reference its output so Terraform orders creation. |
| Folder is destroyed and recreated unexpectedly | type or datacenter_id changed — both are immutable / force-new. |
Treat both as fixed at creation. To re-home a folder, plan the recreate deliberately (it yields a new MOID, breaking existing references). |
Tag not applied, or 404 / "managed object not found" on a tag |
tags was given a tag name instead of a vsphere_tag MOID, or the tag/category does not exist yet. |
Pass MOIDs from tf-mod-vsphere-tag.tag_ids[...]; create the tag and its category first. |
custom_attributes rejected / unsupported |
Connected directly to an ESXi host rather than vCenter. | Connect to vCenter Server — custom attributes (and folder inventory generally) are a vCenter feature. |
Folder unexpectedly moved after editing path |
You changed the leading portion of path, which re-parents the folder (a move, not a recreate). |
Expected behaviour. Change only the final segment to rename in place; change the leading portion only when you intend to move it. |
SCOPE.md— design contract for this module (in-scope vs. consumed-by-ID boundary, privileges, gotchas).vsphere_folderresource — provider documentation.tf-mod-vsphere-tag— source of thevsphere_tagMOIDs consumed bytags.tf-mod-vsphere-datacenter— produces thedatacenter_idMOID consumed here (or resolve a pre-existing one viadata "vsphere_datacenter").
vSphere module library · No terraform apply is ever performed by this module system — plan-only until a human reviews and applies.