KUBERNETES

08. 노드 오토스케일링 – Karpenter

이번 장은 노드 오토스케일링을 알아봅니다.

클라우드 환경에서는 온프레미스 환경에 비하여 워커 노드(VM 인스턴스)의 자유로운 확장과 축소가 용이합니다. 쿠버네티스를 사용하기 전 VM 환경에서 AWS의 Auto Scaling을 사용하여 시스템 부하에 따라 EC2 자원을 유연하게 사용하였듯이 쿠버네티스 환경에서도 워커 노드를 자유롭게 확장, 축소할 수 있습니다.

쿠버네티스 환경에서 노드 오토스케일링은 Cluster AutoScaler(CAS)와 Karpenter를 사용합니다. Karpenter는 기존의 CAS에 비하여 구조가 간편하여 좀 더 빠르고 유연합니다. 노드 비용도 CAS에 비하여 절감할 수 있습니다. 실습을 통하여 자세하게 Karpenter를 알아보겠습니다.

주요 내용

  • CAS(Cluster AutoScaler) vs Karpenter 비교
  • 카펜터의 장점 

실습 과제

  • Karpenter, eks-node-viewer 설치
  • Karpenter를 이용한 EKS 노드 Scale-Out/In 테스트

이번 장에서 사용하는 소스의 깃헙 주소입니다. 

https://github.com/junghoon2/k8s-class/tree/main/karpenter

1. Cluster AutoScaler 대비 Karpenter의 장점

Karpenter는 Kubernetes 클러스터에서 노드 관리와 스케줄링을 최적화하기 위한 오픈 소스 도구입니다. 기존에는 Cluster AutoScaler를 사용하여 쿠버네티스 노드를 동적으로 스케일링하였습니다. CAS는 각 클라우드 서비스 제공자(AWS, Azure, GCP)에 적합한 별도의 API를 이용합니다.

Cluster Autoscaler는 AWS의 Auto Scaling Group(ASG)을 이용하여 EKS의 노드를 관리합니다. ASG는 EKS와 관련없는 별도의 AWS 자원으로 ASG와 EKS 노드 간의 데이터 동기화 작업이 필요하여 추가 시간이 소요됩니다. 실제 테스트를 하면 노드 증설에 CAS는 약 2분의 시간이 소요되고 카펜터는 40초의 시간이 소요됩니다.


From: AWS CON405_How-to-monitor-and-reduce-your-compute-costs.pdf

AGS 노드 그룹은 단일 인스턴스 타입만 사용 가능하여 자원의 낭비가 발생할 수 있습니다. 예를 들어 0.1Core/128Mi 자원을 사용하는 하나의 파드만 실행하는데 4Core/32Gi의 자원을 가지는 노드가 필요하여 비용의 낭비가 발생할 수 있습니다.

CAS가 Karpenter에 비하여 오래된 기술이라 기술적 성숙도가 있으나 비용과 속도 측면에서 Karpenter가 유리하여 최근 많은 기업들이 Karpenter를 도입하는 추세입니다. 필자도 Karpenter를 도입하여 기존 대비 30% 이상의 비용을 절감할 수 있었습니다.


From: AWS CON405_How-to-monitor-and-reduce-your-compute-costs.pdf

카펜터는 Auto Scaling Group를 사용하지 않고 EKS 노드를 직접 증가, 감소합니다. 또한 파드의 자원 사용량에 따라 가장 비용이 저렴한 노드를 자동으로 할당합니다. 그리고 실행 중인 파드가 종료되면 다시 필요한 자원 사용량을 확인하여 필요에 따라 자동으로 비용 최적화를 위하여 기존 노드를 삭제하고 새로운 최적의 노드를 할당합니다. 

하지만 카펜터를 사용하기 위해서는 노드 최적화 작업 시 새로운 노드가 실행되고 기존 노드는 종료되므로 실행 중인 파드가 종료되고 새롭게 실행하여 파드 이중화, PodDistributionBudget, Graceful Shutdown 등 서비스 안정화를 기본 설정 작업이 필요합니다. 이같은 작업은 꼭 카펜터를 위해 필요한 작업은 아니고 시스템 안정성을 향상하기 위한 기본 작업이므로 카펜터와 관련없이 필요한 작업입니다.

2. Karpent 헬름 설치

카펜터를 직접 설치하여 상세 기능을 알아보겠습니다. 카펜터는 EKS 외부의 EC2 인스턴스를 제어하므로 IRSA를 필요합니다. 테라폼의 카펜터 모듈을 이용하면 편리하게 설치할 수 있습니다. 기존 EKS 설치 시 사용한 테라폼 코드에 아래와 같이 카펜터 모듈을 추가합니다.

테라폼 karpenter 모듈

module "karpenter" {
  source = "terraform-aws-modules/eks/aws//modules/karpenter"

  cluster_name           = module.eks.cluster_name
  irsa_oidc_provider_arn = module.eks.oidc_provider_arn

  policies = {
    AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  }

  tags = local.tags
}

테라폼을 이용하여 배포합니다.

$ (⎈ |switch-singapore-test:default) tf init
$ (⎈ |switch-singapore-test:default) tf plan -out planfile
$ (⎈ |switch-singapore-test:default) tf apply planfile

유의할 사항은 테라폼 코드를 실행하고 나면 생성되는 Output의 아래의 카펜터 부문을 별도로 메모하는 것입니다. 카펜터 헬름 설치에 필요한 ‘karpenter_instance_profile_arn’,  ‘karpenter_queue_name’ 정보를 메모합니다.

karpenter_instance_profile_name = "Karpenter-jerry-test-2023082121003044240000001d"
karpenter_queue_name = "Karpenter-jerry-test"

IRSA 설정이 완료되어 이제 카펜터를 설치합니다. 역시 헬름을 사용합니다.

$ (⎈ |sent-seoul-stage:mongodb) helm pull oci://public.ecr.aws/karpenter/karpenter --version v0.27.5
Pulled: public.ecr.aws/karpenter/karpenter:v0.27.5
Digest: sha256:9491ba645592ab9485ca8ce13f53193826044522981d75975897d229b877d4c2

$ (⎈ |sent-seoul-stage:mongodb) rm -rf karpenter-v0.27.5.tgz
$ (⎈ |sent-seoul-stage:mongodb) mv karpenter karpenter-v0.27.5
$ (⎈ |switch-singapore-test:echoserver) cd karpenter-v0.27.5 
$ (⎈ |switch-singapore-test:echoserver) cp values.yaml my-values.yaml

기본 헬름 Value 파일을 아래와 같이 변경합니다.

my-values.yaml

replicas: 2

controller:
  resources:
    limits:
      memory: 1Gi
    requests:
      cpu: 100m
      memory: 1Gi

serviceAccount:
  annotations:
    eks.amazonaws.com/role-arn: $ROLE_ARN
    
settings:
  aws:
    clusterEndpoint: $Endpoint
    clusterName: $Name
    defaultInstanceProfile: Karpenter-jerry-test-2023082121003044240000001d
    interruptionQueueName: Karpenter-jerry-test

. serviceAccount.annotations.eks.amazonaws.com/role-arn: $ROLE_ARN
앞에서 테라폼 카펜터 모듈에서 생성한 각자의 카펜터 ROLE_ARN 정보를 입력합니다.

. settings.aws.clusterEndpoint
각자 cluster의 API 서버 Endpoint 정보를 입력합니다. EndPoint 정보는 AWS EKS 콘솔에서 확인할 수 있습니다.

EKS Endpoint 예제

. settings.aws.defaultInstanceProfile, interruptionQueueName
앞에서 테라폼 카펜터 모듈 설치 시 Output 결과를 입력합니다.

카펜터 네임스페이스에 설치합니다.

$ (⎈ |switch-singapore-test:karpenter) helm install karpenter --namespace karpenter --create-namespace -f my-values.yaml .
NAME: karpenter
LAST DEPLOYED: Fri Sep 22 05:51:10 2023
NAMESPACE: karpenter
STATUS: deployed
REVISION: 1
TEST SUITE: None

설치가 완료되면 카펜터 Controller 파드를 karpenter 네임스페이스에서 확인할 수 있습니다.

$ (⎈ |switch-singapore-test:karpenter) k get pod
NAME                        READY   STATUS    RESTARTS   AGE
karpenter-f654d4b8d-v65dk   1/1     Running   0          7d13h

3. Karpenter Provisioner & AWS Node Template 설치

카펜터가 새로운 노드를 배포하기 위해서 노드의 상세한 설정이 필요합니다. 카펜터는 Provisioner와 AWSNodeTemplate 2가지 CRD(Custom Resource Definition, 사용자 정의 리소스)를 이용하여 상세한 내용을 정의합니다.

Provisioning 용어는 주로 IT 환경에서 가상의 컴퓨팅 리소스를 할당, 구성, 관리하는 과정을 의미합니다. 카펜터에서도 동일하게 쿠버네티스 노드의 물리적인 요구 사항에 관한 정의를 포함합니다. 어떤 리소스 요구 사항을 만족해야 하는지, 어떤 제약 조건을 고려해야 하는지 등에 대한 정책이 포함됩니다.

예제 매니페스트 파일로 알아봅니다.

provisioner.yaml

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: spot
  namespace: karpenter
spec:
  # References cloud provider-specific custom resource, see your cloud provider specific documentation
  providerRef:
    name: default

  # Labels are arbitrary key-values that are applied to all nodes
  labels:
    env: test

  # Requirements that constrain the parameters of provisioned nodes.
  # These requirements are combined with pod.spec.affinity.nodeAffinity rules.
  # Operators { In, NotIn } are supported to enable including or excluding values
  requirements:
  - key: "karpenter.k8s.aws/instance-family"
    operator: In
    values: ["t3"]
  - key: "karpenter.sh/capacity-type" # If not included, the webhook for the AWS cloud provider will default to on-demand
    operator: In
    values: ["spot"]
  limits:
    resources:
      cpu: "100"
      memory: "200Gi"

  # Enables consolidation which attempts to reduce cluster cost by both removing un-needed nodes and down-sizing those
  # that can't be removed.  Mutually exclusive with the ttlSecondsAfterEmpty parameter.
  consolidation:
    enabled: true

. metadata.name: spot
타입에 따라 노드를 구분할 수 있습니다. 예시로 Spot 노드와 On-Demand 노드를 구분하여 노드 타입을 나눌 수 있습니다. Spot 노드에 실행하는 파드들은 Spot 노드 그룹에 배포하도록 스케줄링 할 수 있습니다. 

. spec.requirements.key: “karpenter.k8s.aws/instance-family”
카펜터가 배포하는 노드의 인스턴스 패밀리를 지정합니다. 실행되는 파드의 특성에 따라 c6i(computing 중심), g5(gpu 노드), r6i(메모리 중심) 혹은 복수의 인스턴스 타입을 지정할 수 있습니다. 테스트 환경이라 필자는 가격이 저렴한 t3 타입을 지정하였습니다.

참고로 Arm 계열의 CPU를 사용하는 t4g 계열의 인스턴스를 사용하면 비용 대비 성능이 20% 정도 향상됩니다.

.  spec.requirements.key: “karpenter.sh/capacity-type” 
on-demand 또는 spot 인스턴스를 지정합니다. 기본은 on-demand이며 spot으로 지정하면 변경할 수 있습니다. 주로 Volume을 사용하지 않는 Stateless 파드, 임의로 노드가 종료되어도 자동으로 연결을 재시작할 수 있는 일반 웹 기반의 애플리케이션은 Spot 인스턴스를 적극적으로 사용할 수 있습니다. 쿠버네티스는 고가용성 기반으로 설계되어 Spot 인스턴스를 사용하기 적합합니다. 많은 기업에서 운영 환경에서도 Spot 인스턴스를 적극적으로 사용하고 있습니다.

개인적으로 spot 인스턴스를 잘 사용할 수 있는 것이 eks의 큰 장점입니다.

. spec.requirements.consolidation.enabled: true
노드에서 실행 중인 파드의 개수가 줄면 카펜터가 자동으로 노드를 통합(consolidation)하거나 실행 중인 파드를 다른 노드로 옮겨 비용을 절감합니다. Cluster Autoscaler와 다르게 카펜터는 새롭게 실행하는 인스턴스 타입까지 조정하여 비용이 가장 저렴한 노드에 새로운 파드를 배포합니다.

다음으로 AWS 노드템플릿 매니페스트입니다. 하드웨어 타입을 제외한 볼륨 설정, 보안 그룹, 서브넷 등 노드 실행에 필요한 AWS 고유한 설정을 지정합니다.

AWSNodeTemplate

apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    karpenter.sh/discovery: jerry-test
  securityGroupSelector:
    karpenter.sh/discovery: jerry-test
  tags:
    karpenter.sh/discovery: jerry-test
  blockDeviceMappings: # (2)
  - deviceName: /dev/xvda
    ebs:
      volumeSize: 100Gi
      volumeType: gp3
      encrypted: true
      deleteOnTermination: true

. spec.subnetSelector, securityGroupSelector
AWSNodeTemplate은 필수로 Subnet과 Security Group 정보를 필요합니다. 노드가 어떤 환경에 실행되는지 필수 기본 정보를 명시합니다.

카펜터는 Subnet, Security Group의 태그 정보를 기준으로 자원을 할당합니다. 카펜터를 설치하면 자동으로 Subnet, Security Group에 클러스터 이름을 기준으로 태그가 할당됩니다.

태그 정보는 AWS 콘솔에서 아래와 같이 확인할 수 있습니다.

karpenter 조회

Security Group 검색 창에 위와 같이 karpenter를 입력하면 관련 리소스를 확인할 수 있습니다. 태그를 확인하면 위 ‘AWSNodeTemplate’에서 입력한 정보와 동일합니다.

karpenter Tag

Security Group과 마찬가지로 Subnet도 동일한 태그를 확인할 수 있습니다.

karpenter tag

카펜터가 실행하는 노드는 위와 같이 karpenter 태그가 지정된 Security Group, Subnet을 할당받습니다.

. spec.subnetSelector.Name: “jerry-test-private-ap-northeast-2*”
EKS 노드는 보안과 비용의 이유로 Private Subnet 그룹에 배포하는 것을 권고합니다. 태그 이름을 Private으로 지정하여 Private Subnet에 배포할 수도 있습니다.

‘*’ 문법을 지원하여 ap-northeast-2a/2b/2c를 일일이 지정하지 않을 수 있습니다.

. spec.blockDeviceMappings
노드 디스크 설정을 수정할 수 있습니다. 필자는 안정적으로 운영하기 위하여 기본 디스크 용량을 100Gi로 지정하였습니다.

준비한 매니페스트를 기준으로 카펜터 CRD를 생성합니다.

$ (⎈ |switch-singapore-test:karpenter) k apply -f provisioner.yaml -f awsNodeTemplate.yaml
provisioner.karpenter.sh/spot created
awsnodetemplate.karpenter.k8s.aws/default created

$ (⎈ |switch-singapore-test:karpenter) k get provisioners.karpenter.sh
NAME      AGE
spot      85s

$ (⎈ |switch-singapore-test:karpenter) k get awsnodetemplates.karpenter.k8s.aws
NAME      AGE
default   94s

provisioner, awsnodetemplate 새로운 2개의 CRD를 확인할 수 있습니다.

4. 카펜터 이용 노드 Autoscaling 실습

카펜터가 배포하는 노드를 쉽게 확인하기 위하여 ‘eks-node-viewer’를 설치합니다. ‘eks-node-viewer’는 AWS에서 개발한 도구로 동적으로 노드의 변화량을 시각화해주는 CLI 기반의 툴입니다. ‘brew’를 사용하여 편리하게 설치할 수 있습니다.

brew tap aws/tap
brew install eks-node-viewer

eks-node-viewer를 실행합니다.

Spot 인스턴스로 t3a.large 1개 노드가 실행 중입니다.

스크린 샷에서 확인할 수 있듯이 eks-node-viewer는 현재 클러스터의 노드 상태를 직관적으로 나타냅니다. 인스턴스 타입, Spot/On-demand 여부를 보여주고 비용까지 나타냅니다. 그리고 현재 노드의 자원 할당(Capacity) 가능량 대비 실행 중인 파드의 자원 요청량(Request)을 백분율(%) 형태로 표현합니다. 한눈에 자원 대비 효과적으로 사용하고 있는지 파악하기 용이합니다.

한가지 주의할 점은 실제 자원 사용량(Usage)이 아닌 자원 요청량(Request) 기준인 것입니다. 3가지를 혼돈하면 안되는데 자원 할당 가능량(Capacity)는 물리적으로 노드가 사용 가능한 전체 자원을 의미하고 자원 요청량(Request)은 현재 노드에 실행 중인 파드의 Request 총합을 의미합니다. 마지막으로 사용량(Usage)은 실제 파드가 사용하는 자원 사용량을 의미하는데 이는 Request 보다 낮을 수도 있고 높을 수도 있습니다. (물론 limit 이상은 사용하지 못합니다.) Capacity 대비 Request를 할당량을 높이면 노드의 자원을 효과적으로 사용할 수 있습니다.

또한 eks-node-viewer는 실제 노드가 배포되는 시간도 확인할 수 있습니다. 그럼 카펜터를 이용하여 새로운 노드를 배포하여 노드 변화 내역을 eks-node-viewer로 확인하겠습니다. 카펜터가 파드의 자원 요청에 따라 어떻게 자동으로 노드를 증가하는지 확인하는 실습입니다. 실습에 사용할 파드 매니페스트입니다.

busybox deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: busybox
  namespace: default
spec:
  replicas: 4
  selector:
    matchLabels:
      app: busybox # POD label과 일치
  template:
    metadata:
      labels:
        app: busybox # Selector label과 일치
    spec:
      containers:
      - name: busybox
        image: busybox
        command: ["sh"]
        args: ["-c", "sleep inf"]
        resources:
          requests:
            cpu: 400m
            memory: 128Mi

. spec.template.spec.resources.requests
카펜터는 파드의 CPU, Memory 요청량(requests)을 기준으로 새로운 노드를 실행합니다. 해당 요청량을 만족하는 노드 중 가장 비용이 저렴한 노드를 선택합니다. 위 예시는 400m * 4, 128Mi * 4를 만족하는 노드를 실행합니다. 

터미널 창을 2개로 나누고 위에서는 ‘busybox’ 파드를 실행하고 아래는 ‘eks-node-viewer’를 실행합니다.

$ (⎈ |switch-singapore-test:default) k apply -f busybox-deploy.yaml
deployment.apps/busybox created

아래와 같이 실시간으로 노드가 준비되는 시간을 확인할 수 있습니다. 새로운 노드가 추가되는데 시간이 40s 소요되었습니다. 기존 클러스터 오토스케일러(CAS)를 사용하면 2분 이상 소요되는데, 기존 대비 약 70% 빨라졌습니다.

t3.small 노드가 38s 초만에 준비되었습니다.

노드 타입을 확인하면 t3.small 입니다. 파드가 필요한 자원이 CPU 400m * 4, Memory 128Mi * 4이므로 이를 만족하고 비용이 가장 저렴한 EC2 인스턴스 타입을 카펜터 Provisioner에서 지정한 노드 그룹 중에서 자동으로 선택하였습니다. 

만약 기존의 클러스터 오토스케일러라면 Request 요청량과 관계없이 노드 그룹에 지정된 단일 타입(ex. c6i.large 등)만 할당 가능하여 비용이 낭비됩니다.

다음으로 반대로 수량을 감소(Scale-In)하는 테스트를 하겠습니다. 동일하게 2개의 창으로 나누고 하나의 창에서 파드를 삭제합니다.

$ (⎈ |switch-singapore-test:default) k delete deployments.apps busybox

파드를 삭제하면 위와 같이 약 10s만에 노드까지 같이 삭제됩니다. 역시 CAS에 비하여 굉장히 빠릅니다. 카펜터는 중간 단계의 오토 스케일링 그룹(ASG)를 거치지 않고 직접 노드를 관리하여 속도가 빠릅니다.

노드 증가, 감소 로그는 karpenter 파드에서 확인할 수 있습니다.

$ (⎈ |switch-singapore-test:default) k logs -n karpenter karpenter-f654d4b8d-v65dk
2023-05-24T00:50:00.733Z	INFO	controller.provisioner.cloudprovider	launched instance	{"commit": "698f22f-dirty", "provisioner": "default", "id": "i-0f7c4e1e1e28aea85", "hostname": "ip-10-110-25-141.ap-southeast-1.compute.internal", "instance-type": "t3.micro", "zone": "ap-southeast-1b", "capacity-type": "spot", "capacity": {"cpu":"2","ephemeral-storage":"100Gi","memory":"947Mi","pods":"4"}}

(생략)

2023-05-24T01:00:13.982Z	INFO	controller.deprovisioning	deprovisioning via consolidation delete, terminating 8 machines ip-10-110-24-178.ap-southeast-1.compute.internal/t3.micro/spot, ip-10-110-27-116.ap-southeast-1.compute.internal/t3.2xlarge/spot, ip-10-110-1-0.ap-southeast-1.compute.internal/t3.2xlarge/spot, ip-10-110-22-211.ap-southeast-1.compute.internal/t3.micro/spot, ip-10-110-17-156.ap-southeast-1.compute.internal/t3.micro/spot, ip-10-110-25-158.ap-southeast-1.compute.internal/t3.micro/spot, ip-10-110-12-187.ap-southeast-1.compute.internal/t3.2xlarge/spot, ip-10-110-25-141.ap-southeast-1.compute.internal/t3.micro/spot	{"commit": "698f22f-dirty"}
2023-05-24T01:00:14.052Z	INFO	controller.termination	cordoned node	{"commit": "698f22f-dirty", "node": "ip-10-110-24-178.ap-southeast-1.compute.internal"}
(생략)

노드 증가, 감소 현황을 확인할 수 있습니다. 실무에서 노드 증가, 감소 관련된 문제가 발생하면 Karpenter 파드 로그를 확인해서 관련 원인을 파악할 수 있습니다.

이상 카펜터를 알아보았습니다. 다음 장은 카펜터에서 안전하게 파드를 배포할 수 있도록 terminationGracePeriodSeconds, PodDisruptionBudget 옵션을 알아보겠습니다. 

해당 기술 블로그에 질문이 있으시면 언제든지 문의해 주세요. 직접 답변해 드립니다.
k8sqna@jennifersoft.com

Next

Contact Us

안녕하세요? 제니퍼소프트입니다.
기술 문의의 경우 질문자의 회사/이름/연락처를 본문에 기술해 주셔야만 원할한 지원이 가능합니다.
보내주신 문의 사항을 검토하여 빠른 시일 내에 답변해 드리겠습니다.

  • Chris
  • Irene

메일을 보냈습니다.

메일 전송이 완료되었습니다.
빠른 시일 내에 답변드리겠습니다.
감사합니다.
제니퍼소프트 웹사이트는 쿠키를 사용합니다. 쿠키에 대한 자세한 정보 및 삭제 방법은 제니퍼소프트의 개인정보처리방침을 참고하시기 바라며 본 사이트를 계속해서 이용하는 것은 제니퍼소프트의 쿠키 사용에 동의함을 의미합니다.