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:
# 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:
# 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"
}
# 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
}
}
# 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
}
# 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:
variable "instance_type" {}
variable "ami_id" {}
After Refactoring:
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:
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:
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):
# 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):
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):
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:
# 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:
- 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
- Now let's implement each file:
# locals.tf
locals {
common_tags = {
Environment = var.environment
Project = var.project_name
}
}
# 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
}
}
# 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! :)