🥳 200만 유저의 친구 ‘이루다’ 기술로 AI 캐릭터를 자유롭게 만들어보세요 ‘핑퐁 스튜디오’ 보러가기

Tech

Karpenter와 Spot으로 저렴하고 유연하게 노드 프로비저닝하기

Spot을 이용한 서버 비용 절감기

홍연준 | 2023년 03월 20일 | #Engineering #Kubernetes

핑퐁팀에서는 루다 서비스를 제공하기 위한 서버를 AWS EKS에서 운영하고 있어요. 운영하는 서버 중 상당수는 가격이 비싼 GPU 머신을 사용해요. 그래서 핑퐁팀에서는 서버 비용 절감을 위해 Inferentia 사용, Custom Metrics을 이용한 scaling 등 다양한 노력을 해왔어요. 이번 글에서는 AWS EC2 SpotKarpenter를 사용하여 서버 비용을 절감한 사례를 공유하고자 해요.

AWS EC2 Spot Instance이란? 🤔

AWS EC2 Spot Instance는 AWS Cloud의 남는 EC2 가용 용량을 사용하는 서비스에요. Spot은 On-Demand와 비교하여 최대 90% 할인된 금액을 제공해요. 하지만 AWS 가용 용량이 부족한 경우 2분 이내에 인스턴스가 회수돼요. 또한 Spot Instance는 Service Level Agreeement도 적용되지 않아요. Instance가 갑자기 종료될 수 있다는 점은 큰 위험 요소이지만, Fault Tolerant 한 시스템이라면 매우 저렴하게 EC2 Instance를 사용할 수 있어요.

Untitled

Spot Instance는 회수되기 2분 전 Spot Interrupt Event를 발행해요. 이벤트를 받은 Instance는 2분 이내에 작업을 종료해야 해요.

Spot Interrupt의 발생 확률이 증가하면 Rebalance Recommendation 이벤트도 발행돼요. 이 이벤트는 best-effort로 제공되기에 해당 이벤트 발행 이후 Spot Interrupt가 발생하지 않거나, 반대로 이벤트 발행 없이 Spot Interrupt가 발생할 수도 있어요. 하지만 Spot Interrupt와 달리 최대 15분 전에 발행되기에, proactive 하게 새로운 Spot Instance를 할당받거나 On-Demand Instance로 전환하여 interrupt를 최소화할 수 있어요.

Karpenter

핑퐁팀은 node provisioning을 위해 Cluster Autoscaler를 사용하고 있었어요. Cluster Autoscaler는 Unscheduled Pod가 발생할 경우 AutoScaling Group의 desired size를 조정하여 새로운 node를 provisioning하므로 반응 속도가 느렸어요. 하지만 Spot Instance를 도입하게 되면 Spot Interrupt 발생 상황에서 빠르게 새로운 node가 provisioning 되어야 해서 node provisioning 시간을 단축할 필요가 있었어요.

또한 핑퐁팀에서는 다양한 인공지능 모델들이 각각 특성에 맞는 GPU에 최적화 되어 서빙되고 있어 여러 종류의 instance type을 사용하고 있어요. Cluster Autoscaler는 eksctl과 CloudFormation을 사용하여 새로운 Node Group을 추가해야 하기에 새로운 instance type을 추가하기 불편하고 느리다는 단점도 있었어요.

그래서 저희는 Spot Instance를 적용하면서 Karpenter로 migration하기로 했어요. Karpenter는 Kubernetes Cluster의 node provisioning을 관리하는 오픈소스 프로젝트에요. Karpenter는 직접 EC2 Instance를 생성하여 node를 provisioning하므로 새로운 node 생성 속도가 훨씬 빨라요. 또한 생성할 node의 instance type 등을 kubernetes CRD를 사용하여 지정하기 때문에, 핑퐁팀 개발자라면 익숙한 helm과 ArgoCD로 누구나 쉽게 새로운 node type을 생성할 수 있어요.

Karpenter로 Spot Instance 사용 설정하기

그러면 Karpenter에서 Spot Instance를 설정하는 방법과 그 과정에서 신경써야하는 주의사항을 설명해볼게요.

Karpenter 설치

Karpenter는 Docs를 따라 Helm을 사용해 Cluster에 배포할 수 있어요. Helm CLI 명령어로 설치할 수도 있지만, 저희는 Git과 ArgoCD로 관리하기 위해 helm subchart로 구성했어요.

# Chart.yaml
apiVersion: v2
name: Karpenter

type: application
version: 0.1.0

dependencies:
  - name: karpenter
    version: v0.18.1
    repository: oci://public.ecr.aws/karpenter

Karpenter가 실행될 node는 EKS Managed Node Group으로 생성하고, taints와 toleration으로 Karpenter만 실행될 수 있도록 설정해주었어요.

# Values.yaml
karpenter:
  tolerations:
    - key: karpenter
      value: "true"
      effect: NoSchedule

Node Template

Node Template은 AWS specific한 node 설정을 하는 CRD에요. subnet, security group, userdata 등을 설정할 수 있어요.

# NodeTemplate.yaml
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    aws-ids: subnet-123455,subnet-123456

  securityGroupSelector:
    aws-ids: sg-123456,sg-123455

  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 100Gi
        volumeType: gp3
        iops: 3000
        throughput: 125
        deleteOnTermination: true

  userData: {{ .Files.Get "resources/userdata" | quote }}

Spot Capacity는 AZ(Availability Zone)에 따라서 다르기 때문에, 최대한 많은 Spot Capacity를 확보할 수 있도록 Region의 모든 AZ에 대한 Subnet을 생성하여 넣어주었어요.

Provisioner

Provisioner는 AWS Specific하지 않은 node 설정을 하는 CRD에요. Instance type, cpu architecture 등을 설정할 수 있어요.

# Provisioner.yaml
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: my-provisioner
spec:
  labels:
    type: my-provisioner
  requirements:
    - key: "node.kubernetes.io/instance-type"
      operator: In
      values:
        - c5.xlarge
        - c6g.xlarge
    - key: "karpenter.sh/capacity-type"
      operator: In 
      values:
        - spot
        - on-demand
  consolidation:
    enabled: true
  providerRef:
    name: default

Provisioner 설정에 따라 Karpenter는 Spot Fleet을 사용해 EC2 Instance를 요청해요. Spot Fleet은 다양한 instance type과 allocation strategy를 지정하면 적절한 EC2 인스턴스를 생성해주는 서비스에요. Allocation strategy는 lowestPrice, capacityOptimized 등 여러 옵션이 있지만, Karpenter는 spot capacity와 price를 적절히 고려하는 priceCapacityOptimized 전략을 사용하고 있어요. 최대한 다양한 instance type를 지정해야 Spot Interrupt와 비용을 줄일 수 있어요 . Instance type을 지정할 때에는 CPU, Memory 등의 attribute로 선택할 수도 있지만 핑퐁팀에서는 하드웨어에 민감한 서버가 많기 때문에 직접 지정하고 있어요.

Node Termination Handler

node-termination-handler(NTH)는 eks 환경에서 Spot Interrupt를 처리하는 프로젝트에요. karpenter에서도 Spot Interrupt에 대한 처리는 지원하지만, rebalance recommendation에 대한 처리는 지원하지 않아요. 하지만 NTH에서는 rebalance recommendation 이벤트 발생 시 node를 cordon 및 draining하도록 설정할 수 있어 Spot Interrupt가 발생하기 전 proactive하게 on-demand node 또는 다른 spot node로 Pod을 옮길 수 있어요.

# Chart.yaml
apiVersion: v2
name: karpenter
type: application
version: 0.1.0
dependencies:
  - name: karpenter
    version: v0.18.1
    repository: oci://public.ecr.aws/karpenter
  - name: aws-node-termination-handler
    version: 0.19.3
    repository: https://aws.github.io/eks-charts
# Values.yaml
aws-node-termination-handler:
  enableSpotInterruptionDraining: true
  enableRebalanceMonitoring: true
  enableRebalanceDraining: true
  enableScheduledEventDraining: true
  nodeSelector:
    "karpenter.sh/capacity-type": spot
    "karpenter.sh/initialized": "true"

이제 Spot Instance 사용 준비가 끝났어요. 핑퐁팀에서는 안정성을 높이기 위해 On-Demand 최소 개수 설정, 커스텀한 Rebalance Recommendation 처리 로직 등을 추가로 적용했어요. 이러한 방법은 기회가 된다면 다른 블로그 글에서 다루어볼게요.

Fault Injection Simulator

Fault Injection Simulator (FIS)는 AWS에서 제공하는 chaos engineering을 위한 서비스에요. FIS를 사용하면 임의의 Spot Instance에 Spot Interrupt를 발생시킬 수 있어요. Spot Instance 설정이 끝난 테스트 인프라에서 FIS를 이용하여 Karpenter와 Node Termination Handler가 잘 동작하는지 확인할 수 있어요.

{
  "description": "Spot Interrupt",
  "actions": {
    "rbr_node": {
      "actionId": "aws:ec2:send-spot-instance-interruptions",
      "description": "Terminate spots",
      "parameters": {
        "durationBeforeInterruption": "PT2M"
      },
      "targets": {
        "SpotInstances": "spot"
      }
    }
  },
  "targets": {
      "resourceType": "aws:ec2:spot-instance",
      "selectionMode": "ALL",
      "resourceTags": {
        "Name": "my-node"
      },
      "filters": [
        {
          "path": "State.Name",
          "values": [
            "running"
          ]
        }
      ]
    }
  },
  "stopConditions": [
    {
      "source": "none"
    }
  ],
  "roleArn": "arn:aws:iam::(account id):role/spot-fis-role"
}
aws fis create-experiment-template --cli-input-json experiment.json
aws fis start-experiment --experiment-id (experiment-id)

마치며

이번 포스트에서는 핑퐁팀이 서버 비용 절감을 위해 프로덕션에서 Spot을 적용한 방법에 대해 소개했어요. Spot Instance의 특성상 운영 초반에는 많은 시행착오를 겪었지만, 어느 정도의 안정성이 확보된 이후에는 훨씬 저렴하게 서버를 운영 할 수 있게 되었어요.

핑퐁팀은 안정적이고 지속가능한 서비스를 위해 다양한 고민을 하고 있습니다. 저희와 함께 이런 문제들을 풀어보고 싶은 분들은 채용 공고를 참조해주세요!

스캐터랩이 직접 전해주는
AI에 관한 소식을 받아보세요

능력있는 현업 개발자, 기획자, 디자이너가
지금 스캐터랩에서 하고 있는 일, 세상에 벌어지고 있는 흥미로운 일들을 알려드립니다.