有一段時間,我都是把自己的服務跑在架在 Hetzner 上的 Kubernetes cluster。雖然說是可以動,但偶爾還是有一些亂七八糟的小問題:

  • 有幾次版本升級把整個 cluster 弄到大爆炸,最後得手動修回來
  • 雖然可以跑最小化的 cluster,但那樣就沒辦法做自動升級
  • 想用多節點又要把帳單壓到最便宜,就要跑很多台小 node。Cluster 就會常常處在記憶體警告邊緣,任何 deployment 一動就會把東西搬來搬去。

對於我只是想跑幾個小東西來說,這整套的 overhead 實在有點太大。我自己很多個人用的東西都已經慢慢搬到 Nix/NixOS 上了,所以我就在想不如也考慮把這個 cluster 也處理一下。

安裝 NixOS

我原本以為這會是最麻煩的一段:手動建立伺服器、部署第一階段設定、ssh 進去收尾,最後才開始能遠端部署新的設定。更糟的是,Hetzner 不支援建立一個 NixOS server,所以你還得自己做一份 NixOS snapshot 開始。

還好其實已經有人把這段最難的部分處理完了: nixos-anywhere。這個 project 可以 SSH 進一台新伺服器,下載 NixOS、kexec 進去,重新分割並格式化硬碟,然後用你的設定把 NixOS 裝起來。全部一個指令就可以完成!

實際上,你只要先設定一個 flake,裡面放一個類似像這樣的 host config:

{
  lib,
  pkgs,
  ...
}: {
  imports = [
    ./disk-config.nix
    ./hardware-configuration.nix
    ...
  ];
 
  disko.devices = {
    disk.main = {
      type = "disk";
      device = "/dev/sda";
      content = {
        type = "gpt";
        partitions = {
          boot = {
            size = "1M";
            type = "EF02";
          };
          esp = {
            size = "512M";
            type = "EF00";
            content = {
              type = "filesystem";
              format = "vfat";
              mountpoint = "/boot";
              mountOptions = [
                "umask=0077"
              ];
            };
          };
          root = {
            size = "100%";
            content = {
              type = "filesystem";
              format = "ext4";
              extraArgs = [
                "-L"
                "nixos"
              ];
              mountpoint = "/";
            };
          };
        };
      };
    };
  };
  
  nix.package = lib.mkDefault pkgs.lixPackageSets.stable.lix;
  nix.settings = {
    experimental-features = [
      "nix-command"
      "flakes"
    ];
  };
  nix.gc = {
    automatic = true;
    dates = "weekly";
    options = "--delete-older-than 14d";
  };
  nix.optimise = {
    automatic = true;
    dates = ["weekly"];
  };
  nixpkgs.config.allowUnfree = true;
 
  boot.loader.efi.canTouchEfiVariables = false;
  boot.loader.grub = {
    enable = true;
    efiSupport = true;
    efiInstallAsRemovable = true;
  };
  boot.kernelParams = ["console=ttyS0,115200n8"];
 
  networking.hostName = "hetzner";
  networking.useDHCP = lib.mkDefault true;
  networking.firewall = {
    allowPing = true;
    allowedTCPPorts = [
      22
      80
      443
    ];
  };
 
  services.openssh = {
    enable = true;
    openFirewall = true;
    settings = {
      PermitRootLogin = "prohibit-password";
    };
  };
  services.qemuGuest.enable = true;
  services.fstrim.enable = true;
 
  security.sudo.wheelNeedsPassword = false;
 
  time.timeZone = "America/Los_Angeles";
  i18n.defaultLocale = "en_US.UTF-8";
 
  environment.systemPackages = with pkgs; [
    btop
    curl
    git
    vim
    wget
  ];
 
  system.stateVersion = "25.11";
}

接著你就可以跑 nix run .#nixos-anywhere -- --build-on remote --flake .#hetzner root@<hetzner-ip>,讓它自動完成安裝!

遷移服務

到這裡,你的伺服器應該已經切換到 NixOS 了。從這裡開始,你就可以繼續調整設定,並用 nixos-rebuild switch --flake .#hetzner --target-host root@<hetzner-ip> --build-host root@<hetzner-ip> 來更新。也就是說,是時候該把服務搬過來了。

我的 cluster 上其實沒有什麼真的不能停掉的 production service,所以我就選擇了最簡單的遷移方式:先停掉,搬過來,然後再把 domain 指過來。它們的流程大致就是:scale down deployment、把 disk volume 從 k8s agent 上 detach、attach 到新伺服器、把資料複製到新的 volume、在新伺服器上啟動服務,最後把 domain name 指過來。

為了部署 secrets,我用了 agenix,這樣就可以把 secrets 加密後直接放進 repo 追蹤。

最後結果

這次遷移前,原本 footprint 是四台 CPX21 加上一些相關資源,總共大約 €55,降到只剩一台 CPX41,而且這台也遠超過實際所需。我們大概可以只跑在一台 CPX21 上,這樣成本就只要 €12。

不過成本不是主要想搬的原因。這個新環境維護和部署起來都簡單很多。舊設定需要維護兩個 repo(terraform 和 k8s yamls),新的設定則只需要維護一個 nix flake。

之後我們大概也可以把一些服務從 container 裡搬出來,改用比較輕量的 namespace,甚至直接用普通的 systemd unit 來跑。