Terraform Infrastructure as Code Basics
Tags: terraform, aws, vpc, grafana
Published: 2021-05-13 12:39:00

## Overview

During the course of this article we will cover some basic concepts in the Terraform application to create AWS resources. This article assumes you have the following:

- Terraform configured on your local machine.
- An AWS Account.
- A VPC (the default VPC will suffice).
- A Subnet that has the ability to allocate Public IP's within your VPC.
- An SSH key configured in your AWS Account
- An AWS Access and Secret Key

## Terraform Providers

In our previous article, we discussed Terraform [Providers](https://www.terraform.io/docs/providers/index.html). Expanding upon the last article; Terraform has the ability to manage not only Public Cloud resources but other resources including but not limited to [Grafana](https://www.terraform.io/docs/providers/grafana/index.html), [VMWare vSphere](https://www.terraform.io/docs/providers/vsphere/index.html), and [Docker](https://www.terraform.io/docs/providers/docker/index.html). In this article we will discuss two providers in particular: the [AWS](https://www.terraform.io/docs/providers/aws/index.html) and [Template](https://www.terraform.io/docs/providers/template/index.html) providers. Using these, we will create an EC2 Instance and apply a template created user-data file to change the hostname and reboot.

## Our File Structure

While you can contain all your code in a single `.tf` file, it's best practice to separate your code into multiple files for easier organization and reading. Since Terraform runs in the current directory unless the `-target` flag is specified it will apply all code in the current directory (and sub directories if specified in the HCL). We will also include a template file for our user data.

- main.tf
- vars.tf
- outputs.tf
- main.auto.tfvars
- user-data.tpl

You may also separate your code into individually named files such as `ec2.tf` and `sg.tf` for easier organization and reading.

## Provider Setup

Like the AWS CLI, the AWS Provider can authenticate to AWS leveraging the 4 standard authentication methods provided natively.

- Static Credentials
- Environment Variables
- Shared Credentials File
- EC2 Roles

For our purposes, we will leverage the Environment Variables method with an AWS Access and Secret Key.

```
shell script
export AWS_ACCESS_KEY_ID=my-access-key-id
export AWS_SECRET_ACCESS_KEY=my-secret-access-key
export AWS_DEFAULT_REGION=us-east-1
```

## Terraform Code

We will first start off by creating our variables by making our `vars.tf`

```
hcl
variable "vpc_id" {
  type = string
}

variable "subnet_id" {
  type = string
}

variable "instance_name" {
  type = string
}

variable "keypair" {
  type = string
}

variable "instance_type" {
  type = string
}
```

We can now populate our variables with values from our `main.auto.tfvars`.

```
hcl
vpc_id="vpc-myvpcid"
subnet_id="subnet-mysubnetid"
instance_name="myhost"
keypair="mykeyname"
instance_type="t3.small"
```

We will create our template file `user-data.tpl` user-data script to be used later by our `main.tf`

```shell
#!/usr/bin/env bash

hostnamectl set-hostname ${instance_name}

reboot
```

Now that we have our variables and our template, we can continue by creating our resources in our `main.tf`

```
hcl
provider "aws" {
    region      = "us-east-1"
}

## This grabs our own external IP address to use later when creating our security groups ##
data "external" "myipaddr" {
  program = ["bash", "-c", "curl -s 'https://api.ipify.org?format=json'"]
}

## This grabs the latest Ubuntu 18.04 Bionic Beaver AMI ##
data aws_ami "bionic" {
  owners = ["099720109477"]
  filter {
    name = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"]
  }
  most_recent = true

}

## Create a security group with your local public IP to grant SSH access ##
resource aws_security_group "my_security_group" {
  name = "my_security_group"
  vpc_id = var.vpc_id
  ingress {
    from_port = 22
    protocol = "tcp"
    to_port = 22
    cidr_blocks = [
      "${data.external.myipaddr.result.ip}/32"
    ]
  }
  egress {
    from_port = 0
    protocol = "-1"
    to_port = 0
  }
}

## We can create our templated file that will be rendered into our user-data ##

data template_file "user_data" {
  template = file("./user-data.tpl")
  vars = {
    instance_name = var.instance_name
  }
}

## Now we will create our instance with our created templated user-data and security group id's ##

resource aws_instance "my_ec2" {
  ami = data.aws_ami.bionic.id
  subnet_id = var.subnet_id
  instance_type = var.instance_type
  key_name = var.keypair
  associate_public_ip_address = true
  vpc_security_group_ids = [aws_security_group.my_security_group.id]
  user_data_base64 = base64encode(data.template_file.user_data.rendered)
  ebs_optimized = true
  root_block_device {
    volume_type = "gp2"
    volume_size = 20
    delete_on_termination = true
  }
  tags = {
    Name = var.instance_name
  }
}
```

Now that we've created our `main.tf`, next we will finally create our `outputs.tf` so we can acquire our Public IP address once the instance is created.

```hcl
output "public_ip" {
  value = aws_instance.my_ec2.public_ip
}
```

One thing you may have noticed is how I call results from one resource into another. I use the `id` of the created security group by calling `aws_security_group.my_security_group.id` in the `aws_instance` resource. This gives a level of native ordering within Terraform. This means the `aws_instance` has an [implicit](https://www.terraform.io/docs/configuration/resources.html#resource-dependencies) dependency on the `aws_security_group` resource before it can be created. If you want to employ an [explicit](https://www.terraform.io/docs/configuration/resources.html#depends_on-explicit-resource-dependencies) dependency or ordering on resources that don't natively need something from another resource you can leverage the `depends_on` attribute of that resource.

## Lets Run That Terraform!

Phew...alright, we've got all our Terraform written. Now its time to initialize our Terraform State and create our infrastructure!

But wait...what's Terraform State? Terraform State is the result of the declared resources defined in our `.tf` files. This is where all of our infrastructure definition will be stored. Our example is leveraging a local state file.

Alright, enough of that informative mumbo-jumbo. Let's get Terraforming!

![Let's Go](https://nextlinklabs.com/static/d963c2a9c6b6fa3709194eb6a6104624/0ef8e/lets-go.jpg)

#### Terraform Init

The Terraform Init command creates our localized `.terraform` directory, downloads our modules and providers, and creates and helps manage our state.

Alright, with that out of the way, we can run our Terraform init command, run the `terraform init` on our command line.

![Terraform Init](https://nextlinklabs.com/static/3e5e745dc897287cdb39a5200da41466/1baba/terraform-init.jpg)

Here we can see our backend being initialized, our plugins being downloaded, and other setup commands Terraform needs to run.

#### Terraform Plan

The Terraform Plan command does not create Infrastructure or add anything that is not immutable to our existing Terraform State. It simply checks what changes will be applied against the already created resources within the state and refreshes things like `data` items based on their current result.

Let's run our plan command and see what comes back.

![Terraform Plan](https://nextlinklabs.com/static/60e72471c6e20409c3ed186f38e915b6/571ad/terraform-plan-output.jpg)

Wunderbar! We have a plan. Our next step is to run an apply.

![Celebrate!](https://media.giphy.com/media/7xkxbhryQO7hm/giphy.gif)

#### Terraform Apply

A Terraform Apply will be the command we use to build our Infrastructure. Although there is a flag to `-auto-approve` changes, we will be prompted (with the same result as our Terraform Plan command) to apply our changes. Once we accept them, our output will show the resources being created.

![Terraform Apply](https://nextlinklabs.com/static/e1b453052a38f23c642783005b5ba8d0/89986/terraform-apply-output.jpg)

Here we can see our resources have been created, and our outputs have provided us a Public IP (redacted).

#### SSH Time!

Now that all of our resources are created, we can access our instance via SSH since our Public IP was whitelisted as part of our Terraform.

![Host Access](https://nextlinklabs.com/static/f6def266ce076e108ca2af812d7cf0b9/0cd64/host-access.jpg)

Here we can see our User Data has applied and renamed our host to our variable value `myhost`.

#### Terraform Destroy

Finally, now that our EC2 Instance is up and running, we are going to blow it and the associated Security Group sky high. Not really, but who doesn't like a bit of drama.

![Goodbye](https://nextlinklabs.com/static/8ddfa33602f445c1354a7391480e02d8/11d19/goodbye.png)

To do this it operates very similar to how a `terraform apply` works, we just need to run a `terraform destroy` and accept it. Once that's done our state should be mostly empty and our instance terminated in AWS.

## Some final items

You may have noticed that in your local directory structure there are now two additional files, the `terraform.tfstate` and the `terraform.tfstate.backup`. These are parts of what I had mentioned earlier where Terraform creates its declarative definition of your Infrastructure. In our next article, we will discuss new secure ways to manage our state so it is not part of our code base but remotely accessed during our Plans, Applies, and Destroys.

![Neat!](https://media.giphy.com/media/8vtm3YCdxtUvjTn0U3/giphy.gif)

## Whats Next?

I hope you've enjoyed this little foray into the world of Infrastructure as Code basics with Terraform. In our next article we will dive into more advanced concepts like Remote State Management, Modules for repeatable code, and other providers, such as [Kubernetes](https://www.terraform.io/docs/providers/aws/index.html).

This post appeared on the [Insights blog](https://nextlinklabs.com/insights) at [NextLink Labs](https://nextlinklabs.com/). Check out that blog for more [devops](https://nextlinklabs.com/services/devops-consulting-services) and [devops consulting services](https://nextlinklabs.com/services/devops-consulting-services) posts.