Skip to content
Notifications
Clear all

Comparison: Terraform module structure vs Pulumi Component Resources. Which is cleaner?

1 Posts
1 Users
0 Reactions
33 Views
(@kubernetes_knight)
Estimable Member
Joined: 7 months ago
Posts: 68
Topic starter   [#9144]

Hey folks! I've been neck-deep in infrastructure code for the past few months, orchestrating a pretty complex migration from a pure Terraform codebase to a Pulumi one for our multi-cluster Kubernetes platform. The core architectural debate that consumed our team was around **how to achieve reusable, composable abstractions.** In Terraform, that's the module system. In Pulumi, it's Component Resources (and Classes in the OOP languages).

I'd love to share my experience and see if others have walked this path. My initial take: **Pulumi's model feels more expressive and less "stringly-typed," but Terraform's module system is more immediately familiar and has a vast ecosystem.**

Let's compare. In Terraform, you create a module by defining input variables, output values, and resources in a directory. Consumption looks like this HCL:

```hcl
module "istio_ingress_gateway" {
source = "../../modules/gcp-gke-istio-gateway"

project_id = var.project_id
cluster_name = module.gke.cluster_name
cluster_location = module.gke.location
namespace = "istio-ingress"
node_pool_min_size = 3
node_pool_max_size = 10

custom_helm_values = {
autoscaling = {
enabled = true
targetCPUUtilizationPercentage = 70
}
}
}
```
You're passing maps and strings. Validation happens via `variable` blocks, which is solid, but you're essentially working with structured data. The module's interface is its `variables.tf` and `outputs.tf` files.

Now, in Pulumi (using TypeScript), you'd typically create a `ComponentResource` subclass. The same abstraction might look like this when defined:

```typescript
export class GcpGkeIstioGateway extends pulumi.ComponentResource {
public readonly gatewayServiceIp: pulumi.Output;
public readonly nodePoolId: pulumi.Output;

constructor(name: string, args: GcpGkeIstioGatewayArgs, opts?: pulumi.ComponentResourceOptions) {
super("platform:components:GcpGkeIstioGateway", name, args, opts);

// Build node pool
const nodePool = new gcp.container.NodePool(`${name}-np`, {
...args.nodePoolConfig,
cluster: args.clusterName,
location: args.clusterLocation,
}, { parent: this });

// Install Istio Helm chart
const istioChart = new k8s.helm.v3.Chart(`${name}-chart`, {
chart: "gateway",
repo: "istio",
namespace: args.namespace,
values: {
autoscaling: {
enabled: args.autoscaling?.enabled ?? true,
targetCPUUtilizationPercentage: args.autoscaling?.targetCPU ?? 70,
},
},
}, { parent: this });

this.gatewayServiceIp = istioChart.getResourceProperty("v1/Service", `${name}-istio-gateway`, "status").apply(s => s.loadBalancer.ingress[0].ip);
this.nodePoolId = nodePool.id;

this.registerOutputs({
gatewayServiceIp: this.gatewayServiceIp,
nodePoolId: this.nodePoolId,
});
}
}

export interface GcpGkeIstioGatewayArgs {
clusterName: pulumi.Input;
clusterLocation: pulumi.Input;
namespace?: pulumi.Input;
nodePoolConfig?: gcp.container.NodePoolArgs;
autoscaling?: {
enabled?: boolean;
targetCPU?: number;
};
}
```
And consumption becomes:

```typescript
const gateway = new GcpGkeIstioGateway("main-ingress", {
clusterName: gkeCluster.name,
clusterLocation: gkeCluster.location,
namespace: "istio-ingress",
nodePoolConfig: { nodeCount: 3, ... },
autoscaling: {
enabled: true,
targetCPU: 70
}
});

export const ingressIp = gateway.gatewayServiceIp;
```

**Observations & Pain Points:**

* **Type Safety:** Pulumi's model, especially in TypeScript/Go/Python, gives you compile-time type checking for your component's inputs and outputs. No more `terraform plan` to discover a missing required variable or a typo in an output attribute name. This was a huge win for our team's velocity and safety.
* **State Abstraction:** Both models cleanly encapsulate their internal resources. A `pulumi destroy` on the component resource destroys all child resources, just like destroying a Terraform module.
* **Complex Logic:** Inside a Pulumi Component, you can use full programming language features (loops, conditionals, utility libraries) to construct your resources. In Terraform, you're limited to HCL functions and `dynamic` blocks, which can get... convoluted for advanced scenarios.
* **Ecosystem & Familiarity:** Terraform modules are ubiquitous. Every major provider has examples. Pulumi's pattern is powerful but requires a mental shift. Also, reusing existing Terraform modules from within Pulumi (via the `tf2pulumi` bridge or `terraform` provider) is possible but adds complexity.

So, was the migration worth it? For us, **yes**, primarily because our infrastructure logic had grown complex enough that we needed the expressiveness of a real programming language. The type safety and ability to write unit tests against our component logic were the deciding factors.

But I'm curious:
* Has anyone else designed equivalent abstractions in both systems? Which felt more maintainable after 6 months?
* For those who use Pulumi, do you find yourself leaning heavily on Component Resources, or do you prefer a more functional, composition-based approach?
* How do you handle **versioning** and **dependency management** for Pulumi Components compared to Terraform modules (which often just use Git tags or submodules)?


YAML is not a programming language, but I treat it like one.


   
Quote