Kubespary HA & Upgrade - K8S API 엔드포인트

진웅·2026년 2월 7일

k8s deploy

목록 보기
16/20
post-thumbnail

Kubespray HA 클러스터 API 엔드포인트 아키텍처

API 엔드포인트 개요

Kubespray로 구축한 HA(High Availability) 클러스터에서 각 컴포넌트가 Kubernetes API Server에 접근하는 방식은 노드 타입에 따라 다르다.

  • Worker 노드: nginx static pod를 통한 Client-Side LoadBalancing
  • Control Plane 노드: 로컬 엔드포인트 (127.0.0.1:6443) 직접 연결

이러한 구조는 HA 환경에서 안정성과 성능을 동시에 확보하기 위한 설계다.


Case 1: Worker 노드의 Client-Side LoadBalancing

아키텍처 개요

Worker 노드는 자체적으로 nginx static pod를 실행하여 API Server에 대한 Client-Side 로드밸런싱을 수행한다.

Worker Node (k8s-node4, k8s-node5)
├── nginx-proxy (Static Pod)
│   ├── Listen: 127.0.0.1:6443
│   ├── Health Check: :8081/healthz
│   └── Upstream (least_conn)
│       ├── 192.168.10.11:6443 (k8s-node1)
│       ├── 192.168.10.12:6443 (k8s-node2)
│       └── 192.168.10.13:6443 (k8s-node3)
├── kubelet → https://localhost:6443
└── kube-proxy → https://127.0.0.1:6443

nginx static pod 확인

# Worker 노드의 컨테이너 목록 확인
ssh k8s-node4 crictl ps

출력 예시:

CONTAINER           IMAGE               CREATED             STATE               NAME                POD
3c09f930b22b0       5a91d90f47ddf       15 minutes ago      Running             nginx-proxy         nginx-proxy-k8s-node4

핵심 포인트:

  • nginx-proxy는 Static Pod로 실행된다
  • kubelet이 직접 관리하며, API Server 장애 시에도 동작한다
  • 각 Worker 노드마다 독립적으로 실행된다

nginx.conf 설정 분석

ssh k8s-node4 cat /etc/nginx/nginx.conf

주요 설정:

error_log stderr notice;

worker_processes 2;
worker_rlimit_nofile 130048;
worker_shutdown_timeout 10s;

events {
  multi_accept on;
  use epoll;
  worker_connections 16384;
}

stream {
  upstream kube_apiserver {
    least_conn;  # 로드밸런싱 알고리즘
    server 192.168.10.11:6443;
    server 192.168.10.12:6443;
    server 192.168.10.13:6443;
  }

  server {
    listen        127.0.0.1:6443;
    proxy_pass    kube_apiserver;
    proxy_timeout 10m;
    proxy_connect_timeout 1s;
  }
}

http {
  server {
    listen 8081;
    location /healthz {
      access_log off;
      return 200;
    }
  }
}

설정 항목 설명

worker_rlimit_nofile: 130048

  • nginx worker 프로세스가 열 수 있는 최대 파일 디스크립터 수
  • 대량의 동시 연결을 처리하기 위한 설정

upstream kube_apiserver

  • 3개의 Control Plane API Server를 백엔드로 설정
  • least_conn: 최소 연결 수 알고리즘 사용

왜 RoundRobin이 아닌 least_conn인가?

  • RoundRobin: 순차적으로 분산 (연결 상태 무시)
  • least_conn: 현재 활성 연결이 가장 적은 서버로 요청 전달
  • API Server는 long-lived connection(watch, stream)이 많아서 least_conn이 더 적합
  • 연결이 불균형하게 분산되는 것을 방지

listen 127.0.0.1:6443

  • 로컬 루프백 주소로만 리스닝
  • 외부에서 직접 접근 불가 (보안)
  • 동일 노드의 kubelet, kube-proxy만 접근 가능

healthz 엔드포인트

  • nginx 자체의 헬스체크 엔드포인트
  • 8081 포트에서 제공

nginx 동작 확인

# nginx 헬스체크
ssh k8s-node4 curl -s localhost:8081/healthz -I

출력:

HTTP/1.1 200 OK
Server: nginx
# nginx를 통한 API Server 접근 테스트
ssh k8s-node4 curl -sk https://127.0.0.1:6443/version | grep gitVersion

출력:

"gitVersion": "v1.32.9",
# nginx 리스닝 포트 확인
ssh k8s-node4 ss -tnlp | grep nginx

출력:

LISTEN 0  511  0.0.0.0:8081     0.0.0.0:*  users:(("nginx",pid=15043,fd=6))
LISTEN 0  511  127.0.0.1:6443   0.0.0.0:*  users:(("nginx",pid=15043,fd=5))

의미:

  • 8081: 헬스체크 엔드포인트 (모든 인터페이스)
  • 6443: API 프록시 (로컬호스트만)

kubelet 설정 확인

ssh k8s-node4 cat /etc/kubernetes/kubelet.conf | grep server

출력:

server: https://localhost:6443

의미:

  • kubelet은 localhost:6443으로 API Server에 접근
  • 실제로는 nginx-proxy로 연결됨
  • nginx가 3개의 Control Plane 중 하나로 프록시

kube-proxy 설정 확인

kubectl get cm -n kube-system kube-proxy -o yaml | grep 'kubeconfig.conf:' -A18

출력:

kubeconfig.conf: |-
  apiVersion: v1
  kind: Config
  clusters:
  - cluster:
      certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      server: https://127.0.0.1:6443
    name: default

의미:

  • kube-proxy도 127.0.0.1:6443으로 API Server에 접근
  • nginx-proxy를 통한 로드밸런싱

nginx 설정 파일 생성 과정

Kubespray 템플릿 파일 구조

# nginx.conf 생성 Task 확인
tree roles/kubernetes/node/tasks/loadbalancer
cat roles/kubernetes/node/tasks/loadbalancer/nginx-proxy.yml

주요 Task:

- name: Nginx-proxy | Write nginx-proxy configuration
  template:
    src: "loadbalancer/nginx.conf.j2"
    dest: "{{ nginx_config_dir }}/nginx.conf"
    owner: root
    mode: "0755"
    backup: true

nginx.conf.j2 템플릿 분석

cat roles/kubernetes/node/templates/loadbalancer/nginx.conf.j2

핵심 부분:

stream {
  upstream kube_apiserver {
    least_conn;
    {% for host in groups['kube_control_plane'] -%}
    server {{ hostvars[host]['main_access_ip'] | ansible.utils.ipwrap }}:{{ kube_apiserver_port }};
    {% endfor -%}
  }

  server {
    listen        127.0.0.1:{{ loadbalancer_apiserver_port|default(kube_apiserver_port) }};
    {% if ipv6_stack -%}
    listen        [::1]:{{ loadbalancer_apiserver_port|default(kube_apiserver_port) }};
    {% endif -%}
    proxy_pass    kube_apiserver;
    proxy_timeout 10m;
    proxy_connect_timeout 1s;
  }
}

템플릿 동작 원리:
1. groups['kube_control_plane']: Ansible inventory의 Control Plane 그룹
2. hostvars[host]['main_access_ip']: 각 Control Plane 노드의 IP 추출
3. kube_apiserver_port: 기본값 6443
4. Jinja2 for 루프로 모든 Control Plane을 upstream에 추가

Static Pod 매니페스트

cat roles/kubernetes/node/templates/manifests/nginx-proxy.manifest.j2

주요 내용:

apiVersion: v1
kind: Pod
metadata:
  name: {{ loadbalancer_apiserver_pod_name }}
  namespace: kube-system
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    k8s-app: kube-nginx
  annotations:
    nginx-cfg-checksum: "{{ nginx_stat.stat.checksum }}"
spec:
  containers:
  - name: nginx-proxy
    image: {{ nginx_image_repo }}:{{ nginx_image_tag }}
    volumeMounts:
    - name: nginx-config
      mountPath: /etc/nginx
  volumes:
  - name: nginx-config
    hostPath:
      path: {{ nginx_config_dir }}

특징:

  • Static Pod이므로 /etc/kubernetes/manifests/에 위치
  • kubelet이 직접 실행 및 관리
  • API Server가 다운되어도 계속 실행됨

Case 2: Control Plane 노드의 로컬 엔드포인트

아키텍처 개요

Control Plane 노드는 자신의 로컬 API Server에 직접 연결한다.

Control Plane Node (k8s-node1, k8s-node2, k8s-node3)
├── kube-apiserver
│   ├── --bind-address=::
│   ├── --advertise-address=192.168.10.11
│   └── Listen: *:6443 (IPv4/IPv6)
├── kubelet → https://127.0.0.1:6443
├── kube-proxy → https://127.0.0.1:6443
├── kube-controller-manager → https://127.0.0.1:6443
└── kube-scheduler → https://127.0.0.1:6443

kube-apiserver 바인딩 설정 확인

kubectl describe pod -n kube-system kube-apiserver-k8s-node1 | grep -E 'address|secure-port'

출력:

Annotations: kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 192.168.10.11:6443
--advertise-address=192.168.10.11
--secure-port=6443
--bind-address=::

주요 설정 항목:

--bind-address=::

  • IPv6 와일드카드 주소 (::는 IPv6의 0.0.0.0)
  • IPv4와 IPv6 모두 리스닝
  • 모든 네트워크 인터페이스에서 접근 가능

--advertise-address=192.168.10.11

  • 클러스터 내부에 광고할 IP 주소
  • 다른 노드들이 이 IP로 API Server에 접근
  • 외부 로드밸런서나 다른 노드가 사용

--secure-port=6443

  • HTTPS 포트
  • 기본값이며 변경 가능

리스닝 포트 확인

ssh k8s-node1 ss -tnlp | grep 6443

출력:

LISTEN 0  4096  *:6443  *:*  users:(("kube-apiserver",pid=26124,fd=3))

의미:

  • *:6443: 모든 인터페이스에서 리스닝
  • IPv4/IPv6 모두 지원

다양한 인터페이스로 API 접근 테스트

# 로컬호스트로 접근
ssh k8s-node1 curl -sk https://127.0.0.1:6443/version | grep gitVersion
# 출력: "gitVersion": "v1.32.9",

# 실제 IP로 접근
ssh k8s-node1 curl -sk https://192.168.10.11:6443/version | grep gitVersion
# 출력: "gitVersion": "v1.32.9",

# NAT IP로 접근 (VirtualBox NAT)
ssh k8s-node1 curl -sk https://10.0.2.15:6443/version | grep gitVersion
# 출력: "gitVersion": "v1.32.9",

결론:

  • API Server는 모든 네트워크 인터페이스에서 접근 가능
  • 하지만 Control Plane 컴포넌트는 로컬호스트만 사용

Control Plane 컴포넌트의 API 엔드포인트

admin 자격증명

ssh k8s-node1 cat /etc/kubernetes/admin.conf | grep server

출력:

server: https://127.0.0.1:6443

super-admin 자격증명

ssh k8s-node1 cat /etc/kubernetes/super-admin.conf | grep server

출력:

server: https://192.168.10.11:6443

중요: super-admin.conf실제 IP 주소를 사용한다!

이유:

  • 최고 권한 관리자 설정
  • 노드 간 접근이 필요한 경우를 대비
  • 긴급 복구 시 다른 노드에서도 사용 가능

kubelet

ssh k8s-node1 cat /etc/kubernetes/kubelet.conf | grep server

출력:

server: https://127.0.0.1:6443

kube-proxy

kubectl get cm -n kube-system kube-proxy -o yaml | grep server

출력:

server: https://127.0.0.1:6443

kube-controller-manager

ssh k8s-node1 cat /etc/kubernetes/controller-manager.conf | grep server

출력:

server: https://127.0.0.1:6443

kube-scheduler

ssh k8s-node1 cat /etc/kubernetes/scheduler.conf | grep server

출력:

server: https://127.0.0.1:6443

왜 Control Plane은 로컬호스트를 사용하는가?

  1. 성능: 네트워크 스택을 거치지 않고 직접 통신
  2. 안정성: 네트워크 장애와 무관하게 동작
  3. 보안: 외부 네트워크 노출 최소화
  4. 단순성: 복잡한 로드밸런싱 불필요

Client-Side LB를 사용하는 이유

Worker 노드는 왜 nginx를 사용하는가?

Worker 노드는 어느 Control Plane이 살아있는지 알 수 없다.

문제점:

  • Control Plane 1번 노드만 설정했다면?
    • 1번 장애 시 전체 Worker가 API Server에 접근 불가
    • 수동으로 2번이나 3번으로 변경 필요

해결책:

  • nginx static pod를 통한 Client-Side LoadBalancing
  • 3개의 Control Plane을 모두 upstream으로 설정
  • nginx가 자동으로 살아있는 노드로 요청 전달

Control Plane은 왜 nginx를 사용하지 않는가?

Control Plane은 자신의 로컬 API Server가 항상 있다.

이유:

  • 로컬 API Server가 다운되면 어차피 해당 노드는 비정상
  • 굳이 다른 노드의 API Server로 연결할 필요 없음
  • 로컬 연결이 더 빠르고 안정적

nginx static pod의 rlimit 설정 문제 해결

문제 발견

kubectl logs -n kube-system nginx-proxy-k8s-node4

로그:

2026/01/28 04:02:40 [alert] 20#20: setrlimit(RLIMIT_NOFILE, 130048) failed (1: Operation not permitted)
2026/01/28 04:02:40 [alert] 21#21: setrlimit(RLIMIT_NOFILE, 130048) failed (1: Operation not permitted)

원인:

  • nginx.conf에서 worker_rlimit_nofile 130048 설정
  • containerd의 기본 OCI Spec에서 RLIMIT_NOFILE이 65535로 제한
  • nginx worker 프로세스가 130048로 올리려다 실패

containerd 설정 확인

ssh k8s-node4 cat /etc/containerd/config.toml | grep base_runtime_spec

출력:

base_runtime_spec = "/etc/containerd/cri-base.json"
ssh k8s-node4 cat /etc/containerd/cri-base.json | jq | grep rlimits -A 6

출력:

"rlimits": [
  {
    "type": "RLIMIT_NOFILE",
    "hard": 65535,
    "soft": 65535
  }
]

해결 방법: rlimits 제한 제거

# containerd 기본 변수 확인
cat roles/container-engine/containerd/defaults/main.yml

출력:

containerd_base_runtime_spec_rlimit_nofile: 65535
containerd_default_base_runtime_spec_patch:
  process:
    rlimits:
      - type: RLIMIT_NOFILE
        hard: "{{ containerd_base_runtime_spec_rlimit_nofile }}"
        soft: "{{ containerd_base_runtime_spec_rlimit_nofile }}"

group_vars에 Override 설정 추가:

cat << EOF >> inventory/mycluster/group_vars/all/containerd.yml
containerd_default_base_runtime_spec_patch:
  process:
    rlimits: []  # rlimits 제한 제거
EOF

Ansible Tags를 사용한 부분 적용

# containerd 관련 Task 목록 확인
ansible-playbook -i inventory/mycluster/inventory.ini -v cluster.yml \
  --tags "containerd" --list-tasks

주요 Task:

container-engine/containerd : Containerd | Generate default base_runtime_spec
container-engine/containerd : Containerd | Store generated default base_runtime_spec
container-engine/containerd : Containerd | Write base_runtime_specs
container-engine/containerd : Containerd | Copy containerd config file

특정 노드에만 적용

# k8s-node4에만 containerd 재설정
ansible-playbook -i inventory/mycluster/inventory.ini -v cluster.yml \
  --tags "containerd" \
  --limit k8s-node4 \
  -e kube_version="1.32.9"

옵션 설명:

  • --tags "containerd": containerd 관련 Task만 실행
  • --limit k8s-node4: k8s-node4에만 적용
  • -e kube_version="1.32.9": Kubernetes 버전 명시

설정 확인

ssh k8s-node4 cat /etc/containerd/cri-base.json | jq | grep rlimits

출력:

"rlimits": [],

의미:

  • rlimits 제한이 제거됨
  • nginx가 자유롭게 파일 디스크립터 수를 조정 가능

컨테이너 재시작

# nginx-proxy Pod 강제 재시작
ssh k8s-node4 crictl pods --namespace kube-system --name 'nginx-proxy-*' -q | xargs crictl rmp -f

재시작 후 로그 확인

kubectl logs -n kube-system nginx-proxy-k8s-node4

정상 로그:

2026/01/28 17:50:00 [notice] 1#1: start worker processes
2026/01/28 17:50:00 [notice] 1#1: start worker process 20
2026/01/28 17:50:00 [notice] 1#1: start worker process 21

alert 메시지가 사라짐!


Kubespray 지원 Client-Side LB

Kubespray는 다음 3가지 Client-Side LoadBalancer를 지원한다:

1. nginx (기본값)

장점:

  • 가볍고 빠름
  • Static Pod로 실행되어 안정적
  • 설정이 단순

단점:

  • 고급 헬스체크 기능 부족

2. haproxy

장점:

  • 더 정교한 로드밸런싱
  • 고급 헬스체크 기능
  • 상세한 통계 정보

단점:

  • nginx보다 무거움

3. kube-vip

장점:

  • Kubernetes 네이티브
  • VIP(Virtual IP) 지원
  • Leader Election 기능

단점:

  • 설정이 복잡
  • 상대적으로 최신 기술

선택 가이드:

  • 일반적인 경우: nginx (기본값)
  • 고급 헬스체크 필요: haproxy
  • VIP 기능 필요: kube-vip

Ansible Tags 활용

Tags란?

Ansible Task에 붙이는 레이블이다. 특정 Tag가 붙은 Task만 선택적으로 실행할 수 있다.

Tags 목록 확인

# cluster.yml의 모든 Task와 Tag 확인
ansible-playbook -i inventory/mycluster/inventory.ini cluster.yml --list-tasks

# 특정 Tag의 Task만 확인
ansible-playbook -i inventory/mycluster/inventory.ini cluster.yml \
  --tags "containerd" --list-tasks

자주 사용하는 Tags

Tag설명사용 예시
containerdcontainerd 엔진 설정컨테이너 런타임 재설정
download바이너리 다운로드오프라인 설치 준비
etcdetcd 클러스터 설정etcd 재설정
k8s-clusterKubernetes 클러스터 전체전체 재배포
network네트워크 플러그인CNI 변경
apps애드온 앱 설치Metrics Server 등

Tags 사용 예시

# containerd만 재설정
ansible-playbook -i inventory/mycluster/inventory.ini cluster.yml \
  --tags "containerd"

# download Task만 실행 (바이너리 다운로드만)
ansible-playbook -i inventory/mycluster/inventory.ini cluster.yml \
  --tags "download"

# 여러 Tag 동시 사용
ansible-playbook -i inventory/mycluster/inventory.ini cluster.yml \
  --tags "containerd,network"

# 특정 Tag 제외
ansible-playbook -i inventory/mycluster/inventory.ini cluster.yml \
  --skip-tags "download"

Tags 정의 위치 찾기

# playbooks/ 디렉터리에서 tags 검색
grep -Rni "tags" playbooks/ -A2 -B1

# roles/ 디렉터리에서 tags 검색 (YAML 파일만)
grep -Rni "tags" roles/ --include="*.yml" -A2 -B1

DaemonSet Pod의 API 엔드포인트

Cilium 예시

Cilium과 같은 DaemonSet으로 배포된 Pod들도 API Server에 접근해야 한다.

Cilium Helm values 설정 예시:

k8sServiceHost: 127.0.0.1
k8sServicePort: 6443

Worker 노드의 Cilium Pod

Worker 노드에서 실행되는 Cilium Pod:

  • 127.0.0.1:6443으로 API Server에 연결
  • 실제로는 nginx-proxy를 통해 로드밸런싱
  • 자동으로 살아있는 Control Plane으로 연결

Control Plane 노드의 Cilium Pod

Control Plane 노드에서 실행되는 Cilium Pod:

  • 127.0.0.1:6443으로 API Server에 연결
  • 로컬 API Server에 직접 연결
  • nginx 없이도 안정적으로 동작

결론:

  • DaemonSet은 동일한 엔드포인트 (127.0.0.1:6443) 사용
  • 노드 타입에 따라 자동으로 적절한 경로 선택
  • Worker: nginx-proxy → Control Plane
  • Control Plane: 로컬 API Server

핵심 포인트 요약

API 엔드포인트 전략

  1. Worker 노드 → API Server

    • nginx static pod를 통한 Client-Side LoadBalancing
    • 3개의 Control Plane에 부하 분산
    • least_conn 알고리즘 사용
  2. Control Plane 노드 → API Server

    • 로컬 엔드포인트 (127.0.0.1:6443) 직접 연결
    • 성능과 안정성 최적화
    • super-admin.conf만 실제 IP 사용
  3. nginx 설정의 핵심

    • worker_rlimit_nofile: 대량 연결 처리
    • least_conn: 불균형 방지
    • healthz: nginx 자체 헬스체크
  4. containerd rlimits 제한 제거

    • nginx의 파일 디스크립터 제한 해결
    • --tags "containerd"로 부분 적용
    • --limit <node>로 특정 노드만 적용
  5. API Server 바인딩

    • --bind-address=::: IPv4/IPv6 모두 지원
    • --advertise-address: 클러스터 내부 광고 IP
    • 모든 인터페이스에서 접근 가능하지만 컴포넌트는 로컬만 사용

설계 의도

안정성:

  • Worker는 Client-Side LB로 단일 장애점 제거
  • Control Plane은 로컬 연결로 네트워크 장애 영향 최소화

성능:

  • Control Plane은 로컬 연결로 지연시간 최소화
  • Worker는 least_conn으로 부하 균등 분산

단순성:

  • 동일한 엔드포인트 주소 사용 (127.0.0.1:6443 또는 localhost:6443)
  • 노드 타입에 따라 자동으로 적절한 경로 선택

참고 자료

profile
bytebliss

0개의 댓글