Let's imagine the simplest web page you can have, a page that simply displays "Hello world!"
How hard would it be to change the "Hello World" in this application to "Hello Friends?" Fairly simple, right? How about adding an input field to the same page? That would be a little more difficult, but still not so bad. Maybe we even want to make it so whatever we type in the input field is what is displayed in the header, so that if I type "Hello All" in the input field, the header will display "Hello All." Again, this is a little more complicated, but nothing we haven't seen before.
Applications change. Over time clients make new requests and features are requested that are non-standard amongst other implementations of the same application. Sometimes these changes are low impact, like changing the text on a particular page. They can also be higher impact, like making adjustments to page flow entirely.
Maybe you want to have some configuration for the logo in your app so that clients can display their company branding. Maybe your application is implemented with modules and you want to be able to toggle some modules on and off for particular clients. Inevitability, some level of configurability is introduced into your application as it grows in complexity.
Configuration Complicates Change
In a previous article we've talked about our approach to implementing applications that are on an extreme end of the code vs configuration scale, leaning towards the configurability side.
In short, we use a combination of the SDUI pattern and finite state machines to enable instance level configurability. But we know that there are tradeoffs to this approach.
With this approach, most changes to the application behaviour are no longer made through code and so it can be harder to review changes as part of a change management process. It also complicates the deployment of these changes. Changes made in code can simply be rebuilt and deployed, whereas changes made in configuration files need to be seeded into the correct instances at runtime in order to take effect.
The FSG Stack
We've talked about our usage of finite-state machines and server-driven UIs in the previous article, linked above. The final piece of the puzzle for our stack is GitHub, which we will be using as a service to implement GitOps (FSG = Finite-state machines, Server driven UIs, and Github).
GitOps is a well documented framework for using Git repositories to store configuration, handle changes with merge requests, and reacting to those changes using CI/CD.
As the most popular Git hosting service in the world, GitHub is a reliable and robust choice for implementing a GitOps solution. GitHub exposes a REST API that enables features like reading files from a repository and programmatically committing changes.
We will use GitHub as a source of truth for storing application configurations. The overall flow is as follows:
Using Github gives us access to a version history of how our configuration files change over time. Configurations deployed to production databases may be mutated or overwritten. Having this history is essential for auditing purposes. It also provides insight into essential metadata about every change to our configuration files. We're able to determine who authored the change (the commit author), why the change was made (the commit message), and when the change was made (the commit time).
GitHub additionally allows us to have access to both the latest revision of our configuration on GitHub, and the currently deployed revision. This allows us to introduce a diff check step in our workflow. The deployer of the new configuration (listed above as "Admin") can then perform a final review of the configuration changes before deploying a new one to a production environment.
Lastly, if we ever need to rollback any deployed changes, the process integrates naturally with the Git workflow we are accustomed to. All we need to do is revert the commit that introduced the problematic change from GitHub. After that we can follow the same deployment steps described in the diagram above to revert the configuration change.
Change Is Hard
There exists a silver bullet for every silver bullet. While no solution is perfect, what we've come to call the FSG stack has worked well for us in mitigating the friction associated with maintaining highly configurable applications.