이번 주차는 Kuberentes 서비스 오브젝트에서 ClusterIP, NodePort라는 두 가지 유형에 대해 학습했습니다. (이 외에도 다양한 유형이 존재하는데, 해당 유형은 차주에 진행할 예정입니다.)
실습 환경 구축
먼저, 실습을 위한 kind 클러스터 설정 파일입니다.
실습 환경은 K8S v1.31.0, CNI(Kindnet, Direct Routing mode) ,IPTABLES proxy mode
- Node 네트워크 대역 : 172.18.0.0/16
- Pod 네트워크 대역 : 10.10.0.0/16 ⇒ 각각 10.10.1.0/24, 10.10.2.0/24, 10.10.3.0/24, 10.10.4.0/24
- Service 네트워크 대역 : 10.200.1.0/24
cat <<EOT> kind-svc-1w.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
"InPlacePodVerticalScaling": true
#"MultiCIDRServiceAllocator": true
nodes:
- role: control-plane
labels:
mynode: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
- containerPort: 30001
hostPort: 30001
- containerPort: 30002
hostPort: 30002
- role: worker
labels:
mynode: worker1
- role: worker
labels:
mynode: worker2
- role: worker
labels:
mynode: worker3
networking:
podSubnet: 10.10.0.0/16
serviceSubnet: 10.200.1.0/24
EOT
1.31 버전의 클러스터가 정상 설치되었습니다.
디버깅을 위해 기본 툴을 설치합니다.
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping git vim arp-scan -y'
for i in worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i sh -c 'apt update && apt install tree psmisc lsof wget bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done
kube-proxy의 기본 모드는 iptables이고, kube-proxy ConfigMap을 통해 관련 설정을 찾아볼 수 있습니다.
테스트를 위한 서비스를 배포합니다.
cat <<EOT> 3pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: webpod1
labels:
app: webpod
spec:
nodeName: myk8s-worker
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: webpod2
labels:
app: webpod
spec:
nodeName: myk8s-worker2
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: webpod3
labels:
app: webpod
spec:
nodeName: myk8s-worker3
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
EOT
cat <<EOT> netpod.yaml
apiVersion: v1
kind: Pod
metadata:
name: net-pod
spec:
nodeName: myk8s-control-plane
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOT
cat <<EOT> svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-clusterip
spec:
ports:
- name: svc-webport
port: 9000 # 서비스 IP 에 접속 시 사용하는 포트 port 를 의미
targetPort: 80 # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미
selector:
app: webpod # 셀렉터 아래 app:webpod 레이블이 설정되어 있는 파드들은 해당 서비스에 연동됨
type: ClusterIP # 서비스 타입
EOT
Pod 배포 직후(Pod 상태가 Running 이전 상태) 엔드포인트를 확인해보면 존재하질 않습니다.
Pod 상태가 Running으로 변경된 이후 엔드포인트가 붙었습니다.
서비스 상태에서도 확인이 가능합니다.
netshoot Pod에서 위 서비스를 100번 호출하면 엔드포인트 Pod로 로드밸런싱 되는 것을 알 수 있습니다.
iptables
먼저, 짚고 넘어가야 하는 부분으로 현재는 사용하지 않는 kube-proxy의 user-space proxy 모드는 DNAT를 통해 Service로 전송한 모든 요청 패킷은 kube-proxy로 전달되는 방식이었는데, 이는 kube-proxy에 SPOF 발생 가능성이 있습니다.
이를 보완하기 위해 iptables 모드에서는 kube-proxy가 직접 proxy 역할을 수행하지 않고, 이 역할을 하는 netfilter에 위임합니다. kube-proxy는 단순히 netfilter의 규칙을 알맞게 업데이트하는 것을 담당하며, 규칙을 관리할 뿐 실제 데이터에 대한 트래픽 처리는 직접하지 않습니다. 따라서, user-space proxy 모드보다 안정적이고, 성능 또한 좋습니다.
iptables의 규칙은 테이블 마다 분리되며, 아래 순서로 규칙이 추가 됩니다.
PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-EXT-#(MARK) → KUBE-SVC-# → KUBE-SEP-# ⇒ KUBE-POSTROUTING (MASQUERADE) ← KUBE-EXT-#(MARK)
- PREROUTING : 모든 트래픽이 가장 먼저 매칭되는 테이블이며, KUBE-SERVICES로 포워딩 합니다.
- KUBE-SERVICES : Service 오브젝트 유형의 NodePort 유형이 아닌 ClusterIP 유형에 접속 시 아래 첫 번째 규칙에 매칭되어 KUBE-SVC-YYY로 포워딩 합니다. (노드의 IP를 목적지로 인입되는 경우, KUBE-NODEPORTS로 포워딩 합니다.)
- 여기서 KUBE-SVC-YYY는 서비스 오브젝트에 따라 이름이 달라지며, KUBE-SEP의 SEP는 Service EndPoint를 의미합니다. (실제 트래픽을 처리할 Pod로 포워딩 합니다.)
- KUBE-POSTROUTING : 출발지 IP와 Port를 노드의 IP로 SNAT 하여 처리합니다.
코드 분석
코드 전체는 아래 링크에서 확인 가능합니다.
https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/iptables/proxier.go
kubernetes/pkg/proxy/iptables/proxier.go at master · kubernetes/kubernetes
Production-Grade Container Scheduling and Management - kubernetes/kubernetes
github.com
지난 주차에 iptables는 kube-proxy에서 사용하는 기본 모드라고 설명했습니다. 오늘 정리할 주제로 이 Kubernetes에서 Service 오브젝트 생성 시 iptables가 어떻게 동작되는지에 대한 소스코드를 분석해보려고 합니다.
요약부터 해보면, iptables의 역할을 아래와 같이 정의할 수 있습니다.
- iptables 규칙을 생성, 수정 및 관리하여 Kubernetes 클러스터의 네트워크 트래픽을 제어합니다.
- Service 및 Pod를 모니터링하고 필요에 따라 클러스터 노드의 방화벽 설정을 수정합니다.
- 동시 작업을 스레드로부터 안전한 방식으로 처리합니다. 특히 'sync' 및 'atomic'을 많이 사용하는 경우 더욱 그렇습니다.
- 코드에서 상수를 가져오는 것은 성능과 동시성에 중점을 둡니다. 예를 들어 대규모 클러스터에 대한 임계값은 엔드포인트가 많은 클러스터에서 iptables의 성능을 유지하는 동시에 동시 작업(sync 및 atomic 사용)을 통해 iptables에 대한 안전한 업데이트를 보장합니다.
- 효율적인 검색 및 비교를 위해 IP 주소나 Service 이름과 같은 메타데이터를 해시하고 저장합니다.
iptables 패키지
Kubernetes 관련 패키지를 가져와 사용합니다. (Go 패키지 제외)
- v1 "k8s.io/api/core/v1": Pods, Services, Nodes와 같은 핵심 Kubernetes 리소스를 나타냅니다.
- discovery "k8s.io/api/discovery/v1": 서비스 검색(서비스 및 엔드포인트에 대한 정보 관리)에 사용됩니다.
- Kubernetes 클러스터 내에서 Service를 검색하고 트래픽을 올바르게 라우팅하기 위해 iptables을 관리합니다.
- 엔드포인트 슬라이스 처리(discovery 패키지에서)는 Kubernetes에서 Service 검색 프로세스를 확장하기 위함입니다.
- types: ObjectMeta와 같은 Kubernetes 리소스 유형을 처리합니다.
- sets: 네트워킹 및 필터링에 유용한 컬렉션 유형인 세트를 처리하는 유틸리티를 제공합니다.
import (
...
v1 "k8s.io/api/core/v1"
discovery "k8s.io/api/discovery/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimac...
)
위에서 설명한 각 iptables의 테이블(체인)이 상수로 정의되어 있습니다.
const (
// the services chain
kubeServicesChain utiliptables.Chain = "KUBE-SERVICES"
// the external services chain
kubeExternalServicesChain utiliptables.Chain = "KUBE-EXTERNAL-SERVICES"
// the nodeports chain
kubeNodePortsChain utiliptables.Chain = "KUBE-NODEPORTS"
// the kubernetes postrouting chain
kubePostroutingChain utiliptables.Chain = "KUBE-POSTROUTING"
// kubeMarkMasqChain is the mark-for-masquerade chain
kubeMarkMasqChain utiliptables.Chain = "KUBE-MARK-MASQ"
// the kubernetes forward chain
kubeForwardChain utiliptables.Chain = "KUBE-FORWARD"
// kubeProxyFirewallChain is the kube-proxy firewall chain
kubeProxyFirewallChain utiliptables.Chain = "KUBE-PROXY-FIREWALL"
// kube proxy canary chain is used for monitoring rule reload
kubeProxyCanaryChain utiliptables.Chain = "KUBE-PROXY-CANARY"
// kubeletFirewallChain is a duplicate of kubelet's firewall
kubeletFirewallChain utiliptables.Chain = "KUBE-FIREWALL"
)
largeClusterEndpointsThreshold 상수는 다른 성능 모드로 전환하기 위한 임계값을 정의합니다. 이 값은 클러스터에 엔드포인트가 1000개 이상일 때 사용되며, iptables 성능을 최적화해 디버깅 합니다.
const largeClusterEndpointsThreshold = 1000
엔드포인트 개수가 largeClusterEndpointsThreshold의 값보다 큰 경우, 많은 엔드포인트를 처리하기 위해 최적화된 모드로 전환합니다. 1000개가 넘는 엔드포인트를 처리하기 위해 규칙을 하나씩 수정하는 대신 배치 처리를 사용해 iptables의 호출 수를 최소화 하고, 병렬 실행을 통해 스레드의 안전을 위해 동기화 패키지를 활용하여 규칙을 동시에 업데이트 하기 위해 고루틴을 생성합니다. (아래에서 추가로 설명합니다.)
if len(endpoints) > largeClusterEndpointsThreshold {
// Use optimized iptables update strategy
}
sysctl 매개변수는 네트워크와 관련된 시스템 동작을 구성하는데, sysctlRouteLocalnet을 통해 로컬 라우팅을 활성화 하고, sysctlNFConntrackTCPBeLiberal을 통해 conntrack이 TCP 트래픽 설정을 보장합니다.
const sysctlRouteLocalnet = "net/ipv4/conf/all/route_localnet"
const sysctlNFConntrackTCPBeLiberal = "net/netfilter/nf_conntrack_tcp_be_liberal"
iptables의 규칙 추가
규칙을 추가하려면 일반적으로 utilexec 또는 utiliptable 라이브러리를 통해 iptables 바이너리와 인터페이스하는 기능을 사용해야 합니다. iptables.EnsureRule: 특정 규칙이 있는지 확인하고 필요한 경우 추가하는데, 매개변수르 -A, -d, -j를 받을 수 있습니다. -A 플래그는 추가를 위한 것이고, -d는 목적지를 설정하며, -j는 액션을 지정합니다(아래와 같이 ACCEPT이거나, REJECT).
func (proxier *Proxier) addIptablesRule() error {
ipt := proxier.iptables // utiliptables.Interface instance
chain := kubeServicesChain
// Example rule to add: `iptables -A KUBE-SERVICES -d <destination> -j ACCEPT`
args := []string{"-A", string(chain), "-d", "<destination>", "-j", "ACCEPT"}
if err := ipt.EnsureRule(utiliptables.Append, utiliptables.TableNAT, chain, args...); err != nil {
return fmt.Errorf("failed to ensure rule for %s: %v", chain, err)
}
return nil
}
iptables의 규칙 수정
규칙을 수정하려면 기존 규칙을 삭제하고 새 규칙을 추가하거나 직접 수정해야 할 수 있습니다. 삭제 규칙 기능을 사용하면 규칙이 제거되어 업데이트된 규칙을 추가할 수 있습니다.
func (proxier *Proxier) updateIptablesRule() error {
ipt := proxier.iptables
chain := kubeServicesChain
// Remove existing rule
if err := ipt.DeleteRule(utiliptables.TableNAT, chain, "-d", "<old-destination>", "-j", "ACCEPT"); err != nil {
return fmt.Errorf("failed to delete existing rule: %v", err)
}
// Add updated rule
return proxier.addIptablesRule()
}
iptables의 규칙 삭제
Servce 또는 Pod를 제거할 때는 해당 규칙을 삭제해야 하며, -d 매개변수에 목적지 정보와 -j에 액션을 정의해 규칙을 삭제합니다.
func (proxier *Proxier) removeIptablesRule(destination string) error {
ipt := proxier.iptables
chain := kubeServicesChain
if err := ipt.DeleteRule(utiliptables.TableNAT, chain, "-d", destination, "-j", "ACCEPT"); err != nil {
return fmt.Errorf("failed to delete rule for destination %s: %v", destination, err)
}
return nil
}
동시성 및 성능 최적화
위에서 요약한대로 iptables는 동시성을 통해 안전한 작업을 한다고 설명했습니다. Kubernetes 클러스터는 규모가 크고 매우 동적일 수 있으므로 동시성과 성능을 처리하는 것이 중요합니다. 관리 방법은 다음과 같습니다.
Sync 패키지를 활용한 동기화
동기화 패키지는 스레드 안전을 보장하기 위해 Mutex, RWMutex 및 WaitGroup과 같은 기본 요소를 제공합니다.
type Proxier struct {
sync.Mutex # 한 번에 하나의 고루틴만 iptables를 수정해 경쟁 조건을 방지
iptables utiliptables.Interface
// Other fields
}
func (proxier *Proxier) UpdateRules() {
proxier.Lock()
defer proxier.Unlock() # 오류가 발생하더라도 뮤텍스가 해제되도록 보장
// Safely update iptables rules
}
var activeConnections int32
func incrementConnections() {
# Locking 없이 카운터를 안전하게 증가시킴
atomic.AddInt32(&activeConnections, 1)
}
func getConnections() int32 {
# 카운터 값을 원자적으로 검색
return atomic.LoadInt32(&activeConnections)
}
iptables를 활용한 트래픽 로드밸런싱
Kubernetes에서 Service에 엔드포인트로 연결된 여러 Pod가 존재하는 경우, iptables를 사용하여 트래픽을 여러 Pod에 로드밸런싱 합니다. 일반적인 접근 방식은 DNAT(Destination NAT)를 사용하여 패킷의 대상 IP를 Pod IP 중 하나의 대상 IP로 수정하는 것입니다.
// Example rule to redirect traffic to a pod behind the service
args := []string{"-t", "nat", "-A", string(kubeServicesChain),
"-d", "<service-cluster-ip>", "-p", "tcp", "--dport", "80",
"-j", "DNAT", "--to-destination", "<pod-ip>:<pod-port>"}
Pod가 비정상 상태인 경우, 트래픽이 해당 Pod로 포워딩 되지 않도록 iptables 규칙을 업데이트합니다.
func (proxier *Proxier) handleHealthCheck(serviceIP string, podIP string) {
// If pod health check fails, remove rule
if !proxier.checkPodHealth(podIP) {
proxier.removeIptablesRule(serviceIP)
} else {
// Ensure the rule remains
proxier.addIptablesRule(serviceIP, podIP)
}
}
iptables를 활용한 Masquerading
Masquerading은 Pod에서 클러스터 외부로 나가는 트래픽이 Pod의 IP 대신 노드의 IP 주소를 사용하도록 보장합니다.
func ensureMasqueradeRule(ipt utiliptables.Interface, destination string) error {
args := []string{"-t", "nat", "-A", string(kubeMarkMasqChain),
"-d", destination, "-j", "MASQUERADE"}
if err := ipt.EnsureRule(utiliptables.Append, utiliptables.TableNAT, kubeMarkMasqChain, args...); err != nil {
return fmt.Errorf("failed to ensure masquerade rule: %v", err)
}
return nil
}
NodePort 또는 LoadBalancer 유형을 통해 외부에 노출되는 Service의 경우 iptables는 트래픽이 클러스터 외부에서 내부로 흐르도록 허용해야 합니다. NodePort 서비스의 경우 iptables 규칙은 아래와 같습니다.
// Handle traffic from NodePort
args := []string{"-t", "nat", "-A", string(kubeNodePortsChain),
"-p", "tcp", "--dport", "30000", "-j", "DNAT", "--to-destination", "<pod-ip>:<pod-port>"}
LoadBalancer 서비스는 아래와 같습니다.
// LoadBalancer traffic management
args := []string{"-t", "nat", "-A", string(kubeExternalServicesChain),
"-d", "<external-lb-ip>", "-p", "tcp", "--dport", "80", "-j", "DNAT", "--to-destination", "<pod-ip>:<pod-port>"}
iptables 규칙 모니터링
Canary 체인을 사용해 iptables 규칙이 항상 최신 상태를 유지하도록 모니터링 합니다.
func (proxier *Proxier) monitorCanaryChain() error {
// Canary rule to detect if the rules were flushed
args := []string{"-C", string(kubeProxyCanaryChain), "-j", "ACCEPT"}
if err := proxier.iptables.EnsureRule(utiliptables.Check, utiliptables.TableNAT, kubeProxyCanaryChain, args...); err != nil {
return fmt.Errorf("Canary rule missing, reloading all rules")
}
return nil
}
감사합니다.