概要

社内の機密データを扱うAI基盤は、インターネットから切り離した閉域ネットワークに置かれることがあります。通常のサーバーアプリと違い、GPUで推論するAI基盤は、GPU向けのPythonパッケージ一式やモデルの重みなど、数GB単位の依存物を必要とします。インターネットに接続できる前提で書かれた手順は、閉域ではそのまま使えません。

本記事では、DigitalBaseのLinux向けインストーラを設計・改修してきた経験をもとに、閉域・オフライン環境への導入と更新で考えるべき点を整理します。配布形態の選び方、USB媒体による導入と更新、RHEL系での注意点、パッケージ更新時のサービス再起動、そして 失敗をどこで止め、どこで先に進めるか を順に取り上げます。


配布形態の選び方

AI基盤の配布形態には、主に次の3つがあります。

形態向いている環境更新の方法注意点
deb・rpm パッケージOSのパッケージ管理で資産を管理している環境apt / dnf で新しいパッケージを入れる更新時のサービスの扱いをパッケージ側で正しく書く必要がある
コンテナ(Docker Compose)Dockerの運用に慣れている環境イメージを取得し直して起動し直す閉域ではイメージを社内のレジストリか媒体で持ち込む
Python wheel評価・検証用、Python環境を自前で管理するエンジニアuv tool upgrade など閉域では社内のパッケージインデックスに同じwheelを置く

deb・rpm

パッケージにすると、ファイルの配置(実行ファイル、設定ファイル、systemdのユニット)をOSの仕組みで管理できます。設定ファイルは「存在しなければ雛形から作り、更新では上書きしない」という扱いにしておくと、更新のたびに設定が消える事故を防げます。

コンテナ

コンテナは、OSの差を吸収しやすい形態です。ただし配布の形(Compose、Helm chart、Kubernetesのマニフェストなど)を増やすと、それぞれの保守が必要になります。配布はComposeの1つに絞り、Kubernetesで動かす場合はComposeと同じ構成を利用者側のマニフェストで組んでもらう、という割り切りも有効です。

Python wheel

uv や pipx の隔離環境に入れる形は、評価用として手軽です。閉域でも、社内のパッケージインデックスに同じwheelを置けば、接続先を変えるだけで同じコマンドで入ります。共有のPython環境に直接入れると他のパッケージと名前が衝突することがあるため、隔離環境に入れる方が安全です。


USB媒体で導入・更新する

GPUで推論するAI基盤を閉域の機に入れるには、推論エンジン(vLLM)とGPU向けのPyTorchなど、数GBのwheel一式を持ち込む必要があります。社内の構成では、vLLMを含む媒体で約4GBになりました。

媒体はインターネットに接続できる機で作る

媒体は、インターネットに接続でき、入れる先と同じCPUアーキテクチャ(x86_64またはarm64) の機で作ります。GPUは不要で、作る機には何もインストールしません。媒体に入れるものは次のとおりです。

中身理由
インストーラと本体、そのsha256持ち込んだ本体が壊れていないかを照合する
uv とPythonの配布物閉域の機に特定のバージョンのPythonを事前に用意しなくて済む
依存関係を解決したwheel一式GPUの種類ごとの入手先から、オンラインと同じものを取得しておく
媒体の種類を記録したマニフェストエンジンやGPUの種類、バージョンを記録し、入れる側で判定に使う
モデルのキャッシュ(任意)HuggingFaceに接続できない機でモデルを使う

wheelは入れる先の要件で選ぶ

wheelは、媒体を作る機の環境ではなく、入れる先のOSの要件 に合わせて選びます。作る機のglibcに合わせてしまうと、閉域の機で動かないことがあります。uv pip compile で対象のプラットフォームを指定して依存関係を解決し、そのURLのファイルをそのまま取得します。

uv pip compile requirements.txt \ --python-version 3.13 \ --python-platform x86_64-manylinux_2_39 \ --format pylock.toml -o pylock.toml

閉域の機では、インデックスを使わずに媒体のディレクトリだけから入れます。

uv pip install --python <venv>/bin/python --no-index --find-links <媒体>/wheels vllm

依存関係の中にソースしか配布されていないものがあれば、媒体を作る時点で止めます。閉域の機にはビルド環境がないことが多く、そこで気づいても対処できないためです。

媒体に入らないもの

PostgreSQL(pgvector)や、ffmpeg・tesseractなどのOSパッケージは媒体に入れていません。これらはOSのパッケージ管理の範囲なので、閉域の機に先に入れておきます。また、導入済みの機のディレクトリを別の機にコピーしても動きません。仮想環境は絶対パスを持ち、設定ファイルには機ごとの秘密情報が入っているためです。

更新の流れ

DigitalBaseでは、USB媒体を挿すと管理画面がマウント先(/media、/mnt、/run/media の配下)からマニフェストを探し、「USBの媒体から更新」を表示します。CPUアーキテクチャやエンジンの種類が今の機と違う媒体は一覧には出しますが、更新はできないようにしています。USBメモリが noexec でマウントされていても動くよう、本体は設置先にコピーしてから実行します。


更新を安全に進める順序

閉域では、更新に失敗したときにインターネットから入れ直すことができません。そのため、今動いているものを壊さない順序が重要です。

  1. 同時実行を防ぐ:同じ設置先で2つの更新が重なると、後の方が仮想環境を作り直して先の方が壊れる。ロックファイルで2つ目を断る
  2. 入るかを先に確かめる:uv pip install --dry-run で依存関係が解決できるかを確認する。解決できなければ、サービスを止めずに何も変えずに中断する
  3. 止めてから入れ替える:確認が済んでから、サービスを止めて仮想環境を作り直す
  4. 入ったものを確かめる:vLLMとtorchのビルドが、そのGPU向けかを確認する
  5. 本体は名前を変えて置き換える:一時的な名前で置いてから入れ替え、途中で止まったときの残りは次の実行で片付ける
  6. 動いていたものだけ起動し直す:更新前に動いていた場合だけ、サービスを再開する

古いバージョンを指定して入れようとした場合は、新しいバージョンが書いたデータを読めなくなる恐れがあるため、警告を出します。


RHEL系での注意点

SELinuxとsystemd

RHEL系でSELinuxが有効な環境では、systemdがホームディレクトリ配下のスクリプトを直接起動しようとすると、SELinuxに止められます(status=203/EXEC)。シェル経由で起動すれば通り、更新でファイルのラベルが戻っても起動に失敗しません。

[Service] Type=simple WorkingDirectory=/home/<user>/app ExecStart=/bin/bash /home/<user>/app/run.sh ExecStopPost=-/bin/bash /home/<user>/app/notify-exit.sh Restart=always RestartSec=5

SELinuxが有効かどうかは selinuxenabled の終了コードで判定できます。インストーラでこれを確かめ、有効なときだけこの形のユニットを書くようにしておくと、SELinuxが無効な環境のユニットはそのままにできます。

OSパッケージは1つずつ入れる

RHEL 10では、ffmpegは標準のリポジトリになく(EPELの ffmpeg-free)、ninja-build はCRBリポジトリにしかありません。必要なパッケージを1回の dnf install でまとめて指定すると、1つ見つからないだけで全部が入りません。1つずつ入れ、入らなかったものだけをCRBとEPELの有効化の手順とともに表示します。

PostgreSQLとpgvector

RHEL系の postgresql パッケージはクライアントだけで、サーバーは別のパッケージです。初期化と起動も自分で行います。

sudo dnf install -y postgresql-server postgresql-contrib pgvector sudo postgresql-setup --initdb sudo systemctl enable --now postgresql

pgvectorのパッケージ名は、aptとdnfで異なります。Ubuntuでは PostgreSQLのメジャーバージョンを含むパッケージ名 を指定する必要があり、dnfでは pgvector だけで入ります。

sudo apt install -y postgresql-$(psql -V | grep -oE '[0-9]+' | head -1)-pgvector sudo dnf install -y pgvector

また、RHEL系の初期設定では localhost へのTCP接続の認証が ident になっており、アプリがパスワードで接続できないことがあります。この場合は pg_hba.conf の該当行を scram-sha-256 に変えて再読み込みします。


パッケージ更新時にサービスを止めない

deb・rpmでは、更新時にもアンインストール用のスクリプトが呼ばれます。「アンインストール前にサービスを止めて無効化する」とだけ書いていると、更新のたびにサービスが止まり、自動起動も外れたまま になります。

スクリプトに渡される引数で、更新と削除を区別します。

debrpm
削除前(preremove)remove / purge(削除)、upgrade(更新)0(削除)、1(更新)
導入後(postinstall)configure <前のバージョン>(更新)2(更新)
case "${1:-remove}" in remove|purge|0) systemctl stop app 2>/dev/null || true systemctl disable app 2>/dev/null || true ;; esac exit 0

debでは、postinstallに前のバージョンが渡されたときに新しい本体で再起動します。rpmでは注意が必要です。rpmの更新では、新しいパッケージのpostinstallの 後に 古いパッケージのpreremoveが実行されます。そのため、postinstallで再起動しても、その後に古いパッケージのスクリプトがサービスを止めてしまうことがあります。

そこで、取引の最初に実行される pretrans で「更新である」という目印を /run に置き、取引の最後に実行される posttrans で自動起動を戻して再起動し、目印を消します。こうすると、更新でもサービスを止めてしまう古いパッケージから更新する場合にも対応できます。


途中で止めず、最後にまとめて知らせる

閉域の機では、インストーラを実行する担当者がその場で原因を調べる手段が限られます。1つの手順の失敗でインストーラ全体が止まると、残りの手順が実行されないまま、どこまで進んだのかも分かりにくくなります。

そこで、止める失敗と、警告して先に進める失敗を分けます。

扱い該当する失敗
止める続けると動いているものが壊れるとき(入らないエンジンのバージョンで仮想環境を消す前、本体の取得や照合の失敗、同時実行)、頼まれたことがそもそもできないとき(不正な引数など)
警告して進めるpgvectorの有効化、データベースの作成、OSのパッケージ、systemdへの登録、GPUドライバの確認など

警告して進めた項目は、最後に [ACTION NEEDED] としてまとめ、直すためのコマンドとともに表示します。

[ACTION NEEDED] The database ... is not usable by ... Create it as the postgres user: sudo -u postgres psql -c "CREATE USER ..." -c "CREATE DATABASE ... OWNER ..."

判定には psql の終了コードと標準出力だけを使います。照合順序のずれなどの WARNING や、sudo が出す文言を「存在する」と取り違えないためです。最後にアプリと同じ資格情報で実際に接続し、接続できなければその理由と直し方を返します。


まとめ

閉域・オフライン環境にAI基盤を導入・更新する際の要点は、次のとおりです。

  1. 配布形態を環境に合わせて選ぶ:OSのパッケージ管理ならdeb・rpm、Docker運用ならCompose、評価ならPython wheel
  2. 媒体は入れる先の要件で作る:同じCPUアーキテクチャの機で、入れる先のOSの要件に合うwheelを集め、Pythonの配布物も同梱する
  3. 今動いているものを壊さない:依存関係が解決できることを確かめてからサービスを止める
  4. RHEL系の差を前提にする:SELinuxではシェル経由で起動し、PostgreSQLのサーバーとpgvectorのパッケージ名の違いに注意する
  5. パッケージ更新でサービスを止めない:スクリプトの引数で更新と削除を区別し、rpmは posttrans で再起動する
  6. 失敗は最後にまとめて知らせる:壊れるときだけ止め、それ以外は警告して進め、直し方を最後に一覧で示す

閉域の機では、手順の途中で問題が起きても、その場で調べる手段が限られます。インストーラ自体が「何ができて、何が残っているか」を最後に示すことで、現地での導入と更新を確実に進められます。


関連記事