Clean Chef code: Depend on public cookbook interfaces

Topics: Clean Chef code

Chef cookbooks use node attributes as parameters for system configuration. In software engineering terms however, they’re global variables. They’re implementation details of a cookbook.

In consequence, using another cookbook’s node attributes for any other purpose than defining setup parameters creates a dependency on implementation details that can change at any time.

Let’s take service discovery as an example:

service_nodes = search(:node, "webservice_id:myservice")

Apparently, the cookbook identifies web service nodes via a node attribute webservice_id. A common webservice_id value means nodes belong to the same web service.

The problem is that this service discovery logic might change. For example, we might end up adding another attribute webservice_status so we can reduce the search result set by adding AND webservice_status:active. Since this change in semantics is not necessarily a breaking change, there’s no simple way to signal it to everyone who depends on this unofficial interface.

How about we provide an official interface instead? The web service cookbook could for example provide a class that we can use for service discovery:

webservice = Cookbook::Webservice::Discovery.new("myservice")
service_nodes = webservice.nodes

This is easily implemented as a cookbook library. By using module namespacing, we make sure that method names don’t conflict. In my practice, I use the camel-cased cookbook name under the top-level namespace Cookbook.

With service discovery encapsulated and hidden behind a public interface, we could even rebuild service discovery on a completely different technology like Consul without breaking other people’s code.