名前空間7種とコンテナの正体
한국어 원문으로 표시합니다.
한 줄 요약
커널 소스에 struct container 같은 것은 없습니다. 컨테이너는 네임스페이스 +
cgroup + 보안 레이어 + rootfs 를 한 프로세스에 묶어 놓은 상태를 사람이 부르는
이름이고, 그중 네임스페이스가 담당하는 질문은 딱 하나입니다 — 이 프로세스가
무엇을 볼 수 있는가.
왜 이게 필요했나
"컨테이너를 만든다"는 표현 때문에 많은 사람이 커널 어딘가에 컨테이너라는 상자가
있다고 상상합니다. 그런데 호스트에서 ps 를 쳐 보면 컨테이너 안의 프로세스가
그대로 보이고, 커널 버전도 호스트와 똑같이 나옵니다. 상자가 있다면 이럴 수
없습니다. 실제로 일어나는 일은 정반대입니다. 프로세스는 처음부터 호스트에
그냥 있고, 커널이 그 프로세스에게 일부 자원 목록을 다른 버전으로 보여 줄
뿐입니다. 격리가 아니라 시야 제한입니다.
이 차이가 실무에서 바로 드러납니다. 컨테이너 안에서 nproc 이 32 로 나오는데
실제로는 0.5 코어만 쓸 수 있는 상황, 컨테이너 두 개가 각각 80번 포트를 여는
상황, 컨테이너 안에서 지운 파일이 호스트에는 남아 있는 상황 — 전부 "무엇이
분리됐고 무엇이 안 분리됐는가"를 알아야 예측할 수 있습니다.
어떻게 동작하나
네임스페이스는 7종입니다.
| 종류 | 분리되는 것 |
|---|---|
| pid | 프로세스 번호 공간. 1번이 되면 신호 처리와 좀비 수확 책임이 따라온다 |
| net | 인터페이스, 라우팅 테이블, iptables 규칙, 소켓, 포트 번호 공간 전부 |
| mnt | 마운트 테이블. 이미지 rootfs 가 / 로 보이는 이유 |
| uts | hostname |
| ipc | System V IPC, POSIX 메시지큐 |
| user | UID/GID 매핑. rootless 컨테이너의 핵심 |
| cgroup | 자기 cgroup 위치를 루트로 보이게 |
net 이 포트 번호 공간까지 통째로 나눈다는 점이 중요합니다. 그래서 컨테이너 둘이 각각 80번을 열어도 충돌하지 않습니다. 포트 충돌은 컨테이너 사이가 아니라 호스트로 퍼블리싱하는 순간 생깁니다.
각 네임스페이스에는 inode 번호가 붙어 있고 /proc/<pid>/ns/<종류> 를
readlink 하면 그 번호가 나옵니다. 같은 번호면 같은 네임스페이스입니다.
실제로 재어 보면 이렇습니다.
호스트 pid:[4026531836] net:[4026531840] user:[4026531837] time:[4026531834]
alpine pid:[4026532500] net:[4026532562] user:[4026531837] time:[4026531834]
user 와 time 만 같습니다. 도커가 기본적으로 유저 네임스페이스를 쓰지 않기 때문입니다. 그래서 컨테이너 안의 root 가 호스트의 root 와 같은 UID 0 이 되고, 이것이 다음 코스에서 다룰 보안 문제의 뿌리입니다.
현장에서 만나는 모습
쿠버네티스 파드가 바로 이 구조를 그대로 쓴 것입니다. 파드에는 pause 라는
약 1MB짜리 컨테이너가 먼저 뜨고, 이 컨테이너가 net/ipc/uts 네임스페이스를
보유합니다. 앱 컨테이너들은 새로 만드는 대신 그 네임스페이스에 합류하고,
mnt 와 cgroup 만 컨테이너별로 따로 가집니다. 그래서 같은 파드 안 컨테이너끼리는
localhost 로 통신하고 포트를 공유하지만 파일시스템은 각자 다릅니다.
도커에서 이 구조를 직접 만들 수 있습니다. --network container:<이름> 이 바로
"남의 net 네임스페이스에 합류하라"는 명령입니다. 사이드카 패턴을 도커만으로
재현할 때 쓰는 것이 이것입니다.
네임스페이스는 언제 사라지나
네임스페이스는 오브젝트가 아니라 참조가 남아 있는 동안만 존재하는 것이다. 그 안의 마지막 프로세스가 끝나고 아무도 그것을 붙잡고 있지 않으면 커널이 회수한다. 이 성질을 알면 컨테이너 운영에서 이상해 보이는 몇 가지가 설명된다.
파드가 사라졌는데 네트워크 설정이 남아 있다. 어딘가가 아직 그
네임스페이스를 붙잡고 있다는 뜻이다. 붙잡는 방법은 셋이다. 그 안에서 도는
프로세스, /proc/<PID>/ns/<종류> 를 열어 둔 파일 디스크립터, 그리고
그 경로에 걸어 둔 바인드 마운트다. 마지막 것이 ip netns 가 쓰는 방법이라,
프로세스가 다 끝나도 이름이 남아 있으면 네임스페이스도 남는다.
컨테이너를 지웠는데 이름이 계속 남는다. 같은 이유이고, 실무에서는 대개 정리 절차가 그 바인드 마운트를 풀지 않은 경우다.
거꾸로 이 성질을 이용하기도 한다. 프로세스가 하나도 없는 네임스페이스를 미리 만들어 두고 나중에 거기에 합류시키는 방식이다. 앞에서 본 pause 컨테이너가 정확히 그 일을 한다. 앱 컨테이너가 죽었다 다시 떠도 네트워크 네임스페이스는 그대로 살아 있어서 IP 가 바뀌지 않는다. 파드의 IP 가 컨테이너 재시작에 견디는 이유가 이것이다.
한 가지 더 짚어 둔다. 네임스페이스는 자원을 나누지 않는다. 무엇을 볼 수 있는지만 정할 뿐이고, 얼마나 쓸 수 있는지는 cgroup 이 정한다. 그래서 네임스페이스만으로는 한 컨테이너가 CPU 를 다 쓰는 것을 막지 못한다. 컨테이너가 격리되어 보이는 것은 이 둘이 함께 걸려 있기 때문이고, 둘 중 하나만 걸면 그 절반은 그대로 새어 나간다.
다음 실습에서 할 것
호스트와 컨테이너의 네임스페이스 inode 를 직접 비교하고, PID 1 이 누구인지 확인하고, 호스트명과 파일 트리와 인터페이스 목록이 각각 어느 네임스페이스 때문에 달라지는지 하나씩 확인합니다. 마지막에는 컨테이너 두 개가 net 네임스페이스를 공유하게 만들어 파드의 구조를 손으로 재현합니다.