DEV Community

Cover image for What are variables in Terraform? And how do I arrange my files?
Emmanuel E. Ebenezer
Emmanuel E. Ebenezer

Posted on

What are variables in Terraform? And how do I arrange my files?

Variables are a fundamental concept in every programming language because they are useful in building dynamic programs. We use variables to store temporary or "permanent" values so they can assist programming logic in simple as well as complex programs.

The result of an expression is a value. All values have a type, which dictates where that value can be used and what transformations can be applied to it. Well said by Hashicorp.

Welcome to my day05-07 in #30DaysOfAwsTerraform and in this post, we discuss the types of Variables in Terraform, how they are used and how to setup of Terraform directory for clean configurations.

Terraform variables are essential for building, scalable, highly maintainable and adaptable infrastructure configurations, effectively contributing efficient infrastructure management and deployment practices.

Let's dive in.

Variables day05

Variables can be categorized based on these two categories:

  • Based on Purpose:

    • Input: these variables serve as parameters for your Terraform modules, allowing you to customize configurations without changing the source code. They make your code reusable and flexible.
      Example:
      The input variable is declared with the variable block, and used in the resource block.

      variable "environment" {
          description = "Environment name"
          type        = string
      }
      
      resource "aws_instance" "web" {
          ami           = "ami-12345678"
          instance_type = var.instance_type
      
          tags = {
              Environment = var.environment
          }
      }
      
    • Output variables export values from your module, making them available for other configurations or displaying important information after deployment.

      Example: this values will be printed by default to the console after terraform applies the desired configurations

      output "instance_public_ip" {
      description = "Public IP of the EC2 instance"
      value       = aws_instance.web.public_ip
      }
      
      output "instance_id" {
      description = "ID of the EC2 instance"
      value       = aws_instance.web.id
      }
      
    • Local Variables
      Locals are temporary values you define in Terraform to avoid repetition and clean up complex expressions. Terraform calculates them once when the configuration is evaluated, and you can reuse them anywhere.

      They differ from input variables in one important way:

      Input variables can be changed at runtime (through a dedicated variables file, CLI flags, workspaces, etc.).

      Locals cannot be changed at runtime — they are fixed inside the module and can only be derived from other values (including input variables). Terraform computes them after the plan starts and they cannot be overridden.

      In short:
      Variables are inputs. Locals are internal helpers.

      Example:

      locals {
      common_tags = {
          Project     = "MyApp"
          Environment = var.environment
          ManagedBy   = "Terraform"
      }
      
      instance_name = "${var.environment}-web-server"
      }
      
      # main.tf - Using locals
      resource "aws_instance" "web" {
      ami           = "ami-12345678"
      instance_type = var.instance_type
      
      tags = merge(
          local.common_tags,
          {
          Name = local.instance_name
          }
      )
      }
      
  • Variables Based on Values

    • Primitive Types

      Simple, single-value data types
      String:

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

      Number:

      variable "instance_count" {
          type    = number
          default = 2
      }
      

      Bool:

      variable "enable_monitoring" {
          type    = bool
          default = true
      }
      
    • Complex Types

      Structured data types that can hold multiple values.

      List:

      variable "availability_zones" {
          type    = list(string)
          default = ["us-east-1a", "us-east-1b", "us-east-1c"]
      }
      
      # Usage
      resource "aws_subnet" "public" {
          count             = length(var.availability_zones)
          availability_zone = var.availability_zones[count.index]
          # ... other configuration
      }
      

      Map:

      variable "instance_types" {
          type = map(string)
          default = {
              dev  = "t2.micro"
              prod = "t3.large"
          }
      }
      
      # Usage
      resource "aws_instance" "app" {
          instance_type = var.instance_types[var.environment]
          # ... other configuration
      }
      

      Object:

      variable "server_config" {
      type = object({
          instance_type = string
          volume_size   = number
          enable_backup = bool
      })
      
      default = {
          instance_type = "t2.micro"
          volume_size   = 20
          enable_backup = true
      }
      }
      
      # Usage
      resource "aws_instance" "server" {
          instance_type = var.server_config.instance_type
      
          root_block_device {
              volume_size = var.server_config.volume_size
              }
      }
      

      Set:

      variable "allowed_ports" {
          type    = set(number)
          default = [22, 80, 443]
      }
      
      # Usage
      resource "aws_security_group_rule" "allow_ports" {
          for_each = var.allowed_ports
      
          type        = "ingress"
          from_port   = each.value
          to_port     = each.value
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
      }
      
    • Any or Null Types

      Any:

      variable "custom_config" {
          description = "Flexible configuration that can accept any type"
          default     = {}
      }
      

      Nullable:

      variable "backup_retention_days" {
          type    = number
          default = null  # Allows explicit null value
      }
      

Hey, you can still join the #30DaysOfAwsTerraform. And learn Terraform on AWS for within 30 days.

Terraform Directory Structure, day06

With the knowledge of variables, we can intuitively separate terraform files for better management, instead of putting everything in a single .tf file.

A collection of .tf files is called a module (still more of this later in the series)
Terraform always runs in the context of a single root module. A complete Terraform configuration consists of a root module and the tree of child modules (which includes the modules called by the root module, any modules called by those modules, etc.). In Terraform CLI, the root module is the working directory where Terraform is invoked.

So a sample terraform project would look like this:

terraform-project/
├── main.tf           # Primary resource definitions
├── variables.tf      # Input variable declarations
├── outputs.tf        # Output variable declarations
├── locals.tf         # Local variable definitions
├── providers.tf      # Provider configurations
├── versions.tf       # Terraform and provider version constraints
├── terraform.tfvars  # Variable value assignments (not committed to git)
└── modules/
    └── networking/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf
Enter fullscreen mode Exit fullscreen mode

This structure keeps your configurations organized, maintainable, and easy to navigate following good file organization principles

  1. Separation of Concerns: Group related resources together
  2. Logical Grouping: Organize by service or function
  3. Consistent Naming: Use clear, descriptive file names
  4. Modular Approach: Keep files focused on specific areas
  5. Documentation: Include README and comments

I'm learning Terraform within 30days with Piyush. You can still join the challenge here.

Type Contraints day07

Because Terraform is mostly configuration files than logical programming, resources and terraform blocks need specific types of values, if not provided appropriately, the configuration would never work.

Common Type Patterns Include:

  1. Environment-specific configurations
  2. Resource sizing based on type
  3. Tag standardization
  4. Network configuration validation
  5. Security policy enforcement

Best Practices for writing configuration files include

  1. Always specify types for variables
  2. Use validation blocks for business rules
  3. Provide meaningful error messages
  4. Use appropriate collection types (list vs set vs map)
  5. Validate complex objects thoroughly
  6. Use type conversion functions when needed
  7. Document type requirements in descriptions

Comprehensive Example: VPC Configuration Variable

# variables.tf

variable "vpc_config" {
  description = "object configuration for a vpc"
  type = object({
    cidr_block         = string
    name               = string
    enable_dns         = bool
    environment        = string
    availability_zones = set(string)
    subnet_configs     = map(string)
  })
  validation {
    condition     = contains(["dev", "staging", "prod"], var.vpc_config.environment)
    error_message = "Environment must be one of: dev, staging, prod."
  }

# main.tf - Using the variable with type conversion

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_config.cidr_block
  enable_dns_hostnames = var.vpc_config.enable_dns
  enable_dns_support   = var.vpc_config.enable_dns

  tags = {
    Name        = var.vpc_config.name
    Environment = var.vpc_config.environment
    ManagedBy   = "Terraform"
  }
}

resource "aws_subnet" "subnets" {
  for_each = var.vpc_config.subnet_configs

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value
  availability_zone = element(tolist(var.vpc_config.availability_zones), index(keys(var.vpc_config.subnet_configs), each.key))

  tags = {
    Name        = each.key
    Environment = var.vpc_config.environment
  }
}

# terraform.tfvars - Example values

vpc_config = {
  cidr_block         = "10.0.0.0/16"
  name               = "my-production-vpc"
  enable_dns         = true
  environment        = "prod"
  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
  subnet_configs = {
    "public-subnet-1"  = "10.0.1.0/24"
    "public-subnet-2"  = "10.0.2.0/24"
    "private-subnet-1" = "10.0.10.0/24"
    "private-subnet-2" = "10.0.11.0/24"
  }
}
## What This Example Demonstrates

1. Type specification: Complex `object()` type clearly defined
2. Validation block: validation for business rules
3. Meaningful error message: The validation explains what is wrong and how to fix it
4. Appropriate collections: `set` for unique AZs, `map` for subnet configs
5. Thorough validation: Validates nested object properties using `alltrue()` and loops
6. Type conversion: Uses `tolist()` and `element()` to convert set to list for indexing
7. Documentation: Detailed description with heredoc syntax explaining all requirements

This single variable enforces infrastructure standards while remaining flexible and well-documented!
Enter fullscreen mode Exit fullscreen mode

It's been a wonderful weekend and I'm one more step closer to writing Terraform configurations that effective for deployment, and very explicit for collaborative development.

See you on the next one.

If you want to build infrastructure with code, join Piyush on Youtube and the CloudOps Community on discord.

Top comments (0)