컨테이너 내부 원리 · OCI 이미지 포맷 · 이론
이미지 한 장을 열어 보면 무엇이 들어 있나
한 줄 요약
OCI 이미지는 Image Index → Image Manifest → Image Config 세 겹의 JSON 과
그 아래 레이어 블롭으로 이루어져 있고, 모든 조각이 자기 내용의 SHA-256 으로
주소가 매겨집니다.
왜 이게 필요했나
docker pull 한 줄 뒤에서 무슨 일이 벌어지는지 모르면, 다이제스트 고정 pull 이
왜 실패하는지, 레지스트리를 이중화했더니 왜 태그가 꼬이는지, 이미지 열 개를
올렸는데 왜 디스크는 조금밖에 안 늘었는지 설명할 수 없습니다. 세 겹 구조를
한 번만 열어 보면 이 질문들이 전부 같은 그림 위에 놓입니다.
어떻게 동작하나
Image Index(fat manifest)는 플랫폼별 매니페스트의 목록입니다. arm64 맥에서
pull 했는데 amd64 이미지가 오는 사고가 여기서 갈립니다.
Image Manifest는 config 다이제스트 하나와 순서가 보장된 레이어 목록,
그리고 각 항목의 mediaType 을 담습니다. 자주 보는 mediaType 은 이렇습니다.
application/vnd.oci.image.index.v1+jsonapplication/vnd.oci.image.manifest.v1+jsonapplication/vnd.oci.image.layer.v1.tar+gzipImage Config는 런타임이 실제로 읽는 설정입니다. Env, Entrypoint, Cmd,WorkingDir 같은 값과 rootfs.diff_ids, 그리고 history 가 들어 있습니다.
여기서 가장 자주 헷갈리는 구분이 나옵니다.
- 매니페스트의
layers[].digest는 tar+gzip 으로 압축된 블롭의 해시입니다. - config 의
rootfs.diff_ids는 압축을 푼 tar 의 해시입니다. 로컬에서
네트워크로 내려받는 것이 이것입니다.
레이어를 쌓을 때 쓰는 것이 이것입니다.
둘은 다른 값입니다. docker image inspect 의 RootFS.Layers 에 보이는 것은
diff_id 쪽이고, 레지스트리에서 보는 다이제스트는 압축본 쪽입니다. 이걸 모르면
"같은 이미지인데 해시가 다르다"는 착각에 빠집니다.
콘텐츠 주소 지정의 이득은 세 가지로 정리됩니다. 중복 제거(같은 내용이면 한
번만 저장), 무결성 검증(받은 바이트를 해싱해 이름과 비교), 불변성(같은
해시면 같은 내용).
현장에서 만나는 모습
다이제스트와 태그의 차이가 실무의 절반입니다. 다이제스트는 내용의 해시라서
충돌도, 캐시 무효화 문제도 없습니다. 태그는 사람이 붙인 이름이고 언제든 다른
다이제스트를 가리키도록 바뀝니다. 여기가 상태이고, **분산 시스템에서 어려운
부분은 전부 여기 있습니다**. 레지스트리를 여러 대로 묶으려 하면 결국 태그 갱신을
직렬화하는 문제로 수렴하는 이유가 이것입니다.
포맷 변환도 조심해야 합니다. 도커 스키마에서 OCI 로 옮기면 매니페스트의
바이트가 바뀌고, 바이트가 바뀌면 다이제스트가 바뀝니다. 그러면 **다이제스트로
고정한 pull 과 서명 검증이 함께 깨집니다.** "내용은 그대로인데 왜 안 되죠"의
정답이 이것입니다.
레이어 공유는 숫자로 보면 명확합니다. ubuntu:22.04 위에 node 를 올린 베이스를
쓰는 이미지가 10개라면, 저장되는 것은 베이스 1배 + 앱 레이어 10배입니다.
그래서 베이스를 통일하는 것이 곧 레지스트리 용량 정책입니다.
다음 실습에서 할 것
이미지 ID 가 곧 콘텐츠 해시라는 것을 확인하고, 이미지를 tar 로 내보내 안의
매니페스트와 config 블롭을 직접 열어 봅니다. skopeo 로 같은 정보를 레지스트리
도구의 시선으로 다시 보고, 같은 베이스에서 만든 두 이미지가 첫 레이어를
공유한다는 것을 다이제스트 비교로 증명합니다.