In this blog post, I would like to compare Ansible and Terraform in relation to Day-2 automation. I will leave out the usual use cases, as I believe that Terraform cannot be reduced solely to infrastructure provisioning and Ansible to Day-2 automation. Both tools can be used for both purposes. As mentioned, I will focus explicitly on Day-2 automation in this blog post.
What questions do I want to answer here?
- When does it make sense to use Ansible?
- When does it make sense to use Terraform?
- Are there any limitations?
Introduction#
Let’s start with a brief introduction to both tools.
Ansible#
“Ansible is an open source IT automation engine that automates provisioning, configuration management, application deployment, orchestration, and many other IT processes.” – Redhat
Ansible uses the declarative programming language YAML. In Ansible, automation processes are executed by so-called playbooks. These playbooks contain imperative tasks (which, in my opinion, makes the entire tool imperative again) consisting of modules from various collections that are used to configure your target system. These collections are comparable to class libraries and modules are comparable to methods in other programming languages.
Terraform#
“Terraform provides organizations with a single workflow to provision their cloud, private datacenter, and SaaS infrastructure and continuously manage it throughout its lifecycle.” – Hashicorp
Based on the description, it seems that Terraform is actually rather unsuitable for Day-2 automation of systems. However, this is not the case. Many manufacturers today place a strong focus on so-called Terraform providers for their own tools. Examples include VMware, Microsoft, Google, and many large cloud providers. For this reason, Terraform can be used in a wide variety of ways today. Terraform uses HCL as a declarative programming language. This is where so-called resources and data sources come into play. Resources are used to perform new configuration steps, which are usually mapped as objects by the API. Data sources are used to extract existing data from the system to be configured. When the configuration is applied to the target system, Terraform stores its state in a so-called terraform state file. This is updated with every change and is itself versioned. A major advantage of Terraform is the combination of the provider and the tool itself in the underlying structure. Developers do not have to worry about the order in which things need to be executed so that they build on each other logically. Terraform takes care of this.
For example: When configuring my software-defined network in the cloud, You have to pay attention to which element is created when in imperative programming languages, because it may be that these elements are directly dependent on each other in terms of their existence. For example, if I want to create a router and an associated segment, the router must be created first, otherwise you cannot link my segment.
Use Cases#
When does it make sense to use Ansible?#
Let’s assume that there is no separate Terraform provider for my tool (in the following example, a web server with API). In this case, it is still possible to execute API calls with Terraform. But does this make sense? In the following example, we want to query facts about cats from the website “catfact.ninja” via the API. The API is designed in such a way that we get a different fact with each call. Let’s start with Ansible:
- name: API Call Use Case
hosts: localhost
tasks:
- name: Get cat fact
ansible.builtin.uri:
url: "https://catfact.ninja/fact"
register: response
- name: Show cat fact
ansible.builtin.debug:
var: response.json.factWhenever we execute the playbook, we get a new fact (as desired).
% ansible-playbook playbook.yml
PLAY [API Call Usecase] *****************************************************************************************************************************************
TASK [Get cat fact] *********************************************************************************************************************************************
ok: [localhost]
TASK [Show cat fact] ********************************************************************************************************************************************
ok: [localhost] => {
"response.json.fact": "Cats have 30 vertebrae (humans have 33 vertebrae during early development; 26 after the sacral and coccygeal regions fuse)"
}
% ansible-playbook playbook.yml
PLAY [API Call Usecase] *****************************************************************************************************************************************
TASK [Get cat fact] *********************************************************************************************************************************************
ok: [localhost]
TASK [Show cat fact] ********************************************************************************************************************************************
ok: [localhost] => {
"response.json.fact": "The first cartoon cat was Felix the Cat in 1919. In 1940, Tom and Jerry starred in the first theatrical cartoon “Puss Gets the Boot.” In 1981 Andrew Lloyd Weber created the musical Cats, based on T.S. Eliot’s Old Possum’s Book of Practical Cats."
} But what about Terraform?
In the Terraform example, we use the Terracurl provider to execute the API calls. To get the response back as output, I created an Output in the HCL code.
terraform {
required_providers {
terracurl = {
source = "devops-rob/terracurl"
}
}
}
resource "terracurl_request" "cat_fact" {
name = "cat fact"
url = "https://catfact.ninja/fact"
method = "GET"
response_codes = [200]
}
output "response" {
value = terracurl_request.cat_fact.response
}Until a Terraform configuration is fully executed, there are several validation and simulation steps. When we execute terraform plan, we get a simulation back. It’s basically a “what if” scenario.
% terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# terracurl_request.cat_fact will be created
+ resource "terracurl_request" "cat_fact" {
+ destroy_request_url_string = (known after apply)
+ destroy_retry_interval = 10
+ destroy_timeout = 10
+ drift_marker = (known after apply)
+ id = (known after apply)
+ method = "GET"
+ name = "cat fact"
+ request_url_string = (known after apply)
+ response = (known after apply)
+ response_codes = [
+ "200",
]
+ retry_interval = 10
+ skip_destroy = true
+ skip_read = true
+ status_code = (known after apply)
+ timeout = 10
+ url = "https://catfact.ninja/fact"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ response = (known after apply)As we can see, Terraform plans to execute the API call but, logically, does not yet know the response. So we execute terraform apply and get the response:
% terraform apply
Changes to Outputs:
+ response = (known after apply)
terracurl_request.cat_fact: Creating...
terracurl_request.cat_fact: Creation complete after 0s [id=cat fact]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
response = "{\"fact\":\"In Ancient Egypt, when a person's house cat passed away, the owner would shave their eyebrows to reflect their grief.\",\"length\":117}"Everything is functional so far. One would think that if terraform apply is executed again, we will get a different output. Let’s test it:
% terraform apply -auto-approve
terracurl_request.cat_fact: Refreshing state... [id=cat fact]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
response = "{\"fact\":\"In Ancient Egypt, when a person's house cat passed away, the owner would shave their eyebrows to reflect their grief.\",\"length\":117}"As we can see, nothing has changed. But why is that? When we execute the command terraform plan, Terraform compares the desired state of the configuration with the target system and the so-called Terraform state file. However, because our API call cannot be simulated by the provider, Terraform thinks that nothing has changed and therefore nothing will change for the API call either. This also answers the question: What are the limitations of Terraform?
When does it make sense to use Terraform?#
Let’s assume we have a suitable Terraform provider. What are the advantages? Let’s look at an example where we want to create a simple text file and then a second one with the sha256 checksum of the first file. In Ansible, we need the following three tasks for this:
- name: Create files
hosts: localhost
tasks:
- name: Create file foo
copy:
dest: "foo.bar"
content: "foo!"
- name: Get sha256 sum of foo.bar
stat:
path: foo.bar
checksum_algorithm: sha256
get_checksum: yes
register: foo_bar_sha256
- name: Create file bar
copy:
dest: bar.foo
content: "{{ foo_bar_sha256.stat.checksum }}"How can we do this imperative process in Terraform? In Terraform, we only specify the two files with a link:
resource "local_file" "bar" {
content = local_file.foo.content_sha256
filename = "${path.module}/bar.foo"
}
resource "local_file" "foo" {
content = "foo!"
filename = "${path.module}/foo.bar"
}Terraform automatically recognizes that the files are dependent on each other and determines the order itself.
We can track these dependencies with the command terraform graph:
% terraform graph
digraph G {
rankdir = "RL";
node [shape = rect, fontname = "sans-serif"];
"local_file.bar" [label="local_file.bar"];
"local_file.foo" [label="local_file.foo"];
"local_file.bar" -> "local_file.foo";
}Conclusion – What can we learn from this?#
When it comes to flexibility, Ansible is usually the better choice. It allows you to achieve vendor independence, and if there are suitable collections available, so much the better. However, if there is a vendor-driven Terraform provider, this often saves a lot of work. Terraform also offers major advantages when it comes to versioning the state file and tracking changes. Ultimately, both are very good tools that can be combined excellently with CI/CD tools such as GitLab CI/CD, Ansible Automation Platform, or GitHub Actions.

