도커 컨테이너는 가상머신인가요? 프로세스인가요?

들어가며

도커 컨테이너와 가상머신은 어떻게 다른 건가요?

이 글에서는 도커Docker 컨테이너와 프로세스가 어떻게 다른지 알아보려고 합니다. 도커 컨테이너와 가상머신의 차이에 대해 가상머신은 운영체제 위에 하드웨어를 에뮬레이션하고 그 위에 운영체제를 올리고 프로세스를 실행하는 반면에, 도커 컨테이너는 하드웨어 에뮬레이션 없이 리눅스 커널을 공유해서 바로 프로세스를 실행한다고 설명하면, 군더더기 없는 아주 훌륭한 설명입니다 👏

그럼 다음 질문들은 어떤가요. 도커 컨테이너가 진짜 프로세스에요? 그럼 호스트 시스템에서 ps치면 보이나요? 호스트에서 kill해서 죽일 수 있어요? 프로세스 ID도 있어요? 자, 이론적인 설명보다는 직접 리눅스에 들어가서 리눅스 프로세스와 도커 컨테이너 사이의 표면적인 차이에 집중해서 한 번 살펴보겠습니다.

도커 컨테이너가 사용하는 커널과 파일 시스템 확인하기

도커 컨테이너를 이야기하기에 앞서서 도커의 시작을 잠깐 돌아보겠습니다. 도커는 PYCON 2013 US에서 솔로몬 하이크Solomon Hykes의 라이트닝 토크로 처음 공개되었습니다.

리눅스 컨테이너의 미래The Future of Linux Container라는 제목을 가진 이 발표에서 솔로몬 하이크는 docker라는 어려운 명령어로 Hello world를 출력하는 아주 기묘한 데모를 시연합니다.

솔로몬 하이크의 도커(Docker) 시연 장면

지금도 따라해볼 수 있습니다. -a 옵션만 빼고 실행하면 (적어도 겉보기에는) 똑같이 동작합니다.

$ docker run busybox echo hello world
hello world

아직 컨테이너를 모르신다면 “echo hello world라고 하면 되잖아”하고 호통을 칠지도 모릅니다. 맞습니다.

$ echo hello world
hello world

(겉보기에는) 정말 아무런 차이도 없습니다. 오히려 도커로 실행하면 처음에 busybox 이미지를 풀 받는 시간이 걸리기 때문에 느리게 실행됩니다. busybox가 무엇인지 궁금해할 수도 있습니다. 이것은 도커 컨테이너가 실행되는 환경(=파일시스템)입니다. 도커로 실행되는 컨테이너는 위에서 명시한 busybox라는 환경에서 실행됩니다. 반면에 아래와 같이 리눅스 시스템에서 바로 echo hello world 명령어를 실행하는 경우 호스트 환경(루트 파일 시스템)에서 프로세스가 실행됩니다. 간단한 예를 통해서 도커 컨테이너들이 서로 다른 환경에서 실행되고 있다는 것을 확인해보겠습니다.

먼저 ubuntu:latest 이미지에서 커널과 배포판 정보를 확인해보겠습니다.

$ docker version
Client: Docker Engine - Community
 Version:           19.03.5
  ...

$ docker run -it ubuntu:latest bash
root@bfccfb4136ae:/# uname -a
Linux f44e33a332f2 4.15.0-70-generic #79-Ubuntu SMP Tue Nov 12 10:36:11 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

root@bfccfb4136ae:/# cat /etc/*-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
...

이번에는 centos:latest 이미지에서 확인해봅니다.

$ docker run -it centos:latest bash
[root@bb0a9b851dbd /]# uname -a
Linux bb0a9b851dbd 4.15.0-70-generic #79-Ubuntu SMP Tue Nov 12 10:36:11 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

[root@bb0a9b851dbd /]# cat /etc/*-release
CentOS Linux release 8.0.1905 (Core)
NAME="CentOS Linux"
VERSION="8 (Core)"

컨테이너 안에서 2개의 명령어를 실행해보았는데, 두 결과 모두 흥미롭습니다. 먼저 cat /etc/*-release 파일을 출력한 결과를 통해 배포판(파일시스템)이 다르다는 것을 확인할 수 있습니다. 위에서 실행한 2개의 도커 컨테이너는 분명히 다른 환경에서 실행되고 있습니다.

그리고 uname -a의 출력 결과가 동일하다는 것도 재미있습니다. 서로 다른 환경에서 컨테이너가 실행되었지만 커널이 같다는 것에 주목해주세요. 위의 예제는 우분투Ubuntu 18.04 베이그런트Vagrant 가상머신에서 실행되었습니다. 호스트의 커널을 확인해보겠습니다.

$ uname -a
Linux ubuntu1804.localdomain 4.15.0-70-generic #79-Ubuntu SMP Tue Nov 12 10:36:11 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

오호. 호스트 이름만 다를 뿐 거의 똑같네요? 그렇다면 이번에는 맥OS에 설치된 도커 데스크탑에서도 똑같이 실행해보겠습니다.

$ uname -a
Darwin nacyotui-MacBookAir.local 18.6.0 Darwin Kernel Version 18.6.0: Sun Apr 28 18:06:45 PDT 2019; root:xnu-4903.261.4~6/RELEASE_X86_64 x86_64

$ docker run -it ubuntu:latest bash
root@11f071759deb:/# uname -a
Linux 11f071759deb 4.9.184-linuxkit #1 SMP Tue Jul 2 22:58:16 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
root@11f071759deb:/# cat /etc/*-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic

먼저 호스트 맥OS에서 uname -a로 커널을 확인해보면 Darwin kernel이 출력되는 것을 알 수 있습니다. 그런데 docker를 실행하면 이번에는 4.9.184-linuxkit 커널이 나타납니다. 알쏭달쏭하죠? 이미 알고 있는 분들도 계시겠지만 맥OS는 리눅스가 아니므로 네이티브하게 도커를 사용할 수 없습니다. 따라서 도커 데스크탑은 리눅스킷 기반 경량 가상머신 위에서 도커를 실행합니다. 어쨌건 재미있는 사실을 확인했습니다. 우분투 18.04에서 실행한 컨테이너가 사용하는 커널과 맥OS의 도커 데스크탑에서 실행한 컨테이너에서 사용하는 커널은 다릅니다.

여기까지 확인한 내용을 정리해보겠습니다.

이미지에 대한 더 자세한 내용은 만들면서 이해하는 도커(Docker) 이미지의 구조 편을 참고해주세요. 컨테이너가 서로 다른 파일 시스템을 가질 수 있는 이유는 이미지(파일의 집합)를 루트 파일 시스템으로 강제로 인식시켜 프로세스를 실행하기 때문이빈다. 프로세스는 자신의 루트 디렉터리가 /가 아닌 그 아래의 특정 디렉터리인 것처럼 동작합니다. 이에 대한 더 자세한 원리는 컨테이너 기초 - chroot를 사용한 프로세스의 루트 디렉터리 격리컨테이너 기초 - 정적 링크 프로그램을 chroot와 도커(Docker) scratch 이미지로 실행하기를 참고해주세요.*

* 실제로 도커에서 chroot를 사용해 루트 디렉터리를 격리하지는 않습니다. 하지만 원리를 이해하는 데는 chroot면 충분합니다.

도커 컨테이너의 프로세스 아이디는 1번

이미 도커 컨테이너는 가상머신이 아니고, 프로세스다라는 얘기를 들어보셨을 수도 있습니다. 그렇다면 이건 어떤가요, 도커 컨테이너의 PID를 생각해보신 적이 있으신가요? 이번에는 우분투 이미지에서 실행한 bash 프로세스의 PID를 확인해보겠습니다. 셸의 PID를 확인하려면 $$를 실행해보면 됩니다.

$ echo $$
5673

$ docker run -it ubuntu:latest bash
root@9a09675d42ed:/# echo $$
1

흥미로운 결과입니다. 먼저 호스트 상에서 실행중인 셸의 PID는 5673입니다. 그런데 컨테이너로 실행한 bash 셸의 PID는 1번입니다. 일반적으로 리눅스에서 1번 프로세스는 init 프로세스로 특별한 의미를 가지고 있습니다. 실제로 호스트 상에서 pstree를 실행해보면 모든 프로세스가 1번 프로세스(systemd)에 물려있는 것을 확인할 수 있습니다.

$ pstree
systemd(1)─┬─VBoxService(996)─┬─{VBoxService}(999)
           │                  ├─{VBoxService}(1002)
           │                  ├─{VBoxService}(1004)
           │                  ├─{VBoxService}(1005)
           │                  ├─{VBoxService}(1006)
           │                  ├─{VBoxService}(1007)
           │                  └─{VBoxService}(1008)
           ├─accounts-daemon(808)─┬─{accounts-daemon}(861)
           │                      └─{accounts-daemon}(882)
           ├─agetty(985)
           ├─atd(803)
           ├─containerd(3866)─┬─{containerd}(3872)
           ...

그런데 도커 컨테이너 안에서 pstree를 실행해보면 정말로 1번 프로세스 bash인 것을 확인할 수 있습니다.

root@2d1239925257:/# apt-get update
root@2d1239925257:/# apt-get install psmisc
root@2d1239925257:/# pstree -p
bash(1)---pstree(256)

컨테이너는 프로세스지만, 프로세스라고 부르기보다는 컨테이너라고 부르는 데는 이유가 있는 법입니다. 컨테이너는 (주로) 리눅스 커널에 포함된 프로세스 격리 기술들을 사용해서 생성된 특별한 프로세스입니다. 처음 도커를 보면 가상머신이라고 느끼는 건 바로 이런 이유 때문입니다. 예를 들어 앞선 예제에서 bash 명령어로 마치 컨테이너라는 가상머신에 접속해서 명령어를 실행하는 것처럼 느껴집니다만, 사실 bash는 가상머신에 접속하는 명령어가 아니라 그냥 호스트 상에서는 프로세스일 뿐입니다. 그런데 PID가 1인 특별한 bash 프로세스입니다 (?!). 어떻게 하나의 하늘 아래 두 개의 태양(1번 프로세스)이 존재할 수 있을까요.

root@47672c55680d:/# kill -9 1
root@47672c55680d:/#

일반적인 프로세스와 달리 1번 프로세스는 내부적으로 SIGKILL 시그널로 죽일 수도 없습니다.

PID 1번의 비밀, 도커 없이 일반 프로세스의 PID를 1번으로 실행하기

어떻게 이게 가능한 걸까요? 이를 구현하는 데 사용된 기능이 바로 리눅스 네임스페이스입니다. 정확히는 PID 네임스페이스가 분리되어있기 때문에 도커 컨테이너의 프로세스는 1번이 됩니다. 도커 없이도 unshare 명령어로 프로세스의 PID 네임스페이스를 분리해볼 수 있습니다.*

* 여기서부터는 루트 계정으로 로그인하거나, 루트 권한을 얻어서 따라해보시는 것을 추천합니다.

$ echo $$
5673

$ mkdir busybox-image
$ docker run --name busybox-image busybox:latest
$ docker export busybox-image > ./busybox-image/image.tar
$ cd busybox-image; tar xf image.tar; cd ..
$ sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
/ # echo $$
1

생각만큼 간단하지는 않네요. 최대한 간단히 설명해보겠습니다. unshare는 특정 프로세스를 실행할 때 특정 네임스페이스를 분리해주는 기능입니다. 여기서 특정 네임스페이스라 함은, 네임스페이스에도 UTS, PID, 네크워크, 마운트 등 다양한 종류가 있기 때문입니다. 그 중에서 여기서 분리할 네임스페이스는 PID 네임스페이스로, -p 옵션을 지정해주면 됩니다. 그런데 이것만으로는 잘 되지 않습니다.

$ sudo unshare -p /bin/sh
# echo $$
2170

분명 PID 네임스페이스는 분리가 되었을 텐데, PID가 1번이 아닙니다. 왜냐면 unshare 명령어를 사용하더라도 호스트의 파일 시스템을 그대로 사용하고 있고 PID 관련 정보는 /proc 아래에 있는 정보를 사용하기 때문입니다. 여기도 컨테이너가 프로세스라는 아주 중요한 힌트가 숨어있습니다만, 그 얘기는 뒤에서 다루겠습니다. 어쨌건 PID 네임스페이스가 분리되지 않은 것처럼 보입니다. unshare 명령어는 --mount-proc으로 /proc을 분리하는 기능을 지원합니다. 하지만, 호스트의 /proc에 마운트를 시도하므로 당연히 동작하지 않습니다. 😢

$ sudo unshare -p --mount-proc /bin/sh
unshare: mount /proc failed: Device or resource busy

PID 네임스페이스를 제대로 분리해보기 위해서는 독자적인 환경이 필요합니다. chroot 글에서 자세히 다루고 있습니다만, 루트 디렉터리를 바꿔서 프로세스를 실행한다는 게 생각보다 간단한 일은 아닙니다. 독자적인 환경에는 프로세스를 실행하기 위한 모든 파일들이 준비되어있어야합니다. 직접 준비하는 것도 가능하지만, 도커 이미지를 복사해오는 게 가장 간단합니다. 그래서 여기서는 docker를 사용해 busybox 이미지 내용을 그대로 복사하고, chroot를 사용해 실행 환경(파일시스템)을 격리해 셸을 실행했습니다.

$ mkdir busybox-image
$ docker run --name busybox-image busybox:latest
$ docker export busybox-image > ./busybox-image/image.tar
$ cd busybox-image; tar xf image.tar; cd ..
$ sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
/ # echo $$
1

busybox 이미지의 내용을 복사한 디렉터리를 기반으로 chroot를 실행해서 프로세스 실행 경로를 분리하고 unshare--mount-proc 옵션으로 /proc을 마운트 해줍니다. 이렇게 실행하면, 짠 프로세스 아이디 1번이 되었습니다.

2개의 PID: 프로세스가 바라보는 프로세스 ID, 호스트가 바라보는 프로세스 ID

여기서는 PID 네임스페이스에 대해서 좀 더 알아보겠습니다. 앞서 살짝 힌트가 나왔습니다만, 단순히 PID를 분리해서 셸을 실행하니 PID가 1이 아니라 2170이라는 일반 프로세스의 값이 되었습니다.

$ sudo unshare -p /bin/sh
# echo $$
2170

결론부터 이야기하자면, 1과 2170은 둘 다 맞습니다. 여기서는 잘 확인이 안 됩니다만, 이 프로세스의 아이디는 1이기도 하고, 2170이기도 합니다. 그럼 다시 한 번 unshare로 PID 네임스페이스를 분리해보겠습니다.

$ sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
/ # echo $$
1

PID가 1번입니다. 이 프로세스를 그대로 놔두고, 이제 호스트의 셸을 하나 더 띄웁니다. 거기서 ps로 최근에 실행된 프로세스들을 확인해봅니다.

$ ps aux --sort +start_time | tail -n 5
root      2376  0.0  0.1  21472  3700 pts/0    S    03:49   0:00 bash
root      2386  0.0  0.0   7456   732 pts/0    S    03:49   0:00 unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
root      2387  0.0  0.0   1312     4 pts/0    S+   03:49   0:00 /bin/sh
vagrant   2396  0.0  0.1  37516  3480 pts/1    R+   03:50   0:00 ps aux --sort +start_time
vagrant   2397  0.0  0.0   7508   856 pts/1    S+   03:50   0:00 tail -n 5

방금 실행한 unshare(2386) 명령어가 보입니다. 그리고 그 바로 아래에 /bin/sh(2387)이 보입니다. 뭔가 감이 오지 않나요? 이 프로세스를 강제로 kill해보겠습니다.

$ sudo kill -9 2387

2387 프로세스를 죽였는데, unshare로 실행된 sh도 종료되었을 것입니다. 이는 둘이 같은 프로세스이기 때문입니다. 즉, 프로세스에게는 자신의 PID가 1번으로 보이지만, 호스트 입장에서는 PID가 2387인 일반적인 프로세스라는 의미입니다.

프로세스 ID를 격리하는 PID 네임스페이스

PID 네임스페이스라는 게, 이렇게 보니까 아주 생소하게 느껴집니다만 사실 모든 프로세스는 PID 네임스페이스를 가지고 있습니다. 리눅스 시스템에 대해서 공부해보신 분이라면, /proc 디렉터리에 시스템과 프로세스에 대한 정보가 담겨져있다는 것을 알고 계실 겁니다. 호스트 상에서 /proc 디렉터리로 이동해보면 프로세스 아이디 이름을 가진 디렉터리들이 옹기종기 모여 있습니다.

리눅스의 /proc 디렉터리에는 프로세스에 관련된 정보들이 담겨있습니다

숫자로 된 이 디렉터리들에는 특정 프로세스에 대한 정보들이 담겨있습니다. 위에서 몇 번 언급한 1번 프로세스도 보이네요. 여기서 1번 프로세스는 systemd입니다. 모든 프로세스 디렉터리 아래에는 ns라는 디렉터리가 있는데 여기서 ns가 바로 네임스페이스를 줄인 단어입니다. 1번 프로세스 아래의 ns 디렉터리의 내용을 살펴보겠습니다.

/proc/1/ns# ll -l
total 0
dr-x--x--x 2 root root 0 Jan 11 04:11 ./
dr-xr-xr-x 9 root root 0 Jan 11 03:10 ../
lrwxrwxrwx 1 root root 0 Jan 11 04:12 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 net -> 'net:[4026531993]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 uts -> 'uts:[4026531838]'

앞서 네임스페이스는 PID 뿐만 아니라 UTS, 네트워크, 마운트 등 다양한 네임스페이스가 있다고 언급했습니다. 여기서도 바로 확인할 수 있네요. 다양한 네임스페이스가 있습니다만 지금은 PID 네임스페이스를 살펴보고 있습니다. 자세히 보면 pid:[4026531836]에 10자리 숫자가 적혀있습니다. 바로 이 숫자가 현재 프로세스의 PID 네임스페이스입니다. systemd4026531836 PID 네임스페이스의 첫번째 프로세스이며 PID는 1번입니다. 기본적으로 모든 프로세스는 init 프로세스의 네임스페이스를 그대로 공유합니다.

/proc# ll **/ns/pid
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1002/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1046/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1047/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1059/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 106/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1079/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1084/ns/pid -> 'pid:[4026531836]'
...

다른 프로세스의 PID 네임스페이스를 출력해보면 대부분 1번 프로세스의 값과 같은 것을 알 수 있습니다. unshare를 사용하면 PID 네임스페이스가 정말로 분리되는 걸까요? 다시 한번 unsharesh를 실행해두고, 다른 창을 띄워서 ps로 최근에 실행된 프로세스 목록을 확인해봅니다.

$ sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
/ #

## 다른 창에서
$ ps aux --sort +start_time | tail -n 5
root      2680  0.0  0.2  63516  4216 pts/0    S    04:21   0:00 sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
root      2681  0.0  0.0   7456   764 pts/0    S    04:21   0:00 unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
root      2682  0.0  0.0   1308     4 pts/0    S+   04:21   0:00 /bin/sh
root      2684  0.0  0.1  37516  3516 pts/1    R+   04:22   0:00 ps aux --sort +start_time
root      2685  0.0  0.0   7508   868 pts/1    S+   04:22   0:00 tail -n 5

우리가 확인하고자 하는 프로세스는 unshare 바로 아래에 있는 /bin/sh 프로세스입니다. PID는 2682입나디. /proc/2682/ns/pid의 내용을 확인해보겠습니다.

/proc# ll /proc/1/ns/pid
lrwxrwxrwx 1 root root 0 Jan 11 04:12 /proc/1/ns/pid -> 'pid:[4026531836]'
/proc# ll /proc/2682/ns/pid
lrwxrwxrwx 1 root root 0 Jan 11 04:24 /proc/2682/ns/pid -> 'pid:[4026532296]'

1번 프로세스의 PID 네임스페이스는 4026531836인데, 2682번 프로세스의 PID 네임스페이스는 4026532296입니다. 즉, 값이 다르므로 다른 네임스페이스라는 것을 알 수 있습니다.

도커 컨테이너는 정말로 프로세스인가요?

도커 컨테이너 이야기를 하다가 갑자기 unshare와 PID 네임스페이스로 빠져버렸습니다만, 다시 돌아와보겠습니다. 감이 오시나요? 도커 컨테이너의 PID가 1번인 원리도 위에서 살펴본 것과 똑같습니다. 이번에는 도커 컨테이너를 찾아가보겠습니다. 먼저 실험용으로 Nginx 컨테이너를 하나 실행하겠습니다.

$ docker run -d -p 80:80 nginx:latest
622eb0c2acac4d6c9304e57dcc8ea83fdaec31020ce124e281942dc44ee30725
$ docker ps -l
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
622eb0c2acac        nginx:latest        "nginx -g 'daemon of…"   6 seconds ago       Up 4 seconds        0.0.0.0:80->80/tcp   gracious_rubin

$ curl 0.0.0.0:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

curl 명령어로 정상 동작하는지도 살펴보았습니다. 아, 프로세스를 찾기에 앞서 프로세스 아이디가 1인지부터 확인해보겠습니다. nginx:latest 이미지에는 ps도 없어서 확인이 쉽지는 않습니다만, /proc/1의 명령어를 확인해보겠습니다.

$ docker exec -it 622eb0c2acac cat /proc/1/comm
nginx

1번 프로세스의 명령어가 nginx인 것을 확인할 수 있습니다. 자, 이제 이 컨테이너 = 프로세스에 대해서 알아볼 차례인데요, 도커 이미지에 대해 알아본 글에서는 /var/lib/docker 디렉터리의 내용을 뒤적여보았습니다. 이번에는 /var/run/docker 아래의 내용을 살펴봅니다.

$ cd /var/run/docker/runtime-runc/moby/
$ ls
622eb0c2acac4d6c9304e57dcc8ea83fdaec31020ce124e281942dc44ee30725

앞서 실행한 nginx 컨테이너로 추정되는 디렉터리가 보이네요. 컨테이너 아이디와 앞부분이 같습니다. 이 디렉터리 안에는 state.json 파일이 하나 있습니다. 내용이 꽤 많아서 여기서는 jq로 필요한 부분만 추출해보겠습니다.*

* jq는 커맨드라인에서 JSON 파일 내용을 바로 프로세싱할 수 있는 도구입니다. 우분투에서는 apt install jq로 설치할 수 있습니다. 더 자세한 내용은 커맨드라인 JSON 프로세서 jq : 기초 문법과 작동원리 글을 참고해주세요.

$ cd 622eb0c2acac4d6c9304e57dcc8ea83fdaec31020ce124e281942dc44ee30725
$ cat state.json  | jq '.init_process_pid'
2899

init_process_pid의 내용을 보니 프로세스 아이디가 2899인 것을 확인할 수 있습니다. 여기에는 더 많은 정보들이 있습니다. 그 중에서도 네임스페이스에 관련된 정보들을 한 번 살펴보겠습니다.

$ cat state.json | jq '.namespace_paths'
{
  "NEWCGROUP": "/proc/2899/ns/cgroup",
  "NEWIPC": "/proc/2899/ns/ipc",
  "NEWNET": "/proc/2899/ns/net",
  "NEWNS": "/proc/2899/ns/mnt",
  "NEWPID": "/proc/2899/ns/pid",
  "NEWUSER": "/proc/2899/ns/user",
  "NEWUTS": "/proc/2899/ns/uts"
}

네임스페이스들이 /proc/2899 아래에 새로 정의되어있다는 것을 확인할 수 있네요. 이는 호스트에서 확인할 수 있습니다. 먼저 1번 프로세스와 2899 프로세스의 네임스페이스들을 비교해보겠습니다.

$ diff \
  <(ls -Al /proc/1/ns | awk '{ print $9 $10 $11 }') \
  <(ls -Al /proc/2899/ns | awk '{ print $9 $10 $11 }')

3,7c3,7
< ipc->ipc:[4026531839]
< mnt->mnt:[4026531840]
< net->net:[4026531993]
< pid->pid:[4026531836]
< pid_for_children->pid:[4026531836]
---
> ipc->ipc:[4026532238]
> mnt->mnt:[4026532236]
> net->net:[4026532298]
> pid->pid:[4026532239]
> pid_for_children->pid:[4026532239]
9c9
< uts->uts:[4026531838]
---
> uts->uts:[4026532237]

ipc, mnt, net, pid, pid_for_children, uts 네임스페이스가 다르다는 것을 확인할 수 있습니다. 네임스페이스는 다르지만 ps로 일반적인 프로세스라는 것을 한 번 확인해보겠습니다.

$ ps aux | grep 2899
root      2899  0.0  0.2  10632  5348 ?        Ss   06:20   0:00 nginx: master process nginx -g daemon off;

이렇게 보면 그냥 nginx 프로세스네요. 이 2899 프로세스가 nginx:latest 이미지로 실행된 컨테이너라는 것을 어떻게 확인할 수 있을까요? killSIGKILL 시그널을 보내보면 되겠죠? 😅

$ docker ps -l
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
622eb0c2acac        nginx:latest        "nginx -g 'daemon of…"   29 minutes ago      Up 29 minutes       0.0.0.0:80->80/tcp   gracious_rubin

$ sudo kill -9 2899

$ docker ps -al
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                       PORTS               NAMES
622eb0c2acac        nginx:latest        "nginx -g 'daemon of…"   29 minutes ago      Exited (137) 2 seconds ago                       gracious_rubin

먼저 docker ps로 컨테이너가 잘 실행중인 것을 확인하고, kill을 실행하고 다시 docker ps -al로 컨테이너의 상태를 확인해보았습니다. 위에서 실행중이던 컨테이너가 SIGKILL 시그널을 받고 사망한 것을 확인할 수 있습니다. ps로 찾아보아도 더 이상 2899 프로세스는 보이지 않습니다.

어떤가요, 이제 확실해졌죠? 컨테이너는 그냥 프로세스였습니다. 좀 더 정확히는 호스트 입장에서는 컨테이너도 그냥 하나의 프로세스에 불과합니다.

마치며

여기까지 도커 컨테이너가 프로세스에 불과하다는 점에 대해서 알아보았습니다. 알고 보면 별 게 아니긴 합니다만, 단순히 도커 컨테이너가 프로세스라는 말을 들어오다가 직접 확인해보면 느낌이 많이 다릅니다. 예를 들어 도커 이미지는 여전히 가상머신의 이미지처럼 느껴지곤 합니다만, 도커 컨테이너가 프로세스라는 걸 정확히 이해하면 도커 이미지를 ’단 하나의 타깃 프로세스를 실행하기 위한 파일들의 집합’으로 이해할 수 있게됩니다. docker exec 명령어도 SSH 접속이나 가상머신에서 셸을 실행하는 것이 아니라, 컨테이너가 사용중인 네임스페이스나 파일 시스템에 접근하기 위한 특별한 명령어에 불과하다는 것을 알 수 있습니다.

여담입니다만, 최근에는 컨테이너가 가상머신이 아니라는 것도 아주 정확한 표현은 아닙니다. 리눅스 기반의 도커 컨테이너는 여전히 리눅스 프로세스입니다만, 카타 컨테이너Kata Containers파이어크랙커Firecracker와 같은 도구들은 마이크로VM을 기반으로 컨테이너를 실행하거나 서버리스 환경을 구축하려는 프로젝트들입니다. 기존에는 가상머신과 컨테이너를 기동 시간의 차이로도 많이 설명했습니다만 마이크로VM 기술이 많이 발전하면서 VM으로 컨테이너를 실행하는 시간도 현저히 줄어들고 있으며, 리눅스 프로세스보다 훨씬 격리 수준이 높기 때문에 시간이 지나면 컨테이너가 VM인 게 당연한 날이 올지도 모르겠네요.

구글 앱스 스크립트(Google Apps Script) 외부에서 실행하기

🗒 기사, 2018-08-02 - 구글 앱스 스크립트를 사용하면 구글 드라이브나 G 스위트를 자동화할 수 있습니다. 이 글에서는 간단한 구글 앱스 스크립트를 작성하고, 이 스크립트를 구글 드라이브 외부 환경에서 실행하는 방법을 소개합니다.

direnv를 사용한 디렉토리(프로젝트) 별 개발환경 구축: 루비, 파이썬, 노드 개발 환경 구축

🗒 기사, 2018-08-13 - direnv는 디렉터리 별로 셸 환경을 구축할 수 있게 해주는 도구입니다. 디렉터리 별 환경 변수 설정 법, 루비(Ruby), 파이썬(Python), 노드(Node) 등 프로그래밍 언어 프로젝트를 셋업하는 법을 소개합니다.

젯브레인 IDE로 쾌적한 테라폼(Terraform) 코딩 환경 구축

🗒 기사, 2019-02-26 - 테라폼(Terraform)은 클라우드 시대에 각광받고 있는 인프라스트럭처 관리 도구입니다. 대다수 에디터들이 코드 하이라이팅이나 자동 완성 등을 지원하지만, 아직까지 인텔리J(IntelliJ) 만큼 강력한 지원 기능을 본 적은 없습니다. 어떤 기능인지 둘러보실까요?