Prefer Module Composition Over Inheritance

IaC code can either use inheritance or composition to add functionality. Which is the better path?

Hey folks,

Here’s an important best practice in IaC: prefer composition over inheritance for your IaC modules.

What does that mean?

It means that you shouldn't have a child module that calls a child module that calls a child module that calls a child module... turtles all the way down. Inheriting behavior from a module by depending on it is what we mean by inheritance. At Masterpoint, we've seen this cause pain.

For example, if you need additional functionality like adding auto-scaling to an underlying workload, you need to choose one of these two approaches:

  • Inheritance: Create a module that references the original workload module using the source attribute and add auto-scaling resources and configuration.

  • Composition: Create a module for the auto-scaling use-case, including any auto-scaling resources and configuration and add it to your root module alongside the original workload module.

The latter approach allows for more flexibility. In the future, if someone doesn't need to have that workload auto-scale, then they can use the original module without the overhead of understanding all the ins and outs of the auto-scaling functionality or the cost of running it. If the workload module needs modification, using composition means that change has less chance of impacting the auto-scaling module.

I love how good practices in IaC often come back to good software practices. "Composition over inheritance" is a classic of Object Oriented Programming (OOP) that anyone who knows Java or similar languages has learned.

When To Choose Composition

There isn’t a golden rule for when to use inheritance vs composition. I’m not a purist.

If you're building a light child module layer on top of an existing child module, inheritance can make sense. When you're building a full set of functionality on top of it (like the autoscaling example above), that is when they should be separate and interact with one another explicitly.

For example, consider our terraform-aws-tailscale child module, which inherits our terraform-aws-ssm-agent module. We believe we're adding enough functionality on top of the base module where inheritance makes sense.

A contrasting example where composition makes sense is managing an AWS VPC and subnets. Sure, you could create a module that manages both the VPC AND the subnets. The VPC could be the base layer child module and then the subnet child modules add the ability to manage subnets on top of that VPC. This is no good though, as it overly entangles the modules. Instead, having separate child modules that you compose is best.

A potentially better example is VPC peering. I would use something like Cloud Posse’s terraform-aws-vpc-peering child module alongside your VPC module, instead of baking that functionality into your VPC module itself.

Small, opinionated and service oriented child modules are the way to go IMO. If you need 50 or even 200 of them, that's not the fault of TF or HCL. That's a representation that your infrastructure is complex and you need to compose many building blocks to construct that infrastructure.

What do you think? Have you been bitten by deep layers of IaC inheritance before?

May you keep your modules small and opinionated,

Matt @ Masterpoint

PS Interested in being on a devops-focused podcast? I love to intro folks, so reply to me with a topic you’d like to chat about on a podcast. Or, if you want to argue with me about module inheritance or discuss how your platform team can use composition best, grab some time on my calendar here.