By Richard Lander, CTO | April 26, 2024
This blog post is about the fundamental approach we take to managing software delivery. In particular, using code or config to define the systems that deliver apps to their runtime environment.
Over the past few years, I've noticed a trend that is most notable in DevOps. That trend boils down to an aversion to writing software to manage complex software delivery in favor of overloading config management systems. The end result is systems that are more expensive to build, harder to maintain and less reliable.
A great example of this is on the home page for the Crossplane project. It says, "Build control planes without needing to write code." I have come to refer to Crossplane composite resources as programming in YAML. This strikes me as similar to saying, "Construct a modern building without using power tools." It may sound simpler (cheaper) on the surface since you don't have to buy expensive tools or learn to use them, but I guarantee you it will be more difficult and more expensive in the end.
Note: Crossplane as a project has value. The primitive resources that can be used to deploy cloud provider infrastructure are very useful. To be clear, I'm just questioning the wisdom of using composite resources to define the behavior of a cloud native app platform.
Configuration involves passing values into a piece of software.
For example, we often provide a config file to a piece of software when it starts to provide its runtime configuration. TOML and .ini files are common in this use case. They are very useful for providing categorized key-value pairs that instruct a program on things like where to find its database, what port to listen on, what directory to use to write files, etc.
Configuration also includes values that are sent to an API to provide values for some operation we want a system to execute. These are often stored in files as well and then serialized before being sent over the network to the API. XML was very popular for this but has fallen out of favor compared to the more concise JSON and its cousin, YAML. These configuration formats provide great support for more complex nested data structures.
Code is source code for software. We're talking about general purpose programming languages here. Code is written in a Turing complete programming language that uses an interpreter or compiler to produce instructions for a computer. Languages such as Go, Python, Java, C and Javascript are designed specifically to define logic and construct algorithms to solve complex problems.
Something in between also exists. Templating languages live in this realm. Jinja is commonly used in Python programs. Go also has it's own templating language. These are helpful for web applications in rendering HTML with dynamic content to be sent to a client's browser. They accommodate conditionals, loops and variable substitution to serve their purpose but do not have all the features of the general purpose programming languages by design. In the context of a web application, the complex business logic for the app uses templating as the final step to render what is sent to a browser. Any experienced web developer will tell you that you should keep as much logic out of the templates as possible because templating languages are not great at expressing that logic when compared with general purpose programming languages.
This templating that has long been used to render HTML in web apps has been extended to config management. Helm uses Go templating and YAML to manage Kubernetes resource configuration. A helm chart includes a template for Kubernetes resources in YAML format. At deploy-time, you pass it a set of values (also defined in YAML) and Helm renders the complete Kubernetes resources using the values so they can be provided to the Kubernetes API.
Kustomize allows you to take base Kubernetes YAML manifests and overlay them with runtime values for different environments and use cases. The overlays are, themselves, subsections of the YAML manifests with alternative configurations.
The HashiCorp configuration language (HCL) is another example of a language that falls into this category. HCL is used to declare values for input to another system. Logical constructs have been introduced in order to manipulate and substitute values so as to produce variations of the declared values that are needed for a particular use case.
These languages and tools provide features to provide dynamic configuration that work fine when requirements are low to moderately complex. The limitations of the underlying languages cause them to lose value quickly in more complex scenarios.
The approach you take to software delivery must always take into account the complexity of the software you're managing. Simple software systems can be served just fine with simple config tools and manual operations. You don’t need to build a garden shed out of steel and concrete using cranes and scaffolding.
On the other hand, if you're managing sophisticated cloud-native software that is feature rich, leverages a microservices architecture, and serves a lot of users in different geographical locations, you should invest in writing code to deliver that software. Don't attempt to build an office tower out of lumber with a screw gun and circular saw.
Modern software is not getting less complex. Demands for functionality and value inexorably drive software to be more sophisticated and involved. I've heard people advocating for simpler solutions that reject the complexity of microservices and Kubernetes. I would suggest that this is a result of the inadequacies in the state of the art for software delivery, not in those complex architectures and systems themselves. Complex systems can be be made easy to use with the right abstractions. The progress of human technology is testament to that.
Piecing together some simple tools, scripts and manual operations to manage software delivery is the short path to getting your software into users' hands. That can be a good thing. Just don't fall into the trap of thinking the same systems you use to get started will be the ones you use as your software grows in complexity and sophistication. Every time you add features to your software, consider carefully how you'll deliver those features. If you start splitting up a monolith into different services, your old methods of software delivery will likely become inadequate.
I'm a big fan of not making anything more complicated than it needs to be. Just don't make the mistake of trying to use simple tools to do complicated things. Map the features you can support in your software to the capabilities of the delivery system you use to deliver that software. Know when you will need to re-platform your software or, even better, use extensible platforms that can grow with your software's requirements.
Code is useful in building abstractions that handle the complexity that is difficult and error-prone for humans to manage. If a human is having to set a multitude of values in different places to manage a system, the toil and potential for mistakes can be mitigated with useful abstractions. A piece of software that takes a small set of high-level inputs and translates those into the minutia of a multitude of interdependent config values pays dividends in complex systems.
The more complex the system, the more value a good abstraction provides over the long term.
The holy grail of abstractions is the ability to provide simple use cases with simple interfaces while also providing the ability to accommodate the most complex implementation by bypassing the simple abstraction to a lower level when necessary.
GitOps is the popular state-of-the-art in managing software delivery to Kubernetes. Helm and Kustomize work well for simple resource management in that environment. However, many teams soon find themselves managing thousands of lines of templating and config, hundreds of variables, across dozens of different Kubernetes resources - all for a single application.
If you find yourself in this position, you should strongly consider developing a Kubernetes operator. The Kubernetes operator pattern is powerful and underused today. They are an excellent approach to abstracting the complexity of the granular configuration of all the resources that comprise your application.
If this describes your situation, check out our blog post: What is Platform Engineering?. It outlines how you can use the operator pattern to improve your software delivery.
If you're managing cloud-native software and hitting the challenges of delivering that software to its runtime environment, check out Threeport. Threeport exists to allow teams to sidestep the simplicity trap and use abstractions that make it easy to run all software on top of sophisticated systems like Kubernetes. That allows you to leverage a highly capable platform that is easy to get started with but will grow with your software as it meets more requirements to serve your users. It allows you to completely eliminate the threat of re-platforming your software in the future. Threeport is open source and free to try out. You can download the Threeport CLI from GitHub and try it out any time.
Qleet offers managed Threeport control planes to further simplify the responsibilities of your team so you can focus on building value in the software you deliver.