Govern

Nearly eight years ago, I complained about the lack of an adequate configuration management tool. So, I wrote one.

Eight years ago, in my Agent vs. Agentless post I wrote about how I wanted something new and/or different for a configuration management tool. And after eight years, I had an epiphany. Truth be told, I had the epiphany over a year ago, I am only writing about it now.

In the “Agent vs. Agentless” post, I already had a name picked out: govern. It’s a verb. It is written in Go. It starts with “g-o”. nice.gif

Without further delay, I would like to announce Govern.

This post is mostly about how Govern came to be. If you do not care about that, and just want to read the technical documentation, go here.

What is Govern?

Most configuration management tools provides swathes of built-in plugins that will allow the user to manage packages, manage the contents and properties of files, and do so much more.

Govern, on the other hand, keeps things as simple as possible. Govern is a configuration management tool that tries to do as little as possible and relies on external executables to accomplish the desired tasks.

Bootstrapping

When I initially started trying to write Govern, I ran into an issue where I needed it to actually do something. Sure, I could just start slogging away at writing individual actions like “install a package”, “make sure a service is running”, “template this file”, etc. I quickly lost all motivation, because at that point, I was repeating work that has already been done by dozens (if not hundreds) of people who have contributed to Ansible, SaltStack, Chef, and Puppet.

It stopped being fun. I had not done anything differently. I was left with something that was completely useless on its own.

And after some years of stewing on it, I had the epiphany that lead me to the initial implementation.

Epiphany

I don’t need to implement all of the actions myself. I don’t even need to provide them. All I needed was a way for people to be able to write them and their own.

To provide a brief detour of insight into how I happened upon this realization: at work, we were doing some work with Ansible. My coworker had just written a really, truly, very basic action that rendered a set of templated configuration file based on some predetermined paths. The argument could be made that it could have been done differently, but there were two things that really stuck out to me:

  1. My coworker’s solution, of implementing the task as an action, was elegant as hell. With one task stanza in a playbook, I could generate the complete directory hierarchy, and render out all of the files that needed to be there.
  2. The code was so ugly, its mother would have fed it with a slingshot.

My coworker did a really good job of only writing enough code to accomplish the task. It wasn’t a matter of programming taste. The code was fine, and it did the job in as few lines as required. What stuck out to me was how much boilerplate there was, and how impenetrable and opaque the boilerplate was. What really bothered me was how befuddling the code was. Most of the file was nothing but boilerplate. The code that was actually performing the intended function was maybe 10% of what was written.

That irritation, at how obtuse and cludgy the plugin system was, got me thinking about how this could be made easier.

Tenets

There are several established patterns for configuration management tools:

  • They need to be able to probe for details about the underlying system. Ansible calls these “facts”. SaltStack calls them “grains” (or “pillars”, depending on whether they are details that live on the host, or details that come from the Salt master node). Chef has ohai. Puppet has facter.
  • They need to have a templating language. This particular feature is arguable, and arguably, it should be pluggable, but meh.
  • They need to be declarative. Imperative configuration management is hardly acceptable if you do this kind of work professionally (maybe unless you’re a Windows administrator), and you quickly realize how big of a pain in the ass it is when you have to do it at really large scales.

NOTE: Yes, I know, Windows folks now have SCCM, or MECM as it is now known; and yes, I know Ansible and folks have somewhat supported Windows for a while. It does not change the fact that most Windows administrators I have met, typically did not want to shoehorn a UNIX-style configuration management tool into their GUI-driven lives. It is not a dig at them. It is an indication that “the UNIX way” is not universally accepted as the better approach. Windows distinguished itself by being “not UNIX-like”. It doesn’t mean I think they are right or wrong.

Solution

Instead of writing all of the plugins, or “things that do things” for Govern, I realized all it had to do was be able to execute something. This little realization opened up the world of possibilities for me.

As a brief aside, I do not manage Windows systems. I never have, and $DEITY willing, I never will have to. All of my work, for as long as I have been doing it, targets Linux systems. Once in a while, a snazzy little BSD box shows up, but for the most part, it is Linux day-in, day-out.

Facts

When Govern needs to check for facts, sure, it can probe the underlying system, but how scalable would it be if I needed to write all of those facts into the underlying system from the get-go? The answer is: not very. So, to jump to the conclusion, I thought, “all I have to do is walk a directory tree, and look for files with the executable bit set on their modes.” With that, I wrote some really simple code that crawled one or more directories, looking for executable files. In keeping with “the UNIX way”, each of these executables writes their output to STDOUT.

When you look at system facts gathered by other tools, there is some scrapping about with the names and hierarchies of those facts. In Govern’s case, the fact’s “name” is its position in the directory hierarchy.

For example, say our “facts” directory (holding our executables) is at /usr/share/govern/facts. Within that directory, we can create other directories holding executable files. name is a pretty generic fact name, and it does not tell us anything about what name that fact is providing. But, if we put it under the os directory, its name becomes os/name. This makes it pretty clear that this fact is going to return us the name of the operating system. Now, we are getting somewhere.

Runners

I followed the same approach for “runners”.

Runners, in Govern’s taxonomy, are the actions plugins, or the “things that do things”. If you are using SaltStack, you might invoke the pkg execution module to install a package. The Govern equivalent, is the pkg runner.

Runners are discovered the exact same way: when Govern starts, it looks at all of the runner directories it was configured with, and walks each of those directories looking for executable files.

Unlike facts, which are always expected to return output, runners only need to return output when something goes wrong. In this case, the error is expected to be written to STDERR. Information can be written to STDOUT, but this is expected to be purely diagnostic information in successful cases, and is ignored by Govern unless you explicitly ask for it.

The one different between the discovery of facts and runners, is that runner names are not hierarchical. Govern will start probing for executables in a given base directory, but once all executables under that directory have been found, the hierarchy is flattened, and all runners exist within the same “namespace”.

Conflicts

As I mentioned, you can configure Govern with multiple facts and runner directories. What happens if there are multiple facts or runners with the same name? In keeping with simplicity and prior art, I followed the example of the UNIX $PATH variable: if multiple executables exist with all searched directories, the last one wins.

For example, if you have two facts directories defined in this order:

  • /usr/share/govern/facts
  • /usr/local/share/govern/facts

and both of them contain an os/name fact, the last one will win. In this case, it means /usr/local/share/govern/facts/os/name will be the fact that is executed.

Templating

Again, the necessity of a templating engine is arguable. It is nice to have one, especially if it is documented. Ansible and SaltStack both use Jinja2, and it is really nice.

Fun fact: Python used to be my primary programming language. During the whole Python 2 → 3 changeover, I lost my patience with the whole PyPI ecosystem as some packages quickly updated to Python 3, and some dragged their heels. One day, I experienced my last UnicodeDecodeError, and said enough is enough. I heard about this nifty little language called “Go”, written by some UNIX juggernauts, and from that point on, Go has been my primary programming language.

Go has a built-in templating language: text/template. It has every feature you would want from Jinja2, even if it is juuuuust ever-so-slightly different in use. It is a perfectly functional substitute, in my opinion.

With that, Govern had a templating language.

Core

As I mentioned earlier, my one, big irritation with the existing systems was how opaque it was to extend it. You had to read its documentation, and understand exactly how to initialize your plugin and get it to register with the core system. This is partly fixed by allowing users to place an executable file in a directory (that they then configure Govern to look in).

If you are trying to extend Ansible or SaltStack, you have to write your plugins in Python. If you are using Chef or Puppet, you have to use Ruby. But if that weren’t enough, you also have to import packages or modules, and extend some base classes to have your new plugin be automagically recognized by the core system.

With Govern, you can write your facts or runners in any programming language. The only thing Govern cares about, is that its executable bit is set.

At the end of the day, this means that the Govern core can stay exceptionally small, and be infinitely flexible to the needs of its users. In the existing systems, if you found a bug in any of the existing plugins, you would have to wait for the bug report to be triaged, the bug to be reproduced, and for the maintainers to cut a new release. Depending on the project’s overall cadence, you may likely be waiting a while. With Govern, you have the option to add, replace, or completely remove any facts or runners!

Another note on templating

One thing I really liked from SaltStack, was how the .sls files were passed through the templating engine, before being applied. In Ansible, you have to carve out completely-separate tasks files, or conditionally override variables if, say, a pacakge name is different between two Linux distributions. In Salt, it kept a lot of that in one, very convenient place.

The templating engine in Govern, Go’s text/template package, is bundled into and provided by the core govern executable. With templating being something bundled in, this allowed me to bring along the “templated state files” approach I enjoyed so much, from SaltStack.

Declarations

As I mentioned earlier, declarative state is where it’s at. Imperative configuration management, in professional practice, is for masochists.

Govern needed a file format to allow its users to declaratively define their desired state of the system Govern is run on. After fiddling around with different formats, and eventually settled on HashiCorp’s configuration language, HCL.

While the hclsimple package lets you quickly unmarshal HCL from an io.Reader into a structure, you can also use other foundational subpackages to dynamically parse the read state files. This is important because Govern re-discovers all executable facts and runners each time it is started. In practice, there is no real concern of this being a slow operation, since a lot of filesystem operations are cached by the operating system, and doing something like walking a directory is just walking a bunch of direntry structs in a filesystem journal.

It’s the “dynamic” part that is important.

To help frame the rest of this section, here is an example of a resource declaration within a state file:

pkg "emacs" {
  state = "installed"
}

This resource defines a pkg resource called emacs, and says it should have the installed state.

A resource is structured like so:

runner-name "resource-name" {
  arg = val
  ...
}

So, with our pkg "emacs" resource, it tells Govern the emacs resource should be handled by the pkg runner.

Since runners and facts can come into, or be snuffed from, existence between any two executions of govern, it would not make sense to store any state about Govern’s runners or facts across executions. When Govern starts up, it first scans for facts and runners. Next, it will read in any state files from — you guessed it — one or more “state directories”. As each resource defined in a state file is parsed, Govern will attempt to match the resource’s runner-name to a discovered runner. Govern does not know the name of runners before it executes; it can’t.

If Govern cannot find a runner to hand the resource off to, it will print an error message to the screen, and exit before applying any state.

Similarly for facts, if a fact is explicity requested within a templated state file (and remember, all state files are technically templates), Govern will exit before doing anything else. Unfortunately, this currently does not hold true for templated files. If you attempt to use the template runner (which simply calls govern template) to render a templated file, and it cannot find a fact using the added fact template function, the resource itself will fail, but the rest of the state will still be applied.

What’s more, is that the key = value pairs within the body of a resource block are completely free-form. There are only two, reserved keys in a resource block: before, and after.

These currently-unimplemented features are intended to allow Govern to create a directed-acyclic graph (DAG) of the resources to apply, so that users can order the application of resource state across different state files.

Since the runner names are dynamic, and the key-value pairs within a resource name are relatively free-form, in combination with Go being a statically-typed language, the HCL libraries provide a nice set of tools to be able to flexibly parse structured data, without having to force them into a pre-declared struct.

Simplicity

There are a bunch of features in other configuration management tools that I know I did not want to replicate in Govern.

Scheduled execution

Configuration management processes like SaltStack’s minions, like to run as daemonized processes. Since a lot of these systems want to ensure the state declared by the user as often as possible, these processes like to periodically execute their declared state files on some regular cadence, and this means that these processes (since they are running as daemons), need to have some sort of timer to run on. Linux and other UNIX or UNIX-like systems have had periodic execution scheduling for a very long time. It’s called “cron”. Nowadays, you have systemd.timer(5) units. This does not need to be a thing in Govern, since Govern does not run as a daemonized process.

Version control system integration

This was one feature that always stuck out like a sore thumb to me, from SaltStack. Everyone wanted to use it, probably because “it was there”, but it was always such a chore to set up. The fact of the matter is, if you want to make sure the configuration management system applies the latest declarative state revision, you can pull that down yourself. Be smart about it. You know you already have a task scheduler like cron, or systemd, so write a script that runs git pull in the directory where you have Govern configured to look for state files, and make sure it executes before Govern does. Alternatively, write a wrapper script that pulls the latest revisions, then calls govern apply.

A fat core

As I have already pointed out, ad nauseum, the downside of bundling a lot of things into the core product just leads to a burden for the users when you do not get everything perfect out of the gate. At some point, as the developer and maintainer, I want to keep my job as easy and frustration-free as possible, and I do not want to have to deal with long-running email threads about “oh hey, people don’t use yum(8) any more to install packages on RHEL; they use dnf(8) now,” or suffer through conversations about what someone else thinks the best set of flags to pass to some command are. All that does is burden me with the task of keeping the product supportive of systems I probably do not use on a regular basis, and it makes users far less likely to want to adopt it if it does not support their preferred Linux distribution, out of the box.

Necessity

The most important goal of Govern, for me, is that it stays as a simple and flexible core executable, without being mired in too many features that maybe some people will use. I especially do not want to add features that are already provided by the underlying system. That is why Govern’s plugin system is not some complex internal machinery; it mimics a UNIX (or UNIX-like) system’s shell, where it essentially looks in directories for sets of executables to run, like $PATH. This is why Govern will likely never run as a long-running process. It does not need to. It does not need to replicate existing functionality, nor does it need to provide parity for all of the little, seemingly-convenient features that other systems have.

At the end of the day, Govern is intended to be a very basic, and very functional configuration management tool. It will reuse whatever the underlying system provides.

Conclusion

Govern has been my little hobby on the side, for the past year or so. I have experimented with writing runners and facts in various programming languages.

For facts, mostly, I will use good ole, “standard” POSIX sh(1). This has been fine for the most part.

For runners, I started writing them in POSIX sh(1), but it got really cumbersome as I started to add support for other Linux distributions and BSDs I use on occasion. Depending on the task at hand, if it is simple enough, I have really taken a shine to Lua. It is simple, and embeddable within Go, thanks to go-lua. Remember that one of my desires, in Agent vs. Agentless, was that I did not want to have to depend on an interpreter being installed on the managed host. With being able to embed a Lua runtime into a Go program, Govern becomes the interpreter.

For anything more complicated than feels comfortable writing in Lua, I have started putting together a package that handles all of the boilerplate for writing runners in Go. The upside to this, is that you can easily write runners in Go. The downside, is that it makes these runners impenetrable to a non-programmer (or a programmer or sysadmin who is not familiar with Go) that finds a bug. And despite disk space being cheap these days, it also results in runners being incredibly large, statically-compiled binaries.

The Govern core is very functional. Any time I have put into the project lately, has been mostly around adding runners as I need them. I hope you find Govern interesting and useful.