

Most teams don't debate this for long. They start with Terraform to provision infrastructure, hit a server that needs post-deployment configuration, and reach for Ansible. The tools end up running together.
The question worth asking isn't "which is better?" It's "where does one stop being the right tool?" That boundary is less obvious than it sounds. Ansible can provision cloud resources. Terraform has a provisioner block for running commands on instances. Teams use each tool outside its intended domain and eventually hit hard edges.
This post covers where those edges are, how to combine the tools without creating an unmaintainable tangle, and what's changed in 2026, including Terraform's license change and OpenTofu as a production-ready alternative.
Last updated: April 2026
At a glance Ansible is an open source automation platform maintained by Red Hat, licensed under GPL-3.0-or-later. Current stable release: ansible-core 2.20.4 / community package 13.5.0 (PyPI, March 2026). Terraform is HashiCorp's infrastructure provisioning tool, now under the Business Source License 1.1 since August 2023. Current stable release: 1.14.8 (GitHub, March 2026). OpenTofu is the open source (MPL-2.0) community fork under Linux Foundation governance. Current stable release: 1.11.6 (GitHub, April 2026).
Requirements for the demo in this post:
- A free GitLab account
- An AWS account (the demo runs within the 12-month free tier)
- A free env0 account
TL;DR: The complete repository is on GitLab.
Video walkthrough
The video below shows the full setup in env0: creating a project, template, and environment that provisions the Jenkins machine using both Terraform and Ansible. Skip ahead to the demo section if you want the code walkthrough first.
What is Ansible?
Ansible is a configuration management and automation platform that runs over SSH, with no agent software required on the target host. It uses YAML-based playbooks to define what should happen on a system: install packages, copy files, start services, restart processes.
Red Hat acquired Ansible in 2015 and maintains it today. The community-driven distribution ships as the ansible package on PyPI; the core engine ships separately as ansible-core. Both are open source under the GPL-3.0-or-later license.
Ansible's core strength is idempotent configuration management: you describe the desired state of a system, and Ansible will make it so, whether it's running for the first time or the hundredth. It doesn't replace a system to change it; it modifies what's there. That's the key distinction from Terraform's philosophy, and it matters when you're deciding which tool fits a given task.
In 2026, Ansible distributes plugins, modules, and roles through a collections model, a shift from the older monolithic structure that's worth knowing if you're picking up Ansible for the first time. Collections install via ansible-galaxy collection install <namespace.collection> and are referenced by their fully qualified name (for example, community.docker.docker_container). The demo in this post uses community.docker for container management.
One feature that matters for developer confidence: ansible-playbook --check --diff runs a playbook in dry-run mode, showing what would change without making changes. It's Ansible's equivalent of terraform plan and should be part of any testing workflow before applying to production.
Related reading: The ultimate Ansible tutorial. A step-by-step guide covering installation, inventory, playbooks, and roles.
What is Terraform?
Terraform is a provisioning tool built around declarative Infrastructure as Code (IaC). You describe what infrastructure should exist (VPCs, subnets, EC2 instances, RDS clusters) in HashiCorp Configuration Language (HCL), and Terraform calculates what to create, modify, or destroy to reach that state.
The tool maintains a state file that represents everything it manages. When you run terraform plan, it compares the live state against your configuration and shows you exactly what would change before anything runs. When you run terraform apply, it makes those changes. The state file is what makes terraform destroy safe: it knows precisely what it created. For teams managing many resources, this explicitness is the reason Terraform is the default choice for provisioning, not just a preference.
Related reading: Terraform tutorial: a complete guide. Covers state management, providers, modules, and the apply lifecycle in depth.
A note on the BSL license. Terraform moved from the open source MPL 2.0 license to the Business Source License 1.1 (BSL) in August 2023. BSL is not closed source; the code is publicly visible on GitHub and free to use internally. Per the LICENSE file, the Change License is MPL 2.0 and applies four years from the date each version is published, so the earliest BSL versions convert to open source around 2027. The restriction applies specifically to companies building competing hosted services on top of Terraform. If you're using Terraform to manage your own infrastructure, the license change doesn't affect your day-to-day work.
If your organization requires an OSI-approved open source license, OpenTofu is the community fork under Linux Foundation governance, licensed under MPL-2.0, actively maintained at version 1.11.6, and a drop-in replacement for Terraform 1.6 and earlier configurations.
Related reading: OpenTofu vs Terraform: a practical guide for enterprise teams. Decision framework for teams evaluating a migration.
Ansible vs Terraform: core differences
The fundamental difference is philosophy, not feature overlap.
Terraform is declarative and immutable. You say what should exist; Terraform makes it so. When something changes, its preferred pattern is to rebuild from scratch rather than patch a running system. This makes it the right tool for infrastructure provisioning (networks, VMs, databases, load balancers) where reproducibility matters.
Ansible is procedural and mutable. You describe what steps to run, in what order, on a target host. It operates on existing systems: installing software, applying patches, restarting services. It's designed for Day 1+ operations where you're working with infrastructure that already exists.
Comparing them side by side makes the right choice clearer for most tasks:
| Feature | Ansible | Terraform |
|---|---|---|
| Primary use case | Configuration management, app deployment | Infrastructure provisioning |
| Approach | Procedural (imperative) | Declarative |
| Infrastructure mutability | Mutable; modifies existing systems | Immutable; prefers rebuild over patch |
| License | Open source (GPL-3.0-or-later) | Source-available (BSL 1.1 since Aug. 2023); converts to MPL 2.0 four years per version |
| Cloud support | All major clouds | All major clouds |
| Agent requirement | Agentless (SSH/WinRM) | Agentless |
| State management | No persistent state file | Maintains state file for all managed resources |
| Language | YAML playbooks | HCL (HashiCorp Configuration Language) |
| Resource ordering | Manual; you define execution order | Automatic; manages dependency graph |
| Idempotency | Supported; depends on module implementation | Built into the declarative model |
| Dry run / preview | --check --diff mode |
terraform plan |
| Change detection | Partial; may miss tag changes or external modifications | Detects drift against state; continuous detection requires tooling like env0 |
| Infrastructure teardown | Requires a separate playbook; reverse order is manual | Single command (terraform destroy) |
The "Change detection" row deserves extra attention. Terraform detects drift at plan time by comparing live state to your configuration. But if resources change between applies (manual console edits, for example), Terraform only catches that drift the next time someone runs plan. Continuous drift detection, which surfaces changes as they happen rather than at next run time, is what tools like env0 add on top. The env0 drift detection docs cover how that works.
Related reading: Infrastructure as Code 101. If you're newer to IaC and want the foundational context before comparing specific tools.
When to use Ansible, when to use Terraform
A useful frame is Day 0, Day 1, and Day 2 operations, a pattern the Asian Development Bank documented publicly when describing how they structure their infrastructure lifecycle.
Day 0 is initial provisioning: standing up networks, VMs, databases, and cloud services from scratch. This is Terraform's domain. Its declarative model and state tracking make it the right choice for defining "what exists." Terraform modules are the standard pattern for packaging reusable infrastructure here.
Day 1 is initial configuration of what was provisioned: installing Docker, deploying application code, applying OS hardening. Ansible takes over here because you're describing what to do to a running system, not what the system should be.
Day 2+ is ongoing operations: patching, scaling, updates, compliance enforcement across a fleet. Ansible handles this well. Ansible conditionals and variable layering become important at this stage as the number of managed hosts grows.
The practical rule: reach for Terraform when you're creating cloud resources managed by a provider API (EC2, RDS, GCP networks, Azure VNETs) or any infrastructure that needs clean teardown semantics. Use Ansible when you're working with software running on existing servers, or when you need compliance enforcement, patch management, or application deployment across a fleet.
Some teams use Ansible for cloud provisioning (it has AWS and GCP modules). It works until you need to track what was created across multiple runs or destroy cleanly. Ansible has no state file. For cloud infrastructure provisioning, Terraform or OpenTofu is the better choice.
Using Ansible and Terraform together
Most production setups use both. The tools operate at different layers and don't conflict.
The standard pattern:
- Terraform provisions infrastructure (VPC, subnets, EC2 instance, security groups, SSH keys)
- Terraform exposes values needed by the next step (public IP, private key) as outputs
- Ansible consumes those outputs to configure the provisioned instance (install Docker, deploy an application)
The orchestration challenge is the handoff: Terraform's outputs need to populate Ansible's inventory before the playbook runs. In a basic CI/CD pipeline, you handle this with shell scripts. At scale (multiple environments, approval workflows, multiple teams), that approach becomes brittle.
Related reading: Terraform + Ansible = total flexibility. More detail on the integration patterns that hold up in production.
How env0 handles the orchestration
env zero's Custom Flows handle this natively. An env0.yml file at the root of your repository defines hooks that run before and after each Terraform stage: init, plan, apply, output. The Ansible playbook runs as a post-apply hook, using Terraform's outputs as inputs, all within the same deployment environment.
The value here isn't just execution order. It's what wraps that execution. Approval workflows, audit logs, Role-Based Access Control (RBAC), and Time-to-Live (TTL) policies apply to the combined Terraform + Ansible run as a single unit. You're not managing pipeline permissions and IaC permissions separately.
The env0 Custom Flows documentation covers all available hooks for Terraform, OpenTofu, and other supported frameworks. The env0 Ansible integration docs cover first-class Ansible support if you're running Ansible as the primary tool rather than as a Custom Flow step.
Demo: provisioning an EC2 instance and configuring Jenkins
Let's walk through the complete example. We'll use Terraform to provision an AWS EC2 instance and Ansible to deploy Docker and Jenkins on it, with env0 orchestrating the handoff.
The scenario: a short-lived Jenkins server for CI testing, automatically torn down when it reaches a TTL limit, saving cost if someone forgets to shut it down manually. env0's TTL policy handles the teardown. Terraform handles the infrastructure; Ansible handles Jenkins.
The complete repository is on GitLab.
The env0.yml Custom Flow
deploy:
steps:
terraformOutput:
after:
- ansible-galaxy collection install community.docker
- terraform output -raw private_key > /tmp/myKey.pem
- chmod 400 /tmp/myKey.pem
- sed -i "s/\[placeholder_app\]/$(terraform output -raw public_ip)/g" Ansible/inventory
- pip3 install ansible
- cd Ansible && ansible-playbook --private-key /tmp/myKey.pem -i inventory jenkinsPlaybook.yaml
After Terraform generates its output, six steps run in sequence. First, the community.docker Ansible collection installs. This is required because the playbook uses community.docker.docker_image and community.docker.docker_container; skipping this step causes a module-not-found error at playbook start. Then the private key is extracted to a temp file and locked to read-only (chmod 400 is the minimum permission SSH requires; looser permissions cause ssh to refuse the key entirely).
The sed command replaces the [placeholder_app] marker in the inventory file with the EC2 instance's public IP. Note the escaped brackets in the pattern: \[placeholder_app\]. Without the backslashes, sed interprets the brackets as a regex character class matching any single character in that set, not the literal string.
One thing worth flagging: terraform output -raw private_key prints the sensitive private key in plaintext so Ansible can use it for SSH. This is intentional and acceptable in a controlled CI environment, but it means the key appears in the execution log. In production, use a secrets manager instead.
The inventory file keeps the marker pattern so it works across every environment without modification:
[all:children]
jenkins
[all:vars]
ansible_user=ubuntu
ansible_python_interpreter=/usr/bin/python3
[jenkins]
jenkinsvm ansible_host=[placeholder_app]
Terraform configuration
The main.tf provisions a full network stack: VPC, subnet, internet gateway, security group (ports 22 for SSH, 8080 for the Jenkins web interface, and 50000 for Jenkins agent connections), an Elastic IP, and an EC2 instance.
The AMI lookup targets Ubuntu 22.04 LTS. Ubuntu 20.04 standard support ended in May 2025; use jammy (22.04) or noble (24.04) for new infrastructure. The owners value 099720109477 is Canonical's official AWS account ID:
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
owners = ["099720109477"] # Canonical's official AWS account
}
The TLS private key for SSH access is generated by Terraform and exposed as a sensitive output. Terraform marks sensitive = true outputs to suppress them in CLI output (displaying (sensitive value) in terraform plan), though terraform output -raw will still print them in plaintext. env0 surfaces sensitive outputs as masked environment variables within the Custom Flow context.
output "url" {
value = "http://${aws_eip.env0.public_dns}"
}
output "public_ip" {
value = aws_eip.env0.public_ip
}
output "private_key" {
value = tls_private_key.env0.private_key_pem
sensitive = true
}
The tls_private_key resource attribute private_key_pem is documented in the Terraform Registry and outputs the private key in PEM format, which is what SSH and Ansible expect.
For production use, generating SSH keys through Terraform state isn't ideal. The private key sits in the state file, which is a secret management problem. In a production setup, pull keys from a secrets manager (HashiCorp Vault, AWS Secrets Manager) rather than generating them in Terraform. This demo uses the TLS provider for simplicity.
Related reading: Protecting secrets with Ansible Vault. The Ansible side of secrets management when combining these tools.
Ansible playbook
The jenkinsPlaybook.yaml configures the provisioned instance in six logical stages: install prerequisites, add the Docker GPG key and repository, install Docker, install the Python Docker SDK, pull the Jenkins image, and start the container. It targets ansible-core 2.16+, which is required for the retries without until syntax used on the apt task.
- name: Set up Jenkins with Docker
hosts: jenkins
become: true
tasks:
- name: Install pip3 and unzip
apt:
name: ["python3-pip", "unzip"]
state: present
update_cache: yes
retries: 5
delay: 5
- name: Add Docker GPG key
# Note: apt_key is deprecated on Ubuntu 22.04+; for production use
# ansible.builtin.deb822_repository (requires ansible-core 2.15+) instead
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Add Docker repository
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
- name: Install Docker
apt:
name: docker-ce
state: present
update_cache: yes
- name: Install Python Docker SDK
pip:
name: docker
state: present
- name: Pull Jenkins image
community.docker.docker_image:
name: samgabrail/jenkins-tf-vault-ansible:latest
source: pull
- name: Prepare Jenkins data directory
file:
path: /home/ubuntu/jenkins_data
state: directory
owner: "1000"
group: "1000"
mode: "0755"
- name: Start Jenkins container
community.docker.docker_container:
name: jenkins
image: samgabrail/jenkins-tf-vault-ansible:latest
state: started
restart_policy: unless-stopped
ports:
- "8080:8080"
- "50000:50000"
volumes:
- /home/ubuntu/jenkins_data:/var/jenkins_home
A few things worth flagging in this playbook:
The apt_key module works but is deprecated on Ubuntu 22.04 and later. For production playbooks, use ansible.builtin.deb822_repository (available in ansible-core 2.15+), which handles both the GPG key and repository in a single task using the modern /etc/apt/sources.list.d/*.sources format.
The retry logic on the initial apt install matters: Ubuntu package mirrors can be temporarily unavailable when a fresh instance first connects. Five retries with a five-second delay covers most transient failures without aborting the run. This syntax (retries without an until clause) is valid in ansible-core 2.16+ and will retry until the task succeeds.
The Jenkins image (samgabrail/jenkins-tf-vault-ansible:latest) is the tutorial author's community image pre-configured with Terraform and Vault tooling. It's available on Docker Hub and last updated in May 2024. For a production deployment, build and maintain your own Jenkins image.
Related reading: Ansible playbooks: a step-by-step guide. How to structure playbooks for repeatable, idempotent runs.
Common pitfalls when combining these tools
Race conditions on Terraform outputs. The Custom Flow above runs Ansible after terraformOutput. If you're building this pattern in a basic CI/CD pipeline, Ansible must not start before Terraform's apply completes and outputs are populated. The failure mode is a cryptic SSH error because the IP address was never written to the inventory. Make the dependency explicit.
Using Ansible to provision cloud resources. Ansible's amazon.aws collection can create EC2 instances and S3 buckets. The problem surfaces when you need to track what was created across multiple runs or tear down cleanly. Ansible has no state file, so there's no equivalent to terraform destroy. Cloud provisioning belongs in Terraform or OpenTofu.
Drift accumulates at the Ansible layer. If Terraform provisions an instance and Ansible patches it over time, the running instance gradually diverges from your baseline image. When Terraform replaces the instance (after an AMI update, for example), Ansible needs to re-run to reconfigure it. Plan for this: your IaC pipeline should include the Ansible configuration step, not just the Terraform apply. Otherwise you end up with infrastructure that can't be reproduced from code alone.
SSH key management at scale. Generating SSH keys through the TLS provider and storing them in Terraform state is fine for demos. In production with multiple environments, the private keys accumulate in state files. Move key management to a dedicated secrets store early; retrofitting it later is harder than it looks.
Sensitive outputs in CI logs. terraform output -raw on a sensitive value prints it in plaintext. If your CI system logs all shell output, that's where the key lands. Use ANSIBLE_NOFILES_LOG or ensure your CI is configured to mask secrets in logs before using this pattern at scale.
What env0 adds to this stack
Running Ansible and Terraform together in CI/CD is straightforward in a single environment. The challenges compound when you have many environments, multiple teams, and approval requirements before anything applies.
The specific problems that surface at scale:
- Who approved this apply, and is there an audit trail?
- How do you know the Ansible step actually ran against the right host?
- Which environment still has the Jenkins server running from last week's test?
env zero addresses these at the platform level. Custom Flows manage the Terraform-to-Ansible handoff with defined hooks. TTL policies automatically destroy ephemeral environments when they exceed their configured age. That's why the Jenkins demo uses one: without it, a test server provisioned on a Friday is still running Monday morning. RBAC controls which teams can apply to which environments. Drift detection surfaces when live infrastructure diverges from Terraform state, which matters when Ansible is making ongoing changes to provisioned resources.
None of this requires rewriting your existing Terraform or Ansible code. env zero wraps the execution environment; your IaC stays as-is.
Related reading: What is Checkov and how to use it for IaC security. env zero integrates Checkov as another Custom Flow hook for policy enforcement before apply.
Try it with env zero
The demo in this post is fully functional with a free env zero account, a GitLab account, and AWS free-tier access.
Start a free trial or book a demo to see how Custom Flows fit your existing IaC stack.
References
- Ansible documentation: official Ansible docs
- ansible-core 2.20.4 on PyPI: current stable release and license
- Ansible collections guide: official collections docs
- Ansible COPYING (GPL-3.0-or-later): license file
- ansible.builtin.deb822_repository module: recommended replacement for apt_key on Ubuntu 22.04+
- community.docker.docker_image module: official docs
- Terraform documentation: HashiCorp Developer
- Terraform 1.14.8 release: GitHub
- Terraform LICENSE (BSL 1.1): official license file with Change Date terms
- HashiCorp BSL license FAQ: HashiCorp
- tls_private_key resource docs: Terraform Registry
- OpenTofu 1.11.6 release: GitHub
- Ubuntu release cycle and EOL dates: Canonical
- Find Ubuntu AMIs on AWS: confirms Canonical account ID 099720109477
- env0 Custom Flows documentation: env0
- env0 TTL policy documentation: env0
- env0 Ansible integration: env0
- env0 drift detection: env0
- Asian Development Bank: Terraform + Ansible workflow: case study
Frequently asked questions
What is the difference between Ansible and Terraform?
Ansible is a configuration management tool that operates on existing infrastructure, installing software, applying configuration, and running commands over SSH. Terraform is an infrastructure provisioning tool that defines what cloud resources should exist and manages their lifecycle through a state file. They operate at different layers. Most production setups use both: Terraform provisions the infrastructure, Ansible configures it.
Is Terraform open source in 2026?
Not exactly. Terraform moved from the open source MPL 2.0 license to the Business Source License 1.1 (BSL) in August 2023. BSL is source-available; the code is publicly visible on GitHub and free to use internally. Per the Terraform LICENSE file, each published version converts to MPL 2.0 four years after its release. The restriction applies to companies offering Terraform as a competing hosted service. If you're using Terraform to manage your own cloud infrastructure, the license change doesn't affect you. OpenTofu is the fully open source (MPL-2.0) community fork if your organization requires an OSI-approved license.
Can I use Ansible instead of Terraform?
Technically yes, but you'll run into limits quickly. Ansible has cloud provisioning modules for AWS, GCP, and Azure. The problem: Ansible has no state file, so it can't track what it created, detect drift, or perform clean teardowns across runs. For cloud infrastructure provisioning, Terraform or OpenTofu is the better choice. Ansible belongs in the configuration management layer.
Does Terraform work with Ansible?
Yes. The two tools complement each other at different layers. The standard pattern: Terraform provisions infrastructure and exposes outputs (IP addresses, credentials), Ansible consumes those outputs to configure the provisioned systems. The orchestration (making sure Ansible runs after Terraform with the right inputs) can be handled by your CI/CD pipeline or by a platform like env0, which provides Custom Flows specifically for this pattern.
What are Day 0, Day 1, and Day 2 operations?
Day 0 is initial infrastructure provisioning: standing up networks, VMs, databases, and cloud services. Terraform's domain. Day 1 is initial configuration of what was provisioned, including installing software, configuring services, and deploying applications. Ansible takes over here. Day 2+ is ongoing operations: patching, scaling, updates, compliance enforcement across a fleet. Ansible handles this layer well, though teams often add purpose-built tools depending on complexity and scale.
What is the Ansible equivalent of terraform plan?
ansible-playbook --check --diff runs a playbook in dry-run mode, showing what would change without making any changes to the target systems. Adding --diff alongside --check shows the exact line-by-line changes to files and configuration. Not every Ansible module supports check mode (modules that call external APIs may not), but for most configuration management tasks, it's a reliable way to preview changes before applying.
What is new in Ansible and Terraform in 2026?
ansible-core 2.20.4 is the current stable release (March 2026), with the community package at version 13.5.0. The collections model is now the standard distribution format; plugins, modules, and roles ship as collections rather than the older monolithic structure. Terraform 1.14.8 is the current stable release (March 2026). OpenTofu 1.11.6 reached general availability in April 2026 and continues tracking Terraform's feature set under Linux Foundation governance.
Can env0 orchestrate both Ansible and Terraform?
Yes. env zero's Custom Flows support pre- and post-hooks at each stage of a Terraform or OpenTofu run. You can install Ansible, build an auto-generated inventory from Terraform outputs, and execute a playbook as a post-apply step, all within the same env0 environment. Approval workflows, audit logging, and TTL-based teardowns apply to the combined run as a single unit, not as separate CI/CD jobs. env zero also supports Ansible as a first-class IaC framework. See the env0 Ansible integration docs.

.webp)

![Using Open Policy Agent (OPA) with Terraform: Tutorial and Examples [2026]](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/69d6a3bde2ffe415812d9782_post_th.png)