Docs / Automation & IaC / Build Reusable Terraform Modules for VPS Provisioning

Build Reusable Terraform Modules for VPS Provisioning

By Admin · Mar 15, 2026 · Updated Apr 24, 2026 · 354 views · 4 min read

Terraform modules let you package infrastructure patterns into reusable, versioned components. Instead of copying configuration between projects, you define a module once and instantiate it with different parameters. This guide covers building production-quality Terraform modules for VPS provisioning and common infrastructure patterns.

Module Structure

# Standard module directory layout
modules/
├── vps-server/
│   ├── main.tf           # Core resources
│   ├── variables.tf      # Input variables
│   ├── outputs.tf        # Output values
│   ├── versions.tf       # Provider requirements
│   ├── data.tf           # Data sources
│   ├── locals.tf         # Local values
│   └── README.md         # Documentation
├── load-balancer/
│   ├── main.tf
│   ├── variables.tf
│   └── outputs.tf
└── monitoring/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf

VPS Server Module

# modules/vps-server/variables.tf
variable "name" {
  description = "Server hostname"
  type        = string
  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{1,62}$", var.name))
    error_message = "Name must be lowercase alphanumeric with hyphens, 2-63 chars."
  }
}

variable "plan" {
  description = "Server plan (size)"
  type        = string
  default     = "cx22"
  validation {
    condition     = contains(["cx22", "cx32", "cx42", "cx52"], var.plan)
    error_message = "Must be a valid plan: cx22, cx32, cx42, cx52."
  }
}

variable "image" {
  description = "OS image"
  type        = string
  default     = "ubuntu-24.04"
}

variable "location" {
  description = "Datacenter location"
  type        = string
  default     = "nbg1"
}

variable "ssh_keys" {
  description = "List of SSH key IDs"
  type        = list(string)
}

variable "firewall_rules" {
  description = "Firewall rules"
  type = list(object({
    direction   = string
    protocol    = string
    port        = string
    source_ips  = list(string)
  }))
  default = [
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "22"
      source_ips = ["0.0.0.0/0", "::/0"]
    },
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "80"
      source_ips = ["0.0.0.0/0", "::/0"]
    },
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "443"
      source_ips = ["0.0.0.0/0", "::/0"]
    }
  ]
}

variable "backups" {
  description = "Enable automated backups"
  type        = bool
  default     = true
}

variable "labels" {
  description = "Labels to apply"
  type        = map(string)
  default     = {}
}

variable "user_data" {
  description = "Cloud-init user data"
  type        = string
  default     = ""
}
# modules/vps-server/main.tf
terraform {
  required_version = ">= 1.7.0"
}

locals {
  default_labels = {
    managed_by = "terraform"
    module     = "vps-server"
  }
  labels = merge(local.default_labels, var.labels)
}

resource "hcloud_server" "this" {
  name        = var.name
  server_type = var.plan
  image       = var.image
  location    = var.location
  ssh_keys    = var.ssh_keys
  backups     = var.backups
  labels      = local.labels
  user_data   = var.user_data

  public_net {
    ipv4_enabled = true
    ipv6_enabled = true
  }

  lifecycle {
    ignore_changes = [user_data, ssh_keys]
  }
}

resource "hcloud_firewall" "this" {
  name   = "${var.name}-fw"
  labels = local.labels

  dynamic "rule" {
    for_each = var.firewall_rules
    content {
      direction  = rule.value.direction
      protocol   = rule.value.protocol
      port       = rule.value.port
      source_ips = rule.value.source_ips
    }
  }
}

resource "hcloud_firewall_attachment" "this" {
  firewall_id = hcloud_firewall.this.id
  server_ids  = [hcloud_server.this.id]
}

resource "hcloud_rdns" "ipv4" {
  server_id  = hcloud_server.this.id
  ip_address = hcloud_server.this.ipv4_address
  dns_ptr    = "${var.name}.example.com"
}

# modules/vps-server/outputs.tf
output "id" {
  description = "Server ID"
  value       = hcloud_server.this.id
}

output "ipv4_address" {
  description = "Public IPv4 address"
  value       = hcloud_server.this.ipv4_address
}

output "ipv6_address" {
  description = "Public IPv6 address"
  value       = hcloud_server.this.ipv6_address
}

output "status" {
  description = "Server status"
  value       = hcloud_server.this.status
}

Using Modules

# environments/production/main.tf
module "web_server_1" {
  source = "../../modules/vps-server"

  name     = "web-prod-1"
  plan     = "cx32"
  image    = "ubuntu-24.04"
  location = "nbg1"
  ssh_keys = [hcloud_ssh_key.deploy.id]
  backups  = true

  labels = {
    environment = "production"
    role        = "webserver"
  }

  firewall_rules = [
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "22"
      source_ips = ["203.0.113.0/24"]  # Office IP only
    },
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "80"
      source_ips = ["0.0.0.0/0", "::/0"]
    },
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "443"
      source_ips = ["0.0.0.0/0", "::/0"]
    }
  ]
}

module "db_server" {
  source = "../../modules/vps-server"

  name     = "db-prod-1"
  plan     = "cx42"
  image    = "ubuntu-24.04"
  location = "nbg1"
  ssh_keys = [hcloud_ssh_key.deploy.id]

  labels = {
    environment = "production"
    role        = "database"
  }

  firewall_rules = [
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "22"
      source_ips = ["203.0.113.0/24"]
    },
    {
      direction  = "in"
      protocol   = "tcp"
      port       = "5432"
      source_ips = ["${module.web_server_1.ipv4_address}/32"]
    }
  ]
}

# Use outputs
output "web_server_ip" {
  value = module.web_server_1.ipv4_address
}

output "db_server_ip" {
  value = module.db_server.ipv4_address
}

Module Versioning

# Use versioned modules from a Git repository
module "web_server" {
  source = "git::https://github.com/myorg/terraform-modules.git//vps-server?ref=v1.2.0"
  # ...
}

# Or from a Terraform registry
module "web_server" {
  source  = "myorg/vps-server/hcloud"
  version = "~> 1.2"
  # ...
}

# Or from a local path (for monorepo development)
module "web_server" {
  source = "../../modules/vps-server"
  # ...
}

Testing Modules

# Use terraform test (built-in since Terraform 1.6)
# tests/vps_server.tftest.hcl
run "create_server" {
  command = plan

  variables {
    name     = "test-server"
    plan     = "cx22"
    ssh_keys = ["12345"]
  }

  assert {
    condition     = hcloud_server.this.name == "test-server"
    error_message = "Server name mismatch"
  }

  assert {
    condition     = hcloud_server.this.server_type == "cx22"
    error_message = "Server type mismatch"
  }

  assert {
    condition     = hcloud_server.this.backups == true
    error_message = "Backups should be enabled by default"
  }
}

# Run tests
terraform test

Best Practices

  • Use semantic versioning for module releases and pin versions in consumers
  • Add input validation to catch errors early before resource creation
  • Document all variables and outputs — use terraform-docs to auto-generate docs
  • Keep modules focused: One module = one logical infrastructure component
  • Use lifecycle blocks to prevent accidental destruction of critical resources
  • Write tests using terraform test to validate module behavior

Was this article helpful?