Provision servers with Terraform on vSphere

· by Christoph Stoettner · Read in about 6 min · (1154 words)

My last article showed how to build a server template with Packer.

Now we want to use this template to create some servers on VMware vSphere. DNS will be registered manually and all IP addresses will be defined as fixed in the config files.

This code is tested with Terraform 0.12.

tl;dr

Terraform provides a good way to implement immutable server life cycle. Immutable means that servers aren’t changed, they get destroyed/deleted and created again when you change something.

Immutable servers have a big advantage that configuration drift will not take place. I like the fact that you can trust that servers act equally when you use the same configuration over and over again.

So we can deploy test and production, deploy patches and still be sure that tests can be trusted.

With mutable deployments, it can happen, that servers act differently. Like deploying fix1, fix2 and fix3 on a test server, but in production, you deploy just fix3 (which is cumulative and have all fixes included from 1-2). Normally the servers should act equally, but often we see differences during production deployments.

I think the time of manually deployed systems is over. The amount of time for installing, documenting and patching is just too high.

automationfear
Figure 1. Automation Fear

Most of the time we have fear running automated tasks on our servers because we’re unsure that there were changes applied, which are not covered in our scripts. You need to see the advantages:

  • Documentation automated (most of the time our definition files are documentation enough)

  • Reliable server deployments

  • Automated tests can help to deploy patches and updates to all and not only a few systems

Data should be stored on separate disks or storage systems, then it is independent of your servers.

Often containerization is shown as the solution for immutable deployments. That’s right and I love working with them, but how do you deploy servers running Kubernetes or Docker? I prefer generating virtual machines (automated) instead of using very large containers. So when I can’t limit a container to a single micro service, I create a virtual machine most of the time.

Terraform installation

Just download the zipped binary from https://www.terraform.io/downloads.html and put it into your PATH.

Download needed plugins

The binary checks all files in your current folder and then download the needed plugins for your deployment.

Just run
terraform init

Build the first server with Terraform

build.tf
provider "vsphere" {
  user                 = var.vsphere_user
  password             = var.vsphere_password
  vsphere_server       = var.vsphere_server
  allow_unverified_ssl = true
}

variable "project_folder" {
  default = "stoeps-example"
}

data "vsphere_datacenter" "dc" {
  name = var.vsphere_datacenter
}

data "vsphere_datastore" "datastore" {
  name          = var.vsphere_datastore
  datacenter_id = data.vsphere_datacenter.dc.id
}

data "vsphere_compute_cluster" "cluster" {
  name          = var.vsphere_cluster
  datacenter_id = data.vsphere_datacenter.dc.id
}

data "vsphere_resource_pool" "pool" {
  name          = var.vsphere_resource_pool
  datacenter_id = data.vsphere_datacenter.dc.id
}

data "vsphere_network" "network" {
  name          = var.vsphere_network
  datacenter_id = data.vsphere_datacenter.dc.id
}

data "vsphere_virtual_machine" "template" {
  # Name of Template
  name          = var.template
  datacenter_id = data.vsphere_datacenter.dc.id
}

resource "vsphere_folder" "folder" {
  path          = "${var.my_devops_folder}/${var.project_folder}"
  type          = "vm"
  datacenter_id = data.vsphere_datacenter.dc.id
}
vars.tf
variable "vsphere_server" {
  default = "khnum.example.com"
}

variable "vsphere_user" {
  default = "cstoettner@example.com"
}

variable "vsphere_password" {
  description = "vsphere server password for the environment"
  default     = ""
}

variable "vsphere_datacenter" {
  default = "HVIE"
}

variable "vsphere_cluster" {
  default = "HVIE PWR HOSTS"
}

variable "vsphere_datastore" {
  default = "devops-01_sas_7.2k_raid10"
}

variable "vsphere_network" {
  default = "vm-net-devops"
}

variable "vsphere_resource_pool" {
  default = "rp_hvie_devops"
}

variable "my_devops_folder" {
  default = "devops"
}

variable "vsphere_domain" {
  default = "devops.example.com"
}

variable "vsphere_dns_servers" {
  type    = list(string)
  default = ["10.10.85.5"]
}

variable "admin_user" {    (1)
  default = "root"
}

variable "admin_password" {(2)
  default = "password"
}

variable "template" {
  default = "centos-76-base"
}

variable "ssh-pub-key" {
  # Do not store with your code in git, provide on the commandline!
  default = ""
}
1 User for first SSH login
2 Password for SSH

During provisioning, I run some shell commands to disable the password login for root and putting an SSH Key into /root/.ssh/authorized_keys.

server1.tf
resource "vsphere_virtual_machine" "example-server1" {
  name             = "example-server1"
  resource_pool_id = data.vsphere_resource_pool.pool.id
  datastore_id     = data.vsphere_datastore.datastore.id
  num_cpus         = 4
  memory           = 4096
  guest_id         = data.vsphere_virtual_machine.template.guest_id
  scsi_type        = data.vsphere_virtual_machine.template.scsi_type
  network_interface {
    network_id   = data.vsphere_network.network.id
    adapter_type = data.vsphere_virtual_machine.template.network_interface_types[0]
  }
  folder = "${var.my_devops_folder}/${var.project_folder}"
  disk {
    label            = "disk0"
    size             = 100
    eagerly_scrub    = "false"
    thin_provisioned = "true"
  }
  clone {
    template_uuid = data.vsphere_virtual_machine.template.id
    customize {
      linux_options {
        host_name = "example-server1"
        domain    = var.vsphere_domain
      }
      network_interface {
        ipv4_address = "10.10.85.95"
        ipv4_netmask = 24
      }
      ipv4_gateway    = "10.10.85.1"
      dns_server_list = var.vsphere_dns_servers
      dns_suffix_list = [var.vsphere_domain]
    }
  }
  provisioner "remote-exec" {
    inline = [
      "systemd-machine-id-setup",
      "mkdir /root/.ssh",
      "touch /root/.ssh/authorized_keys",
      "echo ${var.ssh-pub-key} >> /root/.ssh/authorized_keys",
      "chown root:root -R /root/.ssh",
      "chmod 700 /root/.ssh",
      "chmod 600 /root/.ssh/authorized_keys",
      "passwd --lock root"          (1)
    ]
  }
  connection {
    host     = "${self.default_ip_address}"
    type     = "ssh"
    user     = "root"
    password = "${var.admin_password}"
  }
}
1 Lock user root, so only key authentication is possible

When you want to create multiple servers, you can just duplicate the definition file of server1 and change name and IP addresses, or you use loops and counters.

Now let’s build the first deployment plan:
terraform plan -var "vsphere_password=my-funky-password" \        (1)
               -var "template=stoeps-centos-20190604" \           (2)
               -var "ssh-pub-key=$(cat ~/.ssh/stoeps_rsa.pub)" \  (3)
               -out server1.terraform                             (4)
1 set your password for vSphere here
2 template name which should be used for provisioning
3 cat the ssh public key to the variable ssh-pub-key
4 write a deployment file

Providing the password and key on the command line has a big advantage because then your password isn’t stored in your version control system. On the other side, it will appear in shell history. Bash doesn’t store commands starting with a space into the history, or you set a temporary environment variable to store the password there. The SSH Key will be read from the public key file directly.

Terraform will show you all resources and information which will be created, changed or destroyed. Don’t forget that Terraform will work with all files of the current folder.

Changing a variable can trigger a recreation of the whole environment of the current folder, so check carefully what will happen on applying.

Apply the change
terraform apply server1.terraform
Terraform plan and apply

Recreate a single server

Sometimes destroying or changing all servers is too much. With Terraform you have the option to taint a server, a tainted server will be deleted and created again on the next terraform plan and terraform apply.

Mark server for recreation (taint)
terraform taint vsphere_virtual_machine.example-server2   (1)
1 The complete name is needed here

Destroy servers

When you want to delete all servers, you can run

Destroy servers
terraform destroy -var "vsphere_password=`echo $TF`" -var "template=stoeps-centos-20190604" -var "ssh-pub-key=$(cat ~/.ssh/stoeps_rsa.pub)"

Next Ansible

Next step will be further provisioning with Ansible. This can run separately, or as a post-provisioning task from Terraform.