notes on 📖 oreilly's book: Terraform: Up and Running, 3rd Edition By Yevgeniy Brikman
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.
ad hoc scripts
configuration management tools
server templating tools
provisioning tools
So why terraform among other tools? Well, the author of the book presents some good points.
Domain Specific Language
Masterless
The author make some other points, but this two are enough for me I guess.
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.
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 ...] }
provider: this would be aws
resource: this would the type of resource, in our case we will use instance, which is aws lingo for ec2 instance.
name: identifier to use inside terraform. Will come handy when using expressions.
config: resource specific 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.
terraform init: this will scan the code, check which providers we will be using and download the code for them. Everything is put under the `.terraform` directory.
terraform plan: show you what terraform will do without actually doing it. Similar to a git diff on the format.
terraform apply: shows the plan and ask you to confirm it. If you type yes it will talk with the providers and actually create the stuff.
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
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
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
default: if you do not put this, and do not send the variable in the environment or in the CLI, terraform will stop and ask you to input it.
validation: if you want some custom checks on the variable, so enforcing min or max values.
sensitive: if this is set to true terraform will not log it when you run `plan` or `apply`
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}"
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`.
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.
listener this is where we actually tell it to which port it should listen
# configure the aws_lb to listen to port 80 and use http resource "aws_lb_listener" "http" { load_balancer_arn = aws_lb.example.arn port = 80 protocol = "HTTP" # send 404 to requests that do not match any listener rules. default_action { type = "fixed-response" # by default, return just a 404 fixed_response { content_type = "text/plain" message_body = "404: page not found" status_code = 404 } } }
listener rule this is where we map a pattern to a target group
resource "aws_lb_listener_rule" "asg" { listener_arn = aws_lb_listener.http.arn priority = 100 # match any path... condition { path_pattern { values = ["*"] } } # ...and send it to our asg group action { type = "forward" target_group_arn = aws_lb_target_group.asg.arn } }You will notice that we have not defined the target_group, we will do it in the next step
target group here we define a health check for the instances. Take into account we are not defining which ec2 instances to send the traffic to here. Instead we are going to do it in the asg definition.
# Health checks your instances by periodically sending an HTTP request to each # ec2. If one becomes unhealthy will stop sending the traffic to it. # # Ojito, we are not defining which ec2 instances to send requests to # here. That will be done in aws_autoscaling_group using the parameter # aws_lb_target_group_attachment. resource "aws_lb_target_group" "asg" { name = "terraform-asg-example" port = var.server_port protocol = "HTTP" vpc_id = data.aws_vpc.default.id health_check { path = "/" protocol = "HTTP" matcher = "200" interval = 15 timeout = 3 healthy_threshold = 2 unhealthy_threshold = 2 } }
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" }
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)