使用 mkosi 構建 RHEL 和 RHEL UBI 鏡像
mkosi 是一個輕量級工具,用于從發行版軟件包構建鏡像。
mkosi 特性
mkosi 支持一些輸出格式,但最重要的是 可發現磁盤鏡像Discoverable Disk Images(DDI)。同一個 DDI 可用于引導容器、或運行在虛擬機、抑或是復制到 U 盤以引導真實物理機,然后從 U 盤復制到磁盤以引導系統。該鏡像具有標準化的布局和描述其用途的元數據。
mkosi 依賴其他工具來完成大部分工作:使用 systemd-repart
在磁盤鏡像上創建分區,使用 mkfs.btrfs
/ mkfs.ext4
/ mkfs.xfs
等創建文件系統,并使用 dnf
/ apt
/ pacman
/ zypper
下載和解壓包。
mkosi 支持一系列發行版:Debian 和 Ubuntu、Arch Linux、openSUSE,當然還包括
Fedora、CentOS Stream 及其衍生版本,以及最近的 RHEL UBI 和 RHEL
。由于實際的“重活”是由其他工具完成的,mkosi
可以進行交叉構建。這意味著可以使用一個發行版構建各種其他發行版的鏡像。唯一的要求是主機上安裝了相應的工具。Fedora 有原生的 apt
、pacman
和 zypper
,因此它為使用 mkosi 構建任何其他發行版提供了良好的基礎。
它還有一些有趣的功能:鏡像可以由非特權用戶創建,或者在沒有設備文件的容器中創建,特別是沒有對回環設備的訪問權限。它還可以在沒有特權的情況下將這些鏡像啟動為虛擬機(使用 qemu
)。
配置是聲明性的,非常容易創建。使用 systemd-repart
創建磁盤分區,并使用 repart.d
配置文件定義應該如何完成此操作。
有關更多詳細信息,請參見 Daan DeMeyer 在 All Systems Go 大會上的兩個演講:《systemd-repart: Building Discoverable Disk Images》 和 《mkosi: Building Bespoke Operating System Images》。
項目目標
mkosi 的一個目標是允許對軟件項目進行針對不同發行版的測試。它將為一個發行版創建一個鏡像(使用該發行版的軟件包),然后將軟件項目編譯并安裝到該鏡像中,插入不屬于軟件包的額外文件。但是,首個階段,即從軟件包創建鏡像的過程,本身就是有用的。這是我們將首先展示的內容。
我們 [1] 最近添加了對 RHEL 和 RHEL UBI 的支持。讓我們從 RHEL UBI 開始,利用發行版軟件包創建鏡像。
請注意,下面的示例要求 mkosi 19,而且不適用于更早的版本。
帶有 Shell 的基本 RHEL UBI 鏡像
$ mkdir -p mkosi.cache
$ mkosi \
-d rhel-ubi \
-t directory \
-p bash,coreutils,util-linux,systemd,rpm \
--autologin
上面的命令指定了發行版 rhel-ubi
,輸出格式 directory
,并請求安裝軟件包 bash
、coreutils
、…、rpm
。rpm
通常不需要放到鏡像內部,但在這里用于內省會很有用。我們還啟用了以 root 用戶自動登錄。
在啟動構建之前,我們創建了緩存目錄 mkosi.cache
。當存在緩存目錄時,mkosi 會自動使用它來持久化下載的 RPM 包。這將使相同軟件包集合的后續調用速度更快。
然后,我們可以使用 systemd-nspawn
將此鏡像作為容器啟動:
$ sudo mkosi \
-d rhel-ubi \
-t directory \
boot
systemd 252-14.el9_2.3 running in system mode (+PAM +AUDIT +SELINUX -APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS -FIDO2 +IDN2 -IDN -IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK +XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified)
Detected virtualization systemd-nspawn.
Detected architecture x86-64.
Detected first boot.
Red Hat Enterprise Linux 9.2 (Plow)
...
[ OK ] Created slice Slice /system/getty.
[ OK ] Created slice Slice /system/modprobe.
[ OK ] Created slice User and Session Slice.
...
[ OK ] Started User Login Management.
[ OK ] Reached target Multi-User System.
Red Hat Enterprise Linux 9.2 (Plow)
Kernel 6.5.6-300.fc39.x86_64 on an x86_64
image login: root (automatic login)
[root@image ~]# rpm -q rpm systemd
rpm-4.16.1.3-22.el9.x86_64
systemd-252-14.el9_2.3.x86_64
正如前面提到的,此鏡像可以用于啟動虛擬機。但在此設置下,這是不可能的 —— 我們的鏡像沒有內核。事實上,RHEL UBI 根本不提供內核,因此我們無法使用它進行引導(無論是在虛擬機上還是在裸機上)。
創建鏡像
我一開始說是要創建鏡像,但到目前為止我們只有一個目錄。讓我們開始實際創建一個鏡像:
$ mkosi \
-d rhel-ubi \
-t disk \
-p bash,coreutils,util-linux,systemd,rpm \
--autologin
這將生成 image.raw
,一個帶有 GPT 分區表和單個根分區(用于本機架構)的磁盤鏡像。
$ sudo systemd-dissect image.raw
Name: image.raw
Size: 301.0M
Sec. Size: 512
Arch.: x86-64
Image UUID: dcbd6499-409e-4b62-b251-e0dd15e446d5
OS Release: NAME=Red Hat Enterprise Linux
VERSION=9.2 (Plow)
ID=rhel
ID_LIKE=fedora
VERSION_ID=9.2
PLATFORM_ID=platform:el9
PRETTY_NAME=Red Hat Enterprise Linux 9.2 (Plow)
ANSI_COLOR=0;31
LOGO=fedora-logo-icon
CPE_NAME=cpe:/o:redhat:enterprise_linux:9::baseos
HOME_URL=https://www.redhat.com/
DOCUMENTATION_URL=https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9
BUG_REPORT_URL=https://bugzilla.redhat.com/
REDHAT_BUGZILLA_PRODUCT=Red Hat Enterprise Linux 9
REDHAT_BUGZILLA_PRODUCT_VERSION=9.2
REDHAT_SUPPORT_PRODUCT=Red Hat Enterprise Linux
REDHAT_SUPPORT_PRODUCT_VERSION=9.2
Use As: ? bootable system for UEFI
? bootable system for container
? portable service
? initrd
? sysext extension for system
? sysext extension for initrd
? sysext extension for portable service
RW DESIGNATOR PARTITION UUID PARTITION LABEL FSTYPE ARCHITECTURE VERITY GROWFS NODE PARTNO
rw root 1236e211-4729-4561-a6fc-9ef8f18b828f root-x86-64 xfs x86-64 no yes /dev/loop0p1 1
好的,我們現在有一個鏡像,鏡像中包含了一些來自 RHEL UBI 軟件包的內容。我們如何在其上加點我們自己的東西呢?
使用自己的文件擴展鏡像
有幾種方法可以擴展鏡像,包括從頭開始編譯某些東西。但在那之前,讓我們做一些更簡單的事情,將一個現成的文件系統注入到鏡像中:
$ mkdir -p mkosi.extra/srv/www/content
$ cat >mkosi.extra/srv/www/content/index.html <<'EOF'
<h1>Hello, World!</h1>
EOF
現在,該鏡像將包含 /srv/www/content/index.html
。
這種方法用于注入額外的配置或簡單的程序。
從源代碼構建
現在讓我們過一遍完整流程,從源代碼構建一些東西。例如,一個簡單的 Meson 項目,有一個單獨的 C 文件:
$ cat >hello.c <<'EOF'
#include <stdio.h>
int main(int argc, char **argv) {
char buf[1024];
FILE *f = fopen("/srv/www/content/index.html", "re");
size_t n = fread(buf, 1, sizeof buf, f);
fwrite(buf, 1, n, stdout);
fclose(f);
return 0;
}
EOF
$ cat >meson.build <<'EOF'
project('hello', 'c')
executable('hello', 'hello.c',
install: true)
EOF
$ cat >mkosi.build <<'EOF'
set -ex
mkosi-as-caller rm -rf "$BUILDDIR/build"
mkosi-as-caller meson setup "$BUILDDIR/build" "$SRCDIR"
mkosi-as-caller meson compile -C "$BUILDDIR/build"
meson install -C "$BUILDDIR/build" --no-rebuild
EOF
$ chmod +x mkosi.build
總結一下:我們有一些源代碼(hello.c
),一個構建系統配置文件(meson.build
),以及一個由 mkosi 調用的膠水腳本(mkosi.build
)。對于實際的項目,也會有相同的元素,只是更加復雜。
這個腳本需要一些解釋。mkosi 在創建鏡像時使用用戶命名空間。這允許包管理器(例如 dnf
)安裝由不同用戶擁有的文件,即使它是由一個普通非特權用戶調用的。我們使用 mkosi-as-caller
切換回調用者以進行編譯。這樣,在 $BUILDDIR
下編譯期間創建的文件將由調用者擁有。
現在讓我們使用我們的程序構建鏡像。與之前的調用相比,我們需要額外的軟件包:meson
、gcc
。由于我們現在有了構建腳本,mkosi 將執行兩個構建階段:首先創建一個構建鏡像,并在其中調用構建腳本,將安裝產物存儲在一個臨時目錄中,然后構建最終鏡像,并將安裝產物注入其中。(mkosi 設置 $DESTDIR
,meson install
自動使用 $DESTDIR
,因此并不需要我們明確指定。)
$ mkosi \
-d rhel-ubi \
-t disk \
-p bash,coreutils,util-linux,systemd,rpm \
--autologin \
--build-package=meson,gcc \
--build-dir=mkosi.builddir \
--build-script=mkosi.build \
-f
此時,我們有了帶有自定義載荷的鏡像 image.raw
。我們可以啟動我們新創建的可執行文件作為 shell 命令:
$ sudo mkosi -d rhel-ubi -t directory shell hello
<h1>Hello, World!</h1>
獲取 RHEL 的開發者訂閱
RHEL UBI 主要用作容器構建的基礎層。它提供了有限的軟件包(約 1500 個)。現在讓我們切換到完整的 RHEL 安裝。
獲取 RHEL 的最簡單方法是使用 開發者許可證。它提供了權限注冊 16 個運行 RHEL 的物理或虛擬節點,并提供自助式支持。
首先,創建一個賬戶。然后,轉到 管理頁面 并確保啟用了“用于 Red Hat 訂閱管理的簡化內容訪問”。接下來,創建一個新的激活密鑰,選擇 “Red Hat 個人開發者訂閱”。記下顯示的組織 ID。在下面,我們將使用密鑰名稱和組織 ID 分別表示為 $KEY_NAME
和 $ORGANIZATION_ID
。
現在,我們準備使用 RHEL 內容:
$ sudo dnf install subscription-manager
$ sudo subscription-manager register \
--org $ORGANIZATION_ID --activationkey $KEY_NAME
使用 RHEL 構建鏡像
在之前的示例中,我們通過參數開關指定了所有配置。這對于快速開發很友好,但可能在情況復雜時變得難以處理。RHEL 是一個嚴肅的發行版,所以讓我們改為使用配置文件:
$ cat >mkosi.conf <<'EOF'
[Output]
Format=directory
Output=rhel-directory
[Distribution]
Distribution=rhel
[Content]
Packages=
bash
coreutils
util-linux
systemd
systemd-boot
systemd-udev
kernel-core
Bootable=yes
Bootloader=uki
Autologin=yes
WithDocs=no
EOF
首先,讓我們檢查一下一切是否正常:
$ mkosi summary
現在讓我們構建鏡像(呃,或者說,目錄):
$ mkosi build
$ mkosi qemu
Welcome to Red Hat Enterprise Linux 9.2 (Plow)!
[ OK ] Created slice Slice /system/modprobe.
[ OK ] Reached target Initrd Root Device.
[ OK ] Reached target Initrd /usr File System.
[ OK ] Reached target Local Integrity Protected Volumes.
[ OK ] Reached target Local File Systems.
[ OK ] Reached target Path Units.
[ OK ] Reached target Remote Encrypted Volumes.
[ OK ] Reached target Remote Verity Protected Volumes.
[ OK ] Reached target Slice Units.
[ OK ] Reached target Swaps.
...
[ OK ] Listening on Journal Socket.
[ OK ] Listening on udev Control Socket.
[ OK ] Listening on udev Kernel Socket.
...
Red Hat Enterprise Linux 9.2 (Plow)
Kernel 5.14.0-284.30.1.el9_2.x86_64 on an x86_64
localhost login: root (automatic login)
[root@localhost ~]#
很好,我們將“鏡像”構建為一個帶有文件系統樹的目錄,并在虛擬機中引導了它。
在引導的虛擬機中,findmnt /
顯示根文件系統是 virtiofs。這是一個虛擬文件系統,將主機的目錄暴露給客戶機。我們其實也可以構建一個更傳統的鏡像,其中包含文件系統和文件內的分區表,但“目錄 + virtiofs” 對于開發來說更快且更友好。
我們剛剛引導的鏡像未注冊。要允許從鏡像“內部”下載更新,我們必須將 yum
、subscription-manager
和 NetworkManager
添加到軟件包列表,并在下載任何更新之前以與上述相同的方式調用 subscription-manager
。在這之后,我們在基本倉庫中就有大約 4500 個軟件包可用,并且還有一些包含更多專業軟件包的額外倉庫。
最后
今天就是這些了。如果您有問題,可以在 Matrix 上找到我們,地址為 #mkosi:matrix.org,或者在 systemd 郵件列表 上找到我們。