본문 바로가기
Linux & Kubernetes

Docker's Internal

by 탁종민 2021. 1. 7.

Overlay FS in Linux

Linux의 OverlayFS 는 lowerdir, upperdir, merged 로 구성된다.

lowerdir : OverlayFS의 Readonly layer 이다.

upperdir : OverlayFS의 Read-Write layer 이다.

merged : lowerdir와 upperdir가 merged 된 최종 layer이다. 

 

overlayFS는 layer와 layer를 겹겹히 쌓은 파일시스템이다. 위에서 내려다 봤을 때 가장 윗 층에서 변경한 파일의 내용이 보인다. 예를들어 아래 그림에서 /c/d.txt는 lowerdir2 레이어의 내용이 보이고 lowerdir1의 내용은 무시된다. merged 레이어는 lowerdir 와 upperdir layer에 대한 일종의 Read, Write 인터페이스 역할을 한다. merged에서 발생하는 Write의 결과는 모두 upperdir에 반영된다. 이와같이 각 layer층의 filesystem층이 포개져 최종적인 file system을 구성하는게 OverlayFS이다. 

Docker는 OverlayFS를 사용한다. Dockerfile에서 명령어 한줄 한줄 실행해서 library같은 걸 설치할 때 마다 한개의 OverlayFS 레이어가 쌓인다. Dockerfile에 의해 완성된 OverlayFS가 바로 Docker Image가 된다. Docker 컨테이너들은 Docker Image를 lower-dir (Read-Only)레이어로 두고 각자의 upper-dir (Read-Write) 레이어를 그 위에 얹는다.

그림: https://docs.docker.com/storage/storagedriver/



Namespaces

리눅스의 Namespace는 Docker 컨테이너 샌드박스의 핵심이다.

여러 namespace중 Docker 컨테이너 구성의 핵심이 되는 건 MNT namespace, PID namespace, User namespace, Network namespace, IPC namespace등이 있다.

 

MNT Namespace 

Mount namespace는 mount point를 각 namespace로부터 격리한다.

https://elixir.bootlin.com/linux/latest/source/fs/mount.h

MNT namespace는 새로운 MNT namespace를 만들때 "Clone" 방식을 택한다. 완전히 새로운 namespace를 만드는 게 아닌 자기 자신을 그대로 복제한 MNT namespace를 만든다. 따라서 Parent MNT namespace의 모든 파일 시스템은 물론 기존에 존재하던 mount point들까지 child namespace에서 접근할 수 있다. 하지만 그 이후 생성되는 "mount point" 들은 공유되지 않는다.

사실 MNT namespace 분리는 이 "mount point" 를 볼 수 없게 만드는 역할만을 하므로 MNT namespasce를 분리하는 것만으로는 container가 자신만의 root file system을 가지게 만든다거나 container와 host, 혹은 container와 다른 container간의 파일 시스템을 분리할 수 없다.

아래의 예를 보자. 아래 그림은 Host에서 container 생성을 위해 막 새로운 MNT Namespace를 만든 직후에서의 상황이다. 막 Clone된 MNT Namespace는 여전히 Parent MNT Namespace에서와 같이 Host root file system을 비롯해 모든 경로에 접근 가능하다. 다만 아래 그림과 같이 MNT Namespace가 분리된 직후 새로 mount 된 /new-mounted는 Child MNT Namespace에서 접근할 수 없다.

결국 container가 호스트와 완전히 분리된 자신만의 root file system을 가지게 하는 건 pivot-root의 역할이다.

https://man7.org/linux/man-pages/man2/pivot_root.2.html

pivot root는 calling process가 속한 MNT Namespace의 root mount를 변경한다.

아래 예제에서 원래 Cloned MNT Namespace에 속한 프로세스가 pivot root로 root mount를 /var/lib/docker/overlay2/~~~~/merged/ 로 설정해 새로운 root로 옮긴걸 볼 수 있다. 

한번 pivot root가 이루어지면 해당 MNT Namespace의 root는 /var/lib/docker/overlay2/~~~~/merged/ 이므로 root 밖의 경로에는 접근할 수 없다. 그 말은 pivot root를 이용해 또다시 이전의 root로 돌아갈 수 없다는 뜻이 된다.

Docker가 container 실행 시 bind mount를 지원하지만, container가 실행되는 상태에서는 host로부터 container로의 mount를 허용하지 않는 건 MNT Namespace의 특성과 관련이 깊다. 한번 MNT Namespace가 분리된 이후에는 Host에서 아무리 container directory에 ( 예제 그림에서는 /var/lib/docker/overlay2/~~~~/merged/home ) 에 mount 해봤자  이는 Parent MNT Namespace에서 mount 한 것이므로 Child MNT Nmaespace에 있는 container는 해당 mount point를 볼 수 없다. 따라서 Docker가 bind mount를 행하려면 먼저 Child MNT Namespace를 Clone한 이후 pivot-root를 실행하기 전에 Child MNT Namespace에서 bind mount를 행해야 한다. 그 이후 pivot root를 하면 Host 의 directory를 bind mount한 container root file system이 완성된다.

( * 특정 namespace에서 행한 mount를 다른 namespace 에서도 공유할 방법이 존재하긴 하는데 이를 shared mount라 한다. shared mount로 mount된 경로에서는 해당 경로 내부에서 일어난 mount들이 parent 및 child에게 모두 공유되게 한다.

www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt )

 

PID Namespace 

https://elixir.bootlin.com/linux/latest/source/include/linux/pid_namespace.h

https://man7.org/linux/man-pages/man7/pid_namespaces.7.html

PID Namespace는 "Process"에 대한 접근을 각 namespace마다 분리한다.

PID Namespace는 MNT Namespace처럼 Namespace를 clone하는게 아니라 새로운 namespace "Node"를 만든 후 Tree의 Child 형태로 Parent에 연결시켜 parent-child 형태의 Tree 구조형태를 형성시킨다.

Parent Namespace는 Child namespace에 존재하는 Process에 접근 가능 하지만 Child namespace는 상위에 접근할 수 없고 Namespace가 분리된 이후에는 Child Namespace에선 상위에 무엇이 있는지조차 알 수 없다. 따라서 예를 들어 Linux Init프로세스를 container에서 실행시키면 상위에 무엇이 있는지도 모른 채 본인이 PID 1 Root 프로세스인 줄 알고 실행되게 된다. 

 

User Namespace 

https://elixir.bootlin.com/linux/latest/source/include/linux/user_namespace.h#L56

User Namespace역시 구조적으로 Tree형태를 띄지만, PID Namespace와는 약간 다르다.

User namespace는 특정 User ID를 이용해 만든 새로운 User Range 범위로 이루어진 공간이다. linux에는 각 UID의 subuid라는 것을 설정할 수 있는데 이 subuid의 범위 내에서 해당 User Namespace의 UID를 채우게 된다. 

https://man7.org/linux/man-pages/man5/subuid.5.html

예를 들어 1000 UID의 subuid가 YYY ~ YYY + 65536이라면 1000 UID를 이용해 만든 User Namespace의 첫 프로세스의 EUID ( Effective UID ) 는 YYY가 된다. 여기서 EUID가 다른 이유는 User Namespace 내에서는 해당 UID가 0 ( ROOT ) 일 수 있지만, Parent UID Namespace에선 UID가 YYY이기 때문이다. 즉 EUID 란 특정 User Namespace 안에서의 UID가 아닌 실질적인 UID이다. 

 

사실 Docker은 default로 User Namespace를 분리하지 않는다. 따라서 container에서의 Root user process는 Host에서도 Root User이다. 만약 우리가 호스트에서 root 유저의 파일을 권한 644로 설정한 후 container로 bind mount 했다고 하자. Docker에서 default 설정으로 동작 중인 container의 root 프로세스는 EUID역시 root 이므로 이 호스트파일을 write 할 수 있다. Docker가 공식문서에서 user-remap을 통해 User Namespace분리를 권장하는 이유 역시 이런 이유이다.

 

Network Namespace, IPC Namespace

https://elixir.bootlin.com/linux/latest/source/net/core/net_namespace.c

https://man7.org/linux/man-pages/man7/network_namespaces.7.html

 

Network Namespace는 Firewall rule, 라우팅 테이블, Physical, Virtual Network Device, UNIX socket 등을 분리하는 역할을 한다.  Network Namespace가 분리되면 하나의 리눅스 내부 네트워크에서 하나의 가상 Host 처럼 동작하게 된다.

일반적인 네트워크 이외에 리눅스에는 pipe, message queue 등 운영체제 내에서 프로세스끼리의 통신에 이용되는 IPC 수단들이 있다. 만약 이런 IPC객체를 별개의 Namespace에 속한 process들이 접근할 수 있으면 안되므로 Linux는 IPC Namespace를 이용해 분리시킨다.

 

Docker Image and Container with OverlayFS and Namespace

Docker image는 OverlayFS 로 이루어진 filesystem 자체이다. 즉 OverlayFS를 이용해 특정 layer위에 필요한 파일을 설치하고 layer를 쌓으며 이 layer들의 겹겹들로 이루어진 OverlayFS 결과물이 image이다. Docker Image는 아직 그냥 정적인 파일시스템 덩어리에 불과하므로 안에서 돌아가는 프로세스가 없다면 아무런 의미가 없다.

Docker Image( lower-dir)와 그 위에 새로운 read-write(upper-dir) 레이어로 구성된 파일시스템을 구성하고 리눅스의 namespace를 이용해 분리된 환경을 구성해서 프로세스를 실행하면 최종적인 container가 된다.  DockerImage와 read-write레이어로 구성된 OverlayFS를 특정 mount point ( 예를들어 /var/docker/container/deafbeef...../mounts/merged )에 load 하고 새로운 namespace를 생성 한 이후 pivot_root으로 해당 namespace의 root를 "/var/docker/container/deafbeef.../mounts/merged" 로 변경하면 virtual root filesystem과 함께 container를 위한 하나의 가상 environment가 생성된다. 이 환경에서 process 를 실행시키면 DockerImage( lower-dir, read-only)내에 존재하는 파일시스템들의 library등을 이용해 process가 동작하게 된다.

 

- ref

https://docs.docker.com/storage/storagedriver/

hustcat.github.io/mount-namespace-and-mount-propagation/www.programmersought.com/article/63501472692/

https://lwn.net/Articles/159077/

https://netflixtechblog.com/evolving-container-security-with-linux-user-namespaces-afbe3308c082

 

 

'Linux & Kubernetes' 카테고리의 다른 글

Kubernetes Network  (0) 2021.06.23

댓글