My First Impressions of Nix

Michael Lynch

我對 Nix 的第一印象

Nix 是一款能依據原始碼檔案來設定軟體環境的工具。最近我在 Hacker News 和 Twitter 上越來越常聽到關於 Nix 的討論。它的理念很吸引我,所以過去幾週我一直在摸索試用。

我與 infrastructure as code(基礎設施即程式碼) 的淵源

十年前,我發現了 Salt,這是一套能讓你用原始碼來定義電腦系統設定的工具。我很喜歡用一個 Git 儲存庫來定義我的電腦和虛擬機器上安裝了哪些服務的想法。就算把電腦整個清除,只要重新執行設定工具,就能讓它恢復到原本的狀態。

我玩了幾年 Salt,直到後來發現了 Ansible,我覺得它把同樣的理念執行得更好。

我所有的開發工作都是在我的 homelab 伺服器上的虛擬機器中進行。我為每個專案都建立了獨立的虛擬機器,並全部用 Ansible 來管理。

Ansible 的問題

Ansible 最大的問題是速度慢得令人痛苦。在我的一台虛擬機器上跑一次 Ansible,通常要花 10 到 15 分鐘。

假設我想安裝一個新的 apt 套件 foo。我是該直接執行 sudo apt install --yes foo 然後在 5 秒內就拿到套件?還是該打開我的 Ansible role,編輯設定檔新增一個安裝 foo 的步驟,然後執行 playbook,再等上 15 分鐘?顯然,我最後大多會選擇前者,結果我的實際環境就逐漸偏離了原本應該代表它的 Ansible 檔案。

另一個問題是 Ansible 的變更不具備向下相容性。所以,如果我為了使用某個新的 Ansible 功能而更新了一份 playbook,就必須把所有的 playbook 都更新到相容的版本。但我從來沒有動力去重寫並重新測試所有的 playbook,所以就一直卡在舊版本上。我現在還在用三年前發布的 Ansible 2.9。

Nix 的吸引力

我看到越來越多人談論 Nix 和 NixOS。許多我覺得有趣的開發者都在分享他們嘗試 Nix 的經驗。

我看過最有影響力的推薦來自 Hashicorp 共同創辦人 Mitchell Hashimoto(米契爾·哈希莫托),這家公司打造了許多廣泛使用的開源基礎設施工具,像是 Vagrant、Packer、Consul 和 Terraform。他稱 Nix 是「近年來我學過、帶來最正面影響的第一名技術」。

這是我的 Nix 之旅。到目前為止,我依然認為這是近年來我學過、帶來最正面影響的第一名技術。

Nix 的理念給我的感覺很像 Ansible。Nix 讓你用程式碼定義設定,然後它會把系統帶到那個狀態。

Nix vs. Ansible

Nix 在幾個我覺得很有意思的重要面向上與 Ansible 不同。

Nix 比 Ansible 更快

Ansible 沒有它所設定系統「狀態」的概念。如果在一台虛擬機器上跑一次 Ansible 要花 15 分鐘,那麼一分鐘後再跑一次相同的 playbook,大概還是要花 10 分鐘。兩次執行之間大概不會有套件更新,所以會省下一點時間,但它基本上還是做了幾乎所有相同的工作。

Ansible 永遠不會說:「喔,我剛剛才設定過那台機器,所以現在沒什麼事要做了。」Ansible 每次都必須重新執行每一項設定,因為自上次執行以來,什麼事都有可能發生。

相較之下,Nix 確實有狀態的概念。如果你對一份 200 行的 Nix 設定檔只改了一行,它不需要重做其他 199 行的工作。它可以對照設定檔來評估系統狀態,並判斷出只需要套用那一行的變更。而那個變更通常幾秒鐘內就會完成。

編按(2023-06-19):一位更有經驗的讀者澄清 Nix 並非如我所想的那樣具有狀態

Nix 之所以快,不是因為它是有狀態的。而是因為它是函數式且可重現的,這讓它能在不犧牲正確性的前提下進行快取。

我原本以為 Nix 會追蹤哪些任務讓我到達哪個系統狀態。舉例來說,想像執行任務 X 和 Y 會讓我到達狀態 A,而執行任務 X、Y 和 Z 則會到達狀態 B。我以為處於狀態 A 的 Nix 系統會知道只要執行任務 Z 就能到達狀態 B。

我現在的理解是,如果你要求處於狀態 A 的 Nix 系統前往狀態 B,Nix 會執行任務 X、Y 和 Z,但由於 Nix 已經快取了任務 X 和 Y 的結果,所以它們幾乎是瞬間完成的。

Nix 針對本地設定進行最佳化

Ansible 是為了透過網路來設定系統而設計的。你當然還是可以把 localhost 指定為目標,但那並非 Ansible 最佳化的使用情境。

Nix 則是為了設定它所在的環境而設計的。使用 Nix 時,你定義環境中應該包含什麼,然後 Nix 會就地為你建立那個環境。使用 NixOS 時,你定義的是整個作業系統,而 NixOS 會讓作業系統進入那個狀態。你可以更改像是檔案系統、Linux 核心或開機載入程式這類底層的東西。

這解決了我在使用 Ansible 時遇到的問題:把一份 Ansible playbook 升級到較新的 Ansible 版本,就得把系統上所有的 playbook 都升級。你可以讓許多 Nix 系統同時執行不同版本的 Nix,而且都能正常運作。如果我有一些 Ansible 檔案需要 Ansible 2.9,有些需要 2.10,有些又需要 2.14,那麼要同時兼顧它們就會非常麻煩。

Nix 的變更是原子性的

使用 Ansible 時,很容易在設定到一半時失敗,讓系統停留在未定義的狀態。

而使用 Nix,變更是原子性的。Nix 要嘛讓你的系統進入期望的狀態,要嘛就 rollback 回到你嘗試變更設定之前的狀態。

對我有幫助的 Nix 資源

我看到關於 Nix 最常見的抱怨之一,就是它的文件不足、錯誤或寫得很差。我的經驗是,文件給人的感覺像是寫給有經驗的 Nix 使用者看的。

我找到的許多 Nix 文件都會寫類似「只要加上這幾行就好!」這樣的話。

蛤?

哪個檔案?又要加在檔案的哪個位置?

以下是到目前為止我找到最棒的資源:

第一次失敗的嘗試:在虛擬機器中安裝 NixOS

我在 Proxmox 虛擬機器上照著「NixOS for the Impatient」教學操作,一開始一切正常。接著,我做到教學中要變更 hostname 的部分,然後重新開機讓變更生效。但虛擬機器卻進入了一種奇怪的狀態,看起來可以開機,卻無法登入。在登入畫面輸入密碼後,畫面就直接凍結了。

NixOS 已成功安裝在我的 Proxmox 虛擬機器伺服器上,但在第二次開機登入後就卡住了。

第二次失敗的嘗試:在 Raspberry Pi 4 上安裝 NixOS

既然虛擬機器行不通,我想下一步合乎邏輯的選擇就是實體機。我手邊剛好有一台備用的 Raspberry Pi 4,想說用 Pi 來實驗應該會很有趣。

我找到了兩份看起來很官方、在 Raspberry Pi 4 上安裝 NixOS 的教學:

這兩份教學的問題在於,它們都假設你已經在執行 Nix 環境。我是想用我的主力電腦(一台 Win10 系統)來準備 microSD 卡,所以根本還沒有 Nix。

NixOS 下載頁面列出了一個 64 位元 ARM 映像檔。Raspberry Pi 4 支援 64 位元 ARM,所以我想說來試試看。

我打開 Balena Etcher,這是我慣用來燒錄 microSD 卡的工具。第一個警訊是 Etcher 基本上在說:「嘿,你在想什麼?那根本不是可開機的映像檔。」

缺少分割區表。看起來這不是可開機的映像檔。此映像檔似乎不包含分割區表,可能無法被你的裝置識別或開機。

但我還是硬著頭皮繼續!不過當我嘗試用那張 microSD 開機時,Pi 也認為這不是可開機的映像檔,所以就直接卡住了。

Pi 開機畫面顯示『Progress: Trying boot mode USB-MSD』

我試著用官方的 Raspberry Pi Imager 工具來燒錄同一個映像檔,但結果還是一樣。

更新:我最後成功讓它運作了

成功:在 Dell Mini 電腦上安裝 NixOS

我工作上的大多數測試都是在一台 Dell Optiplex 7040 上進行的。那是我手邊唯一一台可以清空重來的實體機,所以我就在那台上試試。

一切都和「NixOS for the Impatient」裡寫的一模一樣。從頭到尾安裝大約花了 10 分鐘,其中 7 分鐘只是在複製檔案。因為這只是一台測試用的裝置,而且我想省去每次重新開機都要輸入密碼的步驟,所以我略過了加密。

在 Dell Optiplex 7040 上安裝 NixOS(我把複製檔案的片段加速了)

最後,我得到了一套完整且可正常運作的 NixOS!

第三次失敗的嘗試:再次在 Raspberry Pi 4 上安裝 NixOS

既然現在已經有一台可運作的 NixOS 機器,我想再給 Raspberry Pi 一次機會。之前的阻礙是沒有可用來準備 microSD 映像檔的 Nix 環境,但現在我有了。

我照著 nix.dev 教學操作,這次比第一次嘗試進展得更遠,但還是無法開機。Pi 只會顯示彩色畫面,然後就卡住:

當我從 NixOS 系統燒錄 NixOS Pi aarch64 microSD 映像檔並用它來啟動 Pi 時,它就卡在彩色畫面上。

更新:我最後成功讓它運作了

任務一:取得 SSH 存取權限

好,回到我在 Dell Optiplex 上可正常運作的 NixOS 安裝。

我需要從主力電腦透過 SSH 連進 NixOS 系統。要做到這點,我得先把我的 SSH 金鑰放到系統上。

在 NixOS 上,我打開 Console 應用程式,然後輸入 sudo nano /etc/nixos/configuration.nix。接著,我找到 environment.systemPackages 並加入以下這幾行:

  environment.systemPackages = with pkgs; [
    vim
    curl
  ];

為了讓變更生效,我執行:

sudo nixos-rebuild switch

現在我已經有 vimcurl 可用,所以可以從 GitHub 拉下我的 SSH 公鑰:

sudo mkdir -p /etc/nixos/ssh

GITHUB_USERNAME='mtlynch'
curl "https://github.com/${GITHUB_USERNAME}.keys" | \
  sudo tee --append /etc/nixos/ssh/authorized_keys

接著,我執行 sudo vim /etc/nixos/configuration.nix 並加入以下幾行:

  # Enable the OpenSSH daemon.
  services.openssh.enable = true;
  users.users.mike.openssh.authorizedKeys.keyFiles = [
    /etc/nixos/ssh/authorized_keys
  ];

最後,我重新建置並重新開機。我不確定重新開機是否真的必要:

sudo nixos-rebuild switch && sudo reboot

成功了!在那之後,我就能從主力電腦 ssh 進我的 NixOS 系統了。

任務二:移除 Gnome 雜物

關於這個作業系統,最先讓我注意到的是有好多雜物。它內建了一堆我不需要的應用程式,像是通訊錄和天氣應用程式:

我上網搜尋如何移除它們,發現它們是 Gnome shell 預設應用程式的一部分。你可以在 /etc/nixos/configuration.nix 中加入這一行來停用它們:

services.gnome.core-utilities.enable = false;

或者你可以逐一移除每個工具:

 environment.gnome.excludePackages = with pkgs.gnome; [
    baobab      # disk usage analyzer
    cheese      # photo booth
    eog         # image viewer
    epiphany    # web browser
    gedit       # text editor
    simple-scan # document scanner
    totem       # video player
    yelp        # help viewer
    evince      # document viewer
    file-roller # archive manager
    geary       # email client
    seahorse    # password manager

    gnome-calculator
    gnome-calendar
    gnome-characters
    gnome-clocks
    gnome-contacts
    gnome-font-viewer
    gnome-logs
    gnome-maps
    gnome-music
    gnome-screenshot
    gnome-system-monitor
    gnome-weather
    gnome-disk-utility
    pkgs.gnome-connections
  ];

我選擇了「全部停用」的選項並重新建置:

sudo nixos-rebuild switch

完成了!所有的雜物都消失了:

任務三:找回系統監視器(失敗)

我覺得唯一值得保留的 Gnome 工具是系統監視器。我試著把它加到 environment.systemPackages 清單中,但重新建置卻失敗了:

$ sudo nixos-rebuild switch
building Nix...
building the system configuration...
error: undefined variable 'gnome-system-monitor'

       at /etc/nixos/configuration.nix:130:5:

          129|     curl
          130|     gnome-system-monitor
             |     ^
          131|   ];
(use '--show-trace' to show detailed location information)

我試了其他可能的名字,像是 gnome-shell-system-monitor,但還是搞不清楚該怎麼安裝它。

編按(2023-06-19):感謝讀者指出正確的套件名稱是 gnome.gnome-system-monitor。我當時缺少的關鍵是,我可以在 search.nixos.org 上搜尋套件。

接下來想了解的事情

我對使用 Nix 和 NixOS 的頭幾天感到滿意。大致符合我聽聞的預期。看起來只要用得好,它可以非常強大,但需要大量的前期投入以及四處搜尋資訊。

我目前只觸及皮毛,所以以下是接下來想進一步了解 Nix 的部分。

在 NixOS 系統上使用 VS Code Remote SSH

我所有的開發都是透過 remote SSH 在 VS Code 中進行。當我嘗試從 VS Code 遠端連進 NixOS 系統時,安裝會失敗。VS Code 必須在目標系統上安裝某種伺服器,而 VS Code 大概不知道如何在 NixOS 上安裝。

有一個 nixos-vscode-server Git 儲存庫,那大概就是我需要的解決方案。我還沒試過。

Nix 的主要概念如何協同運作

我看到像是「flakes」和「derivations」這樣的詞,但目前還不知道它們是什麼意思。我還不懂 Nix 的語言語法,不過它跟 JavaScript 和 Python 夠像,所以現階段我還能勉強應付。但要有效使用 Nix,顯然我得好好學這個語言。

決定性究竟在何時發生?

當我看到關於 Nix 的討論時,最常被提到的頂級特色之一就是 Nix 具有決定性。

到目前為止,我還是不懂它怎麼會是決定性的。當我指定要安裝的套件時,我既沒有指定完整性雜湊值,更別說版本號了。如果一年後我再執行相同的 Nix 設定,我猜會得到不同的系統,因為它會安裝不同版本的 vimcurl 套件。

我猜應該有更精確指定套件版本的方法,只是我還沒學到。

我該信任誰?

當我指定套件時,那只是一份單純的套件名稱清單:

  environment.systemPackages = with pkgs; [
    vim
    curl
  ];

要讓我能像上面那樣指定套件,Nix 必然是從預設的儲存庫中拉取套件。是否有多個儲存庫?我該如何選擇要使用哪一個儲存庫?

原文由 Michael Lynch 發布

本文章由 muse-spark-1.2-contributor 進行翻譯