有一段時間,我都是把自己的服務跑在架在 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 追蹤。
用 LLM 協助遷移服務
我預設把這段收起來,因為我知道有些人不喜歡使用 LLM。
這段過程我有用
codex幫忙。我基本上給它透過 kubectl 存取 cluster 的權限,請它先弄清楚目前的狀態,然後規劃遷移方式。接著我叫它一件一件做:包括寫一支 script 從 Kubernetes 把 secrets 抓出來,pipe 進 agenix 加密(由於不會 pipe 到 stdout,也不會被存起來,所以理論上不會出現在 LLM 的對話紀錄裡面)。中間我需要手動介入幾次,去 detach/attach disk volumes。我相信這部分也可以自動化,但我覺得把它當成一個檢查點也不錯。
這真的幫我省了很多時間,至少不用我手動把一堆 kubernetes yaml 檔手動翻譯成 nix …
最後結果
這次遷移前,原本 footprint 是四台 CPX21 加上一些相關資源,總共大約 €55,降到只剩一台 CPX41,而且這台也遠超過實際所需。我們大概可以只跑在一台 CPX21 上,這樣成本就只要 €12。
不過成本不是主要想搬的原因。這個新環境維護和部署起來都簡單很多。舊設定需要維護兩個 repo(terraform 和 k8s yamls),新的設定則只需要維護一個 nix flake。
之後我們大概也可以把一些服務從 container 裡搬出來,改用比較輕量的 namespace,甚至直接用普通的 systemd unit 來跑。