6.1.2 워커 노드 (Worker Nodes): 애플리케이션 실행의 최전선

컨트롤 플레인이라는 쿠버네티스 클러스터의 ‘두뇌’를 살펴보았다면, 이제 실제 애플리케이션 컨테이너들이 생명을 얻고 열심히 일하는 ‘손과 발’, 즉 워커 노드(Worker Nodes)에 대해 알아볼 차례입니다. 워커 노드는 과거에는 ‘미니언(minion)’이라고 불리기도 했지만, 현재는 ‘워커 노드’라는 명칭이 공식적으로 사용됩니다. 이 노드들은 컨트롤 플레인의 지휘를 받아 우리가 배포한 컨테이너화된 애플리케이션들을 실행하고 관리하는 실질적인 작업 공간입니다. 클러스터의 규모에 따라 수십, 수백, 심지어 수천 개의 워커 노드가 존재할 수 있으며, 이들이 모여 강력한 컴퓨팅 파워를 제공하고 애플리케이션의 고가용성과 확장성을 뒷받침합니다.

각 워커 노드는 물리 서버일 수도 있고, 가상 머신(VM)일 수도 있습니다. 중요한 것은 각 워커 노드가 컨트롤 플레인과 통신하며 할당된 작업을 수행하고, 컨테이너를 실행하는 데 필요한 환경을 갖추고 있다는 점입니다. 이제 각 워커 노드를 구성하는 핵심적인 ‘일꾼’들을 만나보겠습니다. 이들은 마치 현장의 작업반장, 교통경찰, 그리고 실제 기계를 다루는 기술자처럼 각자의 중요한 역할을 수행합니다.

6.1.2.1 kubelet: 노드 에이전트 (파드 생명주기 관리)

독자 여러분, 쿠버네티스 워커 노드의 심장 박동과도 같은 존재, 바로 kubelet(큐블릿)에 대해 더욱 깊이 파고들어 보겠습니다. kubelet은 각 워커 노드에 설치되어 실행되는 핵심 에이전트(agent)로서, 컨트롤 플레인의 지시를 현장에서 직접 이행하는 ‘최일선 작업반장’ 또는 ‘현장 감독관’에 비유할 수 있습니다. 이 작은 에이전트가 없다면, 워커 노드는 컨트롤 플레인의 의도를 전혀 알 수 없고, 결과적으로 컨테이너화된 애플리케이션을 실행하거나 관리할 수 없게 됩니다. kubelet은 그만큼 워커 노드의 기능성에 있어 중추적인 역할을 담당합니다.

kubelet의 가장 기본적인 임무는 컨트롤 플레인의 중앙 API 서버인 kube-apiserver와 지속적으로 통신하며, 자신이 속한 노드에 어떤 파드(Pod)들이 배치되어야 하는지, 그리고 그 파드들이 어떤 상태를 유지해야 하는지에 대한 상세한 지시사항, 즉 파드 명세(PodSpec)를 받아오는 것입니다. 이 파드 명세는 마치 정교한 작업 지시서와 같아서, 어떤 컨테이너 이미지를 사용해야 하는지, 컨테이너 실행 시 어떤 명령어를 내려야 하는지, 어떤 포트를 개방해야 하는지, 어떤 환경 변수를 설정해야 하는지, 어떤 종류의 스토리지를 얼마나 연결해야 하는지 등 파드를 구성하고 실행하는 데 필요한 모든 세부 정보가 담겨 있습니다.

이 지시를 받은 kubelet은 단순히 정보를 전달하는 것을 넘어, 실제 컨테이너들을 생성하고, 시작하며, 필요에 따라 중지시키는 등 파드의 전체 생명주기(lifecycle)를 철저하게 관리합니다. 또한, 실행 중인 컨테이너들이 건강하게 작동하고 있는지 주기적으로 점검하고, 그 결과를 다시 kube-apiserver에 보고하여 컨트롤 플레인이 항상 최신 상태를 파악할 수 있도록 합니다. 이제 kubelet이 수행하는 구체적인 임무들을 하나하나 자세히 살펴보겠습니다.

파드 명세 수신 및 관리: “오늘의 작업 목록은 무엇인가?”
  • API 서버를 통한 파드 명세 수신 (주된 방식): kubelet은 주기적으로 kube-apiserver에 자신이 담당해야 할 파드 목록을 문의합니다. 이는 보통 kube-scheduler에 의해 특정 노드(즉, kubelet 자신이 실행 중인 노드)에 할당된 파드들입니다. kube-apiserver는 해당 노드의 kubelet에게 이 파드들의 상세한 명세(PodSpec)를 전달합니다. kubelet은 이 명세를 기준으로 파드와 그 내부 컨테이너들의 상태를 관리하며, 만약 현재 노드의 상태가 명세와 다르다면 이를 일치시키기 위한 작업을 수행합니다. (예: 새로운 파드 생성, 불필요한 파드 삭제)
  • 정적 파드(Static Pod) 관리: kube-apiserver를 통하지 않고 kubelet이 직접 관리하는 특별한 종류의 파드도 있습니다. 이는 워커 노드의 로컬 파일 시스템 내 특정 디렉토리(보통 /etc/kubernetes/manifests/로 설정됨, 이 경로는 kubelet 설정 파일에서 지정 가능)에 YAML 또는 JSON 형식의 파드 명세 파일을 직접 넣어두면, kubelet이 이 파일을 주기적으로 감지하여 해당 파드를 실행하고 관리하는 방식입니다. 이를 정적 파드(Static Pod)라고 부릅니다. 정적 파드는 주로 컨트롤 플레인 컴포넌트 자체(예: kube-apiserver, etcd, kube-scheduler, kube-controller-manager 등)를 컨테이너화하여 실행하고 관리하는 데 사용됩니다. 특히, kubeadm과 같은 도구를 사용하여 클러스터를 부트스트래핑할 때 컨트롤 플레인 컴포넌트들이 정적 파드 형태로 시작됩니다. kubelet은 정적 파드에 대해 ‘미러 파드(mirror pod)’를 kube-apiserver에 생성하여, kubectl get pods와 같이 API 서버를 통해 정적 파드의 상태를 조회할 수 있도록 하지만, 실제 생명주기 관리는 전적으로 해당 노드의 kubelet에 의해 이루어집니다. 만약 정적 파드 명세 파일이 삭제되면 kubelet은 해당 파드를 자동으로 중지시킵니다.

컨테이너 실행 및 관리 (컨테이너 런타임 연동): “엔진을 켜고 작업을 시작하라!”
  • 파드 명세를 확보한 kubelet은 이제 실제 컨테이너를 구동시켜야 합니다. 이 작업을 위해 kubelet은 해당 워커 노드에 설치된 컨테이너 런타임(Container Runtime)과 긴밀하게 협력합니다. 쿠버네티스는 CRI(Container Runtime Interface)라는 표준화된 인터페이스(gRPC 기반 API)를 통해 다양한 종류의 컨테이너 런타임(예: containerd, CRI-O)과 통신할 수 있는 유연성을 제공합니다.
  • kubelet은 CRI를 통해 컨테이너 런타임에게 다음과 같은 구체적인 명령을 내립니다.
    • 이미지 풀링(Image Pulling): 파드 명세에 지정된 컨테이너 이미지가 로컬에 없으면, 컨테이너 레지스트리(예: Docker Hub, GCR, ECR, 또는 사설 레지스트리)에서 해당 이미지를 가져오도록(pull) 지시합니다. 이때 이미지 풀 정책(imagePullPolicy: Always, IfNotPresent, Never)과 이미지 풀 시크릿(imagePullSecrets) 설정도 고려합니다.
    • 컨테이너 생성 및 시작(Container Creation & Start): 다운로드한 이미지를 기반으로 컨테이너를 생성하고 시작하도록 요청합니다. 이 과정에는 컨테이너를 위한 네트워크 네임스페이스 설정 (파드 내 다른 컨테이너와 공유), 마운트 포인트 설정, cgroups를 이용한 리소스 격리 및 제한 적용, 루트 파일시스템 준비 등 복잡한 운영체제 수준의 작업들이 컨테이너 런타임에 의해 수행됩니다.
    • 컨테이너 중지 및 삭제(Container Stop & Deletion): 파드가 삭제되거나 컨테이너가 재시작되어야 할 때, 기존 컨테이너를 중지하고 삭제하도록 명령합니다.
볼륨 관리: “필요한 도구와 저장 공간을 준비하라!”
  • 파드는 종종 데이터를 저장하거나 컨테이너 간에 데이터를 공유하기 위해 다양한 종류의 볼륨(Volume)을 필요로 합니다. kubelet은 파드 명세에 정의된 볼륨들을 컨테이너가 접근할 수 있도록 준비하고 마운트하는 중요한 역할을 담당합니다.
  • kubelet이 관리하는 볼륨의 종류는 매우 다양합니다.
    • 로컬 볼륨: emptyDir(파드의 생명주기 동안 임시 데이터를 저장하는 빈 디렉토리), hostPath(워커 노드의 파일시스템 경로를 파드에 직접 마운트, 보안상 주의 필요), configMap(설정 데이터를 파일 형태로 주입), secret(민감한 데이터를 파일 형태로 주입) 등이 있습니다.
    • 네트워크 스토리지 (CSI 이전 방식): 과거에는 awsElasticBlockStore, gcePersistentDisk, azureDisk, nfs, cephfs, glusterfs 등 특정 스토리지 기술에 대한 볼륨 플러그인 로직이 kubelet 코드 내에 포함되어 있었습니다 (in-tree plugins). kubelet은 이러한 플러그인을 통해 PersistentVolumeClaim(PVC)을 통해 요청된 네트워크 스토리지를 실제 워커 노드에 연결(attach)하고 컨테이너에 마운트(mount)했습니다.
    • CSI (Container Storage Interface)를 통한 볼륨 관리 (현재 표준): 스토리지 플러그인 로직을 쿠버네티스 코어에서 분리하기 위해 CSI라는 표준 인터페이스가 도입되었습니다. kubelet은 CSI 드라이버(노드에 배포된 외부 컴포넌트)와 통신하여 볼륨의 프로비저닝, 연결, 마운트, 해제, 삭제 등의 작업을 수행합니다. 이를 통해 새로운 스토리지 기술을 쿠버네티스 릴리스 주기와 독립적으로 손쉽게 통합할 수 있게 되었습니다. kubelet은 CSI 노드 플러그인과 상호작용하여 파드가 요청한 볼륨을 사용할 수 있도록 준비합니다.
  • kubelet은 컨테이너가 시작되기 전에 필요한 볼륨들을 마운트하고, 컨테이너가 정상적으로 종료된 후에는 해당 볼륨들을 안전하게 언마운트(unmount)하고 필요한 경우 연결을 해제(detach)하는 작업을 수행합니다.
컨테이너 상태 점검 (프로브, Probes): “모든 것이 계획대로 잘 돌아가고 있는가?”
  • 애플리케이션이 컨테이너 내부에서 실행 중이라고 해서 항상 정상적으로 작동한다고 보장할 수는 없습니다. 때로는 교착 상태(deadlock)에 빠지거나, 초기화가 덜 끝나 요청을 처리할 준비가 되지 않았을 수도 있습니다. kubelet은 파드 명세에 정의된 프로브(Probe) 설정을 사용하여 주기적으로 컨테이너의 건강 상태를 면밀히 점검합니다.
  • kubelet이 지원하는 프로브의 종류는 다음과 같습니다.
    • 라이브니스 프로브(Liveness Probe): 컨테이너가 여전히 ‘살아있는지’, 즉 애플리케이션이 정상적으로 응답하고 있는지 확인합니다. 만약 라이브니스 프로브가 일정 횟수 이상 실패하면, kubelet은 해당 컨테이너를 비정상으로 간주하고 파드의 재시작 정책(restartPolicy: Always, OnFailure, Never)에 따라 컨테이너를 자동으로 재시작합니다. 이는 애플리케이션이 내부적인 문제로 멈춰버렸을 때 자동으로 복구하는 데 매우 유용합니다.
    • 레디니스 프로브(Readiness Probe): 컨테이너가 외부로부터 들어오는 네트워크 요청을 처리할 ‘준비가 되었는지’ 확인합니다. 레디니스 프로브가 성공해야만 해당 파드는 쿠버네티스 서비스(Service)의 엔드포인트 목록에 포함되어 실제 사용자 트래픽을 받기 시작합니다. 애플리케이션이 시작하는 데 시간이 오래 걸리거나, 일시적으로 많은 부하를 받아 새로운 요청을 처리할 수 없는 상태일 때, 레디니스 프로브를 통해 해당 파드로 트래픽이 전달되는 것을 일시적으로 중단시킬 수 있습니다.
    • 스타트업 프로브(Startup Probe): 컨테이너 내의 애플리케이션이 성공적으로 시작되었는지 확인합니다. 특히 초기 구동에 오랜 시간이 소요되는 레거시 애플리케이션이나 복잡한 초기화 과정을 거치는 애플리케이션에 유용합니다. 스타트업 프로브가 성공할 때까지 라이브니스 프로브와 레디니스 프로브의 실행은 지연됩니다. 이를 통해 애플리케이션이 아직 완전히 시작되지 않았음에도 불구하고 라이브니스 프로브 실패로 인해 불필요하게 재시작되는 것을 방지할 수 있습니다.
  • 이러한 프로브들은 다음과 같은 다양한 방식으로 정의될 수 있습니다.
    • HTTP GET 요청 (httpGet): 컨테이너 내의 특정 HTTP 엔드포인트로 GET 요청을 보내고, 응답 상태 코드가 200-399 범위인지 확인합니다.
    • TCP 소켓 연결 시도 (tcpSocket): 컨테이너의 특정 포트로 TCP 연결을 시도하고, 연결이 성공하는지 확인합니다.
    • 컨테이너 내 명령어 실행 (exec): 컨테이너 내에서 지정된 명령어를 실행하고, 종료 코드(exit code)가 0인지 확인합니다.
  • 각 프로브에는 initialDelaySeconds(첫 프로브 실행 전 대기 시간), periodSeconds(프로브 실행 간격), timeoutSeconds(프로브 타임아웃 시간), successThreshold(성공으로 간주하기 위한 연속 성공 횟수), failureThreshold(실패로 간주하기 위한 연속 실패 횟수) 등 세부 동작을 제어하는 파라미터들을 설정할 수 있습니다.
노드 및 파드 상태 보고: “현장 상황을 본부에 보고하라!”
  • kubelet은 자신이 관리하는 워커 노드의 전반적인 상태와, 해당 노드에서 실행 중인 각 파드의 현재 상태를 주기적으로 kube-apiserver에 상세히 보고합니다.
  • 노드 상태 보고: kubelet은 노드의 사용 가능한 리소스(CPU, 메모리, 디스크 공간 등), 노드에 설정된 다양한 컨디션(예: Ready, MemoryPressure, DiskPressure, PIDPressure, NetworkUnavailable), 노드의 IP 주소, 호스트 이름, OS 정보, 커널 버전, 컨테이너 런타임 버전 등 중요한 정보들을 Node 오브젝트의 status 필드에 업데이트합니다. Node의 Ready 컨디션은 해당 노드가 새로운 파드를 받아들일 준비가 되었는지 여부를 나타내는 중요한 지표입니다.
  • 파드 상태 보고: 각 파드에 대해서도 현재 단계(Phase: Pending, Running, Succeeded, Failed, Unknown), 컨테이너들의 상태(ContainerStatuses: waiting, running, terminated 및 각 상태의 상세 이유, 재시작 횟수 등), 파드의 IP 주소, 파드 컨디션(예: Initialized, Ready, ContainersReady, PodScheduled) 등을 Pod 오브젝트의 status 필드에 업데이트합니다.
  • 이러한 상세한 상태 정보는 컨트롤 플레인(특히 kube-scheduler와 kube-controller-manager)이 클러스터 전체의 상황을 정확히 파악하고, 스케줄링 결정을 내리거나, 자가 치유 메커니즘을 작동시키는 데 필수적인 기초 데이터가 됩니다.
리소스 관리 및 격리 (cgroups 및 QoS): “자원을 공정하고 효율적으로 분배하라!”
  • kubelet은 워커 노드의 한정된 시스템 리소스(CPU, 메모리 등)가 여러 파드들에 의해 공정하고 효율적으로 사용되도록 관리하는 중요한 책임을 가집니다. 이는 주로 컨테이너 런타임 및 운영체제 커널의 기능(특히 리눅스의 cgroups와 네임스페이스)과 협력하여 이루어집니다.
  • 리소스 요청(requests) 및 제한(limits) 강제: 파드 명세에서 각 컨테이너는 자신이 필요로 하는 CPU 및 메모리 양을 ‘요청(requests)’하고, 최대로 사용할 수 있는 양을 ‘제한(limits)’으로 설정할 수 있습니다. kubelet은 (컨테이너 런타임을 통해) cgroups를 사용하여 이러한 요청과 제한을 실제로 적용합니다. requests는 스케줄링 시 노드에 충분한 자원이 있는지 판단하는 기준이 되며, limits는 컨테이너가 설정된 양 이상의 리소스를 사용하지 못하도록 강제합니다.
  • QoS (Quality of Service) 클래스 관리: 쿠버네티스는 파드의 리소스 요청 및 제한 설정에 따라 각 파드에 대해 다음과 같은 세 가지 QoS 클래스 중 하나를 할당합니다.
    • Guaranteed: 파드 내 모든 컨테이너가 CPU와 메모리 모두에 대해 요청량과 제한량을 명시하고, 그 값이 동일할 때 할당됩니다. 이 파드들은 가장 높은 우선순위를 가지며, 시스템 리소스 부족 시 가장 마지막에 축출(evict)될 가능성이 높습니다.
    • Burstable: 최소한 하나 이상의 컨테이너가 CPU 또는 메모리에 대해 요청량을 명시했지만, Guaranteed 조건을 만족하지 못할 때 할당됩니다. (예: 요청량 < 제한량, 또는 일부 컨테이너만 요청/제한 설정)
    • BestEffort: 파드 내 어떤 컨테이너도 CPU나 메모리에 대한 요청량이나 제한량을 명시하지 않았을 때 할당됩니다. 이 파드들은 가장 낮은 우선순위를 가지며, 시스템 리소스 부족 시 가장 먼저 축출될 대상이 됩니다.kubelet은 이러한 QoS 클래스를 인지하고, 노드에 리소스 압박(pressure)이 발생했을 때(예: 메모리 부족), 파드 축출(eviction) 결정을 내릴 때 이 QoS 클래스와 파드 우선순위(Pod Priority)를 고려하여 어떤 파드를 먼저 종료시킬지 결정합니다.
  • 노드 할당 가능 리소스(Node Allocatable Resources): kubelet은 노드의 전체 용량 중에서 쿠버네티스 워크로드(파드)에 실제 할당 가능한 리소스 양을 계산합니다. 이는 운영체제 자체나 kubelet과 같은 시스템 데몬이 사용하는 리소스를 제외한 값입니다. kubelet은 이 ‘할당 가능 리소스’ 범위 내에서만 파드들이 리소스를 사용하도록 관리하여 노드 전체의 안정성을 확보합니다.

이처럼 kubelet은 워커 노드에서 쿠버네티스의 철학인 ‘선언적 상태 관리’와 ‘자동화’를 실제로 구현하는 매우 복잡하고 정교한 작업을 수행하는 핵심 컴포넌트입니다. kubelet이 자신의 임무를 제대로 수행하지 못하면, 해당 워커 노드는 클러스터로부터 고립되어 아무런 쓸모가 없게 됩니다. 따라서 kubelet의 로그를 주기적으로 확인하고, 그 설정을 올바르게 유지하며, 버전 호환성을 관리하는 것은 쿠버네티스 클러스터 운영에 있어 매우 중요한 부분입니다. 안정적인 kubelet의 운영은 건강하고 효율적인 워커 노드의 초석이라 할 수 있습니다.

6.1.2.2 kube-proxy: 네트워크 프록시 및 로드 밸런싱

kube-proxy(큐브 프록시)는 각 워커 노드에서 실행되며, 쿠버네티스 클러스터 내의 네트워크 규칙을 관리하고 기본적인 로드 밸런싱 기능을 제공하는 핵심적인 네트워크 컴포넌트입니다. 마치 각 노드에 배치된 ‘교통 경찰’처럼, 파드들이 서로 통신하고 외부와 통신할 수 있도록 네트워크 경로를 설정하고 관리하며, 특히 쿠버네티스 Service 오브젝트의 마법을 현실로 만들어주는 중요한 역할을 담당합니다.

Service는 여러 개의 파드에 대한 안정적인 단일 접근점(IP 주소와 포트)을 제공하여, 파드들의 IP 주소가 동적으로 변경되더라도 클라이언트가 항상 동일한 주소로 서비스에 접근할 수 있게 해줍니다. kube-proxy는 바로 이 Service의 가상 IP 주소(ClusterIP)로 들어오는 요청을, 실제로 해당 서비스를 제공하는 백엔드 파드들 중 하나로 전달(forwarding)하는 역할을 수행합니다.

kube-proxy는 이러한 기능을 구현하기 위해 다음과 같은 여러 가지 모드(mode) 중 하나로 동작할 수 있습니다. 각 모드는 서로 다른 기술을 사용하여 네트워크 규칙을 관리합니다.

  1. userspace 모드 (레거시):
    • 가장 오래된 방식이며, 현재는 거의 사용되지 않습니다.
    • kube-proxy 자신이 직접 프록시 서버 역할을 수행합니다. 서비스의 ClusterIP:Port로 오는 요청을 kube-proxy가 수신한 다음, 백엔드 파드 중 하나를 선택하여 해당 파드로 요청을 전달합니다.
    • 모든 트래픽이 유저 공간(userspace)의 kube-proxy 프로세스를 거쳐야 하므로 커널 공간(kernel space)에서 처리하는 방식보다 성능이 떨어지고 병목 현상이 발생하기 쉽습니다.
  2. iptables 모드:
    • 현재 많은 클러스터에서 기본값으로 사용되거나 널리 사용되는 방식입니다.
    • kube-proxy는 리눅스 커널의 넷필터(Netfilter) 기능, 특히 iptables 규칙을 사용하여 서비스 라우팅 및 로드 밸런싱을 구현합니다.
    • kube-proxy는 kube-apiserver로부터 Service와 Endpoints(또는 EndpointSlice) 정보를 감시하다가 변경 사항이 발생하면, 해당 정보를 바탕으로 iptables 규칙을 동적으로 생성하고 업데이트합니다. 이 규칙들은 특정 서비스의 ClusterIP:Port로 향하는 트래픽을 DNAT(Destination Network Address Translation)하여 실제 백엔드 파드의 IP:Port로 전달하도록 설정됩니다.
    • 여러 개의 백엔드 파드가 있는 경우, iptables의 통계 모듈(statistic module)이나 nth 모듈 등을 사용하여 간단한 라운드 로빈(Round Robin) 방식의 로드 밸런싱을 수행할 수 있습니다.
    • 트래픽이 커널 공간에서 직접 처리되므로 userspace 모드보다 훨씬 빠르고 효율적입니다. 하지만, 서비스와 엔드포인트 수가 매우 많아지면 iptables 규칙의 수도 엄청나게 증가하여 규칙 업데이트 및 관리에 오버헤드가 발생하고, 특정 상황에서는 성능 저하를 유발할 수 있다는 단점이 있습니다.
  3. IPVS (IP Virtual Server) 모드:
    • iptables 모드의 확장성 문제를 해결하기 위해 등장한 최신 방식 중 하나이며, 대규모 클러스터에서 점점 더 많이 채택되고 있습니다.
    • IPVS는 리눅스 커널에 내장된 고성능 L4 로드 밸런서입니다. 해시 테이블을 사용하여 서비스와 백엔드 정보를 관리하므로, iptables 방식보다 훨씬 많은 수의 서비스와 엔드포인트를 효율적으로 처리할 수 있습니다.
    • kube-proxy는 Service와 Endpoints 정보를 바탕으로 IPVS 가상 서버(virtual server)와 실제 서버(real server)를 생성하고 관리합니다.
    • IPVS는 라운드 로빈(Round Robin), 최소 연결(Least Connection), 가중치 기반 라운드 로빈(Weighted Round Robin), 목적지 해싱(Destination Hashing), 소스 해싱(Source Hashing) 등 iptables보다 훨씬 다양한 로드 밸런싱 알고리즘을 지원합니다.
    • iptables 모드에 비해 규칙 동기화 시간이 짧고, 대규모 환경에서 더 나은 성능과 확장성을 제공하는 것으로 알려져 있습니다. IPVS 모드를 사용하려면 노드 커널에 IPVS 모듈이 로드되어 있어야 합니다. IPVS 모드에서도 일부 기능(예: Masquerading, SNAT)을 위해 여전히 iptables을 보조적으로 사용하기도 합니다.
  4. Kernelspace 모드 (Windows):
    • Windows 워커 노드에서 사용되는 방식이며, Windows의 VFP(Virtual Filtering Platform)를 활용하여 kube-proxy와 유사한 기능을 구현합니다.

kube-proxy는 단순히 서비스의 ClusterIP에 대한 프록시 역할만 하는 것이 아니라, NodePort 서비스나 LoadBalancer 타입의 서비스가 외부에서 노드의 특정 포트로 접근했을 때, 해당 트래픽을 적절한 파드로 전달하는 역할도 담당합니다. 또한, 일부 네트워크 정책(NetworkPolicy) 구현과 관련된 규칙 설정에도 관여할 수 있습니다.

결론적으로, kube-proxy는 쿠버네티스 네트워킹의 복잡성을 사용자로부터 감추고, 서비스라는 추상화된 개념을 통해 파드 간의 안정적이고 유연한 통신을 가능하게 만드는 숨은 공로자입니다. 어떤 모드로 동작하든, kube-proxy는 클러스터의 모든 워커 노드에서 실행되며, 컨트롤 플레인과의 긴밀한 협력을 통해 쿠버네티스 네트워킹의 마법을 현실로 만들어냅니다.

6.1.2.3 컨테이너 런타임 (Container Runtime): 컨테이너 실행

컨테이너 런타임(Container Runtime)은 워커 노드에서 실제로 컨테이너를 실행하고 관리하는 소프트웨어입니다. 우리가 애플리케이션을 패키징한 컨테이너 이미지를 가져와서, 격리된 환경에서 프로세스로 실행시키고, 그 생명주기를 관리하는 모든 실질적인 작업을 담당하는 엔진이라고 할 수 있습니다. kubelet이 파드 명세에 따라 “이 컨테이너 이미지를 사용하여 컨테이너를 실행해줘”라고 지시하면, 컨테이너 런타임이 이 지시를 받아 구체적인 작업을 수행합니다.

과거에는 Docker(정확히는 dockershim)가 쿠버네티스의 주요 컨테이너 런타임으로 널리 사용되었지만, 쿠버네티스 생태계가 발전하면서 더 표준화되고 가벼운 접근 방식이 요구되었습니다. 이에 따라 쿠버네티스는 CRI(Container Runtime Interface)라는 표준 플러그인 인터페이스를 정의하게 되었습니다. CRI는 kubelet과 다양한 컨테이너 런타임 간의 통신 규약(gRPC 기반 API)으로, kubelet은 CRI를 통해 컨테이너 런타임에게 컨테이너 및 이미지 서비스에 대한 요청을 보낼 수 있습니다. 이 CRI 덕분에 쿠버네티스는 특정 컨테이너 런타임 기술에 종속되지 않고, CRI 스펙을 준수하는 다양한 런타임들을 유연하게 선택하여 사용할 수 있게 되었습니다. (Docker는 원래 CRI를 직접 구현하지 않았기 때문에 dockershim이라는 중간 계층이 필요했으며, 쿠버네티스 v1.20부터 dockershim은 deprecated 되었고 v1.24에서 완전히 제거되었습니다. 현재는 CRI 호환 런타임을 직접 사용하는 것이 표준입니다.)

CRI 호환 컨테이너 런타임의 주요 역할은 다음과 같습니다.

  1. 이미지 관리:
    • 컨테이너 이미지를 레지스트리(예: Docker Hub, Quay.io, Harbor 등 사설 레지스트리)에서 가져오고(pull), 로컬에 저장하며, 필요 없는 이미지를 삭제하는(prune) 기능을 수행합니다.
    • 이미지 레이어(layer) 관리, 이미지 서명 검증 등 이미지와 관련된 다양한 작업을 처리합니다.
  2. 컨테이너 생명주기 관리:
    • 파드에 정의된 컨테이너 명세에 따라 실제 컨테이너를 생성하고 시작합니다. 이 과정에는 컨테이너를 위한 네트워크 네임스페이스 설정, 마운트 포인트 설정, cgroups를 이용한 리소스 제한 적용, 루트 파일시스템 준비 등 복잡한 작업들이 포함됩니다.
    • 실행 중인 컨테이너를 중지(stop)하거나 삭제(remove)합니다.
    • 컨테이너의 상태(실행 중, 중지됨 등)를 조회하고, 컨테이너 로그를 수집하여 kubelet에 전달하는 인터페이스를 제공합니다.
  3. 샌드박스(Sandbox) 관리 (파드 수준):
    • CRI는 컨테이너뿐만 아니라 ‘파드 샌드박스(Pod Sandbox)’라는 개념도 관리합니다. 파드 샌드박스는 파드 내의 모든 컨테이너가 공유하는 실행 환경(예: 네트워크 네임스페이스, IPC 네임스페이스, PID 네임스페이스 옵션)을 제공합니다.
    • 일반적으로 파드가 생성될 때, 먼저 파드 샌드박스(종종 ‘pause’ 컨테이너라는 매우 가벼운 특수 컨테이너를 사용하여 구현됨)가 생성되고, 이후에 해당 파드에 속한 애플리케이션 컨테이너들이 이 샌드박스 환경 내에서 실행됩니다. 이를 통해 파드 내 컨테이너들은 localhost를 통해 서로 통신하거나, 볼륨을 공유하는 등의 작업을 수행할 수 있습니다.
  4. 리소스 격리 및 제한:
    • 리눅스 커널의 cgroups(control groups)와 네임스페이스(namespaces) 기능을 활용하여 각 컨테이너가 격리된 환경에서 실행되고, 할당된 CPU, 메모리 등의 리소스만 사용하도록 제한합니다. 이는 컨테이너 런타임이 OS 커널과 직접 상호작용하며 수행하는 중요한 역할입니다.

현재 널리 사용되는 CRI 호환 컨테이너 런타임에는 다음과 같은 것들이 있습니다.

  • containerd (컨테이너디):
    • 원래 Docker의 핵심 컴포넌트였으나, CNCF(Cloud Native Computing Foundation)에 기증되어 독립적인 프로젝트로 발전했습니다.
    • 경량이며, 안정성과 성능이 뛰어나고, CRI 플러그인을 통해 쿠버네티스와 완벽하게 호환됩니다.
    • 현재 많은 주요 쿠버네티스 배포판 및 매니지드 서비스에서 기본 컨테이너 런타임으로 채택되고 있습니다. containerd는 runc과 같은 OCI(Open Container Initiative) 호환 로우레벨 런타임을 사용하여 실제 컨테이너를 실행합니다.
  • CRI-O (씨알아이-오):
    • 쿠버네티스 CRI 스펙을 만족시키기 위해 특별히 설계된 경량 컨테이너 런타임입니다. Docker와 관련된 코드를 전혀 포함하지 않는다는 특징이 있습니다.
    • Red Hat이 주도적으로 개발하고 있으며, OpenShift와 같은 플랫폼에서 주로 사용됩니다. containerd와 마찬가지로 runc과 같은 OCI 호환 로우레벨 런타임을 사용합니다.
    • CRI 스펙의 변경 사항을 빠르게 반영하고, 쿠버네티스와의 긴밀한 통합을 목표로 합니다.

이 외에도 frakti(가상 머신 기반 런타임) 등 다양한 목적의 컨테이너 런타임들이 존재할 수 있지만, containerd와 CRI-O가 현재 쿠버네티스 생태계에서 가장 지배적인 CRI 호환 런타임입니다.

결론적으로, 컨테이너 런타임은 kubelet의 지시를 받아 컨테이너라는 마법 상자를 실제로 열고 닫으며, 그 안의 애플리케이션이 안전하고 효율적으로 실행될 수 있도록 모든 기술적인 기반을 제공하는 매우 중요한 컴포넌트입니다. 쿠버네티스가 CRI라는 표준 인터페이스를 통해 다양한 런타임과 협력할 수 있게 됨으로써, 사용자들은 자신의 환경과 요구사항에 가장 적합한 런타임을 선택할 수 있는 유연성을 가지게 되었습니다.

지금까지 워커 노드를 구성하는 세 가지 핵심 요소인 kubelet, kube-proxy, 그리고 컨테이너 런타임에 대해 자세히 알아보았습니다. 이들이 각자의 위치에서 맡은 바 임무를 충실히 수행함으로써, 컨트롤 플레인이 구상한 애플리케이션 배포 계획이 현실 세계에서 완벽하게 구현될 수 있는 것입니다.