[도커 만들기] 3탄: 중복 문제 해결하기

이지호·2025년 8월 15일
0

도커 만들기

목록 보기
3/3
post-thumbnail

0. 지난 이야기

'도커 만들기' 시리즈의 세 번째 여정으로 돌아왔다. 리눅스 커널에 이미 존재하는 기능들을 조합해 직접 컨테이너를 만들어보는 도전을 이어가고 있다.

지난 1탄에서는 첫 시도로 chroot 명령어를 사용해 프로세스가 특정 디렉토리 바깥을 보지 못하도록 가두는 데 도전했다. 그럴듯한 격리 환경이 만들어진 듯했지만, 이내 간단한 C 코드로 감옥을 탈출하는 '탈옥' 현상을 마주하며 chroot만으로는 진정한 컨테이너를 만들 수 없다는 한계를 확인했다.

이어진 2탄에서는 이 '탈옥' 문제를 해결하기 위해 마운트 네임스페이스와 pivot_root라는 더 강력한 도구를 꺼내 들었다. 프로세스마다 독립된 파일 시스템 공간을 만들어주고, 그 안에서 루트 파일 시스템을 통째로 바꿔치기하여 마침내 빠져나올 수 없는 견고한 감옥을 만드는 데 성공했다.

1. 중복이라는 새로운 문제

지난 포스팅까지 우리는 컨테이너에 필요한 도구들을 한데 모아 '패키징'하는 작업을 해왔다. 예를 들어 lssh 같은 필수 명령어와 라이브러리들을 특정 폴더(wlghroot)에 미리 복사해두었는데, 이것이 바로 도커에서 말하는 '이미지(Image)'의 원시적인 형태라 할 수 있다. 그리고 이 '이미지'를 기반으로 실행한 독립적인 환경이 바로 '컨테이너(Container)'다.

지금까지의 방식은 완벽한 격리를 구현하는 데는 성공했지만, 중복이라는 새로운 문제가 발생했다.

상황을 가정해보자.

  1. 첫 번째 컨테이너: 기본적인 리눅스 명령어(ls, ps 등)를 사용하기 위해, 우분투(Ubuntu)의 핵심 파일들을 복사한 my-ubuntu 이미지를 만들었다. (이미지 용량: 500MB)
  2. 두 번째 컨테이너: 이번엔 웹서버를 띄우고 싶어 nginx가 필요해졌다. 가장 쉬운 방법은 my-ubuntu 이미지의 모든 파일을 그대로 복사한 뒤, 그 위에 nginx 관련 파일(50MB)만 추가하여 my-nginx 이미지를 만드는 것이다. (이미지 용량: 500MB + 50MB)
  3. 세 번째 컨테이너: 데이터베이스 컨테이너도 돌려보려 한다. 이번에도 my-ubuntu 이미지 파일을 통째로 복사하고, 그 위에 mysql 파일(150MB)들을 추가해 my-mysql 이미지를 만든다. (이미지 용량: 500MB + 150MB)

여기서 핵심적인 문제가 드러난다. my-nginxmy-mysql 이미지는 모두 my-ubuntu 이미지의 완전한 복사본을 포함하고 있다. 결국 우리 디스크에는 거의 동일한 내용의 우분투 핵심 파일들이 세 번이나 중복 저장되는 셈이다. 이는 컨테이너가 단 3개임에도 불구하고 500MB짜리 공통 파일 때문에 1000MB의 저장 공간이 추가로 낭비되는 결과를 낳는다. 컨테이너가 수십, 수백 개로 늘어난다면 이 문제는 더욱 심각해질 것이다.

그렇다면 이 파일 중복 문제를 어떻게 해결할 수 있을까? 여러 이미지가 공통으로 사용하는 부분은 딱 한 번만 저장하고, 각 이미지에 특화된 부분만 효율적으로 추가 관리할 방법은 없을까?

2. 오버레이 파일시스템(OverlayFS)이란?

파일 중복 문제를 해결하는 열쇠는 바로 오버레이 파일시스템(OverlayFS)이다.

오버레이 파일시스템은 리눅스에서 지원하는 파일 시스템의 한 종류로, 여러 개의 다른 디렉터리를 겹쳐서(overlay) 마치 하나의 통합된 디렉터리처럼 보이게 만드는 기술이다. 이를 전문 용어로 유니온 마운트(Union Mount) 또는 유니온 파일시스템이라고 부른다.

이름 그대로, 여러 디렉터리 레이어를 투명한 셀로판지처럼 겹쳐서 사용하는 원리다.

셀로판지 비유
(이미지 출처: https://blog.naver.com/alice_k106/221530340759)

가장 아래에 그림이 인쇄된 셀로판지(우분투 기본 파일)를 깔고, 그 위에 텅 빈 투명 셀로판지를 한 장 올린다고 상상해보자. 우리는 두 장이 겹쳐진 전체 모습을 보게 되고, 그림을 그리거나 글씨를 쓸 땐 맨 위 투명 셀로판지에만 작업하게 된다. 아래 깔린 원본 그림은 절대 건드리지 않는다.

오버레이 파일시스템은 정확히 이런 방식으로 동작하며, 다음과 같은 세 가지 핵심 요소로 구성된다.

  • lowerdir
    여러 장을 겹칠 수 있는 읽기 전용(Read-only) 레이어다. 방금 비유에서 그림이 인쇄된 셀로판지, 즉 원본에 해당하는 부분이다. 도커의 경우, 우분투 같은 베이스 이미지가 이 레이어에 해당한다. 원본이므로 절대 내용이 바뀌지 않는다.

  • upperdir
    lowerdir 위에 단 한 장만 놓을 수 있는 쓰기 가능(Writable) 레이어다. 비유에서 우리가 그림을 그리거나 글씨를 쓸 수 있는 맨 위의 투명 셀로판지에 해당한다. 컨테이너가 실행되면서 발생하는 모든 변경 사항(파일 생성, 수정, 삭제)은 이 레이어에 기록된다.

  • merged
    lowerdirupperdir를 합쳐 사용자에게 보여주는 가상의 통합 디렉터리다. 사용자는 이 merged 디렉터리를 통해 마치 하나의 일반적인 디렉터리처럼 파일을 읽고, 쓰고, 지울 수 있다. 우리가 겹쳐진 셀로판지 전체를 보는 것과 같다.

구체적으로 변경은 어떻게 처리될까?: Copy-on-Write (COW)

오버레이 파일시스템의 핵심은 원본(lowerdir)을 절대 건드리지 않는다는 점이다. 그렇다면 파일 수정이나 삭제는 어떻게 이루어질까? 바로 Copy-on-Write (COW), 즉 '쓸 때 복사한다'는 전략을 사용한다.

  • 파일 읽기 (Read): merged 뷰에서 파일을 읽을 때, upperdir에 해당 파일이 있으면 그 파일을 우선적으로 읽고, 없으면 lowerdir에 있는 원본 파일을 읽는다.

  • 파일 생성 (Create): 사용자가 merged 디렉터리에서 파일을 생성한다고 해보자. (사용자는 통합된 뷰를 제공하는 merged 디렉터리를 통해 작업하는 것이 원칙이므로, 자연스럽게 그곳에 파일을 생성하게 된다.) 이때 오버레이 파일 시스템은 이 작업을 감지하고, 실제 파일 데이터는 쓰기 전용 레이어인 upperdir에 저장한다.

  • 파일 수정 (Write): lowerdir에 있는 원본 파일을 수정하려고 하면, 오버레이 파일시스템은 그 파일을 즉시 upperdir로 복사한 뒤, 복사된 파일을 수정한다. 원본 파일은 그대로 유지된다. 이제 merged 뷰는 수정된 upperdir의 파일을 원본보다 우선하여 보여준다.

  • 파일 삭제 (Delete): lowerdir의 파일을 삭제하면, 실제 파일이 지워지는 대신 upperdir에 해당 파일이 지워졌다는 표시(whiteout 파일)를 남긴다. merged 뷰는 이 표시를 보고 마치 파일이 없는 것처럼 사용자에게 보여준다.

이러한 COW 전략 덕분에 여러 컨테이너가 동일한 베이스 이미지(lowerdir)를 공유하면서도, 각 컨테이너의 변경 사항(upperdir)은 서로에게 영향을 주지 않고 독립적으로 관리할 수 있다. 결과적으로 디스크 공간을 절약할 수 있게 되는 것이다.

실습

직접 실습하면서 overlayFS를 더 이해해보자.

root@jiho:~/overlayfs# tree
.
├── lower1
│   ├── lower.txt #'This is lower1'
│   └── lower1.txt #'This is lower1'
├── lower2
│   ├── lower.txt #'This is lower2'
│   └── lower2.txt #'This is lower2'
├── merged
├── upper
└── worker

root@jiho:~/overlayfs# cat lower1/lower.txt
This is lower1
root@jiho:~/overlayfs# cat lower1/lower1.txt
This is lower1
root@jiho:~/overlayfs# cat lower2/lower.txt
This is lower2
root@jiho:~/overlayfs# cat lower2/lower2.txt
This is lower2

일단은 이렇게 셋팅을 해준 뒤 overlay를 진행해보자.

mount -t overlay overlay -o lowerdir=./lower2:./lower1,upperdir=./upper,workdir=./worker ./merged

해당 명령어는 mount [옵션] [소스] [마운트 포인트]라는 기본 구조를 따른다. 각 부분의 역할은 다음과 같다.

  • mount
    • 파일 시스템을 특정 디렉터리에 연결(마운트)하는 리눅스 기본 명령어다.
  • -t overlay
    • -t는 마운트할 파일 시스템의 종류(type)를 지정하는 옵션이다.
    • overlay로 지정하여 오버레이 파일 시스템을 사용하겠다고 명시했다.
  • overlay
    • 마운트할 소스(source)에 해당한다.
    • 오버레이 파일 시스템은 실제 동작에 필요한 모든 정보가 -o 옵션으로 전달되므로, 이 부분은 의미 없는 이름(dummy name)을 관례적으로 사용한다.
  • -o lowerdir=...,upperdir=...,workdir=...
    • 마운트에 필요한 세부 옵션(options)을 설정하는 부분이다. 각 옵션은 쉼표(,)로 구분한다.
    • lowerdir=./lower2:./lower1: 읽기 전용 기반 레이어를 지정했다.
      • 콜론(:)을 사용해 여러 디렉터리를 지정할 수 있다.
      • 레이어는 오른쪽에서 왼쪽 순서로 쌓인다. 즉, lower1이 가장 아래에, 그 위에 lower2가 위치한다.
      • 만약 여러 레이어에 동일한 이름의 파일이 있다면, 더 왼쪽에 있는(더 위에 있는) lower2의 파일이 최종적으로 보인다.
    • upperdir=./upper: 쓰기 가능한 최상위 레이어를 지정했다.
    • workdir=./worker: 오버레이 파일 시스템의 내부 작업 공간이다.
      • lowerdir의 원본 파일을 수정할 때 upperdir로 복사해오는 등(Copy-on-Write)의 원자적 작업을 위한 임시 공간이다.
      • 주의: 마운트 시점에 이 디렉터리는 반드시 비어 있어야 한다.
  • ./merged
    • 최종 결과물이 보여지는 마운트 포인트다.

(1) 읽기

마운트 하자마자 다시 tree를 해보면, 아래와 같이 mergedlower, lower1, lower2 텍스트 파일이 생긴 것을 알 수 있다.

root@jiho:~/overlayfs# mount -t overlay overlay -o lowerdir=./lower2:./lower1,upperdir=./upper,workdir=./worker ./merged
root@jiho:~/overlayfs# tree
.
├── lower1
│   ├── lower.txt #'This is lower1'
│   └── lower1.txt #'This is lower1'
├── lower2
│   ├── lower.txt #'This is lower2'
│   └── lower2.txt #'This is lower2'
├── merged
│   ├── lower.txt
│   ├── lower1.txt
│   └── lower2.txt
├── upper
└── worker
    └── work

그렇다면 ./merged/lower.txt에는 어떤 내용이 있을까? (여기까지 잘 읽어주신 독자라면 이미 예측하셨을 것 같다.)

root@jiho:~/overlayfs# cat merged/lower.txt
This is lower2 # 정답!

lower2lower1보다 위에 쌓여있었기 때문에, lower.txt에는 lower2의 내용이 보여진다.

정리하자면 아래 그림과 같다.

읽기 실습

(2) 생성

이제 파일을 생성해보자.

root@jiho:~/overlayfs# touch ./merged/hello.txt
root@jiho:~/overlayfs# tree
.
├── lower1
│   ├── lower.txt
│   └── lower1.txt
├── lower2
│   ├── lower.txt
│   └── lower2.txt
├── merged
│   ├── hello.txt # 여기에 파일을 만듦
│   ├── lower.txt
│   ├── lower1.txt
│   └── lower2.txt
├── upper
│   └── hello.txt # 새롭게 생김!
└── worker
    └── work

uppermerged에 동일하게 파일이 만들어진 것을 확인할 수 있다.

내부적으로 어떻게 동작한 것일까? stat 명령어로 더 살펴보자.

root@jiho:~/overlayfs# stat ./upper/hello.txt
  File: ./upper/hello.txt
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: 202,1	Inode: 264916      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2025-08-15 06:44:57.009422273 +0000
Modify: 2025-08-15 06:44:57.009422273 +0000
Change: 2025-08-15 06:44:57.009422273 +0000
 Birth: 2025-08-15 06:44:57.009422273 +0000

root@jiho:~/overlayfs# stat ./merged/hello.txt
  File: ./merged/hello.txt
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: 0,43	Inode: 264916      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2025-08-15 06:44:57.009422273 +0000
Modify: 2025-08-15 06:44:57.009422273 +0000
Change: 2025-08-15 06:44:57.009422273 +0000
 Birth: 2025-08-15 06:44:57.009422273 +0000

두 파일 모두 Inode 번호까지 264916으로 동일하다! (Device 번호가 다른 것은 upper가 실제 디스크 장치에, merged는 가상 파일시스템 장치에 속해있기 때문이다.) 이는 두 경로가 별개의 파일이 아니라, .upper/hello.txt라는 단 하나의 실제 파일을 ./merged/hello.txt라는 가상의 경로로도 접근할 수 있음을 의미한다. 즉, mergedupper에 생성된 실제 파일의 뷰(view) 역할을 하는 것이다.

그림으로 정리해보자면 아래와 같다.

생성 실습

(3) 수정

이제 merged뷰에서 방금 만든 hello.txt를 수정해보자.

root@jiho:~/overlayfs# echo 'hello world!' >> ./merged/hello.txt
root@jiho:~/overlayfs# cat ./upper/hello.txt
hello world!

upper 디렉터리의 파일이 직접 변경된 것을 볼 수 있다.

그러면 이제 lower1.txt의 내용을 수정해보자.

# 수정 전 tree: upper에는 hello.txt만 있다.
root@jiho:~/overlayfs# tree ./upper/
./upper/
└── hello.txt

# merged 뷰에서 lower1.txt 수정 시도
root@jiho:~/overlayfs# echo 'update lower1' >> ./merged/lower1.txt

# 수정 후 tree: upper에 lower1.txt가 복사되었다! (Copy-on-Write)
root@jiho:~/overlayfs# tree ./upper/
./upper/
├── hello.txt
└── lower1.txt

이제 각 레이어의 파일 내용을 확인해보자.

# merged 뷰: 내용이 수정되었다.
root@jiho:~/overlayfs# cat ./merged/lower1.txt
This is lower1
update lower1

# upperdir: 복사된 파일에 수정 내용이 저장되었다.
root@jiho:~/overlayfs# cat ./upper/lower1.txt
This is lower1
update lower1

# lowerdir: 원본 파일은 그대로 유지되었다!
root@jiho:~/overlayfs# cat ./lower1/lower1.txt
This is lower1

이처럼 원본(lower1/lower1.txt)은 그대로 둔 채, upper 디렉터리로 파일을 복사한 뒤 변경 사항을 적용하는 것을 확인할 수 있다.

그림으로 여기까지를 정리하면 아래와 같다.

수정 실습

(4) 삭제

이제 lower 레이어에만 존재하는 lower2.txt를 삭제하면 어떤 일이 발생하는지 살펴보자.

# 삭제 이전 tree 결과
root@jiho:~/overlayfs# tree
.
├── lower1
│   ├── lower.txt
│   └── lower1.txt
├── lower2
│   ├── lower.txt
│   └── lower2.txt
├── merged
│   ├── hello.txt
│   ├── lower.txt
│   ├── lower1.txt
│   └── lower2.txt # 삭제 해보자!
├── upper
│   ├── hello.txt
│   └── lower1.txt
└── worker
    └── work

# merged 뷰에서 lower2.txt 삭제
root@jiho:~/overlayfs# rm ./merged/lower2.txt

# 삭제 이후 tree 결과
root@jiho:~/overlayfs# tree
.
├── lower1
│   ├── lower.txt
│   └── lower1.txt
├── lower2
│   ├── lower.txt
│   └── lower2.txt
├── merged # merged 내부에는 lower2.txt가 사라졌다.
│   ├── hello.txt
│   ├── lower.txt
│   └── lower1.txt
├── upper
│   ├── hello.txt
│   └── lower1.txt
│   └── lower2.txt # 여기엔 파일이 생겼다!
└── worker
    └── work
        └── #a # overlayFS의 내부동작 흔적
 
# ./upper/lower2.txt에 대해서 자세히 알아보자.
root@jiho:~/overlayfs# ls -al ./upper/lower2.txt
c--------- 2 root root 0, 0 Aug 15 07:02 ./upper/lower2.txt

c---------로 표시된 파일이 바로 화이트아웃(whieout) 파일이다. c는 이 파일이 문자 디바이스(character device) 파일임을 의미한다. 뒤의 0, 0은 이 디바이스의 주/부 번호가 0임을 나타낸다. OverlayFS는 이 특별한 파일을 '여기 해당하는 하위 레이어의 파일은 없는 척해라'는 삭제 표시로 인식한다. 따라서 merged 뷰에서는 lower2 디렉터리에 lower2.txt가 여전히 존재함에도 불구하고 이 화이트아웃 파일 때문에 보이지 않게 되는 것이다.

여기까지의 내용을 그림으로 정리해보면 아래와 같다.

삭제 실습

여기서 한 가지 장난기가 발동한다. 그럼 이 화이트아웃 파일을 삭제하면 다시 merged에서 lower2.txt를 볼 수 있을까?

# upper 디렉터리에서 화이트아웃 파일을 직접 삭제한다. 
root@jiho:~/overlayfs# rm ./upper/lower2.txt

# merged 뷰를 다시 확인해보자.
root@jiho:~/overlayfs# tree
.
├── lower1
│   ├── lower.txt
│   └── lower1.txt
├── lower2
│   ├── lower.txt
│   └── lower2.txt
├── merged
│   ├── hello.txt
│   ├── lower.txt
│   ├── lower1.txt
│   └── lower2.txt # 다시 생겼다!
├── upper
│   ├── hello.txt
│   └── lower1.txt
└── worker
    └── work
        └── #a

# 내용도 원본 그대로 복구되었다.
root@jiho:~/overlayfs# cat ./merged/lower2.txt
This is lower2

마치 원본을 가리고 있던 포스트잇을 떼어내듯, ./upper/lower2.txt 화이트아웃 파일을 지우니 다시 merged 뷰에서 원본 파일에 접근할 수 있게 되었다!

그림으로 정리해보면 아래와 같다.

삭제 원복 실습

이렇게 네 가지 실습을 통해서 오버레이 파일시스템에 대해 더 자세히 이해해보았다. 이제는 실제 컨테이너를 만드는 상황에서 중복 문제를 해결해보자.

3. 실습: 두 컨테이너의 중복 문제를 해결하자

이론은 충분히 익혔으니, 이제 실제 컨테이너를 만드는 상황에 OverlayFS를 적용하여 파일 중복 문제를 해결해보자. 우리의 목표는 다음과 같다.

  • 공통 베이스: ls, sh 등 필수 명령어를 가진다.
  • 컨테이너 1: 공통 베이스에 tree 명령어가 추가된 컨테이너.
  • 컨테이너 2: 공통 베이스에 which 명령어가 추가된 컨테이너.

핵심은 ls, sh와 같은 공통 파일들을 단 한 번만 저장하고, 각 컨테이너에는 필요한 treewhich만 효율적으로 추가하는 것이다.

1단계: 이미지 레이어 준비하기

먼저 도커의 이미지 레이어처럼 디렉터리를 구성했다. 각 디렉터리는 독립적인 레이어 역할을 한다.

  • base_layer: 모든 컨테이너가 공유할 공통 베이스 레이어. (ls, sh 및 필수 라이브러리 포함)
  • tree_layer: 첫 번째 컨테이너에만 추가될 특화 레이어. (tree 명령어 포함)
  • which_layer: 두 번째 컨테이너에만 추가될 특화 레이어. (which 명령어 포함)
  • upper1, upper2: 각 컨테이너가 실행 중 변경 사항을 저장할 쓰기 가능 레이어. (처음엔 비어 있음)
  • merged1, merged2: 각 컨테이너의 최종 루트 파일 시스템이 될 통합 뷰.
root@jiho:~# tree
.
├── base_layer      # 공통 파일 (sh, ls 등)
│   ├── bin
│   │   ├── ls
│   │   └── sh
│   ├── lib
│   │   └── x86_64-linux-gnu
│   │       ├── libc.so.6
│   │       ├── libpcre2-8.so.0
│   │       └── libselinux.so.1
│   └── lib64
│       └── ld-linux-x86-64.so.2
├── tree_layer      # 컨테이너1 전용 (tree)
│   ├── bin
│   │   └── tree
│   ├── lib
│   │   └── libc.so.6
│   └── lib64
│       └── ld-linux-x86-64.so.2
├── which_layer     # 컨테이너2 전용 (which)
│   └── bin
│       └── which
├── merged1
├── merged2
├── upper1          # 컨테이너1 쓰기 영역 (비어 있음)
├── upper2          # 컨테이너2 쓰기 영역 (비어 있음)
├── work1
└── work2

각 레이어에는 ldd 명령어로 확인한 의존성 라이브러리들이 올바른 경로에 미리 복사되어 있다고 가정한다.

2단계: OverlayFS로 레이어 마운트하기

이제 mount 명령어를 사용해 준비된 레이어들을 겹쳐 쌓아 최종 merged 뷰를 만들었다. 이것이 바로 이번 실습의 하이라이트다.

# 컨테이너 1 마운트: tree_layer가 base_layer 위에 쌓인다.
mount -t overlay overlay -o lowerdir=./tree_layer:./base_layer,upperdir=./upper1,workdir=./work1 ./merged1

# 컨테이너 2 마운트: which_layer가 base_layer 위에 쌓인다.
mount -t overlay overlay -o lowerdir=./which_layer:./base_layer,upperdir=./upper2,workdir=./work2 ./merged2

lowerdir 옵션을 자세히 살펴보면 base_layer가 가장 아래에, 그 위에 tree_layerwhich_layer가 겹쳐지게 된다.

마운트 후 merged 디렉터리를 확인해 보자.

# tree merged1
merged1
├── bin
│   ├── ls    (from base_layer)
│   ├── tree  (from tree_layer)
│   └── sh    (from base_layer)
├── lib
│   ├── libc.so.6
│   └── x86_64-linux-gnu
│       ├── libc.so.6
│       ├── libpcre2-8.so.0
│       └── libselinux.so.1
└── lib64
    └── ld-linux-x86-64.so.2

# tree merged2
merged2
├── bin
│   ├── ls    (from base_layer)
│   ├── sh    (from base_layer)
│   └── which (from which_layer)
├── lib
│   └── x86_64-linux-gnu
│       ├── libc.so.6
│       ├── libpcre2-8.so.0
│       └── libselinux.so.1
└── lib64
    └── ld-linux-x86-64.so.2

파일을 전혀 복사하지 않았음에도 불구하고, 각 merged 디렉터리는 마치 base_layer와 각자의 특화 레이어가 처음부터 합쳐져 있었던 것처럼 보인다. 완벽한 통합 뷰가 완성된 것이다.

overlayFS

그림으로 정리하자면 위와 같다.

3단계: pivot_root로 컨테이너 실행하기

이제 2탄에서 배운 기술을 활용하여, 완성된 merged 디렉터리를 각 컨테이너의 루트 파일 시스템으로 만들 차례다.

셸1: tree 특화 컨테이너 생성

첫 번째 셸에서는 merged1을 루트로 삼는 컨테이너를 만들어보았다.

# 1. 마운트 네임스페이스 분리
unshare --mount /bin/sh

# 2. merged1 디렉토리로 이동 후, pivot_root 준비
mkdir merged1/put_old
cd merged1

# 3. pivot_root 실행! 현재 디렉토리(merged1)를 새로운 루트로 지정
pivot_root . put_old

이제 이 셸은 merged1을 루트 디렉터리로 인식하는 완전한 격리 공간이 되었다. 명령어를 확인해 보자.

# ls와 tree는 잘 실행된다.
# ls
bin  lib  lib64  put_old
# tree -L 1
.
|-- bin
|-- lib
|-- lib64
`-- put_old

# 하지만 which 명령어는 존재하지 않는다.
# which ls
/bin/sh: 1: which: not found

예상대로 base_layerlstree_layertree는 사용 가능하지만, which_layer에 있던 which는 보이지 않는다.

셸2: which 특화 컨테이너 생성

별도의 셸을 열어 동일한 과정을 merged2를 대상으로 진행했다.

# 1. 새 터미널에서 마운트 네임스페이스 분리
unshare --mount /bin/sh

# 2. merged2 디렉토리로 이동 후, pivot_root 준비
mkdir merged2/put_old
cd merged2

# 3. pivot_root 실행!
pivot_root . put_old

결과는 어떨까?

# ls와 which는 잘 실행된다.
# ls
bin  lib  lib64  put_old
# which ls
/bin/ls

# 하지만 tree 명령어는 존재하지 않는다.
# tree -L 1
/bin/sh: 1: tree: not found

이번에는 반대로 lswhich는 잘 동작하지만, tree 명령어는 찾을 수 없다는 메시지가 나타난다.

이로써 우리는 공통 베이스 파일(base_layer)은 단 한 벌만 유지하면서도, 각기 다른 기능(tree, which)을 가진 두 개의 독립적인 컨테이너를 성공적으로 실행했다. 디스크 공간을 획기적으로 절약하는 도커 이미지의 레이어 개념을 직접 구현해 본 것이다!

4. 도커는 어떻게 레이어를 관리할까?

도커 레이어 구조
(이미지 출처: https://creboring.net/blog/how-docker-divide-image-layer)

실제 도커는 이 레이어들을 어떻게 관리하고 있을까? 간단한 도커 이미지를 직접 만들고, docker inspect 명령어로 그 속을 들여다보자.

1단계: 도커 컨테이너 실행하기

먼저 COPY 명령어로 이미지에 추가할 간단한 텍스트 파일을 하나 만들었다.

그리고 아래와 같이 Dockerfile을 작성해주었다.

# 1. alpine 이미지의 레이어를 베이스로 가져온다.
FROM alpine:latest

# 2. RUN 명령으로 새로운 레이어를 만들고, curl 패키지를 설치한다.
RUN apk add --no-cache curl

# 3. COPY 명령으로 새로운 레이어를 만들고, hello.txt 파일을 /app 디렉토리에 복사한다.
COPY hello.txt /app/

# 4. 컨테이너 실행 시 기본 명령어를 지정한다. (메타데이터 변경, 새 레이어 X)
CMD ["/bin/sh"]

이후 이미지 빌드를 하자.

docker build -t my-layered-image .

그리고 컨테이너를 실행하자.

# my-layered-image 이미지로 my-container 라는 이름의 컨테이너를 백그라운드에서 실행
docker run -d --name my-container my-layered-image tail -f /dev/null

2단계: docker inspect

docker inspect my-container

수많은 JSON 정보가 출력되는데, 그중 GraphDriver 섹션에서 익숙한 단어를 만났다!

"GraphDriver": {
    "Data": {
        "ID": "221053e5b9b2a644f69aad5e2f874090215bb96da6cb54d42736d282a81deff0",
        "LowerDir": "/var/lib/docker/overlay2/09511d7e09b225464b3b141bac567a6543f8095e489e87d96ced2968898a1105-init/diff:/var/lib/docker/overlay2/wbbilza7w5264qycwlkazyzyu/diff:/var/lib/docker/overlay2/xl1eu4trsguohul88njuf49p6/diff:/var/lib/docker/overlay2/7edcd8f02329487232d90c779a22c667602a20eb4d1ac35553ef1f8d581032a5/diff",
        "MergedDir": "/var/lib/docker/overlay2/09511d7e09b225464b3b141bac567a6543f8095e489e87d96ced2968898a1105/merged",
        "UpperDir": "/var/lib/docker/overlay2/09511d7e09b225464b3b141bac567a6543f8095e489e87d96ced2968898a1105/diff",
        "WorkDir": "/var/lib/docker/overlay2/09511d7e09b225464b3b141bac567a6543f8095e489e87d96ced2968898a1105/work"
    },
    "Name": "overlay2"
},
  • LowerDir: 우리가 만든 이미지가 이 레이어에 저장될 것으로 예상된다!
  • UpperDir: 이 컨테이너만을 위한 쓰기 가능한 컨테이너 레이어이다. 컨테이너에 접속해서 파일을 만들면 이 디렉터리에 COW를 해줄 것이다.
  • MergedDir: LowerDirUpperDir를 합쳐 컨테이너에게 최종적으로 보여주는 통합 뷰가 될 것이다.

반가운 용어들을 보니 뿌듯하다! 지금까지의 실습이 적용되는 모습이다!

5. 정리

개념 총정리

이번 여정을 통해 우리는 컨테이너 기술의 핵심 중 하나인 중복 문제 해결에 대해 깊이 파고들었다.

  • 문제 인식: 여러 컨테이너를 만들 때 발생하는 파일 중복 문제는 심각한 저장 공간 낭비를 초래한다.
  • 해결 열쇠: 리눅스의 오버레이 파일시스템(OverlayFS)은 여러 디렉터리를 겹쳐 하나의 통합된 뷰로 보여주는 유니온 마운트 기술로 이 문제를 해결한다.
  • 핵심 원리: 읽기 전용 lowerdir(이미지 레이어)와 쓰기 가능 upperdir(컨테이너 레이어)를 분리하고, 변경이 필요할 때만 파일을 복사하는 Copy-on-Write(COW) 전략을 사용한다.
  • 실증: mount 명령어로 직접 OverlayFS를 다루어보고, docker inspect로 실제 도커 컨테이너 역시 동일한 LowerDir, UpperDir, MergedDir 구조로 동작함을 확인했다.

소감과 다음

지금까지 파일 시스템 격리에 중점을 뒀고, OverlayFS를 통해 이미지 중복 문제까지 해결해봤다. 리눅스 커널의 기능을 하나씩 조합하며 도커의 내부를 들여다보는 과정은 정말 흥미로웠다. 단순히 명령어를 사용하는 것을 넘어, 그 아래에 어떤 원리가 숨어있는지 이해하게 되니 컨테이너 기술이 더욱 가깝게 느껴진다.

하지만 진정한 컨테이너를 만들기 위한 여정은 아직 끝나지 않았다. 격리해야 하는 것이 파일 시스템만 있는 것은 아니다. 컨테이너 안의 프로세스가 호스트의 다른 프로세스를 보지 못하게 막는 프로세스 격리, 컨테이너가 자신만의 독립적인 네트워크 공간을 갖도록 하는 네트워크 격리 등 아직 넘어야 할 산이 더 있다.

다음 포스팅에서는 이러한 새로운 격리 문제들을 다뤄보며, 컨테이너를 완성하는 여정을 계속 이어가 보겠다.

레퍼런스

0개의 댓글