Skip to content

applicative-systems/terraform-providers

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Declarative NixOS services via paired Terraform providers

Make NixOS services more declaratively configurable than upstream Nixpkgs modules allow, by pairing each service with its Terraform provider and reconciling the service's runtime state once it is up.

Status: the pattern is implemented and the Forgejo pairing is the worked reference (see services/forgejo).

The gap this closes

Upstream NixOS modules configure a service's static surface — package version, config file, the systemd unit. They deliberately do not manage a service's runtime state: Grafana dashboards/datasources, a Git forge's orgs/repos/teams, and so on. Many such services ship a Terraform provider that manages exactly that state.

Here you declare the desired runtime state in Nix, and a systemd unit applies it (via OpenTofu) against the live, local instance after the service's primary unit starts:

services.forgejo = {
  enable = true;
  runtime = {
    enable = true;
    organizations.acme.description = "ACME Corporation";
    repositories.widgets = {
      owner = "acme";
      description = "Widget factory";
    };
  };
};

A pairing only makes sense when a service has admin-declarative runtime state reachable through a provider that the NixOS module cannot already express. Services whose entire surface is config-file-driven (already declarative via their NixOS options) are out of scope.

How it works

For each enabled pairing:

  1. Enable the upstream Nixpkgs service (services.<svc>.enable = true).
  2. Generate .tf.json from services.<svc>.runtime.* (desired state).
  3. Run a Type=oneshot apply unit ordered After= the primary unit, gated on a readiness probe; it re-applies when the generated config changes.

Reconciliation is run-once, not a drift timer. A failed apply fails that unit visibly (systemctl status) without tearing down the service.

Usage

Add this flake as an input and import the pairing's NixOS module (nixosModules.forgejo, or nixosModules.default for all pairings). Full installation, configuration examples, the option reference, the resource table, and the secrets guide live in the per-pairing README:

Secrets

Secrets should never enter the world-readable Nix store. The admin token and any secret-valued resource attribute are read at apply time from a host file path via systemd LoadCredential= into a sensitive Terraform variable; the generated .tf.json holds only a ${var.…} reference. Each secret attribute has an <attr>File form (e.g. dataFile, passwordFile) that takes the host path — prefer it over the literal for any real secret.

Repository layout

flake.nix          # outputs: nixosModules, checks, formatter
treefmt.nix        # treefmt + nixfmt config
modules/
  default.nix      # aggregates per-pairing modules into nixosModules.default
  lib/             # provider-agnostic helpers: tf-label/file, run-once reconciler
services/          # one directory per service<->provider pairing
  forgejo/         # the worked Forgejo <-> svalabs/forgejo pairing
    module.nix     #   NixOS module: services.forgejo.runtime + systemd wiring
    lib.nix        #   provider specifics: wrapped executor + .tf.json generation
    pkg.nix        #   vendor the provider (not in nixpkgs)
    checks.nix     #   NixOS VM test
    README.md      #   usage docs

Development

nix flake check                      # eval modules + run all NixOS VM tests + formatting
nix build .#checks.<system>.forgejo  # run one pairing's VM test
nix fmt                              # treefmt -> nixfmt across the tree

Behavior is proven with NixOS VM integration tests (runNixOSTest): boot a VM with the pairing enabled, let the reconciler apply, then assert the runtime state by querying the live service API — not the Terraform state. Eval-only or build-only success is not evidence the reconciliation works.

Design decisions

  • Executor: OpenTofu (MPL 2.0); terraform (BSL 1.1, unfree) is not used.
  • Config: .tf.json generated directly from Nix (builtins.toJSON) — no HCL, no terranix.
  • State: local, per-host only, co-located under the base service's state directory (e.g. /var/lib/forgejo). No remote backends.
  • Namespace: options live under services.<svc>.runtime.*, so a pairing reads as a transparent extension of the upstream services.<svc> module.
  • Toolchain: flake nixpkgs tracks nixos-unstable; Nix ≥ 2.18.

Contributors: CLAUDE.md documents the full provider implementation contract for adding a new pairing.

License

MIT.

About

Service runtime configuration through Terraform Providers

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages