UTS 네임스페이스를 사용한 호스트네임 격리
컨테이너 네트워크 기초 1편

들어가며: 컨테이너 네트워크 기초

도커Docker와 같은 컨테이너 도구들은 리눅스 커널의 기능들을 활용해 컨테이너를 생성하고 관리합니다. 도커의 기본적인 사용법은 아래 튜토리얼에서 소개합니다.

리눅스 컨테이너는 하드웨어 가상화 없이 프로세스를 마치 다른 시스템에서 동작하는 것처럼 격리하는 기술들의 조합을 의미하며 아주 정확한 정의가 있는 것은 아닙니다. 예를 들어 chroot만 사용해도 초보적인 수준의 프로세스 격리가 가능합니다. 초기 컨테이너 기술에 많이 사용되던 LXC는 chroot on steroid라는 별명으로 불리기도 하였습니다. 프로세스 격리와 직접적인 관련은 없습니다만 도커에서는 효율적인 이미지 관리를 위해 AUFS나 OverlayFS와 같은 유니온 마운트 기술도 적극 사용하였습니다. 이와 더불어 프로세스 격리의 가장 핵심이 되는 기술 중 하나가 바로 리눅스 네임스페이스입니다.

이중에서도 네트워크 격리를 위해 사용되는 네임스페이스가 UTS 네임스페이스와 네트워크 네임스페이스입니다. UTS 네임스페이스를 통해서 컨테이너마다 호스트네임을 부여할 수 있습니다. 또한 이름에서도 유추할 수 있듯이 네트워크 네임스페이스를 통해 네트워크 인터페이스를 격리해줍니다. 실제로 도커 컨테이너는 고유한 호스트네임을 가지고 있으며, 가상 네트워크 인터페이스와 컨테이너만을 위한 내부 IP도 가지고 있습니다.

이 글에서는 먼저 UTS 네임스페이스로 호스트네임을 격리하는 방법을 알아봅니다. 그리고 다음 글에서는 네트워크 네임스페이스를 사용해 프로세스 독립적인 네트워크 환경을 구축하는 방법을 소개하고, 도커의 기본 네트워크 환경과 비슷한 브리지 네트워크 환경을 컨테이너 도구 없이 구성해봅니다.

44BITS 소식과 클라우드 뉴스를 전해드립니다. 지금 5,000명 이상의 구독자와 함께 하고 있습니다 📮

테스트 환경 구축 및 도커 컨테이너 맛보기

먼저 이 글에서 사용한 테스트 환경에서 대해서 소개하고자합니다. 이 글은 리눅스 우분투 배포판 18.04에서 테스트 되었습니다. 실제 작업 환경은 베이그런트와 ubuntu/bionic64 박스로 구축하였습니다. 베이그런트에 대한 더 자세한 내용은 아래 글을 참고해주세요.

다음 명령어로 도커를 설치합니다.

$ wget -qO- https://get.docker.com/ | sh
$ sudo usermod -aG docker vagrant
$ docker --version
Docker version 20.10.2, build 2291f61

먼저 도커로 컨테이너를 하나 만들어보겠습니다.

$ docker run --name nginx01 -d nginx:latest
$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS     NAMES
ab0dca5e8b96   nginx:latest   "/docker-entrypoint.…"   3 minutes ago   Up 3 minutes   80/tcp    nginx01

다음 명령어로 방금 실행한 컨테이너의 Hostname과 IP를 확인할 수 있습니다.

$ docker inspect -f='Hostname:{{.Config.Hostname}}, IP: {{.NetworkSettings.IPAddress}}' nginx01
Hostname:ab0dca5e8b96, IP: 172.17.0.3

호스트네임은 컨테이너ID와 같은 ab0dca5e8b96이 부여된 것을 알 수 있습니다. 또한 컨테이너 고유 IP는 172.17.0.3입니다.

호스트에서 ping을 보내보면 정상적으로 접근 가능한 것을 확인할 수 있습니다.

$ ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.052 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.037 ms
...

nginx 이미지은 기본적으로 80 포트에 Nginx 서버를 실행시켜줍니다. 호스트에서 curl로 접속해보면 서버도 정상적으로 동작중인 것을 알 수 있습니다.

$ curl 172.17.0.3
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

도커의 기본적인 사용법에 대해서 아시는 분이라면, 이 아이피로 호스트에서 접근은 가능하지만 외부 네트워크에 노출되어있지 않다는 것도 알고 있을 것입니다.

외부 네트워크 노출시키기 위해서는 호스트의 네트워크 인터페이스에 연결되어있어야합니다. 예를 들어서 -p <호스트포트>:<컨테이너포트> 옵션으로 컨테이너를 실행하고 호스트에서도 0.0.0.0:<호스트포트>으로 접근이 가능한 상태여야합니다. 지금은 별도로 -p 옵션은 사용하지 않았으므로, 호스트에서만 컨테이너의 IP로 접근이 가능한 상태라는 것을 알 수 있습니다.

컨테이너는 엄밀히 말하면 가상 머신이 아닌 프로세스입니다만, 프로세스 전용으로 호스트네임이 부여되어있고 IP도 별도로 지정되어있는 게 신기해보입니다. 어떤 마법이 숨어있는 걸까요? 그전에 앞서 만든 nginx01 컨테이너는 일단 삭제하겠습니다.

$ docker rm -f nginx01

호스트네임을 분리해주는 UTS 네임스페이스

사실 앞에서 이미 정답을 이야기했습니다만, 이 마법의 비밀은 바로 UTS 네임스페이스와 네트워크 네임스페이스입니다. 물론 실제로는 조금 더 복잡합니다만, 여기서는 간단한 예제로 이 기능들을 살펴보겠습니다. 먼저 UTS 네임스페이스부터 시작합니다.

리눅스에서는 unshare 명령어를 사용해 간단히 네임스페이스 격리 기능을 사용해볼 수 있습니다.

아주 간단한 예제이니, 리눅스 네임스페이스가 아직 익숙하지 않더라도 걱정하지 않아도 됩니다. 먼저 hostname 명령어로 현재 시스템의 호스트네임을 확인해봅니다. 호스트네임은 현재 네트워크 상에서 기기에 부여되는 고유한 이름입니다.

$ hostname
ubuntu-bionic

먼저 네임스페이스를 저장하기 위한 빈 파일을 하나 만들어줍니다. 조금 이상하게 들릴지 모르겠지만, unshare를 사용하면 이 빈 파일을 통해 네임스페이스가 영속화됩니다. 네임스페이스가 영속화되면, 이후에 해당 네임스페이스를 다시 재사용하는 것이 가능해집니다.

$ touch /tmp/utsns1

touch는 단순히 빈 파일을 생성해줍니다. 그럼 이번에는 unshare를 사용해 UTS 네임스페이스를 생성하고 utsns1이라는 호스트네임을 부여해보겠습니다.

$ unshare --uts=/tmp/utsns1 hostname utsns1

unshare의 사용법은 chrootdocker와 비슷합니다. unshare 뒤에는 옵션이 따라오고 마지막으로 실행하고자 하는 명령어를 지정해줍니다. 여기서는 hostname utsns1이 명령어입니다. 즉, 이 명령어는 /tmp/utsns1에 영속화된 네임스페이스에서 hostnameutsns1로 변경하는 명령을 실행하라는 의미입니다. 헷갈릴 수 있으니 다시 한 번 호스트의 hostname을 확인해봅니다.

$ hostname
ubuntu-bionic

이번에는 --uts=/tmp/utsns1에 영속화된 네임스페이스를 기반으로 호스트 이름을 확인해보겠습니다. 네임스페이스를 처음 만들 때는 unshare 명령어를 사용합니다만, 분리한 이후에는 nsenter 명령어를 사용해야합니다. 사용 방법은 비슷합니다.

$ nsenter --uts=/tmp/utsns1 hostname
utsns1

호스트네임이 달라진 것을 알 수 있습니다. 편의상 셸의 프롬프트를 생략하고 있습니다만, Bash의 기본 프롬프트 상에서도 호스트와 /tmp/utsns1 UTS네임스페이스 사이에 호스트네임이 달라지는 것을 확인할 수 있습니다.

root@ubuntu-bionic:~# nsenter --uts=/tmp/utsns1 bash
root@utsns1:~# hostname
utsns1
root@utsns1:~# exit
root@ubuntu-bionic:~#

단, 명령어 몇 개로 UTS 네임스페이스가 분리되었습니다. 여기서는 자세히 다루지는 않았습니다만, 이렇게 명시적으로 네임스페이스를 분리하지 않을 경우 모든 프로세스는 리눅스의 init 1번 프로세스의 네임스페이스를 공유해서 사용합니다. 즉, nsenter를 사용해 네임스페이스를 분리해서 실행한 프로세스가 매우 예외적으로 동작하고 있다는 의미입니다. 물론 컨테이너에서는 이게 기본입니다.

노트
정말로 네임스페이스가 분리되었나요?

네임스페이스를 분리한다는 게 어떤 의미인지 아직 잘 와닿지 않을 수 있습니다. 너무 간단해보여서 정말로 네임스페이스가 분리된 것조차도 확신이 가지 않을 수도 있습니다. 그럼 네임스페이스가 분리되었는지 확인하는 방법에 대해서 한 번 알아보겠습니다. 어떤 네임스페이스에서 실행중인지 보여드리기 위해 아래 예제에서는 셸 프롬프트를 전부 표시하였습니다.

root@ubuntu-bionic:~# echo $$
6063
root@ubuntu-bionic:~# nsenter --uts=/tmp/utsns1 bash
root@utsns1:~# echo $$
6229

여기서 출력한 $$은 현재 bash 셸의 프로세스 아이디입니다. 처음 셸은 일반적으로 실행된 프로세스입니다. nsenter로 실행한 bash 셸은 UTS네임스페이스가 분리된 프로세스입니다. 프로세스 아이디는 각각 6063, 6229인 것을 확인할 수 있습니다.

리눅스에서는 /prod/<PID> 디렉터리에서 프로세스와 관련된 정보를 확인할 수 있습니다. 특히 이 디렉터리 아래의 ns 디렉터리에는 네임스페이스와 관련된 내용이 포함되어있습니다. 먼저 6063 프로세스의 ns 디렉터리로 이동하고, uts 파일을 확인해봅니다.

root@utsns1:~# cd /proc/6063/ns
root@utsns1:/proc/6063/ns# ls -al uts
lrwxrwxrwx 1 root root 0 Jan 24 06:19 uts -> 'uts:[4026531838]'

대괄호 사이의 값이 네임스페이스의 고유 번호입니다. UTS 네임스페이스의 고유값은 4026531838입니다. 앞에서 기본적으로 모든 프로세스는 init 프로세스의 네임스페이스를 공유한다고 이야기하였습니다. 정말 그런지 1번 프로세스의 UTS 네임스페이스 고유값도 확인해보겠습니다.

root@utsns1:/proc/6063/ns# cd /proc/1/ns
root@utsns1:/proc/1/ns# ls -al uts
lrwxrwxrwx 1 root root 0 Jan 24 06:24 uts -> 'uts:[4026531838]'

6063의 고유값과 똑같습니다. 이번에는 UTS네임스페이스를 분리해서 실행한 6229 프로세스로 가봅니다.

root@utsns1:/proc/1/ns# cd /proc/6229/ns
root@utsns1:/proc/6229/ns# ls -al uts
lrwxrwxrwx 1 root root 0 Jan 24 06:25 uts -> 'uts:[4026532230]'

고유값이 다릅니다. 이걸로 UTS 네임스페이스가 실제로 격리되어있다는 것을 확인할 수 있습니다.

마치며

44BITS에서는 UTS 네임스페이스 외에도 PID 네임스페이스를 다룬 적이 있으니, 좀 더 자세한 내용은 다음 글을 참고해주세요.

컨테이너 네트워크에서 UTS 네임스페이스보다 중요하고 조금 더 어려운 게 바로 네트워크 네임스페이스입니다. 다음 글에서는 네트워크 네임스페이스의 기초와 리눅스의 네트워크 관련 기능들을 활용해 도커에서 실제로 사용하는 브리지 네트워크와 유사한 컨테이너의 네트워크 환경을 직집 구축해보도록 하겠습니다.