# Karpenter 노드가 클러스터에 붙지 않을 때 - 디스커버리, 인가, 인터럽션

Karpenter를 새 EKS 클러스터에 처음 붙이면 Pod가 Pending에 머물거나, EC2 인스턴스는 떴는데 kubectl get nodes에 보이지 않는 상황을 만납니다. Karpenter는 노드 프로비저닝을 여러 단계로 나누어 처리하고 각 단계가 서로 다른 전제에 의존하기 때문에, 증상만 보고 원인을 찾기 어렵습니다.

이 글에서는 Karpenter가 노드를 만드는 경로를 단계별로 나누고 각 단계에서 막히는 조건과 확인 방법을 정리합니다. 이어서 노드가 정상적으로 붙은 다음 운영 단계에서 반드시 이해해야 하는 AMI drift와 중단(disruption) 제어를 다룹니다.

# 1. 프로비저닝 경로

Pending Pod
  │  ① 스케줄러가 배치 실패 → Karpenter 가 감지
  ▼
NodePool 선택 → nodeClassRef 로 EC2NodeClass 참조
  │  ② NodeClass 가 Ready 여야 한다 (서브넷/SG/AMI 확인)
  ▼
NodeClaim 생성 → EC2 인스턴스 시작 (RunInstances)
  │  ③ 컨트롤러가 노드 역할을 넘길 권한이 있어야 한다 (PassRole)
  ▼
kubelet 부팅 → API 서버에 등록 요청
  │  ④ 노드 역할이 클러스터에 인가돼 있어야 한다
  ▼
Node Ready

②에서 막히면 인스턴스가 아예 만들어지지 않고 ④에서 막히면 EC2는 실행 중인데 노드 목록에 나타나지 않습니다. 이 차이가 진단의 출발점입니다.

# 2. NodeClass가 Ready가 아니면 스케줄링 자체가 시작되지 않는다

EC2NodeClass는 서브넷, 보안 그룹, AMI를 태그 기반 디스커버리로 찾습니다. 관례적으로 karpenter.sh/discovery: <cluster-name> 태그를 사용합니다.

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: my-cluster-karpenter-node-role
  amiSelectorTerms:
    - alias: al2023@latest
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: my-cluster
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: my-cluster

셀렉터가 아무것도 못 찾으면 해당 status condition이 False가 되고 상위 Ready가 무너집니다. 문서는 그 결과를 명확히 적어 두었습니다.

If a NodeClass is not ready, NodePools that reference it through their nodeClassRef will not be considered for scheduling.

Pod는 계속 Pending인데 EC2 API 호출 흔적조차 없습니다. 로그를 아무리 봐도 실패 이벤트가 없어서 헤매기 쉬운 구간입니다.

kubectl get ec2nodeclass default -o jsonpath='{.status.conditions}' | jq
# SubnetsReady / SecurityGroupsReady / AMIsReady / Ready 확인

# 태그가 실제로 붙어 있는지
aws ec2 describe-subnets \
  --filters "Name=tag:karpenter.sh/discovery,Values=my-cluster" \
  --query 'Subnets[].SubnetId'

클러스터를 Terraform 등으로 새로 만들 때 서브넷·보안 그룹 태그를 빠뜨리는 일이 흔합니다. amiSelectorTerms는 v1에서 필수 필드이므로 생략할 수도 없습니다.

# 3. EC2는 떴는데 노드가 안 보인다 - 노드 인가

이 증상은 kubelet이 부팅해 API 서버에 등록을 시도했지만 인가되지 않았다는 뜻입니다. 두 가지를 함께 확인해야 합니다.

노드 역할의 IAM 정책 - Karpenter가 만드는 노드의 역할에는 다음이 필요합니다.

정책 역할
AmazonEKSWorkerNodePolicy 노드가 EKS 클러스터에 연결
AmazonEKS_CNI_Policy VPC CNI가 노드 네트워크 구성
AmazonEC2ContainerRegistryPullOnly 컨테이너 이미지 pull
AmazonSSMManagedInstanceCore SSM 접근(디버깅에 유용)

클러스터 측 인가 매핑 - IAM 권한만으로는 부족합니다. 노드 역할이 클러스터의 인가 목록에 등록돼 있어야 합니다. 과거에는 aws-auth ConfigMap에 system:bootstrappers, system:nodes 그룹으로 매핑했고 현재는 EKS access entry(노드 역할은 EC2_LINUX 타입)를 쓰는 방식이 표준입니다. Karpenter 문서도 노드 역할이 "connected to an IAM Identity Mapping used to authorize nodes to the cluster" 라고만 적고 그 매핑 생성은 도구(eksctl 등)의 몫으로 둡니다. Terraform으로 클러스터를 직접 구성하면 이 단계가 빠지기 쉽습니다.

# access entry 확인
aws eks list-access-entries --cluster-name my-cluster

# 없으면 노드 역할용 항목 생성
aws eks create-access-entry \
  --cluster-name my-cluster \
  --principal-arn arn:aws:iam::<account>:role/my-cluster-karpenter-node-role \
  --type EC2_LINUX

# 노드에서 직접 확인 (SSM 접속 후)
journalctl -u kubelet | grep -i "Unauthorized\|forbidden"

# 4. 컨트롤러 권한 - PassRole을 빼먹으면 인스턴스가 안 뜬다

Karpenter 컨트롤러는 노드 역할을 EC2 인스턴스에 부여하기 위해 iam:PassRole이 필요합니다. 문서는 이 권한의 목적을 "to give EC2 permission explicit permission to use the KarpenterNodeRole" 로 설명합니다. 이것이 없으면 RunInstances가 거부되고 NodeClaim이 생성됐다가 실패로 남습니다.

kubectl get nodeclaims
kubectl describe nodeclaim <name> | tail -30   # 이벤트에 API 오류가 그대로 나온다
kubectl logs -n kube-system deploy/karpenter | grep -i "error\|unauthorized"

컨트롤러의 자격 증명 자체는 IRSA 또는 EKS Pod Identity로 붙입니다. Pod Identity를 쓰는 경우 association 생성이 별도 작업이라, 클러스터와 IAM 역할만 만들고 association을 빠뜨리면 컨트롤러가 아무 API도 호출하지 못합니다.

aws eks list-pod-identity-associations --cluster-name my-cluster

# 5. 인터럽션 큐 - 없으면 조용히 기능이 빠진다

Karpenter는 Spot 중단 경고, 예정된 유지보수 이벤트, 인스턴스 상태 변경, 상태 검사 실패를 감시해 노드를 미리 비웁니다. 이 기능은 SQS 큐를 지정해야 활성화됩니다.

To enable full interruption handling, configure the --interruption-queue CLI argument with the name of the interruption queue provisioned to handle interruption events.

큐와 함께 다음 EventBridge 규칙이 큐로 라우팅돼야 합니다.

  • AWS Health Events
  • EC2 Spot Instance Interruption Warnings
  • EC2 Instance Rebalance Recommendations
  • EC2 Instance State-change Notifications
  • EC2 Capacity Reservation Instance Interruption Warnings

컨트롤러 정책에는 해당 큐에 대한 ReceiveMessage, DeleteMessage, GetQueueUrl이 필요합니다. 설정을 빼도 Karpenter는 정상 동작하는 것처럼 보이지만 Spot 중단 시 2분의 경고 시간을 활용하지 못하고 Pod가 갑자기 사라집니다. 이런 종류의 누락은 장애가 날 때까지 드러나지 않으므로, 구축 체크리스트에 넣어야 합니다.

# 6. AMI alias와 drift - @latest의 대가

amiSelectorTermsaliasfamily@version 형식입니다. al2023, bottlerocket, windows2022 등의 패밀리에 latest 또는 고정 버전(al2023@v20240703, bottlerocket@v1.20.4)을 지정합니다.

@latest를 쓰면 편하지만 대가가 있습니다. 문서 경고가 직접적입니다.

a new AMI release will cause Karpenter to drift all out-of-date nodes in the cluster, replacing them with nodes running the new AMI.

Drift는 NodeClaim의 실제 상태가 NodePool/EC2NodeClass 정의와 어긋났을 때 Karpenter가 노드를 교체하는 메커니즘입니다. AMI가 새로 나오면 셀렉터가 찾아내는 "올바른 값"이 바뀌므로 기존 노드 전체가 drift 대상이 됩니다. 즉 내가 아무 커밋도 하지 않은 날에 노드가 교체될 수 있습니다. 문서도 하위 환경에서 새 AMI를 먼저 검증하라고 권합니다.

전략 장점 단점
al2023@latest 보안 패치 자동 반영 교체 시점을 통제할 수 없음
버전 고정 교체 시점을 사람이 결정 정기적으로 올리는 운영 부담

장시간 실행되는 워크로드(학습 작업, 상태가 있는 워커)가 있는 클러스터라면 고정이 기본값이어야 합니다.

# 7. 중단 제어 - 무엇을 막을 수 있고 무엇은 못 막는가

Karpenter의 중단은 두 종류로 나뉩니다.

구분 종류 특징
Graceful Consolidation, Drift disruption budget으로 속도 제한 가능
Forceful Expiration, Interruption, Node Repair, 수동 삭제 즉시 drain 시작, 속도 제한 불가

karpenter.sh/do-not-disrupt 애노테이션은 자발적 중단만 막습니다. 문서는 경계를 명확히 그어 둡니다.

The karpenter.sh/do-not-disrupt annotation does not exclude nodes from the forceful disruption methods: Expiration, Interruption, Node Repair, and manual deletion.

따라서 이 애노테이션은 AMI drift로 인한 교체를 막는 수단으로는 유효하지만 expireAfter 만기나 Spot 중단은 막지 못합니다. "중요한 워커니까 애노테이션을 달아 두면 안전하다"는 가정은 성립하지 않습니다.

consolidateAfter의 타이머 동작도 자주 오해합니다.

Karpenter resets this timer whenever a pod is added to or removed from the node, so a node only becomes a consolidation candidate once it has been stable for the full consolidateAfter duration.

WhenEmpty 정책과 함께 쓰면 "마지막 Pod가 빠진 뒤 지정 시간만큼 노드를 유지"하는 의미가 됩니다. 이 값은 콜드 스타트 지연과 유휴 비용의 트레이드오프입니다. GPU 노드처럼 부팅과 이미지 pull에 수 분이 걸리는 경우, 값을 늘리면 재기동 지연이 크게 줄어드는 대신 빈 노드를 더 오래 붙잡습니다.

apiVersion: karpenter.sh/v1
kind: NodePool
spec:
  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 15m
  template:
    spec:
      expireAfter: 720h

# 8. 진단 체크리스트

증상 확인할 곳 흔한 원인
Pod Pending, EC2 호출 없음 kubectl get ec2nodeclass -o yaml status 서브넷/SG 디스커버리 태그 누락
NodeClaim 생성 후 실패 kubectl describe nodeclaim iam:PassRole 누락, 서브넷 IP 소진
컨트롤러가 API 호출 못 함 Pod Identity association / IRSA association 미생성
EC2 Running인데 노드 없음 aws eks list-access-entries, kubelet 로그 노드 역할 인가 매핑 누락
Spot 중단 시 Pod 급사 인터럽션 큐 설정 큐/EventBridge 규칙 미구성
커밋 없이 노드가 교체됨 amiSelectorTerms @latest alias로 인한 AMI drift
노드가 계속 안 줄어듦 consolidateAfter, do-not-disrupt Pod 타이머 리셋, 애노테이션 상주 Pod

# 9. 마무리

  • Karpenter 장애는 어느 단계에서 멈췄는지만 구분하면 대부분 빠르게 좁혀집니다. EC2 인스턴스가 있는지 없는지가 첫 분기점입니다.
  • NodeClass가 Ready가 아니면 스케줄링이 시작조차 하지 않고 실패 이벤트도 남지 않습니다. status condition을 먼저 봐야 합니다.
  • IAM 권한과 클러스터 인가는 별개입니다. 정책을 다 붙였는데 노드가 안 보이면 access entry를 확인합니다.
  • @latest alias는 AMI 릴리스가 곧 노드 교체를 뜻합니다. do-not-disrupt는 자발적 중단만 막으므로, 강제 중단까지 막아 준다고 기대하면 안 됩니다.

# 참고