Skip to main content

How to write and rightsize Terraform modules

There are four key areas to consider when deciding on best practices for designing Terraform modules: scope, code strategy, security, and testing.

When designing Terraform modules, you have to balance a lot of attributes: abstraction, flexibility, and maintainability, etc. Rene Schach, a Senior Cloud Consultant at shiftavenue, recently spoke at HashiDays 2025 to share what he thinks about the “rightsizing” aspect of Terraform module design.

Based on his years of experience building infrastructure platforms with Google Cloud, Kubernetes, and Terraform, he sees module rightsizing as more than just choosing the right amount of resources. He divides the topic into four main pillars:

  • Scope
  • Code strategy
  • Security
  • Testing

This post shares his practical process, lessons, and opinions for real-world teams building Terraform modules.

»Terraform module scope

Every Terraform module should serve a specific purpose and audience. Before writing code, take the time to understand who will use your module and what problem it is meant to solve. (This is why it’s important for your platform team to treat your platform like a product.)

»Talk to your target users

  • Identify module users: development teams, platform engineers, and/or security specialists.

  • Current workflows and challenges: Schedule conversations with them to understand how they currently provision infrastructure and what challenges they face.

  • Input control requirements: Clarify which inputs they need control over and if any should remain fixed for consistency or compliance reasons.

  • Build your own best practices: Your modules should represent real company use cases and abstractions, not theoretical best practices pulled from the internet.

»Keep modules cohesive and loosely coupled

  • Follow clear functional separation: Each module should perform one function well — such as networking, compute, or IAM.

    • For example, a kubernetes_cluster module should only handle cluster creation and configuration.
    • Networking infrastructure (VPCs, subnets, and routes) belongs in its own module.
  • Avoid tight coupling: If a change in one module unexpectedly alters the state of many others during a Terraform plan, that’s a sign they are too tightly coupled. Design boundaries so that modules can evolve independently without creating unintended dependencies.

  • Example: The AWS VPC module is a well-known example of an appropriately scoped, network-focused module.

»Group related resources together, split by volatility

  • Combine resources that always belong together: An example would be a compute instance and its attached disk or service account. Avoid spreading logically connected components across different modules.

  • Split by resource lifespan: Separate short-lived infrastructure (development or ephemeral environments) from long-lived infrastructure (core network or production resources). This prevents frequent changes to temporary components from triggering updates in stable foundational modules.

“Rightsizing modules is more about talking than coding — get to know your users first.”
— Rene Schach, Senior Cloud Consultant, shiftavenue

»Terraform module code considerations

Terraform modules are software artifacts. They should be versioned, tested, and structured like any other codebase. Proper structure also improves maintainability and readability for new contributors.

»Separate module resources

  • Rene’s opinion — split Terraform resources into multiple files based on their purpose rather than keeping everything in one file.

Example Google Cloud Compute Engine (GCE) module structure:

> .github
> examples
account.tf
locals.tf
outputs.tf
README.md
versions.tf
variables.tf
vm.tf
  • In Rene’s experience, most companies require virtual machines to run with their own service account or service user. So in this example above:

    • account.tf defines a custom service account with the appropriate permissions (The default GCE service account has overly broad permissions).

    • vm.tf defines the virtual machine resource itself.

  • This separation helps teams quickly identify where specific logic lives and makes it easier to extend modules without confusion.

»Provide examples and documentation

  • Always include an examples/ directory in your module repository.

  • Write small, self-contained Terraform configurations that demonstrate realistic use cases.

  • Add a short README explaining what each example does and when to use it.

  • Examples: Show users how to create a single VM with a private IP and another VM with a public IP.

»Inputs: Follow the provider’s schema

  • Rene’s opinion — define input variables that align with the underlying Terraform provider resources. This keeps modules aligned with Terraform’s native behavior.

The example variable definition below mirrors the structure of the google_compute_instance resource from the Terraform provider for Google Cloud, making the input predictable and consistent:

variable "instance_name" {
  description = "The VM name"
  type        = string
}
 
variable "location" {
  description = "The VM location"
  default     = "europe-west3-a"
}
 
variable "boot_disk_specs" {
  description = "Boot disk attributes"
  type = object({
    image = string # Operating system
    size  = optional(string, "100")
    type  = optional(string, "pd-ssd")
  })
}
  • Rather than splitting the attributes into separate strings, they are defined as an object because they are defined as an object in the google_compute_instance resource.
  • Avoid diverging from Terraform provider schema unless it significantly improves clarity for module consumers.

»Locals: Keep them centralized

  • Rene’s opinion — Put all local variables in on locals.tf file.

  • Having all locals in one place simplifies navigation when debugging or modifying modules.

»Outputs: Return complete objects

  • Rene’s opinion — Output the full resource object (e.g. the VM instance object) rather than creating an output for every attribute individually.

For example:

output "vm" {
  value = google_compute_instance.vm
}
 
output "service_account" {
  value = google_service_account.vm_sa
}
  • While this means that users will have to navigate object attributes themselves, this also gives users the flexibility to access whatever attributes they need without having to request new outputs later.
  • Provide examples and documentation rather than creating a new module version for every small output addition request.

»Semantically version your modules

  • Version your Terraform modules the same way you would version software: using semantic versioning (MAJOR.MINOR.PATCH).

  • Rene’s recommended versioning rules

    • Add a new required input → increment major version.
      • Users must change their code to upgrade safely.
    • Add a new optional input → increment minor version.
      • Existing users can upgrade without any modification.
    • Add a new output → increment minor version.
      • This does not break compatibility.
    • Remove an output → increment major version.
      • Removing outputs can break user configurations.
    • Add or remove a resource → determine version impact based on related inputs/outputs.
    • Upgrade a provider version → increment patch version if no behavior changes, or minor/major if input or output schemas are affected.
  • These guidelines help teams upgrade modules predictably and safely without introducing unexpected breaks.

»Terraform module security considerations

Modules define the guardrails for how your infrastructure is provisioned. Validating inputs early and limiting configurability help prevent insecure or inconsistent deployments.

»Use Terraform validation blocks

Terraform allows inline input validation directly in variable definitions. Example:

variable "location" {
  description = "The VM location"
  type        = string
 
  validation {
    condition     = startswith(var.location, "europe-west3")
    error_message = "Only europe-west3 (Frankfurt) locations are allowed."
  }
}
  • This example validation ensures users cannot deploy resources outside approved regions.

»Validate configurations early

  • Fail fast during the terraform plan phase instead of waiting for the apply phase to trigger a policy violation.

  • Manual reviews and organizational policy enforcement can also block invalid configurations, but they typically apply later, after state changes begin — making remediation more disruptive.

»Apply policy frameworks when needed

  • Use Sentinel (HashiCorp) or Open Policy Agent (OPA) for higher-level enforcement.

  • These tools can define reusable, centralized compliance rules.

»Expose only what’s necessary

  • Avoid giving users full control over every attribute of a resource.

  • If every VM must be private, don’t even expose a flag to make it public.

  • Keeping configurability narrow reduces misuse and enforces consistency.

»Terraform module testing

Testing Terraform modules used to be cumbersome, but the modern Terraform testing framework makes it practical and integrated.

»Use the official Terraform test framework

  • The terraform test command allows you to write and execute tests using standard Terraform configuration syntax.

  • Rene’s opinion — There is no need for additional tools or programming languages like Go (Terratest) or Ruby (Kitchen-Terraform).

»Integrate testing into CI/CD

  • Run tests automatically on pull requests or before releasing a new version of the module.

  • Failing tests should block merges until corrected, just like any application code.

»Focus tests on real use cases

  • Don’t try to test every single input combination.

  • Instead, create test cases that reflect actual usage patterns and organizational requirements.

»Test your examples

  • If you maintain examples for your module (and you should), reuse them as test cases.

  • This ensures that your documentation remains valid and that examples never go stale.

»Collaborating on Terraform module design

Terraform modules become more successful when treated as shared, evolving assets rather than hidden internal tools.

»Make modules visible to your teams

  • Store them in a shared version control system. Consider a private module registry.

  • Avoid restricting access; transparency helps developers understand what modules do and how they work.

  • Developers are more likely to adopt and contribute to modules they can see and trust.

  • Encourage feedback through pull requests and issue tracking.

»Use architectural decision records (ADRs)

  • Document key design decisions that shape your module structure and conventions.

  • For example, explain why the team chose to enforce private IPs or restrict certain inputs.

  • Ensure your module implementation aligns with these ADRs for long-term consistency.

»Know when not to build a module

  • If your module is only building a wrapper around a single Terraform resource, Rene suggests it shouldn’t be a module.

  • If your module simply exposes every input of a resource one-to-one, it might not make sense as a custom module. In these cases, consider using well-maintained public modules instead.

»Treat module development as a continuous process

  • Start with a minimal viable module that solves a concrete use case.

  • As new requirements arise, extend it through small, incremental changes and pull requests.

  • Apply standard software engineering practices: semantic versioning, code review, changelog maintenance, and CI/CD integration.

»Takeaways

Some of the recommendations from Rene’s talk are somewhat opinionated, but generally these takeaways are widely accepted best practices:

  • Understand your users and design modules around their actual requirements.

  • Keep modules cohesive, loosely coupled, and scoped to a clear responsibility.

  • For inputs, diverging from the Terraform provider schema can cause issues. Consider following the provider’s schema unless breaking from it will allow you to significantly improve clarity for module consumers.

  • Version modules semantically to ensure safe, clear upgrades.

  • Validate inputs early and limit unnecessary configurability.

  • Test modules using Terraform’s native testing framework and integrate those tests into CI/CD.

  • Promote visibility and collaboration through open repositories and documented decisions.

  • Treat Terraform modules as evolving software products, not static templates.

If your organization is looking to optimize or scale its Terraform usage, read our guide: Optimize cloud operations and ROI with The Infrastructure Cloud to learn how you could reclaim up to 40% of your cloud budget with smarter operations.

You can watch the complete talk “Rightsizing your Terraform modules” here:

More posts like this