Step-By-Step Guide - Build Terraform Modules - DevOps Shack
Step-By-Step Guide - Build Terraform Modules - DevOps Shack
DevOps Shack
Step-by-Step Guide:
Build Terraform Modules for Reusable Cloud
Infrastructure
Table of Content
1. Introduction
• What is a Terraform Module?
• Why Use Modules?
• Real-World Use Cases
2. Prerequisites
• Tools Required
• Basic Terraform Knowledge
• AWS (or other provider) Access Setup
3. Project Structure
• Folder and File Layout
• Root Module vs. Child Modules
• Recommended Naming Conventions
2
• Example: Creating a VPC Module
3
• Documentation with README.md in Each Module
12. Conclusion
• Key Takeaways
• When to Refactor Modules
• What’s Next: Dynamic Modules and Workspaces
4
1. Introduction
What is a Terraform Module?
A Terraform module is a container for multiple Terraform configuration files
that are used together. It enables the reuse of configuration code across
projects and environments by encapsulating resources into logical units.
At a basic level, any Terraform configuration in a folder is a module. There are:
• Root Modules: The configuration in the main working directory.
• Child Modules: Reusable modules imported into the root module.
5
Use Case Description
Multi-Environment Use the same modules with different input variables for
Setup dev, staging, prod
2. Prerequisites
Before diving into building Terraform modules, it’s important to have a solid
grasp on a few core tools and concepts. This section ensures you’re fully
equipped to follow along and start creating reusable cloud infrastructure
confidently.
Tools Required
You’ll need to have the following installed and configured on your machine:
Tip: If you're using VS Code, install the Terraform extension for syntax
highlighting and linting.
6
Cloud Provider Access
Since you'll be creating infrastructure, you need access to a cloud provider. In
this guide, we’ll use AWS as an example.
Ensure the following:
1. You have an AWS account with programmatic access (i.e., access key ID
and secret).
2. You’ve configured the AWS CLI with the credentials:
aws configure
This sets up your default profile with AWS credentials and region.
3. IAM permissions are sufficient to create resources like VPCs, EC2
instances, and S3 buckets.
Security Tip: Use IAM roles and policies with least privilege when working
with automation and Terraform.
7
You’ll want to maintain a clean, scalable structure for module development.
Here’s a sneak peek of what it might look like:
terraform-infra/
│
├── main.tf
├── variables.tf
├── outputs.tf
│
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── ec2/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
│
└── environments/
├── dev/
│ └── main.tf
└── prod/
└── main.tf
You’ll build and use these modules throughout this guide.
Summary Checklist
Make sure you have:
8
• Terraform installed and working (terraform -v)
9
3. Project Structure
Creating a clean, modular, and scalable project structure is one of the most
important steps when working with Terraform. It not only keeps your code
organized but also makes collaboration, debugging, and scaling much easier.
10
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ec2/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── s3/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
│
└── environments/ # Environment-specific configurations
├── dev/
│ └── main.tf # Calls modules with dev-specific input values
└── prod/
└── main.tf # Calls modules with prod-specific input values
Pro Tip: You can split main.tf, provider.tf, backend.tf, and others into
separate files for clarity, but Terraform will automatically load all *.tf files in a
folder.
Layer Purpose
Child Module Defines reusable resource blocks with variables and outputs
What’s in a Module?
11
A Terraform module typically contains 3 essential files:
File Purpose
Rule of Thumb: A module should do one thing well (e.g., create a VPC or
an EC2 instance), and expose variables and outputs to make it flexible and
reusable.
Naming Conventions
Keeping consistent naming is key in large Terraform codebases:
• Use snake_case for variables and outputs: instance_type, vpc_id
• Use descriptive module names: aws_vpc_module, rds_mysql_module
• Use prefixes in resource names to identify environments: dev-ec2-web,
prod-db
Summary
By following a modular and layered project structure, you’ll gain:
• Better reusability across environments
• Simplified debugging and upgrades
• Cleaner version control and CI/CD integration
• More maintainable and scalable infrastructure code
12
4. Creating Your First Module
To get hands-on with Terraform modules, we’ll start by creating a simple but
essential module to provision a Virtual Private Cloud (VPC) in AWS.
Objective
You’ll learn how to:
• Create a basic VPC module (main.tf, variables.tf, outputs.tf)
• Parameterize it with variables
• Use the module in a root configuration
Directory Setup
Navigate to your project and create a new folder inside the modules directory:
mkdir -p modules/vpc
cd modules/vpc
touch main.tf variables.tf outputs.tf
You should now have this structure:
css
CopyEdit
modules/
└── vpc/
├── main.tf
├── variables.tf
└── outputs.tf
13
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = var.name
}
}
We're defining two resources: the VPC and an Internet Gateway attached
to it.
variable "cidr_block" {
14
description = "CIDR block for the VPC"
type = string
}
output "igw_id" {
description = "The ID of the Internet Gateway"
value = aws_internet_gateway.igw.id
}
module "vpc" {
source = "./modules/vpc"
15
name = "my-vpc"
cidr_block = "10.0.0.0/16"
}
Summary
You now have:
• A standalone VPC module with inputs and outputs
• A root config that reuses the module
• A basic foundation for modular, scalable infrastructure
16
5. Using the Module in Root Configuration
Once your module is built, the next step is integrating it into a root
configuration. This is where you instantiate the module, pass inputs, and
consume outputs. You’ll also see how this setup supports multiple
environments like dev, staging, and prod using the same module with different
values.
module "vpc" {
source = "./modules/vpc"
name = var.name
cidr_block = var.cidr_block
}
Here, we reference the vpc module and pass two variables to it: name and
cidr_block.
17
type = string
default = "dev-vpc"
}
variable "cidr_block" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
Or use a terraform.tfvars file to assign values per environment:
hcl
CopyEdit
# terraform.tfvars
name = "staging-vpc"
cidr_block = "10.1.0.0/16"
18
To manage multiple environments using the same module, you can create
separate folders for each environment:
environments/
├── dev/
│ ├── main.tf
│ └── terraform.tfvars
├── staging/
│ ├── main.tf
│ └── terraform.tfvars
└── prod/
├── main.tf
└── terraform.tfvars
Each main.tf in those folders can look like:
module "vpc" {
source = "../../modules/vpc"
name = var.name
cidr_block = var.cidr_block
}
And the corresponding terraform.tfvars file can define:
name = "prod-vpc"
cidr_block = "10.2.0.0/16"
19
terraform init
terraform plan -var-file="terraform.tfvars"
terraform apply -var-file="terraform.tfvars"
Repeat for staging or prod as needed.
Summary
At this point, you’ve:
• Learned how to use a module in root Terraform files
• Supplied input variables and retrieved outputs
• Structured your project for multi-environment deployment
20
6. Best Practices for Writing Reusable Modules
Reusable Terraform modules should be clean, scalable, and flexible enough to
be used in multiple contexts (e.g., dev, prod, staging) with minimal changes.
Let’s dive into the principles and strategies that help you build rock-solid
modules.
variable "enable_dns" {
description = "Enable DNS support in the VPC"
type = bool
21
default = true
}
3. Avoid Hardcoding
Never hardcode values like AMI IDs, CIDRs, or regions inside your modules.
Make them configurable via variables.
Bad:
22
This makes your module DRY and improves readability.
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
9. Document Everything
Every module should include a README.md that explains:
• What the module does
• Required/optional variables
• Outputs
• Usage examples
This is invaluable for teams and open-source usage.
Summary
Following these practices ensures your modules are:
• Reusable across projects and environments
• Maintainable and easy to debug
• Safe for collaboration and upgrades
24
7. Automating Terraform with CI/CD Pipelines
Automating your Terraform deployments ensures consistency, reduces human
error, and makes infrastructure management easier. Integrating Terraform into
a CI/CD pipeline also speeds up your deployment cycles and supports modern
DevOps workflows.
Objective
You will learn how to:
• Set up a simple CI/CD pipeline for Terraform using GitHub Actions.
• Automate the Terraform workflow (init, plan, apply).
• Manage remote state for collaboration using Terraform Cloud or AWS
S3.
on:
push:
branches:
- main
pull_request:
25
branches:
- main
jobs:
terraform:
runs-on: ubuntu-latest
steps:
# Checkout the code
- name: Checkout code
uses: actions/checkout@v2
# Set up Terraform
- name: Set up Terraform
uses: hashicorp/setup-terraform@v1
# Initialize Terraform
- name: Terraform Init
run: terraform init
26
run: terraform plan -out=tfplan
27
}
}
Terraform Cloud will automatically manage your state file, and you can trigger
the workflow directly from GitHub Actions.
2.2 Using AWS S3 for Remote State Management
If you prefer using AWS, you can store the state files in S3 and lock them with
DynamoDB.
1. Create an S3 bucket to store the state file.
2. Create a DynamoDB table for state locking (e.g., terraform-lock).
Configure your backend in main.tf:
terraform {
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "path/to/your/statefile.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-lock"
}
}
Terraform will now use S3 for state storage and DynamoDB for state locking,
allowing team members to safely collaborate.
28
3. Modify your workflow to use these secrets:
# .github/workflows/terraform.yml
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Summary
You’ve now automated your Terraform workflow using GitHub Actions, with:
• Automated Terraform runs on commits
• Remote state management via Terraform Cloud or AWS S3
• Secure secrets handling through GitHub Secrets
29
8. Advanced Terraform Features
In this section, we’ll explore more powerful features such as workspaces,
dynamic blocks, and module dependencies. These tools will allow you to
further enhance your Terraform code for better scalability, flexibility, and
maintainability.
Objective
You will learn how to:
• Leverage workspaces for environment-specific configurations.
• Use dynamic blocks for more flexible and reusable code.
• Manage module dependencies to control resource creation order.
30
resource "aws_s3_bucket" "bucket" {
bucket = "my-terraform-bucket-${terraform.workspace}"
acl = "private"
}
Here, the bucket name changes based on the current workspace (dev, staging,
or prod).
1.3 Workspace-Specific Variables
You can also define workspace-specific variables in your terraform.tfvars files.
For example:
# dev.tfvars
instance_type = "t2.micro"
# prod.tfvars
instance_type = "t2.large"
When you run terraform apply, specify the correct variables file based on the
active workspace:
terraform apply -var-file=dev.tfvars
This way, you can maintain different configurations for each workspace.
31
description = "List of subnet CIDR blocks"
type = list(string)
}
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "subnet-${each.value}"
}
}
Here, for_each iterates over the list of subnets, creating one aws_subnet
resource for each CIDR block provided in var.subnets.
2.2 Using Dynamic Blocks for Complex Resources
Dynamic blocks are also helpful when working with complex resources that
may have a nested block structure. For instance, with security groups, you
might have variable numbers of inbound and outbound rules:
resource "aws_security_group" "example" {
name = "example-sg"
description = "Example security group"
dynamic "ingress" {
for_each = var.ingress_rules
32
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
dynamic "egress" {
for_each = var.egress_rules
content {
from_port = egress.value.from_port
to_port = egress.value.to_port
protocol = egress.value.protocol
cidr_blocks = egress.value.cidr_blocks
}
}
}
This allows you to dynamically define any number of ingress and egress rules
by passing a list of objects for var.ingress_rules and var.egress_rules.
33
If one module depends on the output of another, you can specify the
dependency using the depends_on argument:
module "vpc" {
source = "./modules/vpc"
name = "my-vpc"
cidr_block = "10.0.0.0/16"
}
module "subnets" {
source = "./modules/subnets"
vpc_id = module.vpc.vpc_id
depends_on = [module.vpc]
}
In this case, module.subnets will only run after module.vpc has been created,
ensuring that the VPC exists before creating subnets.
3.2 Using Outputs for Module Communication
You can pass data from one module to another using outputs and inputs:
# module vpc outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
34
In this case, the subnets module takes the vpc_id output from the vpc module
and uses it as an input for creating the subnets.
Summary
With these advanced features, you’ve gained the ability to:
• Manage multiple environments using workspaces.
• Build flexible, reusable code using dynamic blocks.
• Control resource creation order and manage module dependencies.
35
9. Troubleshooting and Performance Optimization
In this section, we’ll discuss strategies for debugging Terraform issues and
optimizing the performance of your infrastructure deployments.
Objective
You will learn how to:
• Troubleshoot common Terraform errors and issues.
• Use Terraform Debugging tools.
• Optimize your Terraform configurations to improve performance.
• Utilize state management best practices.
36
Solution:
• Double-check the resource definition to ensure all required variables or
arguments are defined in the module or resource.
• Use terraform validate to check the configuration before running a plan
or apply.
1.3 Error: No valid credential sources found for AWS
If you’re working with AWS and Terraform can’t find your credentials, you’ll see
this error.
Solution:
• Ensure your AWS credentials are correctly set in the environment
variables (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY).
• Alternatively, configure your ~/.aws/credentials file or use an IAM role if
running from EC2 or other AWS services.
38
Every time Terraform interacts with a cloud provider, it makes an API call. Try to
minimize these by:
• Reducing the frequency of terraform plan and apply calls.
• Using terraform taint carefully to force the recreation of resources only
when absolutely necessary.
3.4 Use Parallelism
Terraform allows parallel resource creation by using the -parallelism flag.
Increasing the number of parallel resources can significantly reduce
deployment time for large infrastructures.
bash
CopyEdit
terraform apply -parallelism=10
However, be cautious with parallelism; some cloud providers or resources
might have limits on the number of simultaneous API calls.
3.5 Use Data Sources Efficiently
When you need to reference data from existing resources, use data sources
instead of importing them into the state file, especially if the data is read-only
or doesn’t change often.
For example:
data "aws_ami" "latest" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
39
Step 4: Best Practices for State Management
Proper state management is critical for collaboration and performance. Here
are a few best practices:
4.1 Remote State Storage
Always use remote state to share state across teams and avoid conflicts. You
can use Terraform Cloud, AWS S3, Azure Storage, or Google Cloud Storage for
remote state management.
4.2 State Locking
To avoid race conditions and conflicts when multiple users or processes are
working on the same state, enable state locking. For example, when using AWS
S3, enable DynamoDB-based locking.
4.3 State Backups
Make sure to back up your state files regularly to prevent data loss. Terraform
automatically creates backup files, but you can configure your backend to keep
a more extended history.
4.4 Use Workspaces for Isolated State
As discussed in a previous section, use workspaces to keep environments
isolated and their respective states separate. This ensures that changes to one
environment do not affect others.
Summary
With these tips, you’re now ready to:
• Troubleshoot common Terraform errors with debugging tools and logs.
• Optimize your Terraform workflows using parallelism, targeting, and
data sources.
• Manage remote state efficiently for collaborative teams and
environments.
40
10. Scaling Terraform for Teams and Best Practices
In this final section, we’ll cover how to effectively manage your Terraform
workflows across teams and large organizations. We’ll also discuss some best
practices for version control and ensuring scalability as your infrastructure
grows.
Objective
You will learn how to:
• Manage the Terraform lifecycle in team environments.
• Integrate Terraform with version control systems like Git.
• Scale Terraform workflows across large teams with workspaces, module
sharing, and collaboration tools.
• Follow best practices for infrastructure management using Terraform.
42
git add .
git commit -m "Initial commit for Terraform configuration"
git push origin main
2.2 Using Branches for Different Environments
Utilize branches for different environments (e.g., dev, staging, prod). This
isolates changes to specific environments, reduces risks, and makes it easier to
handle approvals.
• Dev branch: For ongoing development and testing.
• Staging branch: For testing new features and configurations before
production.
• Prod branch: For stable, production-ready configurations.
2.3 Code Reviews and PR Workflow
Use pull requests (PRs) for reviewing changes. When a team member submits
a PR, they can include the output of terraform plan for review, ensuring that
only valid changes are applied.
44
module "vpc" {
source = "github.com/my-org/terraform-modules/vpc"
version = "1.2.3"
}
4.3 Use terraform fmt and terraform validate
Ensure your code is consistent and error-free by using terraform fmt for
formatting and terraform validate for validating configuration files:
bash
CopyEdit
terraform fmt
terraform validate
4.4 Implement a CI/CD Pipeline for Terraform
Automate the execution of your Terraform commands (init, plan, apply) using
CI/CD tools like GitHub Actions, GitLab CI, or Jenkins. This ensures consistent
deployments and reduces manual errors.
45
Conclusion
By following these strategies and best practices, you can scale Terraform
effectively across teams, maintain version-controlled infrastructure code, and
optimize the workflow for large environments. With proper lifecycle
management, collaboration, and reusability, Terraform becomes a powerful
tool for managing infrastructure at scale.
You’ve now learned:
• How to manage the Terraform lifecycle in a team setting.
• How to integrate Terraform with version control systems.
• Best practices for scaling Terraform workflows across large teams.
• Techniques to ensure the security and performance of your Terraform
deployments.
With this guide, you should be well-equipped to manage complex
infrastructure projects using Terraform efficiently. Happy automating!
Let me know if you’d like more details on any of these topics or further
clarification!
46