- 들어가며 — rpm이 모인 디렉터리는 아직 저장소가 아닙니다
- createrepo_c로 메타데이터 만들기
- .repo 파일로 등록하기
- GPG 키를 반입하고 등록하기
- 캐시가 갱신을 삼킵니다
- 저장소가 실제로 동작하는지 확인하기
- 마치며 — 저장소는 파일이 아니라 계약입니다
- 직접 해보기
- 이전 / 다음 편
- 참고 자료
들어가며 — rpm이 모인 디렉터리는 아직 저장소가 아닙니다
2편에서 만든 것은 rpm 파일이 담긴 디렉터리입니다. 그 상태로 폐쇄망에 들고 들어가서 dnf install을 하면 여전히 의존성을 못 찾습니다.
dnf는 rpm 파일을 하나씩 열어 보지 않기 때문입니다. dnf가 읽는 것은 저장소 메타데이터이고, 그건 별도로 생성해 줘야 합니다. 그 생성 도구가 createrepo_c입니다.
이번 편에서는 그 메타데이터를 만들고, 저장소로 등록하고, 서명을 검증하게 만들고, 마지막으로 폐쇄망에서 가장 자주 사람을 잡는 캐시 문제까지 정리합니다.
createrepo_c로 메타데이터 만들기
가장 기본 형태입니다.
# 저장소 도구 설치 (연결망 장비에서 미리 받아 함께 반입해야 합니다)
sudo dnf install createrepo_c
# 디렉터리를 저장소로 만든다
sudo createrepo_c /srv/repo/rhel9-baseos
명령이 끝나면 대상 디렉터리 안에 repodata가 생깁니다. 안을 들여다보면 구조가 보입니다.
ls -1 /srv/repo/rhel9-baseos/repodata/
# repomd.xml
# <checksum>-primary.xml.gz
# <checksum>-filelists.xml.gz
# <checksum>-other.xml.gz
repomd.xml이 색인이고 나머지가 실제 데이터입니다. primary는 패키지의 이름과 버전, 의존성 정보를, filelists는 패키지가 담고 있는 파일 목록을, other는 changelog를 담습니다. 파일 이름 앞에 체크섬이 붙는 이유는 man 페이지에 나옵니다. --unique-md-filenames가 "Include file's checksum in metadata filename for HTTP caching (default)"이고 기본값입니다. 내용이 바뀌면 파일 이름도 바뀌므로 중간 프록시가 오래된 사본을 돌려주는 사고를 막아 줍니다.
실무에서 자주 쓰는 옵션은 이 정도입니다.
# 패키지를 추가한 뒤 전체 재생성 대신 증분 갱신
sudo createrepo_c --update /srv/repo/rhel9-baseos
# 워커를 늘려 대형 저장소 생성 시간을 줄인다
sudo createrepo_c --workers 8 /srv/repo/rhel9-baseos
# 패키지 그룹(comps) 정보를 함께 포함시킨다
sudo createrepo_c --groupfile /srv/repo/comps.xml /srv/repo/rhel9-baseos
man 페이지 기준으로 각 옵션의 정의는 이렇습니다.
| 옵션 | man 페이지 설명 |
|---|---|
--update | "Reuse existing metadata for unchanged rpms based on file size and mtime." |
--workers | "Number of workers to spawn for reading rpms." |
-g, --groupfile | "Path to groupfile to include in metadata." |
-s, --checksum | "Choose the checksum type used in repomd.xml and for packages in the metadata. The default is now sha256." |
--compress-type | "Compression type (bz2, gz, zck, zstd, xz)." |
-i, --pkglist | "Text file containing complete list of packages to include." |
--retain-old-md NUM | "Keep old repodata (0 removes all, positive numbers specify copies to retain)." |
-x, --excludes | "Path patterns to exclude, can be specified multiple times." |
-o, --outputdir | "Optional output directory." |
-d, --database는 man 페이지에 "DEPRECATED: Generate sqlite databases for use with yum"이라고 표시되어 있습니다. 오래된 절차서에서 이 옵션을 보면 지워도 됩니다.
--pkglist는 폐쇄망에서 특히 쓸모가 있습니다. 4편에서 만들 반입 매니페스트를 그대로 입력으로 넘기면, 매니페스트에 적힌 것만 정확히 저장소에 들어갑니다. 목록과 실제 저장소가 어긋나는 사고를 구조적으로 막는 방법입니다.
.repo 파일로 등록하기
메타데이터가 있어도 dnf가 그 위치를 모르면 소용없습니다. RHEL 9 문서는 명확합니다. 저장소는 /etc/dnf/dnf.conf나 /etc/yum.repos.d/ 아래 .repo 파일에 정의하며, "Define your repositories in the .repo file instead of /etc/dnf/dnf.conf"라고 권고합니다.
서버 한 대에서 로컬 디렉터리를 직접 쓰는 경우입니다.
# /etc/yum.repos.d/airgap-local.repo
[airgap-baseos]
name=RHEL 9 BaseOS (airgap local, snapshot 2026-08-15)
baseurl=file:///srv/repo/rhel9-baseos
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
metadata_expire=-1
[airgap-appstream]
name=RHEL 9 AppStream (airgap local, snapshot 2026-08-15)
baseurl=file:///srv/repo/rhel9-appstream
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
metadata_expire=-1
여러 서버에 배포한다면 사내 HTTP 서버 한 대를 세우고 baseurl만 바꿉니다.
# /etc/yum.repos.d/airgap-internal.repo
[airgap-baseos]
name=RHEL 9 BaseOS (internal mirror, snapshot 2026-08-15)
baseurl=http://repo.internal.example.com/rhel9/baseos
enabled=1
gpgcheck=1
gpgkey=http://repo.internal.example.com/keys/RPM-GPG-KEY-redhat-release
metadata_expire=-1
priority=1
이름에 스냅샷 날짜를 박아 둔 것은 취향이 아닙니다. 6개월 뒤 이 서버가 어느 시점 콘텐츠로 설치됐는지 알아내는 가장 빠른 방법이 dnf repolist -v 출력이기 때문입니다.
dnf 설정 문서 기준으로 각 항목의 의미는 이렇습니다. baseurl은 "List of URLs for the repository", enabled는 "Include this repository as a package source. The default is True", gpgcheck는 "Whether to perform GPG signature check on packages found in this repository. The default is False", gpgkey는 "URLs of a GPG key files that can be used for signing metadata and packages of this repository", priority는 "The priority value of this repository, default is 99"입니다.
gpgcheck의 기본값이 거짓이라는 점을 반드시 기억해야 합니다. 적어 두지 않으면 검사하지 않습니다. 폐쇄망에서는 이게 특히 위험한데, 이유는 4편에서 다룹니다.
명령으로 저장소를 추가할 수도 있고, 여기서 버전 차이가 나옵니다.
# RHEL 9 / RHEL 10
sudo dnf config-manager --add-repo http://repo.internal.example.com/rhel9/baseos
# RHEL 8 (공식 문서 표기)
sudo yum-config-manager --add-repo http://repo.internal.example.com/rhel8/baseos
RHEL 8 공식 문서는 yum-config-manager --add-repo로, RHEL 9와 RHEL 10 문서는 dnf config-manager --add-repo로 적고 있습니다. 두 문서 모두 "repositories added by this command are enabled by default"라고 덧붙입니다.
GPG 키를 반입하고 등록하기
gpgcheck=1로 켜 두면 첫 설치에서 키를 요구합니다. 폐쇄망에는 키를 자동으로 가져올 경로가 없으므로 키 파일도 반입 대상입니다.
키 파일의 정확한 경로와 이름은 배포판과 버전에 따라 다르므로, 이 글은 특정 파일명을 단정하지 않습니다. 연결망 장비에서 먼저 확인하고 그 파일을 번들에 담으세요. 아래 예시의 파일명은 확인한 실제 이름으로 바꿔 써야 합니다.
# 먼저 이 시스템이 어떤 GPG 키 파일을 갖고 있는지 확인한다
ls -1 /etc/pki/rpm-gpg/
# 릴리스 패키지가 설치한 키 파일을 정확히 짚는다
rpm -ql redhat-release | grep -i gpg
# 현재 저장소 설정이 어떤 키를 참조하는지 확인한다
grep -h '^gpgkey=' /etc/yum.repos.d/*.repo | sort -u
# 확인한 키를 반입해 표준 위치에 배치
sudo cp ./keys/RPM-GPG-KEY-redhat-release /etc/pki/rpm-gpg/
# 키를 rpm 키링에 등록한다
sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
# 등록된 키 확인
rpm -q gpg-pubkey --qf '%{NAME}-%{VERSION}-%{RELEASE} %{SUMMARY}\n'
여기서 버전 관련 주의사항이 하나 있습니다. 최신 업스트림 RPM의 man 페이지는 -K, --checksig, --import를 "Obsolete compatibility aliases"로 분류하고 rpmkeys(8)을 보라고 안내합니다. RHEL 8·9·10에서는 rpm --import가 그대로 동작하지만, 앞으로를 생각하면 rpmkeys 표기를 쓰는 편이 안전합니다.
# 앞으로 권장되는 표기
sudo rpmkeys --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
# 등록된 키를 지문과 사용자 ID로 나열
rpmkeys --list
rpmkeys man 페이지는 -i, --import를 "Import ASCII-armored public keys. Digital signatures cannot be verified without the corresponding public key (aka certificate)", -l, --list를 "List currently imported public key(s) (aka certificates) by their fingerprint and user ID"로 정의합니다.
패키지 서명과 메타데이터 서명은 다른 설정이라는 점도 짚어 둡니다. gpgcheck는 패키지에 대한 검사이고, repo_gpgcheck는 "Whether to perform GPG signature check on this repository's metadata"이며 기본값은 역시 거짓입니다. 직접 만든 저장소의 메타데이터에 서명을 붙이려면 별도 절차가 필요하고, 붙이지 않았다면 repo_gpgcheck를 켜면 안 됩니다.
캐시가 갱신을 삼킵니다
폐쇄망에서 가장 자주 나오는 신고가 이겁니다. 저장소에 패키지를 추가했는데 서버에서 안 보인다는 것.
원인은 대개 메타데이터 캐시입니다. dnf 설정 문서는 metadata_expire를 "The period after which the remote repository is checked for metadata update and in the positive case the local metadata cache is updated. The default corresponds to 48 hours"로 정의합니다. 즉 기본 상태에서는 최대 이틀 동안 옛 메타데이터를 그대로 씁니다.
저장소를 갱신했다면 이렇게 처리합니다.
# 1. 저장소 쪽: 메타데이터를 다시 만든다 (이걸 빼먹는 경우가 정말 많습니다)
sudo createrepo_c --update /srv/repo/rhel9-baseos
# 2. 클라이언트 쪽: 캐시를 만료 표시만 한다 (가장 가벼움)
sudo dnf clean expire-cache
# 3. 그래도 안 되면 메타데이터 캐시를 지운다
sudo dnf clean metadata
# 4. 최후의 수단 — 전부 지운다
sudo dnf clean all
# 5. 캐시를 미리 다시 채운다
sudo dnf makecache
dnf 문서의 정의는 각각 expire-cache가 "Marks the repository metadata expired", metadata가 "Removes repository metadata", packages가 "Removes any cached packages from the system", all이 "Does all of the above"입니다. 위에서부터 순서대로 시도하는 편이 좋습니다. dnf clean all은 폐쇄망에서 특히 아까운데, 지운 캐시를 다시 채울 인터넷이 없기 때문입니다. 다만 반입 저장소는 로컬이라 재생성 비용이 낮습니다.
앞의 .repo 예시에서 metadata_expire=-1을 쓴 이유가 여기 있습니다. 반입 저장소는 사람이 명시적으로 갱신하기 전까지 절대 바뀌지 않으므로, 만료로 인한 재검사가 의미 없습니다. 갱신 시점은 사람이 dnf clean expire-cache로 알려 주는 편이 예측 가능합니다.
저장소가 실제로 동작하는지 확인하기
등록이 끝났으면 세 가지를 확인합니다.
# 1. 저장소가 인식되고 패키지 수가 0이 아닌가
dnf repolist -v
# 2. 이 저장소만으로 의존성이 다 풀리는가
dnf repoclosure --repo=airgap-baseos --repo=airgap-appstream
# 3. 실제 설치가 되는가 (트랜잭션만 확인하고 멈춤)
sudo dnf install --assumeno httpd
2번은 2편에서 소개한 repoclosure입니다. 반입 전에 한 번, 반입 후 실제 저장소 구성에서 한 번 더 돌리는 것이 좋습니다. 반입 과정에서 파일이 누락되는 일이 실제로 일어나기 때문입니다.
서브스크립션 없이 Red Hat 콘텐츠를 재배포하는 것은 계약 위반일 수 있으니 조직의 라이선스 조건을 먼저 확인하세요. 특히 사내 HTTP 미러는 "재배포"의 성격을 띠므로, 구성 전에 계약 조건을 확인하는 편이 안전합니다.
마치며 — 저장소는 파일이 아니라 계약입니다
createrepo_c 자체는 명령 한 줄입니다. 어려운 부분은 그 뒤에 있습니다.
메타데이터를 다시 만들었는가, 키를 반입해 등록했는가, gpgcheck를 명시했는가, 캐시를 만료시켰는가. 이 네 가지가 폐쇄망 저장소 장애의 대부분을 차지합니다. 넷 다 확인 명령이 한 줄씩이니 절차서에 그대로 넣어 두면 됩니다.
명령과 옵션은 2026-08-15에 공식 문서에서 확인했습니다. RHEL 버전에 따라 다르므로 사용 중인 버전의 문서로 다시 확인하세요.
직접 해보기
- 리눅스 터미널 — 저장소 디렉터리 구조를 만들어 보며 경로 감각 익히기
- chmod 계산기 — HTTP로 배포할 저장소 디렉터리의 권한 설계
- 해시 생성기 — repomd.xml의 체크섬 개념을 직접 확인해 보기
이전 / 다음 편
참고 자료
현재 단락 (1/102)
2편에서 만든 것은 rpm 파일이 담긴 디렉터리입니다. 그 상태로 폐쇄망에 들고 들어가서 `dnf install`을 하면 여전히 의존성을 못 찾습니다.