Standalone module that manages a single VMware vSphere datacenter — the top-level vCenter inventory container that holds hosts, clusters, datastores, networks, and virtual machines.
Why it matters: The datacenter is the root of the vCenter object graph. Almost every other module in this library — clusters, datastores, port groups, VMs — consumes a
datacenter_id(MOID). This module creates that container deterministically, emits its MOID for sibling modules to wire against, and does nothing else: it owns the container, not its contents. Most real configuration lives in the child objects placed inside the datacenter, not on the datacenter itself.
The module wraps exactly one resource — vsphere_datacenter named this — and
carries the universal tail (tags + custom_attributes), both of which the
provider schema supports on this resource.
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.tf-mod-vsphere-datacenteris a foundation root — it consumes nodatacenter_id; it produces one. Itsid/moidflows out to thedatacenter_idinput of nearly every other module in the library;tagsflows in fromtf-mod-vsphere-tag.
graph LR
root(["foundation root — consumes<br/>no datacenter_id (produces one)"])
tag["tf-mod-vsphere-tag<br/>tags"]
dc["tf-mod-vsphere-datacenter"]
cc["tf-mod-vsphere-compute-cluster<br/>datacenter_id"]
ds["tf-mod-vsphere-nas-datastore /<br/>tf-mod-vsphere-vmfs-datastore<br/>datacenter_id"]
dvs["tf-mod-vsphere-distributed-virtual-switch<br/>datacenter_id"]
host["tf-mod-vsphere-host<br/>datacenter_id"]
fold["tf-mod-vsphere-folder<br/>datacenter_id"]
vm["tf-mod-vsphere-virtual-machine<br/>datacenter_id"]
root --> dc
tag -->|tags| dc
dc -->|id / moid| cc
dc -->|id / moid| ds
dc -->|id / moid| dvs
dc -->|id / moid| host
dc -->|id / moid| fold
dc -->|id / moid| vm
style dc 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_datacenter.this— withtagsandcustom_attributesapplied to it.nameandfolderare immutable (force-new).
graph TD
this["vsphere_datacenter.this<br/>(keystone — one datacenter)<br/>name + folder 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
Minimum starting set for the Terraform integration account. Validate with vCenter's Check Privileges feature and expand only as the provider docs require. Do not use the built-in Administrator account.
| Privilege (ID) | vCenter UI name | Held on | Reason |
|---|---|---|---|
Datacenter.Create |
Datacenter → Create datacenter | parent folder (or vCenter root) | Create the datacenter object. |
Datacenter.Delete |
Datacenter → Remove datacenter | the datacenter | Required for terraform destroy and for the destroy half of a force-new replacement (see Architecture / Troubleshooting). |
InventoryService.Tagging.AttachTag |
vSphere Tagging → Assign or Unassign vSphere Tag | the datacenter | Only when tags is non-empty — attach the supplied vsphere_tag MOIDs. |
Global.SetCustomField |
Global → Set custom attribute | the datacenter | Only when custom_attributes is non-empty — write attribute values. The attribute definitions must already exist. |
Grant
Datacenter.Createat the inventory node where the datacenter is created — the parent folder whenfolderis set, otherwise the vCenter root.
- vCenter Server is required. Datacenters are vCenter inventory objects; they
cannot be created against a standalone ESXi host.
custom_attributeslikewise require vCenter and are unsupported on direct ESXi connections. - vCenter version: ≥ 7.0 (compatible with the
vmware/vsphere2.x provider line; 8.0 supported). - License: any licensed vCenter Server edition (vCenter Standard or higher). The datacenter object itself requires no special feature license.
- Terraform: ≥ 1.12.0. Provider:
vmware/vsphere ~> 2.0. - Preconditions:
- If
folderis set, the target inventory folder must already exist. - Any MOIDs passed in
tagsmust reference existingvsphere_tagobjects. - Any keys in
custom_attributesmust reference existing custom-attribute definitions (MOIDs), created out of band. - Provider/auth is configured by the caller, never by this module. Prefer
environment variables (
VSPHERE_SERVER,VSPHERE_USER,VSPHERE_PASSWORD);allow_unverified_sslmust befalsein production.
Source is pinned to a tag (
?ref=v1.0.0) — never a branch. The provider is configured by the caller. No credentials appear in module code. Seeexamples/main.tffor a runnable two-datacenter example.
# Caller's root module — the provider is configured here, never inside the module.
# Authentication is supplied out of band via environment variables:
# VSPHERE_SERVER, VSPHERE_USER, VSPHERE_PASSWORD
provider "vsphere" {
# allow_unverified_ssl = false # MUST remain false in production
}
# Minimal — datacenter at the vCenter root folder.
module "datacenter_prod" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-prod-01"
}
# With placement + the universal tail. vSphere tags are a LIST of tag MOIDs
# (not key/value); custom_attributes is a map of attribute MOID -> value string.
module "datacenter_research" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-research-01"
folder = "/research/" # optional; omit for the root folder
tags = [module.tf_mod_vsphere_tag.tag_ids["environment"]]
custom_attributes = {
(data.vsphere_custom_attribute.cost_center.id) = "AGRI-1042"
}
}1 · Minimal — datacenter at the vCenter root
module "datacenter_prod" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-prod-01"
}2 · Placed in an inventory folder
module "datacenter_east" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-east-01"
folder = "/regions/east"
}3 · With environment tags from tf-mod-vsphere-tag
module "datacenter_staging" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-staging-01"
tags = [
module.tf_mod_vsphere_tag.tag_ids["environment"], # "staging"
module.tf_mod_vsphere_tag.tag_ids["managed-by"], # "terraform"
]
}4 · With custom attributes for cost tracking
module "datacenter_dev" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-dev-01"
custom_attributes = {
(data.vsphere_custom_attribute.cost_center.id) = "INFRA-DEV"
(data.vsphere_custom_attribute.owner.id) = "platform-team"
}
}5 · Full metadata — folder + tags + custom attributes
module "datacenter_prod_full" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-prod-02"
folder = "/regions/us-east"
tags = [
module.tf_mod_vsphere_tag.tag_ids["environment"],
module.tf_mod_vsphere_tag.tag_ids["region"],
]
custom_attributes = {
(data.vsphere_custom_attribute.cost_center.id) = "PROD-OPS"
(data.vsphere_custom_attribute.owner.id) = "cloud-platform"
(data.vsphere_custom_attribute.tier.id) = "tier-1"
}
}6 · Multiple datacenters with for_each (dev / staging / prod)
locals {
datacenters = {
dev = { name = "dc-dev-01", folder = "/lifecycle/dev" }
staging = { name = "dc-staging-01", folder = "/lifecycle/staging" }
prod = { name = "dc-prod-01", folder = "/lifecycle/prod" }
}
}
module "datacenter" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
for_each = local.datacenters
name = each.value.name
folder = each.value.folder
tags = [module.tf_mod_vsphere_tag.tag_ids[each.key]]
}
output "datacenter_ids" {
value = { for k, v in module.datacenter : k => v.id }
}7 · Production datacenter with governance attributes
module "datacenter_prod_us" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-prod-us-01"
folder = "/production"
tags = [
module.tf_mod_vsphere_tag.tag_ids["environment"], # "production"
module.tf_mod_vsphere_tag.tag_ids["compliance"], # "fca-regulated"
module.tf_mod_vsphere_tag.tag_ids["data-class"], # "npi"
]
custom_attributes = {
(data.vsphere_custom_attribute.cost_center.id) = "-PROD"
(data.vsphere_custom_attribute.owner.id) = "cloud-platform"
(data.vsphere_custom_attribute.approved_by.id) = "infra-review-board"
(data.vsphere_custom_attribute.last_reviewed.id) = "2026-06-30"
}
}8 · Disaster recovery datacenter
module "datacenter_dr" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-dr-01"
folder = "/dr"
tags = [
module.tf_mod_vsphere_tag.tag_ids["environment"], # "dr"
module.tf_mod_vsphere_tag.tag_ids["site-role"], # "secondary"
]
custom_attributes = {
(data.vsphere_custom_attribute.dr_pair.id) = module.datacenter_prod.name
(data.vsphere_custom_attribute.rpo_hours.id) = "4"
}
}9 · Lab / sandbox datacenter (no folder)
module "datacenter_lab" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-lab-01"
# folder omitted → created at the vCenter root (intentional for lab envs)
tags = [module.tf_mod_vsphere_tag.tag_ids["environment"]]
}10 · Nested folder path (two levels deep)
module "datacenter_tenant" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-tenant-acme-01"
folder = "/tenants/acme" # parent "/tenants" must already exist
}11 · Import an existing datacenter into Terraform state
# Step 1 — add the import block and apply once.
import {
to = module.datacenter_existing.vsphere_datacenter.this
id = "/dc-legacy-01" # full inventory path from vCenter
}
module "datacenter_existing" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-legacy-01"
# folder omitted — the existing DC is at the root; add it if it's in a folder
}
# Step 2 — remove the import block after the first successful apply.12 · Consume the id output in a downstream module
module "datacenter" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-prod-01"
}
# Wire datacenter id to a folder module (typical dependency chain)
module "vm_folder" {
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 this module
}13 · Regional pair — primary and DR with paired attribute
module "datacenter_primary" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-us-east-01"
folder = "/regions/east"
custom_attributes = {
(data.vsphere_custom_attribute.dr_pair.id) = "dc-us-west-01"
}
}
module "datacenter_dr_west" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-us-west-01"
folder = "/regions/west"
custom_attributes = {
(data.vsphere_custom_attribute.dr_pair.id) = module.datacenter_primary.name
}
}14 · Per-team datacenter with role-based access metadata
module "datacenter_platform" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-platform-01"
folder = "/teams/platform"
tags = [module.tf_mod_vsphere_tag.tag_ids["team"]] # "platform-engineering"
custom_attributes = {
(data.vsphere_custom_attribute.admin_group.id) = "vsphere-platform-admins"
(data.vsphere_custom_attribute.ticket_queue.id) = "INFRA-PLATFORM"
}
}15 · End-to-end: datacenter → folder → resource pool
# Foundation layer: datacenter
module "datacenter" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-datacenter?ref=v1.0.0"
name = "dc-prod-01"
}
# VM inventory folder inside the datacenter
module "prod_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 # ← wired from datacenter
}
# Resource pool — caller resolves parent_resource_pool_id from the cluster
module "prod_resource_pool" {
source = "git::https://github.com/microsoftexpert/tf-mod-vsphere-resource-pool?ref=v1.0.0"
name = "rp-prod-workloads"
parent_resource_pool_id = data.vsphere_compute_cluster.prod.resource_pool_id
}| Name | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
Yes | — | Datacenter name; must be unique within its parent folder. Immutable — changing it forces destroy/recreate. |
folder |
string |
No | null |
Inventory folder path relative to the vCenter root (e.g. "/research/" or "parent/child"). null ⇒ created in the root folder. Immutable — changing it forces destroy/recreate. |
tags |
list(string) |
No | [] |
List of vsphere_tag ID strings (MOIDs) to attach. Not a key/value map. Obtain values from tf-mod-vsphere-tag outputs, e.g. tags = [module.tf_mod_vsphere_tag.tag_ids["environment"]]. |
custom_attributes |
map(string) |
No | {} |
Map of custom-attribute MOID → value string. Requires vCenter (unsupported on direct ESXi). Obtain attribute MOIDs from the vsphere_custom_attribute data source. |
Full type signatures (from variables.tf):
variable "name" { type = string } # required
variable "folder" { type = string, default = null } # optional, force-new
variable "tags" { type = list(string), default = [] } # universal tail
variable "custom_attributes" { type = map(string), default = {} } # universal tail
tagsandcustom_attributesusetype = list(string)/map(string)with a plaindefault, not a top-leveloptional(...)wrapper —optionalis only valid inside anobjectschema and failsterraform validateat the variable's top level.
| Name | Type | Description |
|---|---|---|
id |
string (MOID) |
Primary output. Managed object ID of the datacenter. On provider v2.x the resource id is the MOID. Wire this into any sibling module that takes a datacenter_id input. |
name |
string |
The display name of the datacenter. |
moid |
string (MOID) |
The MOID exposed explicitly by the provider. Equivalent to id on v2.x; prefer it when you want intent-revealing code. |
Every secure default below produces the safe result on an empty call; the caller must add characters to opt out.
| Principle | Secure / sane default | Explicit opt-out | Why |
|---|---|---|---|
| No credentials in the module | module defines no vsphere_server / user / password / allow_unverified_ssl |
caller configures the provider "vsphere" block |
Auth and TLS trust are caller/provider decisions; keeps secrets out of module code and state. |
| TLS verification on | allow_unverified_ssl = false (caller/provider) |
set true for lab/dev with self-signed certs only |
Production must verify vCenter certificates. |
| Empty tag set | tags = [] |
supply vsphere_tag MOIDs |
Caller owns the tag taxonomy; no surprise attachments. |
| Empty custom attributes | custom_attributes = {} |
supply MOID → value map | No silent metadata writes; requires vCenter. |
| Explicit placement | folder = null ⇒ root folder |
set folder to a path |
Placement is intentional, never hidden nesting. |
| Immutability documented | name / folder are force-new |
— | A datacenter is a live container; the module surfaces (rather than hides) that renaming or moving it means destroy/recreate. |
Plan-only. This module system never runs terraform apply — a human reviews
the plan and applies after review.
terraform init -backend=false # no remote state needed to validate the module
terraform validate # type-check the schema
terraform fmt -check # confirm canonical formatting
terraform plan # review the proposed changes
# NO terraform apply here — plan-only. A human reviews the plan and runs apply.| Symptom | Cause | Resolution |
|---|---|---|
Plan shows destroy then create after I changed name or folder. |
Both arguments are immutable — the provider marks them force new resource. | Expected. There is no in-place rename/move through this resource. To preserve inventory, rename/move in vCenter and reconcile with terraform import, or accept recreate only on an empty datacenter. Recreating destroys the container and everything Terraform manages beneath it. |
Error: custom attributes are unsupported on direct ESXi connections (or datacenter create fails outright). |
VSPHERE_SERVER points at an ESXi host, not vCenter. |
Datacenters and custom attributes require vCenter. Point the provider at vCenter Server. |
Permission to perform this operation was denied on create. |
The integration account lacks Datacenter.Create at the target inventory node. |
Grant Datacenter.Create on the parent folder (or vCenter root). Validate with Check Privileges. Do not fall back to the built-in Administrator account. |
| Tag attach fails / "tag not found". | A value in tags is a tag name, not a MOID, or the tag does not exist, or the account lacks the tagging privilege. |
Pass real vsphere_tag MOIDs (e.g. from tf-mod-vsphere-tag.tag_ids), ensure they exist, and grant InventoryService.Tagging.AttachTag on the datacenter. |
| How do I bring an existing datacenter under management? | The datacenter already exists in vCenter. | Import by full inventory path: terraform import module.<name>.vsphere_datacenter.this /dc-name. After refresh, id/moid resolve to the MOID. |
SCOPE.md— design contract for this module.examples/main.tf— runnable, plan-only usage example.- Sibling module
tf-mod-vsphere-tag— source ofvsphere_tagMOIDs fortags. - Provider docs —
vsphere_datacenterresource, applying tags, setting custom attributes.
Plan-only module. No terraform apply is ever performed by this module system —
a human reviews the plan and applies after review.