본문 바로가기

컨테이너/Docker

[Docker] 컨테이너와 네임스페이스 격리

쿠버네티스를 공부할때 항상 도커를 먼저 공부해봐라. 라는 이야기를 들을때가 많다. 그 이유가 무엇일까라는 고민을 했었고,

그에 대한 내 생각을 리서치 결과를 토대로 적어보려고 한다. 내가 근삼이님의 블로그를 되게 자주 보는데 글을 너무 정성껏 쓰셔서 

읽다보면 나도 모르게 기분이 좋다. https://ykarma1996.tistory.com/192 

 

컨테이너의 구조와 오픈소스의 생태계에 관한 리서치(feat. 도커는 적폐인가?)

컨테이너 이미지의 빌드 및 배포에 관한 성능을 개선하기 위해 리서치를 하다보니, 혼자 알기에 너무 재밌는 배경들이 많아서 정리해 보기로 했다. 오늘은 컨테이너와 이미지의 구조 및 원리(특

ykarma1996.tistory.com

이번에도 컨테이너 기술에 대해서 mount namespace의 격리기능을 터미널에서 하나하나 명령어를 쳐보면서 독자를 이해시키는데 

감동스러웠다.

 

컨테이너의 등장


내가 컨테이너에 대해서 가장 먼저 공부했던 책이 이 책이다. 이 책의 구성은 앞단에서 컨테이너라는 녀석이 왜 생겨났는지 부터 도커 command를 일일히 쳐보면서 도커에 대해서 먼저 설명한다. 그리고, 도커가 단일 노드 구성에는 그럭저럭 사용 가능할지 모르지만, 규모가 기업단위로 광범위해졌을때는 컨테이너 관리 툴인 쿠버네티스를 반드시 사용해야 한다. 라고 스토리를 이어간다.

 

도커 그래서 무엇인데 ?

"도커는 컨테이너야" 

 

사실 도커는 단순하게 설명할수 있는 기술이 아니다. 컨테이너의 빌드, 실행, repo관리, 네트워크 관리 등등 컨테이너와 관련한 온갖 기술들을 다 일컬어서 docker라는 하나의 커멘드 라인만으로 사용자들이 컨테이너 세상에 쉽게 접근하고 관리할 수 있도록 만들어진 만능 도구이다.

 

혹은 그 툴을 만들어 낸 기업이기도 하다.

 

내가 아시는분이 면접볼때 항상 질문하시는게 컨테이너는 어떤 기술로 만들어졌는지 꼭 물어보시고 "커널" 이라는 단어가 나올때까지

노려보시는데 ㅋㅋㅋ 비전공자들한테는 커널이라는게 생소할수 있다. (나한테는 생소했다. )

사실, 간단하게 생각하면 커널은  물리장비와 소프트웨어를 연결해주는 인터페이스이다. 

 

조금 더 자세히 알아보자

커널은 CPU/MEM/네트워크등 물리 자원을 사용하기 위한 중간 다리 역할이라고만 알아두자. 여기서 커널에 대한 이야기를 하려는게 아니기 때문에 넘어가도록 하겠다.

 

서론이 길었는데, 본론은 컨테이너는 커널을 공유하며 하나의 프로세스로서 격리하는 기술을 사용한다.

기존의 Hypervisor 가상화 기술이 각각의 OS와 커널을 제공했던 반면, 컨테이너 기술은 커널을 공유하여 자원을 효율적으로 사용하지만 격리된 환경을 제공하여 각각의 컨테이너가 호스트에 영향을 주지 않기 때문에 비용 효율적이며 안전하다.

그럼 이 격리된 환경을 제공하지만 커널의 자원은 공유하여 효율적으로 사용하는 기술은 어떤걸까? 

그렇다 바로, 네임스페이스라는 기술이다. 

 

사실 최근에 도커가 2013년부터 급부상하면서 기술이 수면위로 올라왔지만, 20년도 더 된 기술이다.

컨테이너의 역사는 chroot로 부터 시작된다고 보는 견해가 많은데  앞서 존재했던 기술인 chroot로 생성한 격리공간에는 다음과 같은 단점이 있었다.

사실상 해커의 진입을 막기 위해 생겼던 chroot는 완벽하지는 않았다. 그래서 등장한 개념이 pivot_root이다.

pivot_root는 컨테이너를 위해 생긴 개념은 아니다. 처음에는 부팅 파일시스템과 루트 파일시스템의 마운트 포인트를 바꿔주는 용도로 

개발된 기능이다.

 

 root 파일시스템의 마운트 포인트를 다른 마운트 포인트와 바꿔치기해서 특정 디렉토리를 새로운 루트로 만들어주는 명령어이다.

근삼님 블로그 참고했습니다. 감사합니다.

 

하지만 이 기술을 컨테이너에서 사용하기 위해서는 호스트의 루트 파일 시스템에 영향을 주어서는 안되기 때문에 네임스페이스로 격리 하여

컨테이너만의 공간을 만들수 있는것이다. 마운트 네임스페이스를 설명할때 아래에서 조금 더 설명하도록 하겠다.

 

chroot의 단점
  1. 탈옥이 가능하다.
  2. 완전한 격리가 불가능하다. (Host의 filesystems, network 등에 접근 가능)
  3. root 권한 사용 가능하다.
  4. host의 자원을 무제한으로 사용할 수 있다.

 

namespace


리눅스의 네임스페이스 란?

 

네임 스페이스는 특정 프로세스에 대해 시스템 리소스를 논리적으로 격리하는 리눅스의 가상화 기술이다.

우리가 하도 많이 들어서 알고 있는 namespace는 아마 mount , cgroup, network 일 것이다. 나는 그렇다.

통상적으로 cgroup은 리소스 제한, mount는 파일시스템 격리정도로 알고 있었는데 이 부분을 근삼이님의 블로그의 테스트들을

따라해보면서 정확하게 이해하는데 도움이 되었다.

 

 

종류가 이렇게나 다양하다니! 

uts / ipc 는 처음 들었다..

간단하게 각각 네임스페이스의 기능을 들여다 보자.

 

mount

마운트가 무엇일까? 

  •  usb를 컴퓨터에 꽂은 행위(시스템콜) 이라고 이해한다.

파일 시스템은 무엇일까?

  • 리눅스는 모든 것이 파일 형태로 관리된다. 디바이스 , 네트워크 소켓, 커널의 정보, 프로세스 등 

 

파일 시스템 격리에는 마운트 네임스페이스와 pivot_root라는 개념을 활용한다.

pivot_root는 root 파일시스템의 마운트 포인트를 다른 마운트 포인트와 바꿔치기해서 특정 디렉토리를 새로운 루트로 만들어주는 명령어이다.
그런데 이 명령어를 그냥 사용해 버리면, 당연히 host os에 무시무시한 영향을 줘버린다. 이 기능을 host os에 영향 없이 격리된 환경에서 이용하기 위한 개념이 mount namespace다. (컨테이너의 다양한 설정에 대한 격리를 위해 여러 종류의 namespace들이 등장 했는데, 그 중 가장 먼저 개발 된 것이 mount ns인 이유이다.)


mount namespace는 프로세스들에 서로 다른 파일시스템 마운트 포인트를 제공하는 격리 기술이다.

즉 컨테이너에서 마운트 된 파일시스템은 호스트에서 보이지 않는다는 사실~! 

 

마운트 네임스페이스는 이렇게 정리해보자

 

  • 프로세스와 다른터미널에서의 프로세스가 각기 다른 마운트 네임스페이스를 가짐 ( 즉 격리 )
  • 기본적으로 모든 프로세스는 동일한 기본 네임스페이스를 공유
  • pivot_root를 통한 root 파일시스템의 마운트 포인트를 컨테이너의 마운트 포인트와 바꿔치기
  • 컨테이너의 독립적인 공간을 만들수 있음.

https://themapisto.tistory.com/263

 

[KANS] 쿠버네티스 네트워크 (2) 컨테이너 격리 & 네트워크 보안

cloudNet@ 팀의 가시다 님이 진행하는 쿠버네티스 네트워크 스터디 1주차 정리입니다.  ✅ 대주제도커 컨테이너의 격리 요약컨테이너는 독립된 리눅스 환경(pivot-root, namespace, Overlay filesystem, cgrou

themapisto.tistory.com

실습은 다음과 같이 따라해보시면 이해하기 수월합니다.

 

 

cgroup

 

# stress 툴 설치

$ apt-get install -y stress

$ grep cgroup /proc/filesystems

# 만약 시스템이 cgroupv2를 지원한다면, 다음과 같이 출력이 됩니다.
nodev	cgroup
nodev	cgroup2
# 만약 시스템이 cgroupv1만 지원한다면, 아래 출력에서 'cgroup2' 부분은 보이지 않습니다.


$ cat /proc/$$/cgroup
# 현재 쉘의 pid가 어떤 cgroup인지 확인해보자.


$ mkdir test_cgroup_parent && cd test_cgroup_parent
$ ls


$ echo "+cpu" >> /sys/fs/cgroup/test_cgroup_parent/cgroup.subtree_control

# 이제, cpu.max를 통해 제한을 걸어본다.
cpu.max의 첫 번쨰 값은 허용된 시간(마이크로초) 할당량임. 
이 시간에는 하위 그룹의 모든 프로세스를 전체적으로 실행할 수 있음. 두 번째 값은 총 기간 길이를 지정함.
이번 실습에서는 1000000마이크로초(1초) 중에 100000 마이크로초만 실행되게끔 제한을 걸어보자
(1/10만 실행하도록 설정)
현재 디렉토리는 부모 디렉토리이다.
optional) cpu.weight로는 테스크별 가중치를 조절할수도 있다.
cpu.weight 컨트롤러의 파일 값은 백분율이 아니라 절대값임.
예를 들어, task1이 100이고, task2가 200이라면, task2의 가중치가 두 배 더 높음


$ echo 100000 1000000 > /sys/fs/cgroup/test_cgroup_parent/cpu.max

 

  • 이제 test용 자식 디렉토리를 생성하고, pid를 추가하여 제한을 걸어본다.
    • pid는 자식 디렉토리에서 추가한다.
# 부모(test_cgroup_parent) 위치에서 자식 생성 && 자식으로 이동
mkdir test_cgroup_child && cd test_cgroup_child

# 현재 쉘의 pid를 cgroup.procs에 추가
# 현재 pid는 echo $$ 를 통해 알 수 있음
echo $$ > /sys/fs/cgroup/test_cgroup_parent/test_cgroup_child/cgroup.procs
stress -c 1

# 다른 쉘에서 사용량을 확인해본다.
top

# cpu가 10%만 사용중임을 확인할 수 있다.

이와같이  cgroup을 통해 CPU를 제한할수 있다.

 

network

 

CNI와 도커의 여러 기능들을 살펴보기 전에, 컨테이너 네트워킹을 가능케하는 핵심 기술에 대해서 이해하는 시간을 가져 봅시다.

 

리눅스 커널은 멀티테넌시를 제공하기 위한 여러가지 기능들을 가지고 있습니다. Namespace는 다양한 리소스의 격리를 위한 기능을 제공합니다. 그 중에서 네트워크 namespace는 네트워크 격리를 제공합니다.

 

네트워크 namespace를 사용하는 것은 굉장히 쉽습니다. 대부분의 리눅스에서 제공하는 ip 명령을 이용합니다. 아래와 같이 두개(client와 server)의 네트워크 namespace를 만들어 보겠습니다.

 

ip netns add client
ip netns add server
ip netns list
# server
# client

 

네트워크 네임스페이스는 default 네임스페이스에 만들어집니다. 

이와 같이 IP 할당해서 통신까지 해보겠습니다.

ip link add veth-client type veth peer name veth-server
ip link list | grep veth

클라이언트# ip netns exec client ip link
서버# ip netns exec server ip link

master# ip netns exec client ip address add 10.0.0.11/24 dev veth-client
master# ip netns exec client ip link set veth-client up
master# ip netns exec server ip address add 10.0.0.12/24 dev veth-server
master# ip netns exec server ip link set veth-server up


master# ip netns exec client ip addr
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
#    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# 5: veth-client@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
#    link/ether ca:e8:30:2e:f9:d2 brd ff:ff:ff:ff:ff:ff link-netnsid 1
#    inet 10.0.0.11/24 scope global veth-client
#       valid_lft forever preferred_lft forever
#    inet6 fe80::c8e8:30ff:fe2e:f9d2/64 scope link
#       valid_lft forever preferred_lft forever



master# ip netns exec server ip addr
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
#    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# 4: veth-server@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
#    link/ether 42:96:f0:ae:f0:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
#    inet 10.0.0.12/24 scope global veth-server
#       valid_lft forever preferred_lft forever
#    inet6 fe80::4096:f0ff:feae:f0c5/64 scope link
#       valid_lft forever preferred_lft forever

 

master# ip netns exec client ping 10.0.0.12
# PING 10.0.0.12 (10.0.0.12) 56(84) bytes of data.
# 64 bytes from 10.0.0.12: icmp_seq=1 ttl=64 time=0.101 ms
# 64 bytes from 10.0.0.12: icmp_seq=2 ttl=64 time=0.072 ms
# 64 bytes from 10.0.0.12: icmp_seq=3 ttl=64 time=0.084 ms
# 64 bytes from 10.0.0.12: icmp_seq=4 ttl=64 time=0.077 ms
# 64 bytes from 10.0.0.12: icmp_seq=5 ttl=64 time=0.079 ms

지금까지 두개의 서로 다른 네트워크 namespace를 연결해 보았습니다.

통신까지 잘되는군요.

하지만 여기에는 한계점이 분명합니다.

namespace가 두개 밖에 없는 경우에는 큰 문제가 아니지만 매번 네트워크 namespace가 늘어날때 마다 이런 방식을 사용하는 것은 확장성 관점에서 비효율적 이기 때문입니다.

namespace가 늘어나는 만큼 모든 namespace를 연결하기 위한 조합이 기하급수적으로 늘어나기 때문입니다.(n*(n-1)/2)

 

대신 리눅스 bridge를 만들어서 모든 네트워크 namespace들을 전부 이 bridge에 연결할 수 있습니다. 이것이 바로 도커가 같은 호스트에서 컨테이너 네트워크를 연결하는 방식입니다.

 

이번에는 브릿지에 네임스페이스들을 연결해 봅시다.

# All in one
BR=bridge1
HOST_IP=172.17.0.33
ip link add client1-veth type veth peer name client1-veth-br
ip link add server1-veth type veth peer name server1-veth-br
ip link add $BR type bridge
ip netns add client1
ip netns add server1
ip link set client1-veth netns client1
ip link set server1-veth netns server1
ip link set client1-veth-br master $BR
ip link set server1-veth-br master $BR
ip link set $BR up
ip link set client1-veth-br up
ip link set server1-veth-br up
ip netns exec client1 ip link set client1-veth up
ip netns exec server1 ip link set server1-veth up
ip netns exec client1 ip addr add 172.30.0.11/24 dev client1-veth
ip netns exec server1 ip addr add 172.30.0.12/24 dev server1-veth
ip netns exec client1 ping 172.30.0.12 -c 5
ip addr add 172.30.0.1/24 dev $BR
ip netns exec client1 ping 172.30.0.12 -c 5
ip netns exec client1 ping 172.30.0.1 -c 5


bridge를 이용한 방법도 동일하게 두개의 namespace가 연결된 것을 확인할 수 있습니다.

controlplane $ ip netns exec client1 ping 172.30.0.12 -c 5
# PING 172.30.0.12 (172.30.0.12) 56(84) bytes of data.
# 64 bytes from 172.30.0.12: icmp_seq=1 ttl=64 time=0.138 ms
# 64 bytes from 172.30.0.12: icmp_seq=2 ttl=64 time=0.091 ms
# 64 bytes from 172.30.0.12: icmp_seq=3 ttl=64 time=0.073 ms
# 64 bytes from 172.30.0.12: icmp_seq=4 ttl=64 time=0.070 ms
# 64 bytes from 172.30.0.12: icmp_seq=5 ttl=64 time=0.107 ms

client1 namespace에서 호스트로 ping을 날려 봅시다.

controlplane $ ip netns exec client1 ping $HOST_IP -c 2
# connect: Network is unreachable

Network is unreachable라고 나오는데요, 이것은 정상입니다. 왜냐하면 새롭게 생성한 namespace에는 라우팅 정보가 설정되어 있지 않기 때문입니다. 기본 라우팅 정보를 입력합니다.

# default G/W를 bridge로 향하게 합니다.
controlplane $ ip netns exec client1 ip route add default via 172.30.0.1
controlplane $ ip netns exec server1 ip route add default via 172.30.0.1
controlplane $ ip netns exec client1 ping $HOST_IP -c 5
# PING 172.17.0.23 (172.17.0.23) 56(84) bytes of data.
# 64 bytes from 172.17.0.23: icmp_seq=1 ttl=64 time=0.053 ms
# 64 bytes from 172.17.0.23: icmp_seq=2 ttl=64 time=0.121 ms
# 64 bytes from 172.17.0.23: icmp_seq=3 ttl=64 time=0.078 ms
# 64 bytes from 172.17.0.23: icmp_seq=4 ttl=64 time=0.129 ms
# 64 bytes from 172.17.0.23: icmp_seq=5 ttl=64 time=0.119 ms
# --- 172.17.0.23 ping statistics ---
# 5 packets transmitted, 5 received, 0% packet loss, time 3999ms
# rtt min/avg/max/mdev = 0.053/0.100/0.129/0.029 ms

외부로 나가는 기본 라우팅 정보를 bridge로 향하게 만들었습니다. 그렇기 때문에 이제 각 namespace들이 외부로 연결이 가능하게 되었습니다.

controlplane $ ping 8.8.8.8 -c 2
# PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
# 64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=3.40 ms
# 64 bytes from 8.8.8.8: icmp_seq=2 ttl=117 time=3.81 ms
# --- 8.8.8.8 ping statistics ---
# 2 packets transmitted, 2 received, 0% packet loss, time 1001ms
# rtt min/avg/max/mdev = 3.403/3.610/3.817/0.207 ms

 

특이점

기본적으로 Docker는 네임스페이스를 기본 default 위치에 생성하지 않기 때문에

  • ip netns list 명령으로 도커가 생성한 네트워크 ns 를 볼수 없습니다.
  •  docker가 생성한 네트워크 ns를 확인하기 위해서는 심볼릭 링크를 생성해야 합니다.

 

마무리 

 

자 여기까지, 3가지 네임스페이스를 통해 각각의 독립적인 공간을 컨테이너에게 제공해 줄수 있는 기술적 근간에 대해 

공부해봤습니다. 

 

컨테이너 기술에 대해서 깊이 있게 핵심 동작원리를 파악하는것이 앞으로 큰 도움이 될거라 생각합니다.