LabHub
배우기 러닝패스 코스

Container Internals

Seeing Namespaces With Your Own Eyes

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

이 실습은 진짜 VM 에서 돕니다

이 상자는 파드가 아니라 KubeVirt 가 띄운 가상머신입니다. 리눅스 커널이 따로 돌고 systemd 가 실제로 서비스를 관리하며, docker 는 흉내가 아니라 진짜 도커 엔진입니다. docker run 으로 띄운 컨테이너는 실제로 프로세스가 되고 docker execdocker logs 도 그대로 동작합니다.

예전에는 이 실습이 파드 안에서 돌았습니다. 커널 권한을 전부 내려놓은 상자라 컨테이너를 띄우는 단계가 막혀 있었고, 그래서 이미지 아카이브를 직접 풀어 보는 우회로 배웠습니다. 이제 우회가 필요 없습니다.

알아 둘 것이 둘 있습니다.

목표

네임스페이스 7종 중 pid, uts, mnt, net, user 다섯 가지를 직접 재어 보고, 컨테이너가 '상자'가 아니라 시야가 제한된 평범한 프로세스라는 것을 inode 번호로 증명합니다. 마지막에는 컨테이너 둘이 네트워크 네임스페이스를 공유하는 쿠버네티스 파드 구조를 도커만으로 재현합니다.

왜 중요한가

컨테이너 트러블슈팅이 막히는 지점은 대부분 "이건 분리된 건가 아닌가"를 몰라서 생깁니다. 포트가 왜 충돌하는지, 컨테이너 안에서 본 파일이 호스트 어디에 있는지, 사이드카가 왜 localhost 로 앱에 붙는지 — 답은 전부 "그 자원이 어느 네임스페이스에 속하는가" 한 줄입니다. 네임스페이스에는 inode 번호가 붙고 /proc/<pid>/ns/* 로 읽을 수 있으므로, 이 질문은 추측이 아니라 측정으로 답할 수 있습니다. 같은 번호면 같은 네임스페이스, 다른 번호면 다른 네임스페이스. 그게 전부입니다.

단계

  1. /root/int1 디렉터리를 만들고, 호스트에서 /proc/self/ns/pid 링크가 가리키는 값을 /root/int1/host-pid-ns.txt 에 저장합니다. 파일 내용은 pid:[숫자] 한 줄이어야 합니다.
  2. alpine:3.20 이미지로 dk-ns 라는 이름의 컨테이너를 --hostname labhub-uts 를 붙여 sleep infinity 로 백그라운드 실행하고, 그 컨테이너 안에서 읽은 /proc/self/ns/pid 값을 /root/int1/ctr-pid-ns.txt 에 저장합니다. 1단계 값과 달라야 합니다.
  3. dk-ns 안에서 프로세스 목록을 PID 열이 보이는 형식으로 뽑아 /root/int1/pid1.txt 에 저장합니다 (docker exec dk-ns ps -o pid,comm). PID 1 이 sleep — 그 컨테이너를 띄울 때 준 명령 — 이어야 합니다. systemd 도 init 도 아니라는 것이 요점입니다. 이 VM 에서 그냥 ps -e 를 하면 PID 1 은 systemd 라서 대조가 됩니다.
  4. dk-ns 의 호스트명을 /root/int1/uts.txt 에 저장합니다. 내용은 labhub-uts 한 줄이어야 합니다.
  5. 이 상자의 /etc/os-release 와 alpine 이미지 쪽 /etc/os-release한 파일에 이어서 /root/int1/mnt-proof.txt 에 저장합니다. 파일 안에 호스트 쪽 ubuntu 와 컨테이너 쪽 alpine 이 둘 다 보여야 합니다. docker run --rm alpine:3.20 cat /etc/os-release 와 이 VM 의 같은 파일을 한 파일에 이어 붙이면 됩니다. 함께 uname -r 도 찍어 보세요 — 커널은 같고 파일 트리만 다릅니다. 마운트 네임스페이스가 하는 일이 결국 프로세스의 / 를 다른 트리로 바꿔치는 것뿐이라는 사실이 그 두 줄에 들어 있습니다.
  6. dk-ns 안에서 읽은 /proc/net/dev/root/int1/ctr-net.txt 에 저장합니다. lo: 줄이 있어야 하고, 호스트의 같은 파일과 내용이 달라야 합니다 — 인터페이스 구성과 송수신 통계는 네임스페이스마다 따로 관리됩니다.
  7. dk-ns 의 네트워크 네임스페이스에 합류하는 dk-sidecar 컨테이너를 백그라운드로 띄우고, dk-ns 안에서 읽은 /proc/self/ns/net/root/int1/ns-a.txt 에, dk-sidecar 안에서 읽은 같은 값을 /root/int1/ns-b.txt 에 저장합니다. 두 값은 net:[숫자] 형식이면서 서로 같아야 합니다.
  8. 호스트의 /proc/self/uid_map/root/int1/host-uidmap.txt 에, dk-ns 안의 /proc/self/uid_map/root/int1/ctr-uidmap.txt 에 저장합니다. 두 매핑 내용이 서로 달라야 합니다.

참고

호스트의 PID 네임스페이스 식별자 읽기

/root/int1 디렉터리를 만들고, 호스트에서 /proc/self/ns/pid 링크가 가리키는 값을 /root/int1/host-pid-ns.txt 에 저장합니다. 파일 내용은 pid:[숫자] 한 줄이어야 합니다.

네임스페이스 식별자는 /proc/self/ns/ 아래 심볼릭 링크로 노출됩니다. 링크가 가리키는 문자열 자체가 답이므로 cat 이 아니라 링크를 따라가는 명령을 써야 합니다. 파일에는 pid:[숫자] 한 줄만 남아야 합니다.

컨테이너의 PID 네임스페이스와 비교

alpine:3.20 이미지로 dk-ns 라는 이름의 컨테이너를 --hostname labhub-uts 를 붙여 sleep infinity 로 백그라운드 실행하고, 그 컨테이너 안에서 읽은 /proc/self/ns/pid 값을 /root/int1/ctr-pid-ns.txt 에 저장합니다. 1단계 값과 달라야 합니다.

같은 경로를 컨테이너 안에서 읽어야 다른 번호가 나옵니다. 호스트 셸에서 읽으면 1단계와 똑같은 값이 나와 실패합니다. 컨테이너는 계속 떠 있어야 다음 단계에서 계속 쓸 수 있으니 곧바로 종료되지 않는 명령으로 띄우세요.

컨테이너 안의 PID 1 확인

dk-ns 안에서 프로세스 목록을 PID 열이 보이는 형식으로 뽑아 /root/int1/pid1.txt 에 저장합니다 (docker exec dk-ns ps -o pid,comm). PID 1 이 sleep — 그 컨테이너를 띄울 때 준 명령 — 이어야 합니다. systemd 도 init 도 아니라는 것이 요점입니다. 이 VM 에서 그냥 ps -e 를 하면 PID 1 은 systemd 라서 대조가 됩니다.

프로세스 목록을 뽑되 PID 열이 함께 보이는 형식이어야 합니다. 컨테이너의 주 명령이 곧 1번 프로세스입니다. 호스트에서 뽑으면 1번이 다른 프로세스로 나옵니다.

UTS 네임스페이스로 분리된 호스트명

dk-ns 의 호스트명을 /root/int1/uts.txt 에 저장합니다. 내용은 labhub-uts 한 줄이어야 합니다.

호스트명을 바꾸는 것이 아니라, 컨테이너를 만들 때 붙여 준 호스트명이 UTS 네임스페이스 덕분에 호스트와 따로 논다는 것을 확인하는 단계입니다. 컨테이너 안에서 호스트명을 물어보고 그 값만 저장하세요.

마운트 네임스페이스 — 같은 커널, 다른 파일 트리

이 상자의 /etc/os-release 와 alpine 이미지 쪽 /etc/os-release한 파일에 이어서 /root/int1/mnt-proof.txt 에 저장합니다. 파일 안에 호스트 쪽 ubuntu 와 컨테이너 쪽 alpine 이 둘 다 보여야 합니다. docker run --rm alpine:3.20 cat /etc/os-release 와 이 VM 의 같은 파일을 한 파일에 이어 붙이면 됩니다. 함께 uname -r 도 찍어 보세요 — 커널은 같고 파일 트리만 다릅니다. 마운트 네임스페이스가 하는 일이 결국 프로세스의 / 를 다른 트리로 바꿔치는 것뿐이라는 사실이 그 두 줄에 들어 있습니다.

한쪽만 담으면 비교가 되지 않습니다. 호스트에서 읽은 것과 컨테이너 안에서 읽은 것을 같은 파일에 이어서 남기세요. 이어붙일 때는 덮어쓰기가 아니라 추가 리다이렉션을 씁니다.

네트워크 네임스페이스의 인터페이스 목록

dk-ns 안에서 읽은 /proc/net/dev/root/int1/ctr-net.txt 에 저장합니다. lo: 줄이 있어야 하고, 호스트의 같은 파일과 내용이 달라야 합니다 — 인터페이스 구성과 송수신 통계는 네임스페이스마다 따로 관리됩니다.

인터페이스 목록은 /proc/net/dev 로도 읽을 수 있습니다(이 환경에서는 ip/ifconfig 보다 확실합니다). 컨테이너 안에서 읽으면 lo 와 컨테이너 인터페이스 정도만 보이고 호스트보다 줄 수가 적어야 합니다.

두 컨테이너가 네임스페이스를 공유하게 하기

dk-ns 의 네트워크 네임스페이스에 합류하는 dk-sidecar 컨테이너를 백그라운드로 띄우고, dk-ns 안에서 읽은 /proc/self/ns/net/root/int1/ns-a.txt 에, dk-sidecar 안에서 읽은 같은 값을 /root/int1/ns-b.txt 에 저장합니다. 두 값은 net:[숫자] 형식이면서 서로 같아야 합니다.

새 네트워크 네임스페이스를 만드는 대신 남의 것에 합류시키는 옵션이 있습니다. 쿠버네티스 파드에서 pause 컨테이너가 하는 일과 같습니다. 합류가 성공했다면 두 컨테이너의 net inode 번호가 완전히 같아야 합니다.

유저 네임스페이스의 UID 매핑

호스트의 /proc/self/uid_map/root/int1/host-uidmap.txt 에, dk-ns 안의 /proc/self/uid_map/root/int1/ctr-uidmap.txt 에 저장합니다. 두 매핑 내용이 서로 달라야 합니다.

/proc/self/uid_map컨테이너안UID 호스트UID 개수 세 열입니다. 이 환경은 rootless 라서 호스트 쪽 셸과 컨테이너 안쪽의 매핑이 다르게 나옵니다 — 읽기 자료의 실측표(도커 기본값에서는 user 네임스페이스가 같았다)와 왜 다른지 생각해 보세요.