ステータス: ドラフト (2026-10-16)、自社コード + 障害記録 + 一部実測値。 動作はTT Labバックエンドの
reap_once·delete_vm_sessionと 設計・障害記録に従いました。クォータと上限は同日に読み取り専用で照会し、ストレージの値と一致していることを確認しました。 作成ルール: 実測データを基本とし、計測できなかったものは「未実測」、ドキュメントから見つからなかったものは「未確認」と表記します。
学生VMが起動するまで 5: 寿命と回収
目次
TT Labの1文の原則は「受講生に真のrootシェルを与えるが、外部への流出は防ぎ、 使用したリソースは必ず回収する」 です。VMにおいて最後のフレーズは特に重要です。実習用Podが1つ残れば数百MBが無駄になりますが、VMが1つ残ると4GiBが丸ごとロックされます。
1. 寿命: 60分、延長は最大3倍まで
- セッションを開くと、有効期限が 開始時刻 + 60分に設定されます(デフォルト値、環境変数で変更可能)。
- 学生は延長ボタンを押すことができます。延長すると有効期限は「現在時刻 + 60分」に延びますが、 開始時刻 + 180分を超えることはできません。無限延長を防ぐための上限です。
- 残り時間は、画面のカウントダウンと回収機能が 同一の関数で計算します。両者が個別に計算すると、画面ではあと5分と表示されているのにVMが突然消えるといった現象が発生します。
- 1人1セッションの原則があるため、1人の学生が複数のVMを蓄積することはできません(パート1の②)。
2. 回収機能: 60秒ごとに3つの方向
バックエンドは60秒ごとに回収処理を1回実行します。管理者画面の「今すぐ回収」ボタンも同じ関数を使用しています。確認する対象は3つの方向です。
| 方向 | 対象 | 必要な理由 |
|---|---|---|
| 期限切れ | 時間が経過したセッション → リソースを削除し、ステータスを expired に更新して終了 |
デフォルトのパス |
| 孤立 | クラスターには存在するが、DB上に有効なセッションが存在しないVM → 削除 | バックエンドがVMを作成した直後にクラッシュするなど、記録とリソースの間に不整合が生じた場合 |
| 幽霊 | DB上では実行中だが、実際にはVMが存在しないセッション → ステータスを failed に更新して終了 |
ノードの再起動やOOMによりVMが消失した場合。これがないと「1人1セッション」の制限により、学生が新しい実習を永遠に開始できなくなる |
VMの孤立を個別にスキャンするのにも理由があります。回収機能は当初実習用Podのリストのみを確認していましたが、VMは 別のネームスペースに存在するため、そのリストには決して表示されませんでした。DBのレコードが消えたVMが4GiBのメモリを保持したまま誰からも見えない状態になる可能性がありました。
孤立したリソースを削除する直前に、DBをもう一度確認します。これは、回収機能がリストを読み込んだ後に作成されたばかりのセッションのVMを、孤立したリソースと誤認しないようにするためです。
障害1: 他環境の回収機能が学生VMを削除した
2026-09-12、本番環境において学生APIで開いたVMセッションが準備を完了できずに失敗しました。ログを突き合わせたところ、VMIが起動してから 26秒後 にVMが削除されていました。テストツールによって削除されたのではありませんでした。
原因はリソースの共有でした。開発環境と本番環境のバックエンドのDBは別々ですが、 VMのネームスペースは共通の1つを使用しています。 当時の回収機能はVMのネームスペース全体を読み取り、「自分のDBに存在しないものは孤立」と判断していました。そのため、開発環境の回収機能から見ると本番環境の学生のVMは孤立しているように見えました。同じ条件で行ったテストにおいて、本番環境のVMを削除する呼び出しが再現されました(Kubernetesの監査ログから実際の削除リクエストの送信者の身元まで特定することはしませんでした)。
修正方法はラベルの付与です。バックエンドはVMとSecretを作成する際に 自環境の名前をラベルとして付与し、孤立したリソースの回収ではそのラベルが自環境と一致するVMのみを対象とします。ラベルを持たない古いVMの所有者は推測しません。リスト自体は引き続き全体を参照するため、DBに登録されている古いVMを「存在しないリソース」と誤認することもありません。修正後に学生APIで再度開いたセッションは90.991秒で準備が完了し、終了後にVM・VMI・Secretがすべて消失していることを確認しました(1回)。
障害2: 1行の失敗が回収全体を停止させた
2026-10-14のサイトセキュリティ点検で発見された問題です。期限切れのセッションを削除するループ内で、1つの削除処理が例外を発生させると、 そのサイクル全体が停止していました。 同じ行が毎分同じ場所で失敗し続ける間、その背後にある他の期限切れセッションも、孤立したリソースも、幽霊セッションも、誰も回収されないまま蓄積されていきました。
現在は行ごとに失敗を分離して処理しています。
for r in rows:
try:
delete_session_resource(r)
except Exception as exc:
failed += 1 # 실패를 세고
print(f"[reaper] 만료 세션 회수 실패 {str(r['id'])[:8]}: {type(exc).__name__}", flush=True)
continue # 기록은 그대로 두어 다음 바퀴에 다시 본다
con.execute("update sessions set status='expired', finished_at=now() where id=%s", (r["id"],))
ステータスを expired に変更するのは、 削除に成功した後です。順序が逆になると、削除できなかったVMが「終了したセッション」として記録され、二度と回収対象にならなくなります。失敗の件数は管理者画面に表示される回収ステータスと一緒に記録されます。
同じ点検において、実習用 Podにも期限(activeDeadlineSeconds = 60分 × 3 + 15分)を追加しました。Web PodとDBが同時に停止し、回収機能がまったく動作しなくなった場合でも、期限切れのrootシェルが永遠に残り続けなようにするためです。正常なセッションは延長上限(180分)よりも早く終了するため、この期限に達することはありません。
3. 何を、どの順序で削除するか
バックエンドが直接削除するものは 2つのみです。
VirtualMachineオブジェクト- cloud-init Secret(採点トークンが含まれているもの)
残りはKubernetesとKubeVirtが順番に削除します。順序は以下の通りです。
| 順序 | 対象 | 実行者 | 根拠 |
|---|---|---|---|
| 1 | VMの削除 | バックエンド | delete_vm_session |
| 2 | VMIの停止・削除 | KubeVirt | VMがVMIを所有している |
| 3 | virt-launcher Podの終了 | KubeVirt・Kubernetes | Podはゲストに終了シグナルを渡し、VMがシャットダウンするまで待機する(猶予期間30秒) |
| 4 | 書き込みレイヤー(emptyDir)の削除 | Kubernetes | emptyDirのライフサイクルはPodと同じである |
| 5 | DataVolumeとそのPVCの削除 | Kubernetesガベージコレクター | VMが dataVolumeTemplates で作成したDataVolumeを所有している(永続ディスク実習のみ) |
| — | ゴールデンPVC | 誰も削除しない | 所有者がおらず、回収機能が選択するラベルもない |
バックエンドが直接削除の順序を管理しなくてよい理由の1つとして、Kubernetesの PVC保護(Storage Object in Use Protection)が挙げられます。Kubernetesのドキュメントによると、Podで使用中のPVCは削除命令を出しても、そのPodが使用を停止するまで実際の削除が延期されます。そのため、VM → Pod → ボリュームの順序が自然に維持され、ゲストがまだ使用しているディスクが先に消えることはありません。ゴールデンを削除する際も同様の保護機能が働き、使用中のVMがすべて終了するまでゴールデンは「削除中」の状態にとどまります(パート4)。
3ノード構成のセッションでは、代表となるVMを1つ削除すると、残りのVM・Secret・セッション専用ネットワークのポリシーやサービスがラベルとともにまとめて削除されます。実測では、代表の1つを削除してから15秒後に3台のVM・3つのSecret・サービス・ポリシーがすべて0になり、2つ目のワーカーを作成中に失敗させした場合でも、すでに作成されたものはすべて回収されました(2026-09-24)。
Secret なぜ個別にもう一度削除するのか
cloud-init SecretにはVMを所有者として指定しているため、VMが削除されるとガベージコレクターが一緒に削除するはずです。しかし、 その所有者の関連付けに失敗することがあります。 VMを作成した後にSecretに所有者を設定するのは別の修正リクエストとなりますが、セキュリティ点検の記録によると、バックエンドのサービスアカウントにSecretの 修正権限がないため 、この関連付けが常に失敗しているように見えるということが分かりました。実際、VMが存在しない状態で32日間残り続けたcloud-init Secretが1つ発見されました。そのため、バックエンドはVMを削除した後、Secretを名前で再度削除します。残存していると、同じセッション名が再び割り当てられた際に古いcloud-initが復活してしまうためです。残ったSecretの整理と権限の修正は人間の手で行う作業として記録されています。
4. 上限: 被害の規模を制限する
回収機能がいかに堅牢であっても失敗することはあります。そのため、「最悪の場合にどれだけリソースがリークするか」を上限値によって制限しています。
| 上限 | 値 | 設定箇所 |
|---|---|---|
| 同時実習VM数 | 8台 | バックエンド(クラスターに問い合わせてカウント) |
| VMネームスペースのクォータ — VM数 | 8 | ResourceQuota |
| 同クォータ — CPUリクエスト / メモリリクエスト | 16 / 48Gi | ResourceQuota |
| 同クォータ — PVC数 | 12 | ResourceQuota(ゴールデンおよび取得中のスクラッチのみ使用) |
二重になっている理由は、バックエンドの上限でカウントが正確に行えない場合(KubeVirtの不安定化によりリストが取得できない場合、バックエンド側で上限をかけるとすべてのVM実習が同時に停止してしまうため、バックエンドでは制限をかけません)に、最後にクォータがブロックしてくれるためです。
数字の「8」を引き上げなかった理由も記録されています。複数のVMで構成されるセッションを作成した際(2026-09-24)、KVMを実際に提供するノードのメモリリクエストの空きを合算しても約17GiBであり、測定中に1つのノードがシステムのOOMにより約1分間NotReady状態に陥る現象も発生しました。 クォータを引き上げても、拒否の理由が「空き容量がない」からスケジューリングの失敗とノードへの負荷に変わるだけです。ノードのメモリが先に不足します。VMのメモリリクエストにはディスカウントがなく(パート2)、ノードがメモリ不足に陥った際に最初に終了するのはVMではなく同一ノード上の実習用Podである点も、上限を保守的に設定している理由です。
5. 残された隙間
- VMには、Podの期限のような最終的な安全装置が存在しません。 回収機能が停止すると、期限切れのVMはそのまま残ります。その際の被害を抑えるのは上記の上限(8台、48Gi)のみです。VMオブジェクトに対して期限に相当する設定を適用する方法は 未確認です。
- VMの孤立リソースのクリーンアップはまとめて一括で失敗します。 期限切れの処理ループでは行ごとに失敗を分離していますが、孤立したVMをスキャンするループは、KubeVirtが不安定なときに回収処理全体がクラッシュしないよう全体をラップしているだけであるため、1つの孤立リソースを削除できないとそのサイクルの残りのVMの孤立削除は次のサイクルに持ち越されます。ログには記録されます。
- 学生セッションにおける「期限切れからVMが消失するまで」にかかる時間の分布は 未実測です。回収周期が60秒なのでおよそ1分弱と考えられますが、ゲスト終了猶予(30秒)まで加えた実測値は測定していません。
TTの一言。 消去コードは短いほど良いです。バックエンドはたった2つだけ消去し、残りは「誰が誰を所有するか」と「使用中なら消去しない」に任せます。そして失敗した行はスキップして次の周回でもう一度!1つのせいで全てが止まってはいけないからです。
参考
ドキュメントはすべて2026-10-16に閲覧しました。
- Kubernetesドキュメント、永続ボリューム(使用中のストレージオブジェクトの保護): https://kubernetes.io/docs/concepts/storage/persistent-volumes/
- KubeVirtコンポーネントドキュメント(virt-launcherの終了処理): https://github.com/kubevirt/kubevirt/blob/main/docs/components.md
- TT Labコード:
backend/app/main.py(reap_once·reaper·delete_vm_session·delete_session_resource·extend_session·count_live_vm_sessions),backend/app/vm_session_scope.py,backend/app/vm_group.py,deploy/vms/rbac.yaml(ResourceQuota) - 障害と点検の記録:
docs/plans/2026-09-12-vm-reaper-environment.md,docs/plans/2026-09-24-multi-vm-sessions.md,docs/SECURITY-REVIEW-2026-10-14.md