TrademarkTrademark
Features
Documentation
Comprehensive Guide

Using Terraform with Ansible: The Complete Integration Guide

Comprehensive guide to integrating Terraform and Ansible for end-to-end infrastructure automation, including patterns, workflows, and best practices for 2026
Sebastian StadilFebruary 20, 2026Updated June 11, 2026
Using Terraform with Ansible: The Complete Integration Guide
Key takeaways
  • Terraform provisions infrastructure (Day 0); Ansible configures it (Day 1 and beyond). The integration problem is the handoff: how Ansible learns which hosts Terraform created, and where each tool actually executes.
  • Prefer dynamic inventory (Terraform outputs or cloud inventory plugins) over local-exec provisioners. Provisioners tightly couple the tools, and an Ansible failure mid-apply can leave Terraform in an inconsistent state.
  • The most common integration failures are environmental, not logical: remote runners are minimal images without Ansible installed, and SSH keys that exist on your laptop are not on the runner's disk.
  • Ansible is a Python program, so its behavior is a function of the runner's interpreter. Pin the runtime by owning the runner image instead of pip-installing Ansible into whatever Python the run lands on.
  • In CI/CD, run Terraform and Ansible as separate sequential stages and pass `terraform output -json` between them as an artifact, rather than mixing both tools in one step.

Last Reviewed for Accuracy by Ryan Fee on May 1, 2026.

Introduction

Terraform provisions infrastructure — VMs, networks, DNS, load balancers — and Ansible configures what Terraform created. The hard part of using them together is the handoff: how Ansible learns which hosts exist, where each tool actually executes, and what happens when the machine running your Terraform doesn't look like the laptop you tested on.

HashiCorp Terraform and Ansible (now part of Red Hat) cover different, sequential layers of the automation stack, and the integration patterns between them are well established. What's less well documented is where those patterns break in practice — and in Scalr's support queue, most of the breakage is environmental, not logical.

This guide covers when to use each tool, three integration patterns (provisioners, dynamic inventory from outputs, cloud inventory plugins), how to wire both into CI/CD pipelines, and the failure modes Scalr sees most often when teams move this setup from a workstation to a remote execution platform.

Terraform vs. Ansible: Understanding When to Use Each

Core Differences

The most fundamental distinction between Terraform and Ansible lies in their primary focus:

Terraform is predominantly an orchestration tool. Its core strength is provisioning and managing the lifecycle of infrastructure resources—creating, updating, and destroying virtual machines, networks, storage, and DNS entries across various cloud providers and on-premises systems. Terraform excels at defining the "what"—the desired state of infrastructure components and their interdependencies.

Ansible is primarily a configuration management tool. Its forte is automating the setup and maintenance of software and systems within provisioned infrastructure. This includes installing packages, configuring services, deploying applications, and ensuring systems adhere to specific configurations. Ansible excels at defining the "how"—the steps to bring a system to its desired configured state.

While some overlap exists (Terraform can perform basic configuration via provisioners, and Ansible can provision infrastructure via cloud modules), their architectures and design principles are optimized for these distinct roles.

Philosophical Differences: Declarative vs. Procedural

Terraform employs a declarative approach. Users define the desired end state of infrastructure in HCL (HashiCorp Configuration Language). Terraform then analyzes this desired state against the current actual state (tracked in its state file) and determines the necessary actions (create, update, delete) to achieve it. The order of resource definitions is generally not significant, as Terraform builds a dependency graph to determine execution sequence.

Ansible utilizes a procedural (imperative) approach. Ansible Playbooks, written in YAML, consist of tasks executed in the order they are defined. Users specify explicit steps to reach desired configuration, providing direct control over execution flow.

State Management

Terraform is stateful. It maintains a state file (terraform.tfstate) that stores a representation of managed infrastructure, mapping resources defined in configuration to real-world objects. Terraform relies on this file to plan changes, track dependencies, and manage resource lifecycles.

Ansible is largely stateless. It does not maintain persistent records of configuration state between runs. Each playbook execution assesses current node state and performs actions to achieve the desired configuration. While Ansible modules aim for idempotency, the tool doesn't rely on stored state like Terraform does.

Comparison Table

Feature Terraform Ansible
Primary Use Case Infrastructure Provisioning & Orchestration Configuration Management & Deployment
Approach Declarative Procedural/Imperative
Language HCL YAML
State Management Stateful (maintains tfstate file) Largely Stateless
Infrastructure Type Favors Immutable Often used with Mutable
Resource Lifecycle Strong (create, update, delete) Limited (configuration focus)
Agent Requirement Agentless (APIs) Agentless (SSH/WinRM)
Dependency Handling Builds resource graph Executes tasks in order

Why Use Terraform and Ansible Together?

Complementary Strengths

The core idea behind using Terraform and Ansible together is that they address different, sequential layers of the automation stack:

Terraform handles "Day 0" activities—initial provisioning and lifecycle management of infrastructure components like virtual machines, networks, and load balancers. It answers: "What infrastructure do I need, and where?"

Ansible handles "Day 1 and beyond" tasks—configuration of provisioned resources. This includes installing software, applying security policies, deploying application code, and managing ongoing system states. It answers: "Now that I have this infrastructure, how do I make it do what I need?"

This division of labor plays to each tool's strengths:

  • Terraform's declarative approach and state management ensure reliable, consistent infrastructure provisioning that can be versioned and evolved over time
  • Ansible's procedural nature and extensive module library provide granular control over system configuration and application deployment

End-to-End Automation

By integrating Terraform and Ansible, organizations can automate the entire service lifecycle:

  1. Define Infrastructure - Teams define required resources (servers, networks, databases) using Terraform's HCL
  2. Provision Infrastructure - Terraform provisions resources across cloud providers or on-premises, ensuring correct dependencies
  3. Configure Systems - Ansible playbooks configure provisioned resources, setting up operating systems, installing packages, hardening security
  4. Deploy Applications - Ansible deploys application code, manages configurations, and orchestrates deployment workflows
  5. Ongoing Management - Terraform scales or modifies infrastructure while Ansible applies updates, patches, and configuration changes

Key Benefits

  • End-to-End Automation - From bare cloud resources to fully configured applications
  • Consistency & Reliability - Infrastructure and configuration as code minimize errors
  • Scalability - Easily scale infrastructure with Terraform and configure new resources with Ansible
  • Efficiency - Drastically reduces manual effort and deployment times
  • Better Collaboration - Code-based definitions enable version control and DevOps workflows
  • Faster Disaster Recovery - Terraform can rapidly provision infrastructure while Ansible quickly configures and deploys applications
  • Immutable Infrastructure Support - Ideal for patterns where Ansible bakes configurations into golden images for deployment

Integration Patterns: Connecting Terraform and Ansible

Successfully combining Terraform and Ansible requires a well-defined integration strategy. Several patterns have emerged, each with advantages and typical use cases.

Pattern 1: Terraform Provisioners (local-exec)

How it works: The local-exec provisioner runs a command locally on the machine executing Terraform. It can invoke an Ansible playbook targeting newly created resources, passing IP addresses or other identifiers from Terraform to Ansible.

Example:

resource "aws_instance" "web" {
  ami           = "ami-0c55b31ad2c455b55"
  instance_type = "t2.micro"
  key_name      = "your-ssh-key-pair"
 
  tags = {
    Name = "WebServer"
  }
}
 
resource "null_resource" "wait_for_ssh" {
  depends_on = [aws_instance.web]
 
  provisioner "remote-exec" {
    inline = ["echo 'SSH is up'"]
    connection {
      type        = "ssh"
      user        = "ec2-user"
      private_key = file("~/.ssh/your-ssh-key.pem")
      host        = aws_instance.web.public_ip
    }
  }
}
 
resource "null_resource" "ansible_provision" {
  depends_on = [null_resource.wait_for_ssh]
 
  provisioner "local-exec" {
    command = <<EOT
      ansible-playbook \
        -i "${aws_instance.web.public_ip}," \
        --private-key ~/.ssh/your-ssh-key.pem \
        -u ec2-user \
        playbooks/configure-nginx.yml
    EOT
  }
}

That private_key = file("~/.ssh/your-ssh-key.pem") line is where this pattern fails first once Terraform stops running on your workstation. A managed-services provider we worked with at Scalr hit it on their very first remote-execution run: a remote-exec block to install Python on a fresh Fedora VM, then local-exec running ansible-playbook -i '${self.public_ip},' --private-key .... Two problems surfaced that never appear locally. The private key file didn't exist on the runner's disk, and the obvious workaround — pasting the PEM into a Terraform variable — stripped the line breaks, so SSH authentication kept failing even after the variable was written back out to a file. The pattern that survived was a local_file resource carrying the key as sensitive content with file_permission = "0400", materializing it at run time — or sidestepping runner-side SSH entirely and bootstrapping the host with cloud-init or remote-exec from inside the VM. The rule to internalize: your laptop is not the runner.

Pros:

  • Simple to implement for basic scenarios
  • Configuration occurs immediately after provisioning within same terraform apply workflow

Cons:

  • Tightly couples Terraform and Ansible; Ansible failures can leave Terraform in inconsistent state
  • Increases duration of Terraform runs
  • Requires Ansible on Terraform execution machine with network connectivity to new resources
  • Complex error handling spanning both tools

The third con on that list deserves expansion, because it is the single most common way this pattern fails on hosted platforms. A consultancy wiring Ansible into their Terraform runs tested everything locally, pushed it, and watched every remote run die with exit status 127. Output: /bin/sh: 52: ansible-playbook: not found. Hosted run containers are minimal images; Ansible isn't in them. Their first fix attempt — building a custom runner image directly on the agent VM — hit a second wall: /opt/scalr-agent/embedded/bin/docker: No such file or directory. Their agent had been installed via Docker rather than RPM/DEB, which means task images are pulled from a registry, not built on the VM host. The same-day unblock was a pre-plan hook running python3 -m pip install --user ansible-core, applied environment-wide. The durable fix was a custom image built FROM scalr/runner:latest with pip3 install ansible, pushed to their own registry and referenced via SCALR_AGENT_CONTAINER_TASK_IMAGE_REGISTRY. Before baking Ansible into a runner image, check how your agent was installed — that detail determines where the image has to live.

Pattern 2: Dynamic Inventory from Terraform Output

How it works: Terraform provisions infrastructure and generates outputs (IP addresses, instance IDs, DNS names). Ansible reads these outputs to build its inventory, decoupling the two tools.

Terraform outputs:

output "web_server_ips" {
  value = aws_instance.web[*].public_ip
}
 
output "web_server_ids" {
  value = aws_instance.web[*].id
}

Ansible inventory file (terraform_inventory.ini):

[web_servers]
web_server_1 ansible_host=<IP_from_terraform>
web_server_2 ansible_host=<IP_from_terraform>
 
[web_servers:vars]
ansible_user=ec2-user
ansible_ssh_private_key_file=~/.ssh/your-key.pem

Pros:

  • Decouples Terraform and Ansible execution
  • Ansible operates on inventory reflecting current infrastructure state
  • More reliable and scalable for complex environments
  • Clearer separation of concerns

Cons:

  • Slight delay between provisioning and configuration if not orchestrated by CI/CD
  • Learning curve for dynamic inventory plugins
  • Requires secure management of Terraform state file access

Pattern 3: Cloud-Specific Dynamic Inventory Plugins

How it works: Ansible uses built-in inventory plugins (aws_ec2, azure_rm, gcp_compute) that query cloud provider APIs to discover resources. Terraform applies specific tags (environment:prod, role:webserver) that the Ansible plugin uses to filter and group hosts.

Ansible inventory plugin example (aws_inventory.yml):

plugin: aws_ec2
regions:
  - us-east-1
keyed_groups:
  - key: 'tags.Environment'
    prefix: env
  - key: 'tags.Role'
    prefix: role
filters:
  tag:Provisioner: terraform
hostnames:
  - ip-address

Terraform (applying tags):

resource "aws_instance" "web" {
  ami           = "ami-0c55b31ad2c455b55"
  instance_type = "t2.micro"
 
  tags = {
    Name        = "WebServer"
    Environment = "production"
    Role        = "webserver"
    Provisioner = "terraform"
  }
}

Pros:

  • Most flexible and scalable approach
  • Live discovery from cloud provider APIs
  • Excellent for dynamic, auto-scaling environments
  • Minimal coupling between tools

Cons:

  • Requires cloud API credentials for Ansible
  • Most complex setup initially
  • Plugin-specific syntax and configuration

Using Ansible Provisioner in Terraform

The Terraform Ansible provisioner provides direct integration, allowing Ansible to run immediately after resource creation.

Basic Usage

resource "aws_instance" "example" {
  ami           = "ami-0c55b31ad2c455b55"
  instance_type = "t2.micro"
 
  provisioner "ansible" {
    plays {
      playbook {
        file_path = "${path.module}/playbook.yml"
      }
    }
 
    on_failure = continue  # or fail
  }
 
  depends_on = [aws_instance.example]
}

With Dynamic Inventory

resource "null_resource" "configure_web_servers" {
  provisioner "local-exec" {
    command = "ansible-playbook -i ansible/inventory.ini playbooks/web_setup.yml"
  }
 
  depends_on = [aws_instance.web]
}

Passing Variables from Terraform to Ansible

resource "null_resource" "run_playbook" {
  provisioner "local-exec" {
    command = "ansible-playbook -i inventory.ini playbooks/app_deploy.yml -e 'app_version=${var.app_version} environment=${var.environment}'"
  }
}

One trap when passing credentials this way: shell commands launched from Terraform inherit environment variables, not provider blocks — a provider configured only through an aliased block exports nothing to the subprocess, so an ansible-playbook call can silently fall back to the runner's machine identity.

Dynamic Inventory from Terraform State

Reading Terraform State Directly

Ansible can read Terraform state files to build inventory, though this requires careful access management.

Using terraform_state plugin:

---
plugin: community.general.terraform_state
hostnames:
  - private_ip
groups:
  tag_Name: tags.Name

Accessing remote state:

For remote backends (Terraform Cloud, S3), ensure Ansible has appropriate credentials and access controls.

Generating Inventory from Terraform Outputs

A more secure approach uses Terraform to generate inventory files:

locals {
  ansible_inventory = {
    all = {
      children = {
        web_servers = {
          hosts = {
            for instance in aws_instance.web :
            instance.tags["Name"] => {
              ansible_host = instance.public_ip
              ansible_user = "ec2-user"
            }
          }
        }
      }
    }
  }
}
 
resource "local_file" "inventory" {
  content  = yamlencode(local.ansible_inventory)
  filename = "${path.module}/inventory.yml"
}

Shared Variables and Outputs

Passing Configuration Between Tools

Terraform outputs for Ansible:

output "database_endpoint" {
  value       = aws_db_instance.main.endpoint
  description = "Database endpoint for Ansible configuration"
}
 
output "app_servers" {
  value = {
    for instance in aws_instance.app :
    instance.tags["Name"] => instance.private_ip
  }
}

Ansible reading outputs (via local script):

#!/bin/bash
TERRAFORM_OUTPUTS=$(terraform output -json)
DB_ENDPOINT=$(echo $TERRAFORM_OUTPUTS | jq -r '.database_endpoint.value')

Terraform reading Ansible facts:

resource "null_resource" "gather_facts" {
  provisioner "local-exec" {
    command = "ansible-playbook playbooks/gather_facts.yml --extra-vars 'output_file=/tmp/facts.json'"
  }
}
 
locals {
  ansible_facts = jsondecode(file("/tmp/facts.json"))
}

Workflow Orchestration

Sequential Execution with CI/CD

GitLab CI/CD example:

stages:
  - provision
  - configure
  - test
 
provision_infrastructure:
  stage: provision
  script:
    - terraform init
    - terraform plan -out=tfplan
    - terraform apply tfplan
    - terraform output -json > terraform_outputs.json
  artifacts:
    paths:
      - terraform_outputs.json
      - .terraform
 
configure_with_ansible:
  stage: configure
  dependencies:
    - provision_infrastructure
  script:
    - ansible-playbook -i inventory.ini playbooks/app_setup.yml
  only:
    - main
 
smoke_tests:
  stage: test
  script:
    - ansible-playbook playbooks/smoke_tests.yml

GitHub Actions example:

name: Infrastructure and Configuration
 
on: [push]
 
jobs:
  provision:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: hashicorp/setup-terraform@v1
      - run: terraform init
      - run: terraform apply -auto-approve
      - run: terraform output -json > outputs.json
      - uses: actions/upload-artifact@v2
        with:
          name: terraform-outputs
          path: outputs.json
 
  configure:
    needs: provision
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/download-artifact@v2
        with:
          name: terraform-outputs
      - run: pip install ansible
      - run: ansible-playbook -i inventory.ini playbooks/setup.yml

That pip install ansible step gets less scrutiny than it should. Ansible is a Python program, so its behavior is a function of whatever interpreter the runner provides. In April 2026, a platform team in a regulated industry audited the managed runners in their pipeline and found Python 3.9.2 — end-of-life since October 31, 2025 — paired with pip 20.3.4, while their self-hosted agents ran Python 3.13.11 with pip 25.3. Anything pip-installed at run time, Ansible included, resolved to different versions depending on where the run landed. If Ansible is part of your run, pin the runtime by owning the runner image rather than installing into whatever Python happens to be there.

Real-World Architecture Patterns

Multi-Environment Architecture

infrastructure/
├── terraform/
│   ├── main.tf
│   ├── variables.tf
│   ├── outputs.tf
│   └── environments/
│       ├── dev/
│       ├── staging/
│       └── production/
└── ansible/
    ├── inventory/
    │   ├── dev.ini
    │   ├── staging.ini
    │   └── production.ini
    ├── playbooks/
    │   ├── common.yml
    │   ├── app_deploy.yml
    │   └── monitoring_setup.yml
    └── roles/
        ├── web_server/
        ├── database/
        └── monitoring/

GitOps Workflow

For a deeper look at related tooling, see our guide to the top GitOps tools for 2025.

  1. Developer commits infrastructure code (Terraform) and configuration code (Ansible) to Git
  2. CI/CD Pipeline validates Terraform and Ansible syntax
  3. Terraform runs in plan mode, showing infrastructure changes
  4. Manual Approval gates production deployments
  5. Terraform applies infrastructure changes
  6. Ansible runs automatically to configure infrastructure
  7. Monitoring validates configuration and application health

Auto-Scaling Pattern

# Terraform manages infrastructure
resource "aws_autoscaling_group" "app" {
  min_size         = 2
  max_size         = 10
  desired_capacity = 4
 
  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }
}
 
# User data triggers configuration
resource "aws_launch_template" "app" {
  user_data = base64encode(<<EOF
#!/bin/bash
# Wait for instance to be ready
sleep 30
# Configure with Ansible (golden image approach preferred)
# or trigger dynamic inventory update
EOF
  )
}

Best Practices for 2026

1. Use Dynamic Inventory Over Provisioners

Preferred approach:

  • Let Terraform provision infrastructure
  • Use cloud-native dynamic inventory plugins in Ansible
  • Decouple the tools for better maintainability

Avoid:

  • Tight coupling via provisioners for complex configurations
  • Mixing orchestration and configuration in single tool

2. Implement Proper State Management

# Store Terraform state securely
terraform {
  backend "s3" {
    bucket         = "terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

3. Tag Infrastructure for Orchestration

locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
    Provisioner = "scalr"  # or your management platform
    Team        = var.team
    CostCenter  = var.cost_center
  }
}

4. Use Configuration as Code

# Ansible playbooks should be idempotent
---
- name: Configure web servers
  hosts: web_servers
  gather_facts: yes
 
  roles:
    - common
    - web_server
    - monitoring
 
  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted

5. Implement Comprehensive Testing

# Test playbook syntax
ansible-playbook playbooks/site.yml --syntax-check
 
# Dry run before applying
ansible-playbook playbooks/site.yml --check
 
# Validate with ansible-lint
ansible-lint playbooks/
 
# Test with Terraform
terraform validate
terraform plan -detailed-exitcode

6. Secure Sensitive Data

# Use Terraform Cloud/Enterprise for secrets
variable "database_password" {
  sensitive = true
  type      = string
}
# Use Ansible Vault for sensitive playbook data
ansible-vault create group_vars/databases/vault.yml
ansible-playbook playbooks/site.yml --ask-vault-pass

7. Monitor Orchestration Operations

  • Log Terraform applies and Ansible playbook runs
  • Monitor for configuration drift
  • Track changes in version control
  • Alert on failed deployments
  • Document infrastructure changes

8. Plan for Disaster Recovery

# Keep Terraform state and Ansible playbooks in sync
# Document recovery procedures
# Test recovery regularly
# Maintain backups of state files

Operational Challenges and Solutions

State Management at Scale

Challenge: Securely storing and managing Terraform state files across teams

Solution:

  • Use remote state backends (Terraform Cloud, S3 with encryption, Consul). OpenTofu users can also use dynamic backend blocks to vary configuration by environment.
  • Implement state locking to prevent concurrent modifications
  • Use role-based access control for state file access
  • Maintain state file backups

Inventory Synchronization

Challenge: Keeping Ansible inventory synchronized with Terraform-provisioned resources

Solution:

  • Use dynamic inventory plugins that query cloud APIs
  • Implement automated inventory refresh in CI/CD pipelines
  • Use tagging strategies for inventory grouping
  • Monitor inventory for drift

Secrets Management

Challenge: Handling sensitive data (API keys, passwords, credentials) securely

Solution:

  • Use dedicated secrets management tools (HashiCorp Vault, AWS Secrets Manager)
  • Never commit secrets to version control
  • Rotate credentials regularly
  • Audit secret access

Cross-Team Collaboration

Challenge: Enabling multiple teams to work with Terraform and Ansible safely

Solution:

  • Use management platforms like Scalr for centralized governance — billed per run, with no per-user fees as the team grows
  • Implement role-based access control (RBAC)
  • Enforce policies through Policy as Code (OPA, Sentinel)
  • Maintain clear documentation and runbooks

Conclusion

Terraform handles the "what" of infrastructure, while Ansible handles the "how" of configuration management. Used together with proper integration patterns, they enable organizations to achieve:

  • Complete end-to-end automation from bare resources to fully deployed applications
  • Consistent, reliable infrastructure managed through code
  • Faster deployment cycles and improved disaster recovery
  • Better collaboration through Infrastructure and Configuration as Code practices
  • Scalable automation that grows with organizational needs

The key to success is choosing the right integration pattern for your specific needs—favoring loose coupling through dynamic inventory over tight coupling through provisioners, maintaining clear separation of concerns, implementing proper state and secrets management, and using higher-level orchestration platforms when operational complexity demands it. And when something does break, check the execution environment before the playbook: in our experience, a missing binary, a missing key file, or a mismatched Python on the runner explains most Terraform-Ansible integration failures.

For more information on orchestration alternatives and management platforms, see our guide on Ansible Tower and Automation Controller Alternatives.

Frequently asked questions

Should I use Terraform or Ansible?

Use both, for different layers. Terraform is declarative and stateful, built for provisioning and lifecycle management of infrastructure resources. Ansible is procedural and largely stateless, built for configuring software inside that infrastructure. Some overlap exists, but each tool's architecture is optimized for its own role.

How do I run an Ansible playbook from Terraform?

The simplest method is a local-exec provisioner on a null_resource that invokes ansible-playbook after the instance is reachable. It works for basic scenarios but tightly couples the tools, lengthens Terraform runs, and requires Ansible to be installed on the machine executing Terraform — which is often not the case on remote runners.

Why does ansible-playbook fail with 'not found' when Terraform runs on a remote platform?

Hosted Terraform run containers are minimal images that do not include Ansible, so a local-exec call fails with exit status 127 (ansible-playbook: not found). Fix it by installing Ansible in a pre-plan hook, or more durably by building a custom runner image with Ansible baked in and pointing your agent at it.

How does Ansible get its inventory from Terraform-provisioned hosts?

Three common options: generate an inventory file from Terraform outputs (for example with a local_file resource and yamlencode), read the Terraform state directly with the community.general.terraform_state inventory plugin, or use cloud-native inventory plugins like aws_ec2 that discover hosts by the tags Terraform applied.

Is Ansible a replacement for Terraform?

No. Ansible can provision infrastructure through cloud modules, but it does not maintain a state file or build a dependency graph, so it lacks Terraform's plan/apply lifecycle and resource tracking. The tools are complementary: Terraform defines what infrastructure exists; Ansible defines how systems inside it are configured.

How should SSH keys be handled when Ansible runs inside a remote Terraform run?

Do not assume the key file on your laptop exists on the runner. Materialize the key at run time with a local_file resource using sensitive content and file_permission "0400", or avoid runner-side SSH entirely by bootstrapping the host with cloud-init or remote-exec from inside the VM.
About the author
Sebastian StadilCEO at Scalr
Sebastian Stadil is the CEO of Scalr with 15+ years of DevOps experience. He started with AWS in 2004 and advised early Microsoft Azure and Google Cloud.