Terraform vs Ansible - Demo the Differences - Part 2
Discover the distinctions between Terraform and Ansible in this Terraform vs Ansible comparison demonstration. The outcome may surprise you!
IaCHashiCorpTerraformAWSAnsible
Video
Below is a video explanation and demo.
AWS Lambda Terraform Tutorial with API Gateway
Video Chapters
You can skip to the relevant chapters below:
- 00:00 - Introduction
- 00:33 - Demo Setup
- 02:45 - Ansible Demo Starts
- 15:41 - Terraform Demo Starts
- 24:59 - Terraform and Ansible Working Together
- 27:22 - Terraform 101 Course
Overview
Choosing the right tool for provisioning and configuring infrastructure can be tricky, as there are several options available. Among them, Terraform and Ansible are two of the most widely used. In order to determine which one is the best fit for you, it's important to understand the key differences between the two. This demo comparison will help you make an informed decision.
In this blog post, we will take a practical approach by demoing both Terraform and Ansible to see the difference. This is part 2 of a 2-part blog series. In part 1 called Terraform vs Ansible - Learn the Differences, we discussed many concepts of Infrastructure as Code and Configuration Management to understand Terraform and Ansible better. In particular, we explored the following in case you'd like to refer back to them:
- What is Terraform?
- What is Ansible?
- Infrastructure as Code
- Configuration Management
- Infrastructure State
- Mutable Infrastructure
- Immutable Infrastructure
- Idempotence
- Imperative Programming
- Declarative Programming
Code
Pre-requisites
The following is required to follow along:
- Terraform or use GitHub's Codespaces as your development environment. The code repo is equipped with it.
- An AWS account or if you're subscribed to the TeKanAid Academy Subscription you get your own AWS account on demand.
Ansible Demo
Let's get started with the Ansible demo.
Export the environment variables for AWS
export AWS_ACCESS_KEY_ID=
export AWS_SECRET_ACCESS_KEY=
Run the Playbook
Run the following commands which will change the director to Ansible
and run the playbook.
cd Ansible
ansible-playbook playbook_create.yml
Below is the CLI output after running the playbook.
Ansible CLI Output after Creating
Now go to the AWS console and check the results in the EC2 dashboard.
AWS Console for Ansible Build
Notice the two EC2 instances created and the tag.
Destroy the Environment
Now let's destroy the environment. For Ansible, we can't just run a CLI command to use the same playbook to destroy. We need to create a new playbook and reverse the order in which we created the resources. Run the command below.
ansible-playbook playbook_destroy.yml
Below is the CLI output after running the destroy
playbook.
Ansible CLI Output after Destroying
Ansible Configuration
Below is our folder structure. Notice that we have one folder for Ansible
and one for Terraform
.
Folder Structure
Let's dig deeper into the create
and destroy
playbooks.
Create Playbook
Below is the create
playbook.
---
- name: Provision VPC and EC2 instances
hosts: localhost
connection: local
gather_facts: false
collections: [amazon.aws]
vars:
ansible_python_interpreter: /home/codespace/.python/current/bin/python
count: 2
vpc_cidr: 10.0.0.0/16
subnet_cidr: 10.0.1.0/24
instance_type: t2.micro
region: us-east-1
ami: ami-06878d265978313ca
vpc_name: my_vpc
tasks:
- name: Create VPC
ec2_vpc_net:
name: "{{ vpc_name }}"
cidr_block: "{{ vpc_cidr }}"
region: "{{ region }}"
register: vpc
- name: Create subnet
ec2_vpc_subnet:
vpc_id: "{{ vpc.vpc.id }}"
cidr: "{{ subnet_cidr }}"
region: "{{ region }}"
register: subnet
- name: Create security group
ec2_group:
name: web-server-sg
description: Security group for web server instances
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ region }}"
rules:
- proto: tcp
from_port: 80
to_port: 80
cidr_ip: 0.0.0.0/0
register: security_group
- name: Create key pair
ec2_key:
region: "{{ region }}"
name: my_key
register: key_pair
- name: Create an EC2 Instance
ec2_instance:
key_name: "{{ key_pair.key.name }}"
instance_type: "{{ instance_type }}"
image_id: "{{ ami }}"
region: "{{ region }}"
vpc_subnet_id: "{{ subnet.subnet.id }}"
security_group: "{{ security_group.group_id }}"
exact_count: "{{ count }}"
wait: yes
network:
assign_public_ip: true
tags:
Environment: Prod
register: ec_instances
This is an Ansible playbook that provisions a VPC and EC2 instances in AWS. The playbook is set to run on the localhost and uses the local connection. The gather_facts
variable is set to false
, which means that Ansible will not gather information about the system before running the tasks. This playbook uses the amazon.aws
collection, which contains the modules necessary to interact with AWS services.
The playbook then sets several variables, including the python interpreter path, the number of instances to create, the CIDR blocks for the VPC and subnet, the instance type, the region, and the AMI ID.
The playbook then performs several tasks:
- The first task creates a VPC using the
ec2_vpc_net
module and the variables defined earlier. The result is registered in thevpc
variable. - The second task creates a subnet using the
ec2_vpc_subnet
module and thevpc_id
from the previous task. The result is registered in thesubnet
variable. - The third task creates a security group using the
ec2_group
module with the specified name, description and VPC id. The result is registered in thesecurity_group
variable. - The fourth task creates a key pair using the
ec2_key
module with the specified name and region, the result is registered in thekey_pair
variable. - The fifth task creates EC2 instances using the
ec2_instance
module. It uses thekey_name
,instance_type
,image_id
,vpc_subnet_id
,security_group
, andcount
defined earlier. The instances are launched in the specified region and they wait until the instances are running before moving on to the next task. The instances are also assigned a public IP and have a tag namedEnvironment
with the valueProd
. The result is registered in the ec_instances variable
It is worth mentioning that the playbook makes use of the register
keyword which allows using the output of a task in the following task.
Destroy Playbook
The destroy playbook is similar to the create one, but in reverse order. You can view it in the code repo.
Terraform Demo
Now let's move on to the Terraform demo.
Run the Terraform Demo
Run the commands below which change the directory to Terraform
and runs a few Terraform commands. The explanation for each is found below.
cd Terraform
terraform init
terraform plan
terraform apply
terraform init
This command is used to initialize a Terraform working directory. It is typically the first command that should be run after writing a new Terraform configuration or cloning an existing one. The command is used to download and install any required provider plugins, as well as to set up the backend for storing state. The backend can be either a local file or a remote service such as Terraform Cloud or AWS S3.
terraform plan
The terraform plan command is used to create an execution plan. It shows you what actions Terraform will take when you call the terraform apply command. This command is used to preview the changes that will be made to your infrastructure and to ensure that the configuration is correct before applying it. It is a good practice to run the plan command before applying any changes to your infrastructure.
terraform apply
The terraform apply command is used to apply the changes defined in the Terraform configuration to the infrastructure. It reads the execution plan created by the terraform plan command and makes the necessary changes to the infrastructure. It is important to review the changes that will be made before running this command, as it can make changes to production systems. Once the command is run, Terraform will update the state file with the new infrastructure details.
In summary, terraform init command is used to initialize the terraform working directory, terraform plan command is used to create an execution plan and preview the changes that will be made, and terraform apply command is used to apply the changes to the infrastructure.
Below is CLI output of running the terraform apply
command.
Terraform CLI Output after Creating
Now take a look at the AWS console.
AWS Console for Ansible Build
Destroy the Environment
We can simply issue a CLI command on the same Terraform configuration files to destroy the environment we created. Run the command below.
terraform destroy
Below is the CLI output after running the terraform destroy
command.
Terraform CLI Output after Destroying
Terraform Configuration
Let's turn our attention to the Terraform configuration now.
main.tf
File*
This is the main file for our Terraform configuration.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.19.0"
}
tls = {
source = "hashicorp/tls"
version = "4.0.0"
}
}
}
provider "aws" {
region = var.region
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical https://ubuntu.com/server/docs/cloud-images/amazon-ec2
}
resource "tls_private_key" "mykey" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "aws_key_pair" "mykey" {
key_name = var.my_aws_key
public_key = tls_private_key.mykey.public_key_openssh
}
resource "aws_instance" "webserver" {
count = var.webserver_count
ami = data.aws_ami.ubuntu.id
instance_type = var.my_instance_type
vpc_security_group_ids = [aws_security_group.security_group1.id]
subnet_id = aws_subnet.terraform-vs-ansible.id
key_name = aws_key_pair.mykey.key_name
private_ip = var.private_ips[count.index]
tags = local.mytags
}
resource "aws_vpc" "terraform-vs-ansible" {
cidr_block = var.address_space
enable_dns_hostnames = true
tags = local.mytags
}
resource "aws_subnet" "terraform-vs-ansible" {
vpc_id = aws_vpc.terraform-vs-ansible.id
cidr_block = var.subnet_prefix
map_public_ip_on_launch = var.allow_public_ips
tags = local.mytags
}
resource "aws_security_group" "security_group1" {
name = "${var.project_name}-security-group"
vpc_id = aws_vpc.terraform-vs-ansible.id
ingress {
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP Ingress"
from_port = var.http_port
to_port = var.http_port
protocol = "tcp"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.mytags
}
This is a Terraform configuration file that provisions a VPC, subnet, security group, key pair, and EC2 instances in AWS.
The configuration starts by specifying the required providers for this configuration, in this case, aws
and tls
are being used, also specifying the version of each one.
Then it declares the AWS provider and sets the region variable to the value of the region
variable, this variable along with all other variables are defined in the variables.tf
file.
The configuration then uses the data
block to retrieve the most recent Ubuntu AMI that matches the specified filters, this AMI will be used to launch the instances.
Then it declares the resource tls_private_key
with the name mykey
and the algorithm of RSA with 4096 bits.
Then it declares the resource aws_key_pair
with the name mykey
and the public key from the previous private key resource.
Then it declares the resource aws_instance
named webserver
with the following properties:
count
: the number of instances to launch, defined by the variablewebserver_count
.ami
: the ami id, set to the id of the AMI retrieved earlier.instance_type
: the type of instances, defined by the variablemy_instance_type
.vpc_security_group_ids
: the ids of the security groups to associate with the instances, defined by the variableaws_security_group.security_group1.id
subnet_id
: the id of the subnet to launch the instances in, defined by the variableaws_subnet.terraform-vs-ansible.id
.key_name
: the name of the key pair to associate with the instances, defined by the variableaws_key_pair.mykey.key_name
.private_ip
: the private ip for the instances, defined by the variableprivate_ips[count.index]
tags
: the tags to apply to the instances, defined by the variablelocal.mytags
Then it declares the resource aws_vpc
named terraform-vs-ansible
with the following properties:
cidr_block
: the address space of the VPC, defined by the variableaddress_space
.enable_dns_hostnames
: whether to enable DNS hostnames, set totrue
tags
: the tags to apply to the vpc, defined by the variablelocal.mytags
Then it declares the resource aws_subnet
named terraform-vs-ansible
with the following properties:
vpc_id
: the id of the vpc to create the subnet in, defined by the variableaws_vpc.terraform-vs-ansible.id
.cidr_block
: the cidr block of the subnet, defined by the variablesubnet_prefix
.map_public_ip_on_launch
: whether to assign public ip to instances launched in the subnet, defined by the variableallow_public_ips
.tags
: the tags to apply to the subnet, defined by the variablelocal.mytags
Finally, it declares the resource aws_security_group
named security_group1
with the following properties:
name
: the name of the security group, defined by the variable${var.project_name}-security-group
vpc_id
: the id of the vpc that the security group belongs to, defined by the variableaws_vpc.terraform-vs-ansible.id
ingress
: the ingress rule, it allows HTTP traffic to port 80 from any IP address.egress
: the egress rule, it allows all traffic to any IP address.tags
: the tags to apply to the security group, defined by the variablelocal.mytags
This Terraform configuration creates and configures a VPC, subnet, security group, key pair, and EC2 instances in AWS using the specified variables, it also uses the data block to retrieve the most recent Ubuntu AMI that matches the specified filters, this AMI will be used to launch the instances.
It is worth mentioning that the configuration uses variables for most of the properties, those variables are defined in the variables.tf
file and you can find this file in the code repo.
Terraform and Ansible Differences to Note
Ansible | Terraform | |
---|---|---|
Ordering Resources | Order matters. If you put the launch of the EC2 instance before creating the key pair, it will fail to create the EC2 instance. | Order doesn't matter. Terraform has built-in intelligence to know which resources depend on which. It can also combine resources from different files. |
Detection of Changes | Some elements are not detected out of the box. For example, you can't add tags after the EC2 instance is created. Since there is no state file, you can't make that change. Ansible doesn't detect the change. | Terraform detects most changes out of the box. In the case of tags, it will detect those and update the EC2 instance. However, some changes that may occur with the configuration of the server created with provisioners are not detected. |
External Changes | Creation outside of Ansible is considered. If you create an EC2 instance from the AWS console in the same region, VPC, availability zone, and with a public IP. Basically, it needs to have a very similar configuration to the instance that Ansible created. Ansible is configured for a certain number of EC2 instances and will count the one created from the console. This can have strange and unpredictable effects. Example: If you had a count of 2 and then create a 3rd instance outside of Ansible via the console, then run Ansible again with the same count of 2, Ansible will delete the latest instance it created. So it will take into account the instance created outside of Ansible. | Terraform only manages what it creates. It generates a state file based on what it creates. Anything created outside of Terraform will not be considered and it won't know anything about it. This can lead to drift in some cases, however, this is a predictable behaviour. Drift needs to be addressed at the organizational level with education. |
Destroying Infrastructure | You can't simply run a CLI command to destroy what was created. You need to create a separate playbook to destroy. It's because there is no saved state. So you need to create another playbook to destroy the resources created. You also need to delete things in reverse order to how you created them. | Since Terraform saves a state file, it's simple to run terraform destroy on the same configuration files to destroy the infrastructure that was provisioned. |
Conclusion
In summary, Terraform and Ansible are both tools that can assist with infrastructure management and automation. They both have the capability to provision infrastructure, but they have different methods of doing so. Terraform is better suited for gaining more control over the infrastructure and utilizing immutable servers, while Ansible is more flexible and doesn't require an agent. The choice between the two ultimately depends on the specific requirements and preferences of the user. Many customers use both tools in combination to take advantage of the strengths of each.
It is also possible to use both tools together, as Terraform can be used to set up the infrastructure and pass information about the created resources to Ansible for further configuration. Check this blog post called Jenkins, Vault, Terraform, Ansible, and Consul Delivering an End-to-End CI/CD Pipeline. It's the last part of a 4 part series on how to deploy an application in production with best practices. I explain and demo how to use both Terraform and Ansible together in conjuction with Jenkins as the CI/CD pipeline tool.
I write quite a bit about Infrastructure as Code. If you're interested in Terraform, check out the Terraform 101 course on the TeKanAid Academy site. You could also take a look at some of the blog posts such as this one Terraform to Create a Ubuntu 22.04 VM in VMware vSphere ESXi or the Terraform Import Example - AWS EC2 Instance post..