컨테이너

홍준식·2024년 3월 24일

쿠버네티스

목록 보기
2/5
가상머신: 가상 머신은 실행 중인 어플리케이션과 운영 체제를 포함하여 컴퓨터와 거의 동일한 모든 기능을 수행할 수 있는 컴퓨터의 가상화된 인스턴스이다.
> 가상 머신은 하이퍼바이저라는 물리적 머신의 리소스를 추상화하여 할당하는 소프트웨어를 통하여 컴퓨팅 리소스를 할당받는다.
> 가상 머신은 가상 머신만의 커널을 만들고 커널을 통해 하이퍼바이저에서 제공하는 리소스에 접근한다.

컨테이너: 컨테이너는 호스트 운영체제에서 어플리케이션을 실행하기 위해 격리된 경량 인스턴스이다.
> 컨테이너는 기존 호스트 OS의 커널을 공유하며 유저 모드의 어플리케이션을 격리하여 사용자가 개별 어플리케이션을 사용하고 있는 것처럼 제공한다.
OS(VM) = kernel + filesystem/libraries  
Image(Container) = filesystem/libraries

컨테이너는 가상 머신과 달리 개별 커널을 생성하지 않고 그에 따른 오버헤드를 줄인다.

가상 머신의 경우 Mac OS 위에서 Linux OS를 사용하는 것이 가능하지만, 컨테이너의 경우 Mac과 Linux가 다른 커널을 사용하기 때문에 호환이 불가능할 수 있다.

Mac OS의 경우 도커 실행을 위해 Linuxkit을 통해 가상 머신을 생성하고 그 위에서 컨테이너를 실행하고 있고, Window의 경우 WSL이라는 리눅스 시스템 콜을 윈도우 시스템 콜로 해석하는 시스템을 사용하여 가상 머신 없이 컨테이너를 실행시킨다.

Debian, CentOS, Ubuntu 등의 리눅스 계열 OS에서는 모두 Linux 시스템 콜을 사용하는 동일한 커널을 기반으로 유저 모드만 변경되므로 Linux 계열에서는 서로 다른 OS를 문제 없이 구동할 수 있다.

컨테이너 표준

도커가 제안한 컨테이너가 인기를 끌고 도커를 활용한 쿠버네티스 또한 인기를 끌고 있을때 모놀리틱한 도커 엔진을 나누기 시작하였고 도커의 변경사항에 쿠버네티스가 크게 영향받지 않도록 컨테이너의 표준을 정하였다.
이렇게 정한 컨테이너의 표준을 OCI(Open Container Initiative)라고 한다.
도커는 containerd라는 OCI 표준을 따르는 컨테이너 런타임을 만들었고 Red Hat, Intel 등에서도 쿠버네티스 전용 CRI-O라는 컨테이너 런타임을 만들었다.

컨테이너의 역사

컨테이너는 chroot로부터 시작한다.
chroot는 프로세스 루트 디렉토리를 변경하는 리눅스 시스템 콜로 ssh 등으로 접속한 유저를 특정 디렉토리에만 접근 가능하도록 제어하는 용도로 설계되었다.
chroot는 Change Root Directory의 약어로 특정 프로세스의 루트 디렉토리를 지정하고 해당 루트 밖으로는 나가지 못하도록 가두는 명령어이다. chroot로 갇힌 프로세스는 해당 디렉토리 밖의 라이브러리 등을 사용하지 못하기에 필요한 커맨드 프로그램과 라이브러리 등을 경로에 함께 넣어야 한다.

하지만 모든 라이브러리와 프로그램 등을 직접 복사해 넣는 것은 복잡했고 이를 해결하기 위해 필요한 것을 미리 모아둔 image를 사용하게 된다. 이미지는 프로그램 실행에 필요한 것들을 모아둔 패키지이다.

하지만 컨테이너는 chroot가 탈옥이 가능하고 격리되지 않으며 루트 권한 및 리소스 무제한 문제로 인하여 chroot를 사용하지 않고 linux의 명령어인 pivot_root, namespace, cgroups로 컨테이너를 생성한다.

네임스페이스

리눅스 커널의 네임스페이스는 프로세스에 격리된 환경과 리소스를 제공한다.

* 네임스페이스 안에서의 변경은 내부의 프로세스에만 보이고 외부 프로세스에는 보이지 않는다.
* 모든 프로세스는 네임스페이스 타입 별로 특정 네임스페이스에 속한다.
* 자식 프로세스는 부모 프로세스의 네임스페이스를 상속받는다.

네임스페이스는 격리하는 자원에 따라 다음과 같이 다양하게 구분되어 있다.

마운트 네임스페이스
UTS 네임스페이스
IPC 네임스페이스
PID 네임스페이스
cgroup 네임스페이스
네트워크 네임스페이스
USER 네임스페이스

마운트 네임스페이스

마운트 네임스페이스는 프로세스와 그 자식 프로세스에게 다른 파일 시스템 마운트 포인트를 제공한다.
네임스페이스의 프로세스들에게 보여지는 마운트 포인트를 격리시키고 격리된 마운트 포인트들은 각 프로세스에게 단일 디렉토리 구조로 보이게 된다.

마운트의 종류는 다음과 같다.

private: 각 마운트 포인트가 다른 마운트 포인트에 반영되지 않는 방법
shared: 각 마운트 포인트가 다른 마운트 포인트에 반영되어 보여지는 방법
slave: 파일시스템 하위에서 새로운 마운트는 파일시스템에 반영되나, 반대는 반영되지 않는 방법
# 마운트할 디렉터리를 생성
$ mkdir /tmp/mount_ns

# 현재 bash 프로세스를 마운트 네임스페이스로 이동
# unshare은 네임스페이스 생성 명령어이고 -m이 마운트 네임스페이스를 생성하도록 지정
$ sudo unshare -m /bin/bash

# 현재 네임스페이스의 관련 inode 번호 확인
$ readlink /proc/$$/ns/mnt
mnt:[4026532226]

# tmpfs이라는 임시 파일 시스템 타입으로 마운트
$ mount -t tmpfs tmpfs /tmp/mount_ns

# 임시 파일 생성
$ touch /tmp/mount_ns/a
$ ls /tmp/mount_ns/
a

# 새로 생성한 마운트 지점 확인
$ mount | grep mount_ns
tmpfs on /tmp/mount_ns type tmpfs (rw,relatime,seclabel)

새로운 터미널에서 마운트 지점을 확인하면 기존 터미널이 마운트 네임스페이스로 격리되었기 때문에 새로운 터미널에서 보이지 않는 것을 확인할 수 있다.

# 새로운 터미널
# 다른 네임스페이스이기에 네임스페이스의 inode 번호가 다른 것을 확인할 수 있다.
$ readlink /proc/$$/ns/mnt
mnt:[4026531841]

# ls를 사용하더라도 디렉토리는 보이지만 격리된 a 파일은 보이지 않는다.
$ ls /tmp/mount_ns/

# 마운트 지점 확인 또한 되지 않는다.
$ mount | grep mount_ns

UTS 네임스페이스

UTS 네임스페이스는 호스트 명을 서버와 다르게 사용할 수 있도록 한다.
UTS는 Unix Time Sharing인 시분할 약자로 여러 사용자의 요청을 CPU 시간을 쪼개서 사용하며 여러 사용자의 환경 별로 호스트 명이나 도메인 명을 구분할 수 있도록 격리를 제공한다.

$ hostname
ubuntu1804

$ readlink /proc/$$/ns/uts
uts:[4026531838]

# 현재 bash 프로세스에서 uts 네임스페이스를 격리
$ sudo unshare --uts /bin/bash

# 호스트 명 변경
$ hostname MyHost

$ hostname
MyHost

$ readlink /proc/$$/ns/uts
uts:[4026532227]

새로운 터미널에서 호스트 명이 유지되는 것을 확인할 수 있다.

$ readlink /proc/$$/ns/uts
uts:[4026531838]

$ hostname
ubuntu1804

IPC 네임스페이스

IPC 네임스페이스는 Inter Process Communication을 지원하는 공유 메모리 및 메세지 큐 등의 리소스에 대한 격리를 제공한다.

$ sudo unshare --ipc /bin/bash

# 공유 메모리 생성
$ ipcmk -M 2000
Shared memory id: 0

$ ipcs -m
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x809712ea 0          root       644        2000       0

$ readlink /proc/$$/ns/ipc
ipc:[4026531839]

새로운 터미널에서 공유 메모리를 확인해본다.

# 공유 메모리 생성
$ ipcmk -M 1000
Shared memory id: 0

$ ipcs -m
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0xdf4a6453 0          root   644        1000       0

$ readlink /proc/$$/ns/ipc
ipc:[4026532228]

PID 네임스페이스

PID 네임스페이스는 pid 리소스를 격리한다.
여기서 pid는 프로세스 별로 부여되는 고유 번호로 프로세스 트리 최상위는 init 프로세스라는 pid 1을 갖는 프로세스가 존재한다.
PID 네임스페이스를 격리하면 격리된 컨테이너 안에도 pid 1로 시작하는 트리 구조를 가지게 된다.
PID 네임스페이스는 부모 네임 스페이스와 자식 네임스페이스가 중첩되는 구조로 자식 네임스페이스의 모든 프로세스들은 부모 네임스페이스의 pid와 해당 네임스페이스의 pid를 모두 가지게 된다.

$ echo $$
646748
$ sudo unshare --pid --fork --mount-proc /bin/bash
$ echo $$
1

// 현재 실행중인 프로세스가 컨테이너의 프로세스만 보인다.
$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.5 233096  5112 pts/1    S    14:24   0:00 /bin/bash
root          20  0.0  0.2 232520  2780 pts/1    R+   14:25   0:00 ps aux

cgroup 네임스페이스

cgroup은 프로세스에 할당할 시스템 리소스(CPU, Memory, Network)에 대한 제어를 제공한다.
cgroup은 파일시스템을 기반으로 파일시스템에 디렉토리를 만들고 파일을 수정하는 방식으로 시스템 리소스를 관리한다.
이 때무에 컨테이너에서 cgroup이 관리하는 파일시스템에 접근하게 되면 호스트의 시스템 리소스를 건드릴 수 있다.
cgroup 네임스페이스는 cgroup 파일시스템(/sys/fs/cgroup)을 격리한다.

네트워크 네임스페이스

네트워크 네임스페이스는 컨테이너의 네트워크를 가상화하여 네트워크 상 별도의 노드로 취급하도록 한다.

$ sudo unshare --net /bin/bash
$ lsns -t net -p $$
NS TYPE NPROCS    PID USER    NETNSID NSFS COMMAND
4026532226 net       2 655964 root unassigned      /bin/bash
$ lsns -t net -p 1
NS TYPE NPROCS PID USER    NETNSID NSFS COMMAND
4026531840 net     113   1 root unassigned      /usr/lib/systemd/systemd --switc

NS는 네트워크 네임스페이스의 inode값으로 현재 프로세스와 init 프로세스의 inode 값이 서로 다름을 확인할 수 있다.

USER 네임스페이스

USER 네임스페이스를 통해 컨테이너 안에서만 루트 유저로 동작할 수 있도록 한다.

컨테이너 계층화

컨테이너 이미지는 컨테이너 내부에서 프로그램을 실행하는데 필요한 모든 파일을 캡슐화하는 바이너리 패키지이다.

이미지는 일련의 파일 시스템 계층으로 구성되어있어 각 계층은 파일 시스템의 이전 계층으로부터 파일을 추가, 제거 또는 수정하는 오버레이 파일 시스템을 갖는다.

컨테이너 이미지는 일련의 파일 시스템 계층으로 구성되어 있어 이전 계층을 상속하고 수정한다.

도커의 모든 이미지 레이어는 var/lib/docker/overlay2에 저장되고 해시값을 통해 구분된다.
Dockerfile의 FROMRUNCOPY 명령은 새로운 레이어를 생성한다.

이미지 A: 레이어 A -> 레이어 B -> 레이어 C
이미지 B: 레이어 A -> 레이어 B -> 레이어 C -> 레이어 D
이미지 C: 레이어 A -> 레이어 B -> 레이어 E

위와 같이 이미지 3개가 존재한다고 할 때, 이미지 A를 삭제하더라도 레이어 A, B, C는 이미지 B에서 사용되고 있기 때문에 파일 시스템에서 지워지지 않고, 이미지 C를 추가로 생성할 때에 레이어 A와 B가 이미 다운로드 되어있기에 별도로 다운로드하거나 생성하지 않고 레이어 E만 만들게 된다.

# Dockerfile A
FROM ubuntu

RUN apt-get update && apt-get upgrade -y

RUN apt-get install -y openssh-server
# Dockerfile B
FROM ubuntu

RUN apt-get update && apt-get upgrade -y && apt-get install -y openssh-server

만약 위와 같이 2개의 이미지가 존재한다면, 해당 이미지를 docker inspect 명령어를 통하여 보았을 때 레이어를 파악할 수 있다.

$ docker inspect image_a
"RootFS": {
	"Type": "layers",
	"Layers": [
		"sha256:17ccff5b06b404f920af4557c0e2cb6ee5551b7136dc0cdb4f9aaf1bf71c643c",
		"sha256:ede334733a68a6091f2137efcdc0b86284885442db0c96cb72b0c47b27d74ed6",
		"sha256:47592da01660e3fb88cfbb307cbcd8faf61b879bf6fc2eaccae598d0a35ab53b"
		]
	},

$ docker inspect image_b
"RootFS": {
	"Type": "layers",
	"Layers": [
        "sha256:17ccff5b06b404f920af4557c0e2cb6ee5551b7136dc0cdb4f9aaf1bf71c643c",
        "sha256:d1bd4cf291b49e133ee9c9f4679f45fffe91e2cfca7a048466d67eebd7bef269"
		]
	},

위와 같이 기반 이미지의 레이어인 sha256:17ccff5b06b404f920af4557c0e2cb6ee5551b7136dc0cdb4f9aaf1bf71c643c을 공통으로 갖고 실행된 RUN 명령어마다 새로운 레이어가 추가되는 것을 확인할 수 있다.

0개의 댓글