CloudNet@ Team Study/EKS Workshop 4th Cohort

EKS Nodes Upgrade : Karpenter Node Upgrade

"Everything about infra" 2026. 5. 11. 19:29
포스팅은 CloudNet@팀 서종호(Gasida)님이 진행하시는
AWS EKS Workshop Study 내용을 참고하여 작성합니다.

 

이번 강의는 AWS 정영준님께서

EKS 클러스터 업그레이드의 모범 사례에 대해서 열강 해주셨는데요,

오늘은 NodeGroup Upgrade 중, Karpenter 노드 업그레이드 관련하여

포스팅 해보도록 하겠습니다.

 

 

 

Karpenter 노드는 왜 업그레이드 방식이 다른가?

Managed Node Group은 AWS가 Auto Scaling Group을 통해 노드 라이프사이클을 관리합니다.

버전을 올리면 ASG가 새 Launch Template 버전을 만들고 롤링 교체를 진행합니다.

 

Karpenter는 구조 자체가 다릅니다.

ASG를 거치지 않고 Karpenter 컨트롤러가 EC2 인스턴스를 직접 프로비저닝하고 삭제합니다.
노드 설정의 소스는 NodePool과 EC2NodeClass 두 CRD입니다.

NodePool은 어떤 스펙의 노드를 프로비저닝할지 정의하고,
EC2NodeClass는 어떤 AMI로 노드를 띄울지를 포함한 AWS 인프라 설정을 담습니다.

따라서 Karpenter 노드의 K8s 버전을 올리려면 EC2NodeClass에 지정된 AMI를 새 버전으로 바꿔야 합니다.

이걸 트리거로 Karpenter가 Drift를 감지하고 노드를 교체합니다.

 

Drift란 무엇인가 ?

Drift는 현재 프로비저닝된 노드의 실제 상태와 NodePool / EC2NodeClass에 정의된

desired 상태 사이의 차이를 말합니다.

EC2NodeClass의 AMI가 바뀌면, 기존 노드는 구 AMI로 떠 있는 상태이고 desired는 신 AMI입니다.
이 차이를 Karpenter가 감지하면 Drift가 발생한 것으로 판단하고, 해당 노드를 새 설정으로 교체하는 작업을 시작합니다.

 

교체 흐름은 다음과 같습니다.

EC2NodeClass AMI 변경 → Drift 감지
    ↓
새 AMI로 신규 노드 프로비저닝
    ↓
기존 노드 Cordon (karpenter.sh/disruption taint 추가)
    ↓
기존 노드 파드 Eviction (PDB 존중)
    ↓
기존 노드 Terminate

MNG의 롤링 업데이트와 흐름이 비슷하지만,

ASG가 아닌 Karpenter 컨트롤러가 이 모든 과정을 직접 처리한다는 점이 다릅니다.

 

Disruption Budget이 필요한 이유

Drift가 발생하면 Karpenter는 기본적으로 전체 노드의 10%만 동시에 교체합니다.

그런데 이걸 더 세밀하게 제어하고 싶을 때 NodePool의 spec.disruption.budgets를 사용합니다.

 

예를 들어 Karpenter 노드가 10대 있을 때 AMI를 바꾸면 기본 10% 정책으로는 1대씩 교체됩니다.

그런데 Empty(파드 없는 노드) 교체는 즉시 다 해도 되고, Drift로 인한 교체만 1대씩 하고 싶다면 이렇게 씁니다.

budgets:
  - nodes: "1"
    reasons:
      - Drifted
  - nodes: "100%"
    reasons:
      - Empty
      - Underutilized

업무 시간 중에는 자발적 교체를 아예 막고 싶다면 이렇게 씁니다.

budgets:
  - schedule: "0 9 * * mon-fri"
    duration: 8h
    nodes: "0"
  - nodes: "10%"

이번 실습에서는 Karpenter 노드가 2대 있는 상태에서 nodes: "1" budget을 걸어서

한 대씩 순차적으로 교체 되는 것을 직접 확인합니다.

budget이 없으면 2대가 동시에 교체되어 짧은 시간 동안 서비스 가용성이 떨어질 수 있기 때문입니다.

 


사전 준비 : 현재 Karpenter 환경 확인

업그레이드 전에 현재 NodePool과 EC2NodeClass 설정을 파악합니다.

 

Step1. NodePool 확인 (핵심 항목 출력)

$ kubectl describe nodepool

Spec:
  Disruption:
    Budgets:
      Nodes: 10%               # 기본 budget: 전체의 10%만 동시 교체
    Consolidation Policy: WhenEmpty
  Limits:
    Cpu: 100
  Template:
    Metadata:
      Labels:
        Env:  dev
        Team: checkout         # 이 NodePool로 뜬 노드에는 team=checkout 레이블이 붙음
    Spec:
      Expire After: 720h       # 30일 TTL: 노드가 30일 지나면 자동 교체 대상
      Node Class Ref:
        Name: default          # 아래 확인할 EC2NodeClass 참조
      Taints:
        Key:    dedicated
        Value:  CheckoutApp
        Effect: NoSchedule     # checkout toleration 없는 파드는 이 노드에 스케줄링 불가

team=checkout 레이블로 Karpenter가 프로비저닝한 노드를 쉽게 필터링할 수 있습니다.

dedicated=CheckoutApp:NoSchedule Taint로 checkout 앱 파드만 이 노드에 뜨도록 격리되어 있습니다.

 

Step2. EC2NodeClass 확인

$ kubectl describe ec2nodeclass

Spec:
  Ami Family: AL2023
  Ami Selector Terms:
    Id: ami-0f676a166352f02ab  # 현재 1.30용 AMI가 직접 지정되어 있음
  Role: karpenter-eksworkshop-eksctl
  Security Group Selector Terms:
    Tags:
      karpenter.sh/discovery: eksworkshop-eksctl
  Subnet Selector Terms:
    Tags:
      karpenter.sh/discovery: eksworkshop-eksctl

AMI ID가 직접 지정(id: ami-0f676a166352f02ab)되어 있습니다. 이 AMI가 1.30 기준입니다.

이 값을 1.31용 AMI ID로 바꾸는 순간 Karpenter가 Drift를 감지합니다.

 

Step3. 현재 Karpenter 노드 확인

$ kubectl get nodes -l team=checkout

NAME                                       STATUS   ROLES    AGE   VERSION
ip-10-0-2-174.us-west-2.compute.internal   Ready    <none>   9h    v1.30.14-eks-f69f56f

현재 Karpenter 노드는 1대이고 v1.30입니다.

# 이 노드에 어떤 Taint가 걸려 있는지 확인
$ kubectl get nodes -l team=checkout \
  -o jsonpath="{range .items[*]}{.metadata.name} {.spec.taints[?(@.effect=='NoSchedule')]}{\"\n\"}{end}"
  
ip-10-0-2-174.us-west-2.compute.internal {"effect":"NoSchedule","key":"dedicated","value":"CheckoutApp"}

dedicated=CheckoutApp:NoSchedule Taint가 확인됩니다.

checkout 파드는 이 toleration을 가지고 있어서 이 노드에 뜰 수 있습니다.

 

Step4. checkout 파드 현황 확인

$ kubectl get pods -n checkout -o wide

NAME                             READY   STATUS    AGE   NODE
checkout-9c674566c-rvqcd         1/1     Running   9h    ip-10-0-2-174...
checkout-redis-97ff8589d-6b9kf   1/1     Running   9h    ip-10-0-2-174...

checkout 앱과 checkout-redis가 Karpenter 노드에서 돌고 있습니다.

 


Node Provisioning via Karpenter

Karpenter를 통해 1.31 버전의 노드를 올리고, 기존 1.30 버전의 노드를 삭제하는 실습을 해보겠습니다.

 

Step1. Karpenter 노드 2대 확보 : checkout 파드 10개로 증설

Disruption Budget 시연을 하려면 Karpenter 노드가 최소 2대 이상 있어야 합니다.

노드 1대일 때 budget을 걸어봤자 1대 교체하고 끝이라 순차 교체 효과를 눈으로 볼 수 없기 때문입니다.

 

checkout 파드를 10개로 늘리면 기존 1대 노드의 리소스만으로는 10개 파드의 요청을 감당할 수 없고,

Karpenter가 자동으로 추가 노드를 프로비저닝합니다.

 

모니터링을 먼저 켜둡니다.

아래 명령은 1초마다 NodeClaim 목록, team=checkout 노드 목록, 해당 노드의
Taint, checkout 파드 배치 상태를 한 번에 출력합니다.
파드가 늘어나면서 Karpenter가 새 NodeClaim을 생성하고 노드가 추가되는 과정을 실시간으로 확인할 수 있습니다.
while true; do
  kubectl get nodeclaim
  echo
  kubectl get nodes -l team=checkout
  echo
  kubectl get nodes -l team=checkout \
    -o jsonpath="{range .items[*]}{.metadata.name} {.spec.taints}{\"\n\"}{end}"
  echo
  kubectl get pods -n checkout -o wide
  echo; date; sleep 1
done

GitOps 저장소에서 checkout Deployment의 replicas를 1에서 10으로 변경하고 커밋합니다.

cd ~/environment/eks-gitops-repo
# deployment.yaml에서 replicas: 1 → replicas: 10 으로 변경

git add apps/checkout/deployment.yaml
git commit -m "scale checkout app"
git push --set-upstream origin main

argocd app sync checkout
더보기

ArgoCD sync 대신 kubectl로 직접 올려도 됩니다.

'kubectl scale deploy checkout -n checkout --replicas 10'

파드 10대가 정상적으로 증설이 완료되었습니다.

 

잠시 후 모니터링 출력에서 Karpenter가 새 NodeClaim을 생성하고 노드가

1대 더 추가되는 것을 확인할 수 있습니다.

$ kubectl get nodes -l team=checkout

NAME                                       STATUS   ROLES    AGE     VERSION
ip-10-0-2-174.us-west-2.compute.internal   Ready    <none>   9h      v1.30.14-eks-f69f56f
ip-10-0-8-63.us-west-2.compute.internal    Ready    <none>   2m40s   v1.30.14-eks-f69f56f

새로 뜬 노드도 EC2NodeClass에 지정된 현재 AMI(1.30용)로 프로비저닝됐기 때문에 v1.30입니다.

이제 2대가 준비됐습니다.

 

Step2. 1.31용 AMI ID 확인

현재 EC2NodeClass에 어떤 AMI가 지정되어 있는지 먼저 확인합니다.

kubectl get ec2nodeclass default -o yaml | grep 'id: ami-' | uniq

- id: ami-0f676a166352f02ab   # 현재 1.30용 AMI

SSM에서 1.31 EKS Optimized AMI ID를 조회합니다.

$ aws ssm get-parameter \
  --name /aws/service/eks/optimized-ami/1.31/amazon-linux-2023/x86_64/standard/recommended/image_id \
  --region ${AWS_REGION} \
  --query "Parameter.Value" --output text
  
ami-00e0cfd6e5895fe3a

이 AMI ID를 EC2NodeClass에 적용하면 기존 노드의 AMI(ami-0f676a166352f02ab)와

달라지면서 Drift가 발생합니다.

 

Step3. EC2NodeClass AMI 변경 + Disruption Budget 추가

두 파일을 동시에 변경합니다.

 

1) AMI ID 교체 (apps/karpenter/default-ec2nc.yaml)

spec:
  amiSelectorTerms:
    - id: ami-00e0cfd6e5895fe3a   # 1.30용 ami-0f676a166352f02ab → 1.31용으로 교체

 

2) Disruption Budget 추가 (apps/karpenter/default-np.yaml)

기존 spec.disruption 아래에 budget을 추가합니다.

spec:
  disruption:
    budgets:
      - nodes: "10%"             # 기존 기본 budget
      - nodes: "1"               # 추가: Drift로 인한 교체는 한 번에 1대만
        reasons:
          - Drifted
    consolidationPolicy: WhenEmpty

nodes: "1"과 reasons: [Drifted]의 조합이 핵심입니다.

Drift로 인한 교체는 한 번에 1대씩만 진행되고, 첫 번째 노드 교체가 완전히 끝난 후에야 두 번째 노드 교체가 시작됩니다.
이 budget이 없으면 2대가 동시에 교체될 수 있어 checkout 파드 가용성이 일시적으로 떨어질 수 있습니다.

 

두 파일을 커밋하고 ArgoCD로 동기화합니다.

cd ~/environment/eks-gitops-repo
git add apps/karpenter/default-ec2nc.yaml apps/karpenter/default-np.yaml
git commit -m "disruption changes"
git push --set-upstream origin main

argocd app sync karpenter

ArgoCD가 default EC2NodeClass에 새 AMI ID를 적용하는 순간, Karpenter 컨트롤러가

기존 노드의 AMI와 EC2NodeClass의 desired AMI가 다르다는 것을 감지하고 Drift를 발생시킵니다.

 

Step4. Drift 진행 과정 모니터링

Karpenter 컨트롤러 로그로 Drift 처리 과정을 실시간으로 확인합니다.

로그에서 어떤 노드가 교체 대상으로 선택됐는지, 새 노드가 언제 프로비저닝됐는지,

기존 노드가 언제 삭제됐는지 모두 기록됩니다.

kubectl -n karpenter logs deployment/karpenter -c controller --tail=33 -f

 

로그 출력을 보면 교체가 한 대씩 순차적으로 진행되는 것이 확인됩니다.

# 첫 번째 노드 교체 시작
{"message":"disrupting nodeclaim(s) via replace, terminating 1 nodes","reason":"drifted"}
{"message":"created nodeclaim","NodePool":{"name":"default"},"NodeClaim":{"name":"default-j882g"}}
{"message":"launched nodeclaim","instance-type":"c4.large","zone":"us-west-2c"}
{"message":"registered nodeclaim","Node":{"name":"ip-10-0-38-70..."}}
{"message":"initialized nodeclaim","Node":{"name":"ip-10-0-38-70..."}}

# 기존 노드 disruption taint 추가 후 삭제
{"message":"tainted node","Node":{"name":"ip-10-0-8-63..."},"taint.Key":"karpenter.sh/disruption"}
{"message":"deleted node","Node":{"name":"ip-10-0-8-63..."}}
{"message":"deleted nodeclaim","NodeClaim":{"name":"default-6swc4"}}

# 첫 번째 교체 완료 후 두 번째 노드 교체 시작
{"message":"disrupting nodeclaim(s) via replace, terminating 1 nodes","reason":"drifted"}
{"message":"created nodeclaim","NodePool":{"name":"default"},"NodeClaim":{"name":"default-ct7nn"}}
...
{"message":"tainted node","Node":{"name":"ip-10-0-2-174..."},"taint.Key":"karpenter.sh/disruption"}
{"message":"deleted node","Node":{"name":"ip-10-0-2-174..."}}
{"message":"deleted nodeclaim","NodeClaim":{"name":"default-q9tgw"}}

로그에서 "reason":"drifted"가 교체 트리거가 Drift임을 나타냅니다.

karpenter.sh/disruption taint는 Karpenter가 교체 대상 노드에 붙이는 taint로,

이 taint가 붙는 순간 해당 노드에 새 파드가 스케줄링되지 않고 기존 파드 eviction이 시작됩니다.

 

노드 상태 변화도 별도 터미널에서 모니터링합니다.

while true; do
  kubectl get nodeclaim
  echo
  kubectl get nodes -l team=checkout
  echo
  kubectl get pods -n checkout -o wide
  echo; date; sleep 1
done

이 명령이 각각 보여주는 것은 다음과 같습니다.

kubectl get nodeclaim은 Karpenter가 관리하는 NodeClaim 목록입니다.
교체 과정에서 새 NodeClaim이 생성되고 이전 NodeClaim이 삭제되는 시점을 볼 수 있습니다.

kubectl get nodes -l team=checkout은 team=checkout 레이블이 붙은 노드만 필터링해서 버전과 AGE를 보여줍니다.
새 노드가 추가되고 구 노드가 사라지는 흐름이 여기서 보입니다.

kubectl get pods -n checkout -o wide는 파드가 어느 노드에서 실행 중인지 확인합니다.
교체 과정에서 파드가 신규 노드로 재스케줄링되는 것을 볼 수 있습니다.

 

업그레이드 결과 확인

모든 교체가 완료된 후 노드 버전을 확인합니다.

$ kubectl get nodes -l team=checkout

NAME                                        STATUS   ROLES    AGE     VERSION
ip-10-0-12-207.us-west-2.compute.internal   Ready    <none>   4m5s    v1.31.14-eks-ecaa3a6
ip-10-0-38-70.us-west-2.compute.internal    Ready    <none>   5m48s   v1.31.14-eks-ecaa3a6

두 노드 모두 v1.31로 교체됐습니다. AGE가 짧은 것이 방금 새로 프로비저닝된 노드라는 증거입니다.

 

checkout 파드도 새 노드에서 정상 실행 중인지 확인합니다.

kubectl get pods -n checkout -o wide

10개의 checkout 파드와 checkout-redis가 두 새 노드에 분산되어 Running 상태인 것을 확인합니다.

 


 

Managed Node Group과의 업그레이드 비교

이번 실습까지 진행하면서 MNG와 Karpenter의 업그레이드 방식 차이가 명확해졌습니다.

항목 Managed Node Group Karpenter
노드 관리 주체 AWS (ASG 기반) Karpenter 컨트롤러
업그레이드 트리거 mng_cluster_version 변수
또는 ami_id 변경
EC2NodeClass AMI 변경 → Drift 감지
롤링 제어 max_unavailable_percentage Disruption Budget (nodes, schedule)
교체 단위 ASG가 인스턴스 교체 Karpenter가 직접 EC2 프로비저닝/삭제
상태 확인 kubectl get nodes kubectl get nodeclaim + kubectl get nodes

 

긴 글 읽어주셔서 감사합니다.

다음 섹션에서는 Self-managed node group과 Fargate 업그레이드로 찾아뵙겠습니다.