Skopeo Container Image Utility: Daemonless

๐Ÿ’ก 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/load 100% compatible single tar archive
  • oci: : OCI (Open Container Initiative) image layout standard directory
  • oci-archive: : OCI image layout tar file
  • dir:: 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=false or --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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *