Terraform Dynamic Ansible Inventory Management

The typical workflow design pattern for server provisioning is to create instance image artifacts, and then provision infrastructure, and then provision software and configuration unique to the instance. There may also be a desire to independently update and modify the instance after final provisioning. This is especially true if immutable infrastructure methodology is incompletely implemented.

One of the primary challenges here is brokering the outputs from infrastructure provisioning to inputs for software provisioning. If we assume industry standard tooling to solve this problem, then this solution space can be reduced to the simplified subset problem of mapping Terraform outputs to Ansible inventory inputs. After repeatedly answering portions of this question on Stack Overflow, I decided it was time to solve this problem completely and robustly.

In this article we will explore dynamic management of an Ansible inventory generated through Terraform to solve the problem of chaining infrastructure provisioning into software provisioning for the standard design pattern of a server provisioning workflow.

Prerequsities

With Terraform >= 0.13 it is rather straightforward to retrieve the module for usage with terraform init after basic declaration:

module "ansible_inv" {
  source  = "mschuchard/ansible-inv/local"
  version = "~> 1.1.2"
}

You will also need a functioning installation of Ansible to ingest and utilize the inventories.

Usage with Variable Instances

We can quickly demonstrate basic module usage with some facetious input argument values.

module "ansible_inv" {
  source  = "mschuchard/ansible-inv/local"
  version = "~> 1.1.2"

  formats   = ["ini"]
  instances = {
    "cabin_in_woods" = [
      {
        name = "ronald_swanson"
        ip   = "127.0.0.1"
        vars = { "occupation" = "craftsman" }
      },
      {
        name = "ashley_williams"
        ip   = "127.0.0.1"
        vars = { "boomstick" = true }
      }
    ],
    "sacred_heart" = [
      {
        name = "perry_cox"
        ip   = "127.0.0.1"
        vars = {}
      }
    ],
    "north_houston" = [
      {
        name = "hank"
        ip   = "127.0.0.1"
        vars = { "friends" = ["barry", "cristobal"] }
      }
    ],
    "everywhere" = [
      {
        name = "roy_kent"
        ip   = "127.0.0.1"
        vars = {}
      }
    ]
  }
  group_vars = {
    "cabin_in_woods" = { "walls" = "wooden" }
  }
}

The resulting Ansible inventory would appear like:

[all]

[cabin_in_woods]

ashley_williams ansible_host=127.0.0.1 boomstick=true ronald_swanson ansible_host=127.0.0.1 occupation=craftsman

[cabin_in_woods:vars]

walls=wooden

[everywhere]

roy_kent ansible_host=127.0.0.1

[north_houston]

hank ansible_host=127.0.0.1 friends='[“barry”, “cristobal”]’

[sacred_heart]

perry_cox ansible_host=127.0.0.1

Usage with General Resources

However, for the vast majority of use cases you would want to leverage Terraform’s resource attribute mapping to automatically provide inputs to the module. Hardcoding inputs is a non-ideal solution. We can perform a simple implementation for AWS instances.

resource "aws_instance" "frontend" {
  ...
}
resource "aws_instance" "backend" {
  ...
}

module "ansible_inv" {
  source  = "mschuchard/ansible-inv/local"
  version = "~> 1.1.2"

  formats   = ["yaml"]
  instances = {
    "frontend" = [
      {
        name = aws_instance.frontend.public_dns
        ip   = aws_instance.frontend.public_ip
        vars = aws_instance.frontend.tags
      }
    ],
    "backend" = [
      {
        name = aws_instance.backend.private_dns
        ip   = aws_instance.backend.private_ip
        vars = aws_instance.backend.tags
      }
    ]
  }
  group_vars = {
    "frontend" = { "interface" = "public" }
    "backend"  = { "interface" = "private" }
  }
}

This ensures an accurate future-proof output dynamic Ansible inventory. Infrastructure modifications will automatically correspond to an update to the inventory content. The resulting Ansible inventory would appear like:

---
"all":
  "children":
    "frontend":
      "hosts":
        "frontend.example.com":
          "ansible_host": "123.45.67.89"
          "framework": "django"
      "vars":
        "interface": "public"
    "backend":
      "hosts":
        "backend.example.com":
          "ansible_host": "987.65.43.21"
          "data": "redis"
      "vars":
        "interface": "private"
  "hosts": {}
  "vars": {}

Usage with Platform Provider Resources

The previous example is nice for flexibility and customization of instances in the inventory, but sometimes you may want to just manage an inventory completely automatically. In that situation one can simply map provider instance resources to the module inputs.

resource "aws_instance" "this" {
  for_each = var.my_instances_aws
  ...
}
resource "google_compute_instance" "this" {
  for_each = var.my_instances_gcp
  ...
}
resource "azurerm_linux_virtual_machine" "this" {
  for_each = var.my_instances_azr
  ...
}
resource "vsphere_virtual_machine" "this" {
  for_each = var.my_instances_vsp
  ...
}

module "ansible_inv" {
  source = "mschuchard/ansible-inv/local"
  version = "~> 1.1.2"

  formats       = ["json"]
  instances_aws = aws_instance.this
  instances_gcp = google_compute_instance.this
  instances_azr = azurerm_linux_virtual_machine.this
  instances_vsp = vsphere_virtual_machine.this
  group_vars    = {
    "aws" = { "platform" = "aws" },
    "vsp" = { "on_prem" =  true }
  }
}

This will output an Ansible inventory with information automatically parsed from the resource attributes. The logic will make best guesses for host variables such as ansible_transport according to the information provided by the resource. The resulting Ansible inventory would appear like:

{
  "all": {
    "children": {
      "aws": {
        "hosts": {
          "aws_one": {
            "ansible_host": "123.45.67.89",
            "foo": "bar"
          }
        },
        "vars": {
          "platform": "aws"
        }
      },
      "azr": {
        "hosts": {
          "azr_one": {
            "ansible_become_user": "administrator",
            "ansible_host": "987.65.43.21",
            "ansible_transport": "winrm"
          }
        }
      },
      "gcp": {
        "hosts": {
          "gcp_one": {
            "ansible_host": "987.65.43.21",
            "my_label": "label_value",
            "my_metadata": "metadata_value"
          }
        }
      },
      "vsp": {
        "hosts": {
          "vsp_one": {
            "ansible_host": "123.45.67.89",
            "baz": "bat",
          },
          "vsp_two": {
            "ansible_become_user": "not_administrator",
            "ansible_host": "987.65.43.21",
            "ansible_transport": "winrm"
          }
        },
        "vars": {
          "on_prem": "true"
        }
      }
    },
    "hosts": {},
    "vars": {}
  }
}

Usage Within Immutable Infrastructure Pipeline

Now that we have observed how to manage a dynamic output from Terraform for an Ansible inventory, we should extend this to ingesting the inventory into Ansible within the context of a server provisioning workflow. Assume that we have an immutable infrastructure pipeline in Jenkins. We could implement a Jenkinsfile for this with the Jenkins Pipeline library for Terraform, and the plugin for Ansible Playbook.

// using GitHub Branch Source plugin
@Library('github.com/mschuchard/jenkins-devops-libs@2.0.1')_

pipeline {
  stages {
    stage('Apply Terraform Plan') {
      agent { docker { image 'hashicorp/terraform:1.1.9' } }
      steps {
        script {
          terraform.apply(configPath: '/path/to/plan.tfplan')
        }
      }
    }
    stage('Uniquely Provision with Ansible') {
      agent { docker { image 'cytopia/ansible' } }
      steps {
        // assumes no inventory prefix argument and yaml format argument
        ansiblePlaybook(
          playbook:      '/path/to/playbook',
          inventory:     'inventory.yaml',
          limit:         'aws:&azr',
          credentialsId: 'ssh-creds'
        )
      }
    }
  }
}

The above would streamline the chaining of Terraform’s infrastructure provisioning into Ansible’s software provisioning for the subset of instances in AWS and Azure. Note that the above is not a complete immutable infrastructure pipeline by any stretch of the imagination, but it comprises a functioning component implementation of the pipeline architecture.

Conclusion

In this article we explored how to automatically dynamically manage an Ansible inventory populated from Terraform provisioned infrastructure. Now you can semi-effortlessly chain your infrastructure provisioning into your software provisioning. With this piece, the brokering and interfacing between the two processes in an immutable infrastructure pipeline is a solved problem.

If your organization is interested in managing your server infrastructure and software with code, especially within a codified and automated immutable infrastructure pipeline, then contact Shadow-Soft below.

  • This field is for validation purposes and should be left unchanged.