๐ก Skopeo Container Image Utility: Daemonless โ Executive Summary
Technical architectural analysis, practical configuration manifests, and deployment instructions for Skopeo Container Image Utility: Daemonless.
Installation
RHEL / Rocky Linux (8/9)
bash
sudo dnf install -y skopeo
skopeo --version
Ubuntu / Debian (20.04/22.04/24.04)
bash
sudo apt-get update && sudo apt-get install -y skopeo
skopeo --version
macOS
bash
brew install skopeo
Closed network (air gap) offline packaging & local installation
bash
# [์จ๋ผ์ธ ๋จธ์ ] RHEL/Rocky ์์กด์ฑ RPM ๋ฒ๋ค๋ง
mkdir -p /opt/k8s-airgap-bundle/rpms/skopeo && cd /opt/k8s-airgap-bundle/rpms/skopeo
dnf download --resolve --alldeps skopeo
# [ํ์๋ง ํธ์คํธ] RHEL ๋ก์ปฌ ์คํ๋ผ์ธ ์ฆ์ ์ค์น
sudo dnf --disablerepo="*" -y localinstall /opt/k8s-airgap-bundle/rpms/skopeo/*.rpm || sudo rpm -Uvh --replacepkgs /opt/k8s-airgap-bundle/rpms/skopeo/*.rpm
# [์จ๋ผ์ธ ๋จธ์ ] Ubuntu ์ฌ๊ท ์์กด์ฑ DEB ๋ฒ๋ค๋ง
mkdir -p /opt/k8s-airgap-bundle/debs/skopeo && cd /opt/k8s-airgap-bundle/debs/skopeo
apt-get download $(apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances "skopeo" | grep "^\w" | sort -u) 2>/dev/null || apt-get download skopeo
# [ํ์๋ง ํธ์คํธ] Ubuntu ๋ก์ปฌ ์คํ๋ผ์ธ ์ฆ์ ์ค์น
sudo dpkg -i /opt/k8s-airgap-bundle/debs/skopeo/*.deb
Completion
bash
# bash ์๋์์ฑ ํ์ฑํ
skopeo completion bash | sudo tee /etc/bash_completion.d/skopeo > /dev/null
source /etc/bash_completion.d/skopeo
Exec
bash
# 1. ์๊ฒฉ ์ด๋ฏธ์ง ๋ฉํ๋ฐ์ดํฐ ๊ฒ์ฌ (๋ก์ปฌ ํ ์์ด Manifest ํ์ธ)
skopeo inspect docker://{{ image }}
skopeo inspect --raw docker://{{ image }} | jq .
# 2. ์ด๋ฏธ์ง ํ๊ทธ ๋ชฉ๋ก ์กฐํ
skopeo list-tags docker://{{ repository }}
# 3. ๋ ์ง์คํธ๋ฆฌ โ ๋จ์ผ tar ์์นด์ด๋ธ๋ก ์ ์ฅ (docker load ๋ฐ ํ์๋ง ๋ฐ์
ํธํ)
skopeo copy docker://{{ registry }}/{{ image }}:{{ tag }} docker-archive:{{ file.tar }}:{{ registry }}/{{ image }}:{{ tag }}
# 4. ๋จ์ผ tar ์์นด์ด๋ธ โ ์ฌ์ค ๋ ์ง์คํธ๋ฆฌ(Harbor) ํธ์
skopeo copy --dest-tls-verify=false --dest-creds "{{ user }}:{{ pass }}" docker-archive:{{ file.tar }} docker://{{ harbor_url }}/{{ project }}/{{ image }}:{{ tag }}
# 5. ๋ ์ง์คํธ๋ฆฌ ๊ฐ ์ง์ ๋ณต์ (Direct Streaming, ๋ก์ปฌ ๋์คํฌ I/O ๋ญ๋น ์ ๋ก)
skopeo copy --dest-tls-verify=false --dest-creds "{{ user }}:{{ pass }}" docker://{{ src_registry }}/{{ image }}:{{ tag }} docker://{{ dest_registry }}/{{ project }}/{{ image }}:{{ tag }}
# 6. ํน์ ์ํคํ
์ฒ ๊ฐ์ ์ง์ ๋ค์ด๋ก๋
skopeo copy --override-os linux --override-arch amd64 docker://{{ image }} docker-archive:{{ file.tar }}:{{ image }}
# 7. ์๊ฒฉ ๋ ์ง์คํธ๋ฆฌ ์ด๋ฏธ์ง ํ๊ทธ ์ญ์
skopeo delete --tls-verify=false --creds "{{ user }}:{{ pass }}" docker://{{ registry }}/{{ project }}/{{ image }}:{{ tag }}
copy
bash
# ์๊ฒฉ โ tar ์์นด์ด๋ธ (๋จ์ผ ํ์ผ ๋ด ํ๊ทธ ๋ฉํ๋ฐ์ดํฐ ๊ธฐ๋ก ๋ณด์ฅ)
skopeo copy docker://registry.k8s.io/pause:3.9 docker-archive:/tmp/pause_3.9.tar:registry.k8s.io/pause:3.9
# tar ์์นด์ด๋ธ โ ์ฌ์ค Harbor ์ผ๊ด ํธ์
skopeo copy --dest-tls-verify=false --dest-creds admin:HarborPassword \
docker-archive:/tmp/pause_3.9.tar docker://harbor.internal.net/k8s/pause:3.9
# ๋ ์ง์คํธ๋ฆฌ to ๋ ์ง์คํธ๋ฆฌ ์งํต ๋ณต์ (dockerd ๋ถํ์, ๋ก์ปฌ ๋์คํฌ ๋ฏธ์ฌ์ฉ)
skopeo copy --dest-tls-verify=false --dest-creds admin:HarborPassword \
docker://docker.io/calico/cni:v3.28.2 docker://harbor.internal.net/k8s/cni:v3.28.2
inspect
bash
# ์๊ฒฉ Manifest, ๋ ์ด์ด ํด์, ์ํคํ
์ฒ, ์ํธ๋ฆฌํฌ์ธํธ ์กฐํ
skopeo inspect docker://registry.k8s.io/kube-apiserver:v1.30.10
# ์์ Manifest JSON ์กฐํ
skopeo inspect --raw docker://registry.k8s.io/kube-apiserver:v1.30.10 | jq .
# ์ฌ์ค Harbor ์ธ์ฆ ๋ฐ TLS ์ฐํ ๊ฒ์ฌ
skopeo inspect --tls-verify=false --creds "admin:HarborPassword" \
docker://harbor.internal.net/k8s/kube-apiserver:v1.30.10
list-tags
bash
# ์๊ฒฉ ๋ฆฌํฌ์งํ ๋ฆฌ ์ ์ฒด ํ๊ทธ ๋ชฉ๋ก ์ถ๋ ฅ
skopeo list-tags docker://docker.io/calico/node | jq .Tags
# ํน์ ๋ฒ์ ํ๊ทธ ํํฐ๋ง
skopeo list-tags docker://docker.io/calico/node | jq -r '.Tags[] | select(startswith("v3.28"))'
Features
Supported Transports
docker://: Docker Registry v2 protocol (Harbor, Docker Hub, Quay, ECR, GCR, etc.)docker-archive::docker save/load100% compatible single tar archiveoci:: OCI (Open Container Initiative) image layout standard directoryoci-archive:: OCI image layout tar filedir:: Directory where the unzipped layer and manifest file are exposed as is (for debugging)containers-storage:: Podman / CRI-O local storage (/var/lib/containers/storage)docker-daemon:: Direct access to the locally running dockerd daemon storage
4-thread parallel bundling shell script for closed-net air gap
bash
#!/usr/bin/env bash
set -euo pipefail
DEST_DIR="/opt/k8s-airgap-bundle/images"
mkdir -p "${DEST_DIR}"
IMAGE_LIST=(
"registry.k8s.io/kube-apiserver:v1.30.10"
"registry.k8s.io/kube-controller-manager:v1.30.10"
"registry.k8s.io/kube-scheduler:v1.30.10"
"registry.k8s.io/kube-proxy:v1.30.10"
"registry.k8s.io/pause:3.9"
"docker.io/calico/cni:v3.28.2"
"docker.io/calico/node:v3.28.2"
"docker.io/calico/kube-controllers:v3.28.2"
)
# ๋จ์ผ ์ด๋ฏธ์ง ๋ค์ด๋ก๋ ํฌํผ ํจ์
download_img() {
local IMG="$1"
local OUT_DIR="$2"
local BASENAME=$(echo "$IMG" | awk -F'/' '{print $NF}')
local FILENAME=$(echo "${BASENAME}" | tr ':' '_')
local TAR_PATH="${OUT_DIR}/${FILENAME}.tar"
if [ -f "${TAR_PATH}" ]; then
echo " [๊ฑด๋๋] ์ด๋ฏธ ์กด์ฌ: ${FILENAME}.tar"
else
echo " โฌ๏ธ [๋ค์ด๋ก๋] ${IMG} โ ${FILENAME}.tar"
skopeo copy --override-os linux --override-arch amd64 \
"docker://${IMG}" "docker-archive:${TAR_PATH}:${IMG}"
fi
}
export -f download_img
# xargs -P 4 ๋ฉํฐํ๋ก์ธ์ค ๋ณ๋ ฌ ๋ค์ด๋ก๋ (์์์๊ฐ 70% ๋จ์ถ)
printf "%s\n" "${IMAGE_LIST[@]}" | sort -u | xargs -n 1 -P 4 -I {} bash -c 'download_img "$@"' _ {} "${DEST_DIR}"
echo "๐ ์ ์ฒด ์ด๋ฏธ์ง ์คํ๋ผ์ธ ๋ฒ๋ค๋ง ์๋ฃ!"
Closed network bastion โ Bundle SHA-256 verification, decompression and Harbor batch push all-in-one script
bash
#!/usr/bin/env bash
set -euo pipefail
BUNDLE_DIR="/opt/k8s-airgap-bundle"
IMAGE_DIR="${BUNDLE_DIR}/images"
# [0/3] ๋ฐ์
๋ฒ๋ค ์์นด์ด๋ธ SHA-256 ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ๋ฐ /opt ์๋ ์์ถ ํด์
ARCHIVE_FILE=$(ls -1 k8s-airgap-bundle-*.tar.gz /opt/k8s-airgap-bundle-*.tar.gz 2>/dev/null | head -n 1 || true)
if [ -n "${ARCHIVE_FILE}" ] && [ -f "${ARCHIVE_FILE}" ]; then
ARCHIVE_DIR=$(dirname "${ARCHIVE_FILE}")
ARCHIVE_NAME=$(basename "${ARCHIVE_FILE}")
SHA_FILE="${ARCHIVE_FILE}.sha256"
echo ">>> [1๋จ๊ณ: ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ] ๋ฐ์
๋ฒ๋ค ์์นด์ด๋ธ ๊ฐ์ง: ${ARCHIVE_FILE}"
if [ -f "${SHA_FILE}" ]; then
echo " SHA-256 ์ฒดํฌ์ฌ ๋์กฐ ๊ฒ์ฆ ์ค..."
(cd "${ARCHIVE_DIR}" && sha256sum -c "${ARCHIVE_NAME}.sha256")
echo " โ
SHA-256 ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์๋ฃ: ๋ฒ๋ค ํ์ผ์ด ์์ ์์ด ์จ์ ํฉ๋๋ค."
else
echo " โน๏ธ .sha256 ํ์ผ์ด ์์ด ์ฒดํฌ์ฌ ๊ฒ์ฆ์ ๊ฑด๋๋๋๋ค."
fi
if [ ! -d "${BUNDLE_DIR}/images" ]; then
echo ">>> [2๋จ๊ณ: ์์ถ ํด์ ] ๋ฒ๋ค ์์นด์ด๋ธ ์์ถ ํด์ ์ค โ /opt ..."
sudo tar -xzvf "${ARCHIVE_FILE}" -C /opt
echo " โ
์์ถ ํด์ ์๋ฃ: ${BUNDLE_DIR}"
else
echo " โน๏ธ ์ด๋ฏธ ${BUNDLE_DIR} ๋๋ ํฐ๋ฆฌ๊ฐ ์กด์ฌํ์ฌ ์์ถ ํด์ ๋ฅผ ์๋ตํฉ๋๋ค."
fi
fi
if [ ! -d "${BUNDLE_DIR}" ]; then
echo "โ Error: ๋ฒ๋ค ๋๋ ํฐ๋ฆฌ(${BUNDLE_DIR})๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ๋ฒ๋ค ์์ถ ํ์ผ์ ๋จผ์ ๋ฐ์
ํด์ฃผ์ธ์."
exit 1
fi
# [1/3] ํ์๋ง ํ๊ฒฝ skopeo ์ค์น ์ฌ๋ถ ๊ฒ์ฌ ๋ฐ ๋ฒ๋ค ๋ด ์คํ๋ผ์ธ ์๋ ์ค์น
if ! command -v skopeo >/dev/null 2>&1; then
echo "โ ๏ธ ํธ์คํธ์ skopeo๊ฐ ์์ต๋๋ค. ์คํ๋ผ์ธ ๋ก์ปฌ ์ค์น๋ฅผ ์์ํฉ๋๋ค..."
if [ -d "${BUNDLE_DIR}/rpms/skopeo" ]; then
sudo dnf --disablerepo="*" -y localinstall "${BUNDLE_DIR}/rpms/skopeo"/*.rpm 2>/dev/null || \
sudo rpm -Uvh --replacepkgs "${BUNDLE_DIR}/rpms/skopeo"/*.rpm 2>/dev/null || true
elif [ -d "${BUNDLE_DIR}/debs/skopeo" ]; then
sudo dpkg -i "${BUNDLE_DIR}/debs/skopeo"/*.deb 2>/dev/null || true
fi
fi
HARBOR_URL="harbor.internal.net"
HARBOR_PROJECT="k8s"
HARBOR_USER="admin"
HARBOR_PASSWORD="YourHarborPassword"
export SKOPEO_DEST_CREDS="${HARBOR_USER}:${HARBOR_PASSWORD}"
echo "=== [2/3] Harbor ์ผ๊ด ํธ์ ์์: https://${HARBOR_URL}/${HARBOR_PROJECT} ==="
find "${IMAGE_DIR}" -type f -name "*.tar" | while read -r ARCHIVE; do
BNAME=$(basename "$ARCHIVE" .tar)
# ํ์ผ๋ช
๊ท์น: [์ด๋ฏธ์ง๋ช
]_[ํ๊ทธ].tar โ ๋ง์ง๋ง ์ธ๋์ค์ฝ์ด(_)๋ฅผ ์ฝ๋ก (:)์ผ๋ก ๋ณต์
REPO_NAME=$(echo "$BNAME" | sed 's/\(.*\)_\([^:]*\)$/\1:\2/')
DEST_IMG="docker://${HARBOR_URL}/${HARBOR_PROJECT}/${REPO_NAME}"
echo ">>> Pushing: [${ARCHIVE}] โ [${DEST_IMG}]"
skopeo copy --dest-tls-verify=false --dest-creds "${SKOPEO_DEST_CREDS}" \
"docker-archive:${ARCHIVE}" "${DEST_IMG}"
done
echo "๐ ๋ชจ๋ ์ด๋ฏธ์ง๊ฐ ์ฌ์ค Harbor๋ก ์ผ๊ด ํธ์๋์์ต๋๋ค!"
Troubleshooting
x509: certificate signed by unknown authority
- Cause: Occurs when private Harbor/Nexus uses a private certificate (Self-signed SSL/TLS)
- Solution: Add flag
--src-tls-verify=falseor--dest-tls-verify=false
Prevent creation of \ tag when docker load
- Cause: Occurs when the target image identifier is omitted at the end when specifying
docker-archive: - Solution:
bash
# โ ํ๊ทธ ๋๋ฝ
skopeo copy docker://pause:3.9 docker-archive:/tmp/pause.tar
# โญ ์ ์ (์์นด์ด๋ธ ๋ฉํ๋ฐ์ดํฐ์ ํ๊ทธ ๊ธฐ๋ก๋จ)
skopeo copy docker://pause:3.9 docker-archive:/tmp/pause.tar:registry.k8s.io/pause:3.9
manifest unknown (404 Not Found)
- Cause: Project (e.g.
k8s) is not pre-created in the push target Harbor - Solution: Pre-create the project in Harbor web console and rerun the script




Leave a Reply