pip は二台のコンピュータに分かれる
一言でいうと
エアギャップ環境のpipは、2台のコンピューターに分かれます。ダウンロード側は、インターネットからwheelを集めてハッシュを書き、利用側は、そのwheelを社内インデックスとして立てて、pip.confの1行で指し示します。2つをつなぐのは、ファイルのまとまりと、ハッシュの一覧だけです。
なぜ必要なのか
分析サーバーでpip install requestsを実行すると、エアギャップ環境ではCould not find a version that satisfies the requirementと出ます。外部でrequestsのwheelを1つだけ取得して持ってくると、今度は、urllib3・idna・certifi・charset_normalizerがないと言われます。持ち込み審査が1日に1回の組織では、この往復が1日ずつかかります。しかも、取得するコンピューター(ノートパソコン、Python 3.12)と、使うサーバー(Python 3.11)が違うと、持ってきたwheelのうち、コンパイル済みのものがサーバーで合いません。このラボのイメージでrequests 2.32.3を取得してみると、charset_normalizerだけが、cp312-cp312-manylinux...x86_64というタグが付いたプラットフォームwheelで、残りの4つはpy3-none-anyです。Pythonのバージョンが変わると、まさにその1つが合わなくなります。
どう動くのか
ダウンロード側: pip download -d <디렉터리> <요구사항>は(プレースホルダーはディレクトリと要件です)、インストールせずに依存関係を解決して、ファイルだけを集めます。対象サーバーが違う場合は、--platform・--python-version・--implementation・--abiで、対象を指定します。pipのドキュメントは、これらのオプションを使うとき、--only-binary=:all:か--no-depsが必ず必要だと書いています。ソース配布物を取得して、今のコンピューターでビルドしてしまうと、対象サーバーとずれるからです。
バージョンとハッシュを固定します: pip hash <파일>は(プレースホルダーはファイルです)、--hash=sha256:<값>の1行を出力します(プレースホルダーはハッシュ値です)。requirements.txtの行ごとに、이름==버전 --hash=sha256:...を書いておくと(プレースホルダーは名前とバージョンです)、pipのドキュメントにあるとおり、どれか1つの要件にでも--hashがあれば、ハッシュ検証モードが全体で有効になります。このモードでは、すべての要件が==で固定されている必要があり、取得したファイルのハッシュが1つでも違えば、インストールが止まります。持ち込み媒体を通る間に書き換えられたwheelが、黙って入る道がふさがれます。
利用側: 社内インデックス: pipが読むインデックスは、PEP 503(今はpackaging.python.orgのSimple repository API仕様)が定めた、単純なHTMLです。/simple/<정규화한 이름>/に(プレースホルダーは正規化した名前です)、ファイル名を文字としたリンクがあれば十分です。名前の正規化は、小文字に変えて、.・-・_が連なった部分を、-1つに変えることです。そのため、charset_normalizerのwheelは、/simple/charset-normalizer/にあって初めて見つかります。Pythonの標準ライブラリのhttp.serverは、ディレクトリの一覧を、まさにその形(ファイル名が文字のリンク)で出すので、ディレクトリを正規化した名前で作ってwheelを入れるだけでも、読み取り専用のインデックスになります。pypiserverのようなツールがしていることも、骨格はこれです。
指し示す(pip.conf): Linuxでpipは、設定を、グローバル(/etc/xdg/pip/pip.conf、/etc/pip.conf)→ ユーザー(~/.config/pip/pip.conf、古い場所は~/.pip/pip.conf)→ サイト($VIRTUAL_ENV/pip.conf)→ PIP_CONFIG_FILEの順に読み、後で読んだ値が、前の値を上書きします。環境変数はファイルより、コマンドラインのオプションは環境変数より、優先されます。pip config debugが、どのファイルが実際に読まれたかを見せてくれます。グローバルファイルにindex-urlを書けば、サーバーのすべてのvenvが、社内インデックスを見ます。
インデックスすらないとき: --no-index --find-links <디렉터리>は(プレースホルダーはディレクトリです)、インデックスをまったく無視して、そのディレクトリのファイルだけを見ます。USBで持ち込んだwheelhouseをそのまま使う方法で、wheelhouseが自己完結しているかの確認テストでもあります。
現場での姿
社内インデックスをhttp://で立ててindex-urlだけを書くと、pipはそのアドレスを信頼しません。localhostや127.0.0.1は安全な場所として扱いますが、社内の名前はそうではないからです。このラボでpypi.airgap.internalで試してみると、trusted-hostを外した瞬間、pipは警告だけを残して、そのインデックスを無視してしまい、結果はNo matching distribution foundです。インデックスが空のように見えますが、実際には、見ていないのです。実務では、社内インデックスをHTTPSで立てて、プライベートCAを信頼させるほうが正しいです(後のモジュールで扱います)。
2つ目によくある事故は、PEP 668です。Ubuntu 24.04のシステムPythonは、EXTERNALLY-MANAGEDのマーカーがあるため、venvの外でのpip installを拒否します。--break-system-packagesで押し込まず、venvを作成します。グローバルの/etc/pip.confは、venvの中のpipも読むので、設定は1回で済みます。
証明書の話も1つあります。pipのドキュメントによると、pip 24.2からは、Python 3.10以上で、truststoreによりOSの証明書ストアを一緒に使い、それ以前は、certifiのバンドルだけを使っていました。このラボのイメージのpipは、Ubuntuパッケージの24.0ですが、Ubuntuが修正したバージョンなので、/etc/ssl/certs/ca-certificates.crtを読みます(実測)。同じ24.0でも、PyPIから取得したpipは、違う動作をする可能性があるので、プライベートCAを使う場所では、cert設定やPIP_CERTで、バンドルを明示するほうが安全です。
次のラボですること
requests 2.32.3を、このPodのPython用と、エアギャップ環境のサーバーのPython 3.11用に、それぞれ取得して、ハッシュを埋め込んだrequirements.txtを作成します。/srv/pypi/simpleに、正規化したディレクトリでインデックスを立てて、pypi.airgap.internal:8080として起動し、/etc/pip.confで指し示したあと、外部がふさがれた状態でも取得できるかを、採点が確認します。最後に、インデックスなしで、wheelhouseだけでインストールします。
参考ドキュメント: pip download・pip Configuration・Secure installs(ハッシュ検証モード)・Simple repository API・HTTPS Certificates