새소식

Kubernetes

[Study 4주차] 쿠버네티스 Service - ClusterIP

  • -

CoudNet@ 팀의 가시다님께서 리딩하시는 KANS Study (Kubernetes Advanced Networking Study) 4주차 스터디 내용 정리

 

이번 주차에는 Service 타입 중 ClusterIPNodePort 에 대해 스터디를 진행했습니다.

이번 글에서는 ClusterIP 를 알아보겠습니다.

 

 

1. Kubernetes Service

 

1.1. Service 개요

 

서비스 동작 발전 과정

 

 

1. 파드 생성

서비스가 없을 경우, 파드가 재실행될 시에 파드의 IP 가 변경되기 때문에 아래와 같은 통신 문제가 발생합니다.

 

출처 : 가시다님 쿠버네티스 책 원고

 

2. 서비스 (Cluster Type) 연결

위와 같은 문제로 인해 서비스 (Service) 라는 리소스를 통해 파드에 대한 단일 진입점을 제공해줍니다.
서비스는 파드에 대한 고정 IP도메인네임 (Domain Name) 을 제공해줍니다.

출처 : 가시다님 쿠버네티스 책 원고

 

3. 서비스 (NodePort Type) 연결

하지만 Cluster Type 은 k8s 클러스터 내부에서만 접속이 가능하다는 단점이 있습니다.
외부에서도 서비스 접근을 위해 k8s 에서는 NodePort 타입의 서비스를 제공합니다.

 

출처 : 가시다님 쿠버네티스 책 원고

 

 

1.2. 실습 환경 구성

 

이번 주차에서 실습은 Kind 를 사용한 k8s 클러스터와 통신 테스트를 위한 docker 컨테이너 1대를 사용했습니다.

 

kind 클러스터 생성

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
    topology.kubernetes.io/zone: ap-northeast-2a
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        runtime-config: api/all=true
    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
  serviceSubnet: 10.200.1.0/24
EOT

# k8s 클러스터 설치
kind create cluster --config kind-svc-1w.yaml --name myk8s --image kindest/node:v1.31.0

# 노드에 기본 툴 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils 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 bsdmainutils bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done

# mypc 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 직접 지정
docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity

 

환경 구성 확인

# 컨테이너 확인
docker ps

# 컨테이너 IP 대역 확인
docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'

 

 

 

 

2. ClusterIP

 

출처 : 가시다님 쿠버네티스 책 원고

 

클러스터 내부에서만 ClusterIP 로 접근 가능합니다.
모든 노드에 iptables rule 이 설정되므로, 파드에서 접속 시 해당 노드에 존재하는 iptables rule 에 의해서 분산 접속이 됩니다.

 

 

2.1. 실습 환경 구성

 

파드 생성

# 목적지 파드 생성
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

# 서비스(Cluster IP) 생성
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

 

파드 배포

kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml

 

 

파드 환경 확인

 

# webpod IP 출력
kubectl get pod -l app=webpod -o jsonpath="{.items[*].status.podIP}"

# webpod 파드의 IP를 변수에 지정
WEBPOD1=$(kubectl get pod webpod1 -o jsonpath={.status.podIP})
WEBPOD2=$(kubectl get pod webpod2 -o jsonpath={.status.podIP})
WEBPOD3=$(kubectl get pod webpod3 -o jsonpath={.status.podIP})
echo $WEBPOD1 $WEBPOD2 $WEBPOD3

# net-pod 에서 webpod 로 Curl 접속
for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod; done

# 서비스 IP 변수 지정 : svc-clusterip 의 ClusterIP주소
SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
echo $SVC1

# 위 서비스 생성 시 kube-proxy 에 의해서 iptables 규칙이 모든 노드에 추가됨 
docker exec -it myk8s-control-plane iptables -t nat -S | grep $SVC1

 

 

서비스 생성 시, 노드 iptables 에 해당 서비스에 대한 규칙이 추가되는 것을 알 수 있습니다.

 

 

2.2. service 접근 테스트

 

# Service 의 80 포트로 접근
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1

# Service 의 9000 포트로 접근
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000

 

80 포트로는 접근이 불가하고 9000 포트로는 접근이 가능한데, 이는 서비스에서 진입 포트를 9000 포트로 지정했기 때문입니다.

 

Service LoadBalancing 테스트

 

그렇다면 서비스와 연결된 파드들에 대해 접근과 부하 분산은 어떻게 되는 지 확인해보겠습니다.

# 서비스 10 번 호출
kubectl exec -it net-pod -- zsh -c "for i in {1..10};   do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

# 서비스 100 번 호출
kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

# 서비스 1000 번 호출
kubectl exec -it net-pod -- zsh -c "for i in {1..1000}; do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

 

대략 33% 정도로 잘 분산되는 것을 알 수 있습니다.

 

Service 전송 패킷 확인

 

Worker Node 1번에서 파드의 패킷을 tcpdump 를 사용하여 확인합니다.

# 80 번 포트 모니터링
docker exec -it myk8s-worker tcpdump -i eth0 tcp port 80 -nnq

# 서비스 접근 테스트
kubectl exec -it net-pod -- zsh -c "for i in {1..10};   do curl -s $SVC1:9000 | grep Hostname; sleep 1; done"

 



 

2.3. iptables 정책 확인

 

앞서 서비스가 추가될 때, kube-proxy 가 iptable 에 규칙을 추가해준다고 하였습니다.
어떤 정책으로 iptable 규칙이 추가되는 지 확인해보겠습니다.

 

출처 : 가시다님 쿠버네티스 책 원고

 

 

k8s 서비스 통신 시, 노드에는 위와 같은 4 단계의 규칙으로 iptables 라우팅이 이루어집니다.
실제 실습 환경에서 해당 규칙을 확인해보겠습니다.

 

# Control Plane 노드에 접근
docker exec -it myk8s-control-plane bash

# 1단계) PREROUTING 테이블 정보 확인
iptables -v --numeric --table nat --list PREROUTING

# 2단계) KUBE-SERVICES 테이블 정보 확인
iptables -v --numeric --table nat --list KUBE-SERVICES

# 3단계) KUBE-SVC 테이블 정보 확인
iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF

# 4단계) KUBE-SEP 테이블 정보 확인
iptables -v --numeric --table nat --list KUBE-SEP-TBW2IYJKUCAC7GB3

 

 



2.4. 세션어피니티 (sessionAffinity)

 

세션어피니티는 클라이언트가 접속한 목적지(파드)에 고정적인 접속을 지원하는 기능

 

Sticky Session 방식과 유사하게 동작하며,
한 번 접근한 파드에 대한 상태 정보를 기록하고 확인하여 이후 연결 시도 시 동일한 파드로 접근할 수 있게 됩니다.

 

출처 : 가시다님 쿠버네티스 책 원고

 

# 기본 정보 확인
kubectl get svc svc-clusterip -o yaml | grep sessionAffinity

 

 

세션어피니티 적용

# 세션어피니티 적용
kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinity":"ClientIP"}}'

# 변경 상태 확인
kubectl get svc svc-clusterip -o yaml

 

 

세션어피니티 적용 확인

# 서비스에 1000번 접근
kubectl exec -it net-pod -- zsh -c "for i in {1..1000}; do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

 

 

모두 같은 파드로 접근되는 것을 알 수 있습니다.

 

세션어피니티의 세션 고정 시간의 기본값은 10800초 (3시간)이지만, 이를 수정할 수 있습니다.

# 타임아웃 시간을 30초로 변경
kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinityConfig":{"clientIP":{"timeoutSeconds":30}}}}'

 

 

30초가 지나면 다른 파드로 접근이 가능하게 됩니다.

 

 


 

이번에 ClusterIP 타입의 서비스를 아주 딥하게 공부 했는데, 아주 심화 내용이 많이 있었습니다.
그 동안 별 생각 없이 사용하고 있던 ClusterIP 에 이런 내용이 담겨 있을 줄은 상상도 못했는데, 너무나도 좋은 공부가 된 것 같아 너무 기쁩니다.
꼭 이번 스터디도 완주하여 많은 지식을 얻어갈 수 있도록 하겠습니다! :)

Contents

포스팅 주소를 복사했습니다