DEV Community

Cover image for Introduction to Terraform Variables(Input) and Values
Panchanan Panigrahi
Panchanan Panigrahi

Posted on

Introduction to Terraform Variables(Input) and Values

Introduction

Variables are fundamental building blocks in programming, acting as placeholders for dynamic data. They allow us to store and manipulate temporary values, enabling flexibility and adaptability in program logic—from simple to complex applications.

In this post, we'll explore the basics of Terraform values and Input variables. This is the foundation for working with Terraform, and in future posts, we’ll dive deeper into each type of Terraform value and Input variable.

At the end of each section, I’ll provide a hyperlink to a more detailed blog post on that specific topic, so you can easily explore it further. This approach allows us to gradually build our knowledge of Terraform concepts without being overwhelmed by their complexity.

Let’s get started!

What is a Terraform Module?

In Terraform, modules are essential building blocks that group multiple resources together for reuse and organization. Every Terraform configuration is part of a module, even if it's just a single .tf file. Modules help manage complexity by allowing you to break down large infrastructure into smaller, manageable components.

Since values and input variables are often used inside modules, it’s important to understand the basics of modules to get the full picture of how Terraform values and variables work.

Simple Example of Terraform Modules

Let’s look at a very basic example of a Terraform setup with a root module (the main configuration) and a child module (a reusable component).

NOTE:

  • The root module is your main entry point where you define and call other modules.
  • The child module contains reusable infrastructure code.

Don't worry about below file structures and codes, we will deep dive into modules in an upcoming blog.

Directory Structure

project/
├── main.tf
├── modules/
│   └── child_module/
│       ├── variables.tf
│       ├── outputs.tf
│       └── child.tf
Enter fullscreen mode Exit fullscreen mode

Root Module (main.tf)

This is the root module, where you call other modules. In this case, we are using a simple child module:

# main.tf in the root module
module "example" {
  source = "./modules/child_module"  # Pointing to the child module
  name   = "my-instance"             # Passing input variable to the child module
}
Enter fullscreen mode Exit fullscreen mode

Child Module (child.tf, variables.tf, outputs.tf)

Don’t worry about the code for now;, By the end of this post, you’ll be able to understand them with ease. Stay patient!

The child module contains the actual infrastructure resources, input variables, and outputs. It’s like a reusable block of code.

child.tf

# child.tf in the child module
resource "aws_instance" "example" {
  ami           = "ami-123456"
  instance_type = "t2.micro"
  tags = {
    Name = var.name
  }
}
Enter fullscreen mode Exit fullscreen mode

variables.tf

Here, we define the input variables the child module expects:

# variables.tf in the child module
variable "name" {
  type        = string
  description = "Name tag for the instance"
}
Enter fullscreen mode Exit fullscreen mode

outputs.tf

And finally, we can define outputs to pass back information from the child module to the root module:

# outputs.tf in the child module
output "instance_id" {
  value = aws_instance.example.id
}
Enter fullscreen mode Exit fullscreen mode

Summary

  • The root module is your main entry point where you define and call other modules.
  • The child module contains reusable infrastructure code and can accept input variables and return outputs.

By organizing your code into modules, you can create reusable, modular infrastructure, which becomes crucial when dealing with Terraform values and input variables.

Module Analogy: Functions in Programming

If you’re familiar with traditional programming languages, you can think of Terraform modules as being similar to functions:

  • Input variables are like function arguments — they allow you to pass data into the module.
  • Output values are like function return values — they allow the module to return data back to the calling module.
  • Local values are like a function’s temporary local variables — used within the module for intermediate calculations or storage.

Let’s break these down and understand each of them in turn.

Input Variables:

Input variables let you customize aspects of Terraform modules without altering the module's own source code. This functionality allows you to share modules across different Terraform configurations, making your module composable and reusable.

When you declare variables in the root module of your configuration, you can set their values using CLI options and environment variables. When you declare them in child modules, the calling module should pass values in the module block.

Feel free to deep dive into this blog for more understanding: How to Use Terraform Variables: Examples & Best Practices

Note: For brevity, input variables are often referred to as just "variables" or "Terraform variables" when it is clear from context what sort of variable is being discussed. Other kinds of variables in Terraform include environment variables (set by the shell where Terraform runs) and expression variables (used to indirectly represent a value in an expression).

Declaring an Input Variable

Each input variable accepted by a module must be declared using a variable block.

here are some examples of input variables:


variable "instance_type" {
  type        = string
  description = "Type of EC2 instance to be used"
}


variable "region" {
  type        = string
  default     = "us-east-1"
  description = "AWS region to deploy resources in"
}

variable "db_password" {
  type        = string
  description = "Password for the database"
  sensitive   = true
}


variable "availability_zone" {
  type        = string
  description = "The availability zone for the EC2 instance"
  nullable    = true
}


variable "ami_id" {
  type        = string
  description = "The AMI ID for the EC2 instance"

  validation {
    condition     = length(var.ami_id) > 0
    error_message = "The AMI ID must not be empty"
  }
}
Enter fullscreen mode Exit fullscreen mode

Arguments for Input Variables:

  • default - A default value which then makes the variable optional.
  • type - This argument specifies what value types are accepted for the variable.
  • description - This specifies the input variable's documentation.
  • validation - A block to define validation rules, usually in addition to type constraints.
  • sensitive - Limits Terraform UI output when the variable is used in configuration.
  • nullable - Specify if the variable can be null within the module.

Types of Input Variables

Terraform input variables are categorized into two main types: simple and complex.

  • Simple data types: String, Number, Bool
  • Complex data types: List, Map, Tuple, Object, Set

The following snippets provide examples for each type:

String Type:

  • Used to represent text values.
variable "instance_name" {
  type = string
  default = "my-instance"
}
Enter fullscreen mode Exit fullscreen mode

Number Type:

  • Represents numeric values, useful for counts or sizes.
variable "instance_count" {
  type = number
  default = 2
}
Enter fullscreen mode Exit fullscreen mode

Boolean Type:

  • Represents true or false values, ideal for feature flags.
variable "enable_monitoring" {
  type = bool
  default = true
}
Enter fullscreen mode Exit fullscreen mode

List Type:

  • An ordered collection of values of the same type.
variable "allowed_ips" {
  type = list(string)
  default = ["192.168.1.1", "192.168.1.2"]
}
Enter fullscreen mode Exit fullscreen mode

Set Type:

  • A unique, unordered collection of values.
variable "availability_zones" {
  type = lt = ["us-east-1a", "us-east-1b"]
}
Enter fullscreen mode Exit fullscreen mode

Tuple Type:

  • A fixed-length collection of values with different types.
variable "example_tuple" {
  type = tuple([string, number, bool])
  default = ["ami-123456", 2, true]
}
Enter fullscreen mode Exit fullscreen mode

Object Type:

  • A collection of named attributes, each with a specific type.
variable "instance_config" {
  type = object({
    instance_type = string
    disk_size     = number
  })
  default = {
    instance_type = "t2.micro"
    disk_size     = 30
  }
}
Enter fullscreen mode Exit fullscreen mode

Map Type:

  • A collection of key-value pairs, where all keys are the same type.
variable "tags" {
  type = map(string)
  default = {
    Name    = "my-instance"
    Project = "Terraform"
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: At the later part of this post, we will learn how to pass values to input variables at run time through a real-world project.

Now move to local values.

Local Values:

Terraform Locals are named values which can be assigned and used in your code. It mainly serves the purpose of reducing duplication within the Terraform code. When you use Locals in the code, since you are reducing duplication of the same value, you also increase the readability of the code.

Feel free to deep dive into this blog for more understanding: Terraform Locals: What Are They, How to Use Them

Local Values vs Input Variables

  • Input Variables: These are used to pass dynamic values into a Terraform module from outside, allowing you to customize your configuration without hardcoding values. Input variables are like function arguments, enabling flexibility and reuse of your modules across different environments.

  • Local Values: These are used within a module to define temporary, reusable values that simplify complex expressions or calculations. Unlike input variables, local values cannot be overridden from outside the module—they are only available within the module where they are defined.

How to Use Local Values:

When you use a Terraform local in the code, there are two parts to it:

  • First, declare the local along and assign a value
  • Then, use the local name anywhere in the code where that value is needed

Local values in Terraform can be categorized into several types based on their data structures: simple (like strings, numbers, and booleans) and complex (such as lists, maps, tuples, and objects), allowing for flexibility and organization within your configuration.

here is an example of local block:

locals {
  # String: Type of EC2 instance
  instance_type = "t2.micro"  

  # Number: Number of EC2 instances to launch
  instance_count = 3           

  # List: Allowed IP addresses
  allowed_ips = [
    "192.168.1.1",              
    "192.168.1.2"
  ]

  # Map: Key-value pairs for tagging resources
  tags = {                      
    Name    = "my-instance"
    Project = "Terraform"
  }

  # Object: Configurations for the EC2 instance
  instance_config = {          
    instance_type = local.instance_type
    disk_size     = 30
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: At the later part of this post, we will learn how to use Local Values inside a module through a real-world project.

Output Values:

Terraform outputs serve two main purposes:

  1. Printing details: They display information about a resource, data source, local value, or variable after deployment.
  2. Exporting details: When using modules, they allow you to export specific details about resources, data sources, locals, or variables for use outside the module.

Feel free to deep dive into this blog for more understanding: Terraform Output Values : Complete Guide & Examples

How to Use Output Values

In Terraform, the output block is used to define output values. These values let you display important details from your configuration or share them with other modules.

Syntax for Declaring Output Values

output "output_name" {
  value       = value_expression  # The value you want to output, like a resource attribute
  description = "Description of the output value"  # Optional, but helps understand what the output represents
  sensitive   = true/false  # Optional; hides the output if set to true (useful for sensitive data like passwords)
}
Enter fullscreen mode Exit fullscreen mode

Let’s look at examples to understand how to use output values in different scenarios.

Examples of Output Values

1. Output from a Resource

When working with resources, you might want to extract specific details. For example, after creating an AWS EC2 instance, you might want to get its public IP to connect to it.

# Defining an AWS EC2 instance resource
resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"  # Amazon Machine Image ID
  instance_type = "t2.micro"               # Instance type
}

# Output block to fetch the public IP of the created instance
output "instance_public_ip" {
  value       = aws_instance.example.public_ip  # Fetching the public IP from the resource
  description = "The public IP address of the EC2 instance"
}
Enter fullscreen mode Exit fullscreen mode

In this example, aws_instance.example.public_ip retrieves the public IP address from the created instance, and the output block makes this value available for you to see when you run terraform apply.

2. Output from a Data Source

Data sources fetch information from existing resources, and you can use outputs to display that data. For instance, you might want to know which AWS region your configuration is using.

# Defining a data source to get the current AWS region
data "aws_region" "current" {}

# Output block to display the AWS region
output "aws_current_region" {
  value       = data.aws_region.current.name  # Getting the region name from the data source
  description = "The current AWS region being used"
}
Enter fullscreen mode Exit fullscreen mode

Here, data.aws_region.current.name fetches the region name from the data source, and the output block makes this value visible.

3. Output from Local Values

Local values help define reusable expressions, and you can output them if needed. For example, let’s say you generated a unique S3 bucket name and want to display it.

# Defining local values
locals {
  bucket_name = "my-app-bucket-${random_id.bucket.hex}"  # Creating a unique bucket name
}

# Output block to display the local bucket name
output "local_bucket_name" {
  value       = local.bucket_name  # Using the local value as the output
  description = "The dynamically generated S3 bucket name"
}
Enter fullscreen mode Exit fullscreen mode

In this case, local.bucket_name is a local value, and the output block allows you to see the generated bucket name when you run Terraform commands.

4. Output from Child Module to Root Module

If you're using modules, you might want to pass information from a child module back to the root module. This allows the parent configuration to access values from the child.

In the Child Module:
# Output block in the child module to expose the VPC ID
output "vpc_id" {
  value       = aws_vpc.main.id  # The VPC ID from the resource in the child module
  description = "The VPC ID from the child module"
}
Enter fullscreen mode Exit fullscreen mode
In the Root Module:
# Defining the module in the root configuration
module "network" {
  source = "./modules/network"  # Path to the child module
}

# Output block to use the child module's output
output "child_vpc_id" {
  value       = module.network.vpc_id  # Accessing the output from the child module
  description = "The VPC ID obtained from the child module"
}
Enter fullscreen mode Exit fullscreen mode

Here, the child module exposes its vpc_id using an output block, and the root module accesses this value through module.network.vpc_id.

Congratulation!!!, you have learned fundamental concepts of the Terraform module, Input Variable, local values, and output values. Now time to use these concepts, in a hands-on project.

Deploying Nginx on an Ubuntu EC2 Instance Using Terraform Modules

The purpose of this guide is to demonstrate how to create an EC2 instance and deploy Nginx on it using Terraform modules. Through this process, we will explore the use of Terraform input variables and values.

If you get stuck at any point, you can refer to the code examples and configurations in my GitHub repo for this blog: Infrastructure-Nginx

Project Directory Structure:

Infrastructure-Nginx/
├── README.md
├── main.tf
├── outputs.tf
└── nginx-module/
    ├── ec2.tf
    ├── key-pair.tf
    ├── networking.tf
    ├── outputs.tf
    ├── scripts/
    │   └── nginx-script.sh
    ├── variables.tf
    └── version.tf
Enter fullscreen mode Exit fullscreen mode

The Infrastructure-Nginx directory serves as the root module, where we will invoke the child module named nginx-module.

So, Let's first set up our nginx-module.

Specify the Terraform Version:

Paste the following code snippet inside nginx-module/version.tf.

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.56"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This configuration specifies that Terraform should use the AWS provider and ensures compatibility with Terraform version 1.0 or higher. The provider version is locked to ensure stability and prevent unexpected updates.

Define Networking for EC2 Instance:

In the nginx-module/networking.tf file, add the following code:

# Create VPC
resource "aws_vpc" "main_vpc" {
  cidr_block = var.vpc["cidr_block"]

  tags = {
    Name = "main-vpc"
  }
}

# Create Internet Gateway
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main_vpc.id

  tags = {
    Name = "main-igw"
  }
}

# Create a Public Subnet
resource "aws_subnet" "public_subnet" {
  vpc_id                  = aws_vpc.main_vpc.id

  cidr_block              = var.subnet.cidr_block
  availability_zone       = var.subnet.availability_zone
  map_public_ip_on_launch = var.subnet.map_public_ip_on_launch

  tags = {
    Name = "public-subnet"
  }
}

# Create Route Table
resource "aws_route_table" "public_rt" {
  vpc_id = aws_vpc.main_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
}

# Associate Route Table with Subnet
resource "aws_route_table_association" "public_rt_association" {
  subnet_id      = aws_subnet.public_subnet.id
  route_table_id = aws_route_table.public_rt.id
}


# Create Security Group
resource "aws_security_group" "allow_ssh_http_https" {
  vpc_id = aws_vpc.main_vpc.id

  ingress {
    from_port   = var.ingress["ssh_port"]
    to_port     = var.ingress["ssh_port"]
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = var.ingress["http_port"]
    to_port     = var.ingress["http_port"]
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = var.ingress["https_port"]
    to_port     = var.ingress["https_port"]
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

  tags = {
    Name = "allow-ssh-http-https"
  }
}
Enter fullscreen mode Exit fullscreen mode

This code creates a VPC with an internet gateway, a public subnet with a specified CIDR block and availability zone, and sets up a route table. It also defines a security group that allows only specific ingress and egress traffic.

Create AWS kay_pair for EC2:

Add the following code in key-pair.tf:

resource "tls_private_key" "ssh_key" {
  algorithm = var.tls_keys.algorithm

  rsa_bits = var.tls_keys.rsa_bits
}

resource "local_file" "private_key" {
  content = tls_private_key.ssh_key.private_key_pem

  filename = var.tls_keys.private_key_filename
}

resource "local_file" "public_key" {
  content  = tls_private_key.ssh_key.public_key_openssh

  filename = var.tls_keys.public_key_filename
}

resource "aws_key_pair" "deployer" {
  key_name   = var.aws_key_pair_name

  public_key = tls_private_key.ssh_key.public_key_openssh
}
Enter fullscreen mode Exit fullscreen mode

This configuration generates TLS keys and stores them in specified locations. The public key is used to create the AWS key pair.

Create EC2 Instance:

In ec2.tf, add the following code:

# Create Ubuntu EC2 Instance
resource "aws_instance" "ubuntu_instance" {
  ami                         = var.ec2_instance.ami
  instance_type               = var.ec2_instance.instance_type
  associate_public_ip_address = var.ec2_instance.associate_public_ip_address

  subnet_id              = aws_subnet.public_subnet.id
  vpc_security_group_ids = [aws_security_group.allow_ssh_http_https.id]
  key_name               = aws_key_pair.deployer.key_name

  depends_on = [
    aws_security_group.allow_ssh_http_https,
    aws_internet_gateway.igw
  ]

  user_data = file("${path.module}/scripts/nginx-script.sh")

  tags = {
    Name = "ubuntu-instance"
  }
}
Enter fullscreen mode Exit fullscreen mode

this will create an ubuntu instance and then use the nginx-script.sh as user_data to install and run nginx onto it.

Create the nginx-script.sh:

In the scripts folder, create a file named nginx-script.sh:

#!/bin/bash
sudo apt update -y
sudo apt install -y nginx

# Create index.html with H1 tag in the default NGINX web directory
echo "<h1>Hello From Ubuntu EC2 Instance!!!</h1>" | sudo tee /var/www/html/index.html

# Restart NGINX to apply the changes
sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

this will greet Hello From Ubuntu EC2 Instance!!! on port 80.

Create the variables:

In variables.tf, add:

variable "tls_keys" {
  description = "TLS key configuration: includes the algorithm used, RSA bit size, and filenames for the private and public keys."
  type = object({
    algorithm            = string
    rsa_bits             = number
    private_key_filename = string
    public_key_filename  = string
  })

  default = {
    algorithm            = "your_algorithm"
    rsa_bits             = 2048
    private_key_filename = "your_private_key_path"
    public_key_filename  = "your_public_key_path"
  }
}

variable "aws_key_pair_name" {
  description = "The name of the AWS key pair to be used for SSH access."
  type    = string
  default = "your_key_pair_name"
}

variable "vpc" {
  description = "VPC configuration with CIDR block for defining the network range."
  type = map(string)
  default = {
    cidr_block = "your_cidr_block"
  }
}

variable "subnet" {
  description = "Subnet configuration: includes CIDR block, availability zone, and whether to assign public IPs."
  type = object({
    cidr_block              = string
    availability_zone       = string
    map_public_ip_on_launch = bool
  })

  default = {
    cidr_block              = "your_subnet_cidr_block"
    availability_zone       = "your_availability_zone"
    map_public_ip_on_launch = true
  }
}

variable "ingress" {
  description = "Ingress rules for allowed inbound traffic: HTTP, HTTPS, and SSH port numbers."
  type = map(number)
  default = {
    http_port  = 80
    https_port = 443
    ssh_port   = 22
  }
}

variable "egress_port" {
  description = "Egress port configuration, representing the allowed outbound port range."
  type = number
  default = 0
}

variable "ec2_instance" {
  description = "EC2 instance configuration: includes AMI ID, instance type, and whether to associate a public IP address."
  type = object({
    ami                         = string
    instance_type               = string
    associate_public_ip_address = bool
  })

  default = {
    ami                         = "your_ami_id"
    instance_type               = "your_instance_type"
    associate_public_ip_address = true
  }
}
Enter fullscreen mode Exit fullscreen mode

Define all required variables with descriptions.

Create output values:

In outputs.tf, add:

# Output the Public IPs
output "ubuntu_instance_public_ip" {
  value = aws_instance.ubuntu_instance.public_ip
}
Enter fullscreen mode Exit fullscreen mode

This output displays the public IP of the EC2 instance.

looks good, now time to use this module in the root module.

Define main.tf in root module:

here we will define provider block and give source to our nginx-module then pass the values to our nginx-module input variables. we will local values to pass the values to module.

locals {
  tls_keys = {
    algorithm            = "RSA"
    rsa_bits             = 4096
    private_key_filename = "./.ssh/terraform_rsa"
    public_key_filename  = "./.ssh/terraform_rsa.pub"
  }

  aws_key_pair_name = "ubuntu_ssh_key"

  vpc = {
    cidr_block = "10.0.0.0/16"
  }

  subnet = {
    cidr_block              = "10.0.1.0/24"
    availability_zone       = "us-east-1a"
    map_public_ip_on_launch = true
  }

  ingress = {
    http_port  = 80
    https_port = 443
    ssh_port   = 22
  }

  egress_port = 0

  ec2_instance = {
    ami                         = "ami-0a0e5d9c7acc336f1"
    instance_type               = "t2.micro"
    associate_public_ip_address = true
  }
}



provider "aws" {
  region = "us-east-1"
}

module "nginx-module" {
  source = "./nginx-module"

  tls_keys = local.tls_keys
  aws_key_pair_name = local.aws_key_pair_name
  vpc = local.vpc
  subnet = local.subnet
  ingress = local.ingress
  egress_port = local.egress_port
  ec2_instance = local.ec2_instance

}
Enter fullscreen mode Exit fullscreen mode

output value for ubuntu public_ip:

create one output value to access the Ubuntu ip provided by the module.

output "ubuntu_instance_public_ip" {
  value = module.nginx-module.ubuntu_instance_public_ip
}
Enter fullscreen mode Exit fullscreen mode

Congratulations you just created the infrastructure for nginx on Ubuntu.

let's apply the infrastructure.

Initialize our infrastructure:

move to Infrastructure-Nginx/main.tf and run the below command:

terraform init
Enter fullscreen mode Exit fullscreen mode

terraform init

Plan our infrastructure:

now run the below command to plan our infrastructure, so that we will know what will be deployed:

terraform plan
Enter fullscreen mode Exit fullscreen mode

terraform init

Apply our infrastructure:

run terraform apply

terraform init

Confirm the changes by typing “yes”.

terraform init

Awesome! You just created your Ubuntu EC2 instance via Terraform.

Check through AWS UI:

Navigate to the AWS Management Console to verify your instance and other resources. You can now view the public IP address and other details directly in the console.

terraform init

Access Through Port 80 in Your Browser:

Open your web browser and enter http://your-public-ip:80 in the address bar, replacing your-public-ip with the EC2 instance's public IP. You should see your "Hello From Ubuntu EC2 Instance!!!" message.

terraform init

Destroy the Infrastructure:

If you want to tear down the infrastructure you created, use the terraform destroy command

terraform init

Confirm the changes by typing “yes”.

terraform init

Delete all the resources defined in your configuration, ensuring a clean removal of everything Terraform created.

terraform init

Conclusion

Terraform Modules offer a powerful way to organize and reuse infrastructure code, while input variables provide flexibility in customizing deployments. By leveraging these features, you can efficiently build and manage complex cloud infrastructures like deploying Nginx on an EC2 instance. So, grab your Terraform Lego set and start building your digital empire!

Top comments (0)