Terraform (Series-1)

Terraform has been in use for quite a while now. It is an amazing tool to build, change the infrastructure in a very effective and simpler way. It’s used with a variety of cloud providers such as Amazon AWS, Oracle, Microsoft Azure, Google Cloud, and many more.

Lets learn it !

TABLE OF CONTENT

  1. Introduction to Terraform
  2. Key Features of Terraform
  3. Files and Directories in Terraform
  4. Syntax of Terraform
  5. Terraform Formats ( .tf and .tf.json)
  6. Block Types in Terraform
  7. Resources
  8. Resources Arguments
    • depends_on
    • count
    • for_each
    • provider
    • lifecycle
  9. Provisioners
  10. The Tree Architecture of Terraform
    • main.tf
    • vars.tf
    • output.tf
    • .terraform
    • terraform.state
    • terraform.tfvars
    • locals

Introduction to Terraform

Terraform is a tool for building, versioning, and updating the infrastructure. It is written in GO Language and the syntax language of configuration files is HCL i.e HashiCorp Configuration Language( much easier than YAML or JSON).

Terraform has been in use for quite a while now. It is an amazing tool to build, change the infrastructure in a very effective and simpler way. It’s used with a variety of cloud providers such as Amazon AWS, Oracle, Microsoft Azure, Google Cloud, and many more. Lets learn it !

Key Features of Terraform

There are several key features of Terraform which are useful for making of this tool.

  • 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.

Files and Directories in Terraform

Terraform code that is configuration files are written in tree like structure to ease the overall understanding of code. Lets checkout details about these files and folders.

  • Terraform contains mainly 5 types of files that you will learn later in this tutorial. They are main.tf , vars.tf , providers.tf , output.tf and terraform.tfvars.
  • Terraform code is stored in configuration files with .tf format or .tf.json or .tfvars format . These are called configuration files.
  • There is a special file override.tf or overrride.tf.json that ideally is not a good practice but can be used for some use cases.
  • Modules consist of top-level configuration files in a directory. Modules can call other child modules from local directories or from anywhere in disk or Terraform Registry. Root module can use self outputs as well as child modules output.
  • main.tf file contains the main code which helps in building the resources or fetching the resources.
  • vars.tf contains the input variables which user can customize and use in main.tf configuration file.
  • output.tf : Declare what output are required after the resources are created using terraform.
  • .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 run terraform init command.
  • terraform.tfvars files contains the values which are required to be passed.

Example of vars.tf file

variable "private_zone" {                        # Variable Boolean
  type        = bool
  default     = false
  description = "Route53 Hosted Zone Type"
}

variable  "images" {                             # Variable Boolean
   type    = map
   default = {
      us-east-1 = "image-1"
      us-east-2 = "image2"
    }
}


variable "My_list_of_string" {                    # Variable List
  type    = list(string)
  default = []
}

variable "ssl_policy" {}                          # Variable String


variable "aws_account_name" {                     # Variable String
  type        = String
  value       = "My_Account"
}


variable "My_instances" {                        # Variable List(Object)
 type =  list(object({
  instancetype        = string
  minsize             = number
  maxsize             = number
  private_subnets     = list(string)
  elb_private_subnets = list(string)
            }))
}


variable "MyInstance2" {                         # Variable Map(object)
 type = map(object({
  instancetype        = string
  minsize             = number
  maxsize             = number
  private_subnets     = list(string)
  elb_private_subnets = list(string)
  }))
}
variable "image_id" {
  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" 
  }
}

Example of output.tf file

output "instance_ip_addr" {
  value = aws_instance.server.private_ip
# Prevents Terraform from showing Value in Plan and apply  
  senstive = true                             
}

Syntax of Terraform

Lets learn how terraform modules and resources are created using hcl language. Below is the syntax to create a terraform resource.

# Below Code is a Block in Terraform

resource "aws _vpc" "main" {    # <BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
cidr_block = var.block          # <IDENTIFIER> =  <EXPRESSION>  #Argument (assigns value to name)
}                             

Next, some of the Terraform files are JSON compatible and must have .tf.json format syntax. Let’s check out how they differ in syntax from HCL language.

EXAMPLE 1

{
  "resource": {
    "aws_instance": {
      "automate": {
        "instance_type": "t2.micro",
        "ami": "ami-9876"
      }
    }
  }
}

JSON BASED

resource "aws_instance" "automate" {
  instance_type = "t2.micro"
  ami           = "ami-9876"
}




# 

HashiCorp Configuration Language

JSON is suitable for nested blocks. Let’s look at another example to understand how JSON works better with nested blocks than HCL format.

{
  "resource": {
    "aws_instance": {
      "automate": {
        "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"]
            }
          }
        ]
      }
    }
  }
}

JSON BASED

resource "aws_instance" "automate" {
  

  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",
    ]
  }
}









#

HashiCorp Configuration Language

Block types in Terraform

Below is the list of block types that are available with terraform and what are the basic components with their types that are used inside it. Lets learn !

  1. Resource blocks
    • provider: String,
    • depends_on : Array of strings
    • Ignore_changes: Json/String
  2. Data blocks
    • provider: string,
    • depends_on : Array of strings
    • Ignore_changes: Json/String
  3. Variable blocks
    • type: string,
    • default: JSON
    • description: JSON
  4. Output blocks
    • description: JSON
    • sensitive : JSON
  5. Locals blocks
    • Locals Blocktype: JSON
  6. Module blocks
    • source:String
    • provider: JSON
    • version: String
  7. Providers blocks
    • version: String
    • Alias: String
  8. terraform blocks

Terraform Resources

  1. There are some special arguments that can be used with resources such as depends_on, count, lifecycle, for_each and provider, and lastly provisioners.
  2. Each resource type is implemented by a provider also known as plugin for terraform. These providers get initialized when we run Terraform init command. These providers are available in Terraform Registry.
  3. Timeouts in the resource block customizes how long certain operations are allowed.
resource "aws_instance" "automate" {    # "aws_instance" This is the resource type
  ami           = "ami-XXXX"
  instance_type = "t2.micro"
  timeouts {                          # Customize your operations longevity
    create = "60m"
    delete = "2h"
   }
}

# Here "automate" is Local name which is only referenced in this particular Module but now-where outside this.

Special arguments in the resource block

Lets us quickly learn how to use special arguments used inside the resource block such as depends_on, count, lifecycle, for_each and provider, and lastly provisioners

depends_on: These are used to handle module dependencies

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

Count: Count is used when you need to create multiple identical resources.

resource "aws_instance" "my-machine" {
  count = 4 
  
  ami = "ami-0742a572c2ce45ebf"
  instance_type = "t2.micro"
  tags = {
    Name = "my-machine-${count.index}"
         }
}
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"]
}

for_each: It is used when you need to create multiple resources but with different parameters. for_each meta-argument accepts a map or set of strings. Let’s check out two examples !!

EXAMPLE 1

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” similarly it will create another instance with key as “key2” and instance type as “t2.medium”.

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
}

EXAMPLE 2

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.

  • Below is the main.tf file.
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)
  }))
}
  • Below is the terraform.tfvars file.
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"
    }
  }
}

After you execute the above files you should see the below output.

OUTPUT Of above for_each

EXAMPLE 3

In the below example, similarly you will notice instance_type is using toset which contains two values(t2.micro and t2.medium) . When the code is executed then instance 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}"
  }
}

Provider

Terraform relies on plugins called providers to interact with remote systems. Provider is an configuration or plugin which implements resource types such as module , resource , locals etc. Provider also uses local utilities like generating random string or password. You can create multiple or single configurations for a single provider. You can have multiple providers in your code.

Providers are stored inside “Terraform registry” and some are in-house providers ( companies which to create their own providers). Providers are written in Go Language. There are some built in Providers which you don’t need to give explicitly.

Lets learn how to define a single provider and then defining the configurations of provider inside terraform.

# Defining the Provider

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
}

Next, learn how to define multiple providers and then defining the configurations of providers inside terraform.

# Defining Default provider block with region us-east-1
provider "aws" {      
  region = us-east-1
}

# Name of the provider is same 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
}

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"
}}

Dependency Lock File

Dependency lock file is applicable for Terraform v 0.14 or later versions.

Terraform configuration files are dependent on providers and modules. There are lots of providers that have multiple versions. Dependency lock files helps to work with specific versions. The File format of this is .terraform.lock.hcl. Terraform updates .terraform.lock.hcl when you run terraform init command.

# This is how you define provider in Terraform v0.13 and onwards
terraform {          
  required_providers {
    aws = {
      source = "hasicorp/aws"
      version = "~>1.0"
}}}

Lifecycle

lifecycle defines the behavior of resources, how they should be treated. There are mainly three Arguments used inside the lifecycle those are as follows:

  1. create_before_destroy: Terraform destroys the existing object and then create a new replacement object
  2. prevent_destroy: Terraform skips the destroys the existing object
  3. ignores-changes: Terraform again skips and ignores the difference.
resource "aws_instance" "automate" {
  lifecycle {
    ignore_changes = [
      tags,
    ]
  }
}
resource "azurerm_resource_group" "automate" {



  lifecycle {
    create_before_destroy = true
  }
}

Provisioners

The various provisioners that interact with remote servers over SSH or WinRM can be used to pass such data by logging in to the server and providing it directly, but most cloud computing platforms provide mechanisms to pass data to instances at the time of their creation such that the data is immediately available on system boot.

Features of Provisioners

Provisioners can perform specific action on local machine such as running a command on local machine. They can also perform actions on remote machine such as copying files. These are used to enter or pass data in any resource which cannot be passed at the time of creation of resources. Provisioners allows you to declare conditions such as when = destroy , on_failure = continue. If you wish to run provisioners that aren’t directly associated with a specific resource, use null_resource.

resource "aws_instance" "server" {
   provisioner "" {
     command = "echo The server's Ip address is ${self.private_ip}"
     on_failure = continue
   }
}

How to do JSON Encoding with Terraform code.

resource "aws_iam_role_policy" "example" {
  name   = "example"
  role   = aws_iam_role.example.name
  policy = jsonencode({
    "Statement" = [{
      # This policy allows software running on the EC2 instance to
      # access the S3 API.
      "Action" = "s3:*",
      "Effect" = "Allow",
    }],
  })
}

There are some conditional Variables which are declared as below.

Priority of Variables Values

  1. Environment variables: export TF_VAR_availability_zone_names='[“us-west-1a”,”us-west-1b”]”
  2. teraform.tfvars
  3. terraform.tfvars.json
  4. *.auto.tfvars or *.auto.tfvars.json
  5. -var and -var-file options on the command line

Locals

Locals values are used so that you can use them multiple times in your resource or module without repeating it and just by referring to it. A set of related locals can be declared together in a single block.

locals {                                         # Declaring the Locals
  instance = "t2.micro"
  name     = "myinstance"
}

locals {                                         # Declaring the set of Local values
 common_tags {
  instance_type  = local.instance
  instance_name  = local.name
   }
}


resource "aws_instance" "instance1" {            # Declaring the set of Local values
  tags = local.common_tags
}

resource "aws_instance" "instance2" {             # Declaring the set of Local values
  tags = local.common_tags
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s