레이어의 마법: 컨테이너 이미지 효율성과 일관성 이해하기

레이어의 마법: 컨테이너 이미지 효율성과 일관성 이해하기

컨테이너화된 프로세스의 파일 시스템 관점

컨테이너 내부에서 실행되는 프로세스는 파일 시스템을 어떻게 볼까요? 바로 마운트 네임스페이스(Mount Namespace)와 관련이 있다고 생각할 수 있습니다. 컨테이너 내의 프로세스는 완전히 독립적인 파일 시스템을 보아야 합니다. 이렇게 하면 컨테이너의 디렉토리(예: /tmp)에서 작업을 수행해도 호스트 머신이나 다른 컨테이너의 영향을 전혀 받지 않습니다. 실제로 그럴까요?

마운트 네임스페이스의 역할

마운트 네임스페이스는 다른 네임스페이스와 약간 다르게 동작합니다. 컨테이너 프로세스가 파일 시스템을 보는 방식에 미치는 영향은 마운트 작업이 수행될 때만 적용됩니다. 그러나 일반 사용자로서 우리는 더 사용자 친화적인 시나리오를 원합니다. 새로운 컨테이너가 생성될 때마다 컨테이너 프로세스가 호스트에서 상속된 파일 시스템이 아닌 격리된 환경의 파일 시스템을 보기를 바랍니다. 어떻게 이를 달성할 수 있을까요?

컨테이너 프로세스를 시작하기 전에 전체 루트 디렉토리 "/"를 다시 마운트할 수 있다고 상상해보는 것은 어렵지 않습니다. 마운트 네임스페이스 덕분에 이 마운트는 호스트에 보이지 않으며, 컨테이너 프로세스는 그 안에서 자유롭게 작동할 수 있습니다. Linux 운영 체제에는 “chroot” 라는 명령어가 있어 셸에서 이 작업을 편리하게 수행할 수 있습니다. 이름에서 알 수 있듯이 "루트 파일 시스템 변경"을 도와주며, 프로세스의 루트 디렉토리를 지정된 위치로 리디렉션합니다.

컨테이너 이미지와 rootfs

사실 마운트 네임스페이스는 chroot의 지속적인 개선을 기반으로 발명되었으며, Linux의 첫 번째 네임스페이스입니다. 이러한 루트 디렉토리가 컨테이너에 대해 더 “실제처럼” 보이도록 하기 위해 일반적으로 이 루트 아래에 완전한 운영 체제 파일 시스템(예: Ubuntu 16.04 ISO)을 마운트합니다. 결과적으로 컨테이너가 시작된 후 컨테이너 내부에서 “ls /” 를 실행하면 Ubuntu 16.04의 모든 디렉토리와 파일이 표시됩니다. 컨테이너의 루트 디렉토리에 마운트되어 컨테이너 프로세스에 격리된 실행 환경을 제공하는 이 파일 시스템을 “컨테이너 이미지” 라고 합니다. 또한 더 기술적인 용어로 rootfs (루트 파일 시스템) 라고도 합니다.

컨테이너에 들어간 후 실행된 /bin/bash는 /bin 디렉토리 아래의 실행 파일이며, 호스트의 /bin/bash와 완전히 다릅니다. 이제 Docker 프로젝트의 핵심 원리가 기본적으로 생성할 사용자 프로세스에 대해 Linux 네임스페이스를 구성하고, 특정 Cgroups 매개변수를 설정하며, 프로세스의 루트 디렉토리를 변경(Change Root)하는 것임을 이해해야 합니다. 이렇게 하면 완전한 컨테이너가 탄생합니다.

컨테이너에서 공유 커널 및 애플리케이션 종속성

그러나 Docker는 전환의 마지막 단계에서 pivot_root 시스템 호출을 선호하며, 시스템이 pivot_root를 지원하지 않는 경우 chroot로 대체합니다. 이 두 시스템 호출은 기능이 비슷하지만 미묘한 차이가 있습니다. 또한 rootfs는 운영 체제의 파일, 구성 및 디렉토리만 포함하며 커널은 포함하지 않는다는 점을 명확히 해야 합니다. Linux에서는 이 두 부분이 별도로 저장되며, 특정 버전의 커널 이미지는 부팅 중에만 로드됩니다. 따라서 rootfs는 운영 체제의 “껍질” 만 포함하고 “영혼” 은 포함하지 않습니다.

그렇다면 컨테이너를 위한 운영 체제의 “영혼” 은 어디에 있을까요? 실제로 동일한 머신의 모든 컨테이너는 호스트 운영 체제의 커널을 공유합니다. 즉, 애플리케이션이 커널 매개변수를 구성하거나 추가 커널 모듈을 로드하거나 커널과 직접 상호 작용해야 하는 경우 이러한 작업과 종속성이 호스트 운영 체제의 커널을 대상으로 하며, 이는 해당 머신의 모든 컨테이너에 대한 “전역 변수” 라는 점에 유의해야 합니다. 이것이 가상 머신과 비교하여 컨테이너의 주요 단점 중 하나입니다. 가상 머신은 시뮬레이션된 하드웨어를 샌드박스로 가질 뿐만 아니라 각 샌드박스 내에서 완전한 게스트 OS(Guest OS)를 실행하여 애플리케이션이 사용할 수 있도록 합니다. 그럼에도 불구하고 rootfs 덕분에 컨테이너는 널리 알려진 중요한 기능인 ** 일관성**을 갖게 되었습니다.

컨테이너 이미지의 레이어 개념

컨테이너의 “일관성” 이란 무엇일까요? 클라우드와 로컬 서버 환경의 차이로 인해 애플리케이션 패키징 프로세스는 PaaS를 사용할 때 항상 가장 “고통스러운” 단계 중 하나였습니다. 그러나 컨테이너, 더 정확히는 컨테이너 이미지(즉, rootfs)를 통해 이 문제는 우아하게 해결되었습니다.

rootfs는 애플리케이션뿐만 아니라 전체 운영 체제의 파일과 디렉토리를 패키징하므로 애플리케이션 실행에 필요한 모든 종속성을 캡슐화합니다. 실제로 대부분의 개발자에게 애플리케이션 종속성에 대한 이해는 Golang의 Godeps.json과 같은 프로그래밍 언어 수준에 국한되어 있었습니다. 그러나 오랫동안 간과된 사실은 애플리케이션에게 운영 체제 자체가 실행에 필요한 가장 완전한 “종속성 라이브러리” 라는 것입니다.

컨테이너 이미지가 “운영 체제를 패키징” 하는 기능을 통해 이 기본적인 종속성 환경이 마침내 애플리케이션 샌드박스의 일부가 되었습니다. 이는 컨테이너에 널리 알려진 일관성을 부여합니다. 로컬 머신, 클라우드 또는 다른 곳에서든 사용자는 컨테이너 이미지를 풀기만 하면 애플리케이션 실행에 필요한 완전한 실행 환경을 재현할 수 있습니다.

컨테이너 이미지의 증분 레이어 설계

이러한 운영 체제 수준의 일관성은 애플리케이션의 로컬 개발과 원격 실행 환경 간의 격차를 해소합니다. 그러나 또 다른 까다로운 문제를 발견했을 수도 있습니다. 새로운 애플리케이션을 개발하거나 기존 애플리케이션을 업데이트할 때마다 rootfs를 다시 생성해야 할까요? 직관적인 해결책은 rootfs를 생성하는 동안 모든 “의미 있는” 작업 후에 rootfs를 저장하여 동료가 필요한 rootfs를 사용할 수 있도록 하는 것입니다.

이 솔루션은 확장성이 좋지 않습니다. 그 이유는 동료가 이 rootfs를 수정하면 이전 rootfs와 새로운 rootfs 사이에 관계가 없어져 극심한 파편화가 발생하기 때문입니다. 이러한 수정이 이전 rootfs를 기반으로 하므로 변경 사항을 증분 방식으로 만들 수 있을까요? 이 접근 방식의 장점은 모든 사람이 기본 rootfs에 대한 증분 내용만 유지 관리하면 되며, 수정할 때마다 “포크” 를 생성할 필요가 없다는 것입니다.

물론 대답은 “예” 입니다. 이것이 바로 Docker가 Docker 이미지를 구현할 때 rootfs를 생성하는 표준 프로세스를 따르지 않고 약간의 혁신을 한 이유입니다. Docker는 이미지 설계에 ** 레이어(layer)** 개념을 도입했습니다. 사용자가 이미지를 생성하기 위해 수행하는 각 작업은 레이어를 생성하며, 이는 증분 rootfs입니다. 이 아이디어는 갑자기 나온 것이 아니라 UnionFS(Union File System) 라는 기능을 활용한 것입니다. UnionFS의 주요 기능은 여러 위치의 여러 디렉토리를 단일 디렉토리로 union 마운트(union mount)하는 것입니다.

컨테이너의 레이어링

컨테이너의 레이어링

파트 1: 읽기 전용 레이어. 이 컨테이너의 rootfs 아래쪽 5개 레이어는 ubuntu:latest 이미지의 5개 레이어에 해당합니다. 읽기 전용(ro+wh, 즉 readonly+whiteout)으로 마운트됩니다. 각 레이어는 증분 방식으로 Ubuntu 운영 체제의 일부를 포함합니다.

파트 2: 읽기-쓰기 레이어. 이 컨테이너 rootfs의 최상위 레이어(6e3be5d2ecccae7cc)는 rw(읽기-쓰기)로 마운트됩니다. 파일이 작성되기 전에는 이 디렉토리가 비어 있습니다. 컨테이너 내에서 쓰기 작업이 수행되면 수정 사항이 이 레이어에 증분 방식으로 나타납니다. 그런데 읽기 전용 레이어에서 파일을 삭제하려면 어떻게 해야 할까요? 이 삭제를 수행하기 위해 AuFS는 읽기-쓰기 레이어에 ** 화이트아웃(whiteout)** 파일을 생성하여 읽기 전용 레이어의 파일을 “가립니다” . 예를 들어, 읽기 전용 레이어에서 foo라는 파일을 삭제하면 실제로는 읽기-쓰기 레이어에 .wh.foo라는 파일이 생성됩니다. 따라서 이러한 레이어가 union 마운트되면 foo 파일은 .wh.foo 파일에 의해 가려져 “사라집니다” . 이 기능이 “ro+wh” 마운트 방식이 의미하는 바, 즉 읽기 전용에 화이트아웃이 추가된 것입니다.

파트 3: Init 레이어. Docker 프로젝트에서 생성된 내부 레이어로, “-init” 으로 끝나며 읽기 전용 레이어와 읽기-쓰기 레이어 사이에 위치합니다. Init 레이어는 /etc/hosts 및 /etc/resolv.conf와 같은 정보를 저장하는 데 특별히 사용됩니다. 이러한 레이어가 필요한 이유는 이 파일들이 원래 읽기 전용 Ubuntu 이미지에 속하지만 컨테이너 시작 시 호스트네임과 같은 특정 값이 필요한 경우가 많기 때문입니다. 따라서 읽기-쓰기 레이어에서 수정이 필요합니다. 그러나 이러한 수정은 일반적으로 현재 컨테이너에만 적용되며, docker commit 을 실행할 때 읽기-쓰기 레이어와 함께 커밋되지 않아야 합니다. 따라서 Docker의 접근 방식은 이러한 수정된 파일을 별도의 레이어에 마운트하는 것입니다. 사용자가 docker commit 을 실행하면 읽기-쓰기 레이어만 커밋되며 이 내용은 제외됩니다.

컨테이너 이미지의 레이어 설계 장점

“레이어 이미지” 설계를 통해 Docker 이미지를 중심으로 다양한 회사와 팀의 기술자들이 긴밀하게 연결됩니다. 또한 컨테이너 이미지에 대한 작업이 증분 방식이므로 매번 풀(pull)하거나 푸시(push)하는 내용이 여러 개의 완전한 운영 체제보다 훨씬 작습니다. 공유 레이어는 이러한 모든 컨테이너 이미지에 필요한 총 공간이 각 이미지의 합보다 적다는 것을 의미합니다. 컨테이너 이미지를 기반으로 한 이러한 협업의 민첩성은 수 GB 크기의 가상 머신 디스크 이미지를 훨씬 능가합니다.

더 중요한 것은 이미지가 게시되면 전 세계 어디에서나 다운로드해도 정확히 동일한 내용을 얻을 수 있어 이미지 제작자가 만든 원래 환경을 완전히 재현할 수 있다는 점입니다.

소프트웨어 개발 워크플로우에 대한 컨테이너 이미지의 영향

컨테이너 이미지의 발명은 “개발 - 테스트 - 배포” 과정의 모든 단계를 연결할 뿐만 아니라 컨테이너 이미지가 미래 소프트웨어 배포의 주류 방법이 될 것임을 의미합니다. 이 배포 방식은 가볍고, 높은 일관성을 가지며, 협업을 용이하게 하는 등의 이점을 제공하여 소프트웨어 개발과 배포를 더욱 효율적이고 안정적으로 만듭니다. 가벼움, 일관성 및 효율성을 갖춘 컨테이너 기술은 점차 소프트웨어 개발 및 운영에 필수적인 도구가 되고 있습니다. 기술이 계속 발전하고 혁신함에 따라 컨테이너 기술이 미래에 더욱 중요한 역할을 할 것이라고 믿을 이유가 있습니다.

Novita AI, 무한한 창의성을 위한 원스톱 플랫폼으로 100개 이상의 API에 액세스할 수 있습니다. 이미지 생성 및 언어 처리부터 오디오 향상 및 비디오 조작까지, 저렴한 종량제 요금제로 자체 제품을 구축하면서 GPU 유지 관리의 번거로움에서 해방됩니다. 지금 무료로 사용해 보세요.