✍ Posted by Immersive Builder Seong
1. 리눅스 프로세스
프로세스란?
프로세스는 실행 중인 프로그램의 인스턴스를 의미하며, 각 프로세스마다 고유한 PID를 가집니다.
프로세스는 CPU와 메모리를 사용하는 기본 단위로, OS 커널(Cgroup)에서 프로세스의 자원을 관리합니다.
* 주요 명령어
ps aux / ps -ef | 프로세스 정보 |
pstree -apnTZ | 프로세스 트리 |
top / htop | 실시간 프로세스 정보 |
pgrep -h | 특정 프로세스 정보 |
/Proc 디렉토리
리눅스의 /proc 디렉토리는 커널이 동적으로 생성하는 정보를 실시간을 제공합니다.
하위 디렉토리별로 생성하는 상세 정보는 아래와 같습니다.
/proc/cpuinfo | CPU 모델, 코어 수, 클럭 속도 등 CPU 관련 정보 |
/proc/meminfo | 전체 메모리, 사용 중인 메모리, 가용 메모리, 캐시 메모리 등 메모리 관련 정보 |
/proc/uptime | 시스템이 부팅된 후 경과된 시간 (총 가동시간, 시스템 유휴 시간) |
/proc/loadavg | 시스템의 현재 부하 상태 |
/proc/version | 커널 버전, GCC 버전 및 컴파일된 날짜 등 커널 관련 빌드 정보 |
/proc/filesystems | 커널이 인식하고 있는 파일 시스템의 목록 |
/proc/partitions | 디스크 장치와 해당 파티션 크기 등 시스템에서 인식된 파티션 정보 |
* 프로세스별 정보
/proc/[PID]/cmdline | 해당 프로세스를 실행할 때 사용된 명령어와 인자 |
/proc/[PID]/cwd | 프로세스의 현재 작업 디렉터리에 대한 심볼릭 링크 |
/proc/[PID]/environ | 프로세스의 환경 변수(NULL 문자로 구분) |
/proc/[PID]/exe | 프로세스가 실행 중인 실행 파일에 대한 심볼릭 링크 |
/proc/[PID]/fd | 프로세스가 열어놓은 모든 파일 디스크립터에 대한 심볼릭 링크 |
/proc/[PID]/maps | 메모리 영역의 시작과 끝 주소, 접근 권한, 매핑된 파일 등 프로세스의 메모리 맵 |
/proc/[PID]/stat | 프로세스의 상태, CPU 사용량, 메모리 사용량, 부모 프로세스 ID, 우선순위 등 상태 정보 |
/proc/[PID]/status | PID, PPID, 메모리 사용량, CPU 사용률, 스레드 수 등 사람이 읽기 쉽게 정리한 파일 |
2. 도커
도커란?
도커는 컨테이너를 실행하거나 컨테이너 이미지를 만들고 배포하는 플랫폼입니다.
여기서 컨테이너는 운영 체제에서 실행되는 프로세스이고, 컨테이너 이미지를 실행함으로써 동작합니다.
- Containerized Process
- 도커 플랫폼이 설치된 환경 어디서나 컨테이너를 실행 가능함(온프레미스/클라우드)
- 단, 운영 체제 버전의 차이와 호환성에 따라 동작하지 않을 수 있음
컨테이너 vs 가상 머신
가상 머신은 서버에 하이퍼바이저를 설치하고, 그 위에 게스트 OS와 패키징한 VM을 만들어 실행하는 방식인 HW 레벨의 가상화를 지원합니다. 개별 가상 머신은 독립된 OS를 사용하여 컨테이너에 비해 고립성은 좋지만 오버헤드가 크고 느리다는 단점을 가집니다.
반면, 컨테이너는 OS를 제외한 나머지 어플리케이션 실행에 필요한 모든 파일을 패키징한다는 점에서 OS 레벨의 가상화를 지원합니다. 컨테이너는 호스트 커널을 공유하며 게스트 OS와 하이퍼바이저가 없기 때문에 가볍고 빠르게 프로세스를 실행할 수 있고 컨테이너에 대한 복제와 배포가 용이합니다.
도커 아키텍처
도커 클라이언트와 호스트 간에는 일반적으로 소켓 통신을 합니다. 클라이언트에서 호스트로 요청을 보내 컨테이너 이미지를 빌드하거나 이미지 기반의 컨테이너를 실행합니다. 호스트에 이미지가 없는 경우에는 퍼블릭 또는 프라이빗 저장소에서 이미지를 가져와 컨테이너를 실행합니다.
도커 설치
# 도커 설치
$ sudo su -
$ curl -fsSL https://get.docker.com | sh
# 도커 설치 확인
$ docker info
$ docker version
# 도커 서비스 상태 확인
$ systemctl status docker -l --no-pager
* 도커 기본정보 확인
- docker0 인터페이스 추가됨
- IPTABLES RULE
-- FORWARD ACCEPT => DROP으로 변경됨
-- docker0 → docker0 / 외부 전달 허용 정책이 추가됨
- - POSTROUTING: 외부로 전달 시 마스커레이딩(SNAT) 정책이 추가됨
Manage Docker as a non-root user
도커 데몬은 TCP/IP 통신이 아닌 유닉스 소켓 통신을 합니다. 유닉스 소켓(UDS)은 OS 커널에 구현되어 있는 프로토콜 요소에 대한 추상화된 인터페이스로서 장치 파일의 일종입니다. 일반적으로 로컬 시스템 내에서 실행되는 프로세스들 간의 통신을 구현하기 위해 사용되며, TCP/IP 대비 데이터 크기에 따른 처리 성능이 좋습니다. 예시로 mysql 서버에 로컬로 접속하는 경우 또는 Istio Proxy와 Envoy 프로세스 간 로컬 통신하는 경우에 사용됩니다.
기본적으로 루트 계정이 유닉스 소켓을 소유하고 있으며, 일반 계정은 sudo 권한을 통해서 소켓에 접근할 수 있습니다.
도커 데몬은 항상 루트 계정으로만 실행됩니다.
일반 계정으로 도커 서버의 정보를 조회할 시 소켓에 접근 권한이 없으므로 조회에 실패합니다.
따라서 일반 계정으로 도커를 사용하기 위해서는 도커 설치 시 자동으로 생성되는 docker 그룹에 계정을 포함시켜주어야 합니다.
일반 계정을 docker 그룹에 포함시킨 후, 권한을 얻은 상태에서 일반 계정으로 컨테이너를 실행하면 됩니다.
Docker Out of Docker
Docker Out of Docker(DooD)란 컨테이너 내부에서 외부의 도커 데몬을 사용하는 기술적 접근 방식을 말합니다.
CI/CD 파이프라인에서 빌드/테스트/배포 작업을 컨테이너화하여 격리된 환경에서 실행하는 것이 일반적입니다. 이러한 작업 중에 이미지를 빌드하거나 컨테이너를 실행해야 하는 경우 컨테이너 내부에서 도커 명령어를 실행하여야 할 수 있습니다.
이 때, 도커 호스트의 소켓(/var/run/docker.sock)을 컨테이너 내부로 마운트하면 컨테이너 내부의 도커 클라이언트가 호스트의 도커 데몬과 통신할 수 있습니다.
Jenkins 서버를 컨테이너로 띄우고 컨테이너 내부에서 도커 명령어를 실행해보겠습니다.
다음은 도커 호스트의 소켓(/var/run/docker.sock)과 도커 클라이언트의 실행 파일 경로(/usr/bin/docker)를 Jenkins 컨테이너 내부로 마운트하여 실행한 모습입니다.
Jenkins 컨테이너에서 루트 계정(--user 0)으로 docker run 명령어를 실행해보면 문제없이 동작함을 확인할 수 있습니다.
CPU 아키텍처와 호환성
컨테이너는 운영 체제를 공유하기 때문에 운영 체제 버전이나 CPU 아키텍처에 따라 호환되지 않을 수 있습니다.
예시로, CPU 아키텍처가 x86_64인 호스트에서 우분투 armv8_64 이미지를 실행하면 어떻게 될까요?
CPU 아키텍처가 서로 다르므로 컨테이너 실행에 실패합니다.
즉, 호스트와 CPU 아키텍처가 다른 컨테이너 이미지는 동작하지 않습니다.
3. 컨테이너 격리
컨테이너 격리 기술은 리눅스의 커널 기능을 시초로 오랜 세월 발전을 거듭하여 현재의 도커 컨테이너와 쿠버네티스 네임스페이스 등으로 완성되었습니다.
Chroot + 탈옥
chroot(change root)는 특정 프로세스와 자식 프로세스들이 지정한 디렉토리를 루트 디렉토리(/)로 간주하도록 변경하는 기능입니다.
* chroot 사용법 : chroot [-option] [New_Root] [Command]
=> New_Root 로 루트 디렉토리 변경하고 Command 수행
myroot 디렉토리를 생성하고 하위에 필요한 명령어 패키지(실행 파일 모음- sh, ls)를 복사합니다.
그리고 chroot 명령어를 사용하여 루트 디렉토리로 변경한 후 sh, ls 명령어를 실행합니다.
하위 목록이 다른 것으로 보아 sh 프로세스의 루트 디렉토리가 변경되었음을 알 수 있습니다.
그럼 상위 디렉토리로 이동이 될까요? chroot 디렉토리 내에서는 단순 명령어를 통해 탈옥이 안됩니다.
이번에는 ps 명령어 패키지를 복사하고 chroot 디렉토리에서 ps 명령어를 실행합니다.
왜 ps 명령어가 실행되지 않을까요?
그 이유는 /proc 디렉토리를 마운트하지 않아 프로세스의 정보를 가져오지 못하기 때문입니다.
마운트를 위한 mount, mkdir 명령어 패키지를 복사합니다.
마운트 포인트를 생성하고 마운트합니다. 그리고 다시 ps 명령어를 실행합니다.
이렇게 chroot 디렉토리를 사용하여 격리 환경을 구성할 시에는 필요한 명령어 패키지와 디렉토리 경로를 모아서 구성합니다. 그렇다면 해당 격리 환경에 컨테이너 이미지를 내려받아서 실행할 수 있는지도 테스트해보겠습니다.
Nginx 이미지를 압축 해제하여 chroot 디렉토리에 위치시킵니다.
chroot 디렉토리에 접근하여 Nginx 서비스를 실행합니다.
호스트에서 실행 중인 Nginx 프로세스를 확인할 수 있고, 웹 페이지에 접근도 가능한 모습입니다.
하지만 chroot 디렉토리의 치명적인 단점은 탈옥이 가능하다는 점입니다.
다음과 같이 탈옥 코드를 작성하여 컴파일하고 실행해보겠습니다.
탈옥 코드를 실행하면 다음과 같이 호스트의 루트 디렉토리로 빠져나옴을 확인할 수 있습니다.
이와 같이 상위 디렉토리에 접근 권한을 가질 수 있으면 보안에 매우 취약해집니다.
chroot는 프로세스를 완전히 격리하지 못하므로 도커 컨테이너는 chroot 기능을 베이스로 사용하지 않습니다.
Pivot_Root 와 마운트 네임스페이스
chroot 탈옥 차단을 위해서 나온 개념이 pivot_root와 마운트 네임스페이스를 함께 사용하는 것입니다.
pivot_root는 현재의 루트 파일시스템을 새로운 위치로 이동시키고, 새로운 루트 파일 시스템으로 교체하는 기능입니다. 그리고 마운트 네임스페이스는 마운트 포인트를 격리하는 기능입니다.
* 주요 명령어 사용법
- pivot_root [new-root] [old-root]
- mount -t [filesystem_type] [device_name] [mount_point]
- unshare [options] [program] [arguments]
마운트 네임스페이스를 생성하는 명령어는 unshare 명령어입니다. 마운트 네임스페이스 생성 이후 마운트 정보를 확인해보면 기존과 동일합니다. 왜냐하면 부모 프로세스의 마운트 정보를 그대로 복사해오기 때문입니다.
그러나 자식 마운트 네임스페이스에서 새로운 마운트 포인트를 생성한 시점부터 차이가 발생하기 시작합니다. 자식 마운트 네임스페이스에서는 마운트 포인트가 조회되나, 루트 마운트 네임스페이스에서는 조회되지 않습니다.
자식 마운트 네임스페이스에 임시 파일을 복사하여 실제로 루트 마운트 네임스페이스에서 숨겨지는지 확인합니다.
신규 루트 파일시스템과 기존 루트 파일시스템을 교체하는 명령어는 pivot_root 명령어입니다. 변경할 루트 파일시스템의 경로(/new_root)로 진입하여 기존 루트 파일시스템을 이동시킬 디렉토리(/new_root/put_old)를 생성한 후 pivot_root 명령어를 실행합니다. 그리고 루트 디렉토리의 하위 목록을 출력해보면 기존 루트 디렉토리와 다름을 확인할 수 있습니다.
탈옥을 시도하면 실패합니다. 이처럼 pivot_root와 마운트 네임스페이스를 조합하여 사용하면 한 단계 높은 격리 환경을 구현할 수 있습니다.
Namespace
pivot_root 와 마운트 네임스페이스를 통해 어느 정도 격리된 환경을 구현하였으나 컨테이너에서 호스트의 다른 프로세스들이 보이는 현상, 컨테이너에서 호스트의 포트를 사용하거나 컨테이너에 루트 권한이 있는 경우 등 여전히 문제점이 존재했습니다. 이러한 문제를 해결하고자 나온 개념이 바로 네임스페이스(namespace)입니다.
네임스페이스는 프로세스와 리소스를 격리하기 위한 리눅스 커널 기능으로 mnt, uts, ipc, pid, cgroup, net, user, time 타입의 네임스페이스가 있습니다. 모든 프로세스는 타입별로 특정 네임스페이스에 속하며, 자식 프로세스는 부모 프로세스의 네임스페이스를 상속받습니다. 또는, 프로세스는 네임스페이스 타입별로 일부는 호스트 네임스페이스를 사용하고 일부는 컨테이너의 네임스페이스를 사용할 수도 있습니다.
- Mount : 파일 시스템 (-m, --mount)
- Network : 네트워크 (-n, --net)
- PID : 프로세스 ID (-p, --pid)
- User : 계정 (-U, --user)
- IPC : 프로세스간 통신 (-i, --ipc)
- UTS : Unix Time Sharing, 호스트 네임 (-u, --uts)
- Time : 시간 (-T, --time)
- Cgroup: 자원 그룹 (-C, --cgroup)
마운트 네임스페이스 : unshare -m
unshare 인자로 명령어를 지정하지 않으면 현재 쉘로 지정됩니다. 마운트 네임스페이스를 생성함과 동시에 Inode, 네임스페이스에 속한 프로세스 개수, PID 값이 달라진 것을 확인할 수 있습니다.
UTS 네임스페이스 : unshare -u
UTS는 Unix Time Sharing의 약자로, 유닉스 서버가 널리 보급되지 않던 시기에 여러 사용자가 서버를 나누어 사용할 목적으로 제공된 기능입니다. 호스트 네임 또는 도메인을 격리하기 위해 사용합니다.
UTS를 격리한 상태에서 호스트 네임을 변경하면 기존 호스트와 호스트 네임이 구분됨을 확인할 수 있습니다.
IPC 네임스페이스 : unshare -i
IPC는 Inter-Process Communication의 약자로, 프로세스간 통신 자원을 격리하기 위해 제공되는 기능입니다. 대표적으로 Shared Memory, Pipe, Messege Queue를 구성할 때 동일한 IPC를 사용하도록 할 수 있습니다.
격리된 IPC 네임스페이스에 IPC 자원을 공유 가능한 컨테이너(test1)를 하나 실행합니다.
그리고 컨테이너에 2000 bytes 크기의 Shared Memory를 할당합니다.
방금 생성한 컨테이너(test1)의 IPC 자원을 공유하는 새로운 컨테이너(test2)를 추가 실행합니다.
Share Memory 정보를 출력하면 두 개의 컨테이너가 동일한 IPC 자원을 공유하고 있음을 확인할 수 있습니다.
또한, IPC의 Inode 값도 4026532208로 동일합니다.
PID 네임스페이스 : unshare -p
PID 네임스페이스는 프로세스 ID를 격리하기 위한 기능입니다. 부모 PID 네임스페이스와 자식 PID 네임스페이스는 중첩 구조를 가지며, 부모 PID 네임스페이스에서는 자식 PID 네임스페이스를 볼 수 있습니다. 자식 PID 네임스페이스는 부모 트리의 ID와 서브 트리의 ID 두 개를 소유합니다.
unshare 명령어 실행 시, fork하여 자식 PID 네임스페이스의 PID를 1로 실행합니다. init 프로세스(PID=1)는 커널이 최초로 생성하는 프로세스로 시그널, 좀비(고아) 프로세스들을 처리합니다. init 프로세스가 죽으면 자식 PID 네임스페이스도 종료됩니다.
* unshare -fp --mount-proc /bin/sh
-f : fork / -p : pid namespace
--mount-proc : /proc 파일 시스템 마운트
PID 네임스페이스를 격리하면 내부에서 자식 프로세스(/bin/sh)의 PID가 1로 보입니다. 반면, 호스트에서 조회한 자식 프로세스의 PID는 8883 입니다. PID 네임스페이스의 Inode 값이 4026532205로 일치하므로 동일한 프로세스임을 증명합니다.
그럼 호스트에서 자식 PID 네임스페이스에서 파생된 프로세스를 종료할 수 있을까요?
파생 프로세스뿐만 아니라 자식 프로세스 자체를 종료시키는 것도 가능합니다.
** 도커 Exit Status
CODE # | NAME |
Exit Code 0 | Purposely stopped |
Exit Code 1 | Application error |
Exit Code 125 | Container failed to run error |
Exit Code 126 | Command invoke error |
Exit Code 127 | File or directory not found |
Exit Code 128 | Invalid argument used on exit |
Exit Code 134 | Abnormal termination (SIGABRT) |
Exit Code 137 | Immediate termination (SIGKILL) |
Exit Code 139 | Segmentation fault (SIGSEGV) |
Exit Code 143 | Graceful termination (SIGTERM) |
Exit Code 255 | Exit Status Out Of Range |
USER 네임스페이스 : unshare -U)
USER 네임스페이스는 UID와 GID를 격리하기 위한 기능을 제공합니다. PID 네임스페이스와 마찬가지로 부모와 자식 네임스페이스의 중첩 구조를 가집니다. UID와 GID를 변경(Remap)함으로써 컨테이너의 루트 권한 문제를 해결합니다.
* unshare -U --map-root-user /bin/sh
아래와 같이 우분투 계정으로 컨테이너를 실행하여 접속하면 루트 계정으로 전환됩니다. 그리고 호스트에서 해당 프로세스는 루트 계정으로 실행된 것으로 보입니다. 호스트와 컨테이너의 USER 네임스페이스의 Inode 값을 조회하면 동일합니다. 즉, 해당 프로세스는 루트 권한으로 실행되었으므로 누군가 컨테이너를 탈취하여 호스트에 악의적인 Action을 취한다면 보안상 큰 이슈가 될 소지가 있습니다.
따라서 USER 네임스페이스를 격리하여 컨테이너 내부에서는 루트 계정으로 보이고 실제 호스트에서는 일반 계정으로 보이도록 리맵하면 컨테이너를 탈취하여 권한을 획득하여도 일반 계정 이상의 권한을 얻을 수 없게 됩니다.
다만, USER 네임스페이스를 사용할 경우 패키지 인스톨이 어려워지고 시스템 리소스 이용에 제약이 발생하는 단점이 발생할 수 있습니다. 이 부분은 보안과 편리성 사이의 트레이드 오프 관계에서 적절한 합의점을 찾는 것이 필요해 보입니다.
Cgroup
Cgorup(Control Gruop)은 프로세스의 CPU, MEM, DISK, Network 등 리소스 사용을 제한하고 격리하는 리눅스 커널 기능입니다.
- 프로세스가 사용하는 리소스를 통제
- 하나 또는 복수의 장치를 묶어서 그룹핑
- 컨테이너별로 자원을 분배하고 limit 내에서 운용
- 자원 할당과 제어를 파일 시스템으로 제공함
- Cgroup 네임스페이스로 격리 가능
- Cgroup v1, Cgroup v2
ⅰ) CPU = 1
Cgroup 설정이 없는 경우 프로세스가 1 Core를 100% 점유합니다.
ⅱ) CPU = 0.1 , Resource Limits
프로세스가 점유 가능한 CPU 사용률을 10분의 1로 설정합니다.
stress 부하를 1Core로 주었을 때 프로세스가 점유하는 CPU 사용률은 약 10%내외로 제한됩니다.
* 도커 컨테이너 리소스 사용 제한
컨테이너는 기본적으로 호스트의 리소스를 제한없이 사용할 수 있습니다. 도커 컨테이너에서 리소스를 제한하는 기능 또한 Cgroup이 제공하고 있습니다.
** 컨테이너의 메모리 사용량을 1Gi로 제한
docker run -d --memory="1g" --name memory_1g nginx:alpine
4. 컨테이너 네트워크
도커 없이 네트워크 네임스페이스 환경에서 통신 구성
- 컨테이너는 네트워크 네임스페이스로 격리 환경을 구성합니다.
- 리눅스에서 방화벽 기능을 제공하는 IPtables는 호스트와 컨테이너의 통신에 관여합니다.
RED ↔ BLUE 네트워크 네임스페이스 간 통신
" 네트워크 네임스페이스 생성 시 호스트 네트워크와 구별된다"
- veth(virtual ethernet) 생성
- 네트워크 네임스페이스 생성
- veth0 → RED 네임스페이스
- veth1 → BLUE 네임스페이스
- veth0, veth1 활성화
- veth0, veth1 IP 세팅
- RED & BLUE 설정 정보 확인
- RED ↔ BLUE 통신 테스트
RED ← Bridge(br0) → BLUE 간 통신
"arp table, route table, iptables 와 호스트의 bridge fdb 를 통하여 통신한다"
- 네트워크 네임스페이스 및 veth 생성
- Bridge(br0) 생성
- reth1, beth1 → br0 연결
- reth1, beth1 IP 세팅 및 reth1, beth1, br0 활성화
- RED, HOST, BLUE 설정 정보 확인
# HOST
root@MyServer:~# brctl showmacs br0
port no mac addr is local? ageing timer
2 1a:7a:e0:a5:01:61 yes 0.00
2 1a:7a:e0:a5:01:61 yes 0.00
2 56:cc:9a:72:35:62 no 238.51
1 ca:18:99:84:a2:92 no 179.12
1 f2:17:c5:e8:54:bf yes 0.00
1 f2:17:c5:e8:54:bf yes 0.00
root@MyServer:~# bridge fdb show
01:00:5e:00:00:01 dev ens5 self permanent
01:80:c2:00:00:00 dev ens5 self permanent
01:80:c2:00:00:03 dev ens5 self permanent
01:80:c2:00:00:0e dev ens5 self permanent
33:33:00:00:00:01 dev ens5 self permanent
33:33:ff:a4:5d:eb dev ens5 self permanent
33:33:00:00:00:01 dev docker0 self permanent
01:00:5e:00:00:6a dev docker0 self permanent
33:33:00:00:00:6a dev docker0 self permanent
01:00:5e:00:00:01 dev docker0 self permanent
33:33:ff:74:94:d0 dev docker0 self permanent
02:42:6a:74:94:d0 dev docker0 vlan 1 master docker0 permanent
02:42:6a:74:94:d0 dev docker0 master docker0 permanent
36:26:54:88:4a:66 dev vethf944bb7 vlan 1 master docker0 permanent
36:26:54:88:4a:66 dev vethf944bb7 master docker0 permanent
33:33:00:00:00:01 dev vethf944bb7 self permanent
01:00:5e:00:00:01 dev vethf944bb7 self permanent
33:33:ff:88:4a:66 dev vethf944bb7 self permanent
ca:18:99:84:a2:92 dev reth1 master br0
f2:17:c5:e8:54:bf dev reth1 vlan 1 master br0 permanent
f2:17:c5:e8:54:bf dev reth1 master br0 permanent
33:33:00:00:00:01 dev reth1 self permanent
01:00:5e:00:00:01 dev reth1 self permanent
33:33:ff:e8:54:bf dev reth1 self permanent
56:cc:9a:72:35:62 dev beth1 master br0
1a:7a:e0:a5:01:61 dev beth1 vlan 1 master br0 permanent
1a:7a:e0:a5:01:61 dev beth1 master br0 permanent
33:33:00:00:00:01 dev beth1 self permanent
01:00:5e:00:00:01 dev beth1 self permanent
33:33:ff:a5:01:61 dev beth1 self permanent
33:33:00:00:00:01 dev br0 self permanent
01:00:5e:00:00:6a dev br0 self permanent
33:33:00:00:00:6a dev br0 self permanent
01:00:5e:00:00:01 dev br0 self permanent
33:33:ff:4f:23:70 dev br0 self permanent
2a:18:84:4f:23:70 dev br0 vlan 1 master br0 permanent
2a:18:84:4f:23:70 dev br0 master br0 permanent
root@MyServer:~# bridge fdb show dev br0
33:33:00:00:00:01 self permanent
01:00:5e:00:00:6a self permanent
33:33:00:00:00:6a self permanent
01:00:5e:00:00:01 self permanent
33:33:ff:4f:23:70 self permanent
2a:18:84:4f:23:70 vlan 1 master br0 permanent
2a:18:84:4f:23:70 master br0 permanent
root@MyServer:~# iptables -t filter -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
root@MyServer:~# iptables -t filter -L -n -v
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
22951 40M DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
22951 40M DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
13224 26M ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
12 624 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
9715 15M ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain DOCKER (1 references)
pkts bytes target prot opt in out source destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
pkts bytes target prot opt in out source destination
9715 15M DOCKER-ISOLATION-STAGE-2 all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
22951 40M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
pkts bytes target prot opt in out source destination
0 0 DROP all -- * docker0 0.0.0.0/0 0.0.0.0/0
9715 15M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
pkts bytes target prot opt in out source destination
22951 40M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
- RED ← Bridge(br0) → BLUE 통신 테스트 "실패"
Ping 통신이 실패할 때마다 FORWARD Chain의 DROP Count가 하나씩 증가하고 있습니다.
- 통신이 실패한 이유? "ICMP 요청이 Bridge(br0)로 들어왔으나 beth0로 전달되지 않음"
- HOST : FORWARD Chain 허용 정책 추가
- RED ← Bridge(br0) → BLUE 통신 테스트 재시도 "성공"
RED/BLUE → 호스트 & 외부 통신
" 호스트에 RED/BLUE와 통신 가능한 IP 설정 및 라우팅 추가하고 iptables NAT 통하여 통신한다"
- HOST → RED 통신 테스트 "실패"
- 통신이 실패한 이유? "Bridge에 IP가 세팅되어 있지 않음"
- Bridge(br0) IP 세팅 이후 HOST → RED 통신 테스트 "성공"
- RED/BLUE → 외부 통신 테스트 "실패"
- RED/BLUE - Bridge로 나가는 라우팅 정책 추가
- HOST : POSTROUTING SNAT 정책 추가
- RED/BLUE → 외부 통신 테스트 재시도 "성공"
* conntrack module ?
- 리눅스 커널에서 네트워크 커넥션을 관리, 추적하는 Netfilter Framework의 Stateful Module 입니다.
- 대표적으로 NAT 트래픽의 경우 conntrack을 통해서 네트워크 커넥션을 사용합니다.
※ 네트워크 네임스페이스만으로 직접 수동으로 설정하는 작업은 복잡합니다. 도커는 컨테이너 실행 시 자동으로 해당 컨테이너의 네임스페이스(네트워크, 마운트, PID 등)를 생성하여 호스트와 격리해줍니다.
도커 네트워크 모델
도커 네트워크 모드
Bridge 모드
- 도커에서 컨테이너 생성 시 자동으로 docker0 Bridge를 사용합니다.
- 디폴트 172.17.0.0/16 대역을 컨테이너가 사용합니다.
- 도커는 IPtables의 PREROUTING, POSTROUTING의 NAT Chain을 변경합니다. (외부: SNAT / 내부: DNAT)
root@MyServer:~# docker network inspect bridge | jq
[
{
"Name": "bridge",
"Id": "0ab87dbcf14f5224c0fcbbdf1a457417bac43174eca4dfb2c3d88dcdadedb793",
"Created": "2024-08-31T11:25:23.734237072+09:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"d4f695fbd1b50a8904bb71580d770e6dab87928f5368c8cc199d097c866bfe14": {
"Name": "memory_1g",
"EndpointID": "7502e349b2c8828a9ad47688b5cec8af570c3fea902ce6c0bcc452d4a4b2c25b",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"cohttp://m.docker.network.bridge.default_bridge": "true",
"cohttp://m.docker.network.bridge.enable_icc": "true",
"cohttp://m.docker.network.bridge.enable_ip_masquerade": "true",
"cohttp://m.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"cohttp://m.docker.network.bridge.name": "docker0",
"cohttp://m.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
busybox 컨테이너 두 대를 생성하여 컨테이너 간 통신 및 외부 통신을 테스트합니다.
- 컨테이너 간 통신 테스트 "성공"
- 컨테이너에서 외부 통신 "성공"
※ 도커 호스트가 다수일 때 컨테이너들끼리 직접 통신을 하기 위해서는 네트워크 대역이 중복되지 않아야하고 overlay 혹은 직접 라우팅이 가능하도록 설정이 필요합니다. 이런 부분을 쿠버네티스에서는 CNI(Container Network Interface) 플러그인이 처리하게 됩니다.
HOST 모드
호스트의 환경을 그대로 사용하는 모드로 어플리케이션을 별도의 포트포워딩 없이 서비스 가능합니다.
Nginx 컨테이너를 호스트의 80 포트로 실행합니다.
추가로 Nginx 컨테이너를 실행하면 바로 종료됩니다. 왜냐하면 호스트 포트를 중복으로 사용하기 때문입니다.
HOST 모드를 사용하기에 적합한 케이스는 어떤 게 있을까요?
쿠버네티스 환경에서 네트워크 모니터링을 위한 어플리케이션을 파드로 띄우면 네트워크 인터페이스에 대한 직접적인 접근이 가능하여 네트워크 트래픽이나 리소스 사용량 등 정확한 모니터링을 할 수 있습니다.
예를 들어 Prometheus Node Exporter, Fluentd, Cillium 등이 여기에 해당합니다.
DHCP Server, MetalLB는 외부 네트워크와 직접 통신이 필요한 케이스로, HOST 모드를 사용하면 호스트의 IP 주소를 그대로 사용하여 네트워크 인터페이스와 직접 상호작용할 수 있습니다.
[출처]
1) CloudNet@, KANS 실습 스터디
'K8s > Advanced Network' 카테고리의 다른 글
[KANS3] 6. Ingress + Gateway API (0) | 2024.10.13 |
---|---|
[KANS3] 5. MetalLB + IPVS (0) | 2024.10.06 |
[KANS3] 4. Service ClusterIP + NodePort (3) | 2024.09.29 |
[KANS3] 3. Calico CNI + Mode (0) | 2024.09.22 |
[KANS3] 2. Pause 컨테이너 + Flannel CNI (4) | 2024.09.08 |