Skip to main content

Terraform Refactoring

Introduction

Refactoring is the process of restructuring existing code without changing its external behavior. In the context of Terraform, refactoring means improving your infrastructure as code without altering the resulting infrastructure. As your Terraform projects grow in size and complexity, refactoring becomes essential to maintain clean, efficient, and manageable code.

This guide will walk you through common Terraform refactoring techniques, explain when and why to apply them, and provide practical examples to help you implement these best practices in your own projects.

Why Refactor Terraform Code?

Refactoring your Terraform code offers several benefits:

  • Improved maintainability: Well-structured code is easier to understand and update
  • Enhanced reusability: Modular components can be reused across different projects
  • Reduced complexity: Breaking down complex configurations into simpler units
  • Better collaboration: Cleaner code facilitates teamwork and knowledge sharing
  • Easier testing: Modular code can be tested more effectively

Common Terraform Refactoring Techniques

1. Extract Modules

One of the most effective refactoring techniques is extracting reusable code into modules. Modules allow you to encapsulate related resources and configurations, making your code more modular and easier to maintain.

Before Refactoring:

hcl
# main.tf
provider "aws" {
region = "us-west-2"
}

resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"

tags = {
Name = "main-vpc"
Environment = "production"
}
}

resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"

tags = {
Name = "public-subnet"
Environment = "production"
}
}

resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"

tags = {
Name = "private-subnet"
Environment = "production"
}
}

After Refactoring:

hcl
# main.tf
provider "aws" {
region = "us-west-2"
}

module "vpc" {
source = "./modules/vpc"

cidr_block = "10.0.0.0/16"
environment = "production"
public_subnet_cidr = "10.0.1.0/24"
private_subnet_cidr = "10.0.2.0/24"
}
hcl
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr_block

tags = {
Name = "main-vpc"
Environment = var.environment
}
}

resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr

tags = {
Name = "public-subnet"
Environment = var.environment
}
}

resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidr

tags = {
Name = "private-subnet"
Environment = var.environment
}
}
hcl
# modules/vpc/variables.tf
variable "cidr_block" {
description = "CIDR block for the VPC"
type = string
}

variable "environment" {
description = "Environment name"
type = string
}

variable "public_subnet_cidr" {
description = "CIDR block for the public subnet"
type = string
}

variable "private_subnet_cidr" {
description = "CIDR block for the private subnet"
type = string
}
hcl
# modules/vpc/outputs.tf
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}

output "public_subnet_id" {
description = "ID of the public subnet"
value = aws_subnet.public.id
}

output "private_subnet_id" {
description = "ID of the private subnet"
value = aws_subnet.private.id
}

2. Standardize Variable and Output Definitions

Another important refactoring technique is standardizing your variable and output definitions to improve clarity and enforce validation.

Before Refactoring:

hcl
variable "instance_type" {}
variable "ami_id" {}

After Refactoring:

hcl
variable "instance_type" {
description = "EC2 instance type to deploy"
type = string
default = "t3.micro"

validation {
condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
error_message = "Allowed values for instance_type are t3.micro, t3.small, or t3.medium."
}
}

variable "ami_id" {
description = "AMI ID to use for the EC2 instance"
type = string

validation {
condition = can(regex("^ami-", var.ami_id))
error_message = "The ami_id value must be a valid AMI ID, starting with 'ami-'."
}
}

3. Use Locals for DRY Code

The DRY (Don't Repeat Yourself) principle can be applied to Terraform by using locals to store commonly used values or expressions.

Before Refactoring:

hcl
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"

tags = {
Name = "web-server"
Environment = "production"
Project = "example"
Owner = "team-devops"
}
}

resource "aws_security_group" "web_sg" {
name = "web-sg"
description = "Security group for web servers"

tags = {
Name = "web-sg"
Environment = "production"
Project = "example"
Owner = "team-devops"
}
}

After Refactoring:

hcl
locals {
common_tags = {
Environment = "production"
Project = "example"
Owner = "team-devops"
}
}

resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"

tags = merge(
local.common_tags,
{
Name = "web-server"
}
)
}

resource "aws_security_group" "web_sg" {
name = "web-sg"
description = "Security group for web servers"

tags = merge(
local.common_tags,
{
Name = "web-sg"
}
)
}

4. Improve File Organization

As your Terraform project grows, organizing your files becomes crucial for maintainability.

Before Refactoring (Single File):

hcl
# main.tf
provider "aws" {
region = "us-west-2"
}

# Network resources
resource "aws_vpc" "main" { /* ... */ }
resource "aws_subnet" "public" { /* ... */ }
resource "aws_subnet" "private" { /* ... */ }

# Compute resources
resource "aws_instance" "app" { /* ... */ }
resource "aws_instance" "db" { /* ... */ }

# Database resources
resource "aws_db_instance" "main" { /* ... */ }

# Variables
variable "vpc_cidr" { /* ... */ }
variable "environment" { /* ... */ }

# Outputs
output "vpc_id" { /* ... */ }
output "app_ip" { /* ... */ }

After Refactoring:

project/
├── main.tf # Provider configuration, module calls
├── variables.tf # Project-level variables
├── outputs.tf # Project-level outputs
├── locals.tf # Common expressions and values
├── network.tf # Network-related resources (VPC, subnets)
├── compute.tf # Compute resources (EC2 instances)
├── database.tf # Database resources
└── versions.tf # Terraform version constraints

5. Use for_each Instead of count

Refactoring from count to for_each can make your code more robust when the elements in your list might change.

Before Refactoring (using count):

hcl
variable "subnet_cidrs" {
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}

resource "aws_subnet" "example" {
count = length(var.subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_cidrs[count.index]

tags = {
Name = "subnet-${count.index}"
}
}

After Refactoring (using for_each):

hcl
variable "subnets" {
default = {
"public1" = "10.0.1.0/24"
"public2" = "10.0.2.0/24"
"private1" = "10.0.3.0/24"
}
}

resource "aws_subnet" "example" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value

tags = {
Name = each.key
}
}

Real-World Refactoring Example

Let's walk through a more complete refactoring example. We'll transform a monolithic Terraform configuration into a modular, maintainable structure.

Initial Configuration:

hcl
# main.tf
provider "aws" {
region = "us-east-1"
}

# VPC Setup
resource "aws_vpc" "app_vpc" {
cidr_block = "10.0.0.0/16"

tags = {
Name = "app-vpc"
Environment = "production"
Project = "ecommerce"
}
}

resource "aws_subnet" "app_subnet_1" {
vpc_id = aws_vpc.app_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"

tags = {
Name = "app-subnet-1"
Environment = "production"
Project = "ecommerce"
}
}

resource "aws_subnet" "app_subnet_2" {
vpc_id = aws_vpc.app_vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1b"

tags = {
Name = "app-subnet-2"
Environment = "production"
Project = "ecommerce"
}
}

# Web Server Setup
resource "aws_instance" "web_server_1" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
subnet_id = aws_subnet.app_subnet_1.id

tags = {
Name = "web-server-1"
Environment = "production"
Project = "ecommerce"
}
}

resource "aws_instance" "web_server_2" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
subnet_id = aws_subnet.app_subnet_2.id

tags = {
Name = "web-server-2"
Environment = "production"
Project = "ecommerce"
}
}

# Database Setup
resource "aws_db_instance" "app_db" {
allocated_storage = 20
storage_type = "gp2"
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t3.micro"
name = "appdb"
username = "admin"
password = "Password123!" # Not secure!
parameter_group_name = "default.mysql5.7"
skip_final_snapshot = true

tags = {
Name = "app-database"
Environment = "production"
Project = "ecommerce"
}
}

Refactored Configuration:

Let's refactor this code into a modular structure:

  1. First, let's create the project structure:
project/
├── main.tf
├── variables.tf
├── outputs.tf
├── locals.tf
├── modules/
│ ├── network/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── database/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
  1. Now let's implement each file:
hcl
# locals.tf
locals {
common_tags = {
Environment = var.environment
Project = var.project_name
}
}
hcl
# variables.tf
variable "region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}

variable "environment" {
description = "Environment name"
type = string
default = "production"
}

variable "project_name" {
description = "Project name to use in resource tagging"
type = string
default = "ecommerce"
}

variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}

variable "subnet_cidrs" {
description = "Map of subnet names to CIDR blocks"
type = map(string)
default = {
"app-subnet-1" = "10.0.1.0/24"
"app-subnet-2" = "10.0.2.0/24"
}
}

variable "db_credentials" {
description = "Database credentials"
type = object({
username = string
password = string
})
sensitive = true
default = {
username = "admin"
password = "changeme" # Should be supplied externally, not in code
}
}
hcl
# main.tf
provider "aws" {
region = var.region
}

module "network" {
source = "./modules/network"

vpc_cidr = var.vpc_cidr
subnet_cidrs = var.subnet_cidrs
common_tags = local.common_tags
}

module "compute" {
source = "./modules/compute"

subnet_ids = module.network.subnet_ids
common_tags = local


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)