본문 바로가기
  • lakescript
스터디 이야기/AWS EKS

[AEWS] 5-3. Amazon EKS - AutoScaling (Karpenter)

by lakescript 2024. 4. 2.

Karpenter

Karpenter란?

Karpenter는 노드 수명 주기 관리를 유연하게 제공해 주는 오픈소스 솔루션으로, 단 몇 초 만에 컴퓨팅 리소스 제공합니다.

 

karpenter 주요 기능

- Kubernetes 스케줄러가 스케줄링할 수 없다고 표시한 Pod를 감시합니다.

- Pod가 요청한 스케줄링 제약 조(resource requests, nodeselectors, affinities, tolerations, topology)등을 판단합니다.

- Pod의 요구 사항을 충족하는 Node를 Provisioning합니다.
- 새로운 Node에서 Pod를 실행하도록 스케줄링합니다.
- Node가 더 이상 필요하지 않을 때 Node 제거합니다.

 

 

가장 큰 장점은 ASG를 건너뛰고 바로 EC2 Instance를 활용할 수 있다는 점입니다.

 

 

작동 방식

Karpenter는 Cluster 내에서 스케줄링 안된 Pod가 발견되면 해당 Pod의 Spec 및 요구 사항을 평가하고 Node를 Provisioning 하며, 사용하고 있지 않는 Node가 발견되면 해당 Node를 제거하여 Deprovisioning 합니다.

 

CA에서는 ASG를 통해 이러한 것들을 실행했다면, Kapenter에서는 Provisioner(현재는 Nodepool)이라는 Kubernetes Resource가 실행합니다. 그리고 kubernetes custrom resource 이기에 다른 워크로드 리소스처럼 ArgoCD, Spinnaker 등으로 배포할 수 있습니다.

또한 시작템플릿이 필요 없어 구성이 훨씬 간단합니다. 하지만 보안그룹과 서브넷은 필수로 설정해야 하는데, tag를 입력하거나 리소스 ID를 직접 적어줌으로써 해당 리소스를 설정할 수 있게 합니다. 특히 인스턴스 타입은 스팟 / 온디멘드 등 다양한 인스턴스 type을 설정할 수 있는 가드레일방식으로 선언 가능합니다. 

 

그리고 Karpenter는 Pod에 적합한 인스턴스 중 가장 저렴한 인스턴스로 증설되며, PV가 존재하는 서브넷에 Node가 생성되게 됩니다. 

 

특히, Node를 줄여도 다른 Node에 충분한 여유가 있다면 자동으로 정리해서 할당해 주며 Size가 큰 Node 하나가 작은 Node 여러 개보다 비용이 저렴하다면 자동으로 합쳐줍니다.

 

Karpenter 실습

공식문서에 나온 대로 간단한 실습을 진행해 보겠습니다.

환경변수 설정

export KARPENTER_NAMESPACE="kube-system"
export K8S_VERSION="1.29"
export KARPENTER_VERSION="0.35.2"
export TEMPOUT=$(mktemp)
export ARM_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2-arm64/recommended/image_id --query Parameter.Value --output text)"
export AMD_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2/recommended/image_id --query Parameter.Value --output text)"
export GPU_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2-gpu/recommended/image_id --query Parameter.Value --output text)"
export AWS_PARTITION="aws"
export CLUSTER_NAME="${USER}-karpenter-demo"
echo "export CLUSTER_NAME=$CLUSTER_NAME" >> /etc/profile

 

원활한 cli 배포를 위하여 위의 환경 변수를 설정하도록 하겠습니다.

특히, ARM_AMI_ID, AMD_AMI_ID, GPU_AMI_ID는 현재 Kubernetes version에서 사용할 수 있는 각 AMI 중 최근 Imgae ID를 가져오는 명령어입니다.

 

echo $KARPENTER_VERSION $CLUSTER_NAME $AWS_DEFAULT_REGION $AWS_ACCOUNT_ID $TEMPOUT $ARM_AMI_ID $AMD_AMI_ID $GPU_AMI_ID

 

위의 명령어로 잘 설정되었는지 한 번씩 확인해 주세요!

 

IAM Policy, Role 생성

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 Cluster 배포하기 위해 IAM Policy와 Role을 생성합니다.

 

 

EKS 배포

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
  serviceAccounts:
  - metadata:
      name: karpenter
      namespace: "${KARPENTER_NAMESPACE}"
    roleName: ${CLUSTER_NAME}-karpenter
    attachPolicyARNs:
    - arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}
    roleOnly: true

iamIdentityMappings:
- arn: "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}"
  username: system:node:{{EC2PrivateDNSName}}
  groups:
  - system:bootstrappers
  - system:nodes

managedNodeGroups:
- instanceType: m5.large
  amiFamily: AmazonLinux2
  name: ${CLUSTER_NAME}-ng
  desiredCapacity: 2
  minSize: 1
  maxSize: 10
  iam:
    withAddonPolicies:
      externalDNS: true

 

 

karpenter 실습을 위해 EKS Cluster를 생성합니다. 위에서 설정해 놓은 환경변수로 적용되니 꼭 주의해서 배포해 주세요.

 

...
  tags:
    karpenter.sh/discovery: ${CLUSTER_NAME}
...

 

특히 위의 설정 부분이 중요합니다. 이 Cluster를 Karpenter의 관리대상으로 설정하며 이로 인해 생성되는 모든 Kubernetes Resource들은 해당 tag가 설정되게 됩니다. 즉, Cluster에 tag를 설정함으로써 Karpenter가 관리하는 대상 범주에 속해지게 됩니다!

 

 

Karpenter 설치

helm install karpenter oci://public.ecr.aws/karpenter/karpenter --version "${KARPENTER_VERSION}" --namespace "${KARPENTER_NAMESPACE}" --create-namespace \
  --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=${KARPENTER_IAM_ROLE_ARN}" \
  --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 설치를 진행합니다.

 

 

 

NodePool 생성

apiVersion: karpenter.sh/v1beta1
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: ["spot"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["2"]
      nodeClassRef:
        apiVersion: karpenter.k8s.aws/v1beta1
        kind: EC2NodeClass
        name: default
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: 720h # 30 * 24h = 720h
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2 # Amazon Linux 2
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  amiSelectorTerms:
    - id: "${ARM_AMI_ID}"
    - id: "${AMD_AMI_ID}"

 

 

 

여기서 보셔야 할 것은 spec.disruptions 부분인데, 이 설정은 expireAfter 이후에 Karpenter Instance를 삭제하고 최신의 Instance를 유지한다고 보시면 됩니다.

또한 EC2 NodeClass도 중요합니다. tag로 설정한 karpenter.sh/discovery 값이 있는 서브넷과 보안그룹을 가져오고, 환경 변수로 설정한 AMI의 ID를 사용하여 EC2 Instance를 생성합니다.

 

kubectl get nodepool,ec2nodeclass

 

위의 명령어를 통해 정상적으로 생성되었는지 확인합니다.

 

 

 

부하테스트용 Pod 생성

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
          resources:
            requests:
              cpu: 1

 

 

해당 Pod는 pause 명령어를 실행하는 Pod로, Pod 1개에 CPU 1개 최소 보장하여 할당합니다.

현재 node는 2개인데 해당 EC2의 vCPU는 2 Core인데, 해당 Pod를 여러 개 배포하면 새로운 Node가 필요하게 됩니다.

 

 

Scale up

kubectl scale deployment inflate --replicas 5

 

위의 명령어를 사용하여 Pod의 수를 5개로 늘려보겠습니다. Pod 1개당 1 core CPU를 사용하기 때문에 Node의 리소스가 부족하여 새롭게 띄우고 해당 Pod를 Provisioning 시킵니다.

 

실제로 Pod의 요구사항에 맞는 Instance_type을 검색합니다.

 

 

 

가장 비용이 저렴한 Instance를 Spot 형태로 띄우게 됩니다.

 

 

 

해당 Node가 스케줄링 가능한 상태가 되었고, 나머지 Pod들도 정상적으로 할당된 것을 확인하실 수 있습니다.

 

Scale-down

kubectl delete deployment inflate && date

 

위의 명령어로 Pod를 삭제해 보겠습니다.

 

 

 

CA와는 다르게 정말 빨리 EC2 Instnace를 지우는 것을 확인하실 수 있습니다!!! 

 

Disruption 실습

 

Disruption은 일종이 최적화 전략이라고 보시면 되는데, Expiration, Drift, Consolidation의 3가지 종류가 있습니다.

 

Expiration(만료)

이 전략은 인스턴스가 일정 기간(기본적으로 720시간 또는 30일) 동안 존재한 후 자동으로 만료되도록 설정합니다. 즉, Node가 최신 상태를 유지할 수 있고, 오래된 Instance가 Cluster에 남아있지 않도록 합니다. (보안 패치나 중요한 업데이트를 적용하는데 좋습니다)

 

Drift(드리프트) :

클러스터의 구성 변경 사항(NodePool이나 EC2NodeClass 변경)을 감지하고, 필요한 변경 사항을 자동으로 적용하여 클러스터 구성을 최신 상태로 유지합니다.

 

Consolidation(통합) 

부하가 적은 시간에는 Node의 수를 줄이거나, 여러 작은 Instance를 더 크고 비용이 저렴한 Instance로 통합하는 등 사용되지 않는 리소스를 줄이고, 효율적인 컴퓨팅 리소스를 사용하여 클러스터의 비용을 최적화합니다. 

 

 

 

스팟 인스턴스 시작 시 Karpenter는 AWS EC2 Fleet Instance API를 호출하여 NodePool 구성 기반으로 선택한 인스턴스 유형을 전달합니다.

 

 

helm 설치

helm upgrade karpenter -n kube-system oci://public.ecr.aws/karpenter/karpenter --reuse-values --set settings.featureGates.spotToSpotConsolidation=true

v0.34.0부터 featureGates에 spotToSpotConsolidation 활성화로 사용 가능하기에 upgrade 해줍니다.

 

 

NodePool과 EC2 NodeClass 배포

apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    metadata:
      labels:
        intent: apps
    spec:
      nodeClassRef:
        name: default
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]
        - 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"]
  limits:
    cpu: 100
    memory: 100Gi
  disruption:
    consolidationPolicy: WhenUnderutilized
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: Bottlerocket
  subnetSelectorTerms:          
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  tags:
    Name: karpenter.sh/nodepool/default
    IntentLabel: "apps"

 

위의 yaml 파일을 작성하여 적용합니다.

 

...
 requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]
        - 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"]
...

 

EC2 Instance 조건도 유심히 봐주세요!

 

Pod 배포

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 5
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      nodeSelector:
        intent: apps
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.2
          resources:
            requests:
              cpu: 1
              memory: 1.5Gi

 

이번엔 cpu와 memory를 할당한 Pod를 배포해 보겠습니다.

 

 

배포하자마자 새로운 Node가 바로 띄어지는 것을 확인하실 수 있습니다.

 

kubectl scale --replicas=1 deployment/inflate

 

그 후 replicas를 조절하면 아래와 같이 최적화된 EC2 Instance로 변경되는 것을 확인하실 수 있습니다.

 


Reference

- https://karpenter.sh/docs/