
Cluster Autoscaler
Cluster Autoscaler는 Kubernetes에서 오래전부터 사용되던 노드 스케일링 도구로, AWS에서는 Auto Scaling Group(ASG)과 함께 동작한다. 즉, 미리 정의된 노드 그룹(ASG 단위)을 기준으로, 리소스가 부족하면 노드를 추가하고, 리소스가 여유로우면 노드를 축소한다. 이것은 EC2 Autoscaling 그룹을 사용한다고 보면 된다. 하지만 이방식은 구성하기가 어렵고 구성할 수 있는 범위가 매우 제한적이다. 예를 들어, 사용 가능한 인스턴스 타입이나 지역, 스팟 인스턴스 사용 여부 등을 세부적으로 제어하려면, ASG에서 미리 다 설정해둬야 하기 때문에 실시간 대응에는 다소 한계가 있다. 또한, Pod의 리소스 요청(Request)에 딱 맞는 노드를 찾기보다는, “그룹 단위”로 노드를 증설하기 때문에 최적화에는 어느정도 제약이 있다고 생각한다. 그래서 이 한계를 극복하기 위해서 AWS에서 는 Karpenter라는 노드단위로 AutoScaling을 진행할 수 있는 오픈소스 프로젝트 제시하였다.
Karpenter
Karpenter는 AWS에서 새롭게 제안한 오픈소스 프로젝트로, 기존의 노드 그룹 개념 없이 Pod의 요구사항에 따라 유연하게 EC2 인스턴스를 생성하는 방식이다. Karpenter는 ASG를 사용하지 않고, Kubernetes 컨트롤러 내부에서 직접 EC2를 생성하고 관리한다. Pod가 요청하는 CPU, 메모리 등의 조건을 기준으로 가장 적절한 인스턴스를 실시간으로 계산하여 다양한 인스턴스 타입, OS, 아키텍처(ARM, x86) 등을 자유롭게 선택할 수 있다. 이런 구조 덕분에 스케일링 속도가 빠르고, 비용 효율적인 Spot 인스턴스 혼합 전략도 쉽게 적용할 수 있다. 또한, 불필요한 노드를 자동으로 종료해주는 Consolidation(통합) 기능까지 지원되어 지속적인 비용 최적화가 가능하다. 실제로 Cluster Autoscaler와 Karpenter를 테스트 해보았는데 Autoscaler는 대략 2~3분 후 부하가 발생하였을때 노드가 증설되었고, Karpenter는 40~50초 사이안에 노드가 증설되었다.
KaKao Karpenter 도입 후기: https://devblog.kakaostyle.com/ko/2022-10-13-1-karpenter-on-eks/
EKS클러스터 Karpenter 적용기
안녕하세요! 카카오스타일 SRE팀 네사입니다. 오늘은 카카오스타일 SRE팀에서 올해 EKS 클러스터 이전을 하며 새롭게 도입 했던 AWS Karpenter 에 대해 공유를 해보려 합니다.
devblog.kakaostyle.com
Karpenter를 사용하기 위해서는 몇 가지 사전 설정이 필요하다.
Karpenter는 Pod 스케줄링 요청을 감지해 필요한 EC2 인스턴스를 직접 생성하는 컴포넌트이기 때문에, IAM 권한 부여, 네트워크 리소스 태깅(Subnet, Security Group), aws-auth 설정 등을 필요로 한다.
IAM 역할 구성 (IRSA)
Karpenter는 AWS API를 호출해서 EC2 인스턴스를 생성·관리해야 하므로, 두 가지 IAM 역할이 필요하다.
- KarpenterControllerRole
Karpenter 컨트롤러 Pod에 부여되는 IAM Role이다.
이 역할은 IRSA(IAM Roles for Service Accounts)를 통해 Kubernetes ServiceAccount에 연결되며, EC2 인스턴스 생성, 태깅, 관리 등 다양한 권한을 가진다. - KarpenterNodeRole
Karpenter가 생성하는 EC2 노드에 부여되는 IAM Role이다.
이 역할에는 해당 노드가 EKS 클러스터에 정상적으로 조인하고, AWS 리소스를 사용할 수 있도록 하는 권한이 포함된다.
aws-auth ConfigMap 수정
Karpenter가 생성한 EC2 인스턴스가 EKS 클러스터에 노드로 조인하려면, 그 인스턴스에 할당된 IAM Role을 Kubernetes 내부에 등록해줘야 한다. 이를 위해 사용하는 것이 바로 aws-auth ConfigMap이다. aws-auch CM에 들어가 다음 코드를 추가해줘야한다. 이 작업은 Karpenter가 생성한 EC2 노드가 kubelet으로 정상 동작하고, 클러스터와 통신하기 위한 필수 설정이다.
mapRoles:
- rolearn: arn:aws:iam::<ACCOUNT_ID>:role/KarpenterNodeRole
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
Subnet / Security Group 태깅
Karpenter는 EC2 인스턴스를 어디에 생성할지 판단하기 위해, VPC 안의 서브넷과 보안 그룹을 식별해야 한다.
이를 위해 Subnet, Security Group에 Tag를 직접 붙여야 한다.
서브넷 태그
kubernetes.io/cluster/<CLUSTER_NAME> = shared karpenter.sh/discovery = <CLUSTER_NAME>
보안 그룹 태그
karpenter.sh/discovery = <CLUSTER_NAME>
이 태그를 기반으로 Karpenter는 어떤 서브넷을 사용할지, 어떤 보안 그룹을 붙일지를 자동으로 판단한다.
Helm Values 파일 수정
Karpenter는 Kubernetes에서 Pod의 요구 리소스를 감지하고, EC2 인스턴스를 직접 생성해주는 컨트롤러이기 때문에
Karpenter Controller Pod 자체가 Karpenter가 생성한 EC2 노드에 올라가게 되면 문제가 생길 수 있다. 예를 들어, Karpenter Controller가 올라간 노드가 종료되거나 문제가 생기면, 추가 노드 생성이 중단되면서 전체 클러스터 확장 기능이 마비될 위험이 있다. 이를 방지하기 위해 Helm을 통해 Karpenter를 설치할 때 affinity.nodeAffinity 설정을 반드시 추가해주는 것이 좋다.
serviceAccount:
name: karpenter
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<ACCOUNT_ID>:role/KarpenterControllerRole-my-eks-cluster
settings:
clusterName: my-eks-cluster
clusterEndpoint: https://<EKS_ENDPOINT> # eks describe-cluster로 확인 가능
interruptionQueueName: karpenter-interruption-queue # EventBridge를 사용하는 경우 설정
aws:
defaultInstanceProfile: KarpenterNodeInstanceProfile-my-eks-cluster
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: karpenter.sh/nodepool
operator: DoesNotExist
Karpenter CRD
Kubernetes에서는 기본적으로 Pod, Service, Deployment, ConfigMap 같은 다양한 기본 리소스 타입을 제공한다. 하지만 때때로 사용자의 요구에 맞는 커스텀 리소스가 필요할 수 있다. 이럴 때 사용하는 것이 바로 CRD (Custom Resource Definition) 이다.CRD란 쿠버네티스에서 사용자가 자신만의 리소스 타입을 정의할 수 있게 해주는 확장 메커니즘이다. 예를 들어, Karpenter에서는 다음과 같은 커스텀 리소스 타입을 사용한다
$ kubectl get crd | grep -i karpenter
ec2nodeclasses.karpenter.k8s.aws
nodeclaims.karpenter.sh
nodepools.karpenter.sh
이 리소스들은 NodePool, NodeClaim, EC2NodeClass 등의 형태로 등장하며, Karpenter의 핵심 기능을 표현하는 오브젝트다.
NodePool
Karpenter 공식문서 : https://karpenter.sh/docs/concepts/nodepools/
그 중 NodePool은 어떤 파드를 어떤 조건의 노드에 배치할지를 정의하는 리소스다. 예를 들어, Observability 관련 파드와 일반 서비스 백엔드 파드를 서로 다른 NodePool로 분리하면, 각 파드의 부하에 따라 해당 NodePool에 맞는 노드만 별도로 스케일 아웃할 수 있다. 또한 NodePool은 사용할 인스턴스 타입, CPU 아키텍처, 가용영역(AZ), 그리고 Spot/온디맨드와 같은 비용 모델까지 세부적으로 설정할 수 있다.
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: platform # NodePool의 이름
spec:
template:
spec:
nodeClassRef:
name: default # 사용할 NodeClass 이름 (EC2 인스턴스 설정 정의)
requirements: # 어떤 조건의 노드를 사용할지 제약 조건들
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"] # 컴퓨팅 최적화(c), 범용(m), 메모리 최적화(r) 계열 인스턴스만 사용
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"] # 5세대 이상 인스턴스만 사용
- key: topology.kubernetes.io/zone
operator: In
values: ["ap-northeast-2a"] # 지정된 가용영역(AZ)에서만 생성
- key: kubernetes.io/arch
operator: In
values: ["amd64"] # AMD64 아키텍처만 허용 (ARM 제외)
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"] # 비용 절감을 위해 스팟 인스턴스만 사용
Disruption 설정 (노드 정리)
Disruption은 Karpenter가 노드의 상태를 지속적으로 감시하면서, 필요하지 않은 노드를 자동으로 정리해주는 기능이다. Karpenter는 클러스터에 있는 노드들의 파드 수, 리소스 사용률, 노드 생성 시간을 분석해서 과도하게 비어 있거나 오래된 노드가 있을 경우 이를 자동으로 종료시킨다.
disruption:
consolidationPolicy: WhenUnderutilized
consolidateAfter: 3m
expireAfter: 720h
NodeClass(노드의 EC2 인스턴스 설정)
karpenter 공식문서 : https://karpenter.sh/docs/concepts/nodeclasses/
EC2NodeClass는 Karpenter가 생성할 EC2 인스턴스의 AMI, 서브넷, 보안 그룹, 디스크 설정 등을 정의하는 리소스다.
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2 # 사용할 AMI의 종류 (Amazon Linux 2)
role: "KarpenterNodeRole-${CLUSTER_NAME}" # EC2 인스턴스에 할당할 IAM 역할
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}" # 클러스터 태그로 서브넷 선택
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}" # 클러스터 태그로 보안 그룹 선택
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3 )
iops: 3000
encrypted: true
tags:
Name: "karpenter-node" # EC2 인스턴스의 이름 태그
Kerpenter 사용시 고가용성을 위한 참고 옵션
LimitRange
쿠버네티스 공식문서 : https://kubernetes.io/ko/docs/concepts/policy/limit-range/
Kubernetes에서 Pod는 cpu, memory 등의 자원 요청(Request)을 설정할 수 있다. 하지만 이 값이 실제 사용량보다 과하게 설정되면 불필요한 노드가 추가되어 비용이 증가하고, 너무 적게 설정되면 스케줄링 실패나 OOM 오류가 발생할 수 있다.
그래서 이를 대비하기 위하여 LimitRange를 설정을 사용할 수 있다. 이는 특정 네임스페이스에 기본 Request/Limit 값을 강제적으로 지정할 수 있다.
Kubernetes에서는 기본적으로 리소스 요청(requests) 및 제한(limits)을 설정하지 않으면, 컨테이너가 호스트의 CPU와 메모리를 제한 없이 사용할 수 있다. 이 경우 Kubernetes 스케줄러는 포드의 총 리소스 요청을 기반으로 해당 포드를 예약할 적합한 노드를 결정한다. 포드가 여러 개의 컨테이너를 가질 경우, 스케줄러는 각 컨테이너의 리소스 요청 중 가장 높은 값을 기준으로 노드를 선택한다. Karpenter는 이러한 리소스 요청을 참고하여 적절한 EC2 인스턴스 유형을 선택하고, 그에 맞는 노드를 프로비저닝한다.
만약 일부 포드가 리소스 요청을 명시하지 않으면, LimitRanges를 사용하여 네임스페이스에 기본값을 설정할 수 있다.
apiVersion: v1
kind: LimitRange
metadata:
name: default-limit
namespace: default
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "200m"
memory: "256Mi"
type: Container
PriorityClass
쿠버네티스 공식문서: https://kubernetes.io/ko/docs/concepts/scheduling-eviction/pod-priority-preemption/
참고문서: https://www.gomgomshrimp.com/posts/k8s/karpenter-over-provisioning
Karpenter를 통한 Over Provisioning
들어가며
www.gomgomshrimp.com
PriorityClass는 쿠버네티스에서 Pod가 스케줄링될 때, 우선순위를 정의하는 역할을 한다. 이 우선순위는 자원이 부족할 때 어떤 Pod가 먼저 배포될지 결정하는 중요한 기준이 됩니다. 만약 자원이 부족하면, 우선순위가 낮은 Pod는 Preemption(선점) 되어 우선순위가 높은 Pod가 먼저 배치될 수 있습니다. 예를 들어, 로그 수집기나 모니터링 도구보다 실제 서비스 앱의 파드가 우선 배포되어야 한다면, 서비스 파드에 높은 PriorityClass를 부여해줄 수 있다.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: critical-service
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority // 자원이 부족하면 우선순위가 낮은 Pod를 밀어내고 우선순위가 높은 Pod를 실행
description: "중요한 서비스용 우선순위 클래스"
PDB (PodDisruptionBudget)
쿠버네티스 공식문서: https://kubernetes.io/docs/tasks/run-application/configure-pdb/
클러스터 유지보수 중 노드를 drain하면 파드가 강제로 종료될 수 있다. 이때 모든 파드가 한 번에 내려가면 서비스 중단이 발생할 수다. PodDisruptionBudget을 설정하면, 동시에 중단 가능한 파드 수 또는 최소 실행 파드 수를 보장하여서비스의 가용성을 유지할 수 있다. 예를 들어 현재 deployment로 5개의 파드를 띄운다고 가정하였을때, 두개의 노드가 있으면 파드가 3개 2개의 형태로 각각 노드에 배치되어있을 것이다. 이때 특정 노드를 유지보수하기 위해 kubectl drain 작업을 진행하면, 해당 노드에 있던 파드들은 모두 삭제되고, 다른 노드로 재스케줄링되기 전까지 일시적인 서비스 중단이 발생할 수 있다. 이것을 조금이라도 배제하고자 PDB를 사용하여 최소 파드의 살아 있음을 보장한다.예를 들어 minAvailable: 3으로 설정하면, 클러스터 내에 항상 최소 3개의 파드는 동시에 살아 있어야 한다는 조건이 된다. 만약 한 노드에 3개의 파드가 있고, 그 노드를 drain하려 한다면, 남은 노드에 파드가 2개밖에 존재하지 않으므로 PDB 조건을 만족하지 못하게 된다. 이 경우 Kubernetes는 해당 drain 작업을 일시적으로 차단하고, 파드를 1개라도 유지하여 서비스 가용성을 보장한다.
그 후 시간이 지나 다른 노드에 파드가 재스케줄링되어 다시 3개 이상의 파드가 운영되면, Kubernetes는 그제서야 drain 작업을 계속 진행하고 나머지 파드도 종료된다. 이렇게 하면 서비스 중단 없이 점진적으로 노드를 비워나갈 수 있게 되며, 보다 안정적인 유지보수가 가능해진다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: myapp-pdb
namespace: default
spec:
minAvailable: 2 # 최소 2개는 항상 살아 있어야 함
selector:
matchLabels:
app: myapp
Graceful Shutdown
참고자료: https://jennifersoft.com/ko/blog/kubernetes/2023-10-25-kubernetes-9/
Pod이 삭제되거나 노드가 drain될 때, 갑자기 컨테이너를 죽이지 않고 안전하게 종료 과정을 밟게 해주는 옵션이다. 동작흐름으로 보면, "kubectl delete pod" 명령어 또는 drain 작업으로 Pod 제거요청을 한다. 그럼 Kubernetes가 SIGTERM 시그널을 컨테이너에 전송하면 컨테이너는 이것을 감지하고 데이터정리, 연결종료같은 정리 작업에 들어간다.
그 후 설정해둔 terminationGracePeriodSeconds 초 내에 종료되지 않으면 SIGKILL작업으로 강제 종료된다.
apiVersion: v1
kind: Pod
metadata:
name: graceful-shutdown-example
spec:
terminationGracePeriodSeconds: 60 # SIGKILL 보내기까지 기다리는 시간
containers:
- name: app
image: nginx
lifecycle:
preStop: # 종료 전에 실행되는 훅
exec:
command: ["/bin/sh", "-c", "sleep 5"]
'kubernetes' 카테고리의 다른 글
| [kubernetes] Replication Controller & ReplicaSet (0) | 2025.06.13 |
|---|---|
| [Kubernetes] EKS(Elastic Kubernetes Service) (0) | 2025.04.24 |
| [Kubernetes] Naver Cloud Kubernetes Service(NKS)에서 3Tier Application 배포 (0) | 2024.12.31 |
| [kubernetes] Security Context (0) | 2024.12.16 |
| [kubernetes] CSR (0) | 2024.12.14 |