네트워킹 기초
- 21.1 스위칭, 라우팅, 게이트웨이
- 21.2 DNS
- 21.3 네트워크 네임스페이스
- 21.4 Docker 네트워킹
- 21.5 CNI
- 21.6 클러스터 네트워킹
Kubernetes 네트워킹을 이해하려면 리눅스 네트워킹 기초부터 쌓아 올라가야 해. 스위칭과 라우팅 같은 기본 개념에서 출발해서, DNS, 네트워크 네임스페이스, Docker 네트워킹, CNI 표준까지 거치고 나면 클러스터 네트워킹이 왜 그렇게 생겼는지 자연스럽게 보이거든.
Linux에서 네트워킹의 핵심은 결국 세 가지야. 스위칭으로 같은 네트워크 안에서 통신하고, 라우팅으로 다른 네트워크끼리 연결하고, 게이트웨이로 외부 세계와 소통하는 거거든.
두 대의 컴퓨터가 있으면 스위치에 연결해서 네트워크를 만들어. 각 호스트에는 eth0 같은 인터페이스가 있고, ip link 명령으로 확인할 수 있어. 네트워크가 192.168.1.0이라고 하면, ip addr 명령으로 각 시스템에 같은 네트워크 대역의 IP를 할당해. 그러면 스위치를 통해 같은 네트워크 안에서 서로 통신할 수 있게 돼. 스위치는 같은 네트워크 내에서만 패킷을 전달할 수 있다는 게 포인트야.
다른 네트워크, 예를 들어 192.168.2.0 네트워크에 있는 시스템하고 통신하려면 라우터가 필요해. 라우터는 두 네트워크에 각각 하나씩, 총 두 개의 IP를 가진 장치야. 첫 번째 네트워크에서는 192.168.1.1, 두 번째에서는 192.168.2.1 이런 식으로. 그런데 라우터만 연결한다고 자동으로 되는 게 아니라, 각 시스템한테 "다른 네트워크로 가려면 이 라우터를 통해 가"라고 알려줘야 해. 이게 게이트웨이 설정이야.
route 명령으로 현재 라우팅 테이블을 확인하면, 처음엔 아무 라우팅 구성도 없어. 그래서 시스템 B에서 192.168.2.0 네트워크에 연결하려면 이렇게 경로를 추가해:
ip route add 192.168.2.0/24 via 192.168.1.1
이러면 라우팅 테이블에 경로가 추가되고, 라우터를 통해 다른 네트워크에 도달할 수 있어. 반대쪽 시스템 C에서도 마찬가지로 192.168.1.0 네트워크에 대한 경로를 추가해야 양방향 통신이 되는 거야.
인터넷에 접속해야 한다면? 인터넷에는 수많은 네트워크가 있으니까 일일이 경로를 추가하는 건 말이 안 돼. 그래서 default 게이트웨이를 설정하는 거야. 경로를 모르는 모든 네트워크에 대해 이 라우터를 사용하라는 뜻이지. default 대신 0.0.0.0을 써도 같은 의미야 -- 모든 IP 대상을 뜻하거든. 게이트웨이 필드에 0.0.0.0이 들어가 있으면 게이트웨이가 필요 없다는 뜻이야, 자기 네트워크 안에 있으니까.
네트워크에 내부용 라우터와 인터넷용 라우터가 따로 있다면, 라우팅 테이블에 두 개의 항목을 넣어야 해. 하나는 내부 사설 네트워크용, 다른 하나는 나머지 전부를 위한 기본 게이트웨이. 인터넷 접속에 문제가 있으면 이 라우팅 테이블과 기본 게이트웨이 설정부터 확인하는 게 좋아.
이제 Linux 호스트 자체를 라우터로 쓰는 경우를 보자. 호스트 A, B, C가 있고, B가 두 네트워크에 모두 연결되어 있어 (eth0, eth1 두 인터페이스). A의 IP는 192.168.1.5, C는 192.168.2.5, B는 양쪽 네트워크에서 1.6과 2.6이야. A에서 C로 핑을 보내려면, A에는 "192.168.2.0 네트워크로 가려면 192.168.1.6(호스트 B)을 거쳐라"라는 경로를 추가하고, C에도 비슷하게 역방향 경로를 추가해. 그런데 이렇게 해도 응답이 안 와. 왜냐하면 Linux는 기본적으로 한 인터페이스에서 받은 패킷을 다른 인터페이스로 전달하지 않거든. 보안 때문이야. 예를 들어 eth0이 사설 네트워크, eth1이 공개 네트워크에 연결되어 있으면, 공개 네트워크에서 사설 네트워크로 메시지가 그냥 넘어가면 안 되잖아.
이걸 제어하는 건 /proc/sys/net/ipv4/ip_forward 파일이야. 기본값이 0(포워딩 안 함)인데, 1로 바꾸면 패킷 포워딩이 활성화돼:
echo 1 > /proc/sys/net/ipv4/ip_forward
이건 재부팅하면 초기화되니까, 영구적으로 유지하려면 /etc/sysctl.conf 파일에서 net.ipv4.ip_forward = 1로 설정해야 해.
앞으로 유용하게 쓸 핵심 명령어들을 정리하면 이래. ip link는 호스트의 인터페이스를 나열하고 수정하는 거고, ip addr은 인터페이스에 할당된 IP 주소를 확인하는 거야. ip addr add로 IP를 설정할 수 있는데, 이 명령으로 한 변경은 재시작하면 날아가니까 영구적으로 하려면 네트워크 인터페이스 파일에서 설정해야 해. ip route나 route는 라우팅 테이블을 보는 거고, ip route add는 라우팅 테이블에 항목을 추가하는 거야. 그리고 호스트를 라우터로 쓸 때는 IP 포워딩이 활성화되어 있는지 꼭 확인해야 해.
스위칭과 라우팅으로 네트워크 연결은 됐는데, IP를 일일이 외워서 치는 건 현실적이지 않잖아. 그래서 이름을 붙여서 쓰자는 게 DNS야.
같은 네트워크에 컴퓨터 A(192.168.1.1)와 B(192.168.1.11)가 있다고 해보자. 시스템 B에 데이터베이스가 있어서 "db"라고 부르고 싶어. 그러면 시스템 A의 /etc/hosts 파일에 이렇게 적으면 돼:
192.168.1.11 db
이제 ping db하면 192.168.1.11로 핑이 날아가. 여기서 중요한 건, A가 B의 실제 호스트 이름이 뭔지 확인하지 않는다는 거야. /etc/hosts에 적힌 게 A한테는 진실이야. 심지어 Google의 IP를 B의 IP로 매핑해놓으면 A에서 ping google.com 하면 B한테 핑이 가. 같은 시스템을 가리키는 이름을 여러 개 만들 수도 있고, 원하는 만큼 서버에 원하는 이름을 붙일 수 있어. ping이든 SSH든 어떤 도구를 쓰든, 이름으로 호스트를 참조할 때마다 /etc/hosts 파일에서 IP를 찾아. 이렇게 호스트 이름을 IP로 변환하는 걸 **"이름 확인(name resolution)"**이라고 해.
소규모 네트워크에서는 /etc/hosts만으로 충분했는데, 환경이 커지면 파일 관리가 불가능해져. 서버 IP가 하나 바뀌면 모든 호스트의 hosts 파일을 다 수정해야 하니까. 그래서 모든 항목을 중앙에서 관리하는 DNS 서버가 등장한 거야.
호스트가 DNS 서버를 사용하게 하려면 /etc/resolv.conf 파일에 DNS 서버 주소를 넣어:
nameserver 192.168.1.100
이렇게 모든 호스트에 설정하면, 호스트가 모르는 이름을 만났을 때 DNS 서버에 물어봐. IP가 변경되면 DNS 서버만 업데이트하면 모든 호스트가 새 IP를 알게 되는 거지. 그렇다고 /etc/hosts를 못 쓰는 건 아니야. 테스트 서버처럼 다른 사람이 알 필요 없는 건 로컬 hosts 파일에 넣어도 돼. 그 서버를 내 시스템에서만 resolve 할 수 있고, 다른 시스템에서는 못 하는 거지.
그러면 /etc/hosts와 DNS 서버 양쪽에 같은 이름이 있으면 어떻게 될까? 기본적으로 로컬 /etc/hosts를 먼저 확인하고, 거기 없으면 DNS 서버를 봐. 이 순서는 /etc/nsswitch.conf 파일의 hosts 항목으로 결정돼:
hosts: files dns
files가 /etc/hosts, dns가 DNS 서버야. 이 순서를 바꾸면 DNS를 먼저 확인하게 할 수도 있어.
만약 내 DNS 서버에도 없는 이름이면? resolv.conf에 추가 네임서버를 넣을 수 있어. 예를 들어 Google의 공개 DNS인 8.8.8.8. 호스트에 네임서버를 여러 개 설정할 수 있지만 네트워크의 모든 호스트에 다 설정해야 하니까, 차라리 내부 DNS 서버 자체가 모르는 이름을 8.8.8.8로 전달하도록 설정하는 게 더 깔끔해.
도메인 이름에 대해 좀 더 이야기하면, www.google.com 같은 이름에서 .com은 **최상위 도메인(TLD)**이야. .net은 네트워크용, .edu는 교육기관용, .org는 비영리단체용. .(점)이 루트고, 그 아래에 TLD, 도메인, 서브도메인 순으로 트리 구조를 이루고 있어. maps.google.com에서 maps가 서브도메인인 거지. Google의 지도 서비스는 maps.google.com, 저장소는 drive.google.com, 메일은 mail.google.com 이런 식으로 서브도메인으로 나눠.
maps.google.com에 접속하면, 요청이 먼저 조직 내부 DNS 서버에 가고, 거기서 모르면 인터넷의 루트 DNS 서버 -> .com DNS 서버 -> Google DNS 서버 순으로 찾아가서 IP를 알아내. 조직 DNS 서버는 결과를 몇 초에서 몇 분간 캐시해서 다음에는 전체 과정을 다시 안 거쳐도 돼.
조직 내에서도 비슷한 구조를 쓸 수 있어. web.mycompany.com, mail.mycompany.com, drive.mycompany.com 같은 식으로. 그런데 회사 안에서 매번 web.mycompany.com 전체를 치는 건 귀찮잖아. 가족 안에서 성까지 안 부르는 것처럼. 그래서 /etc/resolv.conf에 search 항목을 추가해:
search mycompany.com prod.mycompany.com
이러면 ping web만 해도 자동으로 web.mycompany.com을 시도해. search 도메인은 여러 개 지정할 수도 있어서, web이라고 하면 web.mycompany.com이나 web.prod.mycompany.com도 다 검색해봐. 물론 web.mycompany.com처럼 도메인을 직접 지정하면 search 도메인을 안 붙여.
DNS 레코드 유형도 알아두면 좋아. A 레코드는 이름을 IPv4에 매핑하는 거고, AAAA(쿼드 A) 레코드는 IPv6에 매핑하는 거야. CNAME 레코드는 이름을 다른 이름에 매핑하는 건데, 같은 애플리케이션에 여러 별칭을 쓸 때 사용해. 예를 들어 음식 배달 서비스에 "먹다"나 "배고프다" 같은 별칭을 붙이는 거지.
DNS 확인을 테스트할 때 ping이 항상 적합한 건 아니야. nslookup이라는 도구가 있는데, DNS 서버에서 호스트 이름을 직접 쿼리해. 주의할 점은 nslookup은 /etc/hosts 파일은 안 본다는 거야. DNS 서버만 쿼리해. 그래서 로컬 hosts 파일에 항목을 추가했는데 nslookup으로 조회하면 못 찾을 수 있어. dig도 비슷한 도구인데, DNS 서버에 저장된 형태와 비슷하게 자세한 정보를 보여줘서 유용해.
이름을 IP로 바꾸는 건 해결했으니, 이제 컨테이너 세계에서 네트워크를 어떻게 격리하는지 보자. 네트워크 네임스페이스는 Docker 같은 컨테이너가 네트워크 격리를 구현하는 핵심 메커니즘이야.
호스트가 집이라면 네임스페이스는 각 아이한테 할당된 방이라고 생각하면 돼. 각 아이는 자기 방 안에 있는 것만 보고, 밖에서 무슨 일이 일어나는지 모르지. 하지만 부모(호스트)는 모든 방과 집 전체를 다 볼 수 있어. 원하면 두 방 사이에 연결을 만들 수도 있고.
컨테이너를 만들 때 네임스페이스로 격리하면, 컨테이너 안에서 프로세스를 나열하면 프로세스 ID 1인 단일 프로세스만 보여. 하지만 호스트에서 같은 프로세스를 보면 다른 프로세스 ID로 표시돼. 같은 프로세스가 안팎에서 다른 ID로 보이는 거야. 이게 네임스페이스가 작동하는 방식이야.
네트워킹 관점에서 보면, 호스트에는 LAN에 연결된 인터페이스, 라우팅 테이블, ARP 테이블이 있어. 네트워크 네임스페이스를 만들면 이 모든 걸 컨테이너로부터 숨길 수 있어. 네임스페이스 안에서는 자체 가상 인터페이스, 라우팅 테이블, ARP 테이블을 가질 수 있지.
새 네트워크 네임스페이스를 만드는 명령은 ip netns add야:
ip netns add red
ip netns add blue
ip netns # 목록 확인
네임스페이스 안에서 명령을 실행하려면 ip netns exec red ip link처럼 앞에 ip netns exec <이름>을 붙이면 돼. ip link -n red도 같은 결과인데, 이건 ip 명령에만 쓸 수 있어. 네임스페이스 안에서 ip link를 실행하면 루프백 인터페이스만 보이고 호스트의 eth0은 안 보여. ARP 테이블이나 라우팅 테이블도 비어 있어. 호스트의 네트워크 정보가 완전히 격리된 거야.
현재 이 네임스페이스들은 네트워크 연결이 없어. 자체 인터페이스도 없고 호스트 네트워크도 안 보여. 먼저 네임스페이스끼리 연결하는 방법부터 보자. 두 네임스페이스를 연결하려면 가상 이더넷 쌍(veth pair), 쉽게 말해 양쪽 끝에 인터페이스가 있는 가상 케이블을 만들어:
ip link add veth-red type veth peer name veth-blue
양쪽 끝을 각 네임스페이스에 연결하고, IP를 할당하고, 인터페이스를 올려:
ip link set veth-red netns red
ip link set veth-blue netns blue
ip -n red addr add 192.168.15.1/24 dev veth-red
ip -n blue addr add 192.168.15.2/24 dev veth-blue
ip -n red link set veth-red up
ip -n blue link set veth-blue up
이제 빨간색 네임스페이스에서 파란색으로 핑이 가능해. 빨간색 네임스페이스의 ARP 테이블을 보면 파란색 이웃의 MAC 주소가 보이고, 반대쪽도 마찬가지야. 그런데 호스트의 ARP 테이블에는 이 네임스페이스 정보가 전혀 안 보여 -- 완전히 격리된 거야.
네임스페이스가 많아지면 일일이 veth pair로 연결하는 건 비현실적이야. 물리 세계에서 여러 장비를 연결할 때 스위치를 쓰듯, 가상 스위치를 만들어야 해. Linux Bridge가 대표적인 솔루션이야 (Open vSwitch 같은 것도 있어):
ip link add v-net-0 type bridge
ip link set dev v-net-0 up
이 브리지는 호스트 입장에서는 eth0처럼 또 다른 네트워크 인터페이스이고, 네임스페이스 입장에서는 연결할 수 있는 스위치야. ip link 출력에 다른 인터페이스와 함께 나타나.
기존 직접 연결 케이블은 삭제하고(ip link del veth-red -- 한쪽 삭제하면 쌍으로 연결되어 있으니 다른 쪽도 자동 삭제), 새로 브리지에 연결하는 케이블을 만들어:
ip link add veth-red type veth peer name veth-red-br
ip link add veth-blue type veth peer name veth-blue-br
ip link set veth-red netns red
ip link set veth-red-br master v-net-0
ip link set veth-blue netns blue
ip link set veth-blue-br master v-net-0
IP 할당하고 인터페이스 올리면 모든 네임스페이스가 브리지를 통해 서로 통신할 수 있어. 같은 절차로 네임스페이스를 더 추가하면 네 개든 다섯 개든 모두 내부 브리지 네트워크에 연결돼서 서로 통신 가능해.
호스트에서 네임스페이스로 접근하려면? 호스트의 IP가 192.168.1.2이고 네임스페이스는 192.168.15.x 대역이면 서로 다른 네트워크니까 직접은 안 돼. 하지만 브리지가 호스트의 네트워크 인터페이스이니까, 거기에 IP를 할당하면 돼:
ip addr add 192.168.15.5/24 dev v-net-0
이제 호스트에서 네임스페이스로 핑이 가능해.
하지만 이 전체 네트워크는 호스트 내부에 갇혀 있어. 네임스페이스에서 외부 LAN(예: 192.168.1.3)에 접근하려면, 네임스페이스의 라우팅 테이블에 외부 네트워크에 대한 경로를 추가해야 해. 호스트가 두 네트워크(내부 브리지 192.168.15.x + 외부 LAN 192.168.1.x)에 모두 연결되어 있으니까 게이트웨이 역할을 할 수 있어:
ip netns exec blue ip route add 192.168.1.0/24 via 192.168.15.5
게이트웨이 IP는 네임스페이스에서 접근 가능한 로컬 네트워크 주소(192.168.15.5)여야 해, 호스트의 외부 IP(192.168.1.2)가 아니라. 경로에 추가할 때 기본 게이트웨이는 네임스페이스에서 연결할 수 있어야 하거든.
그런데 이렇게 해도 "네트워크에 연결할 수 없다"는 메시지는 안 뜨지만 핑 응답이 안 올 수 있어. 홈 네트워크에서 라우터를 통해 인터넷에 연결하는 것과 비슷한 상황인데, 외부 네트워크가 내부 사설 IP(192.168.15.x)를 모르니까 응답을 보낼 수가 없거든. 그래서 NAT가 필요해:
iptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUERADE
이러면 네임스페이스에서 나가는 패킷의 출발지 주소가 호스트 IP로 바뀌어서, 외부에서는 호스트가 보낸 패킷으로 인식해. 이제 핑이 통해.
인터넷까지 접근하려면 기본 게이트웨이를 추가하면 돼. 호스트가 도달할 수 있는 모든 네트워크에 네임스페이스도 접근할 수 있으니까, 외부 네트워크에 연결하려면 호스트와 대화하면 된다고 알려주는 거야:
ip netns exec blue ip route add default via 192.168.15.5
반대로 외부에서 네임스페이스 안의 서비스에 접근하려면 두 가지 방법이 있어. 하나는 외부 호스트에게 내부 네트워크(192.168.15.0)에 대한 경로를 알려주는 건데, 그건 별로 좋지 않아. 다른 방법은 iptables 포트 포워딩 규칙을 추가하는 거야. 호스트의 포트 80으로 들어오는 모든 트래픽을 네임스페이스 IP의 포트 80으로 전달하는 식으로.
네트워크 네임스페이스의 원리를 알았으니, 이제 Docker가 이걸 어떻게 자동화하는지 보자. Docker의 네트워킹은 결국 네트워크 네임스페이스에서 배운 개념을 Docker가 자동화한 거야. 세 가지 네트워킹 옵션이 있는데 하나씩 보자.
첫 번째는 none 네트워크야. 컨테이너가 아무 네트워크에도 연결되지 않아. 외부로 나갈 수도 없고, 외부에서 들어올 수도 없고, 컨테이너끼리도 통신 못 해. 여러 컨테이너를 돌려도 전부 네트워크 없이 만들어지고 서로 통신 불가야. 완전 고립.
두 번째는 host 네트워크야. 컨테이너가 호스트의 네트워크를 그대로 공유해. 호스트와 컨테이너 사이에 네트워크 격리가 없어서, 컨테이너에서 포트 80으로 웹 앱을 띄우면 호스트의 포트 80에서 바로 접근 가능해. 별도 포트 매핑이 필요 없지. 대신 같은 포트를 쓰는 컨테이너를 두 개 띄울 수 없어 -- 두 프로세스가 동시에 같은 포트에서 수신할 수 없으니까.
세 번째가 bridge 네트워크야. 이게 가장 중요하고 Docker의 기본 옵션이야. 172.17.0.0 대역의 내부 사설 네트워크가 만들어지고, Docker 호스트와 컨테이너가 이 네트워크에 연결되며, 각 컨테이너가 이 네트워크에서 자체 IP를 받아.
Docker를 설치하면 자동으로 "bridge"라는 내부 네트워크를 만들어. docker network ls로 확인할 수 있어. 근데 Docker가 "bridge"라고 부르는 이 네트워크는 호스트에서는 **"docker0"**라는 인터페이스로 나타나. ip link 명령으로 보면 docker0이 보이거든. Docker가 내부적으로 ip link add docker0 type bridge 같은 걸 실행한 거야, 네임스페이스 강의에서 본 것과 똑같은 기법이지.
docker0 인터페이스에는 IP 172.17.0.1이 할당돼. ip addr 명령으로 확인할 수 있어. 이 브리지는 호스트한테는 인터페이스이고, 컨테이너한테는 스위치 역할을 하는 거잖아.
컨테이너를 만들면 Docker는 해당 컨테이너를 위한 네트워크 네임스페이스를 생성해. ip netns 명령으로 네임스페이스를 나열할 수 있는데, Docker가 만든 네임스페이스를 보려면 약간의 설정이 필요하긴 해. 각 컨테이너와 연결된 네임스페이스는 docker inspect 명령 출력에서 확인 가능해.
그러면 Docker가 컨테이너를 브리지에 어떻게 연결할까? 이전 강의랑 똑같아. 양쪽 끝에 인터페이스가 있는 **가상 케이블(veth pair)**을 만들어. 호스트에서 ip link 실행하면 docker0 브리지에 연결된 한쪽 끝이 보이고, 네임스페이스와 함께 -n 옵션으로 같은 명령을 실행하면 컨테이너 네임스페이스 안에 있는 다른 쪽 끝이 보여. 컨테이너 안의 인터페이스는 IP도 할당받아 -- 예를 들어 172.17.0.3. ip addr 명령이나 docker inspect로 확인할 수 있어.
새 컨테이너를 만들 때마다 이 과정이 반복돼. Docker가 네임스페이스 생성하고, 인터페이스 쌍 만들고, 한쪽은 컨테이너에, 다른 쪽은 브리지에 연결하고, IP 할당해. 인터페이스 쌍은 홀수-짝수 번호로 식별할 수 있어(9와 10은 한 쌍, 11과 12는 한 쌍). 컨테이너들은 모두 같은 네트워크의 일부라서 서로 통신할 수 있어.
포트 매핑은 어떻게 될까? nginx 컨테이너가 포트 80에서 웹 페이지를 서비스한다고 하자. 컨테이너는 호스트 내부의 사설 네트워크 안에 있으니까, 같은 네트워크에 있는 다른 컨테이너만 접근 가능하고, 호스트 자체에서도 컨테이너 IP로 curl하면 웹 페이지가 보여. 하지만 호스트 외부에서는 안 돼.
외부 접근을 허용하려면 컨테이너 실행 시 포트 매핑 옵션을 써:
docker run -p 8080:80 nginx
이러면 호스트의 포트 8080으로 들어오는 트래픽이 컨테이너의 포트 80으로 전달돼. 이제 호스트 IP와 포트 8080을 사용해서 외부에서도 접근 가능해.
Docker가 이걸 어떻게 구현하느냐면, iptables를 사용해서 NAT 규칙을 만드는 거야. NAT 테이블에 항목을 생성해서 프리라우팅 체인에 규칙을 추가하고 대상 포트를 8080에서 80으로 변경해. Docker는 이 규칙을 DOCKER 체인에 추가하고, 컨테이너의 IP도 대상에 포함시켜. iptables -t nat -L로 Docker가 생성한 규칙을 확인할 수 있어. 결국 네트워크 네임스페이스에서 배운 것과 완전히 같은 원리를 Docker가 자동으로 해주는 거야.
Docker가 자체적으로 네트워킹을 해결하는 건 좋은데, Docker만 있는 게 아니잖아. Rocket, Mesos, Kubernetes 전부 같은 네트워킹 문제를 풀어야 해. 그래서 **CNI(Container Network Interface)**라는 표준이 나온 거야.
왜 이런 게 필요하냐면, Docker든 Rocket이든 Mesos든 Kubernetes든 전부 같은 네트워킹 문제를 풀어야 하거든. 네트워크 네임스페이스 만들고, 브리지에 연결하고, veth pair 생성하고, IP 할당하고, NAT 설정하고... 다 비슷한 방식으로 접근하는데 조금씩 달라. 왜 같은 걸 여러 번 코딩해야 해? 하나의 표준으로 만들자는 거지.
그래서 다양한 솔루션의 아이디어를 모아서 네트워킹 부분을 하나의 프로그램으로 통합한 거야. 예를 들어 "bridge"라는 프로그램을 만들어서, 컨테이너를 브리지 네트워크에 연결하는 데 필요한 모든 작업을 담당하게 해. 이 프로그램을 실행하면서 "이 컨테이너를 이 네임스페이스에 추가해"라고 지정하면, 나머지는 bridge 프로그램이 알아서 처리해. 덕분에 컨테이너 런타임은 네트워킹 세부사항에서 벗어날 수 있어. Rocket이든 Kubernetes든 새 컨테이너를 만들면 그냥 bridge 프로그램을 호출하고 컨테이너 ID와 네임스페이스를 전달하면 돼.
CNI는 두 가지를 정의하는 표준이야. 플러그인을 어떻게 개발해야 하는지, 그리고 컨테이너 런타임이 플러그인을 어떻게 호출해야 하는지.
컨테이너 런타임 쪽 책임을 보면, 각 컨테이너에 대한 네트워크 네임스페이스를 생성하고, 컨테이너가 연결할 네트워크를 식별해야 해. 컨테이너 생성 시에는 add 명령으로 플러그인을 호출하고, 삭제 시에는 del 명령으로 호출해야 해. 그리고 JSON 파일을 사용해서 네트워크 플러그인을 구성하는 방법도 지정하고 있어.
플러그인 쪽 책임은, add, del, check 명령줄 인수를 지원해야 하고, 컨테이너 ID와 네임스페이스 같은 매개변수를 받아야 해. 파드에 IP 주소를 할당하고, 컨테이너가 네트워크의 다른 컨테이너에 도달하는 데 필요한 모든 경로를 처리해야 하고, 결과는 특정 형식으로 반환해야 해. 컨테이너 런타임과 플러그인이 이 표준을 따르면 어떤 런타임이든 어떤 플러그인이든 함께 작동할 수 있어.
CNI에는 이미 기본 제공 플러그인이 있어. bridge, vlan, ipvlan, macvlan, 그리고 Windows용 플러그인이 있고, IPAM(IP 주소 관리)을 위한 host-local과 DHCP 플러그인도 있어. 타사에서 제공하는 플러그인도 많아 -- Weave, Flannel, Cilium, Calico, VMware NSX, Infoblox 등등.
Docker, Rocket, Mesos, Kubernetes 같은 컨테이너 런타임들이 이 표준을 구현하니까 어떤 플러그인이든 작동할 수 있어. 그런데 이 목록에서 빠진 게 하나 있어 -- Docker야. Docker는 CNI를 구현하지 않아. 자체 표준인 **CNM(Container Network Model)**이라는 걸 가지고 있거든. CNI와 비슷한 목적이지만 차이가 있어. 그래서 Docker 컨테이너를 실행할 때 --network=cni 같은 식으로 CNI 플러그인을 직접 지정할 수 없어.
하지만 Docker에서 CNI를 전혀 못 쓴다는 뜻은 아니야. 직접 해결하면 돼. 네트워크 구성 없이 Docker 컨테이너를 만든 다음, bridge 플러그인을 수동으로 호출하면 되는 거야. 이게 바로 Kubernetes가 하는 방식이야. Kubernetes는 Docker 컨테이너를 만들 때 none 네트워크로 생성하고, 그 다음에 구성된 CNI 플러그인을 호출해서 나머지 네트워크 설정을 처리해.
기초 개념을 다 다뤘으니 이제 진짜 Kubernetes 클러스터의 네트워킹 요구사항을 보자. 마스터 노드와 워커 노드에는 반드시 충족해야 하는 네트워킹 요구사항이 있어. 이걸 제대로 안 하면 클러스터가 작동하지 않거든.
기본적으로 각 노드에는 네트워크에 연결된 인터페이스가 최소 하나 있어야 하고, 각 인터페이스에는 IP 주소가 설정되어 있어야 해. 호스트에는 고유한 호스트 이름과 고유한 MAC 주소가 있어야 하는데, 특히 기존 가상 머신을 복제해서 VM을 만든 경우에 이게 겹칠 수 있으니까 주의해야 해.
그다음은 포트야. 제어 평면의 다양한 컴포넌트가 사용하는 포트들이 열려 있어야 해. 마스터 노드에서는 API 서버를 위해 6443 포트에서 연결을 수락해야 해. 워커 노드, kubectl, 외부 사용자, 기타 컨트롤 플레인 구성 요소가 이 포트를 통해 kube API 서버에 접근해. 마스터와 워커 노드 양쪽의 kubelet은 포트 10250에서 수신해. 마스터 노드에도 kubelet이 존재할 수 있다는 점 알아둬. kube-scheduler는 포트 10259, kube-controller-manager는 포트 10257이 열려 있어야 해.
워커 노드에서는 외부 접근을 위한 NodePort 서비스가 30000~32767 포트를 사용하니까 이 범위도 개방되어야 해.
마스터 노드가 여러 개인 경우, etcd 서버는 포트 2379에서 수신해. 그리고 etcd 클러스터의 멤버들끼리 통신할 수 있도록 포트 2380도 추가로 열어둬야 해.
이 포트 목록은 Kubernetes 공식 문서에서도 확인할 수 있어. 노드의 네트워킹, 방화벽, iptables 규칙, 또는 GCP/Azure/AWS 같은 클라우드 환경의 네트워크 보안 그룹을 설정할 때 이 점을 고려해야 해. 클러스터 설정 후 뭔가 안 되면 이 포트 목록부터 점검하는 게 좋은 출발점이야.
정리
21장 읽고 기억할 거 세 가지:
- 리눅스 네트워킹의 기본은 스위칭(같은 네트워크), 라우팅(다른 네트워크), 게이트웨이(외부)이고, 호스트를 라우터로 쓰려면
/proc/sys/net/ipv4/ip_forward를 1로 설정해야 한다. DNS는/etc/hosts(로컬) ->/etc/resolv.conf(DNS 서버) 순으로 이름을 확인하며, 이 순서는/etc/nsswitch.conf로 제어한다. - 네트워크 네임스페이스로 컨테이너 네트워크를 격리하고, veth pair로 연결하며, Linux Bridge로 여러 네임스페이스를 스위치처럼 묶는다. Docker는 이 모든 걸 자동화해서 bridge 네트워크(docker0)를 만들고, iptables NAT로 포트 매핑을 구현한다.
- CNI는 컨테이너 런타임과 네트워크 플러그인 사이의 표준이고, Docker는 CNI 대신 자체 CNM을 쓰지만 Kubernetes는 none 네트워크로 컨테이너를 만든 뒤 CNI 플러그인을 호출하는 방식으로 우회한다. 클러스터 네트워킹에서는 API 서버(6443), kubelet(10250), NodePort(30000~32767) 등 필수 포트가 열려 있어야 한다.