Tech story/Container

알아보기 1. Container Basic

kt cloud 테크블로그 2024. 10. 24. 15:37

[kt cloud Container개발팀 한상준 님]

 

알아보기 1. Container Basic 

 

본 문서에서는 kubernetes 의 모태가 되는 Container 의

  1. 역사 소개로 서두를 놓고,
  2. Low Level(수준) 의 Container 기능 소개를 더불어,
  3. Hands on 가능한 예제와 함께 Container 기본 지식을 탐구 하고자 합니다. 

 

Container 의 역사

 

[출처 openmaru]

 

 
 
연도 제목 내용
1979
chroot chroot 로 directory 격리
2002
mount namespace mount point 격리
2006
uts, ipc namespace hostname, Pipe, Socket, Shared Memory 격리
2009
net namespace network 격리
2012
user namespace user 격리
2013
docker namespaces (PID, USER MNT, UTS,IPC, NET), Cgroup 격리
2014
kubernetes Self Healing, Service Discovery
2015
openshift (OCP) Container Ochestration + PaaS 기능
 

 chroot 부터 Namespaces 까지 


chroot 를 사용한 초창기 Container

  • chroot [change root] 란 무엇일까? 실습을 통해 알아보겠습니다.

 

적당한 directory (여기에서는 tmp) 에서 anonther_root 의 이름으로 실습을 위한 Directory 를 생성합니다.

 

another_root 상위에서 ldd (dependency check) 명령어를 실행하여 다음의 program 들이 동작하기 위한 dependency (엮인 program) 를 알아보겠습니다.

  • bash program 이 실행되기 위한 모든 의존성
  • ls program 이 실행되기 위한 모든 의존성

  • bash 가 보유한 dependency 중 한 예로 libselinux.so.1 file 은 다음의 directory 에 위치하고 있음을 알 수 있습니다.
    • /lib/x86_64-linux-gnu/libselinux.so.1
    • another_root 상위에 똑같은 형상의 directory 를 생성하고 dependency 를 복사합니다.
    • /tmp/another_root/lib/x86_64-linux-gnu/libselinux.so.1

  • 다른 나머지 bash 의 dependecy 들 도 위와 동일한 방법으로 복사합니다.
  • ls program 도 동일한 방법으로 복사합니다.
  • 모든 복사가 완료된 후 tree 명령어를 통해 another_root 상위의 모든 file 들을 검색해 보겠습니다.

 

  • 이제 another_root directory 에는 bin, lib, lib64 등의 형상으로 bash, ls program 의 dependency 들이 모여졌습니다.
  • chroot 명령어를 실행하여 /tmp/another_root/bin/bash 와 ls program 의 root directory 를 /tmp/another_root directory 상위로 변경합니다.
  • ls 명령어를 실행해 보면 prompt 가 변경되며 현재 directory 가 최 상위(root) directory 인 마냥 상위 directory 의 연결 고리가 보이지 않게 됩니다.
    • root@hsj-edu-container01:/tmp/another_root# ===> bash-5.1#
    • bash-5.1#ls ===> bin lib lib64

 

 

 

  • chroot 의 동작 방식을 위의 그림으로 이해해 보겠습니다.
  • 빨간 표시의 화살표 영역은 실제 root directory 의 file-system 구조 입니다.
  • 실습은 /root/tmp 상위에서 another_root directory 를 생성하고 그 상위에 bin, ls program 의 실제 경로 및 파일 자체를 동일 형상으로 복사 후 chroot 를 적용하였습니다. 
  • 노란색 표시의 화살표 영역은 /bin/bash, ls 명령어 입장에서 another_root directory 가 root directory 로 변경된 영역입니다.
  • chroot 가 적용된 bash, ls program 은 상위 directory 인 /tmp directory 를 볼 수 없습니다. (Unview) . 즉 chroot  실습을 통해 알 수 있게 된 사실은 다음과 같습니다.
    • chroot 는 program 관점에서 root directory 를 변경된 directory 로 인식(?) 하게 하는 기능입니다.
    • 즉, chroot 가 적용된 특정 Program 은 실제 /root 하위의 특정 directory 로 공간적 격리가 됩니다.
    • 좀 더 자세히 마화다녕 chroot 를 통해 특정 directory 로 격리된 Program 은 실제 /root 상위의 그 어떤 Program 이나 directory 도 접근 하지 못 합니다.

 

Linux Namespaces 의 등장과 Container의 진화

  • Linux 의 Namespace 란 Process 를 실행할 때 System 의 Resource 를 분리해서 실행 할 수 있도록 도와주는 기능입니다.
  • 현재 지원되고 있는 Namespace (분리 될 수 있는 System Resource) 의 종류는 다음과 같습니다.
    • Mount (Mount Point): process 별 Mount 되는 File System 을 격리
    • UTS (Unix Time-Sharering): Host Name 이나 Domain Name 을 격리
    • IPC (Inter Process Communication): IPC Resource (Pipe, Socket, Shared Memory 등) 를 격리하여, 다른 프로세스의 접근이나 제어를 방지
    • PID (Process Id): Process Id 를 격리하여, Namespace 외 다른 Process 들은 접근이 불가
    • Cgroups (Control Groups): Process 는 /proc/[PID]/cgroup 에 가상 화 된 새로운 Cgroup Mount 를 보유
    • Net (Network): IP, Port, Routing Table 등 Network Resource 를 격리
    • User: 프로세스 별 UID, GID 정보를 격리
    • Time: 시간을 격리

Namespace 의 주요 특징을 알아보겠습니다.

  • Namespace 는 Namespace 에 소속되어 있는 Process 가 존재하지 않는 경우 Linux Kernel 에 의해서 자동으로 제거됩니다.
  • 즉 Namespace 가 생성되기 위해서는 Namespace 에 소속되어 있는 Process 가 반드시 존재해야 합니다.
  • Namespace 와 관련된 System Call 들은 모두 Process 와 연관되어 있습니다.
  • 모든 Process 들은 반드시 모든 Namespace Type (MNT, UTS, IPC, PID, USER, NET) 의 특정 Namespace 에 소속되어야 합니다.
  • Container 의 Process 뿐만 아니라 Host 의 Process 들 도 Host 의 Namespace 에 소속되어 동작합니다.
  • fork() 또는 clone() System Call 을 이용하여 Child Process 를 생성하는 경우, Namespace 관련 설정을 적용하지 않는 이상 기본적으로 Child Process 는 Parent Process 가 소속 된 Namespace 를 상속 받아 그대로 이용합니다.
  • 이러한 상속 특성 때문에 기본적으로 Host 의 Process 들은 Host 의 Namespace 에 소속되고, 각 Container 의 Process 들은 각 Container 의 Namespace 에 소속되어 동작합니다.

  • 위의 그림은 2개의 Process 들 의 모든 Type (Cgroup, IPC, Mount, Net, PID, User, UTS 등) Namespace 를 확인 / 비교 한 것 입니다.
  • Process 가 소속되어 있는 Namespace 는 /proc/[PID]/ns Directory 에 Symbolic Link 로 존재 합니다.
  • 각 Namespace 별로 Symbolic Link 가 존재하며 'cgorup:[4026531835]' 와 같은 숫자는 Symbolic Link 의 Inode Number 를 의미 합니다.
  • 위의 그림에서 /proc/[PID]/ns Directory 에 IPC, Mount, Network, PID, User, UTS Namespace 를 확인 할 수 있습니다.
  • /proc/1071/ns 와 /proc/1086/ns 의 비교에서 모든 Type 의 Symbolic Link 가 동일한 Inode Number 를 보유합니다면, 두 Process 는 모든 Type 의 Host Namespace 들 에 속해 있습니다는 것을 의미 합니다.

 

  • 위의 그림은 Host Mount Namespace 에 속해 있는 Process 와 격리 된 Mount Namespace 에 속해 있는 Process 의 모든 Type Namespace 를 확인 /비교 한 것 입니다.
  • PID 2635 Process 는 unshare -m /bin/bash 를 통해 Host Mount Namespace 에서 격리되어 새로운 Mount Namespace 에 속하게 됩니다.
  • PID 2552 Process 는 Host Mount Namespace 에 속해 있습니다.
  • 이 두 Process 의 Mount Symbolic Link 에 대한 Inode Number 가 각각 ['4026532339'] 와 ['4026531840'] 로 서로 다른 것을 알 수 있습니다.

Namespace 관련 System Call 종류와 특징을 알아보겠습니다.

  • clone()
    • Process 를 생성하는 fork() System Call 의 확장 판 입니다.
    • CLONE_NEW* Option 으로 Namespace 관련 설정을 진행하고 clone() System Call 을 호출하면 (Child) Rrocess 와 새로운 Namespace 가 생성되며, Process 는 해당 Namespace 에 소속됩니다.
    • docker 와 같은 Container Runtime 은 새로운 Container 를 생성 할 시 clone() System Call 을 이용합니다.
    • 그렇게 함으로써 Container 가 이용하는 Namespace 와 Container 의 Init Process 를 동시에 생성합니다.
  • unshare()
    • unshare() System Call 을 호출하면 새로운 Namespace 가 생성되고, unshare() System Call 을 호출한 Process 는 (호출 한 Process 자체가) 새로 생성된 Namespace 에 소속 됩니다.
  • setns()
    • setns() System Call 을 호출하는 Process 는 setns() System Call Parameter 를 통해서 지정하는 다른 Namespace 에 소속 됩니다.
    • Host 에서 Docker Container 내부에서 명령어를 실행 할 때 이용하는 명령어로 docker exec 가 있습니다.
    • docker exec 는 setns() System Call 을 이용하여 Process 를 Docker Container 의 Namespace 에서 동작 시킵니다.

 

Mount Namespace 란 무엇일까? 실습을 통해 알아보겠습니다.

  • 적당한 directory (여기에서는 tmp) 에서 mnt_ns 의 이름으로 실습을 위한 Directory 를 생성합니다.
  • Linux Namespace 구현 시 unshare(), clone(), setns() System Call 이 사용되며, 다음의 Flag 에 따라 원하는 System Resource 를 격리 시킬 수 있습니다.
    • Mount Namespace : CLONE_NEWNS
    • UTS Namespace : CLONE_NEWUTS
    • IPC Namespace : CLONE_NEWIPC
    • PID Namespace : CLONE_NEWPID
    • User Namespace : CLONE_NEWUSER
    • Network Namespace : CLONE_NEWNET

  • unshare 명령어를 사용해서 새로운 mount namespace 를 생성 후 bash program 을 실행 시킵니다.
  • "-m" 옵션은 mount 격리를 의미합니다. 

  • 새로 생성된 Mount Namespace 상에서 실습을 위해 만들어 놓은 /tmp/mnt_ns directory 를 Mount 합니다.

 

  • mount 완료 된 mnt_ns directory 상에 test file 을 하나 생성 합니다.
  • 생성된 test file 내용을 Read 합니다.

 

  • test file 을 생성한 대상은 새로 생성한 Mount Namespace 입니다.
  • exit 명령어를 통해 현재 Namespace 에서 나갑니다.
  • exit 이후 새로운 Mount Namespace 에서 기본 (기존 Host의 기본) Mount Namespace 로 변경 되었습니다.
  • 이전에 test 와 동일하게 /tmp/mnt_ns 상의 test file 이 존재하는 check 합니다.

 

  • unshare 를 통한 Mount Namespace 격리의 동작 방식을 위의 그림으로 이해해 보겠습니다.
  • unshare -m /bin/bash 를 실행하면 unshare() System Call 을 호출한 Process (/bin/bash) 는 기본 Mount Namespace 상의 Mount-Tree 의 사본을 가져옵니다.
  • unshare() System Call 을 호출한 Process 는 새로 생성된 Mount Namespace 에 속하게 되며, /tmp/mnt_ns mount 동작은 새로 생성된 Mount Namespace 에서 작동됩니다. 
  • mount 된 directory 에 생성한 test file 은 격리된 Mount Namespace 로 인해 해당 Process 가 속한 File System 상에만 존재하게 됩니다.
  • 즉, 격리된 Namespace 상에 Mount 된 File System 은 다른 Namespace 와 격리 됩니다.

UTS Namespace 란 무엇일까? 실습을 통해 알아보겠습니다.

  • hostname 명령어를 통해 Host Name 을 조회합니다.

 

 

  • unshare 명령어를 사용해서 새로운 UTS Namespace 를 생성 후 bash program 을 실행 시킵니다.
  • "-u" 옵션은 uts (Host Name, Dns Name) 격리를 의미합니다. 
  • hostname 명령어를 통해 Host Name 을 다시 조회합니다.

 

  • exit 명령어를 통해 현재 Namespace 에서 나갑니다.
  • exit 이후 새로운 UTS Namespace 에서 기본 (기존 Host의 기본) UTS Namespace 로 변경되었습니다.
  • 이전에 test 와 동일하게 hostname 명령어를 통해 Host Name 을 check 합니다.

 

  • unshare 를 통한 UTS Namespace 격리의 동작 방식을 위의 그림으로 이해해 보겠습니다.
  • unshare -u /bin/bash 를 실행하면 unshare() system call 을 호출한 Process (/bin/bash) 는 새로 생성된 UTS Namespace 에 속하게 됩니다.
  • Host Name 변경은 격리된 UTS Namespace 로 인해 unshare() System Call 을 호출한 Process 가 속한 Namespace 상에서만 작동됩니다.
  • 즉, 격리된 Namespace 상에 변경된 Host Name (Domain Name 도 동일) 은 다른 Namespace 와 분리 됩니다.

 

IPC Namespace 란 무엇일까? 실습을 통해 알아보겠습니다.

 

 

  • unshare 명령어를 사용해서 새로운 IPC Namespace 를 생성 후 bash program 을 실행시킨다.
  • "--ipc" 옵션은 IPC (Pipe, Socket, Shared Memory 등) 격리를 의미합니다. 
  • ipcmk -M 2000 명령어를 통해 2000 bytes 크기의 Shared Memory 를 생성합니다.
  • ipcs -m 명령어로 생성된 Shared Memory 를 조회합니다.

 

  • exit 명령어를 통해 현재 Namespace 에서 나갑니다.
  • exit 이후 새로운 IPC Namespace 에서 기본 (기존 Host의 기본) IPC Namespace 로 변경되었습니다.
  • 이전에 test 와 동일하게 ipcs -m 명령어로 Shared Memory 가 존재하는지 check 합니다.

 

 

  • unshare 를 통한 IPC Namespace 격리의 동작 방식을 위의 그림으로 이해해 보겠습니다.
  • unshare --ipc /bin/bash 를 실행하면 unshare() System Call 을 호출한 Process (/bin/bash) 는 새로 생성된 IPC Namespace 에 속하게 됩니다.
  • 즉, 격리된 Namespace 상에 생성된 Shared Memory 는 다른 Namespace 에서 볼 수 없습니다.

 

 

PID Namespace 란 무엇일까? 실습을 통해 알아보겠습니다.

  • echo 를 실행하여 현재 Process (bash) 의 Id 를 조회 합니다.
  • unshare --pid --fork --mount-proc /bin/bash 를 통해 PID Namespace 를 격리 및 /bin/bash 를 실행 합니다.
  • --pid : 새로운 PID Namespace 를 생성 합니다.
  • --fork : 실행하는 Process 를 fork 하여 Pid 1로 지정 합니다.
  • --mount-proc : 새로 생성되는 PID Namespace 내에서 내부 Process 정보를 보기 위해 Host 와 격리된 /proc File System 가 필요하며, 이를 Mount 해줍니다.
  • ps 를 실행하여 현재 Process (bash) 의 Id 를 조회 합니다.
  • 현 Namespace 에서 top Program 을 Background 로 실행합니다.
  • ps 를 실행하여 Process (top) 의 Id 를 조회 합니다.

  • Process (top) 의 Id 를 조회 합니다.

  • Terminal 을 하나 더 Open 하여 ps program 을 실행합니다.
  • unshare, /bin/bash, top Process 의 Id 를 확인합니다.

  • Process (top) 의 Id 를 조회 합니다.

  • unshare 를 통한 PID Namespace 격리의 동작 방식을 위의 그림으로 이해해 보겠습니다.
  • unshare --pid --fork --mount-proc /bin/bash 를 실행하면 unshare() System Call 을 호출한 Process (/bin/bash) 는 Host PID Namespace 와 새로 생성된 PID Namespace 에 속하게 됩니다.
  • 다시 말해서 위의 그림 중 PID 2560 /bin/bash Process 는 Host PID Namespace 에도 속하며 새로 생성된 PID Namespace 상에서 Init Process (PID 1) 로 동작하게 됩니다.
  • 즉, 중첩된 PID Namespace 로 인하여 각각의 PID Namespace 상에서 Process (top) 의 Inode Number 를 비교해보면 ['4026532341'] 로 동일한 것을 알 수 있습니다. 

Network Namespace 란 무엇일까? 실습을 통해 알아보겠습니다.

 

  • ip 명령어를 사용하여 가상 bridge 를 생성합니다.
    • type : Device Type 을 말합니다. 여기에서는 bridge type 의 Device 입니다.
  • 생성된 brigde 에 10.201.0.1/24 IP를 할당하고 10.201.0.255 를 Broadcast IP 로 지정합니다.

  • netns Option 을 사용하여 ns0, ns1 이름으로 두 개의 Network Namespace 를 생성합니다.

 

  • veth0 (peer veth1), veth2 (peer veth3) 이름으로 가상 eth Device 를 생성합니다.
    • type 이 veth 인 Device 를 설정하면 송, 수신이 가능한 한 쌍의 양 방향 Link가 생성됩니다.
    • type 이 veth 인 Device 는 Pair 구조로 되어 있으며, 각각의 링크는 Host Network Namespace 와 새로 생성된 Namespace 에 연결 할 수 있습니다.
  • veth0, veth2 의 Link 를 bridge br0 에 연결하고 veth1, veth3 은 각각 ns0, ns1 에 연결합니다.
  • Network Namespace ns0, ns1 에 각각 10.201.0.2/24, 10.201.0.3/24 의 IP 를 할당합니다.
    • netns : setns() System Call 을 사용하여 특정 Namespace 로 연결합니다.
    • exec : setns() System call 로 연결된 Namespace 내에서 명령어를 수행합니다.

 

  • Host 에서 ns0 Network Namespace 로 ping 을 실행하여 Host → Network Namespace 간 통신이 잘 되는지 확인합니다.

  • ns0 Network Namespace 내에서 Background 로 top 명령어를 실행합니다.

  • Terminal 창을 하나 더 Open 하여 ns1 Network Namespace 내에서 Background 로 top 명령어를 실행합니다.

  • Host 그리고 ns0, ns1 Network Namespace 의 Inode Number 를 비교합니다.

 

  • ip 명령어를 통한 Network Namespace 격리의 동작 방식을 위의 그림으로 이해해 보겠습니다.
    • 그림에서 표시된 것처럼 Host Network Namespace 의 Inode Number 는 [4026531992] 입니다.
    • 가상 Bridge Device 와 두 개의 Network Namespace 를 생성해 주었고 각각 IP 10.201.0.2~3 을 할당 했습니다.
    • 가상 ethernet Device 는 pair 구조로 송, 수신이 동시에 가능한 양방향 링크 입니다.
    • veth 의 특징으로 Host Network Namespace 와 새로운 Network Namespace 간 연결이 가능 합니다.
    • Host - New NS 의 연결 시 Host 상에서는 veth0, veth2 만 노출되며, veth1, veth3 은 각각의 Namespace (ns0, ns1) 내에서만 노출 됩니다.
    • Network Namespace ns0, ns1 내에서 top 명령어를 실행 시킵니다.
    • 이렇게 하면 top Process 는 새로운 Network Namespace 에서 동작하게 되며, Host 상의 PID 를 활용하여, ns0, ns1 의 Inode Number [4026532344], [4026532403] 를 조회 할 수 있습니다.
    • Host 와 ns0, ns1 의 Process (차례대로 bash[$$], top[2788], top[2793]) 는 서로 다른 Inode Number 로 할당된 Network Namespace 에 속하게 되며, 이는 서로 다른 Network Namespace 에 격리되었다는 것을 의미합니다.

감사합니다.