Have you ever looked at an upstream DSL module and thought: “this is exactly what I need!”? Maybe if you’re using multiple distros, multiple releases of those distros and/or multiple operating systems you may say this occasionally. Maybe you also say this if you have a single ops group that handles all of your infrastructure.
I’ve rarely been happy with upstream modules, even in shops with a single ops group. They are more complex than I want and are always missing something I need. Why is this?
- They need to be abstract enough to support multiple distros, multiple releases of those distros and often multiple operating systems.
- Due to the above support they usually need to support multiple versions of the application being wrapped.
- They need to support every configuration option available and since they support multiple versions of the application they also need to support options that may or may not be valid.
- Thanks to all of the above, they need to be configured by variable data (hiera/pillars/variables). Turning the variable data into another mini DSL. The mini-DSL is usually poorly constructed and completely undocumented.
- Their code is riddled with conditionals and numerous ugly templates.
Let’s consider Apache, the eternal poster child for over complication and underwhelming feature support in DSL modules. I’ve never seen a usable upstream Apache DSL module. Here’s a common set of tasks that are needed to configure Apache:
- Install Apache from a package.
- Enable some modules.
- Add some configuration to a vhost.
- Enable the vhost.
- Disable the default vhost.
- Restart (and manage) the Apache service.
It’s simple and doing just this with an abstract module is pretty easy, assuming it just takes a template for the vhost. The abstract module starts getting difficult when you start considering how configurable Apache is and how different Apache configs look between applications. The module is going to get complex really quickly.
Now consider you have 10 teams working on 10 different services (which is a low estimate, assuming SOA or microservices), all of which use this module. If one of those teams needs to modify the module they are also affecting every other team. Changes will need to be coordinated across teams on every change. If the module is upstream you also need to consider every other user of the module or may need to fork it. You may even need to fork it internally.
Why bother with the abstraction? It’ll be simpler, quicker, and will require less coordination between teams to let each team manage their own basic Apache config on their own. When the config needs to get more complex for one team it doesn’t introduce complexity for other teams. It also means that it’s possible to go with the simplest possible implementation. You may have ten copies of roughly the same code, but it’ll be ten simple and maintainable copies versus one complex, difficult to change copy.
The heart of this is that many DSL implementations aren’t well equipped for reuse. If you really want to write something reusable, find the complexities of your DSL implementation, and implement them as features of your configuration management system. It’s a lot easier to write reusable abstract code in a real language.
The title of this post says that reuse isn’t always the right approach, but I don’t want to imply it’s never the right approach. It’s surely possible to write clean, abstract, reusable DSL code. At some point when there’s a bunch of copies of the same code you’ll notice patterns and that’s a good time to start looking at abstraction. For simple modules that don’t have a lot of configuration it’s likely that it’ll even be really useful upstream. Salt Formulas, Puppet Forge, and Ansible Galaxy all have good examples of reusable modules and you should obviously consider them, but you should also consider if you can maintain your own much simpler version instead.