ステータス: ドラフト(2026-10-16)、自社コード + 公式ドキュメント + 一部実測。 動作の説明は、TT Labのバックエンドコード(
backend/app/vms.py,backend/app/main.py)と設計書にそのまま従っています。バージョンは執筆した日に読み込んで確認した値です(Kubernetes v1.34.10、KubeVirt v1.9.0、CDI v1.66.0)。時間の数値は実習を作成しながら計測した記録であり、サンプル数と日付を併記しています。 作成ルール: 実測データが基本であり、計測できなかったものは「未計測」、ドキュメントから見つからなかったものは「未確認」と表示します。
学生VMが立ち上がるまで 1: 4つの層から見た全体像
目次
1. なぜPodではなくVMなのか
TT Labの実習のほとんどは、KubernetesのPod1つで動作します。Podは数秒で起動し、イメージを変えるだけで環境が変わり、削除すれば痕跡が残りません。しかし、Podではどうしても模倣できない実習がありました。
- systemd がありません。 サービスを再起動したりユニットファイルを修正したりする実習は、「文法をチェックする」に置き換えるしかありませんでした。
- kubeadm でノードを構築し、etcd のバックアップとリカバリを行う作業はできません。Pod内ではKubernetesをkwokで模倣していますが、kwokはコンテナを実際に稼働させず、PodをReadyに偽装します。
- カーネル機能を操作できません。 実習用Podはcapabilityが0個であるため、rootless podmanですら動作しません。そして、その0個という制限はTT Labが守る原則であり、増やす予定はありません。
そのため、このような実習のみ 仮想マシンで公開します。学生はVM内で本物のrootとなり、systemd・カーネル・ディスクを自由に操作できます。その代わり、そのrootがVMの外に出ないようにすること、そして使い終わったVMが確実に回収されるようにすることが本シリーズのテーマです。VMはKubernetes上でKubeVirtを使って起動します。実習用Podと同じクラスター、同じスケジューラー、同じストレージを使用するためです。
2. 4つの層
本シリーズでは、あえて4つの層のみを取り上げます。 学生(ブラウザ)、バックエンド(Web Pod)、KubeVirt、Kubernetesストレージです。ゲートウェイ・イメージレジストリ・モニタリングのようにその間に介在するものは図から除外しています。「VMが割り当てられる」という処理の骨組みがこの4つであるためです。
層ごとの役割を一行にまとめると以下のようになります。
| 層 | 役割 | 本シリーズでの扱い |
|---|---|---|
| 学生 | ボタンを押し、WebターミナルでVM内のシェルを使用する | 第1回 |
| バックエンド | 空きを確認し、VMに組み込むものを準備し、ルートディスクを選び、VMオブジェクトを作成する。使い終わったら削除する | 第1・5・6回 |
| KubeVirt | VMオブジェクトを確認してPodを作成し、そのPod内でQEMUによりゲストを起動する | 第2回 |
| ストレージ | ゲストのディスクとなるボリュームを接続する。1つのゴールデンPVCを複数のVMで共有して読み込む | 第3・4回 |
3. 10のステップ
① 「実習開始」
学生がボタンを押すと、ブラウザが POST /api/sessions を送信します。レッスンが runtime: vm であれば、このセッションはPodではなくVMを作成します。セッションの履歴には最初から runtime が含まれます。回収機能が後から「何を削除すべきか」をこの値一つで判断するためです。ここで種類を間違えると、Podは削除されたと記録される一方で、VMは4GiBのメモリを占有し続けます。
② 空き確認
2つの点を確認します。1つ目は 1人1セッションです。他の実習が開いている場合は何が開いているかを通知し、新しく作成しません。ユーザーごとのロック内で確認と記録を同時に行うため、ボタンを2回押してもセッションは1つだけです。
もう1つは、**同時に起動しているVMの数(デフォルト8台)**です。この数値はDBではなく クラスターに問い合わせて 数えます。DBは回収機能が見逃した幽霊VMを数えることができませんが、ノードのメモリを実際に占有しているのはクラスター側だからです。上限に達している場合は、「現在VMの実習が8個開いているため空きがありません」と返します。なぜ8なのかは第5回で解説します。
③ cloud-init Secret
VMはイメージをあらかじめビルドせず、パブリッククラウドのイメージに対して 初回の起動時に cloud-init により 必要なものを組み込みます。組み込むものは次の3つです。
- 採点エージェント。 ゲスト内で8899番ポートをリッスンする小さなHTTPサーバーです。受け取った命令をシェルで実行して採点します。なぜこのような通信経路が必要なのかは⑧で解説します。
- VMごとに異なるトークン。 エージェントはこのトークンが一致するリクエストのみを受け付けます。学生は自分のVMのrootであるためトークンを見ることができます。防ぎたいのは学生ではなく、ネットワークポリシーの設定ミスによって到達してしまう可能性のある他のPodです(第6回)。
- 実習ファイルとセットアップスクリプト。 採点器と実習用の教材です。セットアップスクリプトが失敗した場合、「準備完了」のフラグを立てません。
これらをVMのマニフェストに直接記述せずSecretに格納するのには、実測に基づく理由があります。KubeVirtはインラインのcloud-initを2048バイトに制限していますが、採点エージェントを含めると7千バイトを超えてしまいます。そのため、すべての実習VMが同じ経路(Secret)を通ります。サイズによって経路が分かれてしまうと、エージェントを組み込む瞬間だけに発生する不具合の温床になるためです。
④ ルートディスクの選択
ここが本シリーズの中心です。レッスンが vm_disk: "golden" を指定している場合、バックエンドはオペレーターが事前に作成した ゴールデンPVC を読み込み(get つの)、現在使用可能かどうかを判定します。使用可能な場合はそのPVCを 読み取り専用のベースとして使用し、VMごとに軽量な 書き込み層のみを重ねます。存在しない場合や使用できない場合(不在・権限なし・取得中・単一ノード用ボリューム・異なるイメージ・削除中)、エラーを出さずにデフォルトのディスク(コンテナディスク)で起動します。ゴールデンは最適化であって前提条件ではないためです。ディスクの3つの分岐については第3回、ゴールデンと書き込み層については第4回で詳しく解説します。
⑤ VirtualMachineの作成
バックエンドがKubernetes APIに VirtualMachine オブジェクトを1つ作成します。このオブジェクトにはラベルが付与されます。実習セッションであることを示す目印、セッションID、そして どの環境(開発・本番)のバックエンドが作成したかです。最後のラベルはトラブルから生まれました。開発と本番のバックエンドが同じVMのネームスペースを参照しており、一方の回収機能が自身のDBに存在しないVMを孤立していると誤認し、他方の学生のVMを削除してしまったためです(第5回)。
cloud-initのSecretにはこのVMを所有者として指定し、VMが削除されたときにSecretも一緒に削除されるようにします。ただし、所有者の関連付けに失敗する場合があるため、削除時にはSecretも個別に再度削除します。
⑥ KubeVirt: VM → VMI → Pod → QEMU
ここからはKubeVirtの処理です。VMをもとに実行中インスタンス(VMI)を作成し、VMIごとに virt-launcher Pod を1つ作成します。KubernetesのスケジューラーがそのPodをノードに配置すると、そのノードのvirt-handlerがPod内のlibvirtに対して「このVMを起動せよ」と指示します。ゲストは、そのPod内のQEMUプロセスとして動作します。VMがPod内で動作する理由とその代償は、第2回のテーマです。
⑦ ボリュームの接続
virt-launcher Podが作成される際、ゲストのディスクとなるボリュームも一緒にアタッチされます。ゴールデン実習の場合、ゴールデンPVCが 読み取り専用でアタッチされ、その上にKubeVirtがPodの一時ディレクトリ(emptyDir)に書き込み層のファイルを作成します。ゲストにはこれらが統合された1つのディスク(/dev/vda)のみが見えます。アタッチすべきPVCが存在しない場合、KubeVirtはPod自体を作成できず、VMはいつまで経っても起動しません。④であらかじめ判定を行うのはそのためです。
⑧ 準備完了の確認
バックエンドは2段階で待機します。まずVMIが Running になるのを待ち(実習ごとに設定された上限、デフォルト150秒)、次にゲスト内のエージェントが「準備完了」と応答するのを待ちます。
KubeVirtにはPodの exec のような「ゲスト内でコマンドを実行する機能」はありません。サブリソースはconsole、vnc、portforwardなどのみであり、これは意図された設計です。そのため、準備確認も採点も③で組み込んだエージェントで行います。
Running のみを確認して学生をアクセスさせてはならないことも、実際に経験しました。エージェントはcloud-initの早い段階で起動し、実習環境を構築するセットアップスクリプトはその後に実行されます。その間にアクセスさせてしまったため、第1段階から「ファイルが存在しません」というエラーで失敗し、採点は8段階中4段階しか通過しませんでした。現在はcloud-initの 最終行で準備完了のフラグを作成し、セットアップスクリプトが失敗した場合は作成しないようにしています。これにより、バックエンドはタイムアウトにより 正直に 失敗します。
⑨ コンソールの仲介と ⑩ Webターミナル
バックエンドがVMIの console サブリソースにWebSocketで接続し(⑨)、学生のブラウザのWebターミナルとの間を仲介します(⑩)。ここにも2つの罠がありました。
- Podのターミナルで使用していたKubernetesクライアントのストリーム関数は、最初の1バイトをチャネル番号として読み込みます。KubeVirtのコンソールにはそのようなヘッダーがない生バイト(
plain.kubevirt.io)であるため、同じ関数で接続すると 接続は開くものの画面に何も表示されない という現象が発生します。エラーも発生しないため、より混乱を招きました。そのため、WebSocketで直接接続します。 - コンソールの終端にあるのはシェルではなく、ログインプロンプト(getty)です。実習VMではあえてパスワードを作成していないため、cloud-initによってシリアルコンソールにrootの自動ログインを設定しています。
WebSocketを開く際、バックエンドは接続したユーザーがそのセッションの所有者であるか、セッションが実行中であるかを事前に確認します。コンソールのサブリソースにアクセスできるKubernetesの権限は、バックエンドのサービスアカウントのみです(第6回)。
4. かかる時間 — これまでに計測したもの
| 項目 | 値 | 条件 | サンプル数 |
|---|---|---|---|
| VMIのステージ: Pending → Scheduled → Running | 0秒 → 10秒 → 20秒(10秒単位での記録) | Ubuntu 24.04 コンテナディスク、2 vCPU・4Gi、2026-08-29 | 1回 |
| ゲスト内の cloud-init 完了 | 23.8秒 | 同一VM | 1回 |
| cloud-initによるDockerのインストール実習 | 約76秒(起動25 + aptインデックス14 + インストール37) | 2026-08 実習開発中 | 1回 |
| 学生APIで開いたセッション、リクエストから準備まで | 90.991秒 | Istio実習単体、2026-09-12 | 1回 |
| 3台構成のVMセッション:3台のVMがRunning | 17~23秒 | kubeadm 3ノード実習、2026-09-23~24 | 複数回の試験の範囲 |
| 同じセッション:3ノードの準備スクリプト完了 | 約300秒 | 同じ実習 | 同上 |
| 実習ごとに定めた準備上限の根拠 | k3s 82秒 · Istio 94秒 · k0s 150秒 · Cilium 269秒 | 各実習の正解検証 | 実習ごと |
| VMごとにディスクを新しく取得する方式(DataVolume)の初回起動 | 最大195秒 | 第3部 | 最大値のみ記録 |
表が物語っているのは次の2つです。 VM自体は20秒前後で起動し、残りはゲスト内で実習環境を構築する時間です。そして、ディスクをVMごとに新しく取得するとそれだけで数分かかります。第4部のゴールデンディスクが狙っているのは、この最後の行です。
限界も明確です。大半が1回ずつ測定した値であり、学生セッション全体の準備時間の分布(中央値とばらつき)は 未測定です。セッションの記録には開始時刻と準備時刻が一緒に残るため、記録を集めれば測定可能です。
5. この図であえて省いたもの
- ネットワークポリシー。 学生VMへの接続はデフォルトでブロックされ、バックエンドのみ8899でアクセスできます。外向きはインターネットのみ許可されています。第6部で確認します。
- 回収。 セッション時間が切れると回収器がVMを削除し、書き込み層はポッドと一緒に消滅します。第5部で確認します。
- 複数VM構成のセッション。 kubeadm実習のように3つのノードが必要なセッションでは、同じ手順(②~⑧)をVMごとに繰り返し、セッション専用のネットワークをもう1つ敷きます。本シリーズは1台構成を基準に執筆しています。
T-Tの一言。 ボタンを1回押した裏側には10のステップが隠されています。しかし、VMが起動するのは20秒で終わり、残りのほとんどは「ゲスト内での実習準備」と「ディスクの用意」です。そのため、速くしたいのであればまずディスクを見てください。第4部でお会いしましょう!
参考
ドキュメントはすべて2026-10-16に閲覧しました。
- KubeVirtの構成要素: https://github.com/kubevirt/kubevirt/blob/main/docs/components.md
- KubeVirtのディスクとボリューム: https://kubevirt.io/user-guide/storage/disks_and_volumes/
- TT Labのコード:
backend/app/vms.py(マニフェスト・cloud-init・ゴールデン判定)、backend/app/main.py(create_session·make_vm·vm_console·reap_once),backend/app/vm_agent.py(採点エージェント) - 設計と実測の記録:
docs/plans/2026-08-29-vm-labs-design.md,docs/plans/2026-09-12-vm-reaper-environment.md,docs/plans/2026-09-24-multi-vm-sessions.md,docs/plans/2026-10-14-vm-golden-overlay.md