Introduction
One of my goals for this year was to improve my skills on Cloud Computing, and to be more precise, to gain some skills on Serverless and AWS. At the beginning of that journey, I didn't pay too much attention to how much time I spent to run a service, or how many times I had to repeat a step because I was doing a mistake at the middle of the process, rather than I focused on understanding the implementation details of that particular service. Slowly slowly, I felt more confident, but at that point I realised that I was spending too much time setting up the infrastructure, so I wasn't very happy with that. In addition, every time I decided to make a change on the infrastructure, and the last time that I did a change was a while ago, it was easiest for me to make a mistake and rebuild everything from scratch. As such, I started looking on tools that would help me to solve this issue and I found CloudFormation. I did a few trials, but I found it a bit fragile and hard to understand. In addition, CloudFormation is available only for AWS, and I was keen to learn a skill that would be accessible to other Cloud providers as well. Those reasons made me start looking for a different tool and I found Terraform. At that moment, I realised that Terraform was the right tool for me. As a result, in this post I will be talking about Terraform, talk about it's core concepts, and share a simple example on how to run an nginx server on an EC2 instance in AWS.
What is Terraform and which problems does it solve?
Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. Terraform can manage existing and popular service providers as well as custom in-house solutions.
In a few words, Terraform is an infrastructure provisioning tool that allows to build infrastructure through code. The idea of infrastructure as code (Iac) it's not something new, with many other tools built around it, however, terraform excels on it's simplicity and flexibility. Whenever you run Terraform, it builds an execution plan, and executes each step based on that plan. This means that Terraform focuses on a declarative approach, where you set a target goal and Terraform will find the optimal path on how to achieve that goal. There is no need to manually set the order on how cloud components are created, rather than Terraform will handle it for you.
In addition, Terraform manages it's own state creating different versions of itself. This part is extremely important for a couple of reasons:
- It allows you to track changes on your infrastructure and easily revert it to a previous version, or even remove it.
- It allows multiple developers to build infrastructure, without stepping on each other's toes.
For small scale projects these benefits won't be so obvious, however, big organizations can really benefit from them.
Last but not least, it supports a wide range of cloud providers, including the most famous one, Amazon Web Services(AWS), Microsoft Azure and Google Cloud Platform(GCP).
Terraform Language
Terraform uses it's own configuration language, called HCL. The language is used in many Hashicorp products, but it's not necessary to know the language fluently so you start building your infrastructure. The language supports variables, primitive data types, data structures that we all are familiar with, as well as functions and meta-arguments such as for_each
and count
.
Nonetheless, the main purpose of the language is to declare resources
, which represent infrastructure objects. You can think of a resource as a container of the configuration for a particular cloud component.
Terraform init, plan, apply and destroy
There are several commands exposed by the terraform client, however, the most important ones are init
, plan
, apply
and destroy
. Let's go through them.
terraform init
: is used to bootstrap a terraform project. This command will fetch the provider dependencies, modules and the backend configuration.
terraform plan
: is used to create an execution plan, showing which changes will be applied in each step.
terraform apply
: is used to calculate an execution plan and apply it.
terraform destroy
: is used to calculate an execution plan and incrementally remove all your infrastructure components.
Demo
In this section we will be running an nginx server on an EC2 instance in AWS. I have splitted the code into three different files, which are:
main.js
: includes all the code to create your resourcesvariables.js
: includes any variables that are used within the moduleoutputs.js
: includes any outputs that are displayed at the end of the procedure, or used from different modules
If you are wondering why I did that, this is the recommended structure of a terraform module (in this case the root module). However, you could include all the code on a single file and it would work the same.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
provider "aws" {
region = var.region
}
data "aws_vpc" "default" {
default = true
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
owners = ["099720109477"]
}
resource "aws_security_group" "security_group_web" {
name = "web-sg"
vpc_id = data.aws_vpc.default.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
security_groups = [aws_security_group.security_group_web.name]
user_data = <<-EOF
#!/bin/bash
sudo apt-get update -y
sudo apt-get install nginx -y
sudo service nginx start
EOF
tags = {
Name = "web"
}
}
At the top of the file we set the target provider, which is AWS, and within the provider block we set the AWS region. Using the aws_vpc.default
and aws_ami.ubuntu
data sources, we fetch some information about AWS default VPC and AWS Ubuntu AMI image, and later we declare a security group and an EC2 instance. The security group is attached to the EC2 instance and allows communication on port 80 (HTTP). With that way, we can curl the public ip of the EC2 instance and check if nginx is available.
variable "region" {
description = "AWS region"
type = string
default = "us-east-1"
}
output "web_id" {
description = "web id"
value = aws_instance.web.id
}
output "web_public_ip" {
description = "web instance public ip"
value = aws_instance.web.public_ip
}
output "web_public_dns" {
description = "web instance public dns"
value = aws_instance.web.public_dns
}
output "web_sg_id" {
description = "web security group id"
value = aws_security_group.security_group_web.id
}
Within variables.tf
we declare a terraform variable, which is used within main.js
to set the AWS region. In addition, outputs.tf
sets four outputs to display some information related to the EC2 instance and the security group. We are interested in the web_public_ip
or web_public_dns
, since those will be used to access the nginx server.
Before you run the scripts, you need to have an AWS account which will be used to build the infrastructure. As soon as you have created your account, you need to authenticate yourself. In my case, I set the AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
as environment variables, but if you have any difficulties you can also include the secret_key
and access_key
within the provider block. Here are some more details.
To initiate the project run terraform init
and then run terraform plan
. terraform plan
will create the execution plan and the output on your screen should be similar to that one below:
As you can see, terraform tells us that it will create an EC2 instance with the above properties. To execute the plan we need to run terraform apply
. After you run it, you will be presented with the same execution plan, but this time you need to approve it. As soon as you do it, terraform will start building the infrastructure. When the command is done, terraform displays the outputs, which should look like below:
To verify that nginx is running, you can either copy the web_public_dns
or web_public_ip
and use it with curl. You will get an outputs similar to this:
Summary
In summary, Terraform is an infrastrructure provision tool that allows us to build our infrastructure through code. It comes with it's own configuration language and the main building block is a resource
. Any other language feature is there to support our work on how we run our resources. Lastly but not least, you will frequently find yourself using the commands init
, plan
, apply
, and destroy
, which essentially describe the lifecycle of an infrastructure.