Stop Scripting, Start Architecting: The OOP Approach to Terraform
Source: Dev.to
TL;DR
The Problem: Terraform codebases often suffer from sprawl—copy‑pasted resources, tight coupling, and leaky abstractions that make scaling painful.
The Solution: Treat Terraform modules as classes and module instances as objects.
Key Mapping
| OOP Concept | Terraform Equivalent |
|---|---|
| Class | Child module (e.g., ./modules/web_server/) |
| Object | module block (instantiation) |
| Interface | variables.tf (inputs) and outputs.tf (getters) |
| Private State | locals and internal resources |
| Best Practice | Prefer composition (building modules from other modules) over inheritance. Use dependency injection by passing resource IDs (e.g., vpc_id) rather than looking them up internally with data sources. |
An object‑oriented approach transforms messy, repetitive HCL into a scalable infrastructure architecture. By mapping OOP principles—encapsulation, abstraction, composition, and polymorphism—to Terraform modules, we can build infrastructure that is as maintainable and testable as application code.
The Problem: The Monolithic Terraform File
In the early days of a project, a single main.tf file is convenient. As infrastructure grows, this “scripting” mindset leads to fragility: hard‑coded values repeated across environments, security groups defined inline with instances, and a complete lack of reusability.
When Terraform is treated purely as a configuration script, we miss the structural benefits of software‑engineering design patterns. We need to shift from writing scripts to architecting objects.
The Core Analogy: Modules as Classes
| OOP Concept | Terraform Implementation |
|---|---|
| Class Definition | ./modules/web_server/ – the blueprint that defines how to build something, not what to build. |
| Constructor | variables.tf – defines the required inputs to instantiate the class. |
| Public Methods / Properties | outputs.tf – defines the data explicitly exposed to the caller. |
| Private Members | locals, resource – internal logic and state hidden from the parent scope. |
| Object Instance | module "web_prod" { … } – a specific realization of the blueprint. |
Visualization: The Module Interface
classDiagram
class Module {
>
+variables.tf
+outputs.tf
-locals
-resources
}
class ChildModule {
+module "instance" {}
}
Module B[networking]
A --> C[compute]
B --> D[VPC & Subnets]
C --> E[EC2 Instances]
Code Example
# /modules/app_stack/main.tf
module "networking" {
source = "../networking"
cidr = var.cidr
}
module "compute" {
source = "../compute"
subnet_id = module.networking.private_subnet_id # Wiring components together
vpc_id = module.networking.vpc_id
}
The app_stack module acts as a façade, coordinating the interaction between networking and compute layers while keeping each sub‑module focused on a single responsibility.
4. Abstraction & Reuse: The “Interface” Behavior
(Content truncated in the source; the principle continues the pattern of defining clear input/output contracts via variables.tf and outputs.tf, enabling reusable, interchangeable modules.)