- 본 글은 카카오 엔터프라이즈 글과 유튜브 영상을 보고 따라하며 단순히 정리한 글입니다. 개인적으로 다른 어떤 글보다 내부 원리를 이해하기 좋게 구성되어 있다고 생각하여 별도로 정리하게 되었습니다.
컨테이너
컨테이너는 과거 값비싼 서버를 나눠 쓰는 고민에서 출발하여 오랜 기간 동안 리눅스 기술 토대 위에 잘 쌓아 올려져 왔습니다. 이러한 점에서 컨테이너를 공부하는 것은 단순히 새로운 기술을 배우는 것 이상으로 리눅스나 전산 기초를 더 깊이 이해하는 과정에 가깝습니다.
- 컨테이너란 격리된 환경과 제한된 자원으로 제어되는 프로세스를 의미한다.
- 격리된 환경은 배포에 장점이 있다.
VM과 컨테이너
- VM은 컨테이너보다 훨씬 강력한 격리를 제공한다.
- 하이퍼바이저 라는 소프트웨어가 하드웨어 자원을 가상화하고, 그 위에 별도의 운영체제가 설치된다.
- 이 운영체제는 서버의 호스트OS와 구분해서 게스트OS(이하, GuestOS)라고 부른다. 하이퍼바이저는 서버 위에 여러 종류의 GuestOS를 VM으로 실행한다.
- 컨테이너는 VM에 비해 작고 빠르다. 컨테이너는 호스트 커널에서 바로 처리되고 하드웨어를 직접 관리하는 반면에 VM은 GuestOS가 하드웨어 사용 시 하이퍼바이저의 처리를 기다려야 한다.
사실, 컨테이너는 어느 하루 아침에 docker run 명령어로 바로 실행할 수 있게 나온 것이 아닙니다. 컨테이너는 프로세스 격리라는 문제를 오랜 세월에 걸쳐 풀어오면서 필요한 요건들이 하나 둘 씩 생겨났고 그것들을 구현해 온 결과입니다.
chroot로 컨테이너 이해하기
- Change Root Directory, chroot에 갇힌 프로세스는 현재 디렉터리를 루트로 인지하여 동작하기에 프로세스를 실행할 때 필요한 커맨드 프로그램, 라이브러리 등을 chroot에 함께 넣어야 한다.
- 새로운 루트 경로에서 /bin/bash를 실행한다
- cp /bin/bash new-root
- chroot new-root /binbash → command not found (의존성 없음)
- 필요한 의존성을 확인하고 추가한다. (ldd /bin/bash)
- mkdir -p new-root{lib/x86_64{linx-gnu, lib64}
- cp /lib/x86_64-linux-gnu/{libtinfo.so.5,libdl.so.2,libc.so.6} new-root/lib/x86_64-linux-gnu
- cp /lib64/ld-linux-x86-64.so.2 new-root/lib64
- chroot를 실행한다.
- chrrot new-root /bin/bash
- 즉, 새로운 루트 경로(new-root)를 만들고, 실행할 커맨드와 의존성 라이브러리(ldd)을 새로운 경로에 복사하여 새로운 경로에서 커맨드를 실행하는 것이다.
- 이러한 실행할 커맨드와 필요한 의존성 라이브러리를 쓰기 편하게 모아둔 것을 이미지라고 한다.
chroot의 한계
- 탈옥이 가능하다. (파일 시스템 독립성 훼손)
- 격리되지 않는다. 프로세스가 루트 권한만 있으면 chroot 경로에 접근할 수 있어 다른 프로세스 및 시스템 정보를 볼 수 있다.
- 루트 권한 제어 필요하다. chroot를 쓰려면 루트 권한이 필요하고, chroot로 실행된 프로세스도 루트 권한을 가진다. (루트 권한 없이도 컨테이너를 동작시키기 위한 적절한 권한 부여 방법이 필요)
- 자원 격리가 필요하다. chroot를 쓰더라도 호스트의 자원을 제한 없이 사용할 수 있다.
pivot_root
- pivot_root는 실제로 루트 파일 시스템 자체를 바꿔, 컨테이너가 전용 루트 파일 시스템을 가지도록 한다. 컨테이너는 별도의 루트 파일시스템인 마운트 네임 스페이스가 필요하다!
pivot_root [new_root] [old_root]
- 기존의 루트 시스템을 새로운 루트 파일 시스템으로 변경하는 명령어이다.
- 마운트 네임스페이스를 격리한 상태에서 pivot_root하기 때문에 호스트에는 영향을 주지 않으면서도 컨테이너는 호스트와 격리된 루트 파일시스템을 갖는다.
리눅스 네임스페이스
- 리눅스 네임스페이스는 프로세스에 격리된 환경과 자원을 제공한다.
- 네임스페이스 규칙이 존재한다.
- 네임스페이스 안에서의 변경은 내부 프로세스에서만 보이고, 다른 프로세스에서는 보이지 않는다.
- 네임스페이스와 관련된 프로세스는 부모 자식의 특징을 갖는다.
- 모든 프로세스들은 네임스페이스 타입별로 특정 네임스페이스에 속한다.
- 자식 프로세스는 부모 프로세스의 네임스페이스를 상속받는다.
- 격리하는 자원에 따라 다양하게 구분되어 있다.
1. 마운트 네임스페이스
- pivot_root를 사용하기 위해서 호스트 파일시스템에 영향을 주지 않는 방법이 필요하다.
- 마운트 네임스페이스는 마운트 포인트를 격리하여 호스트 파일시스템에 영향을 주지 않고 컨테이너 전용 파일시스템을 구성할 수 있도록 도와준다.
unshare --mount /bin/bash
- 마운트 네임스페이스를 격리하여 bash를 실행
- 컨테이너가 마운트 네임스페이스로 격리되면 호스트에서는 해당 컨테이너 안에서 마운트된 것이 보이지 않는다.
- 다음 명령어를 통해 특정 프로세스의 네임스페이스 정보를 알 수 있다. 호스트와 비교하면 같다.
- readlink /proc/$$/ns/ment
2. UTS 네임스페이스
컨테이너의 문패, 어떤 게 우리 컨테이너인지 표시
- UTS 네임스페이스는 호스트명을 서버와 다르게 설정하여 사용할 수 있도록 한다.
- UTS는 Unix Time-Sharing의 약자로 여러 사람이 한 대의 서버를 나누어 쓸 때 사용자 환경별로 호스트명이나 도메인명을 구분할 수 있도록 격리를 제공한다.
- uts 네임스페이스 사용
unshare --uts /bin/bash
hostname
명령어를 통해 호스트 명을 확인하면 호스트와 동일하게 출력되고(부모 상속), 컨테이너 안에서 자신만의 hostname을 가질 수도 있다.
3. IPC 네임스페이스
직통 전화를 한 대 설치
- 리눅스에는 IPC(Inter Process Communication)을 지원하는 pipe, message queue, shared memory, socket 등 다양한 자원이 존재한다.
- 컨테이너 터미널에서 ipc 네임스페이스를 격리하여 셸을 실행하고, 공유 메모리를 생성하고 확인한다.
unshare —ipc /bin/bash
ipcmk -M 2000
icps -m
- 호스트 터미널에서 공유 메모리를 생성하고 확인한다.
- 컨테이너와 호스트는 서로 다른 IPC 네임스페이스로 격리되어 있어 서로의 IPC 자원이 보이지 않는다.
4. PID 네임스페이스
가문의 족보와 같이 관계와 계통 표시
- PID 네임스페이스는 pid 자원을 격리한다. 이때 부여되는 pid는 PID 네임스페이스 안에서 unique하다.
- PID 네임스페이스를 격리하면 컨테이너 안에서도 pid 1로 시작하는 자체적인 트리구조를 가지게 된다.
- 위의 그림에서 PID 네임 스페이스를 생성하면, 커널은 child 프로세스 pid 6을 만든다.
- pid 6을 init process로 하여 PID 네임스페이스를 생성한다. pid 1은 init process로 pid 1이 죽으면 PID 네임스페이스도 사라진다.
- PID 네임스페이스 생성하기
echo $$
(현재 프소세스 PID)
unshare --pid --fork --mount-proc /bin/bash
echo $$
(앞에서 출력한 pid값과 비교)
5. cgroup 네임스페이스
자원 계량기
- croup(Control Group)은 프로세스에 할당된 시스템 자원(CPU, Memory, Network …)에 대한 제어를 제공한다.
- cgroup이 시스템 자원을 할당하고 제어하는 방식은 파일 시스템을 기반으로 하며, 특수한 파일시스템에서 디렉터리를 만들고 파일을 수정하는 방식으로 시스템 자원들을 설정하고 관리한다.
- 컨테이너에서 cgroup이 관리하는 파일시스템에 접근하면 다른 컨테이너는 물론 호스트 시스템 자원까지 접근할 수 있다. 따라서 cgroup 네임스페이스를 이용해 컨테이너가 속한 cgroup만 보이게 한다.
- cgroup에서는 디렉토리 각 항목을 subsystem이라고 한다.
- 각 subsystem 디렉토리에 들어가면 subsytem을 제어하기 위한 다양한 파일이 존재한다.
- cgroup 파일 시스템 확인하기
- tree -L 1 /sys/fs/cgroups
6. 네트워크 네임스페이스
- 컨테이너의 네트워크 스택(OSI 7 Layer)를 격리한다. 이를 통해 호스트 안에서 컨테이너를 가상 네트워크상 별도의 노드로 취급할 수 있다.
- 다음 명령어로 호스트와 컨테이너의 네트워크 인터페이스를 비교할 수 있다.
ip a
(호스트)
unshare --net /bin/bash
ip a
(컨테이너)
lsns
를 이용하면 네임스페이스 정보를 더 많이 알 수 있다.
lsns -t net -p $$
(현재 프로세스)
lsns -t net -p 1
(init 프로세스)
7. USER 네임스페이스
- 컨테이너를 루트 유저로 실행하는 것은 보안상 매우 위험하다.
- USER 네임스페이스를 이용하면 호스트에서는 권한이 최소화된 일반 유저지만, 컨테이너 안에서만 루트 유저로 부여할 수 있다.
- USER 네임스페이스 적용이 까다로운 이유는 리눅스 배포판마다 구성 차이가 있고, 루트 권한을 제한하면 컨테이너 생성 자체가 어렵기 때문이다.
- 이미지에 포함된 파일이나 호스트에서 컨테이너로 마운트한 파일, 디렉터리의 권한이 USER 네임스페이스의 권한과 상충하는 경우 등 해결해야 할 문제가 있다.
- USER 네임스페이스는 uid, gid의 number space를 격리한다는 것을 보여주는 예시
- 컨테이너 터미널
id
uid=0(root) groups=0(root)
- exit
- 호스트 터미널
id
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant)
- unshare로 USER 네임스페이스를 격리한 상태에서 컨테이너 id 확인하기
- unshare —user /bin/bash
- vagrant@ubuntu1804 → nobody@ubuntu1804
- id 출력을 확인하면 컨테이너의 uid와 호스트의 uid가 다르다. 다른 USER 네임스페이스를 사용하기 때문이다.
- 컨테이너를 일반 유저(vagrant)로 실행하면 UTS 네임스페이스를 만들 수 있는 권한이 없다.
8. Cgroups 네임스페이스
- cgroup 네임스페이스는 cgroup 파일시스템 격리를 통해 컨테이너에서 함부로 호스트나 다른 컨테이너의 cgroups에 접근하지 못하게 한다.
- cgroups(Control Groups)는 cgroup 파일시스템을 통해서 호스트의 하드웨어 자원을 그룹별로 관리하는 모듈이다.
- 관리할 수 있는 그룹에는 CPU, Memory, Network, Disk IO, Devices 등 다양한 자원이 존재한다.
- Cgroups는 하나 또는 복수의 장치를 묶어서 그룹을 만들 수 있고, 프로세스가 사용하는 자원 총량은 Cgroups의 통제를 받는다.
- Cgroups를 이용해 CPU 사용률 제한해서 프로세스 기동하기
apt install -y cgroup-tools
apt install -y stress
stress -c 1
CPU 100%
- 그룹에 CPU 사용률 제한하여 할당하기
cgcreate -a root -g cpu:mycgroup
cgset -r cpu.cfs_quota_us=30000 mycgroup
cgexec -g cpu:mycgroup stress -c 1
stress -c 1
CPU 30%