イメージを解体して覗き込む
한국어 원문으로 표시합니다.
이 실습은 진짜 VM 에서 돕니다
이 상자는 파드가 아니라 KubeVirt 가 띄운 가상머신입니다. 리눅스 커널이
따로 돌고 systemd 가 실제로 서비스를 관리하며, docker 는 흉내가 아니라
진짜 도커 엔진입니다. docker run 으로 띄운 컨테이너는 실제로 프로세스가
되고 docker exec 도 docker logs 도 그대로 동작합니다.
예전에는 이 실습이 파드 안에서 돌았습니다. 커널 권한을 전부 내려놓은 상자라 컨테이너를 띄우는 단계가 막혀 있었고, 그래서 이미지 아카이브를 직접 풀어 보는 우회로 배웠습니다. 이제 우회가 필요 없습니다.
알아 둘 것이 둘 있습니다.
- 처음 뜨는 데 1분 남짓 걸립니다. VM 이 부팅하고 도커를 설치하기 때문입니다. 파드 실습(보통 40초)보다 느립니다.
- 브라우저 미리보기가 없습니다. VM 으로 들어오는 연결은 채점 포트
하나만 열려 있습니다. 웹 서버를 띄웠다면 VM 안에서
curl로 확인하세요.
목표
이미지를 아카이브로 내보내 안에 든 매니페스트와 config 블롭을 직접 열어 보고,
RootFS.Layers(diff_id)와 매니페스트의 레이어 다이제스트가 어떻게 다른 층의
값인지 손으로 확인합니다. 마지막에는 같은 베이스에서 만든 두 이미지가 첫 레이어를
공유한다는 것을 다이제스트 비교로 증명합니다.
왜 중요한가
이미지 관련 사고는 대부분 "태그를 믿었기 때문"에 일어납니다. 태그는 사람이 붙인
이름이고 언제든 다른 다이제스트를 가리키도록 바뀝니다. 반대로 다이제스트는 내용의
해시라서 같은 해시면 같은 내용이 보장됩니다. 이 구분을 손으로 확인해 두면 배포
파이프라인에서 무엇을 고정해야 하는지가 분명해집니다. 또 하나, 매니페스트에 적힌
레이어 다이제스트는 압축본의 해시이고 config 의 rootfs.diff_ids 는 압축을 푼
tar 의 해시입니다. 둘이 다르다는 것을 모르면 "같은 이미지인데 해시가 다르다"는
착각에서 시간을 오래 씁니다. 그래서 이 실습은 두 값을 각각 다른 파일에서 꺼내
봅니다.
단계
/root/int3디렉터리를 만들고,alpine:3.20이미지의 ID(콘텐츠 해시)를/root/int3/image-id.txt에 저장합니다. 64자리 16진수여야 하며sha256:접두어는 붙어 있어도 됩니다.nginx:1.27-alpine의 rootfs 레이어 다이제스트를 한 줄에 하나씩/root/int3/layers.txt에 저장합니다. 모든 줄이sha256:로 시작하는 64자리여야 하고 줄 수가 실제 레이어 개수와 같아야 합니다.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개 이상 들어 있어야 합니다. - 4단계 매니페스트의
Config필드가 가리키는 config 블롭 파일을 아카이브에서 꺼내/root/int3/config.json으로 저장합니다. 이 파일에는architecture가 있어야 하고rootfs.type이layers,rootfs.diff_ids가 1개 이상이어야 합니다. 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 가 들어가고Digest가sha256:형식이며Layers가 1개 이상이어야 합니다.alpine:3.20을 베이스로 쓰되 서로 다른 내용을 한 겹 얹은 이미지 두 개를labhub/oci:a와labhub/oci:b태그로 빌드합니다. 두 이미지의 첫 레이어 다이제스트는 같고 마지막 레이어 다이제스트는 달라야 합니다./root/int3/oci.md에 다음 네 줄을 정확히 이 형식으로 적습니다. 모두nginx:1.27-alpine기준입니다.layer_count=<레이어 개수>architecture=<아키텍처>os=<운영체제>first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>
참고
docker image inspect <이미지> | jq -r '.[0].Id'로 이미지 ID 를 읽습니다.- 배열을 줄 단위로 펼칠 때는
jq -r '.[0].RootFS.Layers[]'형태를 씁니다. - 아카이브에서 파일 하나만 꺼내려면
tar xf <아카이브> -C <디렉터리> <파일이름>을 씁니다. - skopeo 는
skopeo inspect <전송>:<이미지>형식이고, 로컬 저장소용 전송은containers-storage:입니다. - 흔한 실수 1: 3단계에서
docker export를 쓰면 컨테이너 파일시스템 한 장만 나와 매니페스트가 없습니다. - 흔한 실수 2: 5단계에서 매니페스트를 그대로 복사하면
architecture가 없어 실패합니다.Config가 가리키는 다른 파일을 꺼내야 합니다. - 흔한 실수 3: 7단계에서 두 이미지에 같은 내용을 넣으면 레이어가 완전히 같아져 비교가 되지 않습니다.
이미지 ID 는 곧 콘텐츠 해시
/root/int3 디렉터리를 만들고, alpine:3.20 이미지의 ID(콘텐츠 해시)를 /root/int3/image-id.txt 에 저장합니다. 64자리 16진수여야 하며 sha256: 접두어는 붙어 있어도 됩니다.
docker image inspect 의 Id 필드가 그 값입니다. 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.type 이 layers, rootfs.diff_ids 가 1개 이상이어야 합니다.
매니페스트의 Config 필드는 아카이브 안 다른 파일의 이름입니다. 그 파일을 다시 꺼내야 architecture 와 rootfs.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 가 들어가고 Digest 가 sha256: 형식이며 Layers 가 1개 이상이어야 합니다.
이 환경은 오프라인이므로 원격 레지스트리로 나갈 수 없습니다. skopeo 는 전송(transport) 접두어로 대상을 고르는데, 로컬 이미지 저장소를 가리키는 전송을 쓰면 네트워크 없이 조회됩니다. 출력에는 Name, Digest, Layers 가 함께 나와야 합니다.
베이스 레이어 공유 증명하기
alpine:3.20 을 베이스로 쓰되 서로 다른 내용을 한 겹 얹은 이미지 두 개를 labhub/oci:a 와 labhub/oci:b 태그로 빌드합니다. 두 이미지의 첫 레이어 다이제스트는 같고 마지막 레이어 다이제스트는 달라야 합니다.
두 이미지가 같은 베이스에서 시작하고 서로 다른 내용을 한 겹 얹어야 합니다. 베이스가 다르면 첫 레이어가 갈라지고, 내용이 같으면 마지막 레이어까지 같아져서 둘 다 실패합니다.
OCI 메타데이터 요약
/root/int3/oci.md 에 다음 네 줄을 정확히 이 형식으로 적습니다. 모두 nginx:1.27-alpine 기준입니다.
layer_count=<레이어 개수>architecture=<아키텍처>os=<운영체제>first_layer12=<첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자>
네 줄 모두 키=값 형식이고 공백이나 따옴표가 섞이면 안 됩니다. first_layer12 는 첫 레이어 다이제스트에서 sha256: 을 뗀 앞 12자입니다. 전부 nginx 이미지 기준입니다.