If you are looking to learn to terraform, then you are in the right place; this Learn Terraform: The Ultimate terraform tutorial guide will simply help you to gain complete knowledge that you need from basics to becoming a terraform pro.
Terraform infrastructure as a code tool to build and change the infrastructure effectively and simpler way. With Terraform, you can work with various cloud providers such as Amazon AWS, Oracle, Microsoft Azure, Google Cloud, and many more.
Let’s get started with Learn Terraform: The Ultimate terraform tutorial without further delay.
Table of Content
- Prerequisites
- What is terraform?
- Terraform files and Terraform directory structure
- How to declare Terraform variables
- How to declare Terraform Output Variables
- How to declare Terraform resource block
- Declaring Terraform resource block in HCL format.
- Declaring Terraform resource block in terraform JSON format.
- Declaring Terraform depends_on
- Using Terraform count meta argument
- Terraform for_each module
- Terraform provider
- Defining multiple aws providers terraform
- Conclusion
Prerequisites
- Windows Machine or Ubuntu Machine. This tutorial will use Windows Machine.
- Terraform Installed on Windows Machine or Ubuntu Machine. If you don’t have it already see here Terraform on Windows Machine / Terraform on Ubuntu Machine
What is terraform?
Let’s kick off this tutorial with What is Terraform? Terraform is a tool for building, versioning, and updating the infrastructure. It is written in GO Language, and the syntax language of Terraform configuration files is HCL, i.e., HashiCorp Configuration Language, which is way easier than YAML or JSON.
Terraform has been in use for quite a while now and has several key features that make this tool more powerful such as
- Infrastructure as a code: Terraform execution and configuration files are written in Infrastructure as a code language which comes under High-level language that is easy to understand by humans.
- Execution Plan: Terraform provides you in depth details of execution plan such as what terraform will provision before deploying the actual code and resources it will create.
- Resource Graph: Graph is an easier way to identify and manage the resource and quick to understand.
Terraform files and Terraform directory structure
Now that you have a basic idea of Terraform and some key features of Terraform. Let’s now dive into Terraform files and Terraform directory structure that will help you write the Terraform configuration files later in this tutorial.
Terraform code, that is, Terraform configuration files, are written in a tree-like structure to ease the overall understanding of code with .tf format
or .tf.json
or .tfvars
format. These configuration files are placed inside the Terraform modules.
Terraform modules are on the top level in the hierarchy where configuration files reside. Terraform modules can further call another child to terraform modules from local directories or anywhere in disk or Terraform Registry.

Terraform contains mainly five files as main.tf , vars.tf , providers.tf , output.tf and terraform.tfvars.
- main.tf – Terraform main.tf file contains the main code where you define which resources you need to build, update or manage.
- vars.tf – Terraform vars.tf file contains the input variables which are customizable and defined inside the main.tf configuration file.
- output.tf : The Terraform output.tf file is the file where you declare what output paraeters you wish to fetch after Terraform has been executed that is after terraform apply command.
- .terraform: This directory contains
cached provider
, modules plugins and also contains the last known backend configuration. This is managed by terraform and created after you runterraform init
command. - terraform.tfvars files contains the values which are required to be passed for variables that are refered in main.tf and actually decalred in vars.tf file.
- providers.tf – The povider.tf is the most important file whrere you define your terraform providers such as terraform aws provider, terraform azure provider etc to authenticate with the cloud provider.
How to declare Terraform variables
In the previous section, you learned Terraform files and Terraform directory structure. Moving further, it is important to learn how to declare Terraform variables in Terraform configuration file (var. tf)
Declaring the variables allows you to share modules across different Terraform configurations, making your module reusable. There are different types of variables used in Terraform, such as boolean, list, string, maps, etc. Let’s see how different types of terraform variables are declared.
- Each input variable in the module must be declared using a variable block as shown below.
- The label after the variable keyword is a name for the variable, which should be unique within the same module
- The following arguments can be used within the variable block:
default
– A default value allows you to decalre the value in this block only and makes the variable optional.type
– This argument declares the value types.description
– You can provide the description of the input variable’s.validation
-To define validation rules if any.sensitive
– If you specify the value as senstive then terraform will not print the values in while executing.nullable
– Specify null if you dont need any value for the variable.
variable "variable1" {
type = bool
default = false
description = "boolean type variable"
}
variable "variable2" {
type = map
default = {
us-east-1 = "image-1"
us-east-2 = "image2"
}
description = "map type variable"
}
variable "variable3" {
type = list(string)
default = []
description = "list type variable"
}
variable "variable4" {
type = string
default = "hello"
description = "String type variable"
}
variable "variable5" {
type = list(object({
instancetype = string
minsize = number
maxsize = number
private_subnets = list(string)
elb_private_subnets = list(string)
}))
description = "List(Object) type variable"
}
variable "variable6" {
type = map(object({
instancetype = string
minsize = number
maxsize = number
private_subnets = list(string)
elb_private_subnets = list(string)
}))
description = "Map(object) type variable"
}
variable "variable7" {
validation {
# Condition 1 - Checks Length upto 4 char and Later
condition = "length(var.image_id) > 4 && substring(var.image_id,0,4) == "ami-"
condition = can(regex("^ami-",var.image_id)
# Condition 2 - It checks Regular Expression and if any error it prints in terraform error_message =" Wrong Value"
}
type = string
description = "string type variable containing conditions"
}
Terraform variables follows below higher to the lower priority order.
- Specifying the environment variables like
export TF_VAR_id='["id1","id2"]''
- Specifying the variables in the teraform.tfvars file
- Specifying the variables in theterraform.tfvars.json file
- Specifying the variables in the *.auto.tfvars or *.auto.tfvars.json file
- Specifying the variables on the command line with
-var and -var-file
options
How to declare Terraform Output Variables
In the previous section, you learned how to use terraform variables in the Terraform configuration file. As learned earlier, modules contain one more important file: outputs. tf that contains terraform output variables.
- In the below output.tf file the you can see there are two different terraform output variables named:
- output1 that will store and display the arn of instance after running terraform apply command.
- output2 that will store and display the public ip address of the instance after running terraform apply command.
- output3 that will store but doesnt display the private ip address of the instance after running terraform apply command using sensitive argument.
# Output variable which will store the arn of instance and display after terraform apply command.
output "output1" {
value = aws_instance.my-machine.arn
}
# Output variable which will store instance public IP and display after terraform apply command
output "output2" {
value = aws_instance.my-machine.public_ip
description = "The public IP address of the instance."
}
output "output3" {
value = aws_instance.server.private_ip
# Using sensitive to prevent Terraform from showing the ouput values in terrafom plan and apply command.
senstive = true
}
How to declare Terraform resource block
You are going great in learning the terraform configuration file, but do you know your modules contain one more important main file.tf file, which allows you to manage, create, update resources with Terraform, such as creating AWS VPC, etc., and to manage the resource, you need to define them in terraform resource block.
# Below Code is a resource block in Terraform
resource "aws _vpc" "main" { # <BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
cidr_block = var.block # <IDENTIFIER> = <EXPRESSION> #Argument (assigns value to name)
}
Declaring Terraform resource block in HCL format.
Now that you have an idea about the syntax of terraform resource block let’s check out an example where you will see resource creation using Terraform configuration file in HCL format.
- Below code creates two resources where resource1 create an AWS ec2 instance and other work with Terraform provisioner and install apache on ec2 instance. Timeouts customizes how long certain operations are allowed.
There are some special arguments that can be used with resources such as
depends_on
,count
,lifecycle
,for_each
andprovider
, and lastlyprovisioners
.
resource "aws_instance" "resource1" {
instance_type = "t2.micro"
ami = "ami-9876"
timeouts { # Customize your operations longevity
create = "60m"
delete = "2h"
}
}
resource "aws_instance" "resource2" {
provisioner "local-exec" {
command = "echo 'Automateinfra.com' >text.txt"
}
provisioner "file" {
source = "text.txt"
destination = "/tmp/text.txt"
}
provisioner "remote-exec" {
inline = [
"apt install apache2 -f /tmp/text.txt",
]
}
}
Declaring Terraform resource block in terraform JSON format.
Terraform language can also be expressed in terraform JSON syntax, which is harder for humans to read and edit but easier to generate and parse programmatically, as shown below.
- Below example is same which you previously created using HCL configuration but this time it is using terraform JSON syntax. Here also code creates two resources resource1 → AWS EC2 instance and other resource work with Terraform provisioner to install apache on ec2 instance.
{
"resource": {
"aws_instance": {
"resource1": {
"instance_type": "t2.micro",
"ami": "ami-9876"
}
}
}
}
{
"resource": {
"aws_instance": {
"resource2": {
"provisioner": [
{
"local-exec": {
"command": "echo 'Automateinfra.com' >text.txt"
}
},
{
"file": {
"source": "example.txt",
"destination": "/tmp/text.txt"
}
},
{
"remote-exec": {
"inline": ["apt install apache2 -f tmp/text.txt"]
}
}
]
}
}
}
}
Declaring Terraform depends_on
Now that you learned how to declare Terraform resource block in HCL format but within the resource block, as discussed earlier, you can declare special arguments such as depends_on. Let’s learn how to use terraform depends_on meta argument.
Use the depends_on
meta-argument to handle hidden resource or module dependencies that Terraform can’t automatically handle.
- In the below example while creating a resource aws_rds_cluster you need the information about the aws_db_subnet_group so aws_rds_cluster is dependent and in order to specify the dependency you need to declare depends_on meta argument within aws_rds_cluster.
resource "aws_db_subnet_group" "dbsubg" {
name = "${var.dbsubg}"
subnet_ids = "${var.subnet_ids}"
tags = "${var.tag-dbsubnetgroup}"
}
# Component 4 - DB Cluster and DB Instance
resource "aws_rds_cluster" "main" {
depends_on = [aws_db_subnet_group.dbsubg]
# This RDS cluster is dependent on Subnet Group
Using Terraform count meta argument
Another special argument is Terraform count. By default, terraform create a single resource defined in Terraform resource block. But at times, you want to manage multiple objects of the same kind, such as creating four AWS EC2 instances of the same type in the AWS cloud without writing a separate block for each instance. Let’s learn how to use Terraform count meta argument.
- In the below code terraform will create 4 instance of t2.micro type with (ami-0742a572c2ce45ebf) ami as shown below.
resource "aws_instance" "my-machine" {
count = 4
ami = "ami-0742a572c2ce45ebf"
instance_type = "t2.micro"
tags = {
Name = "my-machine-${count.index}"
}
}

- Similarly in the below code terraform will create 4 AWS IAM users named user1, user2, user3 and user4.
resource "aws_iam_user" "users" {
count = length(var.user_name)
name = var.user_name[count.index]
}
variable "user_name" {
type = list(string)
default = ["user1","user2","user3","user4"]
}

Terraform for_each
module
Earlier in the previous section, you learned to terraform count is used to create multiple resources with the same characteristics. If you need to create multiple resources in one go but with certain parameters, then terraform for_each module is for you.
The for_each meta-argument accepts a map or a set of strings and creates an instance for each item in that map or set. Let’s look at the example below to better understand terraform for_each.
Example-1 Terraform for_each
module
- In the below example, you will notice
for_each
contains two keys (key1 and key2) and two values (t2.micro and t2.medium) inside the for each loop. When the code is executed then for each loop will create:- One instance with key as “key1” and instance type as “t2.micro”
- Another instance with key as “key2” and instance type as “t2.medium”.
- Also below code will create different account with names such as account1, account2, account3 and account4.
resource "aws_instance" "my-machine" {
ami = "ami-0a91cd140a1fc148a"
for_each = {
key1 = "t2.micro"
key2 = "t2.medium"
}
instance_type = each.value
key_name = each.key
tags = {
Name = each.value
}
}
resource "aws_iam_user" "accounts" {
for_each = toset( ["Account1", "Account2", "Account3", "Account4"] )
name = each.key
}

for_each
module example 1 to launch ec2 instance and IAM usersExample-2 Terraform for_each
module
- In the below example, you will notice
for_each
is a variable of type map(object) which has all the defined arguments such as (instance_type, key_name, associate_public_ip_address and tags). After Code is executed every time each of these arguments get a specific value.
resource "aws_instance" "web1" {
ami = "ami-0a91cd140a1fc148a"
for_each = var.myinstance
instance_type = each.value["instance_type"]
key_name = each.value["key_name"]
associate_public_ip_address = each.value["associate_public_ip_address"]
tags = each.value["tags"]
}
variable "myinstance" {
type = map(object({
instance_type = string
key_name = string
associate_public_ip_address = bool
tags = map(string)
}))
}
myinstance = {
Instance1 = {
instance_type = "t2.micro"
key_name = "key1"
associate_public_ip_address = true
tags = {
Name = "Instance1"
}
},
Instance2 = {
instance_type = "t2.medium"
key_name = "key2"
associate_public_ip_address = true
tags = {
Name = "Instance2"
}
}
}

for_each
module example 2 to launch multiple ec2 instancesExample-3 Terraform for_each
module
- In the below example, similarly you will notice
instance_type
is usingtoset
which contains two values(t2.micro and t2.medium). When the code is executed theninstance type
takes each value from the set values inside toset.
locals {
instance_type = toset([
"t2.micro",
"t2.medium",
])
}
resource "aws_instance" "server" {
for_each = local.instance_type
ami = "ami-0a91cd140a1fc148a"
instance_type = each.key
tags = {
Name = "Ubuntu-${each.key}"
}
}

for_each
module example 3 to launch multiple ec2 instances Terraform provider
Terraform depend on the plugins to connect or interact with cloud providers or API services, and to perform this, you need Terraform provider. There are several terraform providers that are stored in Terraform registry such as terraform provider aws or aws terraform provider or terraform azure.
Terraform configurations must declare which providers they require so that Terraform can install and use them. Some providers require configuration (like endpoint URLs or cloud regions) before using. The provider also uses local utilities like generating random strings or passwords. You can create multiple or single configurations for a single provider. You can have multiple providers in your code.
Providers are stored inside the “Terraform registry,” Some are in-house providers ( companies that create their own providers). Providers are written in Go Language.
Let’s learn how to define a single provider and then define the provider’s configurations inside terraform.
# Defining the Provider requirement
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
postgresql = {
source = "cyrilgdn/postgresql"
}
}
required_version = ">= 0.13" # New way to define version
}
# Defining the Provider Configurations and names are Local here i.e aws,postgres,random
provider "aws" {
assume_role {
role_arn = var.role_arn
}
region = var.region
}
provider "random" {}
provider "postgresql" {
host = aws_rds_cluster.main.endpoint
username = username
password = password
}
Defining multiple aws providers terraform
In the previous section, you learned how to use aws provider terraform to connect to AWS resources, which is great, but with that, you can only in one particular aws region. However, consider using multiple aws providers’ Terraform configurations if you need to work with multiple regions.
- To create multiple configurations for a given provider, you should include multiple
provider
blocks with the same provider name but to use the additional non-default configuration, use thealias
meta-argument as shown below. - In the below code, there is one aws terraform provider named aws that works with the us-east-1 region by default and If you need to work with another region, consider declaring same provider again but with different region and alias argument.
- For creating a resource in us-west-1 region declare provider.<alias-name> in the resource block as shown below.
# Defining Default provider block with region us-east-1
provider "aws" {
region = us-east-1
}
# Name of the provider is same that is aws with region us-west-1 thats why used ALIAS
provider "aws" {
alias = "west"
region = us-west-1
}
# No need to define default Provider here if using Default Provider
resource "aws_instance" "resource-us-east-1" {}
# Define Alias Provider here to use west region
resource "aws_instance" "resource-us-west-1" {
provider = aws.west
}
Quick note on Terraform version : In Terraform v0.12 there was no way to give a source but in the case of Terraform v 0.13 onwards you have an option to add a source address.
# This is how you define provider in Terraform v0.13 and onwards
terraform {
required_providers {
aws = {
source = "hasicorp/aws"
version = "~>1.0"
}}}
# This is how you define provider in Terraform v 0.12
terraform {
required_providers {
aws = "~/>1.0"
}}
Conclusion
In this Ultimate Guide, you learned what is terraform, terraform provider, and understood how to declare terraform provider aws and further used to interact with cloud services.
Now that you have gained a handful of Knowledge on Terraform continue with the PART-2 guide and become the pro of Terraform.
Pingback: Learn Terraform: The Ultimate terraform tutorial [PART-2] | Automateinfra