Kubernetes

[Kubernetes] AWS Karpenter 완전 정복(CA와의 차이)

운덩하는 개발자 2024. 3. 22.
반응형

 

저번에는 쿠버네티스의 Cluster AutoScaling을 구현하였지만
노드가 초기화되고 준비되는데 오래 걸려 더 빠르게 프로비저닝 할 수 있는
Karpenter를 구현했던 내용에 대해서 정리해 보겠다✍.

 

Karpenter란?

AWS에서 제공하는 오픈 소스 Kubernetes 클러스터 오토스케일러다.
ASG(Auto Scaling Group)기반의 CA와는 달리 Karpenter는 ASG에 의존하지 않고, AWS Fleet API을 사용하여 더 빠른 프로비저닝을 지원한다.
심지어 파드를 노드에 배치할 때도 직접 Node-Binding을 하므로 scheduler 동작하지 않아 더 빠르다.

 



 

Karpenter 도입 계기

기존 Cluster Autoscaler를 사용하여 관리형 노드 그룹(Managed NodeGroup)을 통해 노드 프로비저닝을 진행을 했다. 하지만 이 방법은 몇 가지 한계를 가지고 있었는데

 



첫 번째로, 스파크성 트래픽이 급격히 증가하는 환경에서 노드가 프로비저닝 되는데 시간이 오래 걸린다

기본적인 CA는 ASG를 통해서 오토스케일링을 수행하므로 ASG 자체가 인스턴스를 초기화하고 프로비저닝 하는 단계에서 오래 걸린다. 하지만 Karpenter의 경우 ASG를 통해 오토스케일링을 수행하는 게 아닌 Fleet API를 사용하며, 파드를 노드에 배치할 때도 직접 Node-Binding을 하므로 scheduler 동작하지 않아 더 빠르다.



실제로 프로젝트를 수행할 때, CA와 Karpenter의 프로비저닝 속도를 비교해 본 결과 CA는 2분, Karpenter는 19초가 걸렸고, 개선된 상황을 수치로 따져보면 프로비저닝 시간을 84%가 개선되었다.

 

두 번째로, 비용 최적화에 제약이 있다.

관리형 노드 그룹을 사용할 경우, 인스턴스 유형이 미리 설정되며, 선택된 유형은 고정되어 있다. LaunchTemplate을 통해 하나의 노드 그룹 내에서 여러 인스턴스 유형을 혼합 사용할 수는 있지만, 이는 트래픽의 실시간 변화에 동적으로 반응하여 인스턴스 유형을 조정하는 것은 어렵다.

 

예를 들어, 트래픽이 분당 1,000건이든 100,000건이든, 프로비저닝 된 인스턴스 유형은 변경되지 않아 리소스가 남거나 부족할 수 있습니다. 이로 인해 사용되지 않는 리소스의 낭비가 발생할 수 있으며, 이는 비용 효율성을 떨어뜨린다.

 

반면, Karpenter는 Pending된 Pod의 요청량에 따라 실시간으로 최적의 인스턴스를 동적으로 선택하고 프로비저닝 하는데, 이는 트래픽이 급증할 때 적절한 리소스를 빠르게 추가하고, 트래픽이 감소했을 때 불필요한 리소스를 제거함으로써 리소스 사용률을 최적화하고 비용을 절감할 수 있다.

 

Karpenter 작동 원리 (CA와 비교)

 

 

  1. Pod가 확장되어 더 이상 기존 노드에 스케줄링되지 못할 경우 Pod는 Pending 상태로 변경 (CA와 공통)
  2. Karpenter는 스케줄링되지 않은 Pod를 실시간(Just-in-time)으로 관찰 (CA는 설정 주기에 맞춰 관찰)
  3. Provisioner 조건에 맞는 EC2 인스턴스 타입을 결정하고 (CA에서는 ASG와 Launch template를 통해 설정)
  4. EC2 Fleet 요청을 통해 인스턴스를 생성 (CA에서는 ASG를 통해 인스턴스 프로비저닝) 
  5. 추가된 노드가 준비되면, Karpenter는 Pod를 노드에 스케줄링 (CA에서는 kube-schedule가 스케줄링)

 

Karpenter 장점

  1. 신속한 노드 추가/제거
    • CA는 ASG를 통해 프로비저닝 하지만, Karpenter는 EC2 Fleet 요청을 통해 노드 프로비저닝하여 CA보다 빠르게 노드를 프로비저닝 한다.(노드 종류별 차이가 있으나 대부분 CA 대비 1분 30 초분 단축)
    • 불필요한 Empty Node가 있는 경우 정리되는 속도도 빠른데, Node 제거에 대해서는 Provisioner 에 정의할 수 있는 ttlSecondsAfterEmpty 파라미터값을 정의하여 사용자 정의할 수도 있다.

 

  1. 다양한 인스턴스 타입 적용의 간소화
    • 기존 Managed NodeGroup을 사용할 경우 ASG 와 Launch Template을 별도로 추가하여 관리하였지만, Karpenter를 도입하고 나서는 Provisioner를 통해 쉽게 노드 유형을 선언하고 사용 할 수 있다.
    • 기존의 CA에서는 Launch template 에서 하나의 인스턴스 타입만 선정할 수 있고, Node Group 에서 여러개의 인스턴스 유형을 정의하게 되면 다음과 같은 안내가 나온다.

 

기존의 Kubernetes Cluster Autoscaler(CA)는 노드 그룹 내 모든 노드가 동일한 리소스를 갖추고 있다고 가정하고 동작하기 때문에, 서로 다른 사양의 인스턴스를 혼합 사용할 경우 오토스케일링 과정에서 문제가 발생할 수 있다고 알려준다.

이는 클러스터의 자동 스케일링이 비효율적으로 이루어질 수 있으며,
결과적으로 불필요한 리소스 할당이나 비용 증가를 초래할 수 있다

하지만 Karpenter의 Provisioner 에서는 다양한 인스턴스 타입을 정의할 수 있는데,
 신규 Node 추가 시 
Karpenter 가 요청 Pod 에 가장 적절한 인스턴스 타입을 실시간으로 선택하므로
Kubernetes 클러스터 운영 효율을 논할 때 늘 나오는
Bin Packing 문제를 해결하며 비용도 최적화 할 수 있다.

spec:
  requirements:
    ## on-demand, spot 선택 또는 둘다 동시 사용
    - key: "karpenter.sh/capacity-type"
    operator: In
    values: ["on-demand"]
    ## 어느 가용용역에 배포
    - key: "topology.kubernetes.io/zone"
    operator: In
    values: [ "ap-northeast-2a", "ap-northeast-2c" ]
    ## 특정 인스턴스 타입으로 선언이 가능
    - key: "node.kubernetes.io/instance-type"
    operator: In
    values: [ "c5a.4xlarge", "c5a.8xlarge" ]
    ## 인스턴스 패밀리로 선언이 가능
    - key: "karpenter.k8s.aws/instance-category"
    operator: In
    values: ["c"]

 

 

구현 (Terraform, Helm Chart)

Karpenter는 Helm Chart 를 통해 간편하게 클러스터에 설치 할 수 있다.
Terraform 을 이용해 인프라를 코드 형태로 관리하면, AWS 서비스와 Kubernets 오브젝트를 프로비저닝 하는데 노동이 줄어드니 추천한다.

 

Terraform 코드

# Karpenter.tf

# Karpenter IAM 정책 설정
resource "aws_iam_policy" "karpenter_iam_policy" {
  name        = "karpenter_iam_policy-${var.infra_name}"
  description = "Karpenter policy"
  policy      = file("${path.module}/policy/karpenter_iam_policy.json")
}

data "aws_iam_policy_document" "karpenter_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    effect = "Allow"
    principals {
      type = "Federated"
      identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, "https://", "")}"]
    }
    condition {
      test = "StringEquals"
      variable = "${replace(data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, "https://", "")}:sub"
      values = ["system:serviceaccount:kube-system:karpenter"]
    }
  }
}

resource "aws_iam_role" "karpenter_iam_role" {
  name = "${var.cluster_name}-karpenter-role"

  assume_role_policy = data.aws_iam_policy_document.karpenter_assume_role_policy.json

  tags = {
    "Name" = "${var.cluster_name}-karpenter-role"
  }
}

resource "aws_iam_role_policy_attachment" "karpenter_iam_policy_attach" {
  role       = aws_iam_role.karpenter_iam_role.name
  policy_arn = aws_iam_policy.karpenter_iam_policy.arn
}

# Karpenter 서비스 계정 설정
resource "kubernetes_service_account" "karpenter_service_account" {
  metadata {
    name        = "karpenter"
    namespace   = "kube-system"
    annotations = {"eks.amazonaws.com/role-arn" = aws_iam_role.karpenter_iam_role.arn}
  }
}

resource "aws_iam_instance_profile" "karpenter_instance_profile" {
  name = "karpenter-instance-profile-${var.infra_name}"
  role = aws_iam_role.karpenter_iam_role.name
}


# Karpenter Helm Release 설정
resource "helm_release" "karpenter" {
  namespace  = "kube-system"
  name       = "karpenter"
  chart      = "karpenter"
  repository = "https://charts.karpenter.sh"

  set {
    name  = "clusterName"
    value = var.cluster_name
  }

  set {
    name  = "clusterEndpoint"
    value = module.eks.cluster_endpoint
  }

  set {
    name  = "aws.defaultInstanceProfile"
    value = aws_iam_instance_profile.karpenter_instance_profile.name
  }


  set {
    name  = "serviceAccount.create"
    value = "false"
  }

  set {
    name  = "serviceAccount.name"
    value = kubernetes_service_account.karpenter_service_account.metadata[0].name
  }

  depends_on = [aws_iam_role_policy_attachment.karpenter_iam_policy_attach]
}

 

 

Helm Chart로 선언한 Node Template

Karpenter가 EC2 인스턴스를 프로비저닝할 때 사용할 네트워크 설정

apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    karpenter.sh/discovery: amz-draw-dev-cluster
  securityGroupSelector:
    karpenter.sh/discovery: amz-draw-dev-cluster
  • karpenter.sh/discovery: amz-draw-dev-cluster 라벨이 있는 서브넷을 선택하도록 설정
  • karpenter.sh/discovery: amz-draw-dev-cluster 태그가 있는 보안 그룹을 사용하도록 설정

 

Helm Chart로 선언한  Provisioner

 Provisioner Karpenter 에 의해 생성되는 Node와 Pod에 대한 설정을 하기 위한 Custom Resource

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  requirements:

    - key: kubernetes.io/arch
      operator: In
      values:
        - amd64
    - key: "node_group"
      operator: "In"
      values: ["service_node_group"]
    - key: karpenter.sh/capacity-type
      operator: In
      values:
        - on-demand
    - key: node.kubernetes.io/instance-type
      operator: In
      values:
        - t3.large
        - t3.xlarge
        - t3.medium
        - c5.large
        - m5.large
        - m5.xlarge

  limits:
    resources:
      cpu: 1k
      memory: 1000Gi
  providerRef:
    name: default
  ttlSecondsAfterEmpty: 30
  • requirements: 생성될 노드가 충족해야 하는 조건들
      • kubernetes.io/arch: 노드 아키텍처가 amd64이어야 함을 지정
      • node_group: 노드 그룹이 "service_node_group"에 속하도록 지정
      • karpenter.sh/capacity-type: 노드가 on-demand 유형이어야 함을 명시
      • node.kubernetes.io/instance-type: 특정 인스턴스 유형(t3.large, t3.xlarge, t3.medium, c5.large, m5.large, m5.xlarge) 중에서 선택
    • limits: 생성될 노드의 리소스 상한을 지정
      • resources: 최대 1,000 CPU 단위와 1000Gi 메모리를 사용
    • providerRef: 이 Provisioner 설정을 관리하는 프로바이더의 참조입니다. 여기서는 default로 설정
    • ttlSecondsAfterEmpty: 노드가 비어 있는 상태로 있을 수 있는 시간을 초 단위로 지정합니다. 여기서는 30초 후에 비활성 노드를 제거

 

Deployment 배포 확인

 

 

Pod

 

 

배포 후 현재 클러스터 상태




부하 테스트(Jmeter)

 

 HTTPS로 www.amazon.shop의 443 포트로 /api/v1/qna 경로에 POST 요청 전송,
데이터는 Title, Content, Email 형태의 JSON




30초 동안  1000개의 사용자(스레드)가 생성,

설정된 요청을 10번 반복
간단히 10,000의 트래픽을 테스트 해보자


 

 

 

부하 테스트 결과

카펜터를 통해 다음과 같이 노드가 잘 늘어나는 것을 볼 수있다!

 

진행 중 발생했던 오류

2024-03-23T17:32:58.111Z        DEBUG   controller.provisioning 26 out of 483 instance types were excluded because they would breach provisioner limits {"commit": "5d4ae35-dirty"}
2024-03-23T17:32:58.128Z        DEBUG   controller.provisioning 26 out of 483 instance types were excluded because they would breach provisioner limits {"commit": "5d4ae35-dirty"}
2024-03-23T17:32:58.129Z        DEBUG   controller.provisioning Relaxing soft constraints for pod since it previously failed to schedule, removing: spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0]={"weight":100,"podAffinityTerm":{"labelSelector":{"matchLabels":{"app.kubernetes.io/component":"ingester","app.kubernetes.io/instance":"loki","app.kubernetes.io/name":"loki-distributed"}},"topologyKey":"failure-domain.beta.kubernetes.io/zone"}}     {"commit": "5d4ae35-dirty", "pod": "monitoring/loki-loki-distributed-ingester-1"}
2024-03-23T17:32:58.130Z        INFO    controller.provisioning Found 4 provisionable pod(s)    {"commit": "5d4ae35-dirty"}
2024-03-23T17:32:58.130Z        INFO    controller.provisioning Computed 1 new node(s) will fit 4 pod(s)        {"commit": "5d4ae35-dirty"}
2024-03-23T17:32:58.130Z        INFO    controller.provisioning Launching node with 4 pods requesting {"cpu":"1780m","memory":"3448Mi","pods":"9"} from types t3.large, m5.large, t3.xlarge, m5.xlarge  {"commit": "5d4ae35-dirty", "provisioner": "default"}
2024-03-23T17:33:00.397Z        ERROR   controller.provisioning Provisioning failed, launching node, creating cloud provider instance, with fleet error(s), UnauthorizedOperation: You are not authorized to perform this operation. User: arn:aws:sts::009946608368:assumed-role/amz-draw-dev-cluster-karpenter-role/1711212318146738001 is not authorized to perform: iam:PassRole on resource: arn:aws:iam::009946608368:role/amz-draw-dev-cluster-karpenter-role because no identity-based policy allows the iam:PassRole action. Encoded authorization failure message: n5r8-k-m5uBUuyWUsc3tz6SueopQJApQ53fXLAyZNYrT3SRyRGXKLQBIgeY8QdkzWp_b_ODXkSbLY_ExyBRIka4l5dLVhtI1UY2pCPxHgue2kUi-z_TNJUhGeZYr6q5K2MktfluYfpxmLkhmT1wJx0ykSbZY-sV76NfZOcO9IWgEtpZc0slIAON644uR2_qehHWhZoykBNjswMInSeyYZWFguT8shGW5KmL7LP74rEKByhnWNa0ft3hPnMBt-kIAP86TRIHNTBCZcqt_oevAzOoZXp5KFjLiC28lX3KzrkCtaQDBKgrtc_stpQr39qCE-qWqZNCVy-1ZL6uMRPziZ2D9_szFeExqZFv4-EL9hNBBjcaCoY74a6BW5IN11bsYLS9ah2kp-BB2Glo-F08lnuu01SP2XdWPpSqDrQ4WGcdd65aAmwLOpbBcQERGqOKlmq1cmnw3wtLbPN1KMtgY6r5BRVqQwASf_1X2g04h5NG52HcK0MMUP8iuhUSTsLglrUol_bVuwQQcp_9F69dujUMimGs3QT4lnxM3AdmQ8T6X_5FIa_6uVQ; UnauthorizedOperation: You are not authorized to perform this operation. User: arn:aws:sts::009946608368:assumed-role/amz-draw-dev-cluster-karpenter-role/1711212318146738001 is not authorized to perform: iam:PassRole on resource: arn:aws:iam::009946608368:role/amz-draw-dev-cluster-karpenter-role because no identity-based policy allows the iam:PassRole action. Encoded authorization failure message: 3i9lrUzIpWOUzqLoySDR9BbEDZ5f22N961QlAJn_EIJ8mau66W7KOZzuCibC_boRhQCExP_318JrcifJEibf0AP9SwnPXF5J6cUD2oE-hCgbZ8Pw_GrgIHsHv62tYu_hrmEaEDcHPxW0gBCH5VS6MVgWNJFA5iXMHIMn1hrHpXD-0CtsGldi1pBoMS9Sb6oKuHjQ9dWnYe8eNVzeSndVdYyhrV7jxxsqzlBtMRHZd03_ku5Lnov5ThmVLkXcvOrVBkut-3yrjcmhNdXImCkk19rZPS-h51x94QdPAvkRKafOceYNVrY7OM-y6WygIvV9buzap1URc_GXSEWbfDEW0ANV4ga8HfNQ0-7N3DWy-n2s5mNx3DHEK9p9gMvTYpGpuPoEPr54ZQPQrDBtr8lpFjL6ISCrE7yMRO0NNNAQ5Eq3awe9h6SHFGPYGTrM5VjYf9XYddF1LB1FawUTCQttZU03rcRbyqGPvn3ooyzOHYrpfLlYqSq3ABBsb0BN56SndDbaAo_C7R5X4T9u_N_Jq_YGfpJZSpdkWIruVgVbz-aCSpzp-vJplA; UnauthorizedOperation: You are not authorized to perform this operation. User: arn:aws:sts::009946608368:assumed-role/amz-draw-dev-cluster-karpenter-role/1711212318146738001 is not authorized to perform: iam:PassRole on resource: arn:aws:iam::009946608368:role/amz-draw-dev-cluster-karpenter-role because no identity-based policy allows the iam:PassRole action. Encoded authorization failure message: XwSpfQd40nWE1dOGcB3d_LRRVToBShSM45F-Z0U3CtFkLN6SWbqS6VaY28PkpEHb6n55wSxIs1xNNKQ2eI0Rvt_svm77PhN-JmbiaJTybdyeVjccO5VeIJgsRNTPFdVFBQ4idY5P-SCLdDmDslPp8uGvv4teum2zuDi_na0E7lPfWZUXKdtZbAN9jXIl3EwX6bZ8gQyqL48uEZQuIwNzzBxZuAqV1nuMogBPFMRm3oDtLAVsOhFsndcX0gLsqaa0bjDNzqkvYcRZ1__34D7LANTQrTjqt0C_2M3FMcwt0e3FxpwUdPpkZFJcgiv84z0gTuFW37YSw96mvbYC6hoaYAliKueBdljPTG3Co7x-2Rm8WsoOsWFpj1issRAPDO3sAykzMQMyLHHF2LcANJw7sNssV3NBuRX-4M18TaxId1MmpUQTuZfsU_u-ir1YuHzUkwNZDsGOp3pWzaQEsdoshVGD0sShz4480xL1w_GmAA8kmbDslrFBYQ3ELZRmCcy3ggrZnENkfRRJE-bZdC19qoOzeqthdVO-cUj75hC_oPbSNCguVP_L; UnauthorizedOperation: You are not authorized to perform this operation. User: arn:aws:sts::009946608368:assumed-role/amz-draw-dev-cluster-karpenter-role/1711212318146738001 is not authorized to perform: iam:PassRole on resource: arn:aws:iam::009946608368:role/amz-draw-dev-cluster-karpenter-role because no identity-based policy allows the iam:PassRole action. Encoded authorization failure message: xzoLCyMj8whEvzJSDUtya7-G6TGWxL6F9YQ5JAA99R0_TlEaG6itUH_C2WrSkO8s2uU_o9_nFt_znqpMNFpl2KhurdQsgRWbMEegY7We3C5IN9F6qvQClSMzCcWGNqby_aUqMnYyeNcai_-Y7FpAzCNSOOmAKRIPtCESJsUpfkRRSQIXUUBeJ2aUD__DKzVZrufy8AHkYz59bp6h8aGPbzvRg50fBTp20njB4_XBd_Bp155XSsVKvGyGKtIOwi12Bw1U8D1UpHGwvMN6grzr8Z-aqH6qcA1SfX9Q1vbGXyveZmDQZE2BnSBZGoqFEel-xKqseD-RX_cdRcz5kTZlRfpAadC0xXkv3SwDqISnzzI51OpDiF_UkwTlTPMnI9EASiOl55X8Lx5XHXrZzPi6gk-s-eDwfGEY2vgAC42ePxj1Phx0dbGtVVWlRQMTyJl0iDFclSDjl1DD6aKo2L1-l4avoh_0Gx0wjP5Ojo2-puj7ClQ7lJI_vZWONKGjQCupw3T493GrbOqfTuK6hdOr6vDXTdqJuHUBlLNCxLGLHVHY_5VamPk2xQ {"commit": "5d4ae35-dirty"}
2024-03-23T17:33:03.145Z

 

로그를 보면  Karpenter가 인스턴스를 만들고 관리하는 데 필요한 충분한 권한이 없다고 한다
'amz-draw-dev-cluster-karpenter-role' 역할에 'iam:PassRole' 작업에 대한 정책이 연결되어 있지 않아 코드를 추가해주었다 


 

반응형

댓글