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

A Rocky Migration: Moving from docker-compose to Podman and gVisor

I've been running a few containers for several years. They were all running under rootless Docker with a single user. Initially, I planned to  migrate the containers to VMs , but I couldn't get a stable workflow after about two months of effort. Later,  gVisor caught my attention , and I decided to migrate to Podman with gVisor instead. The new plan is to run each container with  --userns=auto  and use Quadlet for systemd integration. This approach provides better isolation and makes writing firewall rules easier. I'm now close to migrating all my containers. Here are a couple of rough edges I'd like to share. Network Layout I compared  various networking options  and spent a few hours trying the one-interface-per-group approach before giving up. I settled on a single macvlan network and decided to use static IP addresses for my containers. To prevent a randomly assigned IP address from conflicting with a predefined one, I allocated a large IP range for my ...

GameConqueror 0.09 -- Linux Game Hacking Tool

If you are a game hacker If you've been looking for a `CheatEngine for Linux` Then you can't miss this. ============================================== GameConqueror is a game hacking tool for linux, it's written in PyGTK and uses scanmem as its backend. It's supposed to be with most useful features of CheatEngine for Linux. Currently, I've implemented almost everything about scanning, involving variant data types and scan types: Data Types: int{8/16/32/64}, float{32/64}, unknown type(int or float) and unknown width(will try each of them), byte array and string Scan Types: equal, greater, less, changed, unchanged, increased(by), decreased(by) This should be enough for most cases, so I decided to release it at the current status. ============================================= Here's how you can get it PPA (for Ubuntu users) https://launchpad.net/~coolwanglu/+archive/scanmem (I've not test it in 32bit environments or Jaunty, do please inform me if it doe...

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...