Terraform Syntax - Part 1
This post is intended to give a brief overview of the configuration syntax of Terraform. We would go through an example and touch up on some of the important aspects of Terraform configuration language, to successfully create an IaC and see that in action. This by no means is an attempt to rewrite all the technical details available in Terraform docs, however, if you want to get up and running with Terraform, this is the right place to get the direction.
Note: As a prerequisite, it is assumed that Terraform is installed on your local system, you have access to the AWS management console, and have set up and configured an IAM user for Terraform.
Arguments and Blocks
Referring to an example in the previous post, we created an EC2 instance with the below code.
provider “aws” {
region = “us-west-1”
}
resource “aws_instance” “myec2” {
ami = “ami-12345qwert”
instance_type = “t2.micro”
}
The code consists of 2 blocks wrapped in curly braces ( {}
), and each of these blocks has certain arguments defined. Just like most programming languages, arguments are used to assign values to variables. In Terraform configuration language, these variables are attributes associated with a particular type of block. provider “aws”
block has one argument - “region = “us-west-1”
”, where the region
is an attribute associated with the block, and it is assigned a value “us-west-1”
. The value is of the type string, thus it is enclosed in a pair of double quotes ( “”
). Similarly, the resource block has 2 arguments that set the values of associated attributes.
Terraform configuration language makes use of various types of blocks. Based on the type, blocks represent and enclose a set of attributes and functions. In the given example, we have a block of type provider
and another type resource
.
Terraform makes use of certain types of blocks (provider
and resource
, in the example), and each block has its identifier and a set of input labels. The provider block takes one input label - that is the name of the provider. In this case “aws
”. It also informs Terraform to install aws provider plugin, during init
phase. Resource block takes 2 inputs labels - the type of resource and the name of the resource. In this case - the type is “aws_instance
” and the name is “myec2”. What follows is the block body enclosed in curly braces.
Where to start?
So, how do we start expressing our infrastructure as code and make use of it? Let us take the example of creating a simple EC2 instance on AWS. Let us start by creating a directory of your choice where you would place all the configuration code required to create an EC2 instance. By default, Terraform assumes that all the files with .tf*
extensions in a directory are part of the configuration, irrespective of the file names. Create a file by name main.tf in this directory.
Note: The code used in this example can be referred to from this commit on Github.
The very first thing which we need to declare is - which providers are we going to use? Since we are going to spin an EC2 instance on AWS, we declare the same as below.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
provider "aws" {
region = “us-west-1”
}
We have declared 2 blocks - terraform
and provider
. terraform
is a top-most block, but it is optional as well. It is a good practice to specify this, especially when we work with remote state management. We will talk about remote state management in upcoming posts.
terraform
block has a nested block that specifies required_providers
. We require aws
a provider. aws
within required_providers
is a map, which specifies the source
and version
of the provider
. Next, we have a provider
block for aws
, which specifies the region
.
Generally, this is how every Terraform code would start. Of course, there would be some variations, and the best way to be sure about it is to refer to the Terraform registry for specific versions of Terraform as well as the provider plugin itself. For the sake of the current example, we are referring to AWS plugin documentation. The Terraform registry documents usage of all the resources of various cloud providers for example and it is a great resource for Terraform reference.
Providers
Installing Terraform on the system is not enough. To make configurations work, Terraform makes use of provider plugins. These plugins are installed in the initialization phase. Provider plugins come with their own set of configurations, resource types, and data sources. Terraform registry documents all the details for a given provider.
Resources
Every provider comes with a set of resources. resource
, as the name suggests, represents the actual cloud resource to be created in the configuration language. Providers enable resources. In the given example, aws
is a provider and aws_instance
is a resource provided by the AWS provider. The resource has its attributes. These attributes are documented on the Terraform registry. Out of all the attributes, some of the attributes are required for the Terraform to be able to process the configuration. Resources are the exact constructs that are executed by Terraform.
Continuing with the example, let us define an AWS EC2 instance resource by appending the below code into our main.tf file.
resource "aws_instance" "demo" {
ami = “ami-00831fc7c1e3ddc60”
instance_type = “t2.micro”
tags = {
name = "Demo System"
}
}
We start with a resource block named “aws_instance
” and we pass a second label and name it as “demo
”. The second label is the name of your choice. Next, open the block using curly braces and specify the required attributes used by the resource aws_instance
. The first attribute is ami
which specifies the Amazon machine image ID for the EC2 instance. The second attribute is the instance_type
which specifies the size of the machine to be created. We are also passing tags
which is an optional argument. As a tag, we pass “name” in the key and “Demo System” in the value. That's it we have defined our resource
.
We are now technically ready with the configuration and we can go ahead and initialize the Terraform into this directory so that it installs the provider plugin for AWS and we can then plan and apply this configuration. Save the file, go ahead and run terraform init
and see if it installs AWS provider plug-in. Once that is done successfully run terraform plan
and observe the output.
Let us put everything into perspective - providers
let Terraform know which plugins need to be installed to execute the configuration. resources
represent the actual cloud resources to be created. Generally, every resource has a name (“aws_instance
”). The initial part of the name of the resource is the provider identifier (“aws
”) which is separated by an underscore.
Variables
By now, we know that Terraform is a declarative language. In the example, we have declared the final state of a desired virtual machine on the desired cloud. Now it is up to Terraform to take this configuration and execute it to create the virtual resource. Having said that Terraform gives us the ability to specify input variables to its configuration.
Input variables are like parameters for a given function just like in any programming language.
It is particularly useful when you have to specify the same value at multiple places in your code. As the project grows in size, it becomes easier to change certain values that might be used in multiple places, using variables.
Terraform supports primitive types of variables such as string, number, boolean, and several complex types such as list, set, map, object, and tuple.
Let us define some variables into our code as below:
variable "region" {
default = "us-west-1"
description = "AWS Region"
}
variable "ami" {
default = "ami-00831fc7c1e3ddc60"
description = "Amazon Machine Image ID for Ubuntu Server 20.04"
}
variable "type" {
default = "t2.micro"
description = "Size of VM"
}
As you can see we have introduced three new variables
for the region
, the ami
, and the type
. Let us use this in our configuration so far. The values of the variables can be referred to using var
.<variable name>.
Terraform configuration also gives us the ability to return values. These values are known as output values. When Terraform completes the execution of the configuration, the output values are made available which can be used as input to other interfaces. We have defined one output variable “instance_id
” into our code. The value of this output variable is set using attribute reference of “aws_instance.demo
”. Similarly, we can refer to other output variables available from any resource in the configuration.
Below is the updated code of our main.tf. We have made use of three variables at appropriate places.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
provider "aws" {
region = var.region
}
variable "region" {
default = "us-west-1"
description = "AWS Region"
}
variable "ami" {
default = "ami-00831fc7c1e3ddc60"
description = "Amazon Machine Image ID for Ubuntu Server 20.04"
}
variable "type" {
default = "t2.micro"
description = "Size of VM"
}
resource "aws_instance" "demo" {
ami = var.ami
instance_type = var.type
tags = {
name = "Demo System"
}
}
output "instance_id" {
instance = aws_instance.demo.id
}
Save the file and run terraform plan
. Notice that Terraform has taken note of the output variable this time. It states that the output is known after apply, which is kind of obvious.
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_id = (known after apply)
Go ahead and do terraform apply
, and let me know the output. Don't forget to run terraform destroy
after every successful apply.
Lastly, Terraform also supports local variables, which are temporary values used locally by functions and blocks.
Provisioners
So before we conclude this part, let's take a look at provisioners for a while. Provisioning means to install, update and maintain required software once the hardware or virtual machine is successfully made ready. Terraform can trigger software provisioning processes once a virtual machine is ready, but that does not mean it is a full-time provisioning tool. This ability of Terraform can be used to make the infrastructure ready for management by installing smaller but essential software components.
There exist tools like Salt Stack, Ansible, Chef, etc., and most of these tools are agent-based. Terraform ability to run initial scripts to install some patch updates, agent software, or even set some user access policies to make sure machines are ready to be used.
Terraform comes bundled with generic provisioners as well as it supports vendor-specific provisioners. This is a topic for a future post.