4 minute read

Introduction

In this part I am going to introduce you to some advanced features of terraform. We will understand the use of variable types, constructs like for_each and use dynamic blocks to write terraform code that is not verbose.

Introduction to variables

Just like other programming languages, HCL has the construct of data-types, and we can create variables using them. For example, we now know that modules or even resource block accept arbitary inputs from user. The best practice is to supply these variables by creating a terraform.tfvars file which is used by terraform to instantiate the variables to the user supplied values.

We will start with a simple example.

Example usage

In this example, we will use terraform to create a S3 bucket. We will need 3 files, main.tf, variables.tf and .tfvars inside the demo-project directory.

.
├── backend.tf
├── main.tf
├── terraform.tfvars
└── variables.tf

Main.tf

This file houses the code to create our s3 resource. As you can see, we have used variables using the syntax var.variableName to use the value of variable in resource and provider blocks.

provider "aws" {
  profile = var.profile
  region  = var.region
}

resource "aws_s3_bucket" "testbucket" {
  bucket = var.bucket_name
}

Variables.tf

This is the file where we are going to declare the variables that we need to use in code.


variable "region" {
  type    = string
  default = "us-east-1"
}

variable "profile" {
  type    = string
  default = "profile-name-to-use"
}

variable "bucket_name" {
  type    = string
  description = "Name of S3 bucket to create"
}

Along with basic parameters, you can use other parameters like sensitive, and add condition checks to validate the variables passed by user.

Terraform.tfvars

Finally, we can supply the value of variables using the .tfvars file.

bucket_name = "test_bucket"

Running the program is quite simple. I used the below sequence in every terraform project

# Initialize the backend and provider
terraform init

# Format the code
terraform fmt

# Validate for syntax errors
terraform validate

# Create plan
terraform plan

# Apply the changes
terraform apply

# Decommission 
terrafrom destroy

Types of variables

There are two types or variables in terraform -

  • Primitives - String, Boolean, Numeric
  • Complex - Set, Map, Object, Tuple, List, Any

You have already seen example of string variable when we introduced the idea of types in HCL. Let’s look at a complex variable declaration to make the idea more concrete.

variable "igress_params" {
  type = map(object({
    port  = number
    proto = string
    cidr  = list(string)
  }))
  default = {
    "rule1" = {
      port  = 22
      proto = "tcp"
      cidr  = ["0.0.0.0/0"]
    },
    "rule2" = {
      port  = 80
      proto = "tcp"
      cidr  = ["1.2.3.4/32"]
    }
  }
}

In the above block of code, we create a variable of type map(object) and use it to carry all the rules for a AWS security group that we can use later when creating VPC and Security Group.

Introduction to for_each

The for_each blocks are very good utility blocks when you want to create multiple resources of same type but different variable values. Such as, you have to create three S3 buckets for dev, uat and prod.

Below is an example use of how to use these blocks.

provider "aws" {
  profile = "aws-profile-to-use"
  region  = "us-east-1"
}

variable "s3_buckets" {
  type        = set(string)
  description = "Name of S3 buckets to create"
  default     = ["test-dev", "test-uat", "test-prod"]
}

resource "aws_s3_bucket" "testbucket" {
  for_each = var.s3_buckets
  bucket   = each.value
}

The above code iterates through each value of the s3_buckets variable and create 3 buckets.

Dynamic keyword

When creating a VPC followed by a security group, we often need to specify the inbound and outbound rules. It is often the case that the list of these rules and be length and hence not easy to maintain. Example of defining a security group without dynamic keyword.

resource "aws_security_group" "my-sg" {
  vpc_id = aws_vpc.my-vpc.id

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

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

As you can see, this approach is very verbose, plus to add to this issue the rules are intermingled with the intra code. Whenever possible, the best practice is the seperate variable like structures from actual logic.

Below is how we achieve the same using dynamic keyword.

variable "igress_params" {
  type = map(object({
    port  = number
    proto = string
    cidr  = list(string)
  }))
  default = {
    "rule1" = {
      port  = 22
      proto = "tcp"
      cidr  = ["0.0.0.0/0"]
    },
    "rule2" = {
      port  = 80
      proto = "tcp"
      cidr  = ["1.2.3.4/32"]
    }
  }
}

resource "aws_vpc" "my-vpc" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_security_group" "my-sg" {
  vpc_id = aws_vpc.my-vpc.id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  dynamic "ingress" {
    for_each = var.igress_params

    content {
      from_port   = ingress.value["port"]
      to_port     = ingress.value["port"]
      protocol    = ingress.value["proto"]
      cidr_blocks = ingress.value["cidr"]
    }

  }
}

Using the dynamic block, terraform will create multiple ingress blocks while iterating through the list of values.

Leave a comment