PostCover

Docker 속으로 1편 - 컨테이너 격리기술

컨테이너 격리기술에 대해 알아보자.

docker run -it --rm --name=mycontainer ubuntu:14.04 bash

보통 도커 이미지를 기반으로 컨테이너를 실행할 때는 다음과 같은 명령어를 사용하여 실행하게 된다. 그렇다면 위의 커맨드 라인이 의미하는 바는 무엇일까?

도커 컨테이너의 라이프 사이클을 바라보면 아래와 같다.

Docker Container는 docker create명령을 통해 Docker Image의 snapshot으로 /var/lib/docker영역에 생성된다. docker start명령은 읽고 쓰기가 가능한 Process 영역, 즉, Container Layer를 생성하여 동적 컨테이너를 구성하게 된다. 또한, docker stop은 생성된 Container Layer를 삭제한다.

이때, docker run이라는 명령은 이미지의 snapshot인 컨테이너를 생성하고 시작하는 과정을 압축한 명령이라는 것을 알 수 있다.


그렇다면 Container는 정확히 무엇을 의미할까?

Container는 격리된 환경에서 실행하는 Process이다. Container를 사용하면

  • 애플리케이션 사용에 필요한 코드, 설정, 의존성 라이브러리, 실행 파일을 하나로 묶어서 관리할 수 있다.
  • 격리되어 Process 주변 영향이 차단된다.
  • 자원을 개별 할당하고 사용을 보장할 수 있다.

그리고 이와 같이 격리하기 위해 리눅스의 격리 기술을 사용한다. 가상화된 공간을 생성하기 위해 리눅스의 기능 pivot-root, namespace, cgroup를 사용함으로써 프로세스 단위의 격리환경과 리소스를 제공한다.

따라서 Container는 Linux OS 사용을 전제로 한다고 할 수 있다.

Process는 다음과 같이 정의할 수 있다.

  • 실행 중인 프로그램의 인스턴스
  • OS에서 프로세스를 관리하며, 각 프로세스는 고유한 ID (PID)를 가진다.
  • 프로세스는 CPU와 메모리를 사용하는 기본 단위로, OS 커널(Cgroup)에서 각 프로세스의 자원을 관리한다.

Container 격리에 사용되는 기술 - 파일시스템의 격리

chroot

여러 사람이 이용하는 서버에서 격리된 환경을 만들기 위해서 처음 나온 아이디어는 chroot이다. chroot는 change root의 줄임말고, 파일을 경로에 모으고, 경로에 가둬서 실행할 수 있다.

즉, 프로세스의 루트 디렉토리를 변경, 격리하여 가상의 루트 디렉토리를 생성하는 것을 의미한다.

그렇다면 chroot만으로 컨테이너를 격리하는데 충분할까? chroot는 package와 격리관점에서는 필요한 부분을 갖고 있지만, 탈옥이 가능하다는 단점이 존재하여 실제로 사용할 수는 없다.

pivot_root

위의 단점을 해결하기 위해 루트 파일 시스템을 pivot한다는 대안이 나왔다. 즉, 루트 파일시스템 자체를 바꿔, 컨테이너가 전용 루트 파일시스템을 가지도록 하였다. 그러나 루트 파일 시스템을 pivot하는 경우 호스트에 영향이 간다는 단점이 발생하였다.

namespace

위의 단점으로 인해 네임스페이스라는 개념이 나왔다. 프로세스의 환경을 격리하기 위해서 마운트 환경만 격리한다는 아이디어가 나오게 되었다.

간단하게 살펴보기 위해 마운트 네임스페이스를 기준으로 정리한다. 마운트 네임스페이스는 파일 시스템의 마운트 정보를 격리한다. 즉, 부모 프로세스의 마운트 정보와 마운트 포인트를 복사하여, 자식 프로세스에서 별도의 마운트 네임스페이스를 생성한다. 이를 통해 각 프로세스가 독립적인 파일 시스템 뷰를 가지게 된다.

  1. 부포 프로세스의 마운트 정보 복사 자식 프로세스는 부모 프로세스의 마운트 정보를 복사하여 자신의 마운트 네임스페이스를 초기화한다.
  2. 마운트 디렉토리 생성 자식 프로세스의 파일 시스템 뷰를 별도로 관리하기 위해 특정 디렉토리(/mnt)를 마운트 포인트로 설정한다.
  3. 격리된 네임스페이스 사용 unshare 시스템 호출을 사용하여 네임스페이스를 호스트와 분리한다. 이 단계에서는 호스트 시스템에서 자식 네임스페이스의 마운트 정보가 더 이상 보이지 않게 된다.

이후 pivot_root를 통해 격리된 마운트 네임스페이스에서 루트 파일 시스템을 변경할 수 있다. pivot root는 현재 프로세스의 루트 디렉토리를 새로운 위치로 전환하며, 프로세스의 파일 시스템 뷰를 완전히 독립적으로 만든다. 이 과정을 통해 프로세스는 독립적인 루트 파일 시스템을 가지게 되며, 호스트 시스템의 영향을 받지 않는다.

컨테이너에 전용 루트 파일 시스템을 부여한다는 것은 어떤 의미가 존재할까?

  • 어떤 서버에 띄우더라도 자체적인 루트 파일 시스템이 보장되므로 서버 환경(시스템 파일, 의존성 라이브러리 충돌, 경로 설정)으로 부터 자유롭다.
  • 서버의 파일시스템으로 부터 완전하게 분리됨으로써 보안상 안전하다.
  • 컨테이너 내에서 자유롭게 마운트 변경이 가능하고, 파일시스템을 확장할 수 있다.

Container 격리에 사용되는 기술들 2. 매번 이렇게 똑같이 격리하면 필요없는 중복 파일이 많아지니 이 문제를 해결하자, 오버레이 파일 시스템

컨테이너가 사용할 파일을 모으고, 격리된 환경에서 pivot_root하여 컨테이너의 루트 파일 시스템이 준비된다는 것을 알았다. 그렇다면, 컨테이너가 사용할 환경, 파일들은 어떻게 준비될까?

애플리케이션에 필요한 파일들을 모두 모아서 패키징 되는데, 이를 "이미지"라고 부른다. 이러한 이미지에는 애플리케이션뿐만 아니라 실행에 필요한 시스템 바이너리, 의존성 라이브러리, 설정 등이 포함된다. 이미지는 서비스에 대한 요구사항에 따라서 다양하게 패키징될 수 있다. 이렇다 보니 서로 다른 이미지라도 시스템 바이너리, 웹서버, DB, 그 밖의 관련 라이브러리 등 자주 사용되는 파일들이 중복될 가능성이 높다.

예를 들면, 개발자 A가 Ubuntu 환경에서 웹서버를 컨테이너로 서비스한다고 생각해보자. 이때, 이미지에는 Ubuntu 바이너리들과 Nginx 바이너리를 포함하게 된다.

개발자 B도 Ubuntu와 nginx로 웹서비스 환경을 갖추려고 하는데 여기에 DB를 추가하고 싶다. 이 경우, Ubuntu와 nginx는 동일한 구성이더라도, 개발자 B는 Ubuntu와 nginx에 DB를 추가하여 이미지를 만들게 된다. Ubuntu와 nginx가 개발자 A와 B의 이미지 양쪽에 모두 포함이 된다.

이와같은 패키징은 중복 문제가 존재한다. 중복 문제는 저장공간, 네트워크 비용, 배포 속도, 보안 측면에서 비효율과 비용 문제를 야기한다.

이러한 중복 문제를 해결하기 위해 오버레이 파일 시스템 방식을 사용하게 되었다.

실제로 컨테이너에서 사용할 이미지는 중복을 해결하기 위해 여러 레이어들의 조합으로 제공된다. 이렇게 조합된 레이어들을 통합된 뷰로 제공하는 오버레이 파일 시스템이 있다.

오버레이 파일 시스템의 기원은 UFS (Union File System)으로 일명 '상속 파일 시스템'이라고도 한다. "상속"에서 유추해 볼 수 있듯이, 오버레이 파일 시스템은 파일 시스템 여러 개를 쌓고, 밑에서부터 레이어들을 층층이 쌓아올린다. 최상위층에서 보면 레이어들이 하나로 합쳐져 보인다. 이렇게 여러 파일 시스템을 하나로 합쳐서 하나의 파일 시스템으로 마운트하는 기능을 "Union 마운트"라고 한다. 그리고 Union 마운트를 수행하는 시스템을 UFS(Union File System)이라고 한다.

위처럼 UFS는 다음과 같은 특징을 가지고 있다.

  1. UFS는 Union 마운트, 즉, 여러 개의 파일 시스템을 하나로 합쳐서 마운트한다.
  2. UFS에서는 레이어가 쌓이는 순서가 중요하다. 중복되는 파일명을 가지는 파일이 존재할 때, 상위 레이어의 파일이 오버레이 된다.
  3. UFS는 CoW(Copy-on-Write)이다. Lower-Layer(읽기 전용)에 대한 쓰기 발생 시에 복사본을 생성하여 수행된다(원본 유지).

예를 들어 CD-ROM을 살펴보면, CD-ROM은 한 번 구워지면 Read-Only이기 때문에 내용을 변경할 수 없다. 이 경우 CD-ROM의 내용을 Lower Layer로 하고, writable한 디스크 볼륨을 Upper Layer로 하여 Union 마운트를 한다. 이렇게 하면 파일 변경이 필요할 때 Upper Layer에 write할 수 있다. 즉, Lower Layer는 읽기 전용으로 쌓고, 상위에 Upper Layer에서 변경이나 새로 쓰기가 발생하는 경우를 처리한다.


Container 격리에 사용되는 기술들 3. Namespace

네임스페이스의 특징은 다음과 같다.

  1. 모든 프로세스는 타입별로 네임스페이스에 속한다.
  2. 자식 프로세스는 부모의 네임스페이스를 상속한다.

Mount Napespace

격리 대상은 파일 시스템의 마운트 포인트이다. 이는 프로세스가 사용할 수 있는 파일 시스템의 마운트 포인트를 격리한다. 각 컨테이너는 독립적인 파일 시스템 뷰를 가지며, 다른 컨테이너나 호스트 파일 시스템의 마운트 상태를 볼 수 없다. 컨테이너는 기본적으로 호스트 파일 시스템의 읽기 전용 사본위에 Writable Layer를 추가하여 작업한다. 파일 시스템 격리를 통해 컨테이너 내부에서의 파일 생성, 수정, 삭제는 다른 컨테이너나 호스트에 영향을 마치지 않는다.

UTS Namespace

UTS Namespace는 컨테이너마다 고유한 호스트 이름과 도메인 이름을 설정할 수 있도록 한다. 컨테이너는 자신의 환경 내에서만 호스트 이름을 변경할 수 있으며, 호스트 시스템이나 다른 컨테이너의 이름에 영향을 주지 않는다. 네트워크 환경 격리와 함께 사용되어, 네트워크 설정의 독립성을 보장한다.

PID Namespace

PID Namespace는 각 컨테이너가 자신만의 프로세스 ID 공간을 가지도록 한다. 컨테이너는 자신의 프로세스만 볼 수 있고, 다른 컨테이너나 호스트의 프로세스는 접근할 수 없다. 즉, 호스트에서 실행중인 프로세스는 컨테이너 내부에서 볼 수 없다. 또한, 컨테이너 내부에서는 PID가 1인 init 프로세스가 최상위로 동작하며, 나머지 프로세스는 해당 프로세스 아래에 생성된다.

이때, 컨테이너에서 PID가 1인 프로세스가 kill 될 경우 컨테이너 또한 종료된다.

Network Namespace

Network Namespace는 컨테이너가 자체적인 네트워크 스택을 가지도록 한다. 각 컨테이너는 독립된 IP 주소, 라우팅 테이블, 네트워크 인터페이스 등을 설정할 수 있다. 컨테이너 간 통신은 가상 네트워크 브리지나 다른 네트워크 기술을 통해 수행된다. 따라서, 컨테이너 내부의 네트워크 구성은 호스트나 다른 컨테이너에 영향을 미치지 않는다.

IPC Namepsace

IPC는 Inter-Process Communication을 의미한다. 따라서 IPC Namespace는 컨테이너가 사용하는 프로세스 간 통신 리소스를 격리한다. 컨테이너는 자신의 IPC 리소스만 사용할 수 있으며, 다른 컨테이너나 호스트의 IPC 리소스에는 접근할 수 없다. 이 격리는 컨테이너 간 데이터 누출을 방지하고 보안을 강화한다.


Container 격리에 사용되는 기술들 4. Cgroups

Cgroups는 Control Groups를 의미하며, 컨테이너의 자원 관리를 위한 핵심 기술로, 시스템 자원을 그룹 단위로 분배 및 제한할 수 있도록 설계되었다. 이를 통해 컨테이너가 시스템의 자원을 효율적으로 활용하면서도, 다른 컨테이너나 호스트 시스템에 영향을 주지 않도록 한다.

Cgroups는 다음과 같은 역할을 수행한다

  • 자원 제한: 각 컨테이너에 대해 CPU, 메모리, 디스크 I/O, 네트워크 대역폭 등의 자원을 할당하고 상한선을 설정한다.
  • 자원 격리: 특정 컨테이너가 과도한 자원을 사용하지 못하도록 하여, 다른 컨테이너의 성능 저하를 방지한다.
  • 자원 모니터링: 컨테이너가 소비하는 자원 사용량을 추적하여 관리를 용이하게 한다.
  • 자원 우선순위: 컨테이너 간 자원 사용의 우선 순위를 설정하여, 중요한 컨테이너가 필요한 자원을 더 많이 활용할 수 있돌고 한다.

Cgroups는 시스템 자원을 제어하는 파일 시스템 기반의 인터페이스를 제공한다. 이를 통해 관리자는 자원 사용 정책을 설정하고, 프로세스를 특정 그룹에 포함시킬 수 있다.

  1. Cgroups 계층 구조 생성 자원 제한 규칙을 정의하고, 이를 파일 시스템의 가상 디렉토리에 매핑한다
  2. 프로세스 그룹화 특정 컨테이너나 프로세스를 정의된 Cgruops에 할당한다. ex) /sys/fs/cgruop/memory/mycontainer와 같은 디렉토리에 PID를 할당한다.
  3. 자원 제한 적용 Cgroups에 설정된 규칙에 따라 프로세스의 자원 사용을 제한한다.

다시 가장 위에 있던 명령어를 살펴보자

docker run -it --rm --name=mycontainer ubuntu:14.04 bash

ls를 통해 루트 디렉토리를 확인하면서 chroot & pivot_root를 사용하게 되었다.

df -h를 사용하며 pivot_rootmount namespace를 사용한다.

여기에서는 UTS namespace

여기에서는 PID namespaceIPC namespace

여기에서는 Network namespace를 사용하게 된다.