Curious Devs Corner

Curious Devs Corner

Share this post

Curious Devs Corner
Curious Devs Corner
Terraform Quickstart: Part 7 – Terraform Modules Guide
Mini-Courses

Terraform Quickstart: Part 7 – Terraform Modules Guide

How to Use and Create Terraform Modules with Best Practices

KirshiYin's avatar
KirshiYin
Jun 19, 2025
∙ Paid

Share this post

Curious Devs Corner
Curious Devs Corner
Terraform Quickstart: Part 7 – Terraform Modules Guide
Share

Introduction

As your Terraform projects grow, your configuration files can get longer and harder to manage. You might end up repeating the same code across different projects or teams. This is inefficient and increases the risk of mistakes.

Terraform solves this problem elegantly through modules.

Terraform has two types of modules:

  • Custom modules you write yourself.

  • Community modules from the Terraform Registry.

In this chapter, you’ll learn:

  • How to create your own modules.

  • How to use community modules.

  • Simple best practices for working with modules.

Let’s get started!

Use-Case Example: Reusable GitHub Repository Setup

Terraform modules work like reusable building blocks. Instead of copying the same infrastructure code across projects, you define it once in a module and reuse it.

Let’s say your team needs the same GitHub repository setup for every new project:

  • A repository with specific settings.

  • Branch protection rules.

  • A CODEOWNERS file.

Manually repeating this setup wastes time and increases the chance of errors. With a module, you write the configuration once, share it across projects, and keep things consistent. If you ever need to update the setup, you do it in one place.

Creating Your First Module

A typical module directory looks like:

github-repo-module/
├── main.tf
├── variables.tf
├── outputs.tf

First, define the main.tf:

# Main resource definition
resource "github_repository" "repo" {
  name        = var.repository_name
  description = var.description
  visibility  = var.visibility

  has_issues    = var.has_issues
  has_wiki      = var.has_wiki
  has_projects  = var.has_projects

  auto_init     = var.auto_init
  gitignore_template = var.gitignore_template
  license_template   = var.license_template

  topics = var.topics
  
  allow_merge_commit = var.allow_merge_commit
  allow_squash_merge = var.allow_squash_merge
  allow_rebase_merge = var.allow_rebase_merge
  
  delete_branch_on_merge = var.delete_branch_on_merge
}

# Optional branch protection
resource "github_branch_protection" "protection" {
  count = var.enable_branch_protection ? 1 : 0
  
  repository_id = github_repository.repo.node_id
  pattern       = var.default_branch

  enforce_admins         = var.enforce_admins
  require_signed_commits = var.require_signed_commits
  
  required_status_checks {
    strict   = var.status_checks_strict
    contexts = var.required_status_checks
  }

  required_pull_request_reviews {
    dismiss_stale_reviews           = true
    required_approving_review_count = var.required_approving_review_count
  }
}

Define your variable in the variables.tf:

variable "repository_name" {
  description = "The name of the GitHub repository"
  type        = string
}

variable "description" {
  description = "Description of the repository"
  type        = string
  default     = ""
}

variable "visibility" {
  description = "Repository visibility: public or private"
  type        = string
  default     = "private"
  
  validation {
    condition     = contains(["public", "private"], var.visibility)
    error_message = "Valid values for visibility are: public, private"
  }
}

variable "has_issues" {
  description = "Enable GitHub issues"
  type        = bool
  default     = true
}

variable "has_wiki" {
  description = "Enable GitHub wiki"
  type        = bool
  default     = false
}

variable "has_projects" {
  description = "Enable GitHub projects"
  type        = bool
  default     = false
}

variable "auto_init" {
  description = "Automatically initialize the repository with README"
  type        = bool
  default     = true
}

variable "gitignore_template" {
  description = "Template for .gitignore file (e.g., 'Node', 'Python')"
  type        = string
  default     = null
}

variable "license_template" {
  description = "Template for license (e.g., 'mit', 'apache-2.0')"
  type        = string
  default     = null
}

variable "topics" {
  description = "Repository topics/tags"
  type        = list(string)
  default     = []
}

variable "default_branch" {
  description = "Default branch of the repository"
  type        = string
  default     = "main"
}

variable "allow_merge_commit" {
  description = "Allow merge commits"
  type        = bool
  default     = true
}

variable "allow_squash_merge" {
  description = "Allow squash merges"
  type        = bool
  default     = true
}

variable "allow_rebase_merge" {
  description = "Allow rebase merges"
  type        = bool
  default     = true
}

variable "delete_branch_on_merge" {
  description = "Delete branch on merge"
  type        = bool
  default     = true
}

variable "enable_branch_protection" {
  description = "Enable branch protection rules"
  type        = bool
  default     = false
}

variable "enforce_admins" {
  description = "Enforce branch protection for admins"
  type        = bool
  default     = false
}

variable "require_signed_commits" {
  description = "Require signed commits"
  type        = bool
  default     = false
}

variable "status_checks_strict" {
  description = "Require branches to be up to date before merging"
  type        = bool
  default     = true
}

variable "required_status_checks" {
  description = "List of required status checks"
  type        = list(string)
  default     = []
}

variable "required_approving_review_count" {
  description = "Number of approvals needed for PRs"
  type        = number
  default     = 1
}

Finally, print the URLs:


output "repository_name" {
  description = "The name of the repository"
  value       = github_repository.repo.name
}

output "repository_full_name" {
  description = "The full name of the repository (org/repo)"
  value       = github_repository.repo.full_name
}

output "repository_html_url" {
  description = "URL to the repository on the web"
  value       = github_repository.repo.html_url
}

output "repository_ssh_clone_url" {
  description = "SSH URL for cloning the repository"
  value       = github_repository.repo.ssh_clone_url
}

output "repository_http_clone_url" {
  description = "HTTP URL for cloning the repository"
  value       = github_repository.repo.http_clone_url
}

Create a versions.tf file:

terraform {
  required_version = ">= 1.0.0"
  
  required_providers {
    github = {
      source  = "integrations/github"
      version = ">= 4.0.0"
    }
  }
}

The versions.tf file is not a required file — it's just a convention.

It usually contains:

  • The Terraform version constraint.

  • The required providers, including source and version.

This makes it easy to keep version settings separate from the rest of your config (like resources or modules) and ensures consistency across teams or projects.

Using Your Module

Open the main.tf in your main Terraform project. For example, it could look like this:

terraform {
  required_providers {
    github = {
      source  = "integrations/github"
      version = "~> 5.0"
    }
  }
}

provider "github" {
  token = var.github_token
  owner = var.github_owner
}

# Reference the local module using a relative path
module "my_repository" {
  source = "./github-repo-module"
  
  repository_name = "my-new-project"
  description     = "A project created with a local Terraform module"
  visibility      = "public"
  
  topics = ["terraform", "example"]
}



# Outputs
output "repository_url" {
  value = module.my_repository.repository_html_url
}

If you have pushed your module to a GitHub repo, you could have referenced the repo link using the source argument. In our case, we’ll just test locally.

Create the variables.tf file:

variable "github_token" {
  description = "Personal Access Token for GitHub API"
  type        = string
  sensitive   = true
}

variable "github_owner" {
  description = "Github user"
  type        = string
  sensitive   = false
}

Place your token into the terraform.tfvars file:

github_token = "your token"
github_owner = "your username"

If you don’t want to create this file, you could also pass the values as an argument when you execute the terraform apply command:

$ terraform apply -var "github_token=YOUR_GITHUB_TOKEN"

Alternatively, you could export the value as an environment variable:

$ export TF_VAR_github_token=YOUR_GITHUB_TOKEN

$ terraform apply

In this example, we’ll be using the terraform.tfvars file.

So far, you should have a folder structure like this:

.
├── github-repo-module
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── main.tf
├── terraform.tfstate
├── terraform.tfvars
└── variables.tf

Execute the already familiar Terraform workflow:

$ terraform init
$ terraform plan
$ terraform apply

You should see your newly created repo in GitHub!

You can now reuse your infrastructure module with ease.

Using Modules from the Terraform Registry

Curious Devs Corner is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.

Keep reading with a 7-day free trial

Subscribe to Curious Devs Corner to keep reading this post and get 7 days of free access to the full post archives.

Already a paid subscriber? Sign in
© 2025 Kirshi Yin
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share