CAS + 관리형 노드그룹으로 노드 오토스케일링을 하던 회사의 EKS를 Karpenter로 전환한 것에 대한 이야기이다. 스텝 바이 스텝 핸즈온은 아니고 좀 더 넓은 관점에서의 전략, 또 회사의 인프라 형상에 대처했던 방법 등에 대한 교훈 습득(lesson learned)식의 회고가 될 것이다.
Karpenter
Karpenter observes the aggregate resource requests of unscheduled pods and makes decisions to launch and terminate nodes to minimize scheduling latencies and infrastructure cost.
문서의 카펜터에 대한 설명이다. 그림의 위쪽처럼 대기중인 파드는 스케쥴러가 ‘존재하는 스케쥴 가능한 노드’(=스케쥴 용량)에 스케쥴 하게 된다. 하지만 스케쥴 할 수 없는 파드 즉, 스케쥴 용량이 없는 상황이라면 무한정 대기하게 된다.
카펜터는 이런 우는 아이에게 젖을 준다 (응애). 이런 것이 즉시에(Just-in-time) 가능한 것은 카펜터의 구조와 동작이 AWS 내부 자원과 API 호출을 직접하기 때문에 아주 빠르다.
Karpenter makes cents
카펜터의 진가는 용량의 최적화(right sizing)이다. Just-in-time 뿐만 아니라 파드 자원 할당량(resource quota)에 따라 노드(=EC2 인스턴스)의 크기도 최적화 할 수 있다. 위에서 말한, 내부 구현인 AWS API 직접 호출로 갖게 되는 유연성을 통해 시간적 공간적(크기적?) fit도 가능한 것이다. 하지만 API를 직접 쓰는게 아니라 카펜터 인터페이스(쿠버네티스 객체로 구현됐다)를 통해 쉽게 구성이 가능하다.
울어 보채기만하던 쿠버네티스가 AWS를 만나고 멋있어졌어요
그리고 이건 비용 최적화랑 직결된다. 위 카펜터 다이어그램의 optimized capacity 부분을 보면 된다. 애초에 프로비저닝(provisioning)부터 right sizing을 하지만, 카펜터는 파드들이 종료되거나 등의 이유로 노드의 남는 유휴 자원 공간을 정리하는 “디프로비저닝”(deprovisioning) 메커니즘이 있다. 쉽게 말하면 비용이 나가지만 일 안하는 노드를 정리한다(돈 받으며 놀고 싶은 나…?).
이 가게는 아닙니다 손님.
vs. CAS
보통 카펜터보단 클러스터 오토스케일러(cluster-autoscaler, CAS)를 구성해서 쓰는 사례가 많을 것 같다. 그런 이유는 카펜터는 후발주자이고 아직까지 EKS 특)이기 때문에 덜 범용적인 것이 이유일거다(쿠버네티스 생태계엔 이런 BP 아닌 BP가 꽤 있는거 같다). 그리고 CAS가 스스로 EKS 노드, EC2 인스턴스를 늘릴 수 없기 때문에 노드그룹(AWS ASG(launch template, LT))을 같이 쓸 것이다.
하지만 적어도 EKS를 쓴다면 CAS 대신 카펜터를 쓸 이유는 명확하다.
무신사 발표 CAS 단점 캡쳐
앞서 설명한 카펜터의 두 가지 장점이 CAS에겐 없기 때문이라고 말할 수 있다. 그런 이유는, 무신사 영상에서도 지적한 것처럼 CAS와 노드그룹 사이의 정보 불일치(단절) 때문에 프로비저닝 속도는 느려지고 최적합(best fit)이 될 수 없다. 또 노드그룹 또는 연결된 LT 구성은 생성할 인스턴스 타입을 정적으로 한정한다. 이것은 그저 생성(creating)에 불과하다. 카펜터가 하는 일은 프로비저닝이라고 구분하는 이유이다.
CAS + 노드그룹 | Karpenter | |
속도 | AWS API Pulling → 10s 주기, quotas 제한 | 프로비저닝 등 이벤트 시 API 직접 호출 |
제약 구성 | 정적인 인스턴스 타입 생성, 최소/최대 개수의 제약 | 동적인 인스턴스 타입 프로비저닝, 최소 노드 수가 없고 가드레일 방식으로 프로비저너 당 리소스 총합의 제한
(비용 관점) |
AS-IS
회사 역시 CAS + 노드그룹을 사용하고 있었다. 하지만 문서화는 커녕 IaC도 안되어 있어서 와서 테라포밍하는 상황이었다. 당연히 코드로 관리되지 않았던 인프라 형상은 drift가 곳곳에 있었다. 비슷한 타입의 클러스터를 여러 환경에서 운영 중인데 같아야 할 구성들이(e.g. 보안그룹 규칙) 다른 곳이 많았다. 되려 CAS의 동작 알고리즘도 몰랐고 노드그룹 구성이 어떤 기조로 된 건지 알 수 없었는데, ‘비용 절감’(작업할 당시엔 최적화의 느낌이 아녔다…. )이라는 비즈니스 요구사항으로부터 Karpenter를 조사하다보니 조금 파악하게 됐다.
아무튼 비용 최적화라는 관점에선 중요하게 볼 노드그룹 구성은 특징은 ‘인스턴스 타입’과 ‘최소/최대 노드 개수’이다.
•
노드그룹 마다
◦
한가지 타입의 인스턴스만 스케일링 가능하다.
◦
최소 노드 개수는 항상 유지해야하고 최대 개수로 제약이 생긴다.
전략
우선 비용 최적화보다 명시적인 목표를 정했다. “클러스터 전체 cpu/memory Requests utilization을 끌어 올리는 것”이다.
kube-prometheus-stack 기본 제공 대시보드, ‘Kubernetes / Views / Global’의 패널이다. utilization이 꽤 안좋은 클러스터 것을 가져왔다.
엥 근데 “끌어 올린다”는 하나도 명시적(구체적)이지 않은데요? IaC와 태깅 정책, 비용 추적에 대한 가시성이 확보되지 않은 상태라 이런 제약사항에선 목표치를 값으로 설정하기 어려웠다.
요구사항으론 워크로드 실행에 이상이 없도록 점진적, 안정적으로 전환해야한다.
먼저 점진적으로 바꾸기 위해 노드그룹의 형상을 최대한 따라갔다.
위와 같이 1:1 매핑하는 식으로 기존 인프라 형상을 유지했다. 여기서 LT(또는 노드그룹 구성)에 정적인 인스턴스 타입을 Provisioner에선 인스턴스 패밀리(타입 + 세대)만 주어 사이즈는 카펜터가 판단하도록 하여 right sizing 되도록 했다. 세대까지 제한한 이유는 워크로드 컨테이너가 arm 아키텍처 빌드가 없다는 제약사항 때문에 그랬다. 따라서 아키텍처 역시 x86으로 제한했다.
그리고 안정적인 전환을 위해 카펜터 프로비저닝/디프로비저닝 시 서비스 중단이 있으면 안된다. 특히 디프로비저닝 시 노드가 정리되며 파드들은 방출(eviction) 당한다. 방출 시 graceful stop은 지원하지만, 노드를 옮겨 스케쥴된 파드의 실행(running)까지 보장하진 않는다. 즉, graceful restart/upgrade를 보장하지 않는다.
이를 보장하기 위해 “Eviction Safe Constraints”라는 정책을 만들어 적용했다:
•
파드 최소 복제본 수를 2 이상으로 한다.
•
PDB min available 1을 적용한다.
워크로드 특성상 필요 없는 몇몇 서비스와 비용 최적화(절감…)이라는 목적에 예외사항을 빼곤 기본으로 적용하게 됐다.
마지막으로 디프로비저닝 메카니즘 설정이다. 이건 consolidation으로 통일했다. 노드 자원 유휴 상태의 (디스크) 조각모음 같은 방식이다. emptiness는 너무 소극적인 방식이라 비용 절감 효과가 적을것 같았고, expiration은 결국 ttl을 정하는데 휴리스틱한 접근만 나올 것 같아 고르지 않았다.
방법
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
version = "19.13"
cluster_name = local.cluster_name
irsa_oidc_provider_arn = data.aws_iam_openid_connect_provider.sent_seoul_prod_oidc_provider.arn
irsa_namespace_service_accounts = ["karpenter:karpenter"]
create_iam_role = false
iam_role_arn = aws_eks_node_group.base.node_role_arn
}
...
output "karpenter_irsa_arn" {
description = <<EOT
IRSA ARN for Karpenter Controller
Helm chart value: `serviceAccount.annotations."eks\.amazonaws\.com/role-arn"`
EOT
value = module.karpenter.irsa_arn
}
output "karpenter_queue_name" {
description = <<EOT
SQS Queue Name for Karpenter
Helm chart value: `settings.aws.interruptionQueueName`
EOT
value = module.karpenter.queue_name
}
output "karpenter_instance_profile_name" {
description = <<EOT
IAM Instance Profile Name for Karpenter
Helm chart value: `settings.aws.defaultInstanceProfile`
EOT
value = module.karpenter.instance_profile_name
}
Ruby
복사
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: eks.amazonaws.com/nodegroup
operator: In
values:
- base
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: <REDACTED>
serviceMonitor:
enabled: true
settings:
aws:
clusterEndpoint: <REDACTED>
clusterName: <REDACTED>
defaultInstanceProfile: <REDACTED>
interruptionQueueName: <REDACTED>
YAML
복사
테라폼 출력 값을 제외하곤 affinity 구성을 해주었다. 원하는 base 노드그룹에 스케쥴 될 수 있도록.
만들 Node Templates의 리소스(보안그룹, 서브넷)을 컨트롤러가 찾을 수 있도록 각 리소스에 디스커버리 태그도 붙여준다("karpenter.sh/discovery" = local.cluster_name).
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
name: a-node-template
spec:
subnetSelector:
karpenter.sh/discovery: <REDACTED>
Name: <REDACTED>
securityGroupSelector:
karpenter.sh/discovery: <REDACTED>
Name: <REDACTED>
tags:
karpenter.sh/discovery: <REDACTED>
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
encrypted: true
deleteOnTermination: true
YAML
복사
Node Template 매니페스트 예제이다. 서브넷, 보안그룹 셀렉터 외에 blockDeviceMappings 를 구성했다. 기본 볼륨 크기가 20Gi로 작은 편이라, 꽤 큰 AI 모델 이미지를 올리기 적합하지 않았기 때문에 기본 LT에서 사용하던만큼 늘려줬다.
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: a-provisioner
spec:
providerRef:
name: a-node-template
requirements:
- key: "karpenter.k8s.aws/instance-family"
operator: In
values:
- c6i
- key: "karpenter.sh/capacity-type"
operator: In
values:
- on-demand
- key: kubernetes.io/os
operator: In
values:
- linux
- key: kubernetes.io/arch
operator: In
values:
- amd64
limits:
resources:
cpu: "20"
memory: 40Gi
consolidation:
enabled: true
YAML
복사
Provisioner 매니페스트의 예제이다. 위 전략에서 정의한 요구사항을 넣었다:
•
인스턴스 패밀리만 제약한다.
•
linux/amd64만 사용한다.
•
consolidation을 쓴다.
도구들
이게 제대로 된 color scheme이다.
eks-node-viewer는 애초에 카펜터 팀이 카펜터를 개발하며 만든 가시성 도구다. 인터페이스가 제한적이지만 노드가 뜨고 죽는 것과 자원 사용량(온디맨드 시간당 요금) 등 카펜터 프로비저너 제약이 적절한지 파악하는데에 피드백이 명확한 “갓”툴이다(gif으로 보면 더 좋은데 민감정보 블러처리를 못해서 ㅠ…)
eks-node-viewer UI의 또 다른, 정말 킬러 피쳐라고 생각하는 부분은, cpu/memory 사용량을 표시하는 color scheme이다. 위의 그라파나 패널과 달리, utilization이 낮은 것이 경고의 붉은 색이다. 높아질 수록 즉, right sizing 할 수록 초록색이 된다. 카펜터의 비용 최적화의 의지가 돋보이는 부분이다(그리고 나의 커밋의 빌드업이 된다…). 이 점이 좋아서 위 그라파나 패널은 참고하지 않고 eks-node-viewer를 계속보게 됐다.
이 도구는 직접 만든 도구이다. Python kubernetes API 와 Typer를 조합해 CLI로 만들었다. 명령이 하는 일은 노드 레이블 우선순위에 따라 group by 후 이름과 (현재 스케쥴 파드 수/최대 가용 파드 수)를 출력한다.
이 가시성 도구가 필요했던 이유는 AZ(가용 영역)이 eks-node-viewer로는 살펴볼 수 없는 자원(정확히는 제약사항이라 해야할 거 같다)이라서 그랬다. cpu나 memory가 남는데도 특정 프로비저너는 새 노드를 띄우는 모습을 봤다. 그 이유는 PV(EBS)가 특정 가용 영역에 있기 때문이었다. 그래서 마지막 레이블을 가용 영역(topology.kubernetes.io/zone)으로 grouping해 모니터했다. 그리고 파드 utilization도 보이게 했는데 알고보니 eks-node-viewer에서 이미 볼 수 있어서(eks-node-viewer/node-pods-usage) 필요하진 않았다 ㅎ
Karpenter의 좋은 점
karpenter는 kubernetes 슬랙에 채널이 있다(정확히 SIG인진 모르겠다). 여기에 질문을 하면 AWS 매니저, 개발자 분들이 ‘빠르게, 놓치지 않고’ 답변해준다. 나에겐 놀라웠다. 카펜터가 비록 오픈소스지만 역시 AWS 서비스로써의 고객 집착인가?
카펜터 프로젝트 매니저로 보이는 직원이 담당자를 멘션(쪼아서…?? )해서 일하게 한다
카펜터 도입 작업을 하며 실질적인 문제 뿐만 아니라 서비스의 방향 등 의견도 들을 수 있었다. 이를 계기로 AWS 제품에 대한 신뢰도가 마구 상승하며 나 역시 플랫폼 엔지니어로써 “서비스 수준”의 품질 제공을 하기로 다짐했다
효과
야! 맨 아랫 줄 뭐해!
메모리 요청량 utilization이 약 30%정도 개선됐다. 위 AS-IS 캡쳐는 CPU지만, 비슷한 개선 효과가 있었다. CPU 또는 메모리 한 쪽으로만 최적화가 되는 경우도 있었다. 예를 들어 CPU 효율은 8-90%이지만, 메모리는 3-40%인 경우이다. 이건 현재 파드들에 자원 할당량이 제대로 달려 있지 않는 점도 한 몫해서 앞으로 풀어야할 문제다.
단순히 자원 요청량 utilization 개선으론 “비용 최적화” 실현을 측정할 수 없어서, 떠 있는 인스턴스 타입에 시간당 사용량을 비교하여 비용 메트릭도 수동으로 측정했다. 비슷하게 EC2 컴퓨팅 부분에서 약 30% 정도의 개선효과가 있었고 월 청구서를 통해 추적 중이다. 이 부분도 더 세세하게 발라볼 수 있도록 가시성을 높이는 작업이 이어져야 한다.
이 좋은 걸 안쓴다고?
•
이 좋은 걸 안쓴다!(utilization이 낮은 대부분이 초록색, 높은 CPU util에서 빨간색이다)
•
Memory Limit Utilization을 그리는데 PromQL 오류로 나오지 않음
그라파나 Color scheme 옵션엔 continuous-RdYlG 라는 딱 적절한 것이 있다. 문서를 확인한건 아니고 그라파나 콘솔에서 확인했다. 바로 카펜터에 PR ㄱㄱ 했다. 현재는 merged로 개선된(편-안한) 대시보드를 사용할 수 있다 .
contribution까지한데엔 회사와 동료들의 지지?가 컸다고 할 수 있다. 그냥 커스텀 그라파나 대시보드를 만들 수도 있지만, 평소에 “일반적인 요구사항으로 만들어 표준, 오픈소스 사용하자.” “남들도 고민, 고생한거 반복하지말자” 라는 기조와 비전이 있기에 당연하게 PR을 날려야지라는 생각을 했다. 그럼에도 PR을 만들려면 설명하는 일 귀찮은데 잘했다며 동료들이 격려해주었다(하지만 이젠 나 혼자 남았지…). 아무튼 난 꽤 큰 오픈소스에 기여해서 좋고, (이번에도) docs를 고친 수준이라… 꿀이다 . 담엔 버그 수정이나 기능 추가 등 코드 자체에도 기여해보자!
습득 교훈
•
무언가를 판단하기 위해선 메트릭 측정, 가시성이 무엇보다 중요하다. 적절한 도구 사용으로 정책을 만들자.
•
오픈소스 제품일지라도 AWS의 것은 감동이 있다. PE 관점에서 “서비스 수준”의 플랫폼을 제공하자.
•
오픈소스에 기여하자. 엄청 많은 고객과 영향력이 생긴다.