Deploying and Managing Servers in VSphere with Terraform and Ansible

So often in managing infrastructure, we have to make due with what we have, when we have it. I recently came to find myself in a position where my team had access to plentiful resources on-prem (:confetti_ball:), but due to previous management practices, were left without an efficient means of managing them (:neutral_face:).

This post is a mockup of what’s involved in bringing IaC and remote configuration management to vSphere, using a trial instance of vSphere 7 running on spare kit. It assumes the reader has a degree of comfort with creating virtual machine templates via the GUI, and also asks you to consider not doing that again.

You can start by setting a couple VM templates in the vSphere GUI. For instructions on how to do so, see below:

Windows Server:

https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/manage/hybrid/server/best-practices/vmware-windows-template

RHEL 7:

https://blog.inkubate.io/create-a-rhel-7-terraform-template-for-vmware-vsphere/

Now that that’s done, we’ll start setting up a main.tf file using the latest instructions from the Terraform registry: https://registry.terraform.io/providers/hashicorp/vsphere/latest/docs

Deploying IaC with Terraform

main.tf

terraform {
    required_providers {
        vsphere = {
            source  = "hashicorp/vsphere"
            version = "2.1.1"
        }
    }
}

provider "vsphere" {
    user                 = var.username
    password             = var.password
    vsphere_server       = var.vcenter
    # If you have a self-signed cert, set to true
    allow_unverified_ssl = true
}

Having referenced several (non-existent) variables, let’s create a variables.tf file as well:

variables.tf

# vSphere configuration variables

variable "username" {
    default = "user@domain.com"
}

variable "password" {
    default = "secure_password"
}

variable "vcenter" {
    default = "vcs01.dev.local"
}

If you were to to ‘terraform init’ –> ‘terraform plan’ now in your CLI now, Terraform would congratulate you for doing pretty much nothing. Therefore, we will need to modify our main.tf and variables.tf files to include our two virtual machine templates:

main.tf

terraform {
    required_providers {
        vsphere = {
            source  = "hashicorp/vsphere"
            version = "2.1.1"
        }
    }
}

provider "vsphere" {
    user                 = var.username
    password             = var.password
    vsphere_server       = var.vcenter
    # If you have a self-signed cert, set to true
    allow_unverified_ssl = true
}

# virtual machine modules

module "ws2019-example" {
    source            = "your-templates/windows"
    version           = "0.9.2"
    vmtemp            = "WindowsTemplateName"
    instances         = 1
    vmname            = "example-server-windows"
    vmrp              = "esxi/Resources"  
    vlan              = var.vlan
    data_disk         = "true"
    data_disk_size_gb = 20
    is_windows_image  = "true"
    dc                = var.dc
    ds_cluster        = var.dsc
    ipaddress         = ["10.19.128.47"]
    vmdns             = ["0.0.0.0", "8.8.8.8"]
}

module "rhel07-example" {
    source            = "your-templates/linux"
    version           = "0.9.2"
    vmtemp            = "LinuxTemplateName"
    instances         = 1
    vmname            = "example-server-linux"
    vmrp              = "esxi/Resources"  
    vlan              = var.vlan
    data_disk         = "true"
    data_disk_size_gb = 20
    is_windows_image  = "true"
    dc                = var.dc
    ds_cluster        = var.dsc
    ipaddress         = ["10.19.128.49"]
    vmdns             = ["0.0.0.0", "8.8.8.8"]
}

Rather than hard-coding certain important configuration settings, we have made our deployment code more extensible by abstracting those values to external variables. Having once again referenced several (non-existent) values, we’ll need to add more variables to our variables.tf file:

variables.tf

# vSphere configuration variables

variable "username" {
    default = "user@domain.com"
}

variable "password" {
    default = "secure_password"
}

variable "vcenter" {
    default = "vcs01.dev.local"
}

# virtual machine configuration variables

variable "vlan" {
    default = "Your VLAN name"
}
variable "dc" {
    default = "Datacenter"
}

variable "dsc" {
    default = "Data Store Cluster name"
}

You should now be able to run ‘terraform plan’, and see that two virtual machine resources will be added, with the addition of their dependent resources if they do not already exist. If so, type ‘terraform apply’ in your CLI and watch it run.

Configuration Management with Ansible

Now that we’ve got our infrastructure up and running, we can start managing it using Ansible.

To do so, we can connect to an Ansible host, access the hosts file most commonly found at /etc/ansible/hosts, and open it in our favourite text editor, and add the key-value pair to the bottom:

[virtual-machines]

10.19.128.47
10.19.128.49

[virtual-machines:vars]

ansible_user=ansible
ansible_password=secure-password
#optional, default is 22 for SSH
ansible_port = 8181

For an exhaustive list of items you can add, see here: https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html

With the hosts file configured, let’s create some playbooks for deploying the AWS CLI package to our servers. First, let’s create a new YAML file for our variables.

vars.yml

---
- aws_system_user: ansible
- aws_profile: default
- aws_access_key: "<aws-access-key>"
- aws_secret_key: "<aws-secret-key>"
- aws_region: us-east-1
- aws_format: table

Next, we can create another YAML file for the tasks that we want to carry out, which will be installing and configuring the AWS CLI.

Note that we are using shell commands here to configure the installed package(s), but not to install them. This is intentional, as package installation should be run by package managers (e.g., yum, apt, brew, winget) when available. In this case, we used the Ansible get_url module.

tasks.yml

---
- name: Install package dependencies for the awscli bundle.
  package: name= state=present
  with_items:
    - python
    - unzip

- name: Download the awscli bundle.
  get_url: url=https://s3.amazonaws.com/aws-cli/awscli-bundle.zip dest=/tmp/awscli-bundle.zip
  register: aws_cli_download_bundle

- name: Unarchive the installer.
  unarchive: src=/tmp/awscli-bundle.zip dest=/tmp copy=no creates=/tmp/awscli-bundle
  when: aws_cli_download_bundle.changed
  register: aws_cli_unarchive_installer

- name: Install awscli package.
  shell: python /tmp/awscli-bundle/install -i /usr/local/aws -b /usr/bin/aws
  args:
    creates: /usr/bin/aws
  when: aws_cli_unarchive_installer.changed

- name: Configure AWS.
  shell: aws configure set   --profile 
  no_log: True
  with_dict:
    aws_access_key_id: ""
    aws_secret_access_key: ""
    region: ""
    format: ""
  become_user: ""
  changed_when: false

With that done, we can create one last playbook to execute the tasks, using the variables defined.

add-aws.yaml

---
- hosts: virtual-machines
  vars_files:
    - vars.yml
  tasks:
    - include: tasks.yml

Finally, we can run the playbook by executing the following command in our CLI:

ansible-playbook playbook.yml --check