Use Case

I try to keep my cloud compute footprint to a minimum, but there are some very compelling situations in which it is useful to have a service running off-network. Ordinarily someone might just use the tool they know well, in my case Ansible, to manage the configuration of these services. Regardless, you may often find yourself in need of defining the state of a remote system. Here’s how you can accomplish this the Nix way with Colmena and Flakes.

First you’ll need to install Colmena:

nix-shell -p colmena

And here is an example of a flake to get started:

{
  description = "Colmena Remote NixOS Management";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-22.11";
  };

  outputs = { nixpkgs, ... }@inputs:
    colmena = {
      meta = {
        nixpkgs = import nixpkgs {
          system = "x86_64-linux";
          overlays = [];
        };
        specialArgs = {
          inherit inputs;
        };
      };
      myhost = ./hosts/myhost; # this should be a directory with a default.nix inside
    };
}

This is where things should start to make sense, implying that you’ve dabbled in configuring NixOS before. Pay close attention to the highlighted section. This is where we’ll define which host we should be deploying to, which user to ssh as, and more:

./hosts/myhost/default.nix:

{ name, nodes, pkgs, lib, inputs, ... }: {

deployment = {
  targetHost = "myhost.example.com"; # also supports an IP address
  targetPort = 22;
  targetUser = "root";
  buildOnTarget = true;
};

imports =
  [
    ./hardware-configuration.nix
  ];

boot.loader.grub.enable = true;
networking.hostName = "myhost"; # Define your hostname.
networking.useDHCP = false;
networking.interfaces.eth0.useDHCP = true;
time.timeZone = "America/New_York";
i18n.defaultLocale = "en_US.UTF-8";

nix.settings.trusted-users = [ "root" "@wheel" ];

users.users.myuser = {
  isNormalUser = true;
  extraGroups = [ "wheel" "networkmanager" ];
  home = "/home/myuser";
  packages = with pkgs; [
    vim
    tree
    lego
  ];
  openssh.authorizedKeys.keys = [ "ssh-rsa AAAAAAAAAAAAAAAAAAAZZZZZZZZZZZDDDDDDDDDDDDDD" ];
};

environment.systemPackages = with pkgs; [
  inetutils
  mtr
  sysstat
  dig
  openssl
];

services.openssh = {
  enable = true;
  settings.PermitRootLogin = "yes";
};

networking.firewall.allowedTCPPorts = [ 22 80 443 ];
# networking.firewall.allowedUDPPorts = [ ... ];
networking.firewall.enable = true;
networking.usePredictableInterfaceNames = false;

system.stateVersion = "23.05";
}

I typically pull the contents of hardware-configuration.nix from the target system and place it in my working tree next to default.nix.

Naturally you can extend this default file by including more files in the imports section:

...
imports =
  [
    ./hardware-configuration.nix
    ./fail2ban.nix
    ./nginx.nix
    ./docker.nix
  ];
...

Once you’re happy with your NixOS configurations you can check them into VCS and perform a test build, and then check the output on the target system:

$ colmena build
warning: Git tree '/home/crutonjohn/Documents/nix' is dirty
[INFO ] Using flake: git+file:///home/crutonjohn/Documents/nix
[INFO ] Enumerating nodes...
[INFO ] Selected all 1 nodes.
      ✅ 7s All done!
 myhost ✅ 5s Evaluated myhost
 myhost ✅ 2s Built "/nix/store/qk58hgf2maspbbl1cmy87x4wql7430fa-nixos-system-myhost-24.05pre-git" on target node

$ ssh nord
Last login: Sat Feb  3 12:27:37 2024 from 73.40.37.26

[myuser@myhost:~]$ ls /nix/store/qk58hgf2maspbbl1cmy87x4wql7430fa-nixos-system-myhost-24.05pre-git
activate               boot.json           etc                 init                    kernel          nixos-version   system
append-initrd-secrets  configuration-name  extra-dependencies  init-interface-version  kernel-modules  specialisation  systemd
bin                    dry-activate        firmware            initrd                  kernel-params   sw

If the derivation is to your liking you can go ahead and deploy it:

colmena apply

There are further ways to extend functionality by establishing “tags” for your different target systems like so:

deployment = {
  targetHost = "webserver01.example.com"; # also supports an IP address
  targetPort = 22;
  targetUser = "root";
  buildOnTarget = true;
  tags = [
    "webserver01"
    "infra"
    "us-east"
  ];
};

Deploying configuration to our collection of webservers::

$ colmena apply --on '@webserver*'

Conclusion

The parallels between Ansible and Colmena make it easy for me to grasp. It’s like using Ansible that is natively Nix-aware. I’m in the process of converting a bunch of my own machines to use this deployment method to fully embrace the NixOS life.