Terraform Learnings: Deploy an OVA Using the vSphere Provider

Once i got my head around the basics of Terraform I wanted to play with the vSphere provider to see what its was capable of. A basic use case that everyone needs is to deploy a VM. So my first use case is to deploy a VM from an OVA. The vSphere provider documentation for deploying an OVA uses William Lam’s nested ESXi OVA as an example. This is a great example of how to use the provider but seeing as I plan to play with the NSX-T provider also, I decided to use NSX-T Manager OVA as my source to deploy.

So first thing to do is setup your provider. Every provider in the Terraform registry has a Use Provider button on the provider page that pops up a How to use this provider box. This shows you what you need to put in your required_providers & provider block. In my case I will use a providers.tf file and it will look like the below example. Note you can only have one required_providers block in your configuration, but you can have multiple providers. So all required providers go in the same required_providers block and each provider has its own provider block.

# providers.tf

terraform {
  required_providers {
    vsphere = {
      source  = "hashicorp/vsphere"
      version = "~> 2.1.1"
    }
  }
}
provider "vsphere" {
  user                 = var.vsphere_user
  password             = var.vsphere_password
  vsphere_server       = var.vsphere_server
  allow_unverified_ssl = true
}

To authenticate to our chosen provider (in this case vSphere) we need to provide credentials. If you read my initial post on Terraform you would have seen me mention a terraform.tfvars file which can be used for sensitive variables. We will declare these as variables later in the variables.tf file but this is where we assign the values. So my terraform.tfvars file looks like this

# terraform.tfvars

# vSphere Provider Credentials
vsphere_user     = "administrator@vsphere.local"
vsphere_password = "VMw@re1!"

Next we need variables to enable us to deploy our NSX-T Manager appliance. So we create a variables.tf file and populate it with our variables. Note – variables that have a default value are considered optional and the default value will be used if no value is passed.

# variables.tf

# vSphere Infrastructure Details
variable "data_center" { default = "sfo-m01-dc01" }
variable "cluster" { default = "sfo-m01-cl01" }
variable "vds" { default = "sfo-m01-vds01" }
variable "workload_datastore" { default = "vsanDatastore" }
variable "compute_pool" { default = "sfo-m01-cl01" }
variable "compute_host" {default = "sfo01-m01-esx01.sfo.rainpole.io"}
variable "vsphere_server" {default = "sfo-m01-vc01.sfo.rainpole.io"}

# vCenter Credential Variables
variable "vsphere_user" {}
variable "vsphere_password" {}

# NSX-T Manager Deployment
variable "mgmt_pg" { default = "sfo-m01-vds01-pg-mgmt" }
variable "vm_name" { default = "sfo-m01-nsx01a" }
variable "local_ovf_path" { default = "F:\\OVAs\\nsx-unified-appliance-3.1.3.5.0.19068437.ova" }
variable "deployment_option" { default = "extra_small" } # valid deployments are: extra_small, small, medium, large
variable "nsx_role" { default = "NSX Manager" }          # valid roles are NSX Manager, NSX Global Manager
variable "nsx_ip_0" { default = "172.16.225.66" }
variable "nsx_netmask_0" { default = "255.255.255.0" }
variable "nsx_gateway_0" { default = "172.16.225.1" }
variable "nsx_dns1_0" { default = "172.16.225.4" }
variable "nsx_domain_0" { default = "sfo.rainpole.io" }
variable "nsx_ntp_0" { default = "ntp.sfo.rainpole.io" }
variable "nsx_isSSHEnabled" { default = "True" }
variable "nsx_allowSSHRootLogin" { default = "True" }
variable "nsx_passwd_0" { default = "VMw@re1!VMw@re1!" }
variable "nsx_cli_passwd_0" { default = "VMw@re1!VMw@re1!" }
variable "nsx_cli_audit_passwd_0" { default = "VMw@re1!VMw@re1!" }
variable "nsx_hostname" { default = "sfo-m01-nsx01a.sfo.rainpole.io" }

Now that we have our provider & variables in place we need a plan file to deploy the NSX-T Manager OVA, including the data sources we need to pull information from and the resource we are going to create.

# main.tf

# Data source for vCenter Datacenter
data "vsphere_datacenter" "datacenter" {
  name = var.data_center
}

# Data source for vCenter Cluster
data "vsphere_compute_cluster" "cluster" {
  name          = var.cluster
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

# Data source for vCenter Datastore
data "vsphere_datastore" "datastore" {
  name          = var.workload_datastore
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

# Data source for vCenter Portgroup
data "vsphere_network" "mgmt" {
  name          = var.mgmt_pg
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

# Data source for vCenter Resource Pool. In our case we will use the root resource pool
data "vsphere_resource_pool" "pool" {
  name          = format("%s%s", data.vsphere_compute_cluster.cluster.name, "/Resources")
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

# Data source for ESXi host to deploy to
data "vsphere_host" "host" {
  name          = var.compute_host
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

# Data source for the OVF to read the required OVF Properties
data "vsphere_ovf_vm_template" "ovfLocal" {
  name             = var.vm_name
  resource_pool_id = data.vsphere_resource_pool.pool.id
  datastore_id     = data.vsphere_datastore.datastore.id
  host_system_id   = data.vsphere_host.host.id
  local_ovf_path   = var.local_ovf_path
  ovf_network_map = {
    "Network 1" = data.vsphere_network.mgmt.id
  }
}

# Deployment of VM from Local OVA
resource "vsphere_virtual_machine" "nsxt01" {
  name                 = var.vm_name
  datacenter_id        = data.vsphere_datacenter.datacenter.id
  datastore_id         = data.vsphere_ovf_vm_template.ovfLocal.datastore_id
  host_system_id       = data.vsphere_ovf_vm_template.ovfLocal.host_system_id
  resource_pool_id     = data.vsphere_ovf_vm_template.ovfLocal.resource_pool_id
  num_cpus             = data.vsphere_ovf_vm_template.ovfLocal.num_cpus
  num_cores_per_socket = data.vsphere_ovf_vm_template.ovfLocal.num_cores_per_socket
  memory               = data.vsphere_ovf_vm_template.ovfLocal.memory
  guest_id             = data.vsphere_ovf_vm_template.ovfLocal.guest_id
  scsi_type            = data.vsphere_ovf_vm_template.ovfLocal.scsi_type
  dynamic "network_interface" {
    for_each = data.vsphere_ovf_vm_template.ovfLocal.ovf_network_map
    content {
      network_id = network_interface.value
    }
  }

  wait_for_guest_net_timeout = 5

  ovf_deploy {
    allow_unverified_ssl_cert = true
    local_ovf_path            = var.local_ovf_path
    disk_provisioning         = "thin"
    deployment_option         = var.deployment_option

  }
  vapp {
    properties = {
      "nsx_role"               = var.nsx_role,
      "nsx_ip_0"               = var.nsx_ip_0,
      "nsx_netmask_0"          = var.nsx_netmask_0,
      "nsx_gateway_0"          = var.nsx_gateway_0,
      "nsx_dns1_0"             = var.nsx_dns1_0,
      "nsx_domain_0"           = var.nsx_domain_0,
      "nsx_ntp_0"              = var.nsx_ntp_0,
      "nsx_isSSHEnabled"       = var.nsx_isSSHEnabled,
      "nsx_allowSSHRootLogin"  = var.nsx_allowSSHRootLogin,
      "nsx_passwd_0"           = var.nsx_passwd_0,
      "nsx_cli_passwd_0"       = var.nsx_cli_passwd_0,
      "nsx_cli_audit_passwd_0" = var.nsx_cli_audit_passwd_0,
      "nsx_hostname"           = var.nsx_hostname
    }
  }
  lifecycle {
    ignore_changes = [
      #vapp # Enable this to ignore all vapp properties if the plan is re-run
      vapp[0].properties["nsx_role"], # Avoid unwanted changes to specific vApp properties.
      vapp[0].properties["nsx_passwd_0"],
      vapp[0].properties["nsx_cli_passwd_0"],
      vapp[0].properties["nsx_cli_audit_passwd_0"],
      host_system_id # Avoids moving the VM back to the host it was deployed to if DRS has relocated it
    ]
  }
}

Once we have all of the above we can run the following to validate our plan

terraform plan -out=nsxt01

If your plan is successful you should see an output similar to below

Once your plan is successful run the command below to apply the plan

terraform apply nsxt01

If the stars align your NSX-T Manager appliance should deploy successfully. Once its deployed, if you were to re-run the plan you should see a message similar to below

One of the key pieces to this is the lifecycle block in the plan. The lifecycle block enables you to callout things that Terraform should ignore when it is re-applying a plan. Things like tags or other items that may get updated by other systems etc. In our case we want Terraform to ignore the vApp properties as it will try to apply password properties every time, which would entail powering down the VM, making the change, and powering the VM back on.

lifecycle { ignore_changes = [ 
#vapp # Enable this to ignore all vapp properties if the plan is re-run 
vapp[0].properties["nsx_role"], # Avoid unwanted changes to specific vApp properties. 
vapp[0].properties["nsx_passwd_0"], 
vapp[0].properties["nsx_cli_passwd_0"], 
vapp[0].properties["nsx_cli_audit_passwd_0"], 
host_system_id # Avoids moving the VM back to the host it was deployed to if DRS has relocated it 
] 
}

Hopefully this was useful. I’m sure there are more efficient ways of doing this. I will update the post if i find them. Keep a look out for the next instalment

Terraform Learnings: Getting Started

Playing with Terraform has been on my To-Do list for a while now (it’s a long list 🙂 ). Over the past couple of weeks i’ve been spending time in my homelab getting familiar with it and figured i’d create a blog series that may help others.

So where do you start? There are lots of resources on the web to get started. From blogs to Pluralsight courses. The Terraform documentation & provider documentation in the Terraform Registry is also very good and usually has what you need.

For my setup i use Visual Studio Code. I flip between my mac & a windows jump vm in my homelab, and VSC works seamlessly on both. I’ve installed the following VSC extension:

Installing Terraform is straightforward. Follow the steps for your OS to download and then install Terraform.

Terraform Basic Constructs

Terraform uses the following basic constructs (there are plenty more advanced constructs but baby steps!)

  • Providers
    • Plugins to interact with target endpoints
  • Variables
    • User input to create objects
    • There are multiple (6 i believe) ways to provide variables to Terraform
  • Data Sources
    • Sources of information outside of Terraform that provide infrastructure details to interact with resources
  • Resources
    • Infrastructure objects you interact with
  • Configuration files
    • .tf file extension
    • Read alphabetically and actioned when you plan/apply/destroy your config (more on that later)
    • A single main.tf file can contain everything your infrastructure plan requires:
      • Provider
      • Variables
      • Data Sources
      • Resources
    • Recommended to split these out for larger environments
      • providers.tf
        • You must declare required_providers and then a provider block for each provider.
        • You can use alias = “alias_name” if you want to have multiple instances of a provider.
        • In the screenshot below the credentials are coming from variables defined in my terraform.tfvars file
  • variables.tf
    • List of variables to be used in the configuration
    • Written in Hashicorp Configuration Language (HCL) (or JSON)
  • Sensitive variables such as credentials or access keys should be stored in Terraform variable definition files .tfvars or stored as environment variables.
    • Use a Terraform.gitignore file to ensure your .tfvars with sensitive information are not committed to your git repo.
  • Data Sources & Resources can be in a single file or split out into logical infrastructure files
    • network.tf
    • deploy_vm.tf
    • etc

Terraform Commands

Once you have your configuration defined you first want to validate that it will run

terraform plan -out=plan-name
# This will evaluate your configuration to ensure it is valid and store the result in a file called "plan-name"
terraform apply plan-name
# This will apply your configuration based on the output of the above plan. You will be asked to confirm this action. you can add -auto-approve to skip the confirmation (use with caution)
terraform destroy
# This will destroy the configuration. You will be asked to confirm this action. you can add -auto-approve to skip the confirmation (use with caution)

Hopefully this was helpful. This is just scratching the surface to get started with Terraform. I recommend getting hands on and reading the documentation as you go. I will continue this with a post on using the vSphere provider to deploy an OVA. Stay tuned!