LabHub
배우기 러닝패스 코스

Container Internals

Taking an Image Apart to Look Inside

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

이 실습은 진짜 VM 에서 돕니다

이 상자는 파드가 아니라 KubeVirt 가 띄운 가상머신입니다. 리눅스 커널이 따로 돌고 systemd 가 실제로 서비스를 관리하며, docker 는 흉내가 아니라 진짜 도커 엔진입니다. docker run 으로 띄운 컨테이너는 실제로 프로세스가 되고 docker execdocker logs 도 그대로 동작합니다.

예전에는 이 실습이 파드 안에서 돌았습니다. 커널 권한을 전부 내려놓은 상자라 컨테이너를 띄우는 단계가 막혀 있었고, 그래서 이미지 아카이브를 직접 풀어 보는 우회로 배웠습니다. 이제 우회가 필요 없습니다.

알아 둘 것이 둘 있습니다.

목표

이미지를 아카이브로 내보내 안에 든 매니페스트와 config 블롭을 직접 열어 보고, RootFS.Layers(diff_id)와 매니페스트의 레이어 다이제스트가 어떻게 다른 층의 값인지 손으로 확인합니다. 마지막에는 같은 베이스에서 만든 두 이미지가 첫 레이어를 공유한다는 것을 다이제스트 비교로 증명합니다.

왜 중요한가

이미지 관련 사고는 대부분 "태그를 믿었기 때문"에 일어납니다. 태그는 사람이 붙인 이름이고 언제든 다른 다이제스트를 가리키도록 바뀝니다. 반대로 다이제스트는 내용의 해시라서 같은 해시면 같은 내용이 보장됩니다. 이 구분을 손으로 확인해 두면 배포 파이프라인에서 무엇을 고정해야 하는지가 분명해집니다. 또 하나, 매니페스트에 적힌 레이어 다이제스트는 압축본의 해시이고 config 의 rootfs.diff_ids압축을 푼 tar 의 해시입니다. 둘이 다르다는 것을 모르면 "같은 이미지인데 해시가 다르다"는 착각에서 시간을 오래 씁니다. 그래서 이 실습은 두 값을 각각 다른 파일에서 꺼내 봅니다.

단계

  1. /root/int3 디렉터리를 만들고, alpine:3.20 이미지의 ID(콘텐츠 해시)를 /root/int3/image-id.txt 에 저장합니다. 64자리 16진수여야 하며 sha256: 접두어는 붙어 있어도 됩니다.
  2. nginx:1.27-alpine 의 rootfs 레이어 다이제스트를 한 줄에 하나씩 /root/int3/layers.txt 에 저장합니다. 모든 줄이 sha256: 로 시작하는 64자리여야 하고 줄 수가 실제 레이어 개수와 같아야 합니다.
  3. alpine:3.20 이미지를 아카이브로 내보내 /root/int3/alpine.tar 로 저장합니다. docker save 는 이 상자에서 동작하지 않으므로 skopeo copy oci-archive:/opt/images/alpine_3.20.tar oci-archive:/root/int3/alpine.tar:alpine:3.20 을 씁니다 — 하는 일은 같습니다. 안에 manifest.json(또는 index.json/oci-layout)과 해시 이름의 블롭이 들어 있어야 합니다.
  4. 그 아카이브에서 manifest.json 을 꺼내 /root/int3/manifest.json 으로 저장합니다. 유효한 JSON 이어야 하고 레이어 목록이 1개 이상 들어 있어야 합니다.
  5. 4단계 매니페스트의 Config 필드가 가리키는 config 블롭 파일을 아카이브에서 꺼내 /root/int3/config.json 으로 저장합니다. 이 파일에는 architecture 가 있어야 하고 rootfs.typelayers, rootfs.diff_ids 가 1개 이상이어야 합니다.
  6. skopeo 로 로컬 이미지 저장소의 nginx:1.27-alpine 정보를 조회해 /root/int3/skopeo.json 에 저장합니다. 저장소가 비어 있으므로 먼저 skopeo copy --insecure-policy oci-archive:/opt/images/nginx_1.27-alpine.tar containers-storage:docker.io/library/nginx:1.27-alpine 으로 넣으세요 — podman load 는 이 상자에서 막혀 있지만 skopeo 는 네임스페이스를 만들지 않아 그대로 됩니다. Name 에 nginx 가 들어가고 Digestsha256: 형식이며 Layers 가 1개 이상이어야 합니다.
  7. alpine:3.20 을 베이스로 쓰되 서로 다른 내용을 한 겹 얹은 이미지 두 개를 labhub/oci:alabhub/oci:b 태그로 빌드합니다. 두 이미지의 첫 레이어 다이제스트는 같고 마지막 레이어 다이제스트는 달라야 합니다.
  8. /root/int3/oci.md 에 다음 네 줄을 정확히 이 형식으로 적습니다. 모두 nginx:1.27-alpine 기준입니다.
    • layer_count=<레이어 개수>
    • architecture=<아키텍처>
    • os=<운영체제>
    • first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>

참고

이미지 ID 는 곧 콘텐츠 해시

/root/int3 디렉터리를 만들고, alpine:3.20 이미지의 ID(콘텐츠 해시)를 /root/int3/image-id.txt 에 저장합니다. 64자리 16진수여야 하며 sha256: 접두어는 붙어 있어도 됩니다.

docker image inspectId 필드가 그 값입니다. sha256: 접두어는 있어도 없어도 되지만 뒤의 16진수는 정확히 64자리여야 합니다. 태그 이름이 아니라 해시를 저장하는 단계입니다.

레이어 다이제스트 목록 뽑기

nginx:1.27-alpine 의 rootfs 레이어 다이제스트를 한 줄에 하나씩 /root/int3/layers.txt 에 저장합니다. 모든 줄이 sha256: 로 시작하는 64자리여야 하고 줄 수가 실제 레이어 개수와 같아야 합니다.

RootFS.Layers 배열을 한 줄에 하나씩 풀어서 저장합니다. jq 로 배열을 펼치면 따옴표 없이 나옵니다. 줄 수가 실제 레이어 개수와 정확히 같아야 하므로 빈 줄이나 헤더가 섞이면 안 됩니다.

이미지를 아카이브로 내보내기

alpine:3.20 이미지를 아카이브로 내보내 /root/int3/alpine.tar 로 저장합니다. docker save 는 이 상자에서 동작하지 않으므로 skopeo copy oci-archive:/opt/images/alpine_3.20.tar oci-archive:/root/int3/alpine.tar:alpine:3.20 을 씁니다 — 하는 일은 같습니다. 안에 manifest.json(또는 index.json/oci-layout)과 해시 이름의 블롭이 들어 있어야 합니다.

컨테이너의 파일시스템 한 장을 내보내는 명령과, 레이어·태그·히스토리를 포함한 이미지를 내보내는 명령은 다릅니다. 후자를 써야 안에 매니페스트가 들어갑니다.

아카이브 안의 매니페스트 열기

그 아카이브에서 manifest.json 을 꺼내 /root/int3/manifest.json 으로 저장합니다. 유효한 JSON 이어야 하고 레이어 목록이 1개 이상 들어 있어야 합니다.

tar 아카이브에서 특정 파일 하나만 뽑아낼 수 있습니다. 손으로 JSON 을 지어내지 말고 아카이브 안에 든 것을 그대로 꺼내세요. 꺼낸 파일이 유효한 JSON 이어야 하고 레이어 목록을 담고 있어야 합니다.

config 블롭과 diff_ids

4단계 매니페스트의 Config 필드가 가리키는 config 블롭 파일을 아카이브에서 꺼내 /root/int3/config.json 으로 저장합니다. 이 파일에는 architecture 가 있어야 하고 rootfs.typelayers, rootfs.diff_ids 가 1개 이상이어야 합니다.

매니페스트의 Config 필드는 아카이브 안 다른 파일의 이름입니다. 그 파일을 다시 꺼내야 architecturerootfs.diff_ids 가 들어 있는 진짜 config 가 나옵니다. 매니페스트 자체를 복사하면 실패합니다.

레지스트리 도구로 매니페스트 조회

skopeo 로 로컬 이미지 저장소의 nginx:1.27-alpine 정보를 조회해 /root/int3/skopeo.json 에 저장합니다. 저장소가 비어 있으므로 먼저 skopeo copy --insecure-policy oci-archive:/opt/images/nginx_1.27-alpine.tar containers-storage:docker.io/library/nginx:1.27-alpine 으로 넣으세요 — podman load 는 이 상자에서 막혀 있지만 skopeo 는 네임스페이스를 만들지 않아 그대로 됩니다. Name 에 nginx 가 들어가고 Digestsha256: 형식이며 Layers 가 1개 이상이어야 합니다.

이 환경은 오프라인이므로 원격 레지스트리로 나갈 수 없습니다. skopeo 는 전송(transport) 접두어로 대상을 고르는데, 로컬 이미지 저장소를 가리키는 전송을 쓰면 네트워크 없이 조회됩니다. 출력에는 Name, Digest, Layers 가 함께 나와야 합니다.

베이스 레이어 공유 증명하기

alpine:3.20 을 베이스로 쓰되 서로 다른 내용을 한 겹 얹은 이미지 두 개를 labhub/oci:alabhub/oci:b 태그로 빌드합니다. 두 이미지의 첫 레이어 다이제스트는 같고 마지막 레이어 다이제스트는 달라야 합니다.

두 이미지가 같은 베이스에서 시작하고 서로 다른 내용을 한 겹 얹어야 합니다. 베이스가 다르면 첫 레이어가 갈라지고, 내용이 같으면 마지막 레이어까지 같아져서 둘 다 실패합니다.

OCI 메타데이터 요약

/root/int3/oci.md 에 다음 네 줄을 정확히 이 형식으로 적습니다. 모두 nginx:1.27-alpine 기준입니다.

네 줄 모두 키=값 형식이고 공백이나 따옴표가 섞이면 안 됩니다. first_layer12 는 첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자입니다. 전부 nginx 이미지 기준입니다.