Skip to content
Published on

폐쇄망 컨테이너 이미지 반입 — podman save와 skopeo는 다른 도구입니다

공유하기
Authors

들어가며 — RPM 다음에는 반드시 이미지가 옵니다

패키지 반입 절차가 자리를 잡으면 바로 다음 요구가 들어옵니다. 컨테이너 이미지도 같은 매체로 들여보내 달라는 것입니다.

도구는 두 계열이 있고 자주 혼동됩니다.

  • podman save / podman load로컬 이미지 저장소를 거칩니다. 먼저 pull 해서 로컬에 두고, 그걸 파일로 내보내고, 안쪽에서 다시 로컬로 불러들입니다
  • skopeo copy / skopeo sync로컬 저장소를 거치지 않습니다. 출발지에서 목적지로 직접 옮깁니다

이미지가 한두 개면 어느 쪽이든 됩니다. 수십 개가 되고 그게 정기 작업이 되면 차이가 분명해집니다.

트랜스포트를 먼저 이해합니다

skopeo를 쓰려면 트랜스포트 표기를 알아야 합니다. containers-transports 문서 기준으로 폐쇄망에서 쓰는 것은 이 정도입니다.

표기문서상 의미
docker://"An image in a registry implementing the 'Docker Registry HTTP API V2'."
dir:"An existing local directory path storing the manifest, layer tarballs and signatures as individual files."
docker-archive:"An image is stored in the docker-save(1) formatted file."
oci:"An image in a directory structure compliant with the 'Open Container Image Layout Specification' at path."
oci-archive:"a tar(1) archive with contents compliant with the 'Open Container Image Layout Specification'."
containers-storage:"An image located in a local containers storage."

docker-archive:에는 문서가 명시하는 함정이 있습니다. "a write to a docker-archive: destination completely overwrites path, replacing it with the single provided image." 같은 tar 파일에 이미지를 하나씩 추가하려다 앞의 것을 지우는 사고가 여기서 나옵니다. 여러 이미지를 한 파일에 담으려면 dir:이나 oci:를 쓰거나, podman save의 다중 이미지 옵션을 써야 합니다.

이름 해석 규칙도 기억해 두면 좋습니다. 문서는 "If name does not contain a slash, it is treated as docker.io/library/name"라고 정의합니다. 폐쇄망에서 짧은 이름을 쓰면 존재하지 않는 외부 호스트를 향하게 된다는 뜻입니다.

podman save와 load — 단순하고 확실한 경로

이미지 몇 개를 옮기는 데는 이쪽이 편합니다.

# 연결망 장비: 이미지를 받는다 (대상 아키텍처를 명시)
podman pull --arch amd64 registry.access.redhat.com/ubi9/ubi:9.4

# 단일 이미지를 아카이브로 내보낸다
podman save --quiet -o ubi9.tar registry.access.redhat.com/ubi9/ubi:9.4

# 여러 이미지를 하나의 아카이브에 담는다 (docker-archive 형식에서만 지원)
podman save --multi-image-archive -o bundle.tar \
  registry.access.redhat.com/ubi9/ubi:9.4 \
  registry.access.redhat.com/ubi9/nginx-124:latest

# OCI 형식으로 내보낸다
podman save -o ubi9-oci.tar --format oci-archive registry.access.redhat.com/ubi9/ubi:9.4

--format이 받는 값은 문서에 네 가지로 명시되어 있습니다. docker-archive는 "A tar archive interoperable with docker load(1)", oci-archive는 "A tar archive using the OCI Image Format", oci-dir는 "A directory using the OCI Image Format", docker-dir는 "dir transport with v2s2 manifest type"입니다.

--multi-image-archive, -m은 "Allow for creating archives with more than one image. Additional names are interpreted as images instead of tags. Only supported for --format=docker-archive"입니다. OCI 형식과는 함께 쓸 수 없습니다.

안쪽에서 불러들이는 쪽은 더 단순합니다.

# 폐쇄망: 아카이브에서 로컬 저장소로 불러온다
podman load -i bundle.tar

# 불러온 이미지 확인
podman images

podman load의 옵션은 --input, -i--quiet, -q 그리고 도움말뿐입니다. 문서는 이 명령이 "restores an archive created by podman save as the same image, preserving its layers, history and tags"라고 설명합니다.

문서가 덧붙이는 운영 주의사항이 하나 있습니다. "Use the environment variable TMPDIR to change the temporary storage location of container images. Podman defaults to use /var/tmp." 대용량 이미지를 불러올 때 임시 공간이 부족해 실패하는 경우가 있으니, 폐쇄망 서버의 파티션 구성에 따라 이 변수를 지정해야 할 수 있습니다.

주의: podman save에는 서명 관련 옵션이 없습니다. 공식 문서의 옵션 목록에는 --compress, --format, --help, --multi-image-archive, --output, --quiet, --uncompressed 일곱 가지뿐입니다. 이미지에 서명을 붙이려면 뒤에 나오는 skopeo 쪽을 써야 합니다.

skopeo copy — 로컬을 거치지 않고 옮깁니다

skopeo copy의 문서 설명에는 폐쇄망에서 중요한 문장이 있습니다. "Uses the system's trust policy to validate images, rejects images not trusted by the policy." 복사 자체가 정책 검사를 거친다는 뜻이라, 정책을 제대로 설정해 두면 반입 경로가 곧 검증 경로가 됩니다.

# 레지스트리에서 반입 매체의 디렉터리로
skopeo copy \
  docker://registry.access.redhat.com/ubi9/ubi:9.4 \
  dir:/media/transfer/images/ubi9

# 반입 매체에서 사내 레지스트리로
skopeo copy \
  dir:/media/transfer/images/ubi9 \
  docker://registry.internal.example.com/base/ubi9:9.4

# 레지스트리에서 레지스트리로 직접 (양쪽에 닿을 수 있는 경우)
skopeo copy \
  docker://quay.io/skopeo/stable:latest \
  docker://registry.internal.example.com/skopeo:latest

폐쇄망에서 놓치기 쉬운 옵션 두 개를 짚습니다.

# 멀티 아키텍처 이미지를 전부 복사한다
skopeo copy --all \
  docker://registry.access.redhat.com/ubi9/ubi:9.4 \
  dir:/media/transfer/images/ubi9

# 필요한 아키텍처만 골라서 복사한다
skopeo copy --multi-arch=linux/amd64,linux/arm64 \
  docker://quay.io/skopeo/stable:latest \
  docker://registry.internal.example.com/skopeo:latest

# 재시도를 켠다 (기본값은 재시도 없음)
skopeo copy --retry-times 3 \
  docker://registry.access.redhat.com/ubi9/ubi:9.4 \
  dir:/media/transfer/images/ubi9

--all, -a의 문서 설명은 "If source-image refers to a list of images, instead of copying just the image which matches the current OS and architecture ... attempt to copy all of the images in the list, and the list itself"입니다. 기본 동작이 현재 시스템에 맞는 하나만 복사하는 것이라는 점이 핵심입니다. x86_64 노트북에서 받아 ARM 서버로 반입하면 에러 없이 잘못된 것이 들어갑니다.

--multi-arch는 기본값이 system이며 all, index-only, 그리고 쉼표로 구분한 플랫폼 목록을 받습니다. 문서는 index-only와 플랫폼 목록이 "sparse manifest lists"를 만들며 "usually fail unless the referenced per-architecture images are already present in the destination, or the target registry supports sparse indexes"라고 경고합니다.

--retry-times는 "The number of times to retry. By default, no retries are attempted"입니다. 기본값이 재시도 없음이라는 점은 대용량 이미지를 다루는 반입 스크립트에서 반드시 챙겨야 합니다.

skopeo sync — 폐쇄망 미러링의 정답

여러 이미지를 정기적으로 옮긴다면 이쪽입니다. 공식 문서의 설명 문장이 그대로 이 시리즈의 주제입니다.

"Synchronize images between registry repositories and local directories. Synchronization is achieved by copying all the images found at source to destination - useful when synchronizing a local container registry mirror or for populating registries running inside of air-gapped environments."

출발지와 목적지의 트랜스포트를 별도 플래그로 받는 것이 copy와 다른 점입니다. 소스는 docker, dir, yaml을, 목적지는 docker, dir을 받습니다.

# 연결망: 저장소의 모든 태그를 반입 매체 디렉터리로
skopeo sync --src docker --dest dir \
  registry.access.redhat.com/ubi9/ubi /media/transfer/images

# 폐쇄망: 매체에서 사내 레지스트리로
skopeo sync --src dir --dest docker \
  /media/transfer/images/ubi:9.4 registry.internal.example.com/base/

# 같은 이름의 이미지가 여러 출처에서 올 때 경로 충돌을 막는다
skopeo sync --src docker --dest dir --scoped \
  registry.access.redhat.com/ubi9/ubi /media/transfer/images

# 실제로 옮기지 않고 무엇이 옮겨질지만 본다
skopeo sync --src docker --dest dir --dry-run \
  registry.access.redhat.com/ubi9/ubi /media/transfer/images

문서는 소스가 docker일 때 "If no image tag is specified, skopeo sync copies all the tags found in that repository"라고 명시합니다. 태그를 지정하지 않으면 전부 가져온다는 뜻이니, 반입 용량을 계산할 때 주의해야 합니다.

--scoped는 "Prefix images with the source image path, so that multiple images with the same name can be stored at destination"입니다. 문서 예시에 따르면 이 옵션을 쓰면 매체에 registry.example.com/busybox:1-glibc 형태의 경로가 만들어지고, 쓰지 않으면 busybox:1-glibc가 됩니다. 여러 레지스트리에서 같은 이름을 가져올 때 반드시 필요합니다.

--dry-run("Run the sync without actually copying data to the destination")과 --keep-going("If any errors occur during copying of images, those errors are logged and the process continues syncing rest of the images and finally fails at the end")은 반입 작업 자동화에 특히 유용합니다.

반입 목록이 커지면 YAML로 관리하는 편이 낫습니다. 문서가 정의하는 형식은 이렇습니다.

# sync.yml — 반입 대상 이미지 목록
registry.access.redhat.com:
    images:
        ubi9/ubi:
            - "9.4"
            - "latest"
        ubi9/nginx-124: []
    images-by-tag-regex:
        ubi9/python-311: ^3\.11-[0-9]+$
    tls-verify: true
quay.io:
    images:
        skopeo/stable:
            - latest
# YAML 목록대로 한 번에 동기화한다
skopeo sync --src yaml --dest docker sync.yml registry.internal.example.com/mirror/

문서에 따르면 빈 목록은 모든 태그를 의미하고, 태그 자리에 다이제스트를 적을 수도 있으며, images-by-tag-regex는 정규식으로, images-by-semver는 semver 제약으로 태그를 고릅니다. 이 파일 자체를 반입 기록으로 남기면 4편의 매니페스트와 같은 역할을 합니다.

skopeo sync에는 --multi-arch가 없습니다. 멀티 아키텍처가 필요하면 --all, -a를 씁니다.

서명을 실제로 강제합니다

폐쇄망에서 이미지 서명이 의미를 가지려면 검증이 강제되어야 합니다. 그 설정이 policy.json입니다.

문서에 따르면 정책은 기본적으로 $HOME/.config/containers/policy.json에서 읽고, 없으면 /etc/containers/policy.json에서 읽습니다. 구조는 전역 default와 트랜스포트별 transports 두 부분이며, "The global default set of policy requirements is mandatory"입니다.

{
    "default": [{"type": "reject"}],
    "transports": {
        "docker": {
            "registry.internal.example.com": [
                {
                    "type": "signedBy",
                    "keyType": "GPGKeys",
                    "keyPath": "/etc/pki/containers/internal-signing-key.gpg"
                }
            ]
        },
        "dir": {
            "": [{"type": "insecureAcceptAnything"}]
        }
    }
}

요구 유형별 의미는 문서에 이렇게 정의되어 있습니다. reject는 "This requirement rejects every image, and every signature", insecureAcceptAnything은 "This requirement accepts any image (but note that other requirements in the array still apply)", signedBy는 "This requirement requires an image to be signed using "simple signing" with an expected identity, or accepts a signature if it is using an expected identity and key"입니다. keyType은 문서상 현재 GPGKeys만 지원되며, keyPath, keyPaths, keyData 중 정확히 하나가 있어야 합니다.

매칭 규칙도 알아 둘 필요가 있습니다. "If multiple policy requirements match a given image, only the requirements from the most specific match apply, the more general policy requirements definitions are ignored." 넓은 범위에 엄격한 정책을 걸어 두고 좁은 범위에 예외를 두면, 그 좁은 범위에서는 넓은 쪽 정책이 적용되지 않습니다.

서명을 붙이는 것은 skopeo 쪽입니다.

# 사내 레지스트리로 밀어 넣으면서 서명한다
skopeo copy --sign-by security@example.com \
  dir:/media/transfer/images/ubi9 \
  docker://registry.internal.example.com/base/ubi9:9.4

--sign-by는 "Add a "simple signing" signature using that key ID for an image name corresponding to destination-image"입니다. sigstore 방식이 필요하면 --sign-by-sigstore-private-key가 있습니다.

레지스트리가 서명 저장을 지원하지 않는다면 분리 서명 저장소를 씁니다. registries.d 문서는 lookaside를 "URL of the signature storage. This URL is used for reading existing signatures, and if lookaside-staging does not exist, also for adding or removing them"로, lookaside-staging을 "URL of the signature storage, used for editing it (adding or deleting signatures)"로 정의합니다.

# /etc/containers/registries.d/internal.yaml
docker:
    registry.internal.example.com:
        lookaside: http://sigstore.internal.example.com/signatures
        lookaside-staging: file:///srv/signatures-staging

문서는 sigstoresigstore-staging 키가 더 이상 문서화되지 않는 옛 이름이라고 소스 주석에 명시하고 있으므로, 새로 작성한다면 lookaside 표기를 쓰세요.

대상 아키텍처를 반드시 지정합니다

이 절은 짧지만 실패 빈도가 높습니다.

# 아키텍처를 명시해서 받는다
podman pull --arch arm64 registry.access.redhat.com/ubi9/ubi:9.4

# OS와 아키텍처를 한 번에 (--arch, --os와 함께 쓸 수 없음)
podman pull --platform linux/arm64 registry.access.redhat.com/ubi9/ubi:9.4

--arch는 "Override the architecture, defaults to the host, of the image to be pulled", --platform은 "Specify the platform for selecting the image. (Conflicts with --arch and --os)"입니다. 기본값이 호스트라는 점이 문제의 출발점입니다.

--all-tags, -a에는 별도의 경고가 붙어 있습니다. "IMPORTANT: When using the all-tags flag, Podman does not iterate over the search registries in the containers-registries.conf(5) but always uses docker.io for unqualified image names." 폐쇄망 준비 작업에서 짧은 이름으로 이 옵션을 쓰면 의도하지 않은 레지스트리를 향합니다.

쿠버네티스까지 이어집니다

여기까지가 이미지를 폐쇄망 레지스트리에 올리는 데까지입니다. 그 이미지를 실제 클러스터가 쓰게 만드는 것은 별도의 주제이고, 이 블로그에 이미 정리해 둔 글들이 있습니다.

패키지 관리자 자체를 비교한 npm, uv, rpm, brew 비교도 rpm의 위치를 잡는 데 도움이 됩니다.

서브스크립션 없이 Red Hat 콘텐츠를 재배포하는 것은 계약 위반일 수 있으니 조직의 라이선스 조건을 먼저 확인하세요. Red Hat 컨테이너 이미지 역시 별도의 이용 약관이 적용되므로, 사내 레지스트리 미러 구성 전에 확인이 필요합니다.

마치며 — 도구를 규모에 맞춰 고르세요

이미지 두세 개면 podman savepodman load로 충분합니다. 수십 개이고 정기 작업이면 skopeo sync가 정답이고, 그 사이에 skopeo copy가 있습니다.

어느 쪽이든 두 가지는 빠뜨리지 마세요. 아키텍처를 명시하는 것서명 검증을 정책으로 강제하는 것입니다. 둘 다 빠뜨려도 명령은 성공하고, 문제는 몇 주 뒤에 드러납니다.

명령과 옵션은 2026-08-15에 공식 문서에서 확인했습니다. RHEL 버전에 따라 podman과 skopeo의 제공 방식이 다르므로, 설치 방법은 사용 중인 버전의 문서로 다시 확인하세요.

직접 해보기

이전 / 다음 편

참고 자료