notes on 📖 oreilly's book: Terraform: Up and Running, 3rd Edition By Yevgeniy Brikman

~ terraform

table of contents

why terraform?

First, let me state that it is better to manage a big fleet of servers using code, you can have version control, which honestly seems to be reason enough to consider it.

Before things like terraform, developers worked with operation teams, to deploy their code. This usually was in baremetal systems, where sysadmins had to take care of installing dependencies, updating packages, deploying software, making sure all the servers had the same (similarish) configurations, among other stuff.

This caused a lot of issues, since things can get messy real quickly. Therefore, developers came with the solution, of configuring, deploying and doing pretty much everything with code. This can be done multiple ways, here I list a few.

So why terraform among other tools? Well, the author of the book presents some good points.

The author make some other points, but this two are enough for me I guess.

using terraform

In the book they use aws, so we will use it here too. Knowledge is somewhat transferable to your cloud provider of choice, the thing is that different cloud providers name stuff different, and also have different types of resources. For example, digitalocean's droplet seem to be aws ec2.

This means that names will change. So if you are using something different to aws, this notes might help, but you will have to refer to some specific declarations for your specific cloud provider.

So create an account, give your credit card info to jeff bezos, and lets start.

The account you just created is the root account for your aws, so you can do anything with it. It makes sense to create another one with less privilege for doing what we want in the book.

You manage your accounts on the IAM (Identity and Access Management) panel. Create one and give it AdministratorAccess. Create an Access Key ID and a Secret Access Key. Store them somewhere safe, and export the variables in your shell.

$ export AWS_ACCESS_KEY_ID=(your access key id)
$ export AWS_SECRET_ACCESS_KEY=(your secret access key)

One quick note, we will use the Default VPC for the book. A VPC is an isolated area of your AWS account that has its own virtual network and ip address space.

Once you exported the `env` variables and installed terraform we can continue to create our first `tf` file.

Deploying a Single Server

You will create a `.tf` file. This is files are written in the DSL for terraform called HCL (hashicorp configuration language). It is the declarative language we talked about a bit before.

First thing we usually do is specify the provider, so you go to the directory where you will have your project and create a `main.tf` file with:

provider "aws" {
  region = "us-east-2"
}

We are telling terraform to use aws and that region.

Now, the syntax will be pretty much the same for any provider, the thing that will change is the types of resources you can create. Generally you will do something like:

resource "provider_resource" "name" {
    [config ...]
}

Okay so if we want to create a ec2 instance , with a specific ami (amazon machine images) and with a name we will do something like:

resource "aws_instance" "example" {
    # amazon machine image to run on the ec2 instance, this is an ubuntu 20.04
    ami = "ami-0fb653ca2d3203ac1"
    instance_type = "t2.micro"
    tags = {
        Name = "terraform-example"
    }
}

We use t2.micro, which has one virtual CPU, 1 GB of memory, and is part of the AWS Free Tier.

Okay now we have our file. How do we actually deploy this on aws? Well here are some verbs.

Remember terraform already knows what resources have been created. So if we update the resource, it will update the one we already created. Or if the update needs to destroy and recreate it will do that too.

Last step, add this to your git repo. Here is a quick `.gitignore`

.terraform
*.tfstate
*.tfstate.backup

Deploying a web server

So usually you would build an image with packer upload it to ami and tell the resource to use that image. Since this is a quick example, we are going to use a web server that comes already with ubuntu 20.04.

You have the option of passing shell scripts on the user_data config variable on the resource, so we will take advantage of this to tell our ubuntu to start the web server.

resource "aws_instance" "example" {
    # amazon machine image to run on the ec2 instance, this is an ubuntu 20.04
    ami = "ami-0fb653ca2d3203ac1"
    instance_type = "t2.micro"

    # we would usually create an AMI for this with a real web app, using rails
    # or smth, but this is just a simple example.
    user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

    user_data_replace_on_change = true

    tags = {
        Name = "terraform-example"
    }
}

On the user_data we are sending the commands to start the server. Also note the `user_data_replace_on_change`. This tells terraform to destroy the instance and start it again if user data changes. Which is the case, so if now you run terraform apply, aws will show that the first resource was terminated and that a new one started running.

Ojito (Spanish for pay attention), we are not going to be able to make an http request to the server due to aws not allowing any incoming or outgoing traffic from an ec2 instance.

We need to create a new resource for this called `security_group`. So in the same file we will add:

resource "aws_security_group" "instance" {
    name = "terraform-example-instance"

    ingress {
        from_port = 8080
        to_port = 8080
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

This basically tells aws to accept traffic on port 8080, but we need to tell our ec2 instance to use this resource.

So terraform has expressions, which is anything that return a value, one type is a `reference`, which allows you to access values from other parts of the code. For example for the security group we just created we can call it saying: `aws_security_group.instance.id`

They call this resource attribute reference the syntax is

PROVIDER_TYPE.NAME.ATTRIBUTE

So now we need to add in our ec2 resource

vpc_security_group_ids = [aws_security_group.instance.id]

Adding this we are creating an `implicit dependency` meaning the order in which we create the stuff matters. You can render a graph for this using

$ terraform graph | dot -Tsvg > output.svg

You need to have `graphviz` installed though

Variables

You may want to have variables in your configuration. Maybe a port, a token, or something that you want to pass around. There is the `variable` identifier for this exact reason.

# hostname is the variable name
variable "hostname" {
    description = "something useful for you and your teammates"
    type = "string" # this can be number, bool, list, map, set, object, tuple
    default = "domain.do.com" # the default value if you do not pass anything
}

There are more options you have

You can also create objects, like python's dicts or ruby hashes.

variable "object_example" {
  description = "An example of a structural type in Terraform"
  type        = object({
    name    = string
    age     = number
    tags    = list(string)
    enabled = bool
  })

  default = {
    name    = "value1"
    age     = 42
    tags    = ["a", "b", "c"]
    enabled = true
  }
}

Once you define it you can send the variables via de command line the `-var` flag

terraform plan -var "server_port=8080"
Or you could also just export the variable with the `TF_VAR_name` format. For example:
$ export TF_VAR_server_port=8080
$ terraform plan

Now it is likely that you want to call it inside your file right? You can do that by using `var.variable_name`. (e.g. `var.server_port`). Or if you want to use it inside a string, you can wrap it with "${var.server_port}"

Output Variables

There is this other type of variables that are not for using them inside your file, but for them to display information that was created when running the terraform scripts. They are good for return info useful to you, say for example the public ip of an object you just created.

output "public_ip" {
  value = aws_instance.example.public_ip
}

When you run the `terraform apply` you would see it render at the end

Outputs:

public_ip = "1.2.3.4"
You can also render them with `terraform output`, or more specific even with `terraform output public_ip`.

Deploying a Cluster of EC2s

One ec2 is great. You know what is better? a cluster of ec2's that can scale automatically based on the usage of the application. AWS got you covered with the aws_autoscaling_group. An Auto Scaling Group contains a collection of EC2 instances that are treated as a group for the purposes of scaling and management.

What we are going to do is just taking one of the ec2 instances we created, move the definition to an aws_launch_template, change the name of a few parameters and tell the asg to use that ec2 template.

Do note, that we are not using `aws_launch_configuration` like in the book. Seems to be deprecated, and if you try to use it aws's api will probably complain that you do not have permission to do that. More info.

The template will look something like this:

resource "aws_launch_template" "example" {
    name_prefix   = "example-"
    image_id = "ami-0fb653ca2d3203ac1"
    instance_type = "t2.micro"

    vpc_security_group_ids = [aws_security_group.instance.id]

    user_data = base64encode(<<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p ${var.server_port} &
              EOF
    )
}

There are this things, called data sources which will query the cloud providers api and get some info, in this case we want to query it to get the subnet we are going to use to deploy the ec2 on.

# we need to tell the asg which subnet to use, this data source  will return
# the the default vpc in our aws account, which then we will use to query for
# the subnets.
data "aws_vpc" "default" {
    default = true
}

data "aws_subnets" "default" {
    filter {
        name = "vpc-id"
        values = [data.aws_vpc.default.id]
    }
}

Mind how I am using the data source inside the other data source, that is how you call them: data.aws_vpc.default.id

Now the actual asg definition with min size of 2 ec2s and max of 10, would look like this:

resource "aws_autoscaling_group" "example" {

    launch_template {
        id = aws_launch_template.example.id
        version = "$Latest" # Template version. Can be version number
    }

    # which subnet should asg use? we get this from the data sources.
    vpc_zone_identifier = data.aws_subnets.default.ids

    min_size = 2
    max_size = 10

    tag {
        key = "Name"
        value = "terraform-asg-example"
        propagate_at_launch = true
    }

}

So if you now do `terraform apply` the asg will be deployed and the ec2 instance will be deployed. Although we still have one piece missing. We need to create a load balancer, if not we would have to talk directly to each ec2 instance. It would be nice to have a piece of software that selected which ec2 to use based on a healthcheck.

Well, once again, aws got you covered. There is this resource, `aws_lb` that is a load balancer. There are different types of load balancer, different ones interact with different layers of the OSI model (ALB, NLB). We are going to use ALB which interacts with the layer 7.

Need to define the ALB

resource "aws_lb" "example" {
    name = "terraform-asg-example"
    load_balancer_type = "application"
    subnets = data.aws_subnets.default.ids
}

It comes down to defining three things.

For the full example go to github asg.ec2.tf.

Finally add this output variable so you have the dns

output "alb_dns_name" {
    value = aws_lb.example.dns_name
    description = "dns of the load balancer"
}

Cleanup

Anyway, we do not want to have this aws resources there for the rest of eternity. Mostly because we already gave them our credit card. So once you deployed this, and tested it. We can clean everything up.

`terraform destroy` will do the trick. There is no undo, but the good thing is that you already have it in a file so you can destroy it and apply it however many times you like.

So this -- according to the book -- are the basics of terraform. Now you can go into the wild and start terraforming your favorite cloud provider. (Or you can continue reading this, if I wrote my notes on more chapters)

~ table of contents

↑ go to the top