컨테이너 내부 원리 · OCI 이미지 포맷 · 실습
이미지를 해체해서 들여다보기
이 실습은 진짜 VM 에서 돕니다
이 상자는 파드가 아니라 KubeVirt 가 띄운 가상머신입니다. 리눅스 커널이
따로 돌고 systemd 가 실제로 서비스를 관리하며, docker 는 흉내가 아니라
진짜 도커 엔진입니다. docker run 으로 띄운 컨테이너는 실제로 프로세스가
되고 docker exec 도 docker logs 도 그대로 동작합니다.
예전에는 이 실습이 파드 안에서 돌았습니다. 커널 권한을 전부 내려놓은 상자라
컨테이너를 띄우는 단계가 막혀 있었고, 그래서 이미지 아카이브를 직접 풀어
보는 우회로 배웠습니다. 이제 우회가 필요 없습니다.
알아 둘 것이 둘 있습니다.
- 처음 뜨는 데 1분 남짓 걸립니다. VM 이 부팅하고 도커를 설치하기
- 브라우저 미리보기가 없습니다. VM 으로 들어오는 연결은 채점 포트
때문입니다. 파드 실습(보통 40초)보다 느립니다.
하나만 열려 있습니다. 웹 서버를 띄웠다면 VM 안에서 curl 로 확인하세요.
목표
이미지를 아카이브로 내보내 안에 든 매니페스트와 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.type 이 layers, 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 가 들어가고 Digest 가 sha256: 형식이며 Layers 가 1개 이상이어야 합니다.
7. alpine:3.20 을 베이스로 쓰되 서로 다른 내용을 한 겹 얹은 이미지 두 개를 labhub/oci:a 와 labhub/oci:b 태그로 빌드합니다. 두 이미지의 첫 레이어 다이제스트는 같고 마지막 레이어 다이제스트는 달라야 합니다.
8. /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단계에서 두 이미지에 같은 내용을 넣으면 레이어가 완전히 같아져 비교가 되지 않습니다.
단계 8개
- 이미지 ID 는 곧 콘텐츠 해시
- 레이어 다이제스트 목록 뽑기
- 이미지를 아카이브로 내보내기
- 아카이브 안의 매니페스트 열기
- config 블롭과 diff_ids
- 레지스트리 도구로 매니페스트 조회
- 베이스 레이어 공유 증명하기
- OCI 메타데이터 요약