pip 는 두 대의 컴퓨터로 나뉜다
한 줄 요약
폐쇄망의 pip 는 두 대의 컴퓨터로 나뉜다. 받는 쪽은 인터넷에서 휠을 모으고 해시를 적고, 쓰는 쪽은 그 휠을 사내 인덱스로 세워 pip.conf 한 줄로 가리킨다. 둘 사이를 잇는 것은 파일 묶음과 해시 목록뿐이다.
왜 이게 필요했나
분석 서버에 pip install requests 를 치면 폐쇄망에서는 Could not find a version that satisfies the requirement 가 나온다. 바깥에서 requests 휠 하나만 받아 오면 이번에는 urllib3·idna·certifi·charset_normalizer 가 없다고 한다. 반입 심의가 하루 한 번인 조직에서 이 왕복은 하루씩이다. 게다가 받는 컴퓨터(노트북, 파이썬 3.12)와 쓰는 서버(파이썬 3.11)가 다르면, 받아 온 휠 가운데 컴파일된 것이 서버에서 맞지 않는다. 이 실습 이미지에서 requests 2.32.3 을 받아 보면 charset_normalizer 만 cp312-cp312-manylinux...x86_64 태그가 붙은 플랫폼 휠이고 나머지 넷은 py3-none-any 다. 파이썬 버전이 바뀌면 정확히 그 하나가 틀린다.
어떻게 동작하나
받는 쪽. pip download -d <디렉터리> <요구사항> 은 설치하지 않고 의존성을 풀어 파일만 모은다. 대상 서버가 다르면 --platform·--python-version·--implementation·--abi 로 대상을 적는다. pip 문서는 이 옵션들을 쓸 때 --only-binary=:all: 이나 --no-deps 가 반드시 필요하다고 적는다. 소스 배포본을 받아 지금 컴퓨터에서 빌드해 버리면 대상 서버와 어긋나기 때문이다.
버전과 해시를 고정한다. pip hash <파일> 은 --hash=sha256:<값> 한 줄을 낸다. requirements.txt 의 줄마다 이름==버전 --hash=sha256:... 을 적어 두면, pip 문서의 말대로 어느 한 요구사항에라도 --hash 가 있으면 해시 검사 모드가 전체에 켜진다. 이 모드에서는 모든 요구사항이 == 로 고정되어야 하고, 받은 파일의 해시가 하나라도 다르면 설치가 멈춘다. 반입 매체를 지나며 바뀐 휠이 조용히 들어가는 길이 막힌다.
쓰는 쪽: 사내 인덱스. pip 가 읽는 인덱스는 PEP 503(지금은 packaging.python.org 의 Simple repository API 규격)이 정한 단순한 HTML 이다. /simple/<정규화한 이름>/ 에 파일 이름을 글자로 한 링크가 있으면 된다. 이름 정규화는 소문자로 바꾸고 .·-·_ 가 이어진 곳을 - 하나로 바꾸는 것이다. 그래서 charset_normalizer 휠은 /simple/charset-normalizer/ 에 있어야 찾힌다. 파이썬 표준 라이브러리의 http.server 는 디렉터리 목록을 바로 그 모양(파일 이름이 글자인 링크)으로 내주므로, 디렉터리를 정규화한 이름으로 만들고 휠을 넣기만 해도 읽기 전용 인덱스가 된다. pypiserver 같은 도구가 하는 일도 뼈대는 이것이다.
가리키기: pip.conf. 리눅스에서 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 로 들고 들어간 휠하우스를 그대로 쓰는 방법이고, 휠하우스가 스스로 완결되어 있는지 확인하는 시험이기도 하다.
현장에서 만나는 모습
사내 인덱스를 http:// 로 세우고 index-url 만 적으면 pip 가 그 주소를 믿지 않는다. localhost·127.0.0.1 은 안전한 곳으로 치지만 사내 이름은 아니기 때문이다. 이 실습에서 pypi.airgap.internal 로 재 보면 trusted-host 를 빼는 순간 pip 는 경고만 남기고 그 인덱스를 무시해 버리고, 결과는 No matching distribution found 다. 인덱스가 비어 있는 것처럼 보이지만 사실은 쳐다보지 않은 것이다. 실무에서는 사내 인덱스를 HTTPS 로 세우고 사설 CA 를 믿게 하는 쪽이 맞다(뒤 모듈에서 다룬다).
두 번째로 흔한 사고는 PEP 668 이다. 우분투 24.04 의 시스템 파이썬은 EXTERNALLY-MANAGED 표식이 있어서 venv 밖의 pip install 을 거절한다. --break-system-packages 로 밀어 넣지 말고 venv 를 만든다. 전역 /etc/pip.conf 는 venv 안의 pip 도 읽으므로 설정은 한 번이면 된다.
인증서 이야기도 하나. pip 문서에 따르면 pip 24.2 부터는 파이썬 3.10 이상에서 truststore 로 운영체제 인증서 저장소를 함께 쓰고, 그 전에는 certifi 묶음만 썼다. 이 실습 이미지의 pip 는 우분투 패키지 24.0 인데, 우분투가 고친 판이라 /etc/ssl/certs/ca-certificates.crt 를 읽는다(실측). 같은 24.0 이라도 PyPI 에서 받은 pip 는 다르게 동작할 수 있으니, 사설 CA 를 쓰는 곳에서는 cert 설정이나 PIP_CERT 로 묶음을 명시하는 편이 안전하다.
다음 실습에서 할 것
requests 2.32.3 을 이 파드의 파이썬용과 폐쇄망 서버의 파이썬 3.11 용으로 각각 받고, 해시를 박은 requirements.txt 를 만든다. /srv/pypi/simple 에 정규화한 디렉터리로 인덱스를 세워 pypi.airgap.internal:8080 으로 띄우고, /etc/pip.conf 로 가리킨 뒤, 바깥이 막힌 상태에서도 받아지는지 채점기가 확인한다. 마지막에는 인덱스 없이 휠하우스만으로 설치한다.
참고 문서: pip download · pip Configuration · Secure installs(해시 검사 모드) · Simple repository API · HTTPS Certificates