컨테이너 내부 원리 · 네임스페이스 — 무엇을 볼 수 있는가 · 이론
네임스페이스 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 네임스페이스에 합류하라"는 명령입니다. 사이드카 패턴을 도커만으로
재현할 때 쓰는 것이 이것입니다.
다음 실습에서 할 것
호스트와 컨테이너의 네임스페이스 inode 를 직접 비교하고, PID 1 이 누구인지
확인하고, 호스트명과 파일 트리와 인터페이스 목록이 각각 어느 네임스페이스
때문에 달라지는지 하나씩 확인합니다. 마지막에는 컨테이너 두 개가 net
네임스페이스를 공유하게 만들어 파드의 구조를 손으로 재현합니다.