엔터프라이즈 레벨의 기업 환경에서는 보안 또는 정책상 폐쇄망 환경에서 Docker를 운영하는 경우가 많습니다. 이 경우 새로운 컨테이너 이미지를 배포하거나 기존 이미지를 업데이트하려면 외부 네트워크 없이 수동으로 이미지를 가져와야 합니다. 일반적으로는 인터넷이 되는 곳에서 이미지를 docker save로 tar 파일로 export 한 뒤, 이를 폐쇄망으로 옮겨와 docker load로 로드하는 방식을 사용합니다.
그러나 이 방식에는 큰 비효율이 있습니다. 이미지 크기가 수 GB~수십 GB에 달하면 업데이트 때마다 전체 이미지를 통째로 옮겨야 하기 때문입니다. 예를 들어, 10GB짜리 이미지를 약간 수정했는데도 새로운 10GB 파일을 다시 받아 로드해야 한다면, 네트워크와 저장소 자원을 불필요하게 소모하게 됩니다.
더욱이 이미지가 자주 업데이트된다면 이러한 중복 전송은 배포 속도를 떨어뜨리고, 폐쇄망 유지 보수 작업의 부담을 가중시킵니다. Docker의 레이어 캐싱 개념을 생각하면 동일한 기반 레이어는 재사용될 수 있음에도, 폐쇄망에서는 마치 매번 처음부터 이미지를 받는 셈입니다.
이번 아티클에서는 Docker 이미지가 어떤 구조로 이루어져 있고 레이어 단위의 캐싱이 어떻게 동작하는지 간략히 짚어보겠습니다.
Docker 이미지 구조는 어떻게 되어 있나요?
Docker 이미지는 여러 개의 레이어로 구성된 읽기 전용 파일 시스템 스냅샷들의 조합입니다. 각각의 레이어는 그 아래 레이어와의 변화분(diff) 만을 포함하며, 여러 레이어를 OverlayFS와 같은 유니온 파일 시스템으로 합쳐 하나의 일관된 파일시스템을 제공합니다.
가장 아래에는 베이스 이미지 레이어가 있고, 각 상위 레이어가 순서대로 변경사항(파일 추가/수정/삭제)을 누적해 최종이미지 파일 시스템을 형성합니다. 컨테이너를 실행하면 이 읽기전용 레이어들 위에 쓰기 가능한 컨테이너 레이어가 추가되면서 동작하며, 컨테이너 종료 시 해당 쓰기 레이어는 사라집니다.
각 레이어는 내용기반의 고유 해시(SHA-256) 로 식별되는 불변의 Tar 아카이브 입니다. Docker 엔진은 레이어의 해시를 내용(Content)에 기반하여 계산하고, 이를 사용해 레이어들을 내용 주소 방식으로 관리합니다.
즉, 동일한 내용의 레이어는 동일한 해시를 가지므로 이미지 간의 중복 저장되지 않고 공유됩니다. 이러한 구조 덕분에, 예를 들어 이미지가 같은 기반 OS 레이어를 사용하면 해당 레이어는 시스템에 한 번만 저장되고 모두 공동으로 사용합니다.
또한 한 레이어에서 파일이 삭제된 경우, 실제로는 그 레이어에 화이트아웃 이라는 특수 파일로 삭제 표시를 남겨 하위 레이어의 파일을 무시하게 만드는 방식으로 동작합니다. 이러한 레이어 구조와 Union 마운트 방식으로 Docker는 이미지의 변경 부분만 기록하고 나머지는 재사용함으로써 스토리지 효율성과 이식성을 얻습니다
예를 들어 Python 기반 애플리케이션 이미지를 생각해보면, 아래와 같이 여러 이미지가 공통 기반 레이어를 공유할 수 있습니다. Debian 리눅스 베이스, Python 인터프리터 및 pip 설치 층은 여러 이미지에서 한 번만 전송되고 캐시될 수 있으며, 각 애플리케이션마다 고유한 소스코드와 종속성 레이어만 추가로 받으면 됩니다.
이 구조 덕분에 Docker 허브에서 이미지를 받을 때도 이미 로컬에 있는 레이어는 다시 받지 않고 재사용하여 전송량을 줄입니다. Docker 이미지 tar 파일을 열어보면 다음과 같은 구성 요소 4가지를 확인할 수 있습니다.
파일/디렉터리 | 내용 설명 |
manifest.json | 이미지에 속한 모든 레이어와 config 파일을 가리키는 매니페스트입니다. 여러 이미지(tag)를 한 번에 저장한 경우 manifest 배열에 여러 항목이 들어갑니다 |
repositories | (Docker v1 형식용) 이미지의 레포지토리 이름과 태그를 기록한 맵 파일. 최신 버전의 Docker에서는 manifest의 RepoTags 필드로 대체되지만, 호환성을 위해 포함될 수 있습니다. |
<이미지 config 해시>.json | 컨테이너 실행에 필요한 메타데이터(예: Docker 버전, 환경변수, 명령어 등)를 담은 설정 파일입니다. |
<레이어 해시>/layer.tar | • 레이어의 내용물로 이루어진 디렉터리이며, 디렉터리 이름이 해당 레이어 내용 해시값으로 되어 있습니다. 디렉터리 안에는 실제 파일시스템 변경 내용을 담은 layer.tar 파일이 있습니다. 동일한 레이어 해시값은 언제나 동일한 파일 내용을 가리키므로, 해시가 같다면 중복된 데이터임을 알 수 있습니다. |
두 개의 애플리케이션이 공통 베이스 레이어(Python 및 pip, Debian base)를 공유하여 전송량을 줄이는 Docker 이미지 레이어 구조 개념도
위 그림처럼 Docker 레이어 구조와 캐싱 메커니즘을 활용하면 이미 받은 레이어는 다시 받지 않는 효율적인 배포가 가능합니다. 하지만 폐쇄망 환경에서는 Docker 허브나 사설 레지스트리에 접근할 수 없으므로 기본적으로 이러한 자동 최적화가 이루어지지 않습니다.
변경 레이어만 옮기는 증분 업데이트 전략
폐쇄망 환경의 전송 비효율 문제를 개선하기 위해, 실제 저희 고객사에서는 기존 이미지와 신규 이미지의 차이만 전송하는 전략을 적용했습니다. 이전 버전 이미지 tar와 새 버전 이미지 tar를 비교하여, 중복되는 레이어는 폐쇄망 측에 이미 있으므로 제외하고 변경된 레이어만 묶어서 보낸 뒤 현지에서 결합하는 것입니다.
Docker 이미지의 레이어들은 해시값으로 식별되므로 두 이미지의 manifest를 비교하면 겹치는 레이어를 쉽게 알아낼 수 있습니다. 예를 들어 이전 이미지 A가 레이어 [L1, L2, L3]로 구성되고 새 이미지 B가 [L1, L2, L4]로 구성된다면, 공통 레이어 L1, L2는 이미 폐쇄망에 있으므로 새 레이어 L4만 전송하면 충분합니다.
결과적으로 몇 기가바이트에 이르는 전체 이미지를 보내는 대신, 달라진 부분만 수백 메가바이트 수준으로 보내게 되어 전송 데이터량을 크게 줄일 수 있습니다. 이 접근 방식은 Docker 레지스트리를 사용할 수 없을 때 수작업으로 레이어 캐싱 효과를 내는 셈입니다.
다만 수동으로 tar 파일을 열어 레이어를 구분하고 비교하는 것은 번거로우므로, 이를 자동화해주는 도구나 스크립트가 필요합니다. 저희는 내부적으로 Python으로 작성된 간단한 툴을 개발하여 사용했습니다.
docker-diff 툴은 두 가지 모드(compare, merge)로 동작합니다.
아래는 compare 모드로 기존 이미지와 신규 이미지를 비교하여 diff.tar를 만드는 로직의 핵심 부분입니다.
# 1. 이전 이미지의 레이어 목록 추출
base_layers: dict = extract_layers_info(base_image)
...
# 2. 새로운 이미지의 tar를 열어서 파일 검사
for file_tar in all_files:
if file_tar.isdir() and len(file_tar.name.split("/")) < 2: # 최상위 디렉터리(레이어)
if file_tar.name not in base_layers["Layers"]:
filenames_to_add.add(file_tar.name) # Base에 없는 레이어는 신규
else:
kept_layers.append(file_tar.name) # Base에 있는 레이어는 재사용
# 3. 신규 레이어 및 관련 파일만 diff.tar에 추가 저장
... tar_new.addfile(tarinfo, fileobj) ...
Python
복사
위 코드에서는 먼저 기존(base) 이미지 tar의 manifest.json을 읽어 레이어 해시 목록을 얻은 뒤, 새 이미지 tar의 최상위 디렉터리들을 훑으면서 기존 목록에 없는 디렉터명이면 그 레이어를 신규 추가 목록에 넣습니다.
결국 filenames_to_add 에는 전송할 새로운 레이어 디렉터리들이, kept_layers 에는 재사용할 기존 레이어들 이 모이게 됩니다. 이후 tar 파일을 생성하면서 filenames_to_add에 해당하는 디렉터리와 파일들만 diff.tar 에 추가로 담습니다.
비교 과정이 끝나면 로그로 요약을 출력하는데, 레이어 재사용/추가 개수와 전송 필요 용량을 보여줍니다.
=== compare between two images ===
- reuse layer: 5개
- new layer: 1개
- 전송 필요 용량: 250.42 MB
- diff file: output_diff.tar
=======================
Python
복사
이렇게 생성된 diff.tar 파일과 추가로 출력된 JSON은 재사용 레이어 목록을 담아냈으며 폐쇄망으로 옮길 준비가 됩니다. 다음은 merge 모드로 폐쇄망 내에서 base 이미지와 diff를 합치는 단계입니다.
# 1. 임시 디렉터리에 base.tar와 diff.tar 내용 각각 풀기
extract_tar(base_tar, base_dir)
extract_tar(diff_tar, diff_dir)
# 2. base 디렉터리 내용을 merged 디렉터리에 복사한 뒤, diff 내용으로 갱신/추가
shutil.copytree(base_dir, merged_dir, dirs_exist_ok=True)
shutil.copytree(diff_dir, merged_dir, dirs_exist_ok=True)
# 3. 병합된 merged 디렉터리를 다시 tar로 묶어 출력
with tarfile.open(output_tar, 'w') as outtar:
for root, _, files in os.walk(merged_dir):
for file in files:
fullpath = os.path.join(root, file)
arcname = os.path.relpath(fullpath, merged_dir)
outtar.add(fullpath, arcname=arcname
Python
복사
merge 단계에서는 우선 기존 이미지 tar와 diff tar를 풀어서 각각의 파일 구조를 얻습니다. 그 다음, 빈 merged 폴더에 기존 이미지 내용을 복사하고 거기에 diff의 내용(신규 레이어들과 새 manifest/config)을 다시 복사합니다.
이 때 dirs_exist_ok=True 옵션으로 동일 경로가 겹칠 경우 diff 쪽 파일로 덮어쓰게 되어, 결과적으로 base + 업데이트된 내용이 결합된 디렉터리가 완성됩니다.
마지막으로 그 merged 디렉터리를 tar로 묶으면 최종 업데이트된 새 이미지 tar 파일(output_tar)이 생성됩니다. 이 tar를 폐쇄망 Docker에 docker load하면 업데이트된 이미지를 사용할 수 있게 됩니다.
단계별로 구현하기
위의 전략을 실제로 적용하려면 다음과 같은 단계로 진행됩니다.
1.
기존 이미지 tar를 준비해요. 폐쇄망에 이미 배포되어 있는 Docker 이미지를 동일한 버전으로 외부에서도 확보합니다. 만약 외부에 원본 이미지가 없다면, 폐쇄망에서 해당 이미지를 docker save로 export하여 가져옵니다 (또는 이전에 전달했던 tar 파일을 보관 중이라면 활용). 이 파일을 base.tar라고 부르겠습니다.
2.
신규 이미지 빌드하고 저장해요. 업데이트된 애플리케이션으로 새로운 이미지를 빌드하거나 외부 레지스트리에서 받아 둡니다. 그 이미지를 docker save 명령으로 new.tar 파일로 저장합니다.
3.
차이를 비교하고 저장해요. 외부 환경에서 docker-diff 툴의 compare 모드를 실행하여 -base base.tar --new new.tar --output diff.tar 형태로 입력합니다. 스크립트가 두 tar의 manifest를 비교해 달라진 레이어만 포함된 diff.tar 와 재사용 레이어 목록 JSON을 생성해줍니다. 예를 들어 출력 로그상 신규 레이어 2개만 있다면 diff.tar에는 그 2개 레이어와 새로운 manifest만 들어 있습니다.
4.
diff 차이를 전송해요. 생성된 diff.tar (및 JSON 파일)을 폐쇄망으로 복사합니다. 파일 크기는 전체 이미지 대비 현저히 작으므로 USB 이동이나 승인이 된 망 연계 저장소를 통해 전송이 수월해집니다.
5.
이미지를 병합해요. 폐쇄망 측에서 docker-diff 툴의 merge 모드를 실행합니다. 인자로 -base base.tar --diff-tar diff.tar --output new_full.tar를 주면, 내부에서 base와 diff를 합쳐 new_full.tar를 만듭니다. 이 tar는 new.tar와 동일한 구조의 완전한 신규 이미지이며, 크기도 원래 이미지 크기와 비슷할 것입니다.
6.
이미지 로드하고 배포해요. 최종 생성된 new_full.tar를 docker load -i 명령으로 로드하면 폐쇄망 Docker 데몬에 새 이미지가 등록됩니다. 이제 기존 컨테이너를 내려 받고 새 이미지로 컨테이너를 올리는 등의 배포 작업을 진행하면 됩니다. 필요한 경우 cleanup으로 일회성 파일들을 정리합니다.
이 절차를 따르면 폐쇄망에도 변경된 레이어만 증분 전송하여 업데이트할 수 있습니다. 기존 방식에 비해 네트워크 전송량을 크게 줄이고, 업데이트에 소요되는 시간을 단축할 수 있다는 장점이 있습니다. 특히 원본 이미지가 매우 크고 자주 업데이트되는 상황이라면, 누적된 전송 비용을 획기적으로 절감할 수 있습니다.
폐쇄망 환경에서의 이점
설명한 증분 업데이트 방식을 활용하면, 폐쇄망에 Docker 레지스트리를 직접 두지 못하는 환경에서도 레이어 중복 활용을 통한 전송 최적화가 가능합니다.
•
전송 데이터 감소: 변경된 레이어만 보내므로 매 배포시 전송해야 할 데이터 양이 크게 줄어듭니다. 예를 들어 한 이미지의 10개 레이어 중 2개만 변경되었다면, 통상 전체 100%를 옮기던 것을 20% 이하만 옮기게 됩니다. 이는 내부망으로의 패치 전송 개념과 비슷합니다. 전송 시간이 단축되고, 망 부하 및 저장 공간 낭비도 완화됩니다.
•
빠른 배포 및 롤아웃: 데이터가 적게 이동하므로 업데이트 주기를 짧게 가져갈 수 있습니다. 과거에는 이미지 용량 탓에 몇 주에 한 번 배포하던 것을, 이제는 변경분만 반영해 더욱 자주 배포할 수 있게 됩니다. 긴급 패치도 신속히 전달할 수 있어 시스템 안정성이 향상됩니다.
•
검증 및 신뢰성: base 이미지와 새 이미지의 manifest를 대조하기 때문에, 변경되지 않은 레이어는 건드리지 않음을 확실히 할 수 있습니다. 만약 이미지 차이에 예상치 못한 변동이 있다면 diff 생성 단계에서 바로 인지할 수도 있습니다. 또한 docker load로 가져온 이미지이기 때문에 Docker가 정상적인 이미지로 간주하며 신뢰성을 유지합니다.
•
자동화 가능: 소개한 툴이나 스크립트를 CI/CD 파이프라인에 넣으면, 폐쇄망 배포용 디지털 패키지를 자동으로 생성하는 식으로 활용할 수 있습니다. 개발팀은 평소처럼 Docker 이미지를 빌드하고, 최종 배포 산출물로 diff.tar를 만들어 내는 흐름을 구축할 수 있습니다.
물론 이 방식은 폐쇄망 측에 기존 이미지(base)가 존재한다는 전제가 필요하며, 만약 최초 배포라면 전체 이미지를 보내야 합니다. 그러나 초기 배포 후 반복되는 업데이트 상황에서는 상당한 이득을 볼 수 있습니다.
다른 가능한 대안도 있을까요?
물론 모든 환경에 증분 접근이 꼭 들어맞는 것은 아닙니다. 인프라 여건이 된다면 내부 컨테이너 레지스트리 운영이 가장 이상적일 수 있고, 표준 OCI 도구들을 활용하면 관리 효율을 높일 수도 있습니다.
중요한 것은 자신의 환경에 맞는 전략을 선택하는 것입니다. 이미지 크기, 업데이트 빈도, 네트워크 대역폭, 보안 규정 등을 종합적으로 고려해볼 때, 필요하면 간단한 스크립트 작성부터 체계적인 레지스트리 구축까지 다양한 옵션이 있습니다.
Skopeo이라는 Docker 엔진 없이도 이미지를 레지스트리 간 또는 디렉토리로 직접 복사할 수 있는 툴이 있습니다. 예를 들어 외부 Docker 허브의 이미지를 가져와 폐쇄망 내부의 레지스트리로 바로 복사하거나, tar 파일로 저장할 수 있습니다.
Skopeo를 사용하면 중간에 docker pull/save 없이 한 번의 skopeo copy 명령으로 이미지를 전송할 수 있고, 레지스트리를 사용할 경우 레이어 단위 전송 최적화도 자동으로 이루어집니다. 하지만 Skopeo를 쓰려면 폐쇄망 쪽에 접근 가능한 경로나 중간 서버가 필요하며, 완전 물리적으로 격리된 경우에는 결국 save/load 방식과 큰 차이는 없습니다.
ORAS는 OCI (Open Container Initiative) 표준을 따르는 이미지 레지스트리 및 레이아웃 관리 툴로, 컨테이너 이미지를 OCI 아카이브 형식으로 다룰 수 있습니다. ORAS를 사용하면 컨테이너 이미지를 로컬 파일시스템 OCI 레이아웃으로 내보내고 가져올 수 있으며, OCI 레지스트리 간에 아티팩트를 이동시키는 등 유연한 배포가 가능합니다.
ORAS로 외부에서 이미지의 OCI Layout을 생성한 뒤 파일로 전달하고, 내부에서 해당 Layout을 다시 이미지로 등록하는 식의 작업이 가능합니다. 이 역시 내부에 별도 레지스트리 없이 이미지를 이전하는 한 가지 방법이지만, 구현 난이도가 높고 일반적인 케이스는 아닙니다. 혹은 Docker 이미지 자체를 전달하지 않고, 응용 프로그램 수준에서 패치 파일이나 zip 아카이브를 전달하여 폐쇄망 내에서 Docker 이미지를 재생성하는 방법도 생각해볼 수 있습니다.
예를 들어 소스나 바이너리만 전달해 폐쇄망 내에서 docker build를 다시 수행하거나, rsync와 비슷한 파일 동기화 도구로 컨테이너 파일시스템을 업데이트하는 것입니다. 하지만 이러한 방법들은 Docker 이미지의 일관성을 해칠 수 있고 자동화하기 까다로우며, 일반적인 CI/CD 흐름과도 동떨어져 있어 특별한 경우가 아니면 권장되지는 않습니다.
나가며
꼭 강조하고 싶은 사항이 있습니다. 폐쇄망에서도 CI/CD 파이프라인을 최대한 자동화하여 사람의 수작업을 줄이고 신뢰성을 높이는 것을 권장합니다.
수동으로 tar 파일을 옮기는 작업은 실수를 유발할 수 있으므로, 가능하다면 증분 패키지 생성과 전송, 적용 과정까지 자동화하는 것이 바람직합니다.
Docker 업데이트 효율화는 곧 배포 효율화이며, 이는 업무 생산성과 서비스 안정성으로 이어집니다. 폐쇄망이라는 한계를 넘어, 최적의 도구와 방식을 활용해 스마트한 Docker 이미지 관리 전략을 수립해보시기 바랍니다.