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
- Encapsulation: Group related resources together in a single module to simplify management and promote reuse.
- Versioning: Use version control for your modules to track changes and ensure stability across deployments.
- Naming Conventions: Use clear and consistent naming conventions for modules and variables to enhance readability.
- 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 usingvar.<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
orterraform 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 bothvariables.tf
andterraform.tfvars
, the value fromterraform.tfvars
will override any default value set invariables.tf
.
Summary of Differences
Feature | variables.tf | terraform.tfvars |
---|---|---|
Purpose | Declare variables and their types | Assign values to declared variables |
File Type | .tf file (HCL syntax) | .tfvars file (HCL or JSON syntax) |
Default Values | Can set default values | Cannot set default values |
Automatic Loading | Not automatically loaded | Automatically loaded by Terraform |
Usage Context | Used to define what variables are needed | Used 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)
- Command line flags (
-var
and-var-file
) *.auto.tfvars
filesterraform.tfvars
file- Environment variables (
TF_VAR_name
) - Variable defaults
- 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
- Module structure: Know the standard files and their purposes
- Variable precedence: Understand the order of variable resolution
- Output referencing: Know how to reference module outputs
- Provider inheritance: Understand how providers are passed to modules
- Version constraints: Know the different constraint operators
- Module sources: Understand different ways to source modules
- 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!