TL;DR

  • 컨테이너는 리눅스 커널의 독립된 기능이 아니라 네임스페이스(Namespaces) 등 여러 리눅스 도구를 조합해 별도 머신처럼 보이게 하는 환경임.
  • Docker 이미지는 파일시스템 레이어로 구성되며, 여러 이미지 레이어의 내용이 겹쳐져 최종 파일시스템 뷰를 만듦.
  • 컨테이너가 분리해야 하는 핵심 요소는 네트워크 스택, 파일시스템, 프로세스임.
  • unshare, pivot_root, 마운트, veth 장치를 조합하면 별도의 프로세스·파일시스템·네트워크 환경을 직접 구성할 수 있음.
  • 직접 만든 환경은 Docker와 유사하지만, 변경 사항을 기본 이미지와 분리하는 오버레이 파일시스템이 빠져 있음.

Docker 이미지는 무엇인가?

  • 예시 이미지로 가볍고 보안·단순성·자원 효율성을 고려해 설계된 musl 및 BusyBox 기반 리눅스 배포판인 Alpine Linux 3.12.0을 사용함.
  • docker pull alpine:3.12.0으로 이미지를 내려받고 docker save로 alpine-3.12.0.tar 파일에 저장함.
  • 저장된 이미지 아카이브에는 컨테이너 속성을 설명하는 manifest.json, 이미지의 SHA-256 값이 담긴 JSON 파일, 이미지 레이어 디렉터리, repositories 등이 포함됨.
  • 레이어의 layer.tar를 살펴보면 bin, usr, var 등을 포함하는 완전한 리눅스 파일시스템 구조가 확인됨.
  • 이미지 레이어는 최소한 Dockerfile을 만드는 과정의 개별 단계를 나타내며, 여러 이미지의 내용은 겹쳐져 최종 파일시스템 뷰를 구성함.
  • 오버레이 파일시스템(overlay filesystem)의 세부 원리는 더 살펴볼 수 있으나, 여기서는 레이어가 이미지 구성 단계별 파일시스템이라는 점이 핵심임.

컨테이너란 무엇인가?

  • 리눅스 커널에는 ‘컨테이너’라는 개념 자체가 없으며, 여러 리눅스 도구를 함께 적용한 결과가 컨테이너처럼 보이는 환경임.
  • 컨테이너는 여러 개가 하나의 커널을 공유하더라도 별도의 머신이나 가상 머신처럼 보여야 함.
  • 최소한 네트워크 스택, 파일시스템, 프로세스가 각각 격리돼야 함.
  • 리눅스는 네임스페이스(Namespaces)를 통해 이런 격리를 제공하며, 간단한 셸 명령으로 격리 환경을 직접 구성할 수 있음.

가상 머신 설정

  • 이후 명령은 GCP에서 실행되는 Debian 머신에서 수행하며, gcloud compute instances create로 Debian 10 인스턴스 containers-demystified를 us-west1-a 영역에 만들고 SSH로 접속함.
  • 환경의 커널 버전은 4.19.0-9-cloud-amd64임.
  • Docker 설치를 위해 HTTPS 전송·인증서·curl·GnuPG·소프트웨어 저장소 관리 도구를 설치하고, Docker의 Debian 저장소와 GPG 키를 추가함.
  • 패키지 목록을 갱신한 뒤 docker-ce, docker-ce-cli, containerd.io를 설치하고, docker 그룹을 생성해 현재 사용자를 그룹에 추가함.

처음부터 컨테이너 만들기

  • Alpine 이미지 레이어의 파일을 container/ 디렉터리에 풀면 bin, dev, etc, home, root, tmp, usr, var 등이 있는 파일시스템이 만들어짐.
  • unshare --fork --mount bash로 마운트 네임스페이스를 만들고, container/를 자체 바인드 마운트한 뒤 pivot_root로 새 루트 파일시스템으로 전환함.
  • 기존 루트를 /_old에서 언마운트하면 프로세스가 격리된 파일시스템에서 벗어나지 못하도록 할 수 있음. pivot_root는 호출 프로세스의 루트 파일시스템을 지정한 새 루트로 바꾸고 기존 루트를 두 번째 경로에 둠.
  • 새 루트에서 /proc 파일시스템을 다시 마운트해 프로세스 목록을 확인하면, 마운트 격리만으로는 호스트의 프로세스가 여전히 보임.
  • unshare에 --pid를 추가하면 별도의 프로세스 네임스페이스가 생기며, 프로세스 목록에는 네임스페이스 안의 bash와 ps만 표시됨.
  • 네트워크 장치를 확인하면 루프백 장치뿐 아니라 docker0와 물리 네트워크에 연결된 eth0도 보여 네트워크 격리가 아직 되지 않은 상태임.
  • --uts와 --net을 추가해 UTS 및 네트워크 네임스페이스를 만들고, 호스트에서 컨테이너 프로세스의 PID를 이용해 veth 쌍을 생성함.
  • 호스트 쪽 veth 장치를 올리고 docker0 브리지에 연결함.
  • 새 네임스페이스에서 파일시스템 전환과 /proc 마운트를 수행한 뒤 호스트 이름을 container로 설정함.
  • 루프백과 컨테이너 쪽 veth 장치를 올리고 172.17.42.3/16 주소와 172.17.0.1 기본 게이트웨이를 설정함.
  • 이 설정에서 ping 8.8.8.8은 동작할 수 있지만, /etc/resolv.conf가 없어 DNS는 동작하지 않음.
  • 이 네트워크 구성은 Docker 브리지 설정을 활용하는 방식임. 클라우드 인스턴스에서 브리지를 직접 설정하는 일은 간단하지 않고, 물리 네트워크 장치를 일시적으로 옮기면 인스턴스의 라우팅이 끊길 수 있음.
  • Docker는 NAT를 구성하기 위해 다양한 iptables 규칙을 사용함.

다음 단계

  • 지금까지 만든 환경은 Docker와 같은 도구가 하는 작업에 상당히 가까움.
  • 빠진 핵심 요소는 오버레이 파일시스템에서 컨테이너를 시작해 새 pivot_root 안의 변경 사항이 기본 이미지에 영향을 주지 않도록 하는 구성임.

유용한 링크