- Published on
RHEL 폐쇄망 설치가 어려운 진짜 이유 — 의존성은 그래프이고, 그 그래프는 안쪽에서 풀 수 없습니다
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — "rpm 파일 하나만 받아다 주세요"
- 의존성은 목록이 아니라 그래프입니다
- 그래프를 푸는 쪽과 설치하는 쪽이 서로 다른 네트워크에 있습니다
- Red Hat 콘텐츠는 어떻게 전달되는가
- 저장소는 움직이는 표적입니다
- 모듈러리티가 층을 하나 더 얹습니다
- 이 시리즈가 다루는 것
- 마치며 — 문제를 다시 정의하면 절차가 나옵니다
- 직접 해보기
- 다음 편
- 참고 자료
들어가며 — "rpm 파일 하나만 받아다 주세요"
폐쇄망을 운영해 본 사람이라면 이 요청을 받아 봤을 겁니다. 안쪽 서버에 도구 하나를 깔아야 하는데 네트워크가 없으니, 바깥 장비에서 rpm 파일을 하나 내려받아 USB에 담아 달라는 부탁입니다.
그리고 그 파일을 들고 들어가서 설치하면 이런 화면을 봅니다.
# 반입한 rpm 하나만 놓고 설치를 시도하면 벌어지는 일
sudo dnf install ./htop-3.2.2-1.el9.x86_64.rpm
# Error:
# Problem: conflicting requests
# - nothing provides libnl-3.so.200()(64bit) needed by htop-3.2.2-1.el9.x86_64
여기서 없다는 그 라이브러리를 또 받아 오면, 그 라이브러리가 요구하는 다른 것이 없다고 나옵니다. 서너 번 왕복하다 보면 반나절이 지나 있고, 반입 심의는 하루에 한 번뿐입니다.
이 시리즈는 그 왕복을 없애는 방법을 다룹니다. 첫 편에서는 명령어 대신 구조를 봅니다. 왜 이 문제가 파일 복사 문제가 아닌지를 이해하면 나머지 여섯 편의 명령이 전부 당연해집니다.
의존성은 목록이 아니라 그래프입니다
가장 흔한 오해는 "패키지 A는 B, C를 필요로 한다"는 식의 평평한 목록을 상상하는 것입니다. RPM의 의존성은 그렇게 생기지 않았습니다.
RPM에서 의존성의 단위는 패키지 이름이 아니라 능력(capability) 입니다. 패키지는 자신이 제공하는 능력을 Provides로 선언하고, 필요한 능력을 Requires로 선언합니다. 그 능력에는 패키지 이름뿐 아니라 공유 라이브러리의 소네임, 파일 경로, 심지어 심볼 버전까지 들어갑니다. 앞의 에러에 나온 그 문자열이 바로 그것입니다.
실제 크기를 눈으로 확인해 보면 감이 잡힙니다. 인터넷이 연결된 장비에서 이렇게 물어봅니다.
# httpd가 직접 요구하는 능력 목록 — 패키지 이름이 아니라 심볼과 파일이 섞여 나옵니다
dnf repoquery --requires httpd
# 그 능력들을 실제로 제공하는 패키지로 치환하고, 재귀적으로 끝까지 따라갑니다
dnf repoquery --requires --resolve --recursive httpd | sort -u | wc -l
--requires는 의존 능력을 보여 주고, --resolve는 그 능력을 제공하는 패키지로 바꿔 주며, --recursive는 거기서 멈추지 않고 계속 내려갑니다. 이 세 옵션은 모두 DNF 공식 명령 레퍼런스에 있습니다. 결과 줄 수는 시스템 상태에 따라 다르지만, 손으로 따라갈 만한 숫자가 아니라는 것은 어느 환경에서든 같습니다.
여기에 함정이 하나 더 있습니다. dnf repoquery --tree나 depsolver의 판단은 이미 설치된 것을 빼고 계산합니다. 연결망 장비에 이미 깔려 있는 라이브러리는 "필요 없음"으로 처리되어 결과에서 사라집니다. 그 결과를 그대로 폐쇄망에 들고 들어가면, 안쪽 서버에는 그게 없어서 다시 막힙니다. 2편에서 --installroot로 이 함정을 정면으로 다룹니다.
그래프를 푸는 쪽과 설치하는 쪽이 서로 다른 네트워크에 있습니다
이 문제의 본질은 여기입니다.
의존성을 푸는 주체는 depsolver이고, depsolver의 입력은 저장소 메타데이터 전체입니다. 어떤 패키지가 어떤 능력을 제공하는지 알아야 조합을 찾을 수 있기 때문입니다. 그런데 폐쇄망 안쪽에는 그 메타데이터가 없습니다. 그래서 안쪽에서는 계산 자체가 불가능합니다.
바깥쪽에는 메타데이터가 있지만, 설치할 시스템의 상태를 모릅니다. 계산할 수 있는 쪽과 답이 필요한 쪽이 분리되어 있는 것, 이것이 폐쇄망 패키지 설치의 전부입니다.
그래서 해법은 두 갈래뿐입니다.
- 해결된 결과를 옮긴다. 바깥에서 의존성을 다 풀어 rpm 파일 묶음을 만들고, 그 묶음만 반입합니다. 필요한 것이 명확하고 규모가 작을 때 씁니다.
- 저장소를 통째로 옮긴다. 저장소를 미러링해 반입한 뒤, 안쪽에서 depsolver를 정상적으로 돌립니다. 앞으로 무엇을 깔지 모를 때, 그리고 여러 서버를 오래 운영할 때 씁니다.
2편이 첫 번째, 3편이 두 번째입니다. 대부분의 조직은 결국 두 번째로 갑니다.
Red Hat 콘텐츠는 어떻게 전달되는가
RHEL 9 공식 문서는 콘텐츠 저장소를 이렇게 구분합니다. BaseOS는 모든 설치의 기반이 되는 핵심 운영체제 기능을, AppStream은 사용자 공간 애플리케이션과 런타임 언어, 데이터베이스를 담습니다. 문서는 이 둘 모두가 RHEL에 필수이며 모든 RHEL 서브스크립션에 포함된다고 명시합니다. CodeReady Linux Builder는 모든 서브스크립션에 함께 제공되지만 개발자용이며 Red Hat이 지원하지 않는 패키지가 들어 있습니다.
이 콘텐츠에 접근하려면 시스템이 등록되고 저장소가 켜져 있어야 합니다. 연결망 장비에서 상태를 확인하는 흐름은 이렇습니다.
# 시스템 등록 및 구독 상태 확인
sudo subscription-manager status
# 이 시스템에 켜져 있는 저장소 ID 목록
sudo subscription-manager repos --list-enabled | grep "^Repo ID"
# 필요한 저장소를 켠다 (아키텍처·버전에 따라 ID가 다릅니다)
sudo subscription-manager repos \
--enable rhel-9-for-x86_64-baseos-rpms \
--enable rhel-9-for-x86_64-appstream-rpms
여기서 반드시 짚고 넘어갈 것이 있습니다. 서브스크립션 없이 Red Hat 콘텐츠를 재배포하는 것은 계약 위반일 수 있으니 조직의 라이선스 조건을 먼저 확인하세요. 이 시리즈는 엔타이틀먼트를 우회하는 방법을 다루지 않습니다. 어떤 단계에 엔타이틀먼트가 필요하면 필요하다고 적고 넘어갑니다. 사내 미러를 만들어 사내 시스템에 배포하는 것이 조직의 계약으로 허용되는지는 법무나 Red Hat 담당자에게 확인할 문제이지, 기술로 결정할 문제가 아닙니다.
서브스크립션이 없어서 실습을 못 하는 독자라면 CentOS Stream, Rocky Linux, AlmaLinux, Fedora가 현실적인 대체재입니다. dnf·createrepo_c·rpm의 동작은 대부분 그대로 재현됩니다. 다만 저장소 ID와 GPG 키, 그리고 마이너 버전 고정 방식은 배포판마다 다르므로, 절차를 옮겨 적을 때는 그 부분을 각자 환경 기준으로 다시 확인해야 합니다.
저장소는 움직이는 표적입니다
같은 명령을 오늘과 2주 뒤에 실행하면 다른 결과가 나옵니다. 보안 업데이트가 들어오고, 마이너 릴리스가 올라가고, 패키지가 obsolete 처리되기 때문입니다.
이게 왜 문제냐면, 폐쇄망에서는 반입이 사건이기 때문입니다. 심의를 거쳐 한 번 들어간 번들로 여러 서버를 설치하고, 몇 달 뒤 서버 한 대를 더 늘릴 때 같은 번들을 다시 씁니다. 그때 "그냥 다시 받으면 되지"가 통하지 않습니다. 다시 받으면 다른 것이 들어오고, 기존 서버들과 버전이 어긋납니다.
그래서 반입 단위에는 항상 세 가지가 같이 들어가야 합니다. 저장소 스냅샷을 뜬 날짜, 사용한 릴리스 버전, 그리고 패키지별 정확한 NEVRA와 체크섬입니다. 4편에서 이 매니페스트를 구체적으로 만듭니다.
모듈러리티가 층을 하나 더 얹습니다
RHEL 8은 AppStream 안에 모듈이라는 개념을 도입했습니다. 같은 컴포넌트의 여러 버전을 스트림으로 나눠 한 저장소에 담고, 그중 하나만 활성화해 쓰는 방식입니다. 문제는 이렇게 되면 의존성 해결이 RPM 수준과 모듈 수준에서 동시에 일어난다는 점입니다. RHEL 9 문서의 표현을 빌리면 모듈 의존성은 일반 RPM 의존성 위에 얹힌 추가 계층이고, 저장소 사이의 가상 의존성처럼 동작합니다.
폐쇄망에서 이게 왜 아픈지는 5편에서 자세히 다루지만, 지금 알아야 할 것은 버전마다 상황이 완전히 다르다는 사실입니다. 공식 문서 기준으로 정리하면 이렇습니다.
| 항목 | RHEL 8 | RHEL 9 | RHEL 10 |
|---|---|---|---|
| 문서상 주 명령 | yum (dnf의 별칭) | dnf | dnf (dnf5 기반) |
| 모듈 기본 스트림 | 있음, 기본 스트림이 자동 활성화됨 | 미리 정의된 기본 스트림 없음 | 해당 없음 |
| 모듈 제공 여부 | AppStream의 핵심 방식 | 9.1부터 수명주기가 짧은 추가 버전으로 제공 | 공식 문서에 모듈 장 자체가 없음 |
| 스트림 전환 | distro-sync → module reset → module enable → distro-sync | dnf module switch-to 한 줄 | 해당 없음 |
| 저장소 추가 도구 | yum-config-manager --add-repo | dnf config-manager --add-repo | dnf config-manager --add-repo |
RHEL 10의 공식 "Managing software with the DNF tool" 문서에는 모듈 관련 장이 아예 존재하지 않고, Application Stream의 제공 형식도 RPM과 Software Collections 두 가지만 명시됩니다. RHEL 8 절차서를 그대로 RHEL 10에 들고 가면 존재하지 않는 명령을 실행하게 된다는 뜻입니다.
이 시리즈가 다루는 것
폐쇄망 반입을 처음부터 끝까지, 한 번 만들면 6개월 뒤에도 같은 결과가 나오는 절차로 만드는 것이 목표입니다.
- 왜 어려운가 (이 글)
- 밖에서 받기 —
dnf download,reposync,yumdownloader의 차이와--installroot - 로컬 저장소 만들기 —
createrepo_c, repodata,.repo파일과 GPG 키 - 반입과 무결성 — 체크섬, 서명 검증, 재현 가능한 번들 매니페스트
- 모듈과 버전 고정 — 마이너 버전 잠그기와 상태 재현
- 컨테이너 이미지 —
podman save와skopeo, 사내 레지스트리 - 운영 — 보안 패치 주기, 롤백, CVE 대응 지연 관리
마치며 — 문제를 다시 정의하면 절차가 나옵니다
폐쇄망 설치는 파일을 옮기는 문제가 아닙니다. 계산할 수 있는 쪽에서 계산을 끝내고, 그 결과를 재현 가능한 형태로 옮기는 문제입니다.
이렇게 정의하면 나머지가 따라옵니다. 계산을 끝내려면 깨끗한 루트에서 풀어야 하고, 결과를 재현하려면 스냅샷 시점과 릴리스 버전을 박아 두어야 하고, 옮긴 것을 믿으려면 서명을 확인해야 합니다. 그게 이 시리즈의 나머지 여섯 편입니다.
명령과 옵션은 2026-08-15에 공식 문서에서 확인했습니다. RHEL 버전에 따라 다르므로 사용 중인 버전의 문서로 다시 확인하세요.
직접 해보기
- 리눅스 터미널 —
dnf와rpm명령의 형태를 손에 익히기 - 리눅스 명령어 퀴즈 — 패키지 관리 명령 복습
- 리눅스 에뮬레이터 — 파일시스템 구조와 경로 감각 익히기