Karpenter

Jensen
여기어때 기술블로그
27 min readOct 5, 2023

--

안녕하세요. 여기어때컴퍼니 인프라개발팀에서 EKS(Elastic Kubernetes Service, AWS의 관리형 Kubernetes 서비스)를 담당하고 있는 젠슨입니다. 여기어때에서는 WorkerNode의 AutoScaling 도구로 Karpenter를 사용하고 있습니다.

일반적으로 POD의 수량이 부족한 상황이 되면 HPA에 의해 POD가 Scale out 되며 신규 배포가 수행됩니다. 이때 WorkerNode에 충분한 공간이 있다면 정상적인 배포가 이루어지겠지만 공간이 부족한 상황이라면 POD는 모두 Pending 상태에 빠집니다.

이러한 상황을 해결하기 위해서는 WorkerNode를 Scale out 해주는 과정이 필요한데 이러한 과정을 담당하는 도구가 Karpenter입니다. Karpenter를 이용하여 WorkerNode를 Scale out 하고 Pending 상태에 빠진 POD를 신규로 생성된 WorkerNode에 정상 배포 되도록 할 수 있습니다.

이러한 Karpenter의 설치부터 설정까지 하나 씩 알아보도록 하겠습니다.

1. Karpenter Controller 설치 준비

Karpenter를 사용하기 위해서는 Karpenter Controller를 설치해야 하는데 이전에 IAM Role, Policy, Subnet tag, SecurityGroup tag 등의 사전작업이 필요 합니다.

Karpenter를 이용하여 관리할 WorkerNode에서 사용할 Role에 다음과 같은 Policy를 추가합니다. Terraform, AWS CLI, AWS Console 무엇을 사용하든 상관 없습니다. 한 가지 주의하실 점은 Terraform으로 생성 시 Role Name과 Profile Name을 동일하게 생성해 주는 것이 좋습니다. 만약 기존에 사용하고 있던 WorkerNode Role이 있다면 해당 Role에 다음 Policy들을 추가해 주면 됩니다.

AmazonEKSWorkerNodePolicy
AmazonEKS_CNI_Policy
AmazonEC2ContainerRegistryReadOnly
AmazonSSMManagedInstanceCore

AWS Console에서 확인하면 다음과 같은 형태로 생성되었을 것입니다.

Karpenter WorkerNode Policy

다음으로는 Karpenter Controller가 사용할 Role과 Policy를 생성해야 합니다. 이 때 생성하신 Role Name을 잘 기억해 두시기 바랍니다. 추후에 Karpenter Controller 생성 시 Role Name이 필요합니다. 예를 들어 다음과 같은 이름으로 생성하도록 하겠습니다. KarpenterControllerRole

이제 해당 Role에 Policy를 생성해야 합니다. 마찬가지로 이름은 상관 없습니다. 아래 내용의 Policy만 포함되어 있으면 됩니다. ( “{}” 형태로 묶여 있는 값들은 자신의 AWS 환경에 맞게 변경하시기 바랍니다.)

{
"Statement": [
{
"Action": [
"ssm:GetParameter",
"ec2:DescribeImages",
"ec2:RunInstances",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeLaunchTemplates",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstanceTypeOfferings",
"ec2:DescribeAvailabilityZones",
"ec2:DeleteLaunchTemplate",
"ec2:CreateTags",
"ec2:CreateLaunchTemplate",
"ec2:CreateFleet",
"ec2:DescribeSpotPriceHistory",
"pricing:GetProducts"
],
"Effect": "Allow",
"Resource": "*",
"Sid": "Karpenter"
},
{
"Action": "ec2:TerminateInstances",
"Condition": {
"StringLike": {
"ec2:ResourceTag/karpenter.sh/provisioner-name": "*"
}
},
"Effect": "Allow",
"Resource": "*",
"Sid": "ConditionalEC2Termination"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}",
"Sid": "PassNodeIAMRole"
},
{
"Effect": "Allow",
"Action": "eks:DescribeCluster",
"Resource": "arn:${AWS_PARTITION}:eks:${AWS_REGION}:${AWS_ACCOUNT_ID}:cluster/${CLUSTER_NAME}",
"Sid": "EKSClusterEndpointLookup"
}
],
"Version": "2012-10-17"
}

이제 신뢰관계만 편집해 주면 Role 설정 부분은 완료됩니다. 아래 값으로 변경해 줍니다.(OIDC에 대한 설정 및 구성은 해당 링크를 참조 하시기 바랍니다.)

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_ENDPOINT#*//}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_ENDPOINT#*//}:aud": "sts.amazonaws.com",
"${OIDC_ENDPOINT#*//}:sub": "system:serviceaccount:karpenter:karpenter"
}
}
}
]
}

이렇게 Role관련 설정이 끝났습니다.

다음으로 tag 정보를 입력해야 하는데 Karpenter는 WorkerNode를 생성할 때 AWSNodeTemplate에 설정되어 있는 tag값을 기반으로 동작합니다. 추후에 해당 Sample을 보시면 이해가 가실 겁니다. 일단 여기서 사용할 tag값을 미리 정의해 놓는다 정도로 이해하시면 될 것 같습니다.(ingress를 생성해 보신분은 ingress 생성 시 해당 subnet에 tag를 하는 것과 유사하다고 보시면 됩니다.)

tag는 WorkerNode가 생성되는 Subnet과 WorkerNode가 사용할 혹은 사용하고 있는 SecurityGroup에 해주면 됩니다. 값은 본인이 원하는 값을 사용해 줍니다. 편의상 다음과 같이 하도록 하겠습니다.

key : karpenter.sh/discovery
value : <<<Cluster name>>>

Subnet tag
SecurityGroup tag

마지막으로 aws-auth configmap을 수정해 줘야 하는데 기존 WorkerNode Role을 그대로 사용하시는 분이라면 수정할 것 없이 그대로 사용하시면 됩니다. 신규 Role을 생성하셨다면 아래 절차와 같이 신규 Role 을 추가해 주셔야 합니다. 기존 WorkerNode Role이 eks-workernode-role이라고 가정했을 때 그 하단에 제일 위에서 생성했던 Karpenter에서 사용할 Node Role을 넣어 주시면 됩니다.(Karpenter Controller Role이 아닙니다)

kubectl edit configmap aws-auth -n kube-system

apiVersion: v1
data:
mapRoles: |
- rolearn: arn:aws:iam::<<<aws account>>>:role/eks-workernode-role
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
- rolearn: arn:aws:iam::<<<aws account>>>:role/<<<Karpenter Node Role Name>>>
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes

2. Karpenter Controller 설치

안타깝게도 지금까지가 Karpenter를 사용하기 위한 사전준비 단계였습니다. 지금부터가 진짜 Karpenter를 사용하기 위한 단계 입니다.

EKS에 명령어를 수행할 수 있는 Management Server에서 작업을 합니다. Karpenter 최신 버전은 아래 링크에서 확인하실 수 있습니다.

https://karpenter.sh

# Karpenter Version 설정
export KARPENTER_VERSION=v0.29.0

# helm repo 추가
helm repo add karpenter https://charts.karpenter.sh/
helm repo update

# oci를 이용한 설치는 helm 3.8 이상부터 사용 가능 합니다.
# 만약 이하 버전을 사용 중이라면 아래 명령어를 이용하여 helm을 update 합니다.
curl -L https://git.io/get_helm.sh | bash -s -- --version v3.8.2

# helm 을 이용하여 Karpenter를 설치 합니다.
# serviceaccount 는 위에서 기억해 놓으라고 했던 karpenter controller role name 입니다.
helm template karpenter oci://public.ecr.aws/karpenter/karpenter --version ${KARPENTER_VERSION} --namespace karpenter \
--set aws.defaultInstanceProfile=KarpenterInstanceProfile \
--set clusterEndpoint="${CLUSTER_ENDPOINT}" \
--set clusterName=${CLUSTER_NAME} \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole" \
--version ${KARPENTER_VERSION} > karpenter.yaml

# 위의 명령어를 수행하면 karpenter.yaml 파일이 생성됩니다.
# 해당 파일을 수정하여 Node 선호도를 수정해야 합니다.
# Karpenter Controller 가 추후에 Karpenter 에 의해 생성된 WorkerNode 에 생성되지 않도록 해야 합니다.
# nodeAffinity 부분을 찾아서 다음과 같이 수정 합니다.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: karpenter.sh/provisioner-name
operator: DoesNotExist
- matchExpressions:
- key: eks.amazonaws.com/nodegroup
operator: In
values:
- ${Karpenter에 생성되지 않은 현재 Worker NodeGroup Name}

# 네임스페이스와 CRD를 생성 후 배포 합니다.
kubectl create namespace karpenter
kubectl create -f https://raw.githubusercontent.com/aws/karpenter/${KARPENTER_VERSION}/pkg/apis/crds/karpenter.sh_provisioners.yaml
kubectl create -f https://raw.githubusercontent.com/aws/karpenter/${KARPENTER_VERSION}/pkg/apis/crds/karpenter.k8s.aws_awsnodetemplates.yaml
kubectl create -f https://raw.githubusercontent.com/aws/karpenter/${KARPENTER_VERSION}/pkg/apis/crds/karpenter.sh_machines.yaml
kubectl apply -f karpenter.yaml

# 배포가 잘 되었는지 확인합니다.(helm 버전을 변경해줘야 할 수도 있습니다.)
kubectl get pod -n karpenter
NAME READY STATUS RESTARTS AGE
karpenter-77645d555-d92gx 2/2 Running 0 27s
karpenter-77645d555-lhdgl 2/2 Running 0 27s

이제 Karpenter Controller 설치가 완료 되었습니다. 아래는 이 Controller를 이용하여 WorkerNode를 생성하는 방법입니다.

3. Provisioner 설정

WorkerNode를 생성하기 위해서는 Provisioner와 AWSNodeTemplate이 한 쌍으로 필요합니다. 먼저 Provisioner부터 살펴보도록 하겠습니다.

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: <<<provisioner name>>>
spec:
requirements:
# instance type을 정의 여러개를 한번에 정의할 수 있습니다.
- key: node.kubernetes.io/instance-type
operator: In
values: [ c6i.8xlarge, c6i.4xlarge ]
# WorkerNode를 생성할 Zone
- key: topology.kubernetes.io/zone
operator: In
values: [ ap-northeast-2a, ap-northeast-2c ]
# on-demand, spot 중 원하는 인스턴스를 선언, 둘다 정의할 수 있습니다.
- key: karpenter.sh/capacity-type
operator: In
values: [ on-demand ]
- key: kubernetes.io/os
operator: In
values:
- linux
- key: kubernetes.io/arch
operator: In
values:
- amd64
consolidation:
enabled: false
providerRef:
name: <<<AWSNodeTemplasteName>>>
ttlSecondsAfterEmpty: 30
ttlSecondsUntilExpired: 2592000

consolidation, ttlSecondsAfterEmpty, ttlSecondsUntilExpired 옵션들이 보일텐데 하나씩 설명해 드리겠습니다.

ttlSecondsAfterEmpty는 현재 Karpenter에 의해 생성된 WorkerNode에 DaemonSet 이외에 모든 POD들이 삭제되어 있는 상태라면 해당 WorkerNode를 해당 시간만큼 기다렸다가 삭제하라는 의미 입니다. 즉 불필요해진 WorkerNode로 판단되었기 때문에 해당 시간만큼 기다렸다가 삭제하는 것입니다.

ttlSecondsUntilExpired는 Karpenter에 의해 WorkerNode가 생성되었을 때 해당 WorkerNode의 수명을 의미합니다. 즉, 위에서 설정한 시간만큼 WorkerNode가 살아 있었다면 신규 WorkerNode로 교체하라는 것을 의미합니다. 시간은 “초”로 설정할 수 있으며 위의 값이 의미하는 것은 다음과 같습니다. 60sec * 60min * 24hour * 30days = 2592000

마지막으로 consolidation인데 이 옵션이 아주 중요합니다. consolidation은 WorkerNode의 상태를 감시하며 Node에 현재 배포되어 있는 POD들의 spec을 확인하여 해당 POD를 이동했을 때 WorkerNode를 삭제할 수 있다면 POD를 이동시킵니다. 결국 자원효율성을 최대한으로 사용하기 위한 옵션입니다. 주의 사항으로는 원하지 않는 POD의 이동이 자주 발생할 수 있으며 위에서 설명한 ttlSecondsAfterEmpty 옵션과 동시에 사용이 불가능하다는 점이 있습니다. 2개의 옵션 중 1개만 사용해야 합니다. 제가 위에서 사용한 Sample에도 consolidation을 false로 설정해 놓은 것을 보실 수 있을 겁니다.

4. AWSNodeTemplate 설정

이제 거의 다 왔습니다. Provisioner의 providerRef에 설정해 놓은 name의 AWSNodeTemplate만 생성해주면 됩니다.

apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
name: <<<AWSNodeTemplasteName>>>
spec:
# subnet, securityGroup tag
subnetSelector:
karpenter.sh/discovery: <<<subnet_tag_value>>>
securityGroupSelector:
karpenter.sh/discovery: <<<securityGroup_tag_value>>>
# WorkerNode Role Name
instanceProfile: <<<Instance_Role_Name>>>
# WorkerNode AMI
# amiFamily 사용 시 자동으로 최신버전의 AmazonLinux2 WorkerNode image 사용
# AL2, Bottlerocket, Ubuntu, Windows2019, Windows2022 등을 사용할 수 있습니다.
amiFamily: AL2
# 주석 처리 되어 있지만 amiSelector를 사용하여 특정 AMI 를 지정해줄 수도 있습니다.
# amiSelector:
# aws-ids: <<<AMI ID>>>
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 50
volumeType: gp3
iops: 10000
throughput: 125
deleteOnTermination: true
# WorkerNode tag
tags:
Name : <<<instance_name_tag>>>
# WorkerNode userdata
userData: <<<instance_userdata>>>

provisioner.yaml과 awsnodetemplate.yaml이 모두 완성되었으면 배포해 줍니다.

# 배포
kubectl apply -f provisioner.yaml
# 배포
kubectl apply -f awsnodetemplate.yaml

5. Karpenter 동작 테스트

모든 배포가 완료 되었지만 실제로 동작이 잘 되는지 확인해 봐야 합니다. 간단하게 AWS에서 제공하는 Sample을 통해서 확인해 보도록 하겠습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-to-scaleout
labels:
app: nginx-to-scaleout
spec:
selector:
matchLabels:
app: nginx-to-scaleout
replicas: 5
template:
metadata:
labels:
app: nginx-to-scaleout
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: nginx-to-scaleout
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 500m
memory: 512Mi
ports:
- containerPort: 80

위의 내용을 test.yaml 파일로 생성하여 배포 후 테스트를 진행하겠습니다.

# 배포
kubectl apply -f test.yaml

# 배포 확인
kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-to-scaleout-7687cb758c-8pbpg 1/1 Running 0 44s
nginx-to-scaleout-7687cb758c-958lv 1/1 Running 0 44s
nginx-to-scaleout-7687cb758c-kk2xh 1/1 Running 0 44s
nginx-to-scaleout-7687cb758c-n2v56 1/1 Running 0 44s
nginx-to-scaleout-7687cb758c-p77nt 1/1 Running 0 44s

# pod 수량을 30개로 늘려 보겠습니다.
kubectl scale --replicas=30 deployment/nginx-to-scaleout

# pod 상태 확인(다수의 pod 가 pending)
kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-to-scaleout-7687cb758c-5c4j8 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-6lx4k 1/1 Running 0 18s
nginx-to-scaleout-7687cb758c-7n976 0/1 Pending 0 18s
nginx-to-scaleout-7687cb758c-8pbpg 1/1 Running 0 2m6s
nginx-to-scaleout-7687cb758c-8prpv 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-958lv 1/1 Running 0 2m6s
nginx-to-scaleout-7687cb758c-b95vq 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-dqjrp 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-dvkqd 0/1 Pending 0 18s
nginx-to-scaleout-7687cb758c-dwnx8 0/1 Pending 0 18s
nginx-to-scaleout-7687cb758c-hvh4r 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-k49dp 0/1 Pending 0 18s
nginx-to-scaleout-7687cb758c-khhm4 0/1 Pending 0 18s
nginx-to-scaleout-7687cb758c-kk2xh 1/1 Running 0 2m6s
nginx-to-scaleout-7687cb758c-lq7xh 0/1 Pending 0 18s
nginx-to-scaleout-7687cb758c-ls5zl 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-lzx5p 1/1 Running 0 18s
nginx-to-scaleout-7687cb758c-msfvb 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-n2v56 1/1 Running 0 2m6s
nginx-to-scaleout-7687cb758c-n7wtp 0/1 Pending 0 18s
nginx-to-scaleout-7687cb758c-ncfld 0/1 Pending 0 18s
nginx-to-scaleout-7687cb758c-nwgsh 1/1 Running 0 18s
nginx-to-scaleout-7687cb758c-p77nt 1/1 Running 0 2m6s
nginx-to-scaleout-7687cb758c-r4qf2 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-rdmxv 1/1 Running 0 18s
nginx-to-scaleout-7687cb758c-v27pw 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-vqmgf 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-vrsnj 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-vwfcl 0/1 Pending 0 17s
nginx-to-scaleout-7687cb758c-wrz6k 0/1 Pending 0 17s

# 1~2분 정도 시간이 흐르면 신규 WorkerNode가 생성되고 pod 가 모두 정상배포 됩니다.
kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-to-scaleout-7687cb758c-22twb 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-4s5wv 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-5pfmk 1/1 Running 0 113s
nginx-to-scaleout-7687cb758c-6w498 1/1 Running 0 113s
nginx-to-scaleout-7687cb758c-8m8bv 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-94r2w 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-9l5nt 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-bvjkt 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-bxmrr 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-c59mm 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-c77nm 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-d59rk 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-dcpnm 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-fr7gv 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-g25zx 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-ghv7l 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-gjr7r 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-gqz29 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-hjhbg 1/1 Running 0 113s
nginx-to-scaleout-7687cb758c-mhcjr 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-mn7bl 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-qjtf5 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-qlzrd 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-r6vgx 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-rgldw 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-rhwt7 1/1 Running 0 113s
nginx-to-scaleout-7687cb758c-s9tbw 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-sn8jr 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-sv78b 1/1 Running 0 106s
nginx-to-scaleout-7687cb758c-xq4bn 1/1 Running 0 113s

# test 가 종료되면 test.yaml 파일을 삭제하여 node 수량이 줄어드는지 확인해 보시면 됩니다.
kubectl delete -f test.yaml
kubectl get node
NAME STATUS ROLES AGE VERSION
ip-1-1-1-1.ap-northeast-2.compute.internal Ready <none> 10m v1.27.5-eks-43840fb

지금까지 Karpenter의 설치부터 설정, Test까지 긴 여정을 함께 하셨습니다. 여기어때컴퍼니에서는 CA(ClusterAutoscaler)에서 Karpenter로 변경 후 WorkerNode의 생성 속도가 조금 더 빨라졌고 이외에도 코드로 Node들을 그룹화하여 관리하는 등 여러가지 장점들이 많아졌지만 반대로 소소한 단점들도 발생하였습니다. 다음 시간에는 이러한 단점들 중 한 가지에 대해서 이야기 해보도록 하겠습니다. 감사합니다.

다음편 예고…

“다음 주가 숙박세일페스타인데 우리 WorkerNode 수량 좀 늘려 놔야지?”

“네????”

--

--