シンボリックリンクの迷宮から脱却する:update-alternativesによるJavaとPythonのバージョン管理

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

依存関係の競合という脆い現実

昨年、私は移行作業中に崩壊しかけていた14個のマイクロサービスに追われていました。Java 8のレガシーなアプリケーションと、Java 17や21で構築された新しいサービスが混在していたのです。さらに悪いことに、データ処理スクリプトはPython 3.10と3.12が入り混じった混乱状態でした。チームは最初、JAVA_HOMEに絶対パスをハードコードしたり、.bashrcに大量の汚いエイリアスを詰め込んだりして凌ごうとしましたが、それは今にも崩れそうな砂上の楼閣でした。

すべてが崩壊したのは、Ubuntuノードでの日常的なapt upgradeによって、グローバルの/usr/bin/javaがオーケストレーションツールと互換性のないバージョンに切り替わった時でした。その15分間のダウンタイムが決定打となりました。私は40台以上のサーバーupdate-alternativesシステムを使用して標準化することに決めました。このLinux標準のユーティリティは、OSの動作に必要なシステム依存関係を壊すことなく、同じソフトウェアの複数バージョンを管理できます。

交換機:Alternativesシステムの仕組み

update-alternativesを賢い交換機だと考えてください。/usr/bin/javaがバイナリを直接指すのではなく、3層の階層構造を作成します:

  • 一般名(Generic Name): /usr/bin/javaが交換機にリンクします。
  • 宛先リンク(Destination Link): /etc/alternatives/javaが仲介役として機能します。
  • 実際のバイナリ(Actual Binary): 最終的に/usr/lib/jvm/java-17-openjdk-amd64/bin/javaなどのパスに解決されます。

中間のリンクだけを変更することで、グローバルなツールのバージョンを切り替えることができます。システムはこれらの選択を/var/lib/dpkg/alternatives/にあるデータベースで追跡します。各バージョンには優先度スコアがあります。「auto」モードでは、システムが最も高い数値を選択します。「manual」モードでは、選択が固定されます。新しいパッケージをインストールしても、勝手に切り替わることはありません。

Javaバージョンの精密な管理

Javaはこのツールの本領が発揮される分野です。多くのディストリビューションはOpenJDKの各バージョンを/usr/lib/jvm/に配置しますが、それらが正しく登録されているとは限りません。3つの特定のバージョンをセットアップした際の手順は以下の通りです:

sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-8-openjdk-amd64/bin/java 1081
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-17-openjdk-amd64/bin/java 1171
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-21-openjdk-amd64/bin/java 1211

末尾の数字(1081、1171、1211)が階層を定義します。Java 21が最も高いスコアを持つため、デフォルトになります。手動でバージョンを切り替えるには、configフラグを使用します:

sudo update-alternatives --config java

これにより、シンプルな対話型メニューが開きます。4GBのRAMを搭載したUbuntu 22.04ノードでは、この変更により起動の信頼性が向上しました。JAVA_HOMEを特定するために使用していた肥大化したラッパースクリプトを排除できたからです。現在では、カーネルがシンボリックリンクの解決を即座に処理します。

コンパイラとツールの同期を保つ

エンジニアがjavaランタイムを更新しても、javac(コンパイラ)やjarを更新し忘れるのをよく見かけます。これはビルド中に”Unsupported Class Version”エラーを引き起こす原因になります。常にセット全体を構成することを忘れないでください:

sudo update-alternatives --config javac
sudo update-alternatives --config jar

Python:システムのデフォルトを尊重する

Pythonは少し勝手が異なります。プロジェクトにはvenvが最適ですが、グローバルなシェルでpython3が何を行うかを制御する必要がある場合もあります。しかし、ここでは慎重に行動する必要があります。Ubuntuでは、aptnetplanなどの主要なシステムユーティリティが、OSに同梱されている特定のPythonバージョンに依存しているからです。

私はPythonのバージョンを次のように登録しています:

sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 2

重要な警告: グローバルのpython3を新しいバージョンに切り替えた際に、システムスクリプトに互換性がない場合、パッケージマネージャーが壊れてしまいます。私はこれらのグローバルな切り替えを、ビルドエージェントやCIランナーに厳格に限定しています。本番のウェブサーバーでは、システムPythonには手を触れず、代わりに仮想環境を使用しています。

Slaveを使用してGCCとG++をグループ化する

パフォーマンスが重視されるバックエンドでは、新しい最適化フラグを利用するために特定のGCCバージョンが必要になることがよくあります。--slaveフラグを使用すると、gccg++が常にペアとして切り替わるように設定できます。

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 --slave /usr/bin/g++ g++ /usr/bin/g++-11
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 120 --slave /usr/bin/g++ g++ /usr/bin/g++-12

この連携は強力です。設定メニューから新しいgccバージョンを選択すると、g++コンパイラも自動的に追従します。これにより、あるバージョンでコードをコンパイルし、別のバージョンでリンクするという悪夢を防ぐことができます。

6ヶ月後:その結果

40台のサーバーで半年間運用した結果、最大の収穫は予測可能性でした。「自分のマシンでは動くのに」という言葉を聞くことはなくなりました。すべての開発者が、update-alternatives --display javaが唯一の真実のソースであることを知っています。メニューを整理するために古いバージョンを削除する必要がある場合も、コマンド一つで済みます:

sudo update-alternatives --remove java /usr/lib/jvm/java-8-openjdk-amd64/bin/java

結論

update-alternativesツールは、一つの仕事を完璧にこなす古典的なLinuxユーティリティです。これは、厳格なシステムディレクトリと、現代の開発における柔軟なニーズとの間の架け橋となります。手動のシンボリックリンクから脱却したことで、本番環境は安定し、デプロイスクリプトの複雑さを30%近く削減できました。もしいまだに/usr/binのリンクを手動で編集しているなら、その負担をシステムに任せるべき時です。

Share: