Guivin


Managing Infrastructure and Configuration Together: Terraform Meets Ansible

Terraform is great for provisioning infrastructure. Ansible is great for configuring it. Together, they cover the full automation spectrum — but traditional integrations using local-exec can get messy and hard to scale.

Enter the Terraform Ansible Provider: a cleaner, more seamless way to connect both tools.

In this article, we’ll deploy an NGINX server on DigitalOcean using this provider — bridging infrastructure provisioning and configuration into an unified workflow.

Prerequisites

Install the cloud.terraform Ansible collection :

ansible-galaxy collection install cloud.terraform

Linking Terraform and Ansible: Inventory Configuration Made Easy

The cloud.terraform.terraform_provider plugin bridges Terraform and Ansible by enabling Ansible to interact directly with infrastructure defined in your Terraform state. This integration ensures consistency between both tools by dynamically generating an Ansible inventory from the current Terraform state.

Here’s an example of what the inventory.yml file looks like:

---
plugin: cloud.terraform.terraform_provider

Let’s Write the Terraform Code

The following Terraform configuration provisions infrastructure on DigitalOcean and seamlessly integrates it with Ansible using the Terraform Ansible provider. It defines:

  • The required Terraform providers (DigitalOcean and Ansible)
  • A Droplet provisioned via the DigitalOcean API
  • An Ansible inventory entry for the Droplet, directly from the Terraform state
terraform {
  required_providers {
    ansible = {
      source  = "ansible/ansible"
      version = "1.3.0"
    }

    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "2.45.0"
    }
  }
}

variable "do_token" {
  description = "Token to authenticate to DigitalOcean"
  type        = string
  sensitive   = true
}

resource "digitalocean_ssh_key" "default" {
  name       = "Terraform Example"
  public_key = file(pathexpand("~/.ssh/id_rsa.pub"))
}

resource "digitalocean_droplet" "web1" {
  name     = "web1"
  image    = "ubuntu-24-10-x64"
  region   = "nyc2"
  size     = "s-1vcpu-1gb"
  backups  = false
  ssh_keys = [digitalocean_ssh_key.default.fingerprint]
}

resource "ansible_host" "web1" {
  name   = digitalocean_droplet.web1.ipv4_address
  groups = ["webservers"]
  variables = {
    ansible_user                 = "root"
    ansible_ssh_private_key_file = "~/.ssh/id_rsa"
    ansible_python_interpreter   = "/usr/bin/python3"
    ansible_ssh_common_args      = "-o StrictHostKeyChecking=no"
  }
}

output "ipv4_address" {
  value = digitalocean_droplet.web1.ipv4_address
}
  • Providers Setup : The required_providers block defines dependencies on the ansible and digitalocean providers with explicit versioning for reproducibility.
  • API Token Input : The do_token variable is marked as sensitive, ensuring secure handling of your DigitalOcean credentials.
  • SSH Key Configuration : Uploads your local SSH public key to DigitalOcean, enabling SSH access to the new droplet.
  • Droplet Provisioning : A single web1 Droplet is created with Ubuntu 24.10 in the NYC2 region using a small instance type.
  • Ansible Inventory Resource : The ansible_host resource dynamically maps the Droplet’s IP into an Ansible inventory. This ensures your configuration management is directly aligned with the Terraform state.
  • IP Output : Displays the provisioned server’s IP after a successful apply, making it easy to connect or verify.

Apply the Terraform code with your DigitalOcean token :

$ read -s DO_TOKEN
$ terraform apply -var "do_token=$DO_TOKEN"

Visualizing the Ansible Dynamic Inventory Generated by Terraform

After provisioning your infrastructure and generating the Ansible inventory via Terraform, you can visualize its structure and associated variables using this command:

$ ansible-inventory -i inventory.yml --graph --vars

What you see explained

  • @all: Represents all hosts defined in the inventory.
  • @ungrouped: Hosts that are not assigned to any specific group.
  • @webservers: This group includes all hosts labeled under “webservers,”.

Under the @webservers group, the host 162.243.103.221 has several variables attached:

  • ansible_python_interpreter = /usr/bin/python3. Specifies the Python interpreter path on the target machine.
  • ansible_ssh_common_args = -o StrictHostKeyChecking=no. SSH option to skip the host key verification when connecting.
  • ansible_ssh_private_key_file = ~/.ssh/id_rsa. Path to the SSH private key used for authentication.
  • ansible_user = root. User to connect as over SSH.

Why this matters

This graphical output helps you:

  • Understand your inventory structure at a glance.
  • Verify that all necessary variables are properly set on each host.
  • Debug and review your Ansible configuration before running playbooks.

Apply the Ansible Playbook Using the Terraform-Generated Inventory

Once your infrastructure is provisioned and the dynamic inventory is ready, you can run your Ansible playbook directly using the Terraform-generated inventory.yml file:

ansible-playbook playbook.yml -i inventory.yml

This command executes the playbook against the provisioned infrastructure. In this example, the playbook:

  1. Gathers facts about the target host.
  2. Installs the NGINX package if it’s not already present.
  3. Ensures the NGINX service is started and enabled on boot.

Sample outputs

PLAY [Webservers] *************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************
ok: [162.243.103.221]

TASK [Ensure NGINX package is present.] ***************************************************************************
changed: [162.243.103.221]

TASK [Ensure NGINX is started and enabled.] ***********************************************************************
ok: [162.243.103.221]

PLAY RECAP ********************************************************************************************************
162.243.103.221            : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Verifying the Deployment

You can SSH into the server and check that NGINX is running correctly:

HOSTNAME=$(terraform output -json | jq -r .ipv4_address.value)
ssh root@$HOSTNAME

Once connected:

systemctl status nginx

Sample Status Output:

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Wed 2024-12-04 10:51:51 UTC; 1min 29s ago
   Main PID: 2441 (nginx)
      Tasks: 2 (limit: 1110)
     Memory: 2.1M (peak: 2.3M)
        CPU: 26ms
     CGroup: /system.slice/nginx.service
             ├─2441 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
             └─2442 "nginx: worker process"

This confirms that your playbook was applied successfully, and the NGINX service is now running on your newly provisioned server.

Conclusion

In this article, we explored how to seamlessly integrate Terraform and Ansible using the Terraform Ansible provider to automate both infrastructure provisioning and configuration management.

By leveraging this integration:

  • Terraform takes care of creating cloud resources (like VMs, networks, etc.)
  • Ansible handles the configuration of those resources (like installing packages, starting services, etc.)
  • The Terraform Ansible provider bridges the gap, automatically generating dynamic inventories from your Terraform state

This approach eliminates the need for manual inventory management and ensures consistency between provisioning and configuration.

📦 Want to try it out? You can find the full example code on GitHub: 👉 terraform-ansible-integration-101