5 min read

Deploy any vSphere virtual machine with a startup script (with Terraform)

Deploy any vSphere virtual machine with a startup script (with Terraform)
Join me on deploying any vSphere virtual machine (with dynamic or static IP address), and provide it a startup script that will run at creation!

A couple weeks ago I wrote this blogpost, in which I described the deployment of a standalone Concourse CI server, as well as providing the code. I was so satisfied with the result, that I started working into doing the same but for deploying a MinIO (cloud-native storage) server. After a few days of adding new code to the original, I had it working. But then I thought, there are a few more services which I will want to deploy as standalone VMs, but what sense does it make to keep on manually adding Terraform resources? Can’t this be further automated? The answer, luckily, is yes!

After looking into Terraform’s conditional statements and for_each loops, I decided to modify the script in a way that only adding an extra variable and its corresponding startup shell script will be enough to deploy as many different virtual machines with the same Terraform code. Check out the updated repo.

Understanding the script

There are two main resources being created in this script: “dhcp-vms” and “static-vms”. Each of these variables can contain a colleciton of virtual machines to be deployed (with a dynamic IP / static IP). This is an example of the variables:

dhcp-vms = {
  example = {                              # Deploy example VM (will use /setup-scripts/example-setup.sh as startup script)
    disk : 100,                            # The VM's disk storage in GB
    cpu : 2,                               # The VM's number of vCPUs
    memory : 4000,                         # The VM's memory in MB
    environment-variables = {              # Environment variables to be passed to your VM (at ~/.env)
      environment_variable1 = "dummy"      # DNS domain to be used by the Concourse service
      environment_variable2 = "dummy2"     # Root username to be used by the Concourse service
    }
  }
}

static-vms = {
  example2 = {                             # Deploy example VM (will use /setup-scripts/example2-setup.sh as startup script)
    disk : 100,                            # The VM's disk storage in GB
    cpu : 2,                               # The VM's number of vCPUs
    memory : 4000,                         # The VM's memory in MB
    ip_address : "10.0.0.4"                # The static IP address for this VM
    environment-variables = {              # Environment variables to be passed to your VM (at ~/.env)
      environment_variable1 = "dummy"      # DNS domain to be used by the Concourse service
      environment_variable2 = "dummy2"     # Root username to be used by the Concourse service
    }
  }
}

For each virtual machine it is possible to assign it:

  1. A hostname
  2. Disk size (in GB)
  3. Amount of CPUs
  4. Memory (in MB)
  5. IP address (for static-vms)
  6. Extra environment variables (to be passed to the VM at ~/.env, which can be used by the startup script)

Passing custom environment variables to the VMs

In order for the startup script to use the custom environment variables (provided in “YOUR_VM.environment-variables”), they have to be written in a file. This is achieved by adding the following to the remote exec (look at the line under the comment).

  provisioner "remote-exec" {
    inline = [
      "echo ${self.default_ip_address} ${each.key} | sudo tee -a /etc/hosts",
      "sudo apt update && sudo apt install -y jq & sudo snap install yq",
      # The following line passes the environment variables in json format and turns them into a format that the startup script can use
      "echo '${jsonencode(each.value.environment-variables[*])}'  |  sed 's/^.//;s/.$//' | yq -P '.'  | sed 's/:/=/' | sed -e 's/[\t ]//g;/^$/d' > .env",
      "sed -i -e 's/\r$//' /home/ubuntu/setup.sh",
      "chmod +x /home/ubuntu/setup.sh",
      "sh /home/ubuntu/setup.sh",
      "rm /home/ubuntu/setup.sh && rm /home/ubuntu/snap/ -rf",
      "echo ${self.default_ip_address}"
    ]
    on_failure = continue
  }

Using the environment variables in the startup script

Using the passed environment variables is simple. All you need to do is begin your startup script with the following commands:

#!/bin/bash
. /home/ubuntu/.env # Load environment variables

Deploying Concourse CI + MinIO

Two shell scripts are included by default with this repo, one to deploy Concourse CI and another one for MinIO (cloud-native storage). Let’s create both of them as static VMs

  1. Clone the repo
git clone https://github.com/mestredelpino/standalone-vms.git
  1. Create a file "terraform.tvars" with the following content:
vsphere-user           = ""                         # The username of the vSphere user to be used for this deployment
vsphere-password       = ""                         # The password of the vSphere user to be used for this deployment
vsphere-server         = ""                         # The vSphere server (IP address or FQDN)
vsphere-datacenter     = ""                         # The vSphere Datacenter you will deploy this virtual machine to
vsphere-datastore      = ""                         # The datastore you will deploy this virtual machine to
vsphere-resource_pool  = ""                         # The resource pool to be used by this virtual machine
vsphere-host           = ""                         # The ESXi host you will deploy this virtual machine to
vsphere-network        = ""                         # The network segment to be used by this virtual machine
vsphere-network-cidr   = ""                         # The CIDR of the "vsphere-network"
vsphere-vm-folder      = ""                         # The name of the vSphere folder which will contain the deployed virtual machine(s)

focal-cloudserver-name = ""                         # The name for the ubuntu-server template VM (default is ubuntu-server-template)
static-vms = {
  minio = {                                         # Deploy MinIO (cloud-native storage, will use setup-scripts/minio-setup.sh as startup script)
    disk : 100,                                     # The VM's disk storage in GB
    cpu : 2,                                        # The VM's number of vCPUs
    memory : 4000,                                  # The VM's memory in MB
    ip_address : "10.0.0.3"                         # The static IP address for this VM
    environment-variables = {                       # Environment variables to be passed to your VM (at ~/.env)
      service_domain           = "yourdomain.com"   # DNS domain to be used by the MinIO service
      service_root             = "admin"            # Root username to be used by the MinIO service
      service_root_password    = "password123"      # Root username to be used by the MinIO service
    }
  },
  concourse = {                                     # Deploy Concourse CI (CI tool, will use setup-scripts/concourse-setup.sh as startup script)
    disk : 100,                                     # The VM's disk storage in GB
    cpu : 2,                                        # The VM's number of vCPUs
    memory : 4000,                                  # The VM's memory in MB
    ip_address : "10.0.0.4"                         # The static IP address for this VM
    environment-variables = {                       # Environment variables to be passed to your VM (at ~/.env)
      service_domain           = "yourdomain.com"   # DNS domain to be used by the Concourse service
      service_root             = "admin"            # Root username to be used by the Concourse service
      service_root_password    = "password123"      # Root username to be used by the Concourse service
    }
  }
}
  1. The setup files (minio-setup.sh and concourse-setup.sh) are already in the "setup-scripts/" directory, so they do not need to be added in this case.
  2. In order to execute the script, open a terminal and run:
terraform init
terraform apply

And that’s about it! This is the way I have found out works for passing variables to the VMs’ operating system. Do you know any better way? Please let me know how you do it in the comments below.

Click here to view the full repo.