저번에는 쿠버네티스의 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와 비교)
Pod가 확장되어 더 이상 기존 노드에 스케줄링되지 못할 경우 Pod는 Pending 상태로 변경 (CA와 공통)
Karpenter는 스케줄링되지 않은 Pod를 실시간(Just-in-time)으로 관찰 (CA는 설정 주기에 맞춰 관찰)
Provisioner 조건에 맞는 EC2 인스턴스 타입을 결정하고 (CA에서는 ASG와 Launch template를 통해 설정)
EC2 Fleet 요청을 통해 인스턴스를 생성 (CA에서는 ASG를 통해 인스턴스 프로비저닝)
추가된 노드가 준비되면, Karpenter는 Pod를 노드에 스케줄링 (CA에서는 kube-schedule가 스케줄링)
CA는 ASG를 통해 프로비저닝 하지만, Karpenter는 EC2 Fleet 요청을 통해 노드 프로비저닝하여 CA보다 빠르게 노드를 프로비저닝 한다.(노드 종류별 차이가 있으나 대부분 CA 대비 1분 30 초분 단축)
불필요한 Empty Node가 있는 경우 정리되는 속도도 빠른데, Node 제거에 대해서는Provisioner에 정의할 수 있는ttlSecondsAfterEmpty파라미터값을 정의하여 사용자 정의할 수도 있다.
다양한 인스턴스 타입 적용의 간소화
기존 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]
}
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' 작업에 대한 정책이 연결되어 있지 않아 코드를 추가해주었다
댓글