Tech story/Container
알아보기 1. Container Basic
kt cloud 테크블로그
2024. 10. 24. 15:37
[kt cloud Container개발팀 한상준 님]
알아보기 1. Container Basic
본 문서에서는 kubernetes 의 모태가 되는 Container 의
- 역사 소개로 서두를 놓고,
- Low Level(수준) 의 Container 기능 소개를 더불어,
- Hands on 가능한 예제와 함께 Container 기본 지식을 탐구 하고자 합니다.
Container 의 역사
연도 | 제목 | 내용 |
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 에 격리되었다는 것을 의미합니다.
감사합니다.