CloudFormation vs Terraform

Terraform is superior to CloudFormation in every scenario except when you absolutely have to use bleeding edge features from AWS. Here’s why.


Learning curve:

I think most people learn new technologies, by following tutorials or looking at examples. This is fairly easy to do with most programming languages, at least for an entry level.
Not with CloudFormation. It’s JSON (or YAML) formatted. It’s been designed to be consumed and produced by computers — not humans. Try it yourself, below is an example code snippet required to spin up an EC2 instance (basically a VM):

{
"AWSTemplateFormatVersion" : "2010-09-09",
....
150 lines of blah blah blah ...
....
},

"Resources" : {
"EC2Instance" : {
"Type" : "AWS::EC2::Instance",
"Properties" : {
"UserData" : { "Fn::Base64" : { "Fn::Join" : [ "", [ "IPAddress=", {"Ref" : "IPAddress"}]]}},
"InstanceType" : { "Ref" : "InstanceType" },
"SecurityGroups" : [ { "Ref" : "InstanceSecurityGroup" } ],
"KeyName" : { "Ref" : "KeyName" },
"ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" },
{ "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] }
}
},

"InstanceSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"GroupDescription" : "Enable SSH access",
"SecurityGroupIngress" :
[ { "IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "SSHLocation"} }]
}
},

"IPAddress" : {
"Type" : "AWS::EC2::EIP"
},

"IPAssoc" : {
"Type" : "AWS::EC2::EIPAssociation",
"Properties" : {
"InstanceId" : { "Ref" : "EC2Instance" },
"EIP" : { "Ref" : "IPAddress" }
}
}
},
"Outputs" : {
"InstanceId" : {
"Description" : "InstanceId of the newly created EC2 instance",
"Value" : { "Ref" : "EC2Instance" }
},
"InstanceIPAddress" : {
"Description" : "IP address of the newly created EC2 instance",
"Value" : { "Ref" : "IPAddress" }
}
}
}

Nasty. 210 lines of code to get a VM with public IP protected by Security Group. 210. 210! With every template there’s huge amount of boilerplate code, that’s basically noise (more on this later).
If that’s not enough to put you off at this stage, take a look at official documentation. It has now shifted towards using YAML, but when you want to look at sample snippets, turns out they’re all in JSON. The same is true for google results. 
BTW. when you’ve got different sample snippets per region, you can say something’s fishy

Round #1: CF: 0 TF: 1

Writing code

Pretty much the same arguments as above apply to writing the code itself. For a quick example take a look at the exactly the same resources as above, but described in Terraform:

resource "aws_instance" "web" {
ami = "12345-6789-10"
instance_type = "t2.micro"

tags {
Name = "Sweet"
}
}
data "aws_eip" "pip" {
public_ip = "1.1.1.1"
}

resource "aws_eip_association" "pip" {
instance_id = "${aws_instance.web.id}"
allocation_id = "${data.aws_eip.pip.id}"
}
resource "aws_security_group" "allow_all" {
name = "allow_ssh"
description = "Allow ssh from everywhere"

ingress {
from_port = 0
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_network_interface_sg_attachment" "sg_attachment" {
security_group_id = "${aws_security_group.allow_all.id}"
network_interface_id = "${aws_instance.web.primary_network_interface_id}"
}

Difference is shocking, isn’t it? Note how easy it is to reference other resources by their IDs. With a quick glance you can tell what’s happening and make basic changes to infrastructure. Which nicely brings us to another point

Round #2 CF: 0 TF: 1

Validating code

CF only allows for syntax check. So at best it will tell you that you missed a bracket here and there. Before you try applying CloudFormation template you won’t know if every variable you’ve used is resolvable but what’s the biggest drawback is you don’t know what’s going to happen.
Terraform on the other hand, validates .tf files, checking not only syntax but also if all dependecines resolve properly, and it gives you a plan! Yes, with Terraform you actually get to see what’s going to get created/changed/destroyed before you apply your code!

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ azurerm_resource_group.test_tf101
id: <computed>
location: "ukwest"
name: "test_tf101"
tags.%: <computed>
+ azurerm_subnet.sub1
id: <computed>
address_prefix: "172.16.0.8/29"
ip_configurations.#: <computed>
name: "sub-1"
network_security_group_id: <computed>
resource_group_name: "test_tf101"
route_table_id: <computed>
virtual_network_name: "test_vnet"
Plan: 2to add, 0 to change, 0 to destroy.
--------------------------------------------------------------------

Round #3 CF: 0 TF: 1

Remote state

Terraform allows you to easily import data from remote sources, for example other environments controlled in different state. This allows you for easy separation of resources and responsibilities. Simply declare the source of external information and use whatever is exposed by it. 
CloudFormation has notion of Cross-Stack References, but even getting through the documentation is a pain, and example on AWS to set up VPC peering is 71 lines, compared to 17 in Terraform.

Round #4 CF: 0 TF: 1

Functions

Check the snippet below.

resource "aws_instance" "web" {
# Create one instance for each hostname
count = "${length(var.hostnames)}"

# Pass each instance its corresponding template_file
user_data = "${data.template.web_init.*.rendered[count.index]}"
}

Yes. Terraform has quite a few built in functions that allow you to put logic in your code, so you can build better with less code, or have different structures built using the same code, but with different variables according to needs.

Round #5 CF: 0 TF: 1

Modules

You can group certain resources that you always use in conjunction and create modules, making it even easier to declare certain types of resources. You could compact it so that declaring a VM is just 4 lines of code! What’s more, using variable “count” you can have as many as you want, simply by changing a number.

variable "count" {
default = 2
}

resource "aws_instance" "web" {
# ...

count = "${var.count}"

# Tag the instance with a counter starting at 1, ie. web-001
tags {
Name = "${format("web-%03d", count.index + 1)}"
}
}

Round #6 CF: 0 TF: 1

Team work

Because Terraform’s HCL is like any other programming language it’s Git friendly in a way that pull requests nicely highlight changes, so it’s comfortable to do reviews and collaborate on a piece of code. Try doing the same with JSON which ultimately is a data structure. Half of the diffs is just boilerplate noise, and then some.

Round #7 CF: 0 TF: 1

Providers

Largely underestimated power of Terraform is the ability to control every aspect of your infrastructure with the same tool. You have a list of 70+ providers you can use, ranging from AWS, trough Azure, to Gitlab, Fastly, Chef, Docker, you name it. And it’s all using the same HCL you have to learn once. Amazing!

Round #8 CF: 0 TF: 1


Summary

After 8 rounds, it’s

CloudFormation: 0 vs Terraform: 8.

Even after adding an extra point, heck even two to CloudFormation for being closer to AWS offerings final result is CF 2 TF 8, which means Terraform absolutely crushed its opponent! 
I am fairly sure the same applies to Azure ARM templates vs Terraform, so there it is, two comparisons in one. Now that’s what I call efficiency.

Disclaimer
This post is full of shortcuts and likely also errors and misconceptions, which I’ll happily correct when pointed out. I’d love to spark a discussion, so maybe there’s a bait hidden here or there. Terraform FTW.