원문: Demystifying Containers 101: A Deep Dive Into Container Technology for Beginners

Introduction

당신이 학생이던 어떤 회사에서 일하는 개발자 또는 소프트웨어에 관심이 많은 사람이던 컨테이너에 대해 한번쯤은 들어봤을 것입니다. 컨테이너가 가벼운 가상머신이라고 들었을지도 모릅니다. 하지만 컨테이너가 진정으로 의미하는 바는 무엇이며 어떻게 작동하고 왜 중요한 것일까요?

이 글은 컨테이너의 훌륭한 기술적 아이디어와 그 활용에 관한 이야기입니다. 컴퓨터 과학에 대한 기본적인 이해를 제외하고는 이 분야에 대한 사전 지식이 없다고 가정하겠습니다.

The Kernel and the OS

다른 여느 컴퓨터와 마찬가지로 당신의 노트북은 CPU, 보조(장기) 기억 장치(디스크 드라이브, SSD), 메모리, 네트워크 카드 등과 같은 하드웨어를 기반으로 만들어져 있습니다.

이러한 하드웨어와 상호작용하기 위해서, 커널(kernel)이라 불리는 운영체제 내의 작은 부분이 하드웨어와 커널을 제외한 나머지 시스템 간의 다리 역할을 수행하고 있습니다. 커널은 실행될 프로세스(프로그램)의 스케쥴링을 담당하며, 장치들을 관리하고(디스크나 메모리의 특정 주소를 읽거나 쓰고), 이외에 더 많은 것을 합니다.

운영체제의 나머지 부분은 부팅을 담 당하며, 유저 프로세스가 실행되는 유저 공간(user space)를 관리하고 끊임없이 커널과 상호작용합니다.

<커널은 운영체제의 한 부분으로 하드웨어와의 인터페이스입니다. 운영체제 전체는 “커널 공간(kernal space)”에 존재하는 반면, 유저 프로그램은 “유저 공간”에 존재합니다. 커널 공간은 유저 공간을 관리할 책임이 있습니다.>

The Virtual Machine

MacOS 컴퓨터와 Ubuntu에서 실행되도록 만들어진 응용프로그램이 있다구요? 흠… 널리 사용되는 해결 방법은 MacOS에서 Ubuntu 가상머신을 부팅시켜 거기서 프로그램을 실행시키는 것입니다.

가상머신은 게스트 운영체제를 실행하는 몇 단계의 하드웨어와 커널 가상화로 이루어져 있습니다. 하이퍼바이저(hypervisor)라고 불리는 소프트웨어 조각이 가상 디스크, 가상 네트워크 인터페이스, 가상 CPU 등을 포함하는 가상화된 하드웨어를 생성합니다. 가상머신에는 이 가상 하드웨어와 소통하는 게스트 커널 또한 포함되어 있습니다.

하이퍼바이저는 hosted 될 수 있는데, 이는 예시로 든 것 처럼 하이퍼바이저가 호스트 OS (MacOS) 위에서 동작하는 소프트웨어라는 것을 의미합니다. 하이퍼바이저는 또한 bare metal이 될 수 있는데, 머신 하드웨어 위에서 직접적으로 동작하는 (기존 OS를 대체하는) 것입니다 . 어떤 방식이든, 하이퍼바이저를 사용한 접근법은 다수의 파트에 대한 가상화를 필요로 하기 때문에 무겁다고 여겨집니다.

같은 머신에 다수의 독립적인 그룹을 구성할 필요성이 있을 때, 각각의 그룹에 대해 가상머신을 작동시키는 것은 좋은 접근법이라기엔 너무 무겁고 리소스를 낭비하게 됩니다.

가상머신은 머신 수준의 분리를 위해 하드웨어 가상화가 필요한 반면 컨테이너는 동일한 운영체제 내의 독립된 공간에서 실행됩니다. 독립된 공간의 수가 증가할 수로 오버헤드의 차이가 더 명확해집니다. 일반적인 노트북으로 수십개의 컨테이너를 실행시킬 수 있지만 단 하나의 가상머신도 버거울 수 있습니다.

cgroups

2006년, 구글의 엔지니어들은 Linux “control groups”라는 것을 발명하였고 이를 줄여 cgroups이라 합니다. cgroups는 유저 프로세스의 리소스 사용을 분리하여 관리하는 Linux 커널의 기능입니다.

이러한 프로세스들을 같은 리소스 제한을 공유하는 프로세스의 집합인 네임스페이스(namespace)에 집어 넣을 수 있습니다. 하나의 컴퓨터는 다수의 네임스페이스를 가질 수 있고, 각각은 커널에 의해 강제된 리소스 속성(제한)을 갖습니다.

네임스페이스 별 리소스 할당은 해당 프로세스 집합이 사용할 수 있는 전반적인 CPU, RAM 등의 리소스 양을 제한합니다. 예로, 백그라운드 log aggregation 어플리케이션이 우발적으로 서버를 망가뜨리지 않도록 리소스 제한을 둘 수 있습니다.

원래의 기능은 아니지만, Linux의 cgroups는 다시 만들어져 네임스페이스 분리(namespace isolation)이라는 기능을 포함하게 되었습니다. 네임스페이스 분리 자체는 새로운 아이디어가 아니고, Linux는 이미 많은 종류의 네임스페이스 분리를 가지고 있었습니다. 흔히 사용되는 예시가 프로세스 분리(process isolation)인데 각 개별적인 프로세스를 분리하고 공유 메모리와 같은 일들을 방지합니다.

cgroup 분리는 더 높은 수준의 분리인데, 이는 같은 cgroup 네임스페이스 내의 프로세스들이 다른 네임스페이스의 프로세스들로부터 독립적이도록 보장해주는 것입니다. 몇가지 중요한 네임스페이스 분리 기능들을 아래에 요약하였습니다.

  • PID(프로세스 ID) 네임스페이스: 이는 한 네임스페이스 내의 프로세스가 다른 네임스페이스의 프로세스의 존재를 알지 못하도록 해줍니다.
  • 네트워크 네임스페이스: 네트워크 인터페이스 컨트롤러, iptable, 라우팅 테이블과 더 low 한 level의 네트워킹 툴의 분리를 제공합니다.
  • 마운트 네임스페이스: 파일 시스템이 마운트되어 네임스페이의 파일 시스템 범위를 마운트된 디렉토리로 제한시킵니다.
  • 유저 네임스페이스: 네임스페이스 내 유저를 해당 네임스페이스만으로 제한시켜 네임스페이스에 걸쳐 유저 ID가 충돌하는 것을 방지합니다.

간단히 말해, 네임스페이스 내부의 프로세스에게 각 네임스페이스는 고유한 머신으로 보입니다.

Linux Containers

Linux cgroups는 LXC (linux container)라는 기술의 기반이 되었습니다. LXC는 오늘날 컨테이너라고 불리는 것의 가장 첫 번째 구현이었고, cgroups와 네임스페이스 분리를 사용하여 별개의 프로세스와 네트워킹 스페이스를 가진 가상 환경을 만들어냈습니다.

LXC는 독립적이고 격리된 유저 스페이스를 어느 정도 가능하게 했습니다. 컨테이너의 개념은 LXC에 뒤따라 나왔습니다. 사실, 초기의 Docker는 LXC을 직접적인 기반으로 하여 만들어졌습니다.

Docker

Docker는 가장 널리 사용되는 컨테이너 기술이고, 사람들이 컨테이너라고 하면 대부분 Docker를 가르키는 것입니다. 다른 오픈소스 컨테이너 기술들(CoreOS의 rkt 등)과 직접 컨테이너 엔진을 만드는 거대 기업들(Google의 lmctfy 등)도 있지만, Docker는 컨테이너화(containerization)의 산업 표준이 되었습니다. Docker는 여전히 cgroups와 Linux 커널, 최근 Windows에서 제공되는 네임스페이스 기술을 기반으로 하고 있습니다.

Docker 컨테이너는 여러 image(이미지, 하나의 패키지로 묶인 바이너리)들의 레이어로 이루어져 있습니다. base image는 컨테이너의 운영체제를 포함하고 있고, 이 OS는 호스트의 OS와 다를 수 있습니다.

컨테이너의 OS는 이미지의 형태로 되어 있습니다. 이는 호스트 OS와 다르게 완전한 운영체제가 아닙니다. 완전한 운영체제는 파일시스템, 바이너리, 커널을 포함하는 반면 이 이미지는 파일 시스템과 바이너리만을 포함합니다.

이 base image 위에 각각이 컨테이너의 부분이 되는 다수의 이미지들이 올라갑니다. 예를 들어, apt-get 의존성을 포함하는 이미지가 base image 위에 올라갈 수 있습니다. 그 위에는 어플리케이션 바이너리 등을 포함하는 이미지가 올라갈 수 있습니다.

멋진 사실은 만약 두 컨테이너가 각각 레이어 a, b, c와 레이어 a, b, d로 되어 있다면 각 레이어 a, b, c, d의 하나의 복사본만을 local과 repository에 보관하면 됩니다. 이것이 바로 Docker의 유니온 파일 시스템(union file system)입니다.

해시로 식별되는 각 이미지는 컨테이너를 구성하는 많은 가용 레이어 이미지들 중 하나입니다. 하지만 컨테이어너는 최상위 이미지로 식별되며, 부모 이미지에 대한 참조를 가집니다. 위 그림에서 두 최상위 이미지(image 1과 image 2)는 첫 3개의 레이어를 공유하고 있습니다. image 2는 구성과 관련된 2개의 레이어를 부가 적으로 가지지만, image 1과 같은 부모 이미지를 공유하고 있습니다.

컨테이너가 부팅되면, 해당 이미지와 부모 이미지들이 저장소로부터 다운로드되고, cgorup과 네임스페이스가 만들어지며, 이미지를 사용하여 가상 환경을 생성합니다. 컨테이너 내에서는, 이미지에 명시된 파일과 바이너리들은 전체 머신에서의 유일한 파일들로 나타내어 집니다. 이후 컨테이너의 메인 프로세스가 실행되고 컨테이너는 alive 상태가 됩니다.

Docker는 copy on write, volume(컨테이너 사이에서 공유되는 파일 시스템), docker daemon(머신의 컨테이너들을 관리), 버전 관리 저장소(컨테이너를 위한 Github) 등의 정말 멋진 기능들을 가지고 있습니다. 이 글에서 그 기능들을 알아보고 Docker를 사용하는 실용적인 예제들을 알아봅시다.

<command line 클라이언트(1) 은 도커 데몬(2)이라고 불리는 프로세스에 무엇을 해야할지 알려줍니다. 이 데몬은 registry 또는 repository로부터 이미지를 가져옵니다(3). 이러한 이미지들은 로컬 머신에 캐시되고(4) 데몬에 의해서 부팅되어 컨테이너를 실행합니다(5).>

Why Containers

프로세스 분리 이외에, 컨테이너는 다른 많은 이점이 있습니다.

컨테이너는 스스로 독립된 유닛으로서 해당 컨테이너를 지원하는 어느 곳에서든 실행될 수 있습니다. 그리고 각 인스턴스에서, 컨테이너는 완전히 동일합니다. 호스트 OS가 CentOS이던 Ubuntu이던 MacOS이건 심지어 Windows와 같은 UNIX 기반 OS인지 상관없습니다. 컨테이너 내에서 OS는 컨테이너가 지정한 OS가 됩니다. 그러므로 당신의 노트북에서 만든 컨테이너가 회사 서버에서 작동한다는 것을 확신할 수 있습니다.

또한 컨테이너는 표준화된 작업 또는 연산의 유닛의 역할을 합니다. 흔한 패러다임이 바로 각 컨테이너가 하나의 웹서버, 하나의 데이터베이스 샤드(shard) 또는 하나의 Spark worker 등을 실행하게 하는 것입니다. 그러면 어플리케이션의 규모를 키우기(scale) 위해서는, 단순히 컨테이너의 수를 늘리기만 하면 됩니다.

이러한 패러다임에서는, 각 컨테이너에게는 일정한 리소스 설정(CPU, RAM, 스레드의 수 등)이 주어지고, 어플리케이션의 규모를 증가시키기 위해서는 개별적인 컨테이너의 리소스 설정을 변경하는 대신 단지 컨테이너의 수를 증가시키면 됩니다. 이는 엔지니어가 어플리케이션의 규모를 증가시키거나 축소시켜야 할 때, 훨씬 쉬운 추상화를 제공해줍니다.

컨테이너들을 마이크로 서비스 아키텍처를 구현하기 위한 훌륭한 도구로 사용할 수 있습니다. 이때 각 마이크로 서비스는 같이 동작하는 컨테이너 조합될 수 있겠죠. 예를 들어 Redis 마이크로 서비스는 하나의 마스터 컨테이너와 다수의 슬레이브 컨테이너를 사용하여 구현할 수 있습니다.

이러한 (마이크로) 서비스 지향적 아키텍처는 엔지니어링 팀이 어플리케이션을 개발하고 배포하는 것을 쉽게 해주는 많은 중요한 성질들을 가지고 있습니다.

Orchestration

Linux 컨테이너가 출범한 이후에, Linux 컨테이너의 사용자들은 각 컨테이너에서 프로세스가 실행되는 여러 개의 가상 머신에 걸쳐 규모가 큰 어플리케이션을 배포하고자 하였습니다. 이를 위해서 수만개의 컨테이너를 수백개의 가상 머신에 걸쳐 효과적으로 배포할 수 있어야 했고, 이 사이의 네트워킹, 파일 시스템, 리소스 등을 효과적으로 관리할 수 있어야 했습니다. 오늘날의 Docker는 컨테이너 네트워킹, volume을 위한 파일 시스템, 리소스 설정 등을 정의하는 추상화를 통해서 이를 더욱 쉽게 할 수 있도록 하였습니다.

하지만 다음과 같은 것들을 위해 Docker 이외의 툴이 여전히 필요합니다.

  • 실제로 명세(specification)을 받아들이고 컨테이너를 머신에 할당 (스케쥴링)
  • 실제로 docker를 통해 지정된 컨테이너를 해당 머신에서 실행
  • 업그레이드, 롤백, 끊임없이 변경되는 시스템의 성질에 대한 처리
  • 컨테이너 크래시와 같은 고장에 대한 대응
  • 서비스 디스커버리, VM 간 네트워킹, 클러스터에 대한 추가(ingress), 제거(egress) 등의 클러스터 리소스 생성

이들은 (일시적일 수도 있고 또는 끊임없이 변화할 수도 있는) 컨테이너들의 집합 위에 만들어진 분산 시스템의 오케스트레이션(orchestration)과 관련이 있는 문제들이고, 사람들은 이 문제들을 해결하기 위해서 정말 대단한 시스템을 구축하였습니다.

제 다음 글에서 현재 가장 널리 사용되는 오픈소스 오케스트레이션 툴인 쿠버네츠(Kubernetes)의 구현에 대한 자세한 이야기와 똑같이 중요하지만 덜 알려진 메소스(Mesos)와 보그(Borg)에 대한 이야기를 다루도록 하겠습니다.

이전 글: How Microservices Saved the Internet 다음 글: Orchestration (TBD)