logo
Back

EKS - Karpenter

28 views

1. Karpenter란 무엇인가?

Karpenter는 AWS에서 개발한 오픈소스 기반의 유연하고 효율적인 Kubernetes 노드 오토스케일러입니다.

기존의 Cluster Autoscaler(CA)가 ASG(Auto Scaling Group)에 의존했던 것과 달리, Karpenter는 Group-less 방식으로 동작하며 스케줄링되지 않은 포드에 가장 적합한 EC2 인스턴스를 직접 프로비저닝합니다.

실습에서 보겠지만, 실제 동작 속도 및 효율적인 노드를 관리를 해 준다.

https://youtu.be/bowYzxbfKzc?si=9M2Bk-GxOuMkH4p5 해당 유튜브 영상을 보면 Karperter 에 대해 조금 더 잘 알 수 있다.

2. 왜 Cluster Autoscaler(CA) 대신 Karpenter인가?

비교 항목Cluster Autoscaler (CA)Karpenter
작동 방식ASG의 크기를 조절 (간접적)EC2 Fleet API를 통해 직접 생성 (직접적)
속도노드 추가까지 수 분 소요 (ASG 반응 속도)수 초 이내로 매우 빠름 (Just-in-time)
유연성ASG에 정의된 인스턴스 타입만 사용 가능포드 요구사항에 맞는 모든 인스턴스 타입 선택 가능
비용 최적화복잡한 설정 필요Consolidation(통합) 기능을 통한 자동 비용 절감
관리 단위노드 그룹 단위 관리NodePool 단위를 통한 유연한 관리

3. Karpenter의 핵심 동작 원리

  1. Watch: 스케줄링되지 않은 포드(Pending Pods)를 감시합니다.
  2. Evaluate: 포드의 요구사항(CPU, Memory, GPU, Affinity 등)을 분석합니다.
  3. Provision: 요구사항에 가장 적합한(가장 저렴하고 효율적인) 노드를 직접 띄웁니다.
  4. Remove: 노드가 필요 없어지거나 더 저렴한 노드로 대체 가능할 때 노드를 자동으로 삭제/교체합니다 (Consolidation).

4. Karpenter 주요 개념

  • NodePool: 어떤 종류의 노드를 띄울 것인지에 대한 "정책"을 정의합니다. (CPU, RAM 제한, Taints, Labels 등)
  • EC2NodeClass: AWS 전용 설정입니다. (Subnet, Security Group, AMI Family, EBS 설정 등)

5. Karpenter 설치 및 실습

karpenter 를 배포하기 위해 다음과 같은 내용이 먼저 필요하다. 하기 링크를 보면, 기초적인 설정과 설치법이 나와 있다. https://karpenter.sh/docs/getting-started/getting-started-with-karpenter/

  1. AWS CLI
  2. kubectl - the Kubernetes CLI
  3. eksctl (>= v0.202.0) - the CLI for AWS EKS
  4. helm - the package manager for Kubernetes

환경변수 추가

bash
cd ..
mkdir karpenter
cd karpenter
 
 
# 변수 설정
export KARPENTER_NAMESPACE="kube-system"
export KARPENTER_VERSION="1.10.0"
export K8S_VERSION="1.34"
 
export AWS_PARTITION="aws" # if you are not using standard partitions, you may need to configure to aws-cn / aws-us-gov
export CLUSTER_NAME="chanki-karpenter-demo" # ${USER}-karpenter-demo
export AWS_DEFAULT_REGION="ap-northeast-2"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export TEMPOUT="$(mktemp)"
export ALIAS_VERSION="$(aws ssm get-parameter --name "/aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2023/x86_64/standard/recommended/image_id" --query Parameter.Value | xargs aws ec2 describe-images --query 'Images[0].Name' --image-ids | sed -r 's/^.*(v[[:digit:]]+).*$/\1/')"
 
# 확인
echo "${KARPENTER_NAMESPACE}" "${KARPENTER_VERSION}" "${K8S_VERSION}" "${CLUSTER_NAME}" "${AWS_DEFAULT

쿠버네티스 배포

karpenter 를 테스트 하기 위해 쿠버네티스 자원을 cloudformaion 을 이용해 생성한다.

bash
curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml  > "${TEMPOUT}" \
&& aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-file "${TEMPOUT}" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"
 
 
# 클러스터 생성 : EKS 클러스터 생성 15분 정도 소요
eksctl create cluster -f - <<EOF
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${CLUSTER_NAME}
  region: ${AWS_DEFAULT_REGION}
  version: "${K8S_VERSION}"
  tags:
    karpenter.sh/discovery: ${CLUSTER_NAME}
 
iam:
  withOIDC: true
  podIdentityAssociations:
  - namespace: "${KARPENTER_NAMESPACE}"
    serviceAccountName: karpenter
    roleName: ${CLUSTER_NAME}-karpenter
    permissionPolicyARNs:
    - arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerNodeLifecyclePolicy-${CLUSTER_NAME}
    - arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerIAMIntegrationPolicy-${CLUSTER_NAME}
    - arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerEKSIntegrationPolicy-${CLUSTER_NAME}
    - arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerInterruptionPolicy-${CLUSTER_NAME}
    - arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerResourceDiscoveryPolicy-${CLUSTER_NAME}
 
iamIdentityMappings:
- arn: "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}"
  username: system:node:{{EC2PrivateDNSName}}
  groups:
  - system:bootstrappers
  - system:nodes
  ## If you intend to run Windows workloads, the kube-proxy group should be specified.
  # For more information, see https://github.com/aws/karpenter/issues/5099.
  # - eks:kube-proxy-windows
 
managedNodeGroups:
- instanceType: m5.large
  amiFamily: AmazonLinux2023
  name: ${CLUSTER_NAME}-ng
  desiredCapacity: 2
  minSize: 1
  maxSize: 10
 
addons:
- name: eks-pod-identity-agent
EOF
 

배포된 자원 확인

bash
eksctl get cluster
eksctl get nodegroup --cluster $CLUSTER_NAME
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
eksctl get addon --cluster $CLUSTER_NAME
 
# config rename-context
kubectl ctx
kubectl config rename-context "root@chanki-karpenter-demo.ap-northeast-2.eksctl.io" "karpenter-demo"
 
# k8s 확인
kubectl ns default
kubectl cluster-info
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -n kube-system -owide
kubectl get pdb -A
kubectl describe cm -n kube-system aws-auth

kube-ops-view

bash
# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=LoadBalancer --set env.TZ="Asia/Seoul" --namespace kube-system
echo -e "http://$(kubectl get svc -n kube-system kube-ops-view -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"):8080/#scale=1.5"
open "http://$(kubectl get svc -n kube-system kube-ops-view -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"):8080/#scale=1.5"

이번 실습은 loadbalancer-controller 를 설치해서 구성하지 않는다. 설치하지 않으면, aws 관리 영역에 있는 cloud-controller-manager 를 통해 classic load balancer 로 배포가 된다. NodePort 형태로 배포되어 노드의 IP 를 알기만 하면 통신을 할 수 있다. 간단한 실습을 위해 해당 방식대로 접속해 본다.

karpenter 설치

bash
helm registry logout public.ecr.aws
 
 
# Karpenter 설치를 위한 변수 설정 및 확인
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name "${CLUSTER_NAME}" --query "cluster.endpoint" --output text)"
export KARPENTER_IAM_ROLE_ARN="arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter"
echo "${CLUSTER_ENDPOINT} ${KARPENTER_IAM_ROLE_ARN}"
 
# karpenter 설치
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter --version "${KARPENTER_VERSION}" --namespace "${KARPENTER_NAMESPACE}" --create-namespace \
  --set "settings.clusterName=${CLUSTER_NAME}" \
  --set "settings.interruptionQueue=${CLUSTER_NAME}" \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait
 
# 확인
helm list -n kube-system
kubectl get-all -n $KARPENTER_NAMESPACE
kubectl get all -n $KARPENTER_NAMESPACE
kubectl get crd | grep karpenter

프로메테우스 & 그라파나 설치

bash
# 프로메테우스 / 그라파나 설치
helm repo add grafana-charts https://grafana.github.io/helm-charts
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
kubectl create namespace monitoring
 
# 프로메테우스 설치
curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/getting-started/getting-started-with-karpenter/prometheus-values.yaml | envsubst | tee prometheus-values.yaml
helm install --namespace monitoring prometheus prometheus-community/prometheus --values prometheus-values.yaml
 
# 프로메테우스 얼럿매니저 미사용으로 삭제
kubectl delete sts -n monitoring prometheus-alertmanager
 
# 프로메테우스 접속
kubectl port-forward --namespace monitoring svc/prometheus-server 9090:80 &
open http://127.0.0.1:9090
 
# 그라파나 설치
curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/getting-started/getting-started-with-karpenter/grafana-values.yaml | tee grafana-values.yaml
helm install --namespace monitoring grafana grafana-charts/grafana --values grafana-values.yaml
 
# admin 암호
kubectl get secret --namespace monitoring grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
 
 
# 그라파나 접속
kubectl port-forward --namespace monitoring svc/grafana 3000:80 &
open http://127.0.0.1:3000

다음과 같이 두개의 대시보드가 추가 된 것을 볼 수 있다.

NodePool 설치

노드 풀의 경우 어떤 종류의 노드를 띄울 것인지에 대한 "정책"을 정의하는 과정이다. 아래와 같이 추가해본다.

보면 아래에 어떤 instance type 를 쓸것인지, 어떤 아키텍쳐 등 이런 것을 셋팅한다.

bash
# NodePool, EC2NodeClass 생성
cat <<EOF | envsubst | kubectl apply -f -
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["2"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h # 30 * 24h = 720h
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: "KarpenterNodeRole-${CLUSTER_NAME}"       # replace with your cluster name
  amiSelectorTerms:
    - alias: "al2023@${ALIAS_VERSION}"            # ex) al2023@latest 
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
EOF
 
# 확인 
kubectl get nodepool,ec2nodeclass,nodeclaims

테스트 용 deploy 배포

bash
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      securityContext:
        runAsUser: 1000
        runAsGroup: 3000
        fsGroup: 2000
      containers:
      - name: inflate
        image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
        resources:
          requests:
            cpu: 1
        securityContext:
          allowPrivilegeEscalation: false
EOF
 
kubectl get pod
kubectl scale deployment inflate --replicas 5
 
# 출력 로그 분석해보자!
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
kubectl logs -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | grep 'launched nodeclaim' | jq '.'
 
kubectl get nodeclaims

scale 시, 아래와 같이 노드가 자동으로 추가되며 엄청 빠른 시간에 노드가 바로 나오게 된다.

아래는 KubeOpsView 에도 노드가 추가된 것을 볼 수 있다.

bash
kubectl scale deployment inflate --replicas 30
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'

30 개로 증가 할 때에, 다음과 같이 대기 상태에 빠진 pod 가 아래와 같이 나온다.

node claim 을 봤을 때 한개의 노드가 추가 된 것을 볼 수 있다.

아래와 같이 1분도 되지 않아 노드가 추가되며, 높은 instance 가 추가되어 추가 파드들을 전부 serving 하는 것을 볼 수 있다.

scale down

bash
kubectl delete deployment inflate && date
 
# 출력 로그 분석해보자!
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
 
kubectl get nodeclaims

아래와 같이 taint, 그리고 결국 노드가 지워지는 것을 볼 수 있다.

kube-ops-view 에서도 마찬가지로 지워진 모습을 볼 수 있다.

Disruption(Expiration , Drift , Consolidation) 실습

Drift(드리프트)란?

Drift는 "현재 실행 중인 노드의 설정"과 "NodePool/EC2NodeClass에 정의된 설정" 사이의 불일치를 의미합니다.

왜 Drift 기능이 중요한가요?

보통 인프라 설정(AMI, 보안 그룹, 서브넷 등)이 바뀌면 기존 노드들을 일일이 교체해야 했습니다. 하지만 Karpenter의 Drift 기능을 사용하면 설정 변경 시 Karpenter가 이를 감지하고 자동으로 노드를 최신 사양으로 교체(Rolling Update) 해줍니다.

Drift가 발생하는 주요 케이스

  1. AMI 업데이트: EC2NodeClass에서 사용하는 AMI ID가 변경되거나, 최신 버전의 AMI가 출시되었을 때.
  2. 설정 변경: NodePool의 Labels, Taints 또는 EC2NodeClass의 Security Groups, Subnets 설정이 바뀔 때.
  3. 수동 변경: 누군가 AWS 콘솔에서 노드 설정을 임의로 수정했을 때.

설정 방법 (v1beta1 기준): Karpenter 설정에서 drift를 Enabled로 설정하면 작동합니다.


Disruption (노드 중단 및 교체 정책)

Karpenter는 클러스터의 효율성을 위해 노드를 언제든 삭제하거나 교체할 수 있습니다. 이를 제어하는 것이 disruption 설정입니다.

① Consolidation (비용 최적화)

노드가 낭비되고 있을 때 Karpenter가 내리는 결정입니다.

  • WhenEmpty: 노드에 실행 중인 포드가 없으면 즉시 삭제합니다.
  • WhenUnderutilized: 노드의 리소스 사용률이 낮아, 실행 중인 포드들을 다른 노드로 옮기고 현재 노드를 삭제할 수 있다고 판단되면 실행합니다. (더 저렴한 인스턴스로 교체 포함)

② Expiration (노드 수명 설정)

노드가 생성된 지 특정 시간이 지나면 강제로 교체합니다.

  • 용도: 장기 실행 노드에서 발생할 수 있는 메모리 누수 방지, 정기적인 보안 패치 적용(최신 AMI 적용) 등.
  • 설정: expireAfter: 720h (예: 30일 뒤 교체)

③ Interruption (스팟 인스턴스 중단 처리)

AWS로부터 스팟 인스턴스 중단 통보(Spot Interruption Notice)를 받으면, Karpenter는 즉시 새로운 노드를 프로비저닝하고 기존 노드의 포드들을 안전하게 Drain(대피)시킵니다.


실습

기존 노드풀과 노드 클래스를 삭제한다. 그리고 다시 정의 해 본다.

bash
kubectl delete nodepool,ec2nodeclass default
 
# 모니터링
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
eks-node-viewer --resources cpu,memory --node-selector "karpenter.sh/registered=true" --extra-labels eks-node-viewer/node-age
watch -d "kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L karpenter.sh/capacity-type"
 
# Create a Karpenter NodePool and EC2NodeClass
cat <<EOF | envsubst | kubectl apply -f -
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      requirements:
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-size
          operator: NotIn
          values: ["nano","micro","small","medium"]
        - key: karpenter.k8s.aws/instance-hypervisor
          operator: In
          values: ["nitro"]
      expireAfter: 1h # nodes are terminated automatically after 1 hour
  limits:
    cpu: "1000"
    memory: 1000Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized # Karpenter는 노드가 비어 있거나 활용도가 낮을 때 노드를 교체 policy enables Karpenter to replace nodes when they are either empty or underutilized
    consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  amiSelectorTerms:
    - alias: "al2023@latest"
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
EOF
 
# 확인 
kubectl get nodepool,ec2nodeclass
 
# Deploy a sample workload
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 5
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      securityContext:
        runAsUser: 1000
        runAsGroup: 3000
        fsGroup: 2000
      containers:
      - name: inflate
        image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
        resources:
          requests:
            cpu: 1
            memory: 1.5Gi
        securityContext:
          allowPrivilegeEscalation: false
EOF

확인

bash
kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L karpenter.sh/capacity-type
kubectl get nodeclaims
kubectl describe nodeclaims
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
kubectl logs -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | grep 'launched nodeclaim' | jq '.'

현재는 노드가 3개가 배포 되어 있는 상태이다.

kubectl scale deployment/inflate --replicas 12 레플리카를 증가시켜 본다. kubectl get nodeclaims 로 nodeclaims 확인 해 본다.

kube-ops-view 에서도 노드가 추가된 것을 볼 수 있다.

scale down

replica 를 다시 5개로 줄여 본다.

bash
kubectl scale deployment/inflate --replicas 5
kubectl get nodeclaims

노드가 줄어든 것도 볼 수 있다.

kube-ops-view 에서도 확인 해 본다.

삭제한다.

bash
kubectl delete deployment inflate
kubectl delete nodepool,ec2nodeclass default

최종 리소스 삭제

리소스 삭제 순서가 중요하다.

bash
# Karpenter helm 삭제 
helm uninstall karpenter --namespace "${KARPENTER_NAMESPACE}"
 
# Service(CLB) 삭제
kubectl delete svc -n kube-system kube-ops-view
 
# EC2 Launch Template 삭제
aws ec2 describe-launch-templates --filters "Name=tag:karpenter.k8s.aws/cluster,Values=${CLUSTER_NAME}" |
    jq -r ".LaunchTemplates[].LaunchTemplateName" |
    xargs -I{} aws ec2 delete-launch-template --launch-template-name {}
 
# 클러스터 삭제
eksctl delete cluster --name "${CLUSTER_NAME}"
 
# Karpenter IAM Role 등 생성한 CloudFormation 삭제
aws cloudformation delete-stack --stack-name "Karpenter-${CLUSTER_NAME}"
logo
이 블로그는 NextJS 14, Strapi를 이용해 만들었습니다
Copyright © 2026 김기찬
문의사항은 admin@devchanki.com 으로 주시면 감사 하겠습니다.

문의 보내기

남겨주면 블로그 주인에게 바로 전달돼요.

답장은 별도도 확인 후 연락 드려요.