컨테이너의 파일 시스템 격리 설계
이 글은 mydocker 프로젝트의 5번째 단계로, 기존의 pivotRoot 기반 루트 파일 시스템 전환을 넘어선 진정한 컨테이너 격리를 실현합니다. 주요 목적은 컨테이너 내부에서의 모든 쓰기 작업이 호스트 시스템의 원본 이미지에 영향을 주지 않도록 하는 것입니다.
OverlayFS의 핵심 개념
OverlayFS는 유니온 파일 시스템(UnionFS)의 하나로, 여러 개의 디렉터리를 하나의 통합된 뷰로 결합하는 기술입니다. 이 시스템은 다음 네 가지 계층으로 구성됩니다:
- lower: 읽기 전용 계층. 컨테이너 이미지의 원본 데이터가 위치.
- upper: 쓰기 가능 계층. 컨테이너 내부에서 발생하는 모든 변경 사항이 저장.
- merged: 최종적으로 사용되는 가상 루트 디렉터리. 모든 계층의 내용을 통합하여 보여줌.
- work: 내부 동작에 필요한 임시 공간.
특히 중요한 점은, 읽기 전용 계층에 대한 수정 요청은 실제 원본을 변경하지 않고, 상위 계층인 upper에 복제하여 처리한다는 것입니다. 이 방식은 쓰기 시 복사 기법(Copy-on-Write, CoW)을 기반으로 하며, 자원 낭비를 줄이고 성능을 유지합니다.
구현 단계
다음은 컨테이너 시작 전 준비 과정입니다.
1. 이미지 준비 및 디렉터리 생성
func prepareImageLayer(rootPath string) error {
imageDir := rootPath + "image/"
tarFile := rootPath + "busybox.tar"
// 이미 존재 여부 확인
if _, err := os.Stat(imageDir); os.IsNotExist(err) {
if err := os.MkdirAll(imageDir, 0755); err != nil {
return err
}
// 압축 해제
cmd := exec.Command("tar", "-xvf", tarFile, "-C", imageDir)
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}
2. OverlayFS 마운트 설정
필요한 계층 디렉터리들을 생성하고, overlay 타입으로 마운트합니다.
func mountOverlay(rootPath, mountPoint string) error {
upperDir := rootPath + "upper/"
workDir := rootPath + "work/"
mergedDir := mountPoint
// 디렉터리 생성
for _, dir := range []string{upperDir, workDir, mergedDir} {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
// 마운트 옵션 조립
opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s",
rootPath+"image/", upperDir, workDir)
cmd := exec.Command("mount", "-t", "overlay", "overlay", "-o", opts, mergedDir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
3. 컨테이너 실행 시 루트 경로 변경
마운트 완료 후, pivotRoot 호출 시 merged 디렉터리를 새로운 루트로 지정합니다.
func createContainerProcess(tty bool) (*exec.Cmd, *os.File) {
rootDir := "/root/"
mntDir := rootDir + "merged/"
// 초기화: 이미지 추출, 디렉터리 생성, 마운트
prepareImageLayer(rootDir)
mountOverlay(rootDir, mntDir)
cmd := exec.Command("/proc/self/exe", "init")
cmd.Dir = mntDir
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC |
syscall.CLONE_NEWUSER | syscall.CLONE_NEWPID | syscall.CLONE_NEWNET,
}
readPipe, writePipe, _ := os.Pipe()
cmd.ExtraFiles = []*os.File{readPipe}
return cmd, writePipe
}
컨테이너 종료 시 리소스 정리
컨테이너 종료 시, 마운트를 해제하고 일시적 디렉터리들을 제거함으로써 변경 사항을 영구적으로 삭제합니다.
func cleanupOverlay(mountPoint string) error {
// 마운트 해제
if err := syscall.Unmount(mountPoint, 0); err != nil {
return err
}
// upper, work, merged 디렉터리 삭제
dirs := []string{"upper", "work", "merged"}
for _, d := range dirs {
path := "/root/" + d
if err := os.RemoveAll(path); err != nil {
return err
}
}
return nil
}
테스트 결과 분석
컨테이너 내부에서 파일을 생성하면:
- 원본
/root/image디렉터리는 그대로 유지됨. - 변경 내용은
/root/upper에만 반영됨. /root/merged는 두 계층의 통합 뷰를 제공하므로 해당 파일을 볼 수 있음.- 컨테이너 종료 후,
upper,work,merged폴더는 삭제되며, 변경 사항은 완전히 소멸.
결론
이 구현은 다음과 같은 컨테이너 특성을 재현했습니다:
- 이미지 계층은 불변 상태 유지.
- 컨테이너 내부의 쓰기 작업은 호스트에 영향 없음.
- 컨테이너 종료 시 모든 변경 사항이 자동 삭제.
이러한 메커니즘은 실제 도커의 이미지 레이어링과 동일한 원리이며, 컨테이너의 격리성과 재사용성을 확보하는 핵심 기술입니다.