In Part 1 of this blog, we talked about Terraform and Ansible – their respective strengths/weaknesses/purposes, and how they can be used together. In this blog we’ll dive into how you combined both Ansible and Terraform in this specific use case, with a look at sample code.

Terraform Deploys and Provisions Virtual Machines in vSphere

One of Terraform’s most important constructs is that of resources. The resource code blocks describe one or more infrastructure objects such as network, compute instances, storage configuration, and so on. For our example, we use Terraform to deploy and provision five virtual machines in vSphere with these attributes:

  • The virtual machines are clones of a VM template (VM is running CentOS 8.2)
  • Static IP addresses assigned to each machine with a variable that increments a base address by one
  • A unique hostname is assigned to each machine with a hostname postfix number that is incremented by one
  • A provisioner adds a user’s SSH key to each host (the user account is baked into the VM template)
  • Store sensitive credentials in a separate file that is not checked into Git (included in .gitignore)
  • The user account is added to the VM template ahead of time.
  • The vsphere_virtual_machine resource includes a provisioner to run an Ansible playbook to install an Apache web server

Show me the code!

The code is posted to DevNet Automation Exchange. For brevity, I show the relevant code section below:

resource "vsphere_virtual_machine" "vm1" {
  count            = 5
  name             = "apache-web-server-${count.index + 1}"
  resource_pool_id = "${data.vsphere_compute_cluster.cluster.resource_pool_id}"
  datastore_id     = "${data.vsphere_datastore.datastore.id}"
  firmware         = "${var.vsphere_vm_firmware}"
  num_cpus = 2
  memory   = 8096
  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]}"
  disk {
    label            = "disk0"
    size             = "${data.vsphere_virtual_machine.template.disks.0.size}"
    eagerly_scrub    = true
    thin_provisioned = "${data.vsphere_virtual_machine.template.disks.0.thin_provisioned}"
  provisioner "remote-exec"  {
    inline = [
    "mkdir /home/delgadm/.ssh",
    "chmod 700 /home/delgadm/.ssh",
    "touch /home/delgadm/.ssh/authorized_keys",
    "chmod 600 /home/delgadm/.ssh/authorized_keys",
    "echo ${var.mel-ssh-pub-key} >> /home/delgadm/.ssh/authorized_keys"
    connection {
    type     = "ssh"
    user     = "${var.mel_delgado_username}"
    password = "${var.mel_delgado_password}"
    host     = "10.200.0.${101 + count.index}"
  provisioner "local-exec" {
   command = "ansible-playbook -u delgadm -i apache-web-servers.txt main.yml"
  clone {
    template_uuid = "${data.vsphere_virtual_machine.template.id}"
    customize {
      linux_options {
        host_name = "apache-webserver-${count.index + 1}"
        domain    = "test.internal"
      network_interface {
        ipv4_address = "10.200.0.${101 + count.index}"
        ipv4_netmask = 24
      ipv4_gateway = ""

Once the code is complete, Terraform first needs to pull in the necessary resources with the terraform init -var-file=”secret.tfvars” command. Secret.tfvars is where sensitive information such as credentials are stored but not checked into Git. It’s necessary to include at the command line. If you don’t include it, you are prompted for the variable values each time you run a Terraform command against the infrastructure that needs these credentials.

Terraform then creates an execution plan, or what is colloquially called a Terraform Plan, with the command terraform plan -out apache-web-servers.tfplan -var-file=”secret.tfvars”. In this example, the resulting file is named apache-web-servers.tfplan along with a list of actions printed to the command line. These explain what actions Terraform is about to take on the target infrastructure.

The next step is to apply the Terraform apply with the command terraform apply -var-file=”secret.tfvars” and confirm the deployment. Once completed, the resulting five virtual machines have Apache installed with port 80 opened on the firewall of each machine. Terraform deployed the virtual machine according to the list above. Ansible did the rest of the work described in the next section.

Ansible Installs and Configures an Apache Web Server

Ansible lets you group your content into a reusable construct known as a role. Within a role, related tasks, variables, handlers, and other Ansible artifacts are stored in a known file structure. The Ansible roles perform the following tasks:

  • Configure the virtual machine’s DNS settings in /etc/resolv.conf
  • Open port 80 on the firewall
  • Install Apache
  • Add a unique index.html to each host that displays the hostname using a template with a Jinja2 variable
  • Inventory is maintained in a separate file named apache-web-servers.txt
- name: Install and manage one or more Apache Web Server(s)
  hosts: all
  become: yes
    - name: Include group variables
        file: variables.yml
    - name: Configure /etc/resolv.conf
        name: resolv.conf
        - configure_resolv_conf
    - name: Open port 80 as needed for the Apache Web Server
        name: firewalld
    - name: Install Apache
        name: apache-web-server
        - install_apache

Full code example in DevNet Automation Exchange

The full code example posted on DevNet Automation Exchange expands on each role along with the implementation details.

The Terraform example in the previous section calls Ansible with the command ansible-playbook -u delgadm -i apache-web-servers.txt main.yml.  –u indicates the user, and -i is the inventory parameter followed by apache-web-servers.txt. That’s the file containing the hosts and their IP addresses in this deployment.

After Ansible runs, the result is a running Apache web server with a custom HTML file for each machine that displays the hostname.

Combining Both Ansible and Terraform

What if you would like to change the HTML file? In this example, you could change the HTML file and rerun the ansible playbook. That’s a mutable approach to managing infrastructure and applications. Admittedly, HTML files (or any other persistent data) could be externally stored on persistent media. This simple use case, however, shows what could otherwise be a configuration file for an application. Change the configuration file, restart the process (if necessary), and the service is available quickly. No need to take time to redeploy the VM on which the process runs.

Alternatively, you could rerun the terraform apply command. This will destroy the infrastructure it deployed, redeploy five virtual machines, and re-run the Ansible playbook on the newly deployed machines. That’s an immutable approach since the state of the machine is well known and the opportunities for configuration drift are removed.

You do incur a time lag while the new machines are destroyed and built. However, you also have the option to make a quick change by running the Ansible playbook directly. Lastly, if your automation is already established with Ansible, you could keep what is already built and add Terraform for provisioning the infrastructure (by deploying virtual machines in this example). You could also slowly transition from using a mutable paradigm to an immutable approach by slowly transitioning configuration management tasks built in Ansible to Terraform.

Related resources

  • Visit the DevNet Networking Dev Center to find use cases, resources, and getting started.
  • DevNet Create 2021: Two days of learning and co-creation with apps and infrastructure. Register now for this free, global, virtual event.


We’d love to hear what you think. Ask a question or leave a comment below.
And stay connected with Cisco DevNet on social!

Twitter | Facebook | LinkedIn

Visit the new Developer Video Channel


Mel Delgado

Developer Advocate

Cisco DevNet - Data Center Compute