Generating/Executing Terraform Plans Using Ansible

14 minute read

Recently I have been working on a little project of my own based on provisioning a vSphere environment using Ansible as the primary automation tool. My intention of this project was to use IaC as the main driver.

NOTE: You can view this project on GitHub. ansible-vsphere-management.

As part of this project involved provisioning numerous Core Services VMs to provide services such as:

  • DNS
  • DHCP
  • IPAM
  • Active Directory (Samba based AD)

The above Core Services are provisioned using Ansible to drive PowerCLI scripts for the most part. I took this route because I found that the vSphere Ansible modules were not flexible enough for my needs. However, once the Core Services were deployed. I wanted to take an easier approach to provisioning additional VMs and etc. This is where Terraform comes into play. Terraform is a great tool for defining your infrastructure as code, provisioning the infrastructure and also provides the ability to tear it all down. However, being that this project is based on IaC, I needed a way to leverage variable definitions already present in the project to also drive the Terraform provisioning. So how did I accomplish this? Quite easily actually. I took the foundation of what Terraform files are required to define the infrastructure that I wanted to provision and used Ansible to generate those configuration files by using Jinja2 templates. By taking this approach it has proven to be a very dynamic way to build out my infrastucture. With all of this being said, I will be outlining with examples on how I was able to accomplish all of this. Hopefully, this post will be beneficial to others as well.

Terraform Variables

Leveraging Existing group_vars To Define Terraform Variables

Here we will be defining our Terraform variables.tf file. Because all of the variables are already defined throughout the project we can leverage those and use a Jinja2 template to generate our variables.tf file.

So for example, in our group_vars/all/all.yml Ansible variables we have some of the following defined:

vsphere_dns_servers:
  - "{{ vsphere_dnsdist_vm_ips[0] }}"
  - "{{ vsphere_dnsdist_vm_ips[1] }}"

vsphere_dvswitches:
  - name: dvSwitch0
    active_uplinks:
      - dvUplink1
      - dvUplink2
      - dvUplink3
      - dvUplink4
    datacenter: "{{ vsphere_vcenter_datacenter['name'] }}"
    discovery_protocol:
      status: enabled
      type: LLDP
      operation: both
    hosts: "{{ groups['vsphere_hosts'] }}"
    load_balancing_policy: LoadBalanceSrcId
    mtu: 1500
    nics:
      - vmnic2
      - vmnic3
      - vmnic4
      - vmnic5
    port_groups:
      - name: VDS-VLAN-101
        num_ports: 128
        state: present
        type: earlyBinding
        vlan_id: 101
      - name: VDS-VLAN-102
        num_ports: 128
        state: present
        type: earlyBinding
        vlan_id: 102
      - name: VDS-VLAN-201
        num_ports: 128
        state: present
        type: earlyBinding
        vlan_id: 201
    standby_uplinks:
      []
      # - dvUplink3
      # - dvUplink4
    state: present
    unused_uplinks:
      []
      # - dvUplink4
    uplink_ports: 4

vsphere_linux_vapp_template_name: ubuntu-16.04-template

pdns_api_vip: "{{ vsphere_lb_vips[0] }}"

pdns_webserver_port: 8081

vsphere_pri_domain_name: lab.etsbv.internal

Terraform VMs

We also have our terraform_vms defined as below which is what we will be using to also generate our VM naming and etc. within our variables.tf file. The terraform_vms definition will also be used for Terraform VM Resources.

group_vars/all/terraform_vms.yml:

---
# If defining addl_disk, size is in GB
terraform_vms:
  - group: docker_lbs
    count: 2
    inventory_parent_folder: Docker
    memory_mb: 512
    naming: docker-lb
    vcpu: 1
  - group: docker_storage
    addl_disk:
      - size: 100
    count: 2
    inventory_parent_folder: Docker
    memory_mb: 512
    naming: docker-storage
    vcpu: 1
  - group: docker_swarm_managers
    count: 3
    inventory_parent_folder: Docker
    memory_mb: 1024
    naming: docker-mgr
    vcpu: 1
  - group: docker_swarm_workers
    count: 4
    inventory_parent_folder: Docker
    memory_mb: 4096
    naming: docker-wrk
    vcpu: 1

Now let’s break down the terraform_vms variable a bit to understand what is going on here and what is being defined.

group: Defines the Ansible group as well as the Terraform vsphere_virtual_machine resource name which we will touch on further down when we get to Terraform VM Resources.

count: Defines the number of VMs which should be generated for each group.

inventory_parent_folder: Defines the vCenter folder where the VMs will be located when provisioned.

memory_mb: Defines the amount of memory in MB to allocate each VM within the group.

naming: Defines the VM naming scheme to use when generating the VMs.

vcpu: Defines the number of CPUs to allocate to each VM within the group.

Terraform Variables Jinja2 Template

The terraform_variables.tf.j2 template below is what we will use for generating our variables.tf file based on the variables defined in Leveraging Existing group_vars To Define Terraform Variables:

variable "dns_servers" {
  default = [
{% for dns_server in vsphere_dns_servers %}
    "{{ dns_server }}",
{% endfor %}
  ]
}
variable "dvSwitches" {
  default = [
    "{{ vsphere_dvswitches[0]['name'] }}",
  ]
}
variable "esxi_hosts" {
  default = [
  {% for host in groups['vsphere_hosts'] %}
    "{{ host }}",
  {% endfor %}
  ]
}
variable "linux_vm_template" {
  default = "{{ vsphere_linux_vapp_template_name }}"
}
variable "pdns_api_key" {}
variable "pdns_server_url" {
default = "http://{{ pdns_api_vip }}:{{ pdns_webserver_port }}/api/v1"
}
variable "pri_domain_name" {
  default = "{{ vsphere_pri_domain_name }}"
}
{% for vms in terraform_vms %}
variable "vms_{{ vms['group'] }}" {
  default = [
  {%   for vm in range(vms['count']) %}
  {%     if loop.index < 10 %}
    "{{ vms['naming'] }}-0{{ loop.index }}.{{ vsphere_pri_domain_name }}",
  {%     elif loop.index >= 10 %}
    "{{ vms['naming'] }}-{{ loop.index }}.{{ vsphere_pri_domain_name }}",
  {%     endif %}
  {%   endfor %}
  ]
}
{% endfor %}
variable "vsphere_datacenter" {
  default = "{{ vsphere_vcenter_datacenter['name'] }}"
}
variable "vsphere_datacenter_cluster" {
  default = "{{ vsphere_vcenter_cluster['name'] }}"
}
variable "vsphere_default_vm_datastore" {
  default = "{{ vsphere_vm_services_datastore }}"
}
variable "vsphere_default_vm_network" {
  default = "{{ vsphere_vm_services_vswitch }}"
}
variable "vsphere_host_license_key" {}
variable "vsphere_networks" {
  default = [
  {% for switch in vsphere_dvswitches %}
  {%   for net in switch['port_groups'] %}
    "{{ net['name'] }}",
  {%   endfor %}
  {% endfor %}
  ]
}
variable "vsphere_password" {}
variable "vsphere_server" {
  default = "{{ vsphere_vcsa_network_ip }}"
}
variable "vsphere_username" {}
variable "vsphere_vcenter_license_key" {}

Generated variables.tf

And if we were to generate our variables.tf based on the above variables and Jinja2 template it would look similar to below:

variable "dns_servers" {
  default = [
    "10.0.102.40",
    "10.0.102.41",
  ]
}
variable "dvSwitches" {
  default = [
    "dvSwitch0",
  ]
}
variable "esxi_hosts" {
  default = [
    "esxi-01.lab.etsbv.internal",
    "esxi-02.lab.etsbv.internal",
  ]
}
variable "linux_vm_template" {
  default = "ubuntu-16.04-template"
}
variable "pdns_api_key" {}
variable "pdns_server_url" {
  default = "http://10.0.102.100:8081/api/v1"
}
variable "pri_domain_name" {
  default = "lab.etsbv.internal"
}
variable "vms_docker_lbs" {
  default = [
    "docker-lb-01.lab.etsbv.internal",
    "docker-lb-02.lab.etsbv.internal",
  ]
}
variable "vms_docker_storage" {
  default = [
    "docker-storage-01.lab.etsbv.internal",
    "docker-storage-02.lab.etsbv.internal",
  ]
}
variable "vms_docker_swarm_managers" {
  default = [
    "docker-mgr-01.lab.etsbv.internal",
    "docker-mgr-02.lab.etsbv.internal",
    "docker-mgr-03.lab.etsbv.internal",
  ]
}
variable "vms_docker_swarm_workers" {
  default = [
    "docker-wrk-01.lab.etsbv.internal",
    "docker-wrk-02.lab.etsbv.internal",
    "docker-wrk-03.lab.etsbv.internal",
    "docker-wrk-04.lab.etsbv.internal",
  ]
}
variable "vsphere_datacenter" {
  default = "LAB"
}
variable "vsphere_datacenter_cluster" {
  default = "LAB-Cluster"
}
variable "vsphere_default_vm_datastore" {
  default = "vSphere_iSCSI_Datastore_1"
}
variable "vsphere_default_vm_network" {
  default = "VSS-VLAN-102"
}
variable "vsphere_host_license_key" {}
variable "vsphere_networks" {
  default = [
    "VDS-VLAN-101",
    "VDS-VLAN-102",
    "VDS-VLAN-201",
  ]
}
variable "vsphere_password" {}
variable "vsphere_server" {
  default = "10.0.102.60"
}
variable "vsphere_username" {}
variable "vsphere_vcenter_license_key" {}

So as you can see we easily leveraged existing Ansible defined variables along with our Jinja2 template to define our Terraform variables.tf file. And remember why this is important and useful. We now have a streamlined methodology to not only provision out our infrastructure using Ansible but also to define our Terraform provisioning consistently.

Terraform Secret Variables

We now also need to maintain some secret variables which we would never want to keep in our version control system. And again we can use a Jinja2 template to generate out these super secret variables as well. For example, the Jinja2 template below could be used to generate our terraform.tfvars config:

terraform.tfvars.j2:

pdns_api_key = "{{ pdns_webserver_password }}"
vsphere_host_license_key = ""
vsphere_password = "{{ vsphere_vcsa_sso_user_info['password'] }}"
vsphere_username = "{{ vsphere_vcsa_sso_user_info['username'] }}"
vsphere_vcenter_license_key = ""

Which would produce the following terraform.tfvars:

pdns_api_key = "changeme"
vsphere_host_license_key = ""
vsphere_password = "VMw@re1!"
vsphere_username = "[email protected]"
vsphere_vcenter_license_key = ""

Terraform Data Sources

We now will use a standard data_sources.tf file which will not be generated using Ansible. However, it will be configured to leverage our variables.tf file which we already generated. Below is an example of what our data_sources.tf might look like.

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

data "vsphere_host" "esxi_hosts" {
  count = "${length(var.esxi_hosts)}"
  name = "${var.esxi_hosts[count.index]}"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}

data "vsphere_distributed_virtual_switch" "dvs" {
  count = "${length(var.dvSwitches)}"
  name = "${var.dvSwitches[count.index]}"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}

data "vsphere_network" "net" {
  count = "${length(var.vsphere_networks)}"
  name = "${var.vsphere_networks[count.index]}"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}

As you can see from above, we are leveraging pre defined variables.

Terraform Providers

We are only focusing on the vsphere provider as part of this example so we actually will use a static vsphere_profider.tf config which would look something like:

provider "vsphere" {
  user           = "${var.vsphere_username}"
  password       = "${var.vsphere_password}"
  vsphere_server = "${var.vsphere_server}"

  # if you have a self-signed cert
  allow_unverified_ssl = true
}

Terraform Inventory Resources

For our Terraform inventory resources we will be using yet another Jinja2 template that will be generated for us. This file will define the vCenter folder(s) for our VMs to be defined within. If you remember in the Terraform VMs section we discussed what the inventory_parent_folder was defined for. Well here is the Jinja2 template that will define our inventory resources.

resource "vsphere_folder" "terraform_deployed" {
  path = "Terraform Deployed"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}

{% set _folders = [] %}
{% for folder in terraform_vms %}
{%   if folder['inventory_parent_folder'] is defined %}
{%     set _folders = _folders.append(folder['inventory_parent_folder']) %}
{%   endif %}
{% endfor %}
{% set folders = _folders|unique|list %}
{% for folder in folders %}
resource "vsphere_folder" "{{ folder }}" {
  path = "${vsphere_folder.terraform_deployed.path}/{{ folder }}"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}

{% endfor %}
{% for folder in terraform_vms %}
{%   if folder['inventory_parent_folder'] is not defined %}
resource "vsphere_folder" "{{ folder['group'] }}" {
  path = "${vsphere_folder.terraform_deployed.path}/{{ folder['group'] }}"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
{%   endif %}

{% endfor %}
{% for folder in terraform_vms %}
{%   if folder['inventory_parent_folder'] is defined %}
resource "vsphere_folder" "{{ folder['group'] }}" {
  path = "${vsphere_folder.{{ folder['inventory_parent_folder'] }}.path}/{{ folder['group'] }}"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
{%   endif %}
{% endfor %}

What we are doing above is first iterating all of our top level parent folders and ensuring that we do not have any duplicates, and then iterating through each child folder to define our complete inventory resources structure. And after this file is generated it would look like below:

resource "vsphere_folder" "terraform_deployed" {
  path = "Terraform Deployed"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}

resource "vsphere_folder" "Docker" {
  path = "${vsphere_folder.terraform_deployed.path}/Docker"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
resource "vsphere_folder" "docker_lbs" {
  path = "${vsphere_folder.Docker.path}/docker_lbs"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
resource "vsphere_folder" "docker_storage" {
  path = "${vsphere_folder.Docker.path}/docker_storage"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
resource "vsphere_folder" "docker_swarm_managers" {
  path = "${vsphere_folder.Docker.path}/docker_swarm_managers"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}
resource "vsphere_folder" "docker_swarm_workers" {
  path = "${vsphere_folder.Docker.path}/docker_swarm_workers"
  type = "vm"
  datacenter_id = "${data.vsphere_datacenter.dc.id}"
}

Terraform VM Resources

Now for our VM resources. We already touched on how the variables are defined for terraform_vms above in Terraform VMs. So now let’s see what this would like here now. Because those variables are already defined we can use another Jinja2 template to generate our vm_resources.tf config. The following is an example of what that Jinja2 template might look like:

terraform_vm_resources.tf.j2:

{% for vms in terraform_vms %}
resource "vsphere_virtual_machine" "{{ vms['group'] }}" {
  count = "${length(var.vms_{{ vms['group'] }})}"
  name = "${var.vms_{{ vms['group'] }}[count.index]}"
  datacenter = "${data.vsphere_datacenter.dc.name}"
  disk {
    datastore = "${var.vsphere_default_vm_datastore}"
    template = "${var.linux_vm_template}"
    type = "thin"
  }
{%   if vms['addl_disk'] is defined %}
{%     for addl_disk in vms['addl_disk'] %}
  disk {
      datastore = "${var.vsphere_default_vm_datastore}"
      size = "{{ addl_disk['size'] }}"
      name = "${var.vms_{{ vms['group'] }}[count.index]}_{{ loop.index }}"
      type = "thin"
    }
{%     endfor %}
{%   endif %}
  dns_servers = "${var.dns_servers}"
  domain = "${var.pri_domain_name}"
  folder = "${vsphere_folder.{{ vms['group'] }}.path}"
  memory = {{ vms['memory_mb'] }}
  network_interface {
    label = "${var.vsphere_default_vm_network}"
  }
  vcpu = {{ vms['vcpu'] }}
}
{% endfor %}

And if we were to run this through Ansible to generate our vm_resources.tf config it would look similar to below:

resource "vsphere_virtual_machine" "docker_lbs" {
  count = "${length(var.vms_docker_lbs)}"
  name = "${var.vms_docker_lbs[count.index]}"
  datacenter = "${data.vsphere_datacenter.dc.name}"
  disk {
    datastore = "${var.vsphere_default_vm_datastore}"
    template = "${var.linux_vm_template}"
    type = "thin"
  }
  dns_servers = "${var.dns_servers}"
  domain = "${var.pri_domain_name}"
  folder = "${vsphere_folder.docker_lbs.path}"
  memory = 512
  network_interface {
    label = "${var.vsphere_default_vm_network}"
  }
  vcpu = 1
}
resource "vsphere_virtual_machine" "docker_storage" {
  count = "${length(var.vms_docker_storage)}"
  name = "${var.vms_docker_storage[count.index]}"
  datacenter = "${data.vsphere_datacenter.dc.name}"
  disk {
    datastore = "${var.vsphere_default_vm_datastore}"
    template = "${var.linux_vm_template}"
    type = "thin"
  }
  disk {
    datastore = "${var.vsphere_default_vm_datastore}"
    size = "100"
    name = "${var.vms_docker_storage[count.index]}_1"
    type = "thin"
  }
  dns_servers = "${var.dns_servers}"
  domain = "${var.pri_domain_name}"
  folder = "${vsphere_folder.docker_storage.path}"
  memory = 512
  network_interface {
    label = "${var.vsphere_default_vm_network}"
  }
  vcpu = 1
}
resource "vsphere_virtual_machine" "docker_swarm_managers" {
  count = "${length(var.vms_docker_swarm_managers)}"
  name = "${var.vms_docker_swarm_managers[count.index]}"
  datacenter = "${data.vsphere_datacenter.dc.name}"
  disk {
    datastore = "${var.vsphere_default_vm_datastore}"
    template = "${var.linux_vm_template}"
    type = "thin"
  }
  dns_servers = "${var.dns_servers}"
  domain = "${var.pri_domain_name}"
  folder = "${vsphere_folder.docker_swarm_managers.path}"
  memory = 1024
  network_interface {
    label = "${var.vsphere_default_vm_network}"
  }
  vcpu = 1
}
resource "vsphere_virtual_machine" "docker_swarm_workers" {
  count = "${length(var.vms_docker_swarm_workers)}"
  name = "${var.vms_docker_swarm_workers[count.index]}"
  datacenter = "${data.vsphere_datacenter.dc.name}"
  disk {
    datastore = "${var.vsphere_default_vm_datastore}"
    template = "${var.linux_vm_template}"
    type = "thin"
  }
  dns_servers = "${var.dns_servers}"
  domain = "${var.pri_domain_name}"
  folder = "${vsphere_folder.docker_swarm_workers.path}"
  memory = 4096
  network_interface {
    label = "${var.vsphere_default_vm_network}"
  }
  vcpu = 1
}

And once again we can see how easy this has been to maintain consistency across our project.

Terraform Ansible Playbook

Below is an example of the Ansible playbook which we will use to put everything together in order to generate all of our Terraform configuration files and then also plan and deploy.

---
- hosts: localhost
  connection: local
  become: false
  gather_facts: false
  vars:
    terraform_apply: false
    terraform_destroy: false
    terraform_dir: ../terraform
  tasks:
    - name: Capturing Terraform Command
      command: which terraform
      register: _terraform_command
      changed_when: false
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_init
        - terraform_inventory
        - terraform_plan

    - name: Setting Terraform Command Path
      set_fact:
        terraform_command: "{{ _terraform_command['stdout'] }}"
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_init
        - terraform_inventory
        - terraform_plan

    - name: Generating Terraform Variables
      template:
        src: templates/terraform_variables.tf.j2
        dest: "{{ terraform_dir }}/variables.tf"
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_init
        - terraform_inventory
        - terraform_plan

    - name: Generating Secret Terraform Variables
      template:
        src: templates/terraform.tfvars.j2
        dest: "{{ terraform_dir }}/terraform.tfvars"
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_init
        - terraform_inventory
        - terraform_plan

    - name: Generating Terraform Inventory Resources
      template:
        src: templates/terraform_inventory_resources.tf.j2
        dest: "{{ terraform_dir }}/inventory_resources.tf"
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_init
        - terraform_inventory
        - terraform_plan

    - name: Generating Terraform VM Definitions
      template:
        src: templates/terraform_vm_resources.tf.j2
        dest: "{{ terraform_dir }}/vm_resources.tf"
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_init
        - terraform_inventory
        - terraform_plan

    - name: Generating Terraform PDNS Resources
      template:
        src: templates/terraform_pdns_resources.tf.j2
        dest: "{{ terraform_dir }}/pdns_resources.tf"
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_init
        - terraform_inventory
        - terraform_plan

    - name: Capturing Terraform Init State
      command: "{{ terraform_command }} init -get=true -input=false"
      register: _terraform_init_state
      changed_when: false
      args:
        chdir: "{{ terraform_dir }}"
      tags:
        - terraform_init

    - name: Running Terraform Plan
      command: "{{ terraform_command }} plan -out=tfplan -input=false -detailed-exitcode"
      register: _terraform_plan
      changed_when: false
      args:
        chdir: "{{ terraform_dir }}"
      tags:
        - terraform_apply
        - terraform_plan
      failed_when: _terraform_plan['rc'] > 2

    - name: Displaying Terraform Plan
      debug: var=_terraform_plan
      tags:
        - terraform_apply
        - terraform_plan

    - name: Applying Terraform Plan
      command: "{{ terraform_command }} apply -input=false -auto-approve=true tfplan"
      register: _terraform_apply
      changed_when: false
      args:
        chdir: "{{ terraform_dir }}"
      tags:
        - terraform_apply
      when: >
        _terraform_plan['rc'] == 2 and
        terraform_apply

    - name: Destroying Terraform Plan
      command: "{{ terraform_command }} destroy -force"
      register: _terraform_destroy
      changed_when: false
      args:
        chdir: "{{ terraform_dir }}"
      tags:
        - terraform_destroy
      when: terraform_destroy

    - name: Capturing Terraform State
      command: "{{ terraform_command }} state pull"
      register: _terraform_state
      changed_when: false
      args:
        chdir: "{{ terraform_dir }}"
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_inventory

    - name: Generating Terraform Ansible Inventory
      template:
        src: terraform.inv.j2
        dest: "{{ vsphere_inventory_directory }}/terraform.inv"
      tags:
        - terraform_apply
        - terraform_destroy
        - terraform_inventory

Terraform Ansible Inventory

And of course none of this would be beneficial to any post Terraform deployment in order to provision our VMs with some additional Ansible playbooks without an inventory. In order to dynamically generate this during our Terraform playbook example above we have to capture the Terraform State at the very end of our playbook.

NOTE: Capturing the Terraform State actually just displays what is in the terraform.tfstate file in our project folder. So you can also get creative with this a bit more as well.

If you look at the tasks below which are from the playbook example above you will see how we are accomplishing this.

- name: Capturing Terraform State
  command: "{{ terraform_command }} state pull"
  register: _terraform_state
  changed_when: false
  args:
    chdir: "{{ terraform_dir }}"
  tags:
    - terraform_apply
    - terraform_destroy
    - terraform_inventory

- name: Generating Terraform Ansible Inventory
  template:
    src: terraform.inv.j2
    dest: "{{ vsphere_inventory_directory }}/terraform.inv"
  tags:
    - terraform_apply
    - terraform_destroy
    - terraform_inventory

And the terraform.inv.j2 Jinja2 template might look something like:

{% set _groups = [] %}
{% for key, value in (_terraform_state['stdout']|from_json)['modules'][0]['resources'].items() %}
{%   if 'vsphere_virtual_machine' in key %}
{%     set _group = key.split('.') %}
{%     set _groups = _groups.append(_group[1]) %}
{%   endif %}
{% endfor %}
{% set groups = _groups|unique|list %}
{% for group in groups %}
[{{ group }}]
{%   for key, value in (_terraform_state['stdout']|from_json)['modules'][0]['resources'].items() %}
{%     if 'vsphere_virtual_machine' in key %}
{%       set _group = key.split('.') %}
{%       if _group[1] == group %}
{{ value['primary']['attributes']['name'] }}
{%       endif %}
{%     endif %}
{%   endfor %}

{% endfor %}
[terraform_vms]
{% for key, value in (_terraform_state['stdout']|from_json)['modules'][0]['resources'].items() %}
{%   if 'vsphere_virtual_machine' in key %}
{{ value['primary']['attributes']['name'] }} ansible_host={{ value['primary']['attributes']['network_interface.0.ipv4_address'] }} mac_address={{ value['primary']['attributes']['network_interface.0.mac_address'] }} uuid={{ value['primary']['attributes']['uuid'] }}
{%   endif %}
{% endfor %}

And once we generate our inventory with Ansible it might look a little something like below:

terraform.inv:

[docker_storage]
docker-storage-02.lab.etsbv.internal
docker-storage-01.lab.etsbv.internal

[docker_swarm_managers]
docker-mgr-01.lab.etsbv.internal
docker-mgr-02.lab.etsbv.internal
docker-mgr-03.lab.etsbv.internal

[docker_swarm_workers]
docker-wrk-03.lab.etsbv.internal
docker-wrk-04.lab.etsbv.internal
docker-wrk-01.lab.etsbv.internal
docker-wrk-02.lab.etsbv.internal

[docker_lbs]
docker-lb-02.lab.etsbv.internal
docker-lb-01.lab.etsbv.internal

[terraform_vms]
docker-storage-02.lab.etsbv.internal ansible_host=10.0.102.178 mac_address=00:50:56:aa:01:a0 uuid=422ad693-162f-1c32-b90c-eee1b0a73d2b
docker-storage-01.lab.etsbv.internal ansible_host=10.0.102.162 mac_address=00:50:56:aa:5f:cb uuid=422a264a-0816-60ff-475c-23af6c0b9d0e
docker-mgr-01.lab.etsbv.internal ansible_host=10.0.102.171 mac_address=00:50:56:aa:a9:c0 uuid=422abe05-4483-88f8-34b7-e354fdc7a211
docker-mgr-02.lab.etsbv.internal ansible_host=10.0.102.166 mac_address=00:50:56:aa:ba:a0 uuid=422a5d74-4de2-1df8-646b-ca62311f98ab
docker-mgr-03.lab.etsbv.internal ansible_host=10.0.102.179 mac_address=00:50:56:aa:e3:06 uuid=422a8d34-68f7-a7be-9c6a-18949ce809ed
docker-wrk-03.lab.etsbv.internal ansible_host=10.0.102.207 mac_address=00:50:56:aa:6b:d5 uuid=422a809a-0cd2-cd84-756b-0822bc3f813a
docker-wrk-04.lab.etsbv.internal ansible_host=10.0.102.183 mac_address=00:50:56:aa:b5:43 uuid=422aae57-67e8-50d8-66f6-3a11bdc87a78
docker-wrk-01.lab.etsbv.internal ansible_host=10.0.102.155 mac_address=00:50:56:aa:e4:93 uuid=422a87fe-baa2-75d6-e666-36c15f351269
docker-wrk-02.lab.etsbv.internal ansible_host=10.0.102.201 mac_address=00:50:56:aa:e0:36 uuid=422a49e3-746c-b550-189c-0e0179c60418
docker-lb-02.lab.etsbv.internal ansible_host=10.0.102.160 mac_address=00:50:56:aa:f8:25 uuid=422adcf8-347a-e9a5-e113-00114c1d2de9
docker-lb-01.lab.etsbv.internal ansible_host=10.0.102.163 mac_address=00:50:56:aa:9c:b3 uuid=422a4adb-e7d3-ea74-a69a-3ff10c13063f

Final Notes

So there you have it. A creative way to use Ansible to generate/execute you Terraform deployments. I have been using this methodology for a short while now and it has been incredibly useful. Is it perfect? Of course not. I would love to hear your thoughts and etc.

Enjoy!

Leave a comment