Skip to main content

A Declarative Approach to Config File Management

Configuration files for different services are rarely independent. For example, in nftables, I might tag traffic with a firewall mark, and that mark is then used by systemd-networkd or in ip routes. Similarly, when the name of the primary network interface changes, multiple services like nftables, postfix, and samba need to be updated.

Requirements

  • I want to define core data in one place, then update all config files with a simple command.
  • If a configuration file is modified by an external process (for example, a package update from a vendor or distribution), the changes must be handled gracefully. Either the merge should be automatic and permanent, or I should be notified to easily resolve any conflicts.
  • It should be obvious within the config file itself what changes I have made.

Existing Solutions

I did some quick survey and found a few options.

1. Templates

These tools render a template using provided data sources. To manage /etc/config.txt, I would create a /etc/config.txt.template with all the moving parts marked using the required syntax.

Examples include:

The biggest issue is that the generated config file is no longer the source of truth. This means if the generated file is modified by other tools, those changes will be lost the next time I render the template.

Perhaps these tools are better suited for scenarios like building images with bootc or mkosi.

2. Patching

These tools record and apply the diff between a default state and the desired state.

Examples include:

There are two issues with these tools:

  1. The diff is stored separately from the config file, which is hard to read and maintain. I might also need to keep a copy of the original, unpatched file for reference.

  2. The patch might not be reliable if there isn't enough context to locate the exact area for patching. For example, consider a config file like this:

    [Config for user A]
    // many lines
    use_https = true
    
    [Config for user B]
    // many lines
    use_https = false
    

    We want to modify the use_https setting for user A and generate a diff. Later, if the vendor's config file swaps the order of user A and B, the patch might still apply without error, but it will modify the wrong section!

Note that while Ansible can place markers around managed blocks, it must first insert them. For the initial insertion, it relies on regular expressions (insertafter and insertbefore) to find the location, which can be brittle.

3. Generators

NixOS allows you to generate all config files using custom data and functions in the same language.

The biggest issues with this approach are:

  1. You are forced to commit to a specific ecosystem like NixOS or another tool that fully manages your system's configuration.

  2. Merge conflicts almost never happen because your own NixOS configuration is just an override of the default values. This means you aren't notified of potential semantic conflicts. For example, if a default value you were referencing changes upstream, your configuration will adapt silently, which may not be the desired behavior without a manual review.

My Plan

The existing solutions I found almost solve my problem, but not 100%.

The closest approach I found is a combination of:

  • Adding a custom, unique anchor comment in the config file.
  • Using Ansible's blockinfile with the anchor comment for insertafter or insertbefore.

But I still don't like that the diff is stored separately from the config file. To solve this, my plan is to embed the template directly inside the configuration file, like this:

### BEGIN MANAGED BLOCK
### binds_to = {{ config.permanent_lan_interface.name }}
### END OF TEMPLATE
### END MANAGED BLOCK

I'll then write a script that:

  • Deletes all text after END OF TEMPLATE.
  • Parses the template before END OF TEMPLATE.
  • Renders the template using libraries like Jinja.
  • Puts the rendered template after END OF TEMPLATE.

Final Thoughts

My current plan may not be elegant, but it seems to meet my requirements more effectively than the other solutions.

Meanwhile, I'm still looking for new options. Please let me know if you know any.

Comments

Popular posts from this blog

Determine Perspective Lines With Off-page Vanishing Point

In perspective drawing, a vanishing point represents a group of parallel lines, in other words, a direction. For any point on the paper, if we want a line towards the same direction (in the 3d space), we simply draw a line through it and the vanishing point. But sometimes the vanishing point is too far away, such that it is outside the paper/canvas. In this example, we have a point P and two perspective lines L1 and L2. The vanishing point VP is naturally the intersection of L1 and L2. The task is to draw a line through P and VP, without having VP on the paper. I am aware of a few traditional solutions: 1. Use extra pieces of paper such that we can extend L1 and L2 until we see VP. 2. Draw everything in a smaller scale, such that we can see both P and VP on the paper. Draw the line and scale everything back. 3. Draw a perspective grid using the Brewer Method. #1 and #2 might be quite practical. #3 may not guarantee a solution, unless we can measure distances/p...

Qubes OS: First Impressions

A few days ago, while browsing security topics online, Qubes OS surfaced—whether via YouTube recommendations or search results, I can't recall precisely. Intrigued by its unique approach to security through compartmentalization, I delved into the documentation and watched some demos. My interest was piqued enough that I felt compelled to install it and give it a try firsthand. My overall first impression of Qubes OS is highly positive. Had I discovered it earlier, I might have reconsidered starting my hardware password manager project. Conceptually, Qubes OS is not much different from running a bunch of virtual machines simultaneously. However, its brilliance lies in the seamless desktop integration and the well-designed template system, making it far more user-friendly than a manual VM setup. I was particularly impressed by the concept of disposable VMs for temporary tasks and the clear separation of critical functions like networking (sys-net) and USB handling (sys-usb) into the...

Exploring Immutable Distros and Declarative Management

My current server setup, based on Debian Stable and Docker, has served me reliably for years. It's stable, familiar, and gets the job done. However, an intriguing article I revisited recently about Fedora CoreOS, rpm-ostree, and OSTree native containers sparked my curiosity and sent me down a rabbit hole exploring alternative approaches to system management. Could there be a better way? Core Goals & Requirements Before diving into new technologies, I wanted to define what "better" means for my use case: The base operating system must update automatically and reliably. Hosted services (applications) should be updatable either automatically or manually, depending on the service. Configuration and data files need to be easy to modify, and crucially, automatically tracked and backed up. Current Setup: Debian Stable + Docker My current infrastructure consists of several servers, all running Debian Stable. System Updates are andled automatically via unattended-upgrades. Se...