brunch

You can make anything
by writing

C.S.Lewis

by 이권수 Jul 24. 2023

Karpenter 작동 원리

어떻게 Karpenter는 노드를 관리할까?

Karpenter는 노드 생성 및 삭제를 도와주는 오픈소스 도구이다. 클러스터 상에 파드를 올릴 노드가 없으면 Karpenter는 노드를 생성한다. Karpenter는 자신이 생성한 노드의 수명 주기를 관리한다. 사용자는 서비스에 맞게 노드의 사양을 지정할 수 있다. Karpenter는 사용자 설정에 따라서 노드를 늘렸다가 줄일 뿐이다.


쿠버네티스 환경에서 노드 관리는 중요한 요소이다. 노드가 없으면 애플리케이션 파드를 띄울 수 없고, 노드가 지나치게 많으면 비용이 많이 나온다. 그렇다고 매 순간 모니터링을 하면서 사용자가 노드를 관리할 수도 없는 노릇이다. 그래서 트래픽에 따라 필요한 만큼 노드를 늘리고 줄여주는 기능이 반드시 필요하다. 


Karpenter는 이러한 요구사항을 만족시켜 주는 솔루션 중 하나이다.

이번 포스트에서는 Karpenter가 어떻게 노드를 관리하는지 알아보고자 한다. 


파드는 어떻게 노드에 올라갈까?

쿠버네티스에서 파드는 하나의 애플리케이션 단위이다. 즉, 가장 기본이 되는 단위이다. 만약 5개의 애플리케이션을 운영한다면 보통 5개의 서로 다른 파드를 생성한다. 하나의 파드에는 여러 개의 컨테이너가 동작할 수 있지만, 서로 다른 애플리케이션을 같은 파드에서 운영하지는 않는다.


사용자는 쿠버네티스 API를 통해 파드를 생성할 수 있다. 쿠버네티스 API 서버는 API 요청을 받으면 해당 정보를 etcd라는 저장소에 저장한다. 정보를 저장하더라도 바로 파드를 생성하지 않는다. 실제로 생성을 요청하는 녀석은 쿠버네티스 스케쥴러이다.


쿠버네티스 스케쥴러는 아직 노드에 스케쥴링되지 않는 파드가 있는지 쿠버네티스 API 서버에 물어본다. 만약 스케쥴링이 필요한 파드가 있으면 요청 스펙을 보고 적합한 노드를 찾는다. 예컨대, CPU가 4 core 이상 필요하다면 4 core 이상의 여유가 있는 노드만 선별하고, 그 중 가장 적합하다고 생각되는 노드를 선정한다. 선정이 완료되면 선정된 노드에 파드를 생성해 달라고 쿠버네티스 API 서버에 요청한다.


각 노드의 Kubelet은 자신의 노드에 할당된 파드가 있는지 지속적으로 확인한다. 만약 노드에 할당된 파드가 있으면 쿠버네티스 API 서버에 파드 정보를 요청해서 스펙을 확인하고, 그에 맞게 파드를 생성한다. 이때 파드 생성에 실패할 수도 있다. 파드를 생성하지 못하더라도 쿠버네티스 스케쥴러는 다시 노드를 선정하지 않는다. 노드를 지정(bind)한 후에는 더 이상 스케쥴러가 파드 생성에 관여하지 않는다.


Karpenter가 동작하는 시점

그러면 Karpenter는 언제 동작해야 할까?


바로, 스케쥴러가 적합한 노드를 찾지 못할 때이다. Karpenter 컨트롤러는 준실시간으로 스케쥴링되지 않은 파드를 찾는다. 한 번 쿠버네티스 스케쥴러가 스케쥴링을 시도했음에도 여전히 Pending 상태라면 Karpenter는 일을 시작


출처: AWS 공식 블로그


Provisioner 코드 중 GetPendingPods() 함수를 살펴보면, 할당된 노드가 없는 파드 리스트를 불러온다.

그 후 각 파드를 돌면서 IsProvisionable() 함수를 통해 정말 노드를 추가해야 하는지 확인한다. 


노드를 추가해야하는 조건은 다음과 같다.

스케쥴링되어 있지 않다.

선점(Preempt)되어 있지 않다.

스케쥴링에 실패했다.

Daemonset으로 생성된 파드가 아니다.

Static 파드가 아니다.


이 중 하나라도 False 조건이라면 Karpenter는 해당 파드를 무시한다.


조건에 충족되는 파드가 있다면, Karpenter 컨트롤러는 새로운 노드를 생성한다.


Karpenter는 어떻게 적합한 노드 사양을 선택할까?

Karpenter는 사용자가 지정한 조건에 맞게 노드 사양을 선택한다. 사용자는 Provisioner라는 CRD(Custom Resource Definition)를 만든다. Provisioner는 하나의 노드 관리 그룹을 의미한다. 사용자는 Provisioner를 만들 때 관리하고자 하는 노드의 정보를 넣을 수 있다. 


예컨대, 아래의 경우 GPU 서버 관리를 위한 Provisioner이다. requirements에 보면 p3.8xlarge, p3.16xlarge 타입의 인스턴스만 사용하도록 설정하였다. 그리고 taints를 통해 해당 노드에 할당할 수 있는 파드를 한정하고 있다.

출처: https://karpenter.sh/docs/concepts/provisioners/#example-use-cases


이렇게 사용자는 라벨을 사용해서 인스턴스 사양을 제한할 수 있다. 만약 아무것도 지정하지 않으면 Karpenter는 기본으로 아래의 사양을 선택한다.

kubernetes.io/os: linux

kubernetes.io/arch: amd64

karpenter.sh/capacity-type: on-demand

karpenter.k8s.aws/instance-category: ["c", "m", "r"]

karpenter.k8s.aws/instance-generation: "2" 초과


참고로, Default 설정 코드는 아래 SetDefaults() 함수를 통해 확인할 수 있다.


Provisioner는 전체 EC2 인스턴스 타입을 불러오고, 그 중 사용자가 지정한 설정에 부합하는 타입만 걸러낸다. 그 후, EC2 Fleet 요청을 통해 가장 적합한 인스턴스를 생성한다. 다시 말해, karpenter가 최종 타입을 결정하지 않는다. karpenter는 필요한 요구 조건에 부합되지 않는 타입을 제외한 모든 인스턴스 타입을 EC2 Fleet에 전달하고, EC2 Fleet이 가장 적합한 노드를 선정한다.


아래는 Karpenter가 CreateFleet API를 통해 인스턴스를 생성하는 코드이다. 



Karpenter는 어떻게 노드를 삭제할까? 

Karpenter는 주기적으로 여러 개의 deprovisioner를 실행한다. Deprovisioner는 특정 조건에 부합하는 노드를 삭제하는 역할을 한다. 현재 기본으로 들어가는 deprovisioner는 다음과 같다. 위에서부터 우선순위가 높게 적용되는 deprovisioner이다.

기본으로 설정된 deprovisioner 리스트


각 deprovisioner에는 ShouldDeprovision() 함수가 존재한다. 이 함수는 해당 deprovisioner가 동작해야 하는 조건을 검사한다. 즉, 노드를 내려도 되는 조건을 확인한다는 의미이다.


예컨대, Emptiness deprovisioner의 로직은 다음과 같다. 코드에서 알 수 있듯이 ttlSecondsAfterEmpty 값이 존재하는 경우에만 Emptiness deprovisioner가 동작한다. 만약 해당 값을 설정하지 않으면 deprovisioner는 동작하지 않는다. 만약 파드가 떠있지 않는 노드를 종료하고 싶다면, ttlSecondsAfterEmpty 값을 지정하면 된다. 그러면 karpenter는 파드가 없는 상태에서 ttlSecondsAfterEmpty 초만큼 지난 노드를 삭제한다.

Emptiness deprovisioner의 ShouldDeprovision() 로직


Emptiness의 경우, 파드가 존재하지 않을 때 사용하는 deprovisioner이다. 이 경우에는 노드를 그냥 바로 내려도 된다. 하지만 노드 안에 아직 파드가 존재한다면 갑자기 노드를 내려서는 안된다. 특히 반드시 수행해야 하는 로직이 있다면 현재 작업을 마무리한 후에 종료해야 한다. 


사용자는 특정 파드가 떠 있는 노드를 karpenter가 내리지 못하도록 막을 수 있다. 바로 파드의 어노테이션에 karpenter.sh/do-not-evict: "true"를 추가하면 된다. 아래는 제거할 대상을 고르는 함수이다. 

Candidate(제거할 대상 노드)를 고르는 함수


중간에 보면 hasDoNotEvictPod() 함수가 보인다. 해당 함수는 어노테이션에서 karpenter.sh/do-not-evict 값이 설정되어 있는지 확인한다. 만약, 해당 값이 true이면 방출(evict) 대상에서 제외한다.


파드 어노테이션에 karpenter.sh/do-not-evict = "true"가 있으면 제거하지 않는다.


deprovisioner가 종료를 시작해도 Finalizer 때문에 노드가 죽지 않는다. 이때 termination 컨트롤러는 먼저 노드에 스케쥴링되지 않도록 cordon 작업을 한다. Cordon 작업이 끝나면 노드에서 모든 파드를 방출한다. 이후에는 EC2 인스턴스를 제거하고, 비로소 Finalizer를 없앤다. 


이렇게 노드가 종료되고 나면 deprovisioner는 새롭게 삭제할 노드를 물색한다.




참고자료

매거진의 이전글 istio proxy의 작동원리
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari