IaC with Terraform and AWS

IaC with Terraform and AWS

Today we’ll be using Terraform (https://www.terraform.io/) to deploy a web server (Apache) with a custom index.html on AWS. The webpage we’ll create won’t run on the default VPC but a new one we’ll create separately. This new VPC will contain an internet gateway, routes, a public subnet and all the goodies for us to get this properly running. On top of that, we’ll create a security group allowing access on port 80 and create a separate directory holding our variables to make this a little more dynamic and interesting. It’s worth mentioning that everything used in this tutorial is part of AWS’ ‘Free Tier’ so you don’t have to worry about any possible expenses.

Here we have a list of pre-requisites needed to follow along with this tutorial:

• An AWS account
• A host with AWS CLI installed and our credentials properly setup
• Our host needs to have access to the internet
• Basic AWS and Linux knowledge

So to start off, we’ll need to install the latest version of Terraform on our host. In my case I’m running a CentOS 7 machine, so to do that we can run the following commands

				
					sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
 sudo yum install -y terraform
				
			

The first command installs the hashicorp repositories whilst the second installs Terraform. If you’re on anything other than Centos 7 you can find the steps to install Terraform here (https://www.terraform.io/downloads)

You can confirm that Terraform was properly installed by checking its version

				
					[root@terra-test]# terraform -v
Terraform v1.1.7
on linux_amd64
				
			

As we get started I’d like to recommend installing Terraform’s auto complete functionality. That can be accomplished by running ‘terraform -install autocomplete’. Once that’s done re-login with your user and try typing any Terraform commands + tab. You should see all the different options present.

Moving on, we can create the main directory we’ll be using to hold our Terraform configuration files. Create a directory with your preferred naming convention, in our case that will be the directory ‘infra’, then create a file called ‘provider.tf’(we’ll talk about this in minutes) and lastly proceed to initiate it. In short, ‘initiating a directory’ just means making it ready for us to start working with Terraform configuration files. To do this you can run:

				
					mkdir infra
cd infra
touch provider.tf
terraform init
				
			

Now that we have the basic setup covered let’s edit the provider.tf file we just created. Open up the file with your text editor and paste this code

				
					terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.27"
    }
  }

  required_version = ">= 0.14.9"
}

provider "aws" {
  profile = "default"
  region  = var.region
}
				
			

Providers in Terraform are plugins that allow you to manage your resources, in this case that’s AWS. The profile block reads your AWS credentials and grants you access to deploy resources in your AWS account. As a security measure, it’s always advised to avoid hard coding your credentials.

Even though we could create all our resources in a single main.tf config file, I’d like to separate all our resources in different configs as that’s closer to the approach you’d take in a real environment. We can start by creating our VPC, routes, public subnet and internet gateway

				
					resource "aws_vpc" "greater_vpc" {
  cidr_block = var.vpc_cidr

  tags = {
    Name = "Greater VPC"
  }
}

resource "aws_subnet" "pub_subnet" {
  vpc_id            = aws_vpc.greater_vpc.id
  cidr_block        = var.pub_subnet_cidr
  map_public_ip_on_launch = true
  availability_zone = "eu-central-1a"

  tags = {
    Name = "Pub Subnet"
  }
}

resource "aws_route_table" "pub_route_table" {
  vpc_id = aws_vpc.greater_vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "Pub Routing Table"
  }
}

resource "aws_route_table_association" "pub_route_table_asso" {
  subnet_id      = aws_subnet.pub_subnet.id
  route_table_id = aws_route_table.pub_route_table.id
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.greater_vpc.id

  tags = {
    Name = "VPC IGW"
  }
}
				
			

These blocks create a VPC called ‘Greater VPC’, a public subnet in the eu-central-1a AZ and an internet gateway to allow our VPC to connect to the internet.

Now we can create our security group which will allow access on port 80 from all external sources. Create a file called ‘sg.tf’ and paste the following code

				
					touch sg.tf
vim sg.tf
				
			
				
					resource "aws_security_group" "sg" {
  name        = "http_access"
  vpc_id      = aws_vpc.greater_vpc.id

  ingress {
    description      = "HTTP Whitelist"
    from_port        = 80
    to_port          = 80
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  tags = {
    Name = "HTTP Access"
  }
}
				
			

All of these things would be pointless if we didn’t create an EC2 instance to run our webserver in. To create the EC2 instance we can create an ‘ec2.tf’ file and use this code

				
					touch ec2.tf
vim ec2.tf
				
			
				
					resource "aws_instance" "web" {
  ami             = "ami-0f61af304b14f15fb"
  instance_type   = var.instance_type
  subnet_id       = aws_subnet.pub_subnet.id
  security_groups = [aws_security_group.sg.id]

  user_data = <<-EOF
  #!/bin/bash
  sudo yum install httpd -y
  echo "If you see this it means it's working :-)" >> "/var/www/html/index.html"
  sudo systemctl enable --now httpd
  EOF

  tags = {
    Name = "Greater-Instance"
  }

  volume_tags = {
    Name = "Web-Instance"
  }
}
				
			

The AMI ID we are using here refers to a default (free tier) Amazon Linux 2 VM in the eu-central-1 region. Each region has its own AMI ID so take that into consideration if you’re trying to create your instance somewhere else. Regarding the other parameters passed, the instance type we’ll be using is ‘t2.micro’. This VM will be deployed in our public subnet and use the security group we previously created. The ‘user_data’ parameter lets us run specific commands right after the VM is installed, so here we are installing the httpd package for Apache, then we echo a message into /var/www/html/index.html and we wrap things up by enabling and starting the httpd service.

Throughout this article you probably noticed that there’s a lot of references to different variables we don’t know the values of, but don’t panic we’ll take care of that next.

Create a file called ‘variables.tf’ and a directory called ‘vars’ (if you’ve been working with the root user that’d be /root/infra/vars)

				
					touch variables.tf
vim variables.tf
				
			
				
					variable "region" {
default = "eu-central-1"
}
variable "instance_type" {}
variable "ami" {}
variable "vpc_cidr" {}
variable "pub_subnet_cidr" {}
				
			

Now create a file called test.tfvars in the vars directory

				
					touch vars/test.tfvars
vim vars/test.tfvars
				
			
				
					region = "eu-central-1"
instance_type = "t2.micro"
ami = "ami-0f61af304b14f15fb"
vpc_cidr = "10.0.0.0/16"
pub_subnet_cidr = "10.0.1.0/24"
				
			

You might now be wondering “what’s the difference between variables.tf and vars/test.tfvars?” and that’s a really fair question for anyone starting with Terraform. variables.tf basically defines all the valid variables we can use and *.tfvars gives those variables a value. This comes especially handy when you have a bigger infrastructure managing multiple environments.

Before we run our code to create the defined resources we will create one last file called ‘output.tf’ which will output the public IPv4 address of the EC2 instance. We can use this output to open up our browser, visit the site and confirm that our deployment was successful.

				
					touch output.tf
vim output.tf
				
			
				
					output "web_instance_ip" {
    value = aws_instance.web.public_ip
}
				
			

With everything in place now it’s time to execute our deployment. This consists of 2 different steps:

• Running terraform plan
• Running terraform apply

As it clearly hints, the first command creates an execution plan with the changes that will be made. This doesn’t actually create anything but ensures that our resources can be created and shows the differences between the changes that will be applied and the current state our infrastructure is in (if the resources already exist and some changes were made to them). To plan our deployment run this command:

				
					terraform plan -var-file=vars/test.tfvars
				
			

Once our plan is created and everything looks good, we can create our resources

				
					terraform apply -var-file=vars/test.tfvars -auto-approve

				
			

The ‘-auto-approve’ flag is self explanatory. If you don’t pass this flag you will be prompted to type ‘yes’ to confirm the resources you’re deploying, so by adding auto-approve we can skip this step entirely. It might take a minute or so for your resources to be created but, once it’s finished running, you will get an output message with the IP address of your EC2 VM. Feel free to copy this IP address and paste it in your browser. If everything went as expected you should now see the message “If you see this it means it’s working 🙂 ” displayed. And there you have it!

Today we did some IaC and learnt how to use Terraform to deploy a few resources into AWS. Hopefully this is something that can work as a foundation for improving your DevOps skills and jumping into more demanding tasks in real life scenarios. Cheers!