Note: I have only studied this area for a few days from official documentation and other materials on the internet. The description contains basic information. There are a number of general questions that are important for production use. Such as optimal division of configuration files, use of variables, naming conventions, etc. Some answers are provided in the official documentation, but it's not entirely consistent and doesn't align with various examples. This means further study and testing is needed.
HashiCorp Terraform
Terraform is a software tool created by HashiCorp. It allows you to use infrastructure as code (Infrastructure as Code) to provision and manage an organization's infrastructure. We can automate the lifecycle of cloud, private datacenter, and SaaS infrastructure.
Terraform enables you to safely and efficiently build, change, and version infrastructure. For declarative description, it uses the HashiCorp Configuration Language (HCL), alternatively also JSON-compatible syntax. We can create resources (such as vApps, networks, VMs, FW) and repeat deployment in different environments with consistent configuration. Terraform compares the description with the current state and makes only necessary changes. We can integrate into CI/CD pipeline (DevOps processes).
Terraform State
Using Terraform, we can define new resources (objects) that will be created in the infrastructure after application. Terraform stores the state State of managed infrastructure and configuration in the terraform.tfstate file. It is used for mapping resources declared in configuration to actual objects in the target infrastructure. It contains information about identities and attributes of created resources. Using the state, Terraform determines the differences between the current and desired state that need to be applied.

Terraform expects that it is the only one creating (or destroying) objects and stores their identity in the state. So there is a one-to-one mapping between configured resource instances and remote objects. If this is not the case, we have commands available for state recovery, import of externally created objects, forgetting objects, or basic state editing. These are more advanced operations that this article does not cover.
The state file is therefore important and we should not lose it (if multiple people are working with Terraform, we should share it). When an object exists in the state file but is not found in the configuration, it will be removed during application. If we create an object configuration (resource block) that already exists in the infrastructure but we don't have information about it in the state, an error will occur during application.
Error: error creating vApp Demo-vApp: error executing task request: error instantiating a new vApp:: API Error: 400: [ ] The VMware Cloud Director entity Demo-vApp already exists.
Note: If we define only a new object in the configuration that we want to create in the infrastructure, Terraform works only with it and ignores existing infrastructure objects. In other words, it works only with objects described in configuration files.
Official Documentation
Installation (on Windows)
Terraform CLI
Terraform can run on various operating systems such as Windows, macOS, or Linux (or using Windows Subsystem for Linux). From the official page Install Terraform we download the latest version (at the time of writing 1.14.4). On Windows, it's a single EXE file that we save somewhere on the disk and ideally add its path to the PATH system environment variable.
We can verify the version on the command line:
C:\>terraform -version Terraform v1.14.4 on windows_amd64
Microsoft Visual Studio Code (VS Code)
As an editor for Terraform (HCL) files, Visual Studio Code is very commonly used. There is an official HashiCorp Terraform extension for it. It highlights syntax, has auto-completion, validation, etc. We download the installation from the official site Visual Studio Code.
After installation and launch, we install extensions (in the left panel, click on the Extensions icon (Ctrl+Shift+X)):
- HashiCorp Terraform
- HashiCorp HCL
VMware Cloud Director Provider
Terraform uses binary plugins called providers to interact with cloud services, SaaS, and other APIs. Terraform configuration must declare which providers it requires so that Terraform can install and use them. HashiCorp maintains the Terraform Registry, from which we can obtain public providers and modules.
There is an official VMware Cloud Director Provider vmware/vcd. The current version 3.14 supports Cloud Director (VCD) versions 10.4, 10.5, and 10.6. Documentation VMware Cloud Director Provider 3.14. It allows you to define Cloud Director infrastructure as code in Terraform configuration files. We can define resources that are automatically provisioned or deprovisioned.
Our Terraform Project for VMware Cloud Director (VCD)
Terraform uses configuration files in HCL language, which are text files with the .tf extension. When we perform operations using Terraform CLI, all configuration files from the current directory are loaded and dependencies in our configuration are resolved. This allows organizing configuration into multiple files, which is recommended. File naming and content division is up to us. Certain examples are in the Terraform documentation and tutorials.
On disk, we create a folder for our VCD project (in the example, it's d:\VCD-terraform). Here we will create individual configuration files. In VS Code, we open the created folder using File - Open Folder.
In our example, we will create files
terraform.tf- Terraform configuration and VMware Cloud Director Providervariables.tf- variable name definitionsterraform.tfvars- variable value assignmentsoutput.tf- output of information about existing infrastructure objectsmain.tf- resource configuration, creating infrastructure objects in VCD

Terraform and VCD Provider Configuration
We create a terraform.tf file, into which we insert the configuration of Terraform itself (terraform block), including specifying which providers to install. We add the configuration of the VMware Cloud Director provider (provider block), which we could also place in another file.
# Version and providers requirement terraform { required_version = ">= 1.5.0" required_providers { vcd = { source = "vmware/vcd" version = "~> 3.14" } } } # Configure the VMware Cloud Director Provider provider "vcd" { user = var.vcd_user password = var.vcd_pass auth_type = "integrated" org = var.vcd_org vdc = var.vcd_vdc url = var.vcd_url max_retry_timeout = var.vcd_max_retry_timeout allow_unverified_ssl = var.vcd_allow_unverified_ssl logging = var.vcd_log logging_file = var.vcd_log_file }
Variable Definitions
In many cases (repeated data, reusability), it is recommended not to insert values directly into configuration files, but to use variables. We must define them first and then assign their value. We have already used variables in the VCD provider configuration. For definition, we create a variables.tf file. In a variable definition, we can specify a number of parameters, such as type, validation, description, but everything is optional.
We can use only an empty definition variable <LABEL> { }. We reference variables in the configuration using var.<NAME>.
# Logging variable "vcd_log" { description = "Enable Logging" type = bool } variable "vcd_log_file" { description = "Logging File" type = string } # VMware Cloud Director variables variable "vcd_user" { description = "VCD username" type = string } variable "vcd_pass" { description = "VCD username password" type = string sensitive = true } variable "vcd_org" { description = "VMware Cloud Director Organization" type = string } variable "vcd_vdc" { description = "Virtual Data Center" type = string } variable "vcd_url" { description = "VCD API URL" type = string } variable "vcd_max_retry_timeout" { type = number } variable "vcd_allow_unverified_ssl" { description = "Allow unverified SSL" type = bool } variable "vcd_catalog_name" { description = "VCD Catalog" type = string } variable "vcd_template_name" { description = "Catalog Template" type = string }
Assigning Values to Variables
Assigning values to variables can be done in several ways. For example, using a parameter when calling Terraform CLI, using environment variables, or a file with variable definitions. Here we will use the terraform.tfvars file.
# Logging vcd_log = "false" vcd_log_file = "vcd-debug.log" # VMware Cloud Director Connection Variables vcd_org = "Firma" vcd_vdc = "Firma vDC" vcd_url = "https://server.domain.com/api" vcd_user = "test2" vcd_pass = "XXX" vcd_max_retry_timeout = "240" vcd_allow_unverified_ssl = false # Catalog vcd_catalog_name = "Firma_catalog" vcd_template_name = "template-Alma"
Connecting to VMware Cloud Director
Login Credentials
To connect to VMware Cloud Director, credentials with sufficient permission (role) are required. This is of course a very sensitive matter. Here we show the simplest method - a specially created user and entering the username and password. We can also use Bearer or API token or SAML. If we enter the credentials in a file, we must protect this file and not store it in Git.
Alternatively, we can enter variables (such as password) at runtime:
terraform apply -var vcd_pass = "XXX"
Connection Parameters
In this article, we are connecting to a specific organization (tenant) and a specific VDC. We are not a Sys Admin managing multiple organizations. When configuring resources, we can specify Org and VDC. We will not do that, so these values set in the provider will be used.
In the connection parameters to VMware Cloud Director, we must enter the correct details of our environment. At what address (URL) the Cloud Director API runs for Terraform connection. The name of our organization and Virtual Data Center in which the API operations will be performed. The organization name can be found under Administration - Settings - General - Organization name (not Organization full name). The vDC name can be found under Data Centers - our vDC - Settings - General - Name.
Organization name is case sensitive. If we don't enter the name correctly, we will get an error that the organization was not found.
Error: org FIrma not found: [ENF] entity not found
If we encounter problems, we can enable logging of API calls (requests and responses, plus debug) to a file in the provider.
Terraform Workspace Initialization
When we have the basic Terraform configuration ready, we must perform working directory initialization for the first time. The specified provider will be downloaded. We perform initialization with the CLI command terraform init, which must be run in the project folder. In VS Code, we can use the terminal (display View - Terminal).
PS D:\VCD-terraform> terraform init Initializing the backend... Initializing provider plugins... - Finding vmware/vcd versions matching "~> 3.14"... - Installing vmware/vcd v3.14.1... - Installed vmware/vcd v3.14.1 (signed by a HashiCorp partner, key ID 8BF53DB49CDB70B0) Partner and community providers are signed by their developers. If you'd like to know more about provider signing, you can read about it here: https://developer.hashicorp.com/terraform/cli/plugins/signing Terraform has created a lock file .terraform.lock.hcl to record the provider selections it made above. Include this file in your version control repository so that Terraform can guarantee to make the same selections by default when you run "terraform init" in the future. Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.
Configuration Validation
We can perform syntax validation and internal consistency of our configuration. Execution does not occur, only code checking.
PS D:\VCD-terraform> terraform validate Success! The configuration is valid.
VMware Cloud Director Resources
A Resource is any infrastructure object that we want to create or manage. Resource types depend on installed providers. In each resource, we can use various arguments that define its properties.
The VMware Cloud Director Provider offers a number of resources (detailed description can be found in the documentation), for example:
Querying Information About Existing Objects (Resources)
Data Sources fetches data from the provider but do not create or modify resources. The data <TYPE> <LABEL> block is used. Type specifies the data source (resource) type. Label specifies the object name (label). We can then reference the fetched data using data.<type>.<label>.<attribute>. Inside the block, we must use (in our examples) at least the name argument, where we specify the resource name of the given type.
Fetched (or configured) information can be displayed on the command line using the output <LABEL> block. Using the required value argument, we specify the returned value (it can be computed). Here we reference the loaded object and can display it entirely or some of its attributes.
As an example, we create an output.tf file (commonly used as output for modules), where we load information about the vApp Demo-vApp and display the list of VM names found in it.
# read vApp details data "vcd_vapp" "my-vapp" { name = "Demo-vApp" } # display vApp details output "vapp-details" { value = data.vcd_vapp.my-vapp.vm_names }
Configuration Evaluation (terraform plan) - Loading and Displaying Data
When we have the configuration ready (whether fetches data sources or creating resources), the next step is to use the Terraform CLI command terraform plan. It evaluates the configuration and determines the desired state of all declared resources. Then it compares this state with the actual infrastructure objects. And displays a description of the changes needed to achieve the desired state. It does not make any infrastructure changes, only presents a change plan.
If there are output values (output block), they will be reported thanks to terraform plan.
PS D:\VCD-terraform> terraform plan
data.vcd_vapp.my-vapp: Reading...
data.vcd_vapp.my-vapp: Read complete after 3s [id=urn:vcloud:vapp:8855943a-e16a-416e-bae5-0fd1dfc251e3]
Changes to Outputs:
+ vApp-details = [
+ "Demo-VM",
]
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
-------------------------------------------------------------------------------------------------------------------------
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run
"terraform apply" now.
We can also use the terraform apply command. If we don't have new resources in the configuration (resource block), only these values will be saved (fetched and outputs) to the State. No infrastructure changes occur.
PS D:\VCD-terraform> terraform apply
data.vcd_vapp.my-vapp: Reading...
data.vcd_vapp.my-vapp: Read complete after 2s [id=urn:vcloud:vapp:8855943a-e16a-416e-bae5-0fd1dfc251e3]
Changes to Outputs:
+ vApp-details = [
+ "Demo-VM",
]
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
vApp-details = tolist([
"Demo-VM",
])
Saved outputs (to State) can be displayed using terraform output.
PS D:\VCD-terraform> terraform output vApp-details = tolist([ "Demo-VM", ])
Creating New Objects (provisioning infrastructure)
For resource configuration, the resource <TYPE> <LABEL> block is used. We must specify the resource type (Type) from our provider. And a unique resource name - object label (Label), which is used to track the resource in the state file. In the configuration, we can reference the resource using <type>.<label>.<attribute>. Inside the resource block, we configure the arguments of the given resource.
In our project, we create a main.tf file, into which we insert various configuration blocks for different objects.
Creating a New vApp
We configure a new vApp Demo-vApp.
# create vApp
resource "vcd_vapp" "Demo" {
name = "Demo-vApp"
}
Attaching Network
We attach an existing organization network Demo_net to the vApp. We set a dependency that the vApp must be completed first before Terraform starts working on the network.
# add vApp network attached to existing Org network
resource "vcd_vapp_org_network" "vappOrgNet" {
vapp_name = vcd_vapp.Demo.name
org_network_name = "Demo_net"
depends_on = [ vcd_vapp.Demo ]
}
Loading Template
We retrieve information about our catalog and find a specific template in it, from which we will create a VM.
# read Catalog data "vcd_catalog" "my-catalog" { name = var.vcd_catalog_name } # read Catalog Template data "vcd_catalog_vapp_template" "my-vapp-template" { catalog_id = data.vcd_catalog.my-catalog.id name = var.vcd_template_name }
Creating VM from Template
We create a VM from a predefined template. We define its parameters (update will occur after creation). We assign a network with a manually entered IP address (must fall within the given range). After VM creation, it automatically starts (we can change this with the power_on argument).
# create VM from template wtih manual IP assignment
resource "vcd_vapp_vm" "DemoVM" {
vapp_name = vcd_vapp.Demo.name
name = "Demo-VM"
computer_name = "Demo-VM"
vapp_template_id = data.vcd_catalog_vapp_template.my-vapp-template.id
memory = 2048
cpus = 2
cpu_cores = 1
depends_on = [ vcd_vapp_org_network.vappOrgNet ]
network {
type = "org"
name = vcd_vapp_org_network.vappOrgNet.org_network_name
ip_allocation_mode = "MANUAL"
ip = "172.30.21.100"
is_primary = true
}
}
Applying Configuration Changes (terraform apply)
The last step is (execution) applying changes according to the Terraform plan, i.e., actually creating or modifying objects (or destroying them) according to our configuration.
User permissions are important when connecting to Cloud Director (VCD). When we don't want to use the Organization Administrator role, for example. I tried vApp Author, who could create a vApp but couldn't load the catalog (even though they should have those permissions). An error was displayed
Error: [catalog read DS] error retrieving catalog Firma_catalog: [ENF] entity not found - no records found for catalog 'Firma_catalog' in org 'Firma'
Note: Before applying, we can first use the Terraform CLI command terraform plan and check the output.
To execute operations proposed by the Terraform plan, the Terraform CLI command terraform apply is used. The command creates an execution plan, prompts us to approve this plan (we enter yes), and performs the specified operations.

PS D:\VCD-terraform> terraform apply
data.vcd_catalog.my-catalog: Reading...
data.vcd_catalog.my-catalog: Read complete after 3s [id=urn:vcloud:catalog:e9e5c417-cfbc-4d4d-8136-6623b8615d6d]
data.vcd_catalog_vapp_template.my-vapp-template: Reading...
data.vcd_catalog_vapp_template.my-vapp-template: Read complete after 3s [id=urn:vcloud:vapptemplate:22ad75a3-bb77-4c36-8cee-8faf7
8b60a03]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
+ create
Terraform will perform the following actions:
# vcd_vapp.Demo will be created
+ resource "vcd_vapp" "Demo" {
+ href = (known after apply)
+ id = (known after apply)
+ inherited_metadata = (known after apply)
+ metadata = (known after apply)
+ name = "Demo-vApp"
+ power_on = false
+ status = (known after apply)
+ status_text = (known after apply)
+ vapp_network_names = (known after apply)
+ vapp_org_network_names = (known after apply)
+ vm_names = (known after apply)
+ lease (known after apply)
+ metadata_entry (known after apply)
}
# vcd_vapp_org_network.vappOrgNet will be created
+ resource "vcd_vapp_org_network" "vappOrgNet" {
+ id = (known after apply)
+ is_fenced = false
+ org_network_name = "Demo_net"
+ reboot_vapp_on_removal = false
+ retain_ip_mac_enabled = false
+ vapp_name = "Demo-vApp"
}
# vcd_vapp_vm.DemoAS will be created
+ resource "vcd_vapp_vm" "DemoVM" {
+ accept_all_eulas = true
+ computer_name = "DemoVM"
+ consolidate_disks_on_create = false
+ cpu_cores = 1
+ cpu_hot_add_enabled = false
+ cpu_limit = (known after apply)
+ cpu_priority = (known after apply)
+ cpu_reservation = (known after apply)
+ cpu_shares = (known after apply)
+ cpus = 2
+ description = (known after apply)
+ expose_hardware_virtualization = false
+ extra_config = (known after apply)
+ firmware = (known after apply)
+ hardware_version = (known after apply)
+ href = (known after apply)
+ id = (known after apply)
+ imported = (known after apply)
+ inherited_metadata = (known after apply)
+ internal_disk = (known after apply)
+ memory = 2048
+ memory_hot_add_enabled = false
+ memory_limit = (known after apply)
+ memory_priority = (known after apply)
+ memory_reservation = (known after apply)
+ memory_shares = (known after apply)
+ metadata = (known after apply)
+ name = "DemoVM"
+ os_type = (known after apply)
+ placement_policy_id = (known after apply)
+ power_on = true
+ prevent_update_power_off = false
+ security_tags = (known after apply)
+ sizing_policy_id = (known after apply)
+ status = (known after apply)
+ status_text = (known after apply)
+ storage_profile = (known after apply)
+ vapp_id = (known after apply)
+ vapp_name = "Demo-vApp"
+ vapp_template_id = "urn:vcloud:vapptemplate:22ad75a3-bb77-4c36-8cee-8faf78b60a03"
+ vm_type = (known after apply)
+ boot_options (known after apply)
+ customization (known after apply)
+ metadata_entry (known after apply)
+ network {
+ adapter_type = (known after apply)
+ connected = true
+ ip = "172.30.21.100"
+ ip_allocation_mode = "MANUAL"
+ is_primary = true
+ mac = (known after apply)
+ name = "Demo_net"
+ secondary_ip = (known after apply)
+ secondary_ip_allocation_mode = (known after apply)
+ type = "org"
}
}
Plan: 3 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
vcd_vapp.Demo: Creating...
vcd_vapp.Demo: Creation complete after 6s [id=urn:vcloud:vapp:8855943a-e16a-416e-bae5-0fd1dfc251e3]
vcd_vapp_org_network.vappOrgNet: Creating...
vcd_vapp_org_network.vappOrgNet: Creation complete after 5s [id=urn:vcloud:network:3be9573d-b435-4f73-ad36-734aa010e910]
vcd_vapp_vm.DemoAS: Creating...
vcd_vapp_vm.DemoAS: Still creating... [00m10s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [00m20s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [00m30s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [00m40s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [00m50s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [01m00s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [01m10s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [01m20s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [01m30s elapsed]
vcd_vapp_vm.DemoAS: Still creating... [01m40s elapsed]
vcd_vapp_vm.DemoAS: Creation complete after 1m46s [id=urn:vcloud:vm:c33102c0-77d0-4455-a220-df52dcf19658]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

For terraform apply and terraform plan, we can use a number of parameters that affect its behavior. For example, skipping automatic state refresh.
terraform apply -refresh=false
Or limit the number of concurrent operations (default is 10).
terraform apply -parallelism=1
Destroying Objects (terraform destroy)
If we are testing or developing, we may want to deprovision the created objects. In Terraform CLI, we have the terraform destroy command available (actually terraform apply -destroy is used), which deletes all objects managed by the Terraform configuration.
Note: We can also use terraform plan -destroy.
By default, removing a vApp network from a vApp will not succeed if the vApp is running, and then the vApp won't be deleted either. In the network configuration (vcd_vapp_org_network), we can allow shutting down the parent vApp for network removal using:
reboot_vapp_on_removal = true
There are no comments yet.