Understanding Terraform Module Structure

Example Structure

Without modules

  • complex configurations
  • huge file
  • no overview

why modules

  • organize and group configurations
  • encapsulate into distinct logical components
  • reuse

Modules Project Structure

  • main.tf
  • variables.tf
  • outputs.tf
  • providers.tf

Here’s an example structure for a simple AWS VPC module:

modules/
└── vpc/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf
  • main.tf: This file contains the resource definitions. For instance, defining an AWS VPC:

    resource "aws_vpc" "example" {
        cidr_block = var.cidr_block
        tags = { Name = var.name }
    }
  • variables.tf: Here, you declare the input variables:

    variable "cidr_block" {
        description = "The CIDR block for the VPC."
        type        = string
    }
     
    variable "name" {
        description = "The name of the VPC."
        type        = string
    }
  • outputs.tf: This file defines what output values the module will return:

    output "vpc_id" {
        value = aws_vpc.example.id
    }

Using Modules in Terraform Configuration

To utilize a module within your main Terraform configuration, you would include it like this in your root module:

module "my_vpc" {
  source     = "./modules/vpc"
  cidr_block = "10.0.0.0/16"
  name       = "my-vpc"
}

This allows you to create multiple instances of your VPC with different configurations by simply reusing the module.

Best Practices for Module Structure

  1. Encapsulation: Group related resources together in a single module to simplify management and promote reuse.
  2. Versioning: Use version control for your modules to track changes and ensure stability across deployments.
  3. Naming Conventions: Use clear and consistent naming conventions for modules and variables to enhance readability.
  4. Documentation: Provide documentation within each module to explain its purpose, input variables, and outputs.

Advanced Structuring Techniques

As projects scale, it may be beneficial to adopt more complex structures:

  • Stacks vs. Modules: A stack can consist of multiple modules, allowing for independent deployment and management of infrastructure components[3]. This separation can help manage large infrastructures more effectively.
  • Environment-Specific Modules: Create separate directories or repositories for different environments (e.g., development, staging, production) to maintain clear boundaries and configurations[4].

By following these guidelines, you can create robust and reusable Terraform modules that streamline your infrastructure management processes.

The distinction between variables.tf and terraform.tfvars in Terraform is primarily about declaration versus assignment of variables.

variables.tf

  • Purpose: This file is used to declare variables that your Terraform configuration will use. It defines the variable names, types, and optional default values.
  • Content Example:
    variable "instance_type" {
      type    = string
      default = "t2.micro"
    }
     
    variable "region" {
      type = string
    }
  • Functionality: By declaring a variable in variables.tf, you inform Terraform that this variable exists and can be referenced using var.<variable_name> in your configuration files. You can also set default values, making the variable optional during execution.

terraform.tfvars

  • Purpose: This file is specifically for assigning values to the variables declared in variables.tf. It provides the actual values that Terraform will use when executing plans or applying configurations.
  • Content Example:
    instance_type = "t2.large"
    region        = "us-west-2"
  • Functionality: Terraform automatically loads this file when you run commands like terraform apply or terraform plan, allowing you to set specific values for your variables without needing to specify them in the command line each time. If a variable is defined in both variables.tf and terraform.tfvars, the value from terraform.tfvars will override any default value set in variables.tf.

Summary of Differences

Featurevariables.tfterraform.tfvars
PurposeDeclare variables and their typesAssign values to declared variables
File Type.tf file (HCL syntax).tfvars file (HCL or JSON syntax)
Default ValuesCan set default valuesCannot set default values
Automatic LoadingNot automatically loadedAutomatically loaded by Terraform
Usage ContextUsed to define what variables are neededUsed to provide specific values for those variables

In essence, variables.tf defines what inputs your configuration expects, while terraform.tfvars provides the actual inputs for those definitions when running Terraform commands


Other

Terraform Modules: Complete Guide for Associate Exam

What Are Terraform Modules?

A Terraform module is a container for multiple resources that are used together. Every Terraform configuration has at least one module, called the root module, which consists of the resources defined in the .tf files in the main working directory.

Key Concepts

  • Root Module: The main configuration directory where you run terraform commands
  • Child Module: A module called by another module
  • Published Module: A module published to the Terraform Registry or a private registry

Module Structure

Basic Module Directory Structure

my-module/
├── main.tf          # Primary resource definitions
├── variables.tf     # Input variable declarations
├── outputs.tf       # Output value definitions
├── versions.tf      # Provider requirements (optional)
├── README.md        # Documentation (recommended)
└── examples/        # Example usage (recommended)
    └── basic/
        ├── main.tf
        └── variables.tf

Essential Files

main.tf

Contains the primary logic and resources:

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = var.instance_type
  
  tags = {
    Name = var.instance_name
  }
}

variables.tf

Defines input variables:

variable "ami_id" {
  description = "The AMI ID to use for the instance"
  type        = string
}
 
variable "instance_type" {
  description = "The instance type"
  type        = string
  default     = "t3.micro"
}
 
variable "instance_name" {
  description = "Name tag for the instance"
  type        = string
}

outputs.tf

Defines outputs that other modules can reference:

output "instance_id" {
  description = "The ID of the EC2 instance"
  value       = aws_instance.web.id
}
 
output "public_ip" {
  description = "The public IP of the instance"
  value       = aws_instance.web.public_ip
}

Using Modules

Module Sources

Local Modules

module "web_server" {
  source = "./modules/web-server"
  
  ami_id        = "ami-12345678"
  instance_type = "t3.small"
  instance_name = "web-01"
}

Registry Modules

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 3.0"
  
  name = "my-vpc"
  cidr = "10.0.0.0/16"
}

Git Sources

module "example" {
  source = "git::https://github.com/user/repo.git//modules/example?ref=v1.0.0"
}

Other Sources

  • HTTP URLs
  • S3 buckets
  • GCS buckets

Module Block Syntax

module "MODULE_NAME" {
  source = "SOURCE"
  
  # Input variables
  variable_name = value
  
  # Meta-arguments
  count      = 2
  for_each   = var.environments
  depends_on = [resource.example]
  providers  = {
    aws = aws.us_west_2
  }
}

Input Variables

Variable Types

# String
variable "region" {
  type = string
}
 
# Number
variable "instance_count" {
  type = number
}
 
# Boolean
variable "enable_monitoring" {
  type = bool
}
 
# List
variable "availability_zones" {
  type = list(string)
}
 
# Map
variable "tags" {
  type = map(string)
}
 
# Object
variable "server_config" {
  type = object({
    name = string
    port = number
  })
}

Variable Validation

variable "instance_type" {
  type        = string
  description = "EC2 instance type"
  
  validation {
    condition = contains([
      "t3.micro", "t3.small", "t3.medium"
    ], var.instance_type)
    error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
  }
}

Variable Precedence (highest to lowest)

  1. Command line flags (-var and -var-file)
  2. *.auto.tfvars files
  3. terraform.tfvars file
  4. Environment variables (TF_VAR_name)
  5. Variable defaults
  6. Interactive prompts

Output Values

Output Syntax

output "instance_ip" {
  description = "The private IP address of the instance"
  value       = aws_instance.web.private_ip
  sensitive   = false  # Default is false
}

Sensitive Outputs

output "database_password" {
  value     = aws_db_instance.example.password
  sensitive = true
}

Accessing Module Outputs

# In root module
resource "aws_security_group_rule" "allow_web" {
  source_security_group_id = module.web_server.security_group_id
}

Data Sources in Modules

data "aws_ami" "latest" {
  most_recent = true
  owners      = ["amazon"]
  
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}
 
resource "aws_instance" "web" {
  ami = data.aws_ami.latest.id
  # ...
}

Module Composition

Calling Multiple Modules

module "vpc" {
  source = "./modules/vpc"
  
  cidr_block = "10.0.0.0/16"
}
 
module "web_servers" {
  source = "./modules/web-server"
  
  vpc_id    = module.vpc.vpc_id
  subnet_id = module.vpc.public_subnet_ids[0]
  
  depends_on = [module.vpc]
}

Module Dependencies

module "database" {
  source = "./modules/database"
  
  depends_on = [module.vpc]
}

Provider Configuration in Modules

Default Provider Inheritance

Child modules inherit provider configurations from the root module.

Explicit Provider Passing

# Root module
provider "aws" {
  alias  = "us_west_2"
  region = "us-west-2"
}
 
module "servers" {
  source = "./modules/servers"
  
  providers = {
    aws = aws.us_west_2
  }
}

Module Provider Requirements

# In module's versions.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.0"
    }
  }
}

Module Versioning

Semantic Versioning

  • MAJOR: Breaking changes
  • MINOR: New features (backward compatible)
  • PATCH: Bug fixes (backward compatible)

Version Constraints

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 3.14"  # Allows 3.14.x
}

Version Constraint Operators

  • =: Exact version
  • !=: Exclude version
  • >, >=, <, <=: Comparison operators
  • ~>: Pessimistic constraint

Module Registry

Public Registry

  • Browse at registry.terraform.io
  • Naming convention: terraform-PROVIDER-NAME
  • Automatically versioned from Git tags

Private Registry

  • Terraform Cloud/Enterprise feature
  • Custom modules for organization use

Module Requirements for Registry

  • GitHub repository
  • Named terraform-PROVIDER-NAME
  • Repository description
  • Standard module structure
  • Git tags for versions

Local Values in Modules

locals {
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    Owner       = var.owner
  }
  
  instance_name = "${var.project_name}-${var.environment}-web"
}
 
resource "aws_instance" "web" {
  tags = merge(local.common_tags, {
    Name = local.instance_name
  })
}

Module Development Best Practices

1. Use Consistent File Structure

  • Always include main.tf, variables.tf, outputs.tf
  • Use versions.tf for provider requirements

2. Comprehensive Variable Definitions

variable "instance_type" {
  description = "The type of instance to start"
  type        = string
  default     = "t3.micro"
  
  validation {
    condition     = can(regex("^t3\\.", var.instance_type))
    error_message = "Instance type must be in the t3 family."
  }
}

3. Meaningful Outputs

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

4. Use Data Sources When Appropriate

data "aws_availability_zones" "available" {
  state = "available"
}

5. Avoid Hard-coded Values

Use variables or data sources instead of hard-coded values.

Module Meta-Arguments

count

module "servers" {
  source = "./modules/server"
  count  = 3
  
  server_name = "web-${count.index + 1}"
}

for_each

module "buckets" {
  source   = "./modules/s3-bucket"
  for_each = toset(var.bucket_names)
  
  bucket_name = each.value
}

depends_on

module "database" {
  source = "./modules/database"
  
  depends_on = [module.vpc]
}

providers

module "vpc" {
  source = "./modules/vpc"
  
  providers = {
    aws = aws.us_west_2
  }
}

Testing Modules

Example Usage

Create examples in the examples/ directory:

examples/
├── basic/
│   ├── main.tf
│   ├── variables.tf
│   └── outputs.tf
└── complete/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf

Validation Commands

# Validate syntax
terraform validate
 
# Check formatting
terraform fmt -check
 
# Plan example
terraform plan -var-file="testing.tfvars"

Common Patterns

Conditional Resources

resource "aws_instance" "web" {
  count = var.create_instance ? 1 : 0
  
  ami           = var.ami_id
  instance_type = var.instance_type
}

Dynamic Blocks

resource "aws_security_group" "web" {
  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

Module Refactoring

Moving Resources

Use moved blocks when refactoring:

moved {
  from = aws_instance.web
  to   = module.web_server.aws_instance.main
}

State Management

  • Use terraform state mv for complex moves
  • Plan carefully when restructuring modules

Troubleshooting Common Issues

1. Reference to Undeclared Output Value

Problem: module.example.output_name doesn’t exist Solution: Define the output in the module’s outputs.tf

2. Variable Not Declared

Problem: Using a variable not defined in variables.tf Solution: Add variable declaration

3. Provider Configuration Issues

Problem: Provider not available in module Solution: Pass provider explicitly or ensure inheritance

4. Version Conflicts

Problem: Module requires different provider version Solution: Update version constraints

Exam Tips

Key Points to Remember

  1. Module structure: Know the standard files and their purposes
  2. Variable precedence: Understand the order of variable resolution
  3. Output referencing: Know how to reference module outputs
  4. Provider inheritance: Understand how providers are passed to modules
  5. Version constraints: Know the different constraint operators
  6. Module sources: Understand different ways to source modules
  7. Meta-arguments: Know when and how to use count, for_each, etc.

Common Exam Scenarios

  • Identifying missing output declarations
  • Understanding variable precedence
  • Recognizing proper module structure
  • Choosing correct version constraints
  • Troubleshooting module reference errors

Commands to Know

# Initialize modules
terraform init
 
# Get/update modules
terraform get
terraform get -update
 
# Validate configuration
terraform validate
 
# Format code
terraform fmt
 
# Show module tree
terraform providers

Summary

Terraform modules are essential for:

  • Code reusability: Write once, use multiple times
  • Organization: Logical grouping of resources
  • Abstraction: Hide complexity behind simple interfaces
  • Collaboration: Share standardized infrastructure patterns
  • Maintenance: Centralized updates and bug fixes

Master these concepts and you’ll be well-prepared for the module-related questions on the Terraform Associate exam!