Integrating Terraform with Ansible/Chef for Infrastructure and Configuration Automation


Bicycle

In modern infrastructure automation, teams often combine Terraform with configuration management tools like Ansible or Chef to get end-to-end control of their systems. Terraform excels at provisioning and managing cloud resources (networks, servers, load balancers, etc.), while Ansible and Chef specialize in configuring those resources (installing software, setting up services, enforcing security policies). By using Terraform for “day 0” provisioning and Ansible/Chef for “day 1” configuration, you keep a clear separation of concerns: Terraform defines what infrastructure exists, and Ansible/Chef define how each server is configured.

Why Use Both: Complementary Strengths

  • Separation of Concerns: Terraform is declarative infrastructure-as-code, ideal for creating resources (VMs, networks, containers, etc.), whereas Ansible/Chef are optimized for post-provisioning tasks like installing packages, managing files, and deploying applications. Keeping these roles distinct makes your code cleaner and easier to maintain. For example, a Terraform config might spin up a cluster of servers, and an Ansible playbook then configures each server with the required software.

  • Idempotency and Reusability: Both tools are idempotent: Terraform tracks the state of your infrastructure so it only makes changes when the configuration differs, and Ansible/Chef apply configurations in a way that results converge without unintended side effects. Combined, you get a fully reproducible pipeline (provision infra only when needed; configure servers consistently) that reduces drift and manual errors.

  • End-to-End Automation: Using both in tandem lets you build fully automated pipelines. Terraform handles the “heavy lifting” of resource creation across clouds or platforms, while Ansible/Chef handle the finer details of machine configuration. This can significantly speed up deployments. For instance, after terraform apply finishes, you can immediately run an Ansible playbook against the new servers (using the IPs or hostnames Terraform outputs) to finalize setup.

  • Flexibility and Scaling: If you need to rebuild a server from scratch, Terraform can destroy and recreate it (on fresh images), and Ansible/Chef can run automatically after. You can scale infrastructure up/down with Terraform, and then have Ansible/Chef bring new nodes to the desired state. The tools play to their strengths: Terraform’s graph model handles complex dependencies between cloud resources, while Ansible’s procedural YAML or Chef’s recipes handle in-depth OS-level changes.

Common Integration Patterns

There are several effective strategies for integrating Terraform with Ansible or Chef. The best approach depends on your team’s workflow and requirements, but common patterns include:

  • Post-Provisioning Configuration (Terraform ⇒ Ansible/Chef): The most straightforward method is: first use Terraform to create all infrastructure, then run your configuration tool against the freshly created servers. For example, a Terraform configuration might create three EC2 instances, and output their IP addresses. You then generate an Ansible inventory or Chef node definitions from those outputs and execute an Ansible playbook or Chef run-list on them. In this pattern, Terraform’s job ends when the instances are live, and Ansible/Chef takes over to “finish the job.”

    Example: A Terraform configuration provisions three Ubuntu VMs with keys and public IPs (using a for_each loop). After terraform apply you have their IP addresses as outputs. You then write an Ansible inventory (or use a dynamic inventory script) that includes those IPs, and run ansible-playbook to install nginx, copy files, etc. This clean handoff (Terraform handles “servers exist”, Ansible handles “servers are configured”) leverages each tool appropriately.

  • Dynamic Inventory and State Integration: To connect Terraform outputs to Ansible seamlessly, many teams use the Terraform state or output as the source of truth for Ansible inventory. For instance, by using a dynamic inventory plugin that reads Terraform’s state file and exports hosts defined in Terraform. One popular solutions is the Terraform Collection for Ansible Automation Platform, which includes inventory plugins that read the Terraform state file and export hosts defined in Terraform. This removes manual inventory management: Terraform tags or outputs define the groups of machines, and Ansible uses those groups.

  • Local-Exec or Null Resource Triggers: Terraform can directly invoke external programs via the local-exec provisioner or a null_resource. For example, you can attach a local-exec to run an Ansible command after provisioning completes. A typical snippet might use null_resource with a triggers block that depends on your instances, and then local-exec runs ansible-playbook -i inventory playbook.yml. This ensures Terraform will wait for the resources and then call Ansible locally. However, use this with caution: Terraform’s documentation and experts warn that provisioners can be brittle and unreliable for complex configuration tasks. They are usually best for simple bootstrapping (e.g. copying SSH keys) or invoking scripts, rather than full-scale configuration. In practice, teams often prefer decoupling (see next point) rather than embedding long Ansible runs inside Terraform.

  • CI/CD or Orchestration Pipelines: A very common pattern is to use an external orchestrator or CI/CD tool (Jenkins, GitLab CI, Spacelift, etc.) to run Terraform and Ansible sequentially in separate steps. For example, you might have a Jenkins pipeline where one stage runs terraform apply to provision infrastructure, and then the next stage runs ansible-playbook against those hosts using an inventory file. This decouples the tools nicely: Terraform runs in one container/step, Ansible in another. For example, in the pipeline, the “Provision” stage calls Terraform, and the “Configure” stage calls Ansible with the inventory file. Similarly, a GitOps or workflow engine (FluxCD, GitHub Actions, etc.) can trigger an Ansible playbook after the Terraform code ran. This approach avoids Terraform provisioners altogether and makes failures easier to track (CI logs will clearly show Terraform vs Ansible steps).

  • Cloud-Init or User Data Scripts: For simple cases, you can use cloud-init/user-data (or the remote-exec provisioner with a shell script) to install Ansible or Chef agent on instance boot, and have it pull its configuration from a central server. For example, Terraform’s aws_instance can include a user_data script that runs yum install ansible and then ansible-pull. This can work, but it mixes concerns (infrastructure code contains configuration scripts) and is less transparent than running Ansible/Chef explicitly from your control machine.

Leveraging Terraform Provisioners/Providers and Packer

Terraform has provisioners like remote-exec, file, and local-exec that can invoke configuration on created resources. For Ansible, the local-exec pattern (above) is effectively a Terraform provisioner triggering the Ansible CLI. Terraform itself does not have a “Ansible provisioner”, but there is an Ansible provider that can be used to create and manage Ansible inventories, playbooks and more (more on this later).

For Chef, Terraform historically had a Chef provisioner that could bootstrap a Chef client on a machine. However, this is now deprecated. According to Chef’s docs, “Terraform deprecated the Chef Provisioner in the 0.13.4 release and they will remove it in a future version”. There also was a Chef provider which has been archived. Instead, current guidance is:

  • If you use Chef Infra Server (Chef Server), you might still use Terraform’s remote-exec to install and configure the Chef client on each node, connecting it to the server.
  • If you use Chef Solo/Workstation, you generally use remote-exec. For instance, you can use a remote-exec to download a Chef RPM, install it, fetch policy files, and run chef-client -z.
  • Many teams now prefer to pre-bake images: use Packer with Chef to create a golden image, then let Terraform deploy that image as instances. Packer can be used to generate new machine images for multiple platforms on every change to Chef/Puppet.

Overall, remember that Terraform is primarily an infra tool. The HashiCorp best practice is to minimize reliance on provisioners. HashiCorp recommends Terraform provisioners as a last resort. Use them sparingly for bootstrapping only.

Example: AWS EC2 Provisioning with Terraform and Ansible

To illustrate, consider provisioning a small fleet of EC2 instances and configuring them. One can follow these steps:

  1. Define Terraform resources. Write HCL code to create an AWS key pair, security group, and an aws_instance resource (possibly using for_each or count to create multiple hosts). For example:

     1resource "aws_key_pair" "ssh_key" {
     2  key_name   = "mykey"
     3  public_key = file(var.public_key)
     4}
     5
     6resource "aws_instance" "web" {
     7  count         = 3
     8  ami           = data.aws_ami.ubuntu.id
     9  instance_type = "t2.micro"
    10  key_name      = aws_key_pair.ssh_key.key_name
    11  associate_public_ip_address = true
    12  tags = {
    13    Role = "webserver"
    14    Name = "web${count.index+1}"
    15  }
    16}
    17
    18output "web_ips" {
    19  value = aws_instance.web[*].public_ip
    20}
    

    This creates three web servers (each with a public IP) and outputs their addresses. As shown in one example, using Terraform’s for_each you can easily extend to N hosts. After terraform apply, suppose it outputs IPs like ["54.1.2.3", "54.4.5.6", "54.7.8.9"].

  2. Generate Ansible inventory. Create an Ansible inventory file (or use a dynamic inventory plugins) listing the new hosts under the appropriate group. You could use Terraform’s output directly, e.g. by writing a local file using a local-exec. Alternatively, use an inventory plugin or script that reads Terraform state, or the Ansible Terraform provider. A simple static inventory might look like:

    [webservers]
    54.1.2.3
    54.4.5.6
    54.7.8.9
    

    For larger setups, a dynamic approach (using Terraform outputs or the Ansible Terraform provider) can automate this.

  3. Run Ansible playbooks. With the inventory ready, you run ansible-playbook -i inventory site.yml. Your playbook (or roles) then targets the group, e.g.:

     1- hosts: webservers
     2  become: yes
     3  tasks:
     4    - name: Install Nginx
     5      apt:
     6        name: nginx
     7        state: present
     8    - name: Copy index.html
     9      copy:
    10        src: index.html
    11        dest: /var/www/html/index.html
    

    Because the inventory came from Terraform, Ansible will configure exactly those instances.

  4. (Optional) Automate with Terraform’s local-exec. If you want Terraform to automatically call Ansible after creating resources, you can use the null_resource with a local-exec provisioner that runs the Ansible command. For example:

    1resource "null_resource" "configure" {
    2  depends_on = [aws_instance.web]
    3  provisioner "local-exec" {
    4    command = "ansible-playbook -i inventory site.yml"
    5  }
    6}
    

    On terraform apply, Terraform will wait for the EC2 instances, then execute the playbook. This can simplify one-step workflows. Many teams prefer running Ansible outside Terraform (in CI or manually) for better control.

Best Practices

  • Keep Codebases Separate. Maintain different repositories or directories for Terraform and Ansible/Chef code. This reinforces separation: one codebase defines cloud/network resources, the other defines machine configuration. For instance, you might have a terraform/ repo (organized by environment, with modules, etc.) and an ansible/ repo (with playbooks, roles, and inventory templates). Separate VCS tracking makes reviews and CI integration easier.

  • Isolate Environments Properly. Do not use one Terraform workspace for “dev” and “prod” without caution. A best practice is to create separate state (and possibly separate Terraform root modules) per environment. This way, production infrastructure changes cannot accidentally impact development state, and access controls can be applied differently. (Avoid putting entirely different environments in a single statefile via workspaces.)

  • Use Terraform Outputs for Inventory. Make Terraform output the data needed by Ansible/Chef (e.g. IP addresses, hostnames, etc.). You can then script the generation of inventory or directly feed those outputs into your config tool. There are even Terraform providers/plugins for Ansible inventory.

  • Minimize Terraform Provisioners. As noted, avoid heavy use of local-exec, remote-exec or file provisioners. They work but can lead to unpredictable results (network issues, timeouts, partial config). If you must bootstrap, do only the minimum (e.g. install an SSH key or agent). For richer configuration, use the dedicated config tool. HashiCorp itself warns that provisioners should be “last resort” only.

  • Bake Images for Immutable Infrastructure. When possible, use tools like Packer to create AMIs or VM images with your software pre-installed (via Ansible/Chef during image build). Then Terraform simply deploys those golden images. This reduces the need for large post-provisioning steps. If a change is needed, you rebuild the image and Terraform can replace the instances.

  • Idempotent and Declarative Playbooks. Write your Ansible roles or Chef recipes to be idempotent and as declarative as possible. This way, you can re-run them safely if needed. Avoid destructive imperative scripts. Using roles and policies (Ansible roles, Chef policyfiles or environments) helps maintain consistency across runs.

  • Secure Authentication. Ensure Terraform and Ansible share the necessary credentials securely. For instance, give Terraform a key or IAM role to create instances, and give Ansible a way to SSH (the same keypair created by Terraform). Use SSH key pairs from Terraform outputs, or use SSH agent forwarding, or use a bastion. Never hard-code sensitive data in your Terraform/Ansible code – use secrets managers or environment variables.

  • Testing and Validation. Before rolling out to production, test your combined process. Run Terraform in a dry-run (plan) mode and then apply to a test account. Run Ansible with --check mode against test instances. Use linting tools (e.g. tflint, ansible-lint) and CI pipelines that validate syntax. Catch issues early.

  • Version Pinning. Lock versions of modules, providers, Ansible roles, and even Terraform itself to known-good releases. Drift between versions can cause unexpected behavior in either Terraform or Ansible. Keep a clean audit trail by tagging commits/releases in your repos.

  • Collaboration Tools. Use code review and pull requests for any changes, whether to Terraform or Ansible code. Document in git history how the pieces connect. For example, an Ansible playbook should document which Terraform outputs it expects as inventory.

  • Monitor and Audit. Whatever integration method you use, ensure you have logs and state recording. Terraform has a state file (and Terraform Cloud/Enterprise gives you run logs). Ansible Tower/AWX gives job logs. If using a simple pipeline (e.g. Jenkins), archive the console output for both Terraform and Ansible stages so you can trace what happened.

Potential Pitfalls

  • State Drift. Terraform only knows about resources it created; if Ansible or a user manually changes something on a machine (like deleting a file, or installing a package outside Ansible), Terraform won’t see that drift and might try to “fix” it on next run. Conversely, if Terraform re-creates a resource (e.g. replaces an instance), any previous configuration on the old instance is lost. To mitigate this, limit Terraform’s scope to true infrastructure, and let Ansible handle desired state on each run.

  • Overlapping Responsibilities. Beware of having Terraform and Ansible/Chef manage the same thing. For example, don’t have Terraform create a database and also have an Ansible role that re-creates that database – that doubles work and can conflict. Decide which tool owns which resource (Terraform can even manage some OS-level resources via community providers, but it’s usually better to let Ansible/Chef do the in-VM configuration).

  • Provisioner Failures. If you rely on Terraform provisioners (remote-exec/local-exec) and the configuration step fails, Terraform may not roll back or may leave resources in a partial state. This can make debugging hard. Always test provisioner scripts separately and consider using timeouts and retry logic (or avoid them entirely in favor of external orchestration).

  • Ordering and Dependencies. Ansible playbooks typically expect hosts to be reachable (SSH up and accepting connections). If Terraform finishes but the OS is still booting or cloud-init is still running, the first Ansible runs may fail. Use “wait_for” tasks in Ansible or Terraform’s “remote-exec” connection settings to ensure SSH is ready, or add a short manual wait.

  • Security and Networking. Ensure that network ACLs, security groups, and firewall rules created by Terraform do allow Ansible/Chef to connect (SSH or WinRM). A common gotcha: Terraform creates a private subnet with no access – Ansible then can’t reach the machines. Solutions include using a bastion host, or temporarily assigning a public IP as in our example, or using an SSH proxy. Plan your network so configuration access is possible.

Advanced Tip: Terraform’s Ansible Provider

Recent Terraform versions support a community-maintained Ansible provider that lets you define Ansible inventory and even tasks in HCL. For example, the ansible_host resource can create hosts in a Terraform-managed inventory automatically. Using this, you could eliminate the separate inventory file: you define each host and group in Terraform, and the Ansible provider writes out an inventory file for you. Here’s a snippet:

1resource "aws_instance" "web1" { /* ... */ }
2resource "ansible_host" "web1" {
3  name   = "web1"
4  groups = ["aws"]
5  variables = {
6    ansible_user = "ubuntu"
7    ansible_host = aws_instance.web1.public_ip
8  }
9}

This is another way to keep everything in Terraform’s plan. However, it shifts complexity (you must know the Ansible provider’s syntax), so many teams still prefer the classic separate playbook approach.

Conclusion

Integrating Terraform with Ansible or Chef combines the strengths of both: Terraform’s reliable, stateful infrastructure provisioning with the rich configuration management of Ansible/Chef. The keys to success are clear separation of duties (Terraform for infra, Ansible/Chef for software), robust workflow orchestration (CI/CD or scripted sequences), and adherence to best practices (idempotence, small atomic changes, version control). By following these patterns and tips (e.g. provision first, configure second, etc.) you can achieve a smooth end-to-end automation pipeline. Always test your workflow and watch for drift, but with care you’ll get a powerful, repeatable system that scales with your cloud environment.

Go Back explore our courses

We are here for you

You are interested in our courses or you simply have a question that needs answering? You can contact us at anytime! We will do our best to answer all your questions.

Contact us