Ingress란?
Kubernetes 클러스터 내부의 서비스에 외부에서 접근할 수 있도록 HTTP 및 HTTPS 트래픽을 라우팅하는 API 객체이다.
인그레스는 도메인 기반, 경로 기반 라우팅을 지원하며, SSL/TLS 종료, 가상 호스팅 등 다양한 기능을 제공한다.
Ingress Controller란?
Ingress 리소스를 감시하고, 실제 트래픽을 관리하는 컴포넌트이다. Ingress Controller는 Ingress 리소스에 정의된 규칙을 바탕으로 외부 트래픽을 클러스터 내부의 서비스로 라우팅한다.
구분 | Ingress | Ingress Controller |
정의 | 트래픽 라우팅 규칙을 정의하는 쿠버네티스 API 객체 | 인그레스 리소스를 감시하고 실제 트래픽을 라우팅하는 컴포넌트 |
역할 | 외부 트래픽이 클러스터 내부 서비스로 어떻게 전달될지 설정 | 인그레스 리소스에 따라 로드 밸런서 및 프록시 서버 구성 |
구현 | 도메인 기반, 경로 기반 트래픽 라우팅 규칙 | NGINX, Traefik, HAProxy 등 다양한 소프트웨어 기반 컨트 롤러 |
하지만 현재 Ingress는 더이상 유지보수가 되지 않고, Gateway API를 사용하길 권장하고 있다.
Ingress는 Kubernetes 클러스터 내에서 기본적인 기능은 제공하겠지만, Gateway API가 더 다양한 기능을 제공하고 있기 때문에 복잡한 시나리오(권한 분리 등)에서 Ingress를 대체할 가능성이 높다.
Note:Ingress is frozen. New features are being added to the Gateway API.
https://kubernetes.io/docs/concepts/services-networking/ingress/
Ingress 관련 자료
- Ingress 종류별 비교 자료 : https://docs.google.com/spreadsheets/d/191WWNpjJ2za6-nbG4ZoUMXMpUK8KlCIosvQB0f-oq3k/edit
- Ingress 구성도 예시 : https://kubetm.github.io/k8s/08-intermediate-controller/ingress/
Ingress 테스트 환경 구성
kind를 통한 cluster 구성
스터디에서 공부한 환경과 조금 다르게 wsl2와 kind를 사용해 windows에서 실습환경을 구성해보자.
기존에 구성한 순서대로 wsl를 설치하고, wsl에 접속해서 kind와 docker 등 필요한 도구를 설치한다.
cat <<EOF > kind-ersia-6w.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
"InPlacePodVerticalScaling": true #실행 중인 파드의 리소스 요청 및 제한을 변경할 수 있게 합니다.
"MultiCIDRServiceAllocator": true #서비스에 대해 여러 CIDR 블록을 사용할 수 있게 합니다.
nodes:
- role: control-plane
labels:
mynode: control-plane
topology.kubernetes.io/zone: ap-northeast-2a
extraPortMappings: #컨테이너 포트를 호스트 포트에 매핑하여 클러스터 외부에서 서비스에 접근할 수 있도록 합니다.
- containerPort: 80
hostPort: 80
- containerPort: 443
hostPort: 443
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
- |
kind: ClusterConfiguration
apiServer:
extraArgs: #API 서버에 추가 인수를 제공
runtime-config: api/all=true #모든 API 버전을 활성화
controllerManager:
extraArgs:
bind-address: 0.0.0.0
etcd:
local:
extraArgs:
listen-metrics-urls: http://0.0.0.0:2381
scheduler:
extraArgs:
bind-address: 0.0.0.0
- |
kind: KubeProxyConfiguration
metricsBindAddress: 0.0.0.0
- role: worker
labels:
mynode: worker1
topology.kubernetes.io/zone: ap-northeast-2a
- role: worker
labels:
mynode: worker2
topology.kubernetes.io/zone: ap-northeast-2b
- role: worker
labels:
mynode: worker3
topology.kubernetes.io/zone: ap-northeast-2c
networking:
podSubnet: 10.10.0.0/16 #파드 IP를 위한 CIDR 범위를 정의합니다. 파드는 이 범위에서 IP를 할당받습니다.
serviceSubnet: 10.200.1.0/24 #서비스 IP를 위한 CIDR 범위를 정의합니다. 서비스는 이 범위에서 IP를 할당받습니다.
EOF
해당 yaml을 아래의 명령어로 수행해 kind cluster를 생성하자.
기존 kind환경과 다른 점은 control-plane node에 ingress-ready 설정을 추가한 점이다.
생성이 완료되면 ingress-ready 설정을 확인한다.
# ingress-nginx controller이 아직 1.31버전을 호환하지 못하므로 1.30.x을 설치한다.
# 참고 : https://github.com/kubernetes/ingress-nginx?tab=readme-ov-file#supported-versions-table
kind create cluster --image kindest/node:v1.30.0 --config kind-ersia-6w.yaml --name ersia
# 생성 후 control-plane node의 label에서 ingress-ready 설정을 확인한다.
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.ingress-ready}{"\n"}{end}'
ersia-control-plane true
ersia-worker
ersia-worker2
ersia-worker3
여기까지 세팅하면 아래와 같은 기본 cluster 구성이 완료된다.
LoadBanlancer 및 테스트용 Pod 생성
서비스 배포를 위한 namespace와 LoadBalancer 서비스를 생성한다.
cat <<EOF > ns-lb-ersia-6w.yaml
# Namespace 정의
apiVersion: v1
kind: Namespace
metadata:
name: web-ns
---
# Service 정의
apiVersion: v1
kind: Service
metadata:
name: web-svc-lb
namespace: web-ns
spec:
selector:
app: deploy-websvr
ports:
- name: webport
port: 80
targetPort: 8080
protocol: TCP
type: LoadBalancer
EOF
kubectl apply -f ns-lb-ersia-6w.yaml
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get svc -n web-ns
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-svc-lb LoadBalancer 10.200.1.183 <pending> 80:31535/TCP 91s
서비스를 위한 테스트 pod를 생성한다.
cat <<EOF > echoserver-ersia-6w.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-echo
namespace: web-ns
spec:
replicas: 3
selector:
matchLabels:
app: deploy-websvr
template:
metadata:
labels:
app: deploy-websvr
spec:
containers:
- name: cndk-websvr
image: k8s.gcr.io/echoserver:1.5
ports:
- containerPort: 8080
EOF
kubectl apply -f echoserver-ersia-6w.yaml
여기까지 배포한 상태에서는 별다른 통신이 되지 않는다.
왜냐하면 우리는 kind로 외부의 80/443 port를 각각 80/443 port로만 연결했을 뿐, 외부에서 접근할 수 있는 방법이 없기 때문이다. (ClusterIP에 NodePort를 설정하면 가능하지만, 여기서는 NodePort를 설정하지 않았다.)
Nginx Ingress 설치
curl -L https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/kind/deploy.yaml -o ingress-deploy.yaml
kubectl apply -f ingress-deploy.yaml
kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=180s
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get pod -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-xn2hc 0/1 Completed 0 90s
ingress-nginx-admission-patch-xkfh2 0/1 Completed 1 90s
ingress-nginx-controller-8fb8cdb7c-qhlj7 1/1 Running 0 90s
nginx ingress controller를 설치하자.
helm으로도 설치할 수 있지만 helm은 스터디에서 사용한 방법이므로 여기서는 Manifest yaml로 설치한다.
cat <<EOF > ingress-ersia-6w.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
namespace: web-ns
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-svc-lb
port:
number: 80
EOF
kubectl apply -f ingress-ersia-6w.yaml
여기서 주의해야할 점은 service는 사전에 생성한 LoadBalancer 타입의 서비스 name이라는 점이다.
여기까지 세팅하면 아래와 같은 구성이 된다. (system Pod는 생략)
이제 로컬 PC의 웹브라우저에서 http://localhost:80 (80포트는 생략 가능)으로 접속하면 아래와 같은 화면을 볼 수 있다.
새로고침을 수행하면 Pod Information이 바뀌면서 로드밸런싱이 되는 것을 볼 수 있다.
아래와 같이 curl 명령어로도 확인이 가능하다.
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# for i in {1..100}; do curl -s localhost:80 | grep Hostname ; done | sort | uniq -c | sort -nr
34 Hostname: deploy-echo-7945977dd7-vt78v
33 Hostname: deploy-echo-7945977dd7-kths4
33 Hostname: deploy-echo-7945977dd7-9zbbk
- 여기서 client_address는 해당 Pod로 접속을 수행한 Src IP 주소이고,
- x-forwarded-for는 원래 클라이언트의 IP 주소이다. Kind 클러스터의 호스트 시스템 IP가 찍히게 된다.
그러면 통신이 어떻게 되는지 확인해보자.
Ingress 통신 흐름 분석
패킷 캡쳐
아래의 명령어를 통해 tcpdump로 패킷을 캡쳐하자.
총 3군데에서 패킷을 캡쳐할 것이다.
- wsl2를 통해 기동한 ubuntu 서버에서 수행 (kind docker node 네트워크)
- control-plane역할을 수행하는 node에서 수행
- 실제 echo server 웹서버를 구동하는 pod에서 수행
# 1. kind를 최초 접속한 ubuntu 서버에서 tcpdump 수행
# 로컬PC에서 kind로 들어오는 docker network bridge 장치에서 캡쳐
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# docker network inspect kind | grep Subnet
"Subnet": "172.18.0.0/16",
"Subnet": "fc00:f853:ccd:e793::/64",
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# ip a | grep 172.18
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-82f7f6bc330d
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# tcpdump -i br-82f7f6bc330d -w traffic_dump.pcap
# 2. control-plane역할을 수행하는 node로 접속해 tcpdump 수행
# ingress-nginx-controller pod에서 패킷을 캡쳐하면 좋지만
# 해당 pod는 보안 설정을 해놓아서 root 권한이 제거되어있는 등 tcpdump를 실행하기 귀찮은 조건이다.
# 따라서 해당 pod가 구동되어있는 node에서 패킷을 캡쳐하는 것으로 대체한다.
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get pod -A -o wide | grep ingress-nginx-controller
ingress-nginx ingress-nginx-controller-f7f474944-h475r 1/1 Running 0
50m 10.10.0.9 ersia-control-plane <none> <none>
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# docker exec -it ersia-control-plane /bin/bash
root@ersia-control-plane:/# tcpdump -i eth0 -w traffic_dump2.pcap
root@ersia-control-plane:/# exit
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# docker cp ersia-control-plane:traffic_dump2.pcap .
Successfully copied 505kB to /root/.
# 3. 실제 echo server 웹서버를 구동하는 pod에서 tcpdump 수행
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# kubectl exec -it deploy-echo-7945977dd7-9zbbk -n web-ns -- /bin/bash
root@deploy-echo-7945977dd7-9zbbk:/# tcpdump -i eth0 -w traffic_dump3.pcap
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
^C25 packets captured
25 packets received by filter
0 packets dropped by kernel
root@deploy-echo-7945977dd7-9zbbk:/# exit
kubectl cp web-ns/deploy-echo-7945977dd7-9zbbk:traffic_dump3.pcap ./traffic_dump3.pcap
3군데의 패킷을 다운받아 wireshark로 병합해주고, flow-graph를 확인해보면 아래와 같이 나오는 것을 볼 수 있다.
Ingress 통신 흐름
- 위의 절차대로 수행했다면, ingress-nginx-controller Pod의 IP는 10.10.0.2가 된다.
- 내 경우 해당 Pod에 몇가지 테스트를 하다가 Pod를 재시작하면서 Pod의 IP가 10.10.0.9로 변경되었다.
위 과정을 살펴보면 다음과 같다.
웹 브라우저 (localhost:80, 실제 로컬PC)
↓ kind 포트 매핑 호스트 80 → 컨트롤 플레인 포트 80
kind 호스트 (IP : 172.18.0.1, 가상 네트워크)
↓
kind control-plane Node (IP : 172.18.0.5)
↓
nginx Ingress Controller Pod (IP : 10.10.0.9)
↓ Pod에 대한 네트워크 토폴로지 확인 및 서비스 포트 8080으로 직접 전달
Ingress (web-ingress, nginx 정책확인)
↓
Pod (cndk-websvr 컨테이너, deploy-echo 10.10.1.2 / 10.10.2.2 / 10.10.3.2, 8080 포트)
위의 내용을 그림으로 간단하게 표현해보면 아래와 같이 된다.
패킷을 직접 확인해보면 LoadBalancer를 거치지 않고, Pod로 트래픽을 바로 전달하는데,
Kubernetes에서 Ingress Controller는 클러스터 내부의 네트워크 토폴로지를 이미 인식하고 있기 때문이다.
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get endpoints -A
NAMESPACE NAME ENDPOINTS AGE
default kubernetes 172.18.0.5:6443 25h
ingress-nginx ingress-nginx-controller 10.10.0.9:443,10.10.0.9:80 25h
ingress-nginx ingress-nginx-controller-admission 10.10.0.9:8443 25h
kube-system kube-dns 10.10.0.3:53,10.10.0.4:53,10.10.0.3:53 + 3 more... 25h
web-ns web-svc-lb 10.10.1.2:8080,10.10.2.2:8080,10.10.3.2:8080 25h
# ClusterRole은 클러스터 전체에 적용되는 권한 세트
# Role은 특정 네임스페이스 내에서만 적용되는 권한 세트
# ingress-nginx ClusterRole과 Role 모두 endpoints 리소스에 대한 권한을 가지고 있다.
# ClusterRole과 Role은 Nginx Ingress Controller Pod가 사용하는 Service account에 바인딩되기 때문에
# 이를 통해 Ingress Controller 파드가 필요한 권한을 가지게 된다.
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# kubectl describe clusterrole ingress -n ingress-nginx | egrep '(Verbs|endpoints)'
Resources Non-Resource URLs Resource Names Verbs
endpointslices.discovery.k8s.io [] [] [list watch get]
endpoints [] [] [list watch]
Resources Non-Resource URLs Resource Names Verbs
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# kubectl describe roles ingress-nginx -n ingress-nginx | egrep '(Verbs|endpoints)'
Resources Non-Resource URLs Resource Names Verbs
endpoints [] [] [get list watch]
endpointslices.discovery.k8s.io [] [] [list watch get]
# list: endpoints 목록을 조회
# watch: endpoints의 변경 사항을 실시간으로 모니터링
# get: 특정 endpoint의 상세 정보를 조회
Endpoint를 통해 실제 서비스하는 Pod의 라우팅 정보를 Ingress Controller에서 감지하고 불필요한 네트워크 홉(hop)을 줄이기 위해 가능한 한 직접적으로 목적지 pod에 트래픽을 전달하게 된다. 또한 이 Endpoint 정보를 바탕으로 LoadBalancer를 거치지 않고도 로드밸런싱이 Ingress에서 자체적으로 이루어진다.
- nginx ingress controller 이 endpoint ip(port) list 획득 코드 내용 : https://github.com/kubernetes/ingress-nginx/blob/82e1fc8cac1c4c77223302ed256ff5907252baf1/internal/ingress/controller/endpoints.go#L77-L82
참고자료
- https://github.com/kubernetes/ingress-nginx
- https://github.com/kubernetes-sigs/kind/issues/2889
- Wireshark - Merge (파일병합) : https://blog.naver.com/eztcpcom/220525261242
- 와이어샤크 패킷 흐름 시각화(Flow graph) : https://malwareanalysis.tistory.com/284
- crtctl 사용법 : https://platform9.com/docs/kubernetes/containerd-commands-and-info