Unikernel アプリケーションの開発ツール Unikraft を使ってみる
Unikraft とは
Unikraft は unikernel 環境上でアプリケーションを動作させるための一連の開発キットです。Linux Foundation と Xen プロジェクトに属するオープンソースとして公開されています。
特徴
プロジェクトのトップページに書いてあるとおり以下の特徴があります。
- 高性能かつ軽量な unikernel 環境が簡単に作成可能
- 仮想マシンのようなセキュアに分離された環境でコンテナのように軽量かつ高速にアプリケーションを実行できる
- Docker などの主流なコンテナ管理ツールとの統合
- Linux API との互換性
unikernel を管理するツールは他にもいくつかあるようですが、unikraft では特に以下を実現することを目標にしています。
- 従来の VM とコンテナのメリットを両方取り入れ、VM のようにセキュアかつ高い分離性を維持しながらコンテナの高速・軽量さを兼ね備えたアプリケーション実行基盤 (unikernel) を作成する。
- ユーザーが高度な知識を必要とせず、unikernel 環境上で動作するアプリケーションのビルド、デプロイ作業を簡略化するための開発ツールを提供する。
このあたりは以下の concept に記載されています。
VM のようなセキュアな環境とコンテナの軽量性を両立させるというコンセプトは以前に紹介した hyperlight や kata container といったプロジェクトと同様であり、広い目で見れば unikraft もこのカテゴリに分類されると言ってもいいかもしれません。
また unikraft では docker コマンドと同様に kraft run
や kraft logs
といったコマンドで unikernel アプリケーションを操作できるようになっており、既に docker コマンドを使い慣れている人はシームレスに移行できるように設計されています。Ease of use, including integration with Dockerfiles and other mainstream tools. とあるようにこの辺の互換性もアピールポイントになってそうです。
その他、python や go, rust などいろいろな言語やフレームワークで書かれたアプリケーションをパッケージ化して unikernel 上で動作させることにも対応しています。実行基盤に依存せずにアプリケーションを実行できるという点はコンテナの性質を引き継いでいると言えます。
動作について
ざっくり説明すると unikraft では Qemu, Xen, Firecracker といった VMM を利用して unikernel 用にビルドされたアプリケーションを管理する仕組みになっています。
unikraft の仕組み。(d) が unikraft に対応。Concept より引用
VMM を使うことでハイパーバイザー型 VM と同レベルの分離性を実現し、アプリケーションは unikernel に組み込んで実行することでコンテナ並みの高速性・軽量性を実現する構成になっています。unikraft はこのためのアプリケーションのビルドや VMM でアプリケーションを管理するためのインターフェイス(CLI)部分、加えて unikernel 上でアプリケーションを動かすためのライブラリ等で構成されています。このあたりの詳細な話は concept や internal を参照。
検証環境
今回は Openstack 上に作った以下の環境で unikraft の動作を検証していきます。
- OS: ubuntu 24.03
- メモリ: 4 GB
- vCPU: 4
- ネスト仮想化有効
ハードウェア要件等は確認できませんでしたが、unikraft は軽量であることをアピールしているのでそれほど大きいリソースは必要ないと思われます。
事前準備として qemu と docker をインストールしておきます。
kraft コマンドのインストール
unikraft では基本的に CLI (kraft) を使って unikernel アプリケーションのビルドや実行を行います。https://unikraft.org/docs/cli/install にいろいろなインストール方法が記載されていますが、以下のコマンドだと依存関係もまとめてインストールされるので楽です。
curl -sSfL https://get.kraftkit.sh | sh
指示に従っていくと kraft コマンドや依存関係パッケージがインストールされます。kraft を実行して以下のような表示が出れば ok。
$ kraft
.
/^\ Build and use highly customized and ultra-lightweight unikernels.
:[ ]:
| = | Version: 0.11.6
/|/=\|\ Documentation: https://unikraft.org/docs/cli
(_:| |:_) Issues & support: https://github.com/unikraft/kraftkit/issues
v v Platform: https://unikraft.cloud
' '
USAGE
kraft [FLAGS] SUBCOMMAND
...
unikernel サンプルアプリの実行
はじめのステップとして、以下のドキュメントを参照に kraft コマンドを使って簡単なアプリケーションを実行していきます。
helloworld
unikraft では helloworld や nginx といったいくつかの基本的なアプリケーションは事前にパッケージ化されたアプリケーションカタログとして公開されています。ソースコードは以下の unikraft/catalog リポジトリで公開されているので自分でビルドすることも可能です。
アプリケーションカタログのアプリは unikraft.org/nginx:1.15
のように docker に似た形式で事前にアプリケーションレジストリにアップロード済みのため、わざわざ自分でビルドしなくても手軽にローカル環境からイメージを取得・利用できるようになっています。例えば、以下のコマンドではアプリケーションレジストリから事前にビルド済みの helloworld unikernel アプリケーションを取得して実行できます。
kraft run -W unikraft.org/helloworld
本来は上記で実行できるはずですが手元の環境では qemu 関連のエラーが発生してうまくいかなかったので、ここでは catalog リポジトリのソースコードから自前でビルドする方法を試してみます。
まずは catalog リポジトリを clone.
git clone https://github.com/unikraft/catalog.git
アプリケーションレジストリにアップロードされているアプリケーションは library
にあるので移動。
cd library/helloworld
イメージ作成に必要なファイルは用意済みなので kraft build
コマンドで作成できます。
kraft build を実行するとどの VMM、arch 向けにカーネルをビルドするか(ビルドターゲット)を聞かれるので qemu/x64_64
を選択。
$ kraft build
[?] select target:
▸ helloworld (fc/x86_64)
helloworld (qemu/arm64)
helloworld (qemu/x86_64)
helloworld (xen/x86_64)
これにより unikernel のビルドが実行され、しばらく待つと .unikraft/build/helloworld_qemu-x86_64
に unikernel カーネルイメージが生成されます。
$ kraft build
[?] select target: helloworld (qemu/x86_64)
[+] updating index... done! [11.1s]
[+] finding core/unikraft:stable... done! [0.0s]
[+] pulling core/unikraft:stable ••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• 100% [1.3s]
[+] configuring helloworld (qemu/x86_64) ••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• 100% [0.9s]
[+] building helloworld (qemu/x86_64) ••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• 100% [3.2s]
[●] Build completed successfully!
│
└─ kernel: .unikraft/build/helloworld_qemu-x86_64 (242 kB)
Learn how to package your unikernel with: kraft pkg --help
イメージを作成したら kraft run -W .
で unikernel 上で動作する helloworld アプリケーションが実行されます。
$ kraft run -W .
i using arch=x86_64 plat=qemu
Powered by
o. .o _ _ __ _
Oo Oo ___ (_) | __ __ __ _ ' _) :_
oO oO ' _ `| | |/ / _)' _` | |_| _)
oOo oOO| | | | | (| | | (_) | _) :_
OoOoO ._, ._:_:_,\_._, .__,_:_, \___)
Kiviuq 0.20.0~5a22d73
Hello from Unikraft!
This message shows that your installation appears to be working correctly.
For more examples and ideas, visit: https://unikraft.org/docs/
このように run コマンドで実行される unikernel 上で動作するアプリケーション
のことをドキュメント上では unikernel インスタンス と表記しているため、以降はこの表記で統一します。docker コマンドと比較すると、docker run
でコンテナを作成するのと同様に kraft run
で unikernel インスタンスを作成できるようになっています。
nginx
これだけではあまり面白みがないので次は nginx も試してみます。nginx のイメージに対応するコードは library/nginx/[nginx_version]
にあるので移動。
cd ../nginx/1.25
nginx のビルド実行時には buildkit を使って同ディレクトリに存在する Dockerfile からビルドが行われます。接続可能な buildkitd が起動していない場合は一時的な buildkitd コンテナが作成されますが、やや時間がかかるのであらじめコンテナを起動して通信先に設定することが推奨されています。
docker run -d --name buildkitd --privileged moby/buildkit:latest
export KRAFTKIT_BUILDKIT_HOST=docker-container://buildkitd
ビルドを実行。platform、arch は事前にオプションとしても指定できます。
$ kraft build --plat qemu --arch x86_64
nginx は unikernel インスタンス内の port 80 で待ち受けるので、ホスト側の port 8080 にアクセスしたらルーティングされるようにポートマッピングを指定します。docker コマンドと同様に -p [ホスト側ポート:インスタンス側ポート]
でポートマッピングが指定できます。
$ kraft run -W -p 8080:80
i using arch=x86_64 plat=qemu
en1: Added
en1: Interface is up
Powered by Unikraft Kiviuq (0.20.0~5a22d73)
en1: Set IPv4 address 10.0.2.15 mask 255.255.255.0 gw 10.0.2.2
別ターミナルを開いてホスト側の port 8080 にアクセスすると unikernel 上の nginx に接続されてレスポンスが返ってきます。
$ curl 0.0.0.0:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
docker と同様に稼働中の unikernel インスタンスは kraft ps
で確認できます。
$ kraft ps
NAME KERNEL ARGS CREATED STATUS MEM PORTS PLAT
xenodochial_ham project://nginx:qemu/x86_64 /usr/bin/nginx 1 minute ago running 64M 0.0.0.0:8080->80/tcp qemu/x86_64
ちなみに kraft コマンドでは --log-level [log_level]
、もしくは環境変数 KRAFTKIT_LOG_LEVEL
でログレベルを変更できます。例えば debug に指定するとホスト側で実行されている qemu プロセスの詳細を確認できます。
$ kraft run --log-level debug -W -p 8080:80
D kraftkit version=0.11.6
D determining how to proceed given provided input and context
D using compatible context candidate=kraftfile-unikraft
i using arch=x86_64 plat=qemu
D qemu-system-x86_64 -version
D qemu-system-x86_64 -accel help
D qemu-system-x86_64 -append /usr/bin/nginx -cpu qemu64,+pdpe1gb,+rdrand,-vmx,-svm -daemonize -device virtio-net-pci,mac=02:b0:b0:92:ca:01,netdev=hostnet0 -device pvpanic -device sga -display none -kernel /home/ubuntu/catalog/library/nginx/1.25/.unikraft/build/nginx_qemu-x86_64 -machine pc -m size=64M -monitor unix:/home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/mon.sock,server,nowait -name fe81b0bfb8f3 -netdev user,id=hostnet0,hostfwd=tcp::8080-:80 -nographic -no-reboot -S -parallel none -pidfile /home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/machine.pid -qmp unix:/home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/ctrl.sock,server,nowait -qmp unix:/home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/evnt.sock,server,nowait -rtc base=utc -serial file:/home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/vm.log -smp cpus=1,threads=1,sockets=1 -vga none
en1: Added
en1: Interface is up
Powered by Unikraft Kiviuq (0.20.0~5a22d73)
en1: Set IPv4 address 10.0.2.15 mask 255.255.255.0 gw 10.0.2.2
前述の通り unikraft では VMM を使って unikernel の管理を行うため、kraft run
コマンドを実行するとホスト側で qemu などの VMM のプロセスが起動し、これが unikernel インスタンスの実体となっています。ps コマンドを実行すると unikernel インスタンスに対応する qemu のプロセスが確認できます。
$ ps aux | grep qemu
ubuntu 95017 1.3 2.9 882184 118428 ? Sl 08:33 0:00 qemu-system-x86_64 -append /usr/bin/nginx -cpu qemu64,+pdpe1gb,+rdrand,-vmx,-svm -daemonize -device virtio-net-pci,mac=02:b0:b0:92:ca:01,netdev=hostnet0 -device pvpanic -device sga -display none -kernel /home/ubuntu/catalog/library/nginx/1.25/.unikraft/build/nginx_qemu-x86_64 -machine pc -m size=64M -monitor unix:/home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/mon.sock,server,nowait -name fe81b0bfb8f3 -netdev user,id=hostnet0,hostfwd=tcp::8080-:80 -nographic -no-reboot -S -parallel none -pidfile /home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/machine.pid -qmp unix:/home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/ctrl.sock,server,nowait -qmp unix:/home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/evnt.sock,server,nowait -rtc base=utc -serial file:/home/ubuntu/.local/share/kraftkit/runtime/fe81b0bfb8f3/vm.log -smp cpus=1,threads=1,sockets=1 -vga none
unikernel インスタンス上で実行されるアプリケーション(unikernel)は kraft build
実行時に .unikraft/build/
に生成されます。nginx の場合の中身を見てみると様々なライブラリなどが生成されていますが、その中に指定した platform や arch に対応した実行ファイル nginx_qemu-x86_64
も生成されていることが確認できます。
ls -1 .unikraft/build/
...
nginx_qemu-x86_64
nginx_qemu-x86_64.bootinfo
nginx_qemu-x86_64.bootinfo.cmd
nginx_qemu-x86_64.cmd
nginx_qemu-x86_64.dbg
nginx_qemu-x86_64.dbg.cmd
nginx_qemu-x86_64.dbg.gdb.py
nginx_qemu-x86_64.multiboot.cmd
provided_syscalls.in.cmd
provided_syscalls.in.new.cmd
uk-gdb.py
qemu プロセスでは nginx_qemu-x86_64
が -kernel
に渡されているわけですが、アプリケーションである nginx はどこで定義されているかというと同ディレクトリの Dockerfile に記載されています。マルチステージビルドとなっており、最終的に scratch をベースイメージとして実行ファイル、ログファイル、設定ファイル、依存関係のライブラリなど nginx の動作に必要なファイルをイメージ内にコピーしています。
FROM nginx:1.25.3-bookworm AS build
# These are normally syminks to /dev/stdout and /dev/stderr, which don't
# (currently) work with Unikraft. We remove them, such that Nginx will create
# them by hand.
RUN rm /var/log/nginx/error.log
RUN rm /var/log/nginx/access.log
FROM scratch
# Nginx binaries, modules, configuration, log and runtime files
COPY /usr/sbin/nginx /usr/bin/nginx
COPY /usr/lib/nginx /usr/lib/nginx
COPY /etc/nginx /etc/nginx
COPY /etc/passwd /etc/passwd
COPY /etc/group /etc/group
COPY /var/log/nginx /var/log/nginx
COPY /var/cache/nginx /var/cache/nginx
COPY /var/run /var/run
# Libraries
COPY /lib/x86_64-linux-gnu/libcrypt.so.1 /lib/x86_64-linux-gnu/libcrypt.so.1
COPY /lib/x86_64-linux-gnu/libpcre2-8.so.0 /lib/x86_64-linux-gnu/libpcre2-8.so.0
COPY /lib/x86_64-linux-gnu/libssl.so.3 /lib/x86_64-linux-gnu/libssl.so.3
COPY /lib/x86_64-linux-gnu/libcrypto.so.3 /lib/x86_64-linux-gnu/libcrypto.so.3
COPY /lib/x86_64-linux-gnu/libz.so.1 /lib/x86_64-linux-gnu/libz.so.1
COPY /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6
COPY /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2
COPY /etc/ld.so.cache /etc/ld.so.cache
# Custom configuration files, including using a single process for Nginx
COPY ./conf/nginx.conf /etc/nginx/nginx.conf
# Web root
COPY ./wwwroot /wwwroot
Kraftfile
unikraft ではどのように unikernel をビルドするか、rootfs はどこから持ってくるかなどの情報は Kraftfile と呼ばれる yaml ファイルに記載します。docker で Dockerfile を用意しておけば常に同じイメージがビルドできるのと同様に、unikraft では Kraftfile を使ってアプリケーションのビルド、パッケージ化、デプロイを管理します。Kraftfile の仕様は以下を参照。
アプリケーションカタログ
アプリケーションカタログのリポジトリの examples ディレクトリ には go の http server や python の flask, rust の http server などいろいろな言語のサンプルアプリケーションコードが準備されており、自分でアプリケーションを作る際の参考になります。試しに python アプリケーションの動作を見てみましょう。
上記のディレクトリでは python の Flask + sqlite 構成の unikernel インスタンスを実行するサンプルコードになっています。まずはディレクトリに移動。
cd catalog/examples/flask3.0-python3.12-sqlite3
ビルドする前にディレクトリ内に含まれるファイルの構成を見てみると unikraft に関するファイルは Kraftfile のみであり、その他はすべて通常の flask や sqlite で使用するファイルとなっていることがわかります。
$ tree
.
├── Dockerfile
├── Kraftfile
├── README.md
├── init_db.py
├── manifest.json
├── requirements.txt
├── schema.sql
├── server.py
├── static
│ └── css
│ └── style.css
└── templates
├── base.html
├── create.html
├── edit.html
├── index.html
└── post.html
Dockerfile は scratch をベースに必要なライブラリのみコピーしている構成です。
FROM python:3.12.11-bookworm AS build
WORKDIR /app
COPY requirements.txt /app/
RUN pip3 install -r requirements.txt --no-cache-dir
COPY . /app/
RUN python3 init_db.py
FROM scratch
# SQLite library
COPY /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0
# Python libraries
COPY /usr/local/lib/python3.12 /usr/local/lib/python3.12
# Application files
COPY /app /app
Kraftfile もシンプルな構成。python3.12 をベースに Dockerfile から rootfs を作成し、unikernel 上では /app/server.py を実行します。
spec: v0.6
name: flask3.0-python3.12-sqlite3
runtime: python:3.12
rootfs: ./Dockerfile
cmd: ["/usr/bin/python3", "/app/server.py"]
Kraftfile では runtime に python:3.12
が指定されていますが、検証環境で実行するとやはり qemu 実行時にエラーが出て起動しないのでこちらも手元でビルドして準備します。
python 3.12 に対応するコードは catalog/library/python/3.12
にあるので移動してビルド。
cd ~/catalog/library/python/3.12
kraft build
ビルドしたイメージは kraft pkg
コマンドでパッケージにすることで他のプロジェクトでも利用できます。名前は何でもいいので myapp/python:3.12
を指定。
kraft pkg --as oci --name myapp/python:3.12
作成したパッケージは kraft pkg ls --apps
で確認できます。
$ kraft pkg ls --apps
TYPE NAME VERSION FORMAT CREATED UPDATED PULLED MANIFEST INDEX PLAT SIZE
app myapp/python 3.12 oci 44 seconds ago 43 seconds ago 43 seconds ago 0bd620f 6acad99 qemu/x86_64 71 MB
もとの flask + sqlite のディレクトリに戻り、Kraftfile
の runtime を作ったものに置き換えます。
- runtime: python:3.12
+ runtime: myapp/python:3.12
unikernel インスタンスを実行。
$ kraft run -W --rm -p 8080:8080 --plat qemu --arch x86_64 -M 512M
i using arch=x86_64 plat=qemu
[+] building rootfs via Dockerfile... done! x86_64 [2.7s]
en1: Added
en1: Interface is up
Powered by Unikraft Kiviuq (0.20.0~5a22d73)
* Serving Flask app 'server'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8080
* Running on http://0.0.0.0:8080
Press CTRL+C to quit
en1: Set IPv4 address 10.0.2.15 mask 255.255.255.0 gw 10.0.2.2
ブラウザで http://[ホストip]:8080
より flask app にアクセスできます。
unikraft は Docker との統合を特徴にしているので、このようにアプリケーション用に Dockerfile を用意すれば比較的容易に unikraft インスタンス上で実行できるようになっています。つまり、アプリケーションが既にコンテナ化されているのであればあまり労力をかけずに unikernel インスタンスに統合できると言えます。もちろん Dockerfile の修正や unikernel での動作確認などは必要になりますが。
ちなみに unikernel インスタンスはコンテナではないので、 docker exec のようにインスタンス環境でシェルを起動してアタッチしてデバッグ、のようなことはできない点に注意。
ビルドの裏側で起こっていること
library ディレクトリ以下の nginx などのコードと example ディレクトリ以下のコードを実際にビルドしてみると、生成される成果物のファイル数などに差があることに気がつきます。これらの差はどこから発生しているのでしょうか。
2 つのビルド中に起こっていることについては以下のガイドで説明されているため、この内容に沿ってどのような差が発生しているのか見ていきます。
アプリケーションカタログの library/nginx/1.25 のビルドを実行すると .unikraft/build
以下に生成物が作成されますが、この中にはカーネルイメージに対応する nginx_qemu-x86_64
や、unikernel 上のルートファイルシステムに対応する cpio アーカイブ initramfs-x86_64.cpio
が含まれています (initramfs)。
$ kraft build --plat qemu --arch x86_64
[●] Build completed successfully!
│
├──── kernel: .unikraft/build/nginx_qemu-x86_64 (15 MB)
└─ initramfs: .unikraft/build/initramfs-x86_64.cpio (14 MB)
$ .unikraft/build/nginx_qemu-x86_64
.unikraft/build/nginx_qemu-x86_64: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped
$ .unikraft/build/initramfs-x86_64.cpio
.unikraft/build/initramfs-x86_64.cpio: ASCII cpio archive (SVR4 with no CRC)
ガイドによると nginx の kraft build
実行時には以下の手順でカーネルファイルが生成されるとのことです。
- Kraftfile から rootfs に指定したパスを読み取ってルートファイルシステムを生成する。nginx の場合は Dockerfile から rootfs を生成する。
- rootfs を initial ramdisk (initrd) にパックする。
- Kraftfile 内のカーネル設定などを元にカーネルをビルドする。
- 出力されたカーネルファイルに initrd を埋め込む。
上記の過程と生成物を比較すると、initramfs-x86_64.cpio
が inital ramdisk、nginx_qemu-x86_64
が最終的に initrd が埋め込まれたカーネルイメージ(unikernel + nginx)に対応していると思われます。
そして kraft run を実行した際の qemu に渡されているオプションを見ると、kernel
オプションにこのカーネルイメージが渡されていることが確認できます。
qemu-system-x86_64 \
-append /usr/bin/nginx \
...
-kernel /home/ubuntu/catalog/library/nginx/1.25/.unikraft/build/nginx_qemu-x86_64 \
一方で examples 以下のサンプルアプリケーション(例えば httpserver-go1.21 など)に対して kraft build
を実行すると、カーネルイメージはビルドされず initramfs (cpio アーカイブ) のみが生成されます。
$ kraft build --plat qemu --arch x86_64
[+] building rootfs via Dockerfile... done! x86_64 [3.8s]
[●] Build completed successfully!
│
└─ initramfs: .unikraft/build/initramfs-x86_64.cpio (10 MB)
Learn how to package your unikernel with: kraft pkg --help
この違いは Kraftfile 内の runtime に起因しています。runtime で既存の unikernel イメージを指定しているため、ビルドではカーネルイメージの生成は実行されず rootfs のみが Dockerfile から生成されるようになっています(以下の例では手元でビルドした mytest に置き換えてますが)。
spec: v0.6
name: httpserver-go1.21
runtime: mytest:base
rootfs: ./Dockerfile
cmd: ["/server"]
kraft run の qemu を確認すると、-kernel
に加えて -initrd
オプションにビルドされた cpio アーカイブが渡されていることがわかります。
qemu-system-x86_64 \
-append "vfs.fstab=[ \"initrd0:/:extract:::\" ] env.vars=[
\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\" ] -- /server" \
-initrd .unikraft/build/initramfs-x86_64.cpio \
-kernel /tmp/kraft-run-166630483/unikraft/bin/kernel \
このときのカーネルイメージは runtime
に指定したカーネルイメージが使用され、実行時にローカル環境の /tmp/kraft--run-[ランダムな文字]/unikraft/bin/kernel
にダウンロードされて qemu に渡されます。今回はローカル環境でビルドした mytest:base
を指定しているので、sha256sum でハッシュを比較すると一致していることが確認できます。
$ sha256sum ~/catalog/library/base/.unikraft/build/base_qemu-x86_64
a71c0669a7fdb205e581fdef1b797fc7604765fc930ec24adc607e81d5b50d01 /home/ubuntu/catalog/library/base/.unikraft/build/base_qemu-x86_64
$ sha256sum /tmp/kraft-run-166630483/unikraft/bin/kernel
a71c0669a7fdb205e581fdef1b797fc7604765fc930ec24adc607e81d5b50d01 /tmp/kraft-run-166630483/unikraft/bin/kernel
また、カーネルイメージは runtime のものが使用されるのでこのコードでは kraft build
を実行する必要がなく、kraft run
で直接 unikernel インスタンスを実行することができます。そして kraft run
実行時には以下の手順が実行されます。
- runtime に指定したカーネルイメージを取得して /tmp に配置。上記の場合は mytest:base
- Dockerfile から rootfs を生成する。
- rootfs を initramfs (initrd) ファイルにパックする。
- qemu に initramfs とカーネルイメージを渡して unikernel インスタンスを起動。
生成された initramfs の中身は以下の cpio
コマンドで確認できます。
$ cpio -itv < .unikraft/build/initramfs-x86_64.cpio
drwxr-xr-x 0 root root 0 Sep 21 14:21 ./lib
drwxr-xr-x 0 root root 0 Sep 21 14:21 ./lib/x86_64-linux-gnu
-rwxr-xr-x 1 root root 1922136 Sep 30 2023 ./lib/x86_64-linux-gnu/libc.so.6
drwxr-xr-x 0 root root 0 Sep 21 14:21 ./lib64
-rwxr-xr-x 1 root root 210968 Sep 30 2023 ./lib64/ld-linux-x86-64.so.2
-rwxr-xr-x 1 root root 8005392 Sep 21 14:21 ./server
19804 blocks
そのため、example 以下のサンプルコードではアプリケーションを含む rootfs はカーネルイメージに含まれているわけではなく、unikernel インスタンス実行時に動的に渡されてロードされていると推測されます。
unikernel ではカーネルイメージ内にアプリケーションコードも組み込まれている構成が一般的ですが、unikraft のファイルシステムの設計では上記のように実行時に外部のファイルシステムを動的に読み込むことができるようになっています。unikraft におけるファイルシステムの仕組みは次でもう少し詳しく見ていきます。
unikraft のファイルシステム
※以下の記事の要約に該当。
通常の OS では /
にルートファイルシステムが必要となりますが、unikraft では動作に必要なライブラリがコンポーネント化されているため、アプリケーションがファイルの読み書きを必要としないのであれば必ずしもルートファイルシステムを設定する必要はありません。とはいえ多くの場合何らかのファイルの読み書きは発生するので、unikernel インスタンスにはルートファイルシステムが必要といえます。パフォーマンスを最適化しつつ上記のニーズに答えるため、 unikraft では以下の 3 つのルートファイルシステム形式に対応しています(4 つ目は 3 つの複合なので割愛)
- Initial Ramdisk Filesystem (initramfs)
- Embedded Initial Ramdisk Filesystems (einitrds)
- External Volumes
unikernel バイナリアプリケーションとルートファイルシステムの組み合わせのバリエーション。Filesystem より引用
- initramfs
- initramfs はファイルシステムを qemu 実行時の initrd オプションで受け渡す形式です。unikraft ではこれをルートファイルシステムとして扱うことができます。ファイルシステムですが unikraft ではメモリ内で保持されるため、以下の埋め込み initramfs と比較してパフォーマンスに優れるとのこと。
- Embedded initramfs
- embedded initramfs (組み込み initramfs) は unikernel をビルドする際にカーネルイメージにルートファイルシステムを埋め込む形式です。この形式ではカーネルイメージに既にルートファイルシステムが組み込まれているため、qemu で起動する際の initrd に別の initramfs を指定してファイルシステムの一部のみを変更するなどカスタム性に優れているメリットがあります。一方で元のファイルシステムはカーネルに組み込まれているので、変更する場合はカーネルの再ビルドを行う必要があるなど一長一短になってます。
- external volumes
- 外部ボリューム(external volumes) はコンテナの文脈におけるボリューム(ストレージ)と同様に、データの永続化・複数の unikernel インスタンス間でのデータ共有などを目的に使用されます。また、docker コマンドと同様に
-v
でホスト側のディレクトリやファイルを unikernel インスタンス側に設定するといった使い方ができます。
- 外部ボリューム(external volumes) はコンテナの文脈におけるボリューム(ストレージ)と同様に、データの永続化・複数の unikernel インスタンス間でのデータ共有などを目的に使用されます。また、docker コマンドと同様に
kraft run -v ./html:/nginx/html unikraft.org/nginx:1.25
ビルドの裏側で起こっていること
で見た例を考えると、nginx の場合は initramfs をカーネルイメージに埋め込んでいたため qemu -kernel
オプションにカーネルイメージのみが渡されていましたが、go http server の場合はベースのカーネルイメージに加えて -inird
に initramfs が渡されています。ファイルシステムの仕組みを考慮すると、nginx では埋め込み initramfs としてカーネルイメージにファイルシステムが組み込まれており、go http server では base のカーネルイメージを利用してインスタンス起動時に -initrd
で initramfs を渡してファイルシステムの一部を書き換えていると推測されます。
カーネルイメージへのファイルシステムの埋め込み、unikernel インスタンス実行時のファイルシステムの動的設定の両方に対応していると便利なだけでなく、それと同時にビルドプロセスを効率化できるというメリットもあります。実際やってみるとわかりますが、nginx のような runtime が指定されていない unikernel ビルドの場合、 rootfs 作成のための Dockerfile のビルド + カーネルのビルドが実行されるため kraft build
にはそれなりに時間がかかります。例えば time コマンドでビルド時間を計測したところ 46 秒かかりました (docker キャッシュあり)。
一方で example 配下のサンプルコードの kraft build
では Dockerfile のビルドしか行わないのでビルド時間が短縮されます。go http server で同様に計測したところ 3 秒程度で完了しました(docker キャッシュあり)。
# nginx
$ time kraft build --plat qemu --arch x86_64
kraft build --plat qemu --arch x86_64 45.49s user 8.58s system 186% cpu 29.011 total
# go http server
$ time kraft build --plat qemu --arch x86_64
kraft build --plat qemu --arch x86_64 2.33s user 0.89s system 42% cpu 7.571 total
実際の開発ではコードや Dockerfile の修正 → ビルドの実行の繰り返し作業はどうしても多く発生しがちなので、カーネルイメージのビルドを省くことによる効率化の影響はかなり大きくなっています。
unikraft のファイルシステムはパフォーマンス、リソース制約、セキュリティなどを考慮して設計されており、上記 3 つの形式を組み合わせて多彩なユースケースに対応できるようになっています。それと同時に必要なときのみカーネルイメージの再ビルドを行うことでビルドプロセスを効率化できるというような側面も持ち合わせていると言えます。
Docker + urunc で unikernel インスタンスを操作する
すでにコンテナを使い慣れている人は docker や nerdctl といったコンテナ管理ツールで unikernel インスタンスを管理したいと思うかもしれません。unikernel はコンテナとは明確に仕組みが異なるためコンテナランタイムなどはありませんが、runc の代替である runu を使うことで docker などのコンテナ管理ツールから unikernel インスタンスを操作できるようになります。runu も unikraft のツールの一部であるため必要な手順に関しては以下に記載されています。
runu は kraft コマンドを含む kraftkit リポジトリ で開発されており release ページからダウンロードできます。ただ手元で試したところドキュメントの手順ではエラーでアプリケーションがうまく動作しなかったため、ここでは runu の代わりに、同じく unikernel 向けのコンテナランタイムである urunc を使います。
まずは インストール手順 に従って urunc をインストールします。
RUNC_VERSION=$(curl -L -s -o /dev/null -w '%{url_effective}' "https://github.com/opencontainers/runc/releases/latest" | grep -oP "v\d+\.\d+\.\d+" | sed 's/v//')
wget -q https://github.com/opencontainers/runc/releases/download/v$RUNC_VERSION/runc.$(dpkg --print-architecture)
sudo install -m 755 runc.$(dpkg --print-architecture) /usr/local/sbin/runc
rm -f ./runc.$(dpkg --print-architecture)
urunc による unikernel のビルド方法 はいくつか記載されていますが、ここではアプリケーションを含む unikernel を OCI 形式のイメージにビルドする方法にします。unikernel の構築には、unikernel 構築プロセスを簡略化するために開発された bunny を使うと楽らしいので リポジトリ のリリースからダウンロードしておきます。
例として、アプリケーションカタログの example として提供されている Go 用の http server を OCI イメージにビルドして実行します。ファイルはそのままですが、Kraftfile
内の runtime を手元環境でビルドした mytest:base に変更しています。
spec: v0.6
name: httpserver-go1.21
#runtime: base:latest
runtime: mytest:base
rootfs: ./Dockerfile
cmd: ["/server"]
bunny を使ってアプリケーションを OCI イメージにするには bunnyfile という yaml ファイルに設定を記載していきます。ドキュメントに記載の構文を参考に以下を追加していきます。
#syntax=harbor.nbfc.io/nubificus/bunny:latest
version: v0.1
platforms:
framework: unikraft
monitor: qemu
architecture: x86
rootfs:
from: local
path: .unikraft/build/initramfs-x86_64.cpio
kernel:
from: local
path: ./base_qemu-x86_64
cmdline: "/server"
重要なのは以下。
- rootfs: ルートファイルシステム (initramfs)
- from: ローカルにあるファイルを指定するので local
- path: initramfs (cpio アーカイブ)までのパスを指定。
kraft build
を実行して事前に作成しておく。
- kernel: カーネルイメージ
- from: ローカルにあるファイルを指定するので local
- path:
mytest/base
ビルド時に生成された kernel までのパスを指定。kraft build
で作成して事前に作業ディレクトリにコピーしておく。
- cmdline: unikernel インスタンス内で実行するコマンドを指定。ここでは / にある
server
バイナリを実行する。
このときのディレクトリ構成は以下。
.
├── .unikraft
│ ├── build
│ │ ├── initramfs-x86_64.cpio
├── Dockerfile
├── Kraftfile
├── README.md
├── base_qemu-x86_64
├── bunnyfile
└── server.go
docker build -f bunnyfile -t [イメージ名] .
で OCI イメージをビルド。
$ docker build -f bunnyfile -t urunc-test .
[+] Building 3.4s (9/9) FINISHED docker:default
=> [internal] load build definition from bunnyfile 0.0s
=> => transferring dockerfile: 307B 0.0s
=> resolve image config for docker-image://harbor.nbfc.io/nubificus/bunny:latest 2.6s
=> CACHED docker-image://harbor.nbfc.io/nubificus/bunny:latest@sha256:5cfa082f077dce1ed11819c8ccd93c2075bd985bfc767723a39b31d149c08b0b 0.1s
=> => resolve harbor.nbfc.io/nubificus/bunny:latest@sha256:5cfa082f077dce1ed11819c8ccd93c2075bd985bfc767723a39b31d149c08b0b 0.0s
=> Internal:Read-bunnyfile 0.0s
=> => transferring context: 31B 0.0s
=> local://context 0.0s
=> => transferring context: 5.67kB 0.0s
=> CACHED copy /base_qemu-x86_64 /.boot/kernel 0.0s
=> CACHED copy /.unikraft/build/initramfs-x86_64.cpio /.boot/rootfs 0.0s
=> CACHED mkfile /urunc.json 0.0s
=> exporting to image 0.2s
=> => exporting layers 0.0s
=> => exporting manifest sha256:b6bb2683ddb2bdbd1e11e33103a02528b87d7052d75b287ca79175a417c64a6b 0.0s
=> => exporting config sha256:a64b4081e14780c8d3a3b7374fc06bacfa0619caa4b2ca3bda5c8efe3f7cbeb4 0.0s
=> => exporting attestation manifest sha256:b9e2b4d4b68503295d90bafef6997bf63cc03e94c98b377eff7f63fc11c3a760 0.1s
=> => exporting manifest list sha256:2444d8fb459975dad004751e839c444fe3ce7e2c4e5b3fd2fc458157752908e2 0.0s
=> => naming to docker.io/library/urunc-test:latest 0.0s
=> => unpacking to docker.io/library/urunc-test:latest 0.0s
これにより unikernel インスタンス用の docker イメージがビルドされます。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
urunc-test latest 2444d8fb4599 5 days ago 17.8MB
起動するには --runtime io.containerd.urunc.v2
を指定して docker run
を実行。
起動すると kraft run 実行時と同じ出力が表示され、qemu 上で unikernel インスタンスが起動します。
$ docker run --rm -it -p 8080:8080 --runtime io.containerd.urunc.v2 urunc-test
SeaBIOS (version 1.15.0-1)
iPXE (https://ipxe.org) 00:02.0 C000 PCI2.10 PnP PMM+0FF8B290+0FECB290 C000
Booting from ROM..1: Set IPv4 address 172.17.0.3 mask 255.255.255.0 gw 172.17.0.1
en1: Added
en1: Interface is up
Powered by Unikraft Kiviuq (0.20.0~5a22d73)
Listening on :8080...
別ターミナルから curl を実行してアクセスできることを確認。
$ curl 0.0.0.0:8080
Bye, World!
起動した unikernel インスタンスは docker ps
で確認できます。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
273d6e6e94f6 urunc-test "/server" 43 seconds ago Up 42 seconds 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp boring_mestorf
プロセスツリーを見ると urunc で起動したコンテナから qemu で unikernel インスタンスを実行していることがわかります。
$ ps auxwwf
root 47170 0.0 0.2 1233544 9544 ? Sl 03:53 0:00 /usr/local/bin/containerd-shim-urunc-v2 -namespace moby -id 273d6e6e94f6562249fec21810acb09225c771fb21cf723e0b4b5b98f5c139b1 -address /run/containerd/containerd.sock
root 47190 0.5 2.5 489056 102088 ? Ssl 03:53 0:00 \_ /usr/bin/qemu-system-x86_64 -m 256M -L /usr/share/qemu -cpu host -enable-kvm -nographic -vga none --sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny -kernel /.boot/kernel -net nic,model=virtio,macaddr=7e:02:05:bd:c2:d4 -net tap,script=no,downscript=no,ifname=tap0_urunc -initrd /.boot/rootfs -append Unikraft env.vars=[ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=273d6e6e94f6 TERM=xterm ] netdev.ip=172.17.0.3/24:172.17.0.1:8.8.8.8 vfs.fstab=[ "initrd0:/:extract:::" ] -- /server
コンテナの削除は通常通り docker rm [コンテナ名]
で実行できます。もしくは kill コマンドで qemu のプロセスを kill するとコンテナも停止するようになっています。
細かい部分を抜きにすれば、kraft run
で unikernel インスタンスを実行する作業をほぼそのまま docker コマンドで置き換えることができます。
もちろん nerdctl (containerd) でも runtime オプションを指定することで同様に実行できます。
$ sudo nerdctl build -f bunnyfile -t urunc-test .
$ sudo nerdctl run --rm -it -p 8080:8080 --runtime io.containerd.urunc.v2 urunc-test
$ sudo nerdctl ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aef7c79ae053 docker.io/library/urunc-test:latest "/server" 30 seconds ago Up 0.0.0.0:8080->8080/tcp urunc-test-aef7c
$ sudo nerdctl rm -f urunc-test-aef7c
ちなみに独自の runtimeClass リソースを作成することで kubernetes 上でも動かせるとのこと。
イメージの中身
ところで urunc でビルドした OCI イメージの中身はどうなっているのでしょうか。イメージをローカルに展開して中身を見てみます。
docker save urunc-test:latest > urunc.tar
tar xvf urunc.tar
ディレクトリ構成は OCI に準拠。
.
├── blobs
│ └── sha256
│ ├── 2444d8fb459975dad004751e839c444fe3ce7e2c4e5b3fd2fc458157752908e2
│ ├── 692fcf5bb27916759fb735771aed6628f424df307fd06ab1af6621b17ff6dd0c
│ ├── 91bb784307c2981bdfbb0f67c7d221805040a3af406851c0f9f067c71a5b9768
│ ├── 964acd7b2bb356db3b320d9a959159d4b1710edf5f0403b406b326c0bb496399
│ ├── 9b230e579b70350e5c8d9c4dc55691c34e14fa25c48f7d8364a32db1cbd98adf
│ ├── a64b4081e14780c8d3a3b7374fc06bacfa0619caa4b2ca3bda5c8efe3f7cbeb4
│ ├── b6bb2683ddb2bdbd1e11e33103a02528b87d7052d75b287ca79175a417c64a6b
│ ├── b7c416926de0a1387ed1a20d51dfc003956d40d1d228fee733ae7d6e6d5ea28a
│ └── b9e2b4d4b68503295d90bafef6997bf63cc03e94c98b377eff7f63fc11c3a760
├── index.json
├── manifest.json
├── oci-layout
└── urunc.tar
index や manifest は普通のイメージと比較するとやや異なります。
schemaVersion: 2
mediaType: application/vnd.oci.image.index.v1+json
manifests:
- mediaType: application/vnd.oci.image.index.v1+json
digest: sha256:2444d8fb459975dad004751e839c444fe3ce7e2c4e5b3fd2fc458157752908e2
size: 856
annotations:
io.containerd.image.name: docker.io/library/urunc-test:latest
org.opencontainers.image.ref.name: latest
- Config: blobs/sha256/a64b4081e14780c8d3a3b7374fc06bacfa0619caa4b2ca3bda5c8efe3f7cbeb4
RepoTags:
- urunc-test:latest
Layers:
- blobs/sha256/692fcf5bb27916759fb735771aed6628f424df307fd06ab1af6621b17ff6dd0c
- blobs/sha256/9b230e579b70350e5c8d9c4dc55691c34e14fa25c48f7d8364a32db1cbd98adf
- blobs/sha256/b7c416926de0a1387ed1a20d51dfc003956d40d1d228fee733ae7d6e6d5ea28a
blob 内の b6bb26
の構造がわかりやすい。
schemaVersion: 2
mediaType: application/vnd.oci.image.manifest.v1+json
config:
mediaType: application/vnd.oci.image.config.v1+json
digest: sha256:a64b4081e14780c8d3a3b7374fc06bacfa0619caa4b2ca3bda5c8efe3f7cbeb4
size: 1064
layers:
- mediaType: application/vnd.oci.image.layer.v1.tar+gzip
digest: sha256:692fcf5bb27916759fb735771aed6628f424df307fd06ab1af6621b17ff6dd0c
size: 723475
- mediaType: application/vnd.oci.image.layer.v1.tar+gzip
digest: sha256:9b230e579b70350e5c8d9c4dc55691c34e14fa25c48f7d8364a32db1cbd98adf
size: 5234789
- mediaType: application/vnd.oci.image.layer.v1.tar+gzip
digest: sha256:b7c416926de0a1387ed1a20d51dfc003956d40d1d228fee733ae7d6e6d5ea28a
size: 252
annotations:
com.urunc.unikernel.binary: /.boot/kernel
com.urunc.unikernel.cmdline: /server
com.urunc.unikernel.hypervisor: qemu
com.urunc.unikernel.initrd: /.boot/rootfs
com.urunc.unikernel.mountRootfs: "false"
com.urunc.unikernel.unikernelType: unikraft
イメージは 3 つのレイヤーで構成されていますが、上から順にカーネルイメージ、initramfs (rootfs)、urunc の設定 json が含まれているようです。
$ mkdir 692
$ tar zxvf 692fcf5bb27916759fb735771aed6628f424df307fd06ab1af6621b17ff6dd0c -C 692
.boot/
.boot/kernel
$ tar zxfv 9b230e579b70350e5c8d9c4dc55691c34e14fa25c48f7d8364a32db1cbd98adf -C 9b2
.boot/
.boot/rootfs
$ mkdir b7c
$ tar zxfv b7c416926de0a1387ed1a20d51dfc003956d40d1d228fee733ae7d6e6d5ea28a -C b7c
urunc.json
$ cat b7c/urunc.json | yq -P
com.urunc.unikernel.binary: Ly5ib290L2tlcm5lbA==
com.urunc.unikernel.cmdline: L3NlcnZlcg==
com.urunc.unikernel.hypervisor: cWVtdQ==
com.urunc.unikernel.initrd: Ly5ib290L3Jvb3Rmcw==
com.urunc.unikernel.mountRootfs: ZmFsc2U=
com.urunc.unikernel.unikernelType: dW5pa3JhZnQ=
残りは buildkit によって生成される Provenance attestations に対応しています。
_type: https://in-toto.io/Statement/v0.1
predicateType: https://slsa.dev/provenance/v0.2
subject:
- name: pkg:docker/urunc-test@latest?platform=linux%2Famd64
digest:
sha256: b6bb2683ddb2bdbd1e11e33103a02528b87d7052d75b287ca79175a417c64a6b
predicate:
builder:
id: ""
buildType: https://mobyproject.org/buildkit@v1
materials:
- uri: pkg:docker/harbor.nbfc.io/nubificus/bunny@latest
digest:
sha256: 5cfa082f077dce1ed11819c8ccd93c2075bd985bfc767723a39b31d149c08b0b
invocation:
configSource:
entryPoint: bunnyfile
parameters:
frontend: gateway.v0
args:
cmdline: harbor.nbfc.io/nubificus/bunny:latest
source: harbor.nbfc.io/nubificus/bunny:latest
locals:
- name: context
environment:
platform: linux/amd64
metadata:
buildInvocationID: l0b1ktu7bxabnx09g83dy76hn
buildStartedOn: "2025-09-22T03:50:51.232975136Z"
buildFinishedOn: "2025-09-22T03:50:54.279964741Z"
completeness:
parameters: true
environment: true
materials: false
reproducible: false
https://mobyproject.org/buildkit@v1#metadata:
vcs:
localdir:context: examples/httpserver-go1.21
localdir:dockerfile: examples/httpserver-go1.21
revision: 5a22d73139c02021e863f637383e1dc94d965564
source: https://github.com/unikraft/catalog
docker ビルドしたイメージではレイヤーの実体は Dockerfile で定義したコマンドに対応するファイルシステムとなっていますが、urunc でビルドした OCI イメージではビルド時に指定したカーネルイメージ、rootfs がそのまま入っている点が大きく異なっています。
その他いろいろ
Firecracker で unikernel インスタンスを実行する
上記の検証ではいずれも VMM として qemu を使いましたが、代わりに Firecracker を使うこともできます。手順は以下に記載されているためこちらに沿って試してみます。
手順の firecracker のバージョンはだいぶ古いので現時点で最新の v1.13 をインストールします。
cd /tmp
wget https://github.com/firecracker-microvm/firecracker/releases/download/v1.13.1/firecracker-v1.13.1-x86_64.tgz
tar zxvf firecracker-v1.13.1-x86_64.tgz
sudo cp release-v1.13.1-x86_64/firecracker-v1.13.1-x86_64 /usr/local/bin/firecracker
firecracker 用の base をダウンロード。--plat fc
が firecracker に対応。
kraft pkg pull -w base unikraft.org/base:latest --plat fc --arch x86_64
nginx アプリケーション用のカーネルをビルドします。
cd catalog/library/nginx/1.25
kraft build --plat fc --arch x86_64 .
firecracker 用のネットワークインターフェイスの作成。
sudo ip tuntap add dev tap0 mode tap
sudo ip address add 172.45.0.1/24 dev tap0
sudo ip link set dev tap0 up
fc-x86_64.json の作成。作成したカーネル .unikraft/build/nginx_fc-x86_64
を kernel_image_path などに指定。
{
"boot-source": {
"kernel_image_path": ".unikraft/build/nginx_fc-x86_64",
"boot_args": ".unikraft/build/nginx_fc-x86_64 netdev.ip=172.45.0.2/24:172.45.0.1 -- /usr/bin/nginx"
},
"drives": [],
"machine-config": {
"vcpu_count": 1,
"mem_size_mib": 128,
"smt": false,
"track_dirty_pages": false
},
"cpu-config": null,
"balloon": null,
"network-interfaces": [
{
"iface_id": "net1",
"guest_mac": "06:00:ac:10:00:02",
"host_dev_name": "tap0"
}
],
"vsock": null,
"logger": {
"log_path": "/tmp/firecracker.log",
"level": "Debug",
"show_level": true,
"show_log_origin": true
},
"metrics": null,
"mmds-config": null,
"entropy": null
}
ログファイルを作成して実行。
$ touch /tmp/firecracker.log
$ sudo firecracker --api-sock /tmp/firecracker.socket --config-file fc-x86_64.json
2025-09-20T08:57:04.317170231 [anonymous-instance:main] Running Firecracker v1.13.1
2025-09-20T08:57:04.317261152 [anonymous-instance:main] Listening on API socket ("/tmp/firecracker.socket").
1: Set IPv4 address 172.45.0.2 mask 255.255.255.0 gw 172.45.0.1
en1: Added
en1: Interface is up
Powered by Unikraft Kiviuq (0.20.0~5a22d73)
別ターミナルで上記の IP アドレスに向けて curl を実行。
$ curl http://172.45.0.2:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
ps で firecracker 関連のプロセスが動いていることが確認できます。
root 102247 0.0 0.1 11904 5652 pts/0 S+ 08:57 0:00 sudo firecracker --api-sock /tmp/firecracker.socket --config-file fc-x86_64.json
root 102248 0.0 0.0 11904 896 pts/2 Ss 08:57 0:00 sudo firecracker --api-sock /tmp/firecracker.socket --config-file fc-x86_64.json
root 102249 0.7 1.1 139028 46092 pts/2 Sl+ 08:57 0:00 firecracker --api-sock /tmp/firecracker.socket --config-file fc-x86_64.json
現時点では kraft build コマンドで unikernel を生成 → firecracker コマンドで実行という流れにはなりますが、上記のように qemu 以外の VMM でも unikernel インスタンスを管理できます。
compose コマンド
kraft コマンドには docker compose に対応する kraft compose サブコマンドがあります。
用途も docker compose と同じで複数の unikernel インスタンスを宣言的に記述してまとめて管理するために使われるようです。compose ファイルの記載例はほとんど見当たりませんでしたが以下のあたりが参考になりそうです。ファイルを見る限り docker compose ファイルとの互換性はありそう。
app elfloader
一般的な unikernel ではアプリケーションを実行するためには unikernel そのものに組み込んでカーネルイメージとしてビルドする必要があり、Linux 上で実行可能な ELF ファイルを作ってそのまま unikernel に持ってきても動きません。一方で今まで見てきた検証によると、例えば flask サンプルの場合は dockerfile から python アプリケーションを含む rootfs を作成して、既存の unikernel イメージと一緒に qemu に渡していました。これはアプリケーションを unikernel 用にビルドしていないにも関わらず unikernel インスタンス上で動作しているように見えますが、実は
unikraft では binary-compatibility layer と呼ばれるレイヤーを導入して Linux の ELF を実行する互換性を実現しているようです。これに関しては以下の compatibility である程度記載されています。
コアとなる部分は以下の 2 つのようです。
- syscall shim: システムコールを unikraft 関数にマッピングする役割
- app-elfloader: Linux ELF を unikernel 上でロードして解析する役割
syscall shim の実体はライブラリなのでなかなかイメージが掴みづらいですが、app-elfloader は単体のリポジトリがあるのでややわかりやすいかもしれません。要は Linux ELF を unikernel 上で実行する役割を担っているため、これがないとアプリケーションが unikernel 上で動作しないことが想定されます。実際 catalog リポジトリの library 以下のコードを見てみるとほとんどの Kraftfile
内で app-elfloader が template として読み込まれていることが確認できます。
template:
source: https://github.com/unikraft/app-elfloader.git
version: staging
逆に言えば、app-elfloader が含まれない場合は ELF が実行できないのでエラーになりそうです。実際に Kraftfile 内の上記部分をコメントアウトしてビルドして確かめてみましょう。
$ cd catalog/library/nginx/1.25
# vim 等で上記をコメントアウト
$ vim Kraftfile
# 事前のキャッシュやビルド設定を削除
$ rm .config.nginx_*
$ rm -rf .unikraft
# ビルド
$ kraft build --plat qemu --arch x86_64
# unikernel インスタンスを起動
$ kraft run -W --rm -p 8080:8080 --plat qemu --arch x86_64 .
i using arch=x86_64 plat=qemu
en1: Added
en1: Interface is up
Powered by Unikraft Kiviuq (0.20.0~5a22d73)
weak main() called. Symbol was not replaced!
となり、確かに nginx の実行に失敗します。
なお上記の weak main() called. Symbol was not replaced!
エラーは elfloader が読み込まれないことで unikraft 側の weak main が実行されているとのこと。
OCI リポジトリにパッケージを push/pull する
kraft pkg
コマンドではカーネルイメージをパッケージ化できますが、このとき --as oci
をつけると OCI 形式で保存されます。このようなイメージは OCI イメージとして扱われるため、ghcr.io などの OCI イメージに対応したコンテナレジストリに push/pull することができます。
ここでは ghcr.io を使ってテストしてみましょう。
まず kraft login ghcr.io -u USERNAME -t [github token]
でコンテナレジストリにログインします。成功・失敗にかからわず特にメッセージは表示されないので注意。
kraft login ghcr.io -u USERNAME -t [token]
パッケージを ghcr.io/[username]/[イメージ名]
に設定して kraft pkg push
でプッシュ。
kraft pkg --as oci --name ghcr.io/git-ogawa/flask:latest
kraft pkg push ghcr.io/git-ogawa/flask:latest
いったん push が完了したら docker など他のコンテナ管理ツールで pull できます。なお platform はビルド時の [vmm]/[arch]
で登録されるため、--platform
で明示的に指定しないとイメージを見つけられずエラーになるようです。
$ docker pull --platform qemu/x86_64 ghcr.io/git-ogawa/flask
サポートされている VMM とアーキテクチャ
kraftkit リポジトリの Compatibility に書いてある。現在は Qemu, Xen, Firecracker のみをサポート。
参考資料
-
https://unikraft.org/docs/getting-started
- unikraft のドキュメント
-
https://github.com/unikraft/unikraft
- unikraft の git リポジトリ。主に unikernel 用のライブラリなどがこのリポジトリで開発されている。
-
https://github.com/unikraft/kraftkit
- kraft コマンドや runu の開発はこのリポジトリ
-
https://github.com/unikraft/catalog
- kraft コマンドで実行可能なアプリケーションカタログ、サンプルなどを含むリポジトリ
-
https://urunc.io/
- urunc のドキュメント
Discussion