Today, I am going to introduce a new DevOps tool: Terraform. Terraform belongs to the provisioning part of the DevOps spectrum of tools. But, what is it used for? what are the best scenarios where it excels? How about a simple example? Please read along.

Problems that Terraform (and DevOps in general) address

Before embracing DevOps, the software development process was a slow, and - most of the time - a painful process. That's because developers worked in a different mindset, with different targets, different scopes and responsibilities than the Ops team. Let's have an example.

Snowflake servers

Although it looks beatiful, you don't want to have a snowflake server in your environment

Company XYZ is creating a web application that uses Node.js for an application server and MongoDB for the backend database. Developers spend days and weeks crafting the application, installing NodeJS dependencies, testing them, sometimes making some tweaks to let all parts play nice together, then they pass it on to the Ops team. The Ops team is responsible for the application from that time forward. They avail the servers, install the prerequisites, and deploy the application. Everything works as expected, everybody is happy. But, two weeks later, a bug is discovered in the application. Developer release patches to solve that bug. The Ops team installs the patch but it fails. Probably there were some configuration changes that were needed to be made to the servers and the developers just forget to notify the Ops team about it. Or, whoever was deploying the patch to the server made a mistake, perhaps forgot a deployment step. The result is an application that is acting abnormally. Ops try to fix things by tweaking the server settings. As more and more of stuff like this happens, there becomes what's commonly known as configuration drift. Eventually, you have a server that is referred to as snowflake server.

Nobody wants to have a server like that in their environments. A snowflake server is undocumented. Because of this, people tend to approach it with a lot of caution, if at all. Changes are seldom made to it because, due to its nature, they may produce unpredictable results.

Delayed releases

As time passes, the configuration drift increase. More and more issues and bugs start to pop up on production servers. Phrases like but it works on my machine are heard more than often. The problem is clear enough: production environment is in a totally deviated state than the development one. As a result, the Ops team becomes reluctant to make frequent changes to the prod servers. Software releases start to take longer and longer and when the time comes for a major release, deployment problems are so huge that most of them are unsolvable. Nobody wants to be in this place.

Devs and Ops should work in harmony

To address the above problems, there had to be a way through which the Dev and Ops teams work more closely. No longer should “the application is running without bugs” be the only target of the Devs team. Similarly, keeping the environments uptime as close as possible to %99.9999 should not be the one and only responsibility of the Ops team.

The target of the DevOps movement is to make code always in a deployable and maintainable state. Instead of making a new release every six months because the prod environment should be seldom touched, you can have up to a dozen or more releases per day.

This is can be achieved with the shared mindset between the Devs and the Ops, and also a set of tools that help achieve this. Tools like version control, continuous integration/deployment, automated testing, instant provisioning, among others, are what aid DevOps teams in being more productive.

Infrastructure as Code (IaC)

Embracing DevOps was accompanied by a large shift in how software is hosted. Virtualization technologies have improved throughout the past few years giving rise to tools like Vagrant and Docker. Also cloud giants like AWS, Google Cloud, and Microsoft Azure are offering new services every while. This shift helped Dev and Ops teams shift their mindset. Both of them are now using the same set of tools. An Ops staff no longer has to add to his/her tasks the hardware requirements of availing a new server. Instead, they should focus more on what this server is going to host and how it stays up to date with code changes. Similarly, developers are no longer living in isolated islands, with no idea where or how their code runs on production.

Accordingly, you don't know (or care) where or how your physical server is going to be powered on, as long as it is going to serve your application. This abstraction required that you, the DevOps engineer, describe what your server requirements, as well as the tasks that it is going to be perform, in the form of code. Just like how an application is built using code that is written, version controlled, tested, approved, then deployed, infrastructure environments are built the same way using code files.

OK, so I want to provision a server

With IaC defined, let's now have a real-world environment. The DevOps team is tasked with availing a new web server that will be running Apache. As you can see, the task is divided into two main parts: availing a new server, and installing and running Apache. There are two ways of approaching a task like this: either by splitting it into two subtasks (server creation then provisioning), or handling it as one task. The first method can be done in one of two ways:

Shell scripts

This is the oldest way of provisioning a server. You write a script in a language of your choice like Bash, Perl, Python, or Ruby. You log in to the server, run the script and that's it.

The problem with shell scripts is that they are ad-hoc. Everyone can has his/her own coding style. For example, these are two shell scripts that will do exactly the same thing: install Apache. However, each has a totally different style:

sudo apt update
sudo apt install apache2 -y
sudo systemctl start apache2
sudo systemctl enable apache2
    
function prepare{
    sudo apt update
}
function install{
    sudo apt install $1
}
function start{
    sudo systemctl start $1
}
function enable{
    sudo systemctl enable $1
}
service=apache2
prepare
install $service
enable $service

There are endless ways to write shell scripts. Each one has a style and a “best way” of writing shell scripts. For example, the write of the first script may argue that the code is concise and, thus, easier to debug. While the writer of the second one may claim that his code, while more verbose, is more modular and simpler to modify.

In an environment were people coming from different backgrounds (dev,ops,testing…etc.) are working on the same set of tools and scripts, not having a standard coding style will only make things worse.

Configuration management tools

The second, more DevOps way of provisioning servers is using a tool designed specifically for this. It's called a configuration management tools and there are some excellent examples out there: Ansible, Chef, Puppet, and SaltStack.

CM tools outrank shell scripts in a number of ways. Enforcing a specific coding style is one of them. Let's see an Ansible playbook that will achieve the same result as the shell script described earlier:

- name: Install Apache
  apt:
    update_cache: yes
    name: apache2
    state: present
- name: Start and enable Apache
  service:
    name: apache2
    state: started
    enabled: yes

Even if you've never seen an Ansible playbook before, you'll probably figure out what it does. However, configuration management tools offer more than just code-style enforcement:

Idempotence

Running the shell script once may work without throwing errors. Running the same script against the same server more than once may throw errors or at least produce unwanted results. Think of it for a moment. What if a line in your script is adding a new virtual host to Apache? running the script again will unnecessarily duplicate that virtual host. Or, if part of the setup was to create a service account for the application, running the script again will halt execution when attempting to create a user that already exists. Of course you can get around those kinds of issues by adding if..then..else statements, may be creating empty files just to make the script aware of the state of the server to know when and whether to make changes or not.

On the other hand configuration management tools are idempotent by default. Idempotence refers to running the same set of tasks on the same target machines several times without throwing errors or without duplicating changes.

Centralization

For a shell script to run, you need to upload it on the target host(s), login to each one and execute the script. Of course you can use sshtactics to run the script remotely from your local machine, but that brings its own challenges. Ansible and other CM tools are designed to run from a central server. Ansible can be even run from your local laptop or from a dedicated control machine. This greatly improves control and overall server management especially as the number of hosts increases.

Templating tools

The second method of building and provisioning a server is to address both tasks as one. Simple, use a template image of a server that already has everything it needs installed and configured. Tools like Docker, Packer, and Vagrant are used for this purpose.

There are two main ways by which an image can be used to avail a server:

As a virtual machine

A VM is an abstraction of hardware like CPU, memory, network, disks and others, used to start one or more operating systems on the same physical host. Once the VM is up and running, it has no idea whether it is running on physical or a virtual hosts as long as it has access to its required devices.

As a container

Containerization can also be used to achieve the same result although through a totally different approach. So, while a virtual machine uses a virtualized subset or the physical host's hardware, a container shares the physical (or virtual) hardware of its host. Two or more containers may share the same kernel, CPU cycles, memory space, and disk. A container feels just like another application or process running on your host. It starts in much less time than a virtual machine. However, containerization does have its drawbacks, which is not withing the scope of this article.

As a Packer image

Packer is another tool created by HashiCorp. It uses a JSON file of a specific format to prepare and create an image that can be used as an Amazon AMI or other cloud provier specific images. Let's have an example, the following code can be used by Packer to create an AMI image that can be used as a template for spwaning EC2 instances, each of them will be having Apache 2 installed:

{
  "builders": [
    {
      "type": "amazon-ebs",
      "access_key": "...",
      "secret_key": "...",
      "region": "us-east-1",
      "source_ami": "ami-fce3c696",
      "instance_type": "t2.micro",
      "ssh_username": "ubuntu",
      "ami_name": "webserver-image"
    }
  ],

  "provisioners": [
    {
      "type": "shell",
      "script": "sudo apt update && sudo apt install apache2"
    }
  ]
}

This is a slightly modiofed example from packer.io. Notice that we only install Apache on the image, we do not start the service. That's because this image is supposed to be a template to spwan instances.

Terraform: a Server Provisioning tool

When it comes to server provisioning tools, you are not bound to one server image, but rather a complete infrastructure that can contain application servers, databases, CDN servers, load balancers, firewalls, and others.

How is Terraform different than CM tools?

While configuration management tools ensure that each individual server is at the desired state, a server provisioning tool like Terraform ensures that the infrastructure as a whole is in the desired state. Let's have a quick example:

Let's say that you have an infrastrucrure that contains the following:

  • An AWS Elastic Load Balancer
  • An Apache web server running on an EC2 instance (together with PHP and some static HTML and other files)
  • An RDS database
  • An API service written using Go and hosted on an EC2 instance.

To build this infrastructure, you used Packer to create the base images, Terraform to build the required instances based on the Packer images.

Now, what if the webserver instance is down for some reason? Running the Terraform command against the environment will take the failing instance away, create a new instance from the Packer image and replaces the failing one with the working one.

If you are using Ansible, for example, against this instance, it will go ahead and ensure that the correct services are running, the configuration files have the correct values, the necessary files (like .htaccess for example) are in their correct places and have the correct instructions.

In short, a configuration managment tool will bring an instance or a group of instances to the desired state, while a server provisioning tool like Terraform will bring the whole infrastructure to the desired state.

About Terraform

Terraform is a free and open source tool created by HashiCorp and written in the Go programming language. It can be used to provision entire infrastructures that span accross multiple public and private cloud providers like AWS, Google Cloud, Digital Ocean, Microsoft Azure, OpenStack and others.