Dockerは果たして最終目的地なのだろうか?
DevOpsに携わるエンジニアにとって、Dockerはもはや馴染み深い存在でしょう。「自分のマシンでは動くのに、サーバーでは動かない」という古典的な問題を解決する上で, Dockerは圧倒的な力を発揮してきました。しかし、数年間Proxmoxで自作のホームラボを運用し、約12台の仮想マシン(VM)を動かしてきた中で、従来のコンテナが抱える課題を感じるようになりました。
例えば、シンプルなNode.jsのコンテナを考えてみましょう。通常、DebianやAlpineといった数百MBもの軽量Linux OSを一緒にパッケージングする必要があります。セキュリティ面では、コンテナはホストOSとカーネルを共有しています。万が一カーネルに脆弱性があれば、攻撃者はコンテナを脱出し、サーバー全体を支配できてしまいます。そこで私が注目したのが、Unikernel、特にNanoVMsです。
Unikernelとは?なぜ NanoVMsが注目されているのか?
実行ファイル(バイナリ)を1つだけ動かしたいと想像してみてください。肥大化したLinuxをインストールしてからDockerを動かす代わりに、Unikernelはアプリケーションと最小限のシステムライブラリだけをパッケージ化します。その結果、非常にコンパクトなイメージファイルができあがり、KVM、QEMU、AWS Firecrackerなどのハイパーバイザ上で直接起動できます。
NanoVMsが解決する課題とは?
- アタックサーフェス(攻撃対象領域)がほぼゼロ: システムにはシェル(bash)もSSHもユーザー管理もありません。攻撃者は仮想マシンに「リモート」で入ることができません。なぜなら、コマンドを打つためのツールがそもそも存在しないからです。
- 驚異的なスピード: 起動時間はミリ秒単位です。Linuxの不要なバックグラウンドプロセスにリソースを割く必要がありません。
- 完全な分離: 各アプリケーションは、独自のカーネルを持つ個別のUnikernelインスタンス上で動作します。Dockerのようにホストとカーネルを共有することはありません。
環境構築
まず、Linux(Ubuntuが最適です)を搭載したマシンが必要です。NanoVMsは高速化のためにKVMを利用するため、ハードウェア仮想化がサポートされていることを確認してください。私のようにProxmox上のVMを使用する場合は、「Nested Virtualization」を有効にするのを忘れないでください。
NanoVMsを操作するためのコマンドラインツール(CLI)であるOPSをインストールします:
curl https://ops.city/get.sh -sSfL | sh
準備が整ったかバージョンを確認します:
ops version
実践:NanoVMsでGoアプリケーションをデプロイする
ここではGo言語が最適な選択肢です。Goは静的バイナリにコンパイルされるため、Unikernelの哲学と非常に相性が良いです。
ステップ1:シンプルなWebサーバーを作成する
以下の内容で main.go ファイルを作成します:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "NanoVMs上で動作するUnikernelへようこそ!")
})
fmt.Println("サーバーはポート8080で起動しています...")
http.ListenAndServe(":8080", nil)
}
ステップ2:アプリケーションのコンパイル
GoのコードをLinux用のバイナリ形式にコンパイルします:
GOOS=linux go build main.go
ステップ3:Unikernelとしてアプリケーションを実行する
Dockerfileのことは忘れてください。NanoVMsなら、ops コマンドを使うだけです。このツールが自動的にライブラリを分析し、イメージをパッケージ化して、極小のKVM仮想マシンを起動します:
ops run main -p 8080
このコマンドは自動的に以下の4つのステップを実行します:
- バイナリファイル
mainを含むディスクイメージを作成。 - NanoVMsのカーネル
nanosをダウンロード。 - QEMU/KVMを起動してイメージをブート。
- 仮想マシンのポート8080を外部に公開。
http://localhost:8080 にアクセスしてみてください。すぐにウェルカムメッセージが表示されるはずです。興味深いのは、アプリケーションが独立した仮想マシン内で動いているにもかかわらず、リソース消費が驚くほど低いことです。
高度な設定
実際には、環境変数や設定ファイルを渡す必要があります。NanoVMsは config.json ファイルを通じてこれらをサポートしています。
設定ファイルの例:
{
"Args": ["main"],
"Env": {
"APP_ENV": "production",
"DB_HOST": "10.0.0.5"
},
"Files": ["config.yaml"],
"MapDirs": {"./static": "/var/www/static"}
}
この設定で実行するには、次のコマンドを使用します:
ops run main -c config.json
実測比較:NanoVMs vs Docker
自宅のProxmoxクラスター上で、Goで書かれた同じAPIアプリケーションを使用して簡単なテストを行いました:
- Docker: イメージサイズ 150MB(Ubuntuベース)、アイドル時のRAM消費 20MB。起動時間は約1〜2秒。
- NanoVMs: イメージサイズわずか 12MB。RAM消費はたったの 8MB。コマンド入力からリクエストを受け付けるまでの起動時間は約300ms。
しかし、最も価値があるのはセキュリティです。コンテナの場合、ポートが空いていれば、攻撃者は ls や cat /etc/passwd を使って偵察を行うことができます。NanoVMsには ls も cat も、マルウェアをダウンロードするためのパッケージマネージャーもありません。アプリケーションだけが唯一存在し、動作しているのです。
結論:どのような時に NanoVMsを使うべきか?
公平に見て、NanoVMsがDockerを完全に置き換えることはまだ難しいでしょう。アプリケーションが複雑なモノリス構成であったり、多くの子プロセスを呼び出す必要があったり、Linux特有の機能に深く依存している場合、Unikernelへの移行は大きな挑戦となります。
しかし、以下のようなケースではNanoVMsが非常に威力を発揮します:
- マイクロサービス: パフォーマンスを最大限に引き出す必要がある、小さく独立したサービス。
- サーバーレスインフラ: 超高速起動を活かしてコストを削減。
- エッジコンピューティング: ハードウェアリソースが極端に制限されている環境。
NanoVMsへのアプローチは、最初は少し戸惑うかもしれません。しかし、セキュリティとスピードのメリットを考えれば、今すぐラボで試してみる価値のある技術であることは間違いありません。

