설명하기에 앞서, Pod 의 대한 설명을 하겠습니다.
Pod 는 적어도 한개 이상의 컨테이너를 보유하면서, Pod 내 컨테이너끼리 같은 Host에 위치하며 Network Stack (Network Namespace), Volume 리소스를 서로 공유하는 Kubernetes 의 기본 실행 단위 입니다.
여기서 Network Stack 을 공유 한다는 것은, Pod 내 컨테이너 중 하나에서 localhost 요청을 보낼 시 외부 컨테이너나 Host에 요청이 도달하지 않고, 같은 Pod 내의 컨테이너 들에게 도달한다는 의미입니다. 즉, 같은 Pod 내의 컨테이너는 localhost 로의 요청을 통해 서로 접근할 수 있습니다. 이는 Linux Network Namespace 기능을 이용해 구현됩니다.
nginx 컨테이너가 존재하며 80 포트로 html 문서를 Serve 하고 있는 상황을 상상해 보겠습니다.
이 상황에서 python 이 올라간 컨테이너가 추가되어 nginx 컨테이너가 Serve 하고 있는 문서를 다운로드 받으려 한다면 어떻게 될까요?
만약 Network Namespace 공유를 하지 않는다면 컨테이너마다 veth 가상 인터페이스가 생성되고, 이 인터페이스는 docker0 브릿지에서 IP를 할당받을 것입니다.
위 그림처럼 컨테이너마다 할당 받은 IP를 찾는다면 IP 주소를 통해 통신 할 수 있습니다. 하지만 컨테이너는 매우 유연하여 쉽게 컨테이너가 내려가거나 올라올 수 있는데, 이때 IP가 변경될 수 있으며 변경된다면 다시 IP를 찾아야 할 것입니다.
또한 여러 노드에 걸쳐 클러스터링을 하려고 하는데, 컨테이너가 실행되는 과정에서 순서가 뒤바뀌거나 이미 다른 컨테이너가 IP를 쓰는 등 여러 문제로 IP가 매번 바뀌어 클러스터 마다 IP를 다르게 설정하는 불편함을 겪을수도 있습니다.
하지만 Kubernetes 를 통해 같은 Pod 내에 생성된 컨테이너들은 Network Namespace 를 공유하기 때문에 http://localhost:80 요청을 통해 nginx 로 바로 접근하여 문서를 받을 수 있습니다.
이때, Network Namespace 를 공유하기 위해 생성되는 것이 Pause Container 입니다.
참고: 위 그림에서 docker0 이 cbr0 으로 변경되었는데, Kubernetes 는 docker 의 브릿지 대신 자체 브릿지인 cbr0 을 사용합니다.
Pause Container 는 Pod 내부의 컨테이너들을 위한 일종의 '부모 컨테이너' 와 같은 역할을 수행합니다.
PID Namespace
대상 프로세스들을 다른 Namespace 의 프로세스들과 격리
Network Namespace
대상 프로세스들의 네트워크를 다른 네트워크와 격리, 같은 네임스페이스 내의 프로세스들과 포트 중복 불가 (같은 인터페이스를 공유하기 때문)
UTS Namespace
컨테이너마다 고유의 Hostname 을 부여 (프로세스의 Hostname을 격리)
좀비 프로세스
부모 프로세스와 자식 프로세스가 있다고 할 때, 부모 프로세스가 자식 프로세스보다 먼저 종료되면 자식 프로세스는 종료되지 못하게 됨. 이를 좀비 프로세스 라고 부름.
리눅스에서는 이러한 좀비 프로세스를 PID 1번을 가진 프로세스 (Init Process) 에 할당해주며, 이 1번 프로세스는 좀비 프로세스를 종료시켜줌.
컨테이너의 프로세스 생성 방식
컨테이너는 기본적으로 외부와 단절된 단일 프로세스로 구성되도록 설계 되어 있기 때문에, 컨테이너 내에선 기본적으로 시작 프로세스의 PID 가 1번으로 설정
그런데 만약 컨테이너 내의 프로세스가 하나만 실행되는 것이 아닌 여러 프로세스가 실행된다면 위에서 언급한 좀비 프로세스 문제가 발생할 수 있는데, 문제는 시작 프로세스가 부모 프로세스이면서 1번 PID 를 가지게 되어 있기 때문에 자식 프로세스는 1번 프로세스가 죽으면 그대로 좀비 프로세스로서 계속 시스템에 남게 됨
이를 해결하기 위해 Pod 에선 PID Namespace 기능을 이용해 Pod 내의 컨테이너가 1번 PID 로 Pause Container 를 사용하게 해 Pause Container 의 프로세스가 좀비 프로세스를 죽이도록 함
Pod 내의 네트워크 통신은 이렇듯 간편하나, Pod 와 Pod 끼리의 통신은 쉽지 않은 문제입니다.
Kubernetes 는 Cluster 라는 특성 상 각 Pod 가 다른 Worker Node 위에서 실행될 가능성이 높은데, 이 경우 통신을 위해 해결해야 할 문제가 있습니다.
Kubernetes 는 이를 해결하기 위해 다음과 같은 방법을 사용합니다.
Overlay Network 란 서로 다른 네트워크 내에 있는 자원들이 별도의 추가적인 장비를 설치하거나 구성할 필요 없이, 이미 설치되어 있는 물리적 링크에 구성된 서로 접근 가능한 네트워크 라우트를 유용해 통신할 수 있게 구성된 네트워크 망을 말합니다.
이 네트워크는 터널링, 라우팅, 브릿지, 가상 네트워크 인터페이스 등의 기술로 구성되며, 일반적인 네트워크와 동등한 사용성을 갖습니다.
위 그림은 Overlay Network 가 구성된 Kubernetes 네트워크 입니다. (cbr0 브릿지가 cni0 브릿지로 대체됨)
각 노드에 존재하는 Pod 에는 서로 다른 IP가 할당되어 있으며, Pod가 서로의 IP로 통신을 시도 할 경우 별 다른 설정 없이 시스템 내 구성되어 있는 Route Table 을 따라 Host 내부의 가상 인터페이스 (vtap0) 를 경유해 eth0 인터페이스를 타고 외부로 요청이 나가며, 이 때 CNI 플러그인은 패킷에 적절한 처리를 가해 다른 노드에 존재하는 CNI 플러그인이 메세지를 수신 할 수 있도록 라우트 합니다. Router 에서는 미리 구성되었던 Router 내 Route Table 을 참조해 올바른 Worker Node 로 요청을 라우트 합니다.
Kubernetes 에서 위와 같은 Overlay Network 를 구성하는 일을 CNI 플러그인에 위임하고 있으며, calico, flannel, weave 등의 여러 CNI 플러그인이 저마다의 이점을 가지고 경합하고 있습니다.
이러한 여러 개선 방안을 통해 Kubernetes 는 네트워크의 사용성을 개선하였으나, 궁극적으로는 Pod 등이 재생성 되거나 교체 된다면 IP 주소가 쉽게 바뀔 수 있으며, 이는 Kubernetes 특성상 자주 일어날 수 있는 상황입니다.
이를 해결하기 위해 Kubernetes 는 Service Object 를 제공합니다
Service 는 특정한 Pod 에 대해 영구적으로 사용할 수 있는 엔드포인트를 제공합니다. 이 Service 는 한번 생성되면 삭제 되기 전 까지 영구히 유지 되며, 연결 될 대상 Pod 를 찾을때에는 label 등의 메타데이터를 이용해 select 하기 때문에 Pod 가 대체되거나 IP가 변경되더라도 올바른 메타데이터가 설정 되어 있다면 쉽게 찾을 수 있습니다.
Service 에는 여러 모드가 있는데, 이 중에서 많이 사용되는 타입은 아래 3개 입니다.
ClusterIP
클러스터 내의 임의의 IP를 할당하여 사용하는 타입이며, 일종의 고정 IP 서비스 같은 개념입니다. 클러스터 내의 IP가 할당되는 탓에 외부에서 접근할 수 없어 주로 내부 서비스 간의 통신에서 사용됩니다.
이 클러스터 내 IP는 Pod 에 할당되는 IP 가 아닌 Service 전용 IP CIDR 에서 할당됩니다.
아래 그림에서 마치 Service 와 kube-proxy 가 한 노드에만 있는것처럼 그려졌으나, 실제로는 모든 노드에 kube-proxy 가 있고, Service 는 모든 노드에 배포되어 적절한 위치로 트래픽을 프록싱 합니다.
NodePort
클러스터 내에 있는 모든 Node 에 특정 포트를 개방하여 사용하는 타입입니다. Node 의 IP를 사용하기 때문에 클러스터 외부에서 접근하기 쉽지만 모든 Node 에 포트가 개방되어 접근할 수 있기에 보안이 약해 보통 관리용으로만 사용됩니다.
다만 NodePort 서비스가 라우팅되는 Cluster IP 또한 자동으로 발급되기 때문에 Cluster IP 타입처럼 사용할 수 있습니다.
LoadBalancer
AWS, GCP, Azure 같은 클라우드 환경에서만 사용 가능한 타입으로, 외부 IP를 가지고 있는 L4 클라우드 로드밸런서를 할당 받아서 동작하며, 해당 로드밸런서는 Node 의 NodePort 타입 서비스를 접근하는 것처럼 동작합니다.
여기서 LoadBalancer 는 Kubernetes 내부에 존재하는 것이 아닌 같이 생성 될 AWS, GCP, Azure 의 L4 타입 로드밸런서와 1:1 로 매칭되는 항목일 뿐이며 이 LoadBalancer 서비스가 로드밸런싱을 수행해주지는 않습니다.
다만, kube-proxy 가 자체적으로 수행하는 'readiness (준비성) 체크 후 스케줄 알고리즘에 따른 노드 선택 및 프록싱' 과정이 Health Check 후 LB 하는 역할을 비슷하게 수행하며, L7 로드밸런서는 후술할 Ingress 에서 구현이 가능합니다.
kube-proxy 는 모든 Worker Node 에 배치되어 있는 네트워크 프록시 입니다.
kube-proxy는 Service IP (Cluster IP)를 향한 요청을 감지하면 NAT 로서 동작하며 Service IP 를 향한 트래픽을 Pod IP 로 프록싱 해 줍니다.
Service IP 정책은 API Endpoint 를 Watch 하고 있다 변경 사항을 발견하면 적용하는 방식으로 최신화 합니다.
kube-proxy 에는 세가지 동작 모드가 있습니다.
userspace
iptables 에서 Service IP를 향한 요청을 감지하면 kube-proxy 로 트래픽을 보냅니다. kube-proxy 는 직접 Pod에 트래픽을 프록싱 합니다.
iptables
iptables 에서 Service IP를 향한 요청을 감지하면 iptables 는 kube-proxy 가 미리 설정해놓은 정책대로 트래픽을 타겟 Pod 로 프록싱 합니다. kube-proxy 를 거치지 않기 때문에 userspace 모드보다 성능이 낫습니다.
IPVS (IP Virtual Server)
Linux Kernel 에서 제공하는 L4 Load Balancer 인 IPVS 를 iptables 대신 사용하는 모드입니다. iptables 모드처럼 kube-proxy 가 정책을 정하며, IPVS 는 Linux Kernel Space 에서 동작하며 데이터 구조를 Hash-table 로 가져가기 때문에 iptables 모드 보다 좋은 성능을 냅니다.
Service 의 LoadBalancer 는 L4이기에 호스트 기반 라우팅 및 URL Path 기반 라우팅이 불가능 합니다.
Kubernetes 는 HTTP 및 HTTPS 기반의 L7 로드 밸런싱을 위해 Ingress 라는 기능을 지원합니다.
Ingress 는 단순한 리소스일 뿐이기에 ingress-nginx 와 같은 Ingress Controller 가 같이 생성되어야 하며, 요청 처리는 실제로는 해당 컨트롤러가 처리합니다.
Ingress 는 단순히 L7 로드밸런서의 역할만을 하기 때문에 요청 자체는 Service 에서 들어오며, 기본적으로는 LoadBalancer 가 생성되나 NodePort, ClusterIP 방식으로 설정도 가능합니다.