AWS 환경에서 EKS를 운영하면서 자동으로 확장 및 축소되는 기능을 설정하려면 AutoScaling 기능이 필요하다.
이를 위해 일반적으로 Cluster Autoscaler(CA)를 사용한다. 그러나 최근에 Karpenter라는 제품에 대한 관심이 늘어나고 있어, Karpenter 기준으로 두 제품을 사용하여 비교해 보고 느낀 차이점을 작성해 보려 한다.
Karpenter란?
Karpenter는 Cluster Autoscaler와 유사하게 Kubernetes 클러스터에서 자동 스케일링을 관리하기 위한 오픈 소스 도구이다.
아래 그림을 통해 두 제품의 실제 작동 방식을 비교해 보자.
Cluster Autoscaler(CA)의 동작은 아래와 같다.
- HPA에 의해 자동확장 또는 재배포하여 새로운 Pod가 생성된다.
- Kube-scheduler에 의해 기존에 존재하는 Worker node에 새로운 Pod를 할당하려 한다.
- 기존 Worker node에 새로운 Pod를 할당할 자원이 부족하여 Pod는 Pending 상태가 된다.
- Cluster Autoscaler는 Pending 상태의 Pod를 감지하고 Auto Scaling Group의 desired 값을 증가시킨다.
- Auto Scaling Group의 증가된 desired 값에 의해 새로운 Worker node가 생성된다.
- Kube-scheduler에 의해 새로 생성된 Worker node에 Pending 상태인 Pod가 배포된다.
Karpenter의 동작은 아래와 같다.
- HPA에 의해 자동확장 또는 재배포하여 새로운 Pod가 생성된다.
- Kube-scheduler에 의해 기존에 존재하는 Worker node에 새로운 Pod를 할당하려 한다.
- 기존 Worker node에 새로운 Pod를 할당할 자원이 부족하여 Pod는 Pending 상태가 된다.
- Karpenter는 Pending 상태의 Pod를 감지하고 새로운 Worker node를 생성한다.
- Kube-scheduler에 의해 새로 생성된 Worker node에 Pending 상태인 Pod가 배포된다.
Karpenter와 Cluster Autoscaler 간의 주요 차이점은 Auto Scaling Group(ASG)의 존재 여부이다.
Cluster Autoscaler는 AWS의 Auto Scaling Group(ASG)와 같은 클라우드 제공업체의 리소스를 활용하여 노드를 관리한다. ASG를 통해 노드를 확장하고 축소하며, 클러스터 노드의 리소스 상태를 모니터링한다.
반면에 Karpenter는 Auto Scaling Group(ASG) 없이 직접 노드를 관리한다. 즉, ASG와는 독립적으로 노드를 프로비저닝 하고 관리한다. 이를 통해 Karpenter는 ASG에 의존하지 않고 노드 관리를 보다 빠르고 세밀하게 제어할 수 있다.
Karpenter를 사용하면 아래와 같은 장단점이 있다.
장점
- 빠른 AutoScaling 속도
Karpenter는 Cluster Autoscaler 대비 단순한 구조로 Karpenter가 바로 autoscaling 작업을 진행하기 때문에 autoscaling 작업이 Cluster Autoscaler 보다 빠르다. (Auto Scaling Group의 Warm Pool 기능을 사용하면 Cluster Autoscaler에서도 scale out 속도를 높일 수 있다. 참고: [AWS Auto Scaling Group(ASG) Warm pool, Karpenter 속도 비교] ) - 효율적인 자원 관리
Cluster Autoscaler는 노드의 스펙 변경을 위해 Launch Template 수정, 노드를 다시 생성해야 하는 등 번거로움이 존재하며, 적은 리소스 부족으로 인해 scale-out이 진행되어 노드가 증가되면 기존 노드 타입과 동일한 instance type의 노드가 생성되어 자원 낭비가 발생할 수 있다. Karpenter는 사용할 instance type을 여러 개 지정할 수 있고 현재 자원 상황에 효율적인 instance type을 선택 후 노드를 생성한다. 그 외 스토리지 타입, 사이즈 등을 간단하게 설정할 수 있다. - 다양한 기능을 손쉽게 설정
Cluster Autoscaler는 설정 변경을 위해 배포되어 있는 Cluster Autoscaler Pod 재기동이 필요한데, Karpenter는 다양한 기능을 CRD(customresourcedefinition) 설정을 통해 Karpenter Pod 재기동 없이 손쉽게 설정할 수 있다.
예를 들어, ttlSecondsUntilExpired 설정을 통해 최신 이미지(ami)로 노드를 자동 업데이트하는 노드 lifecycle를 관리할 수 있고, ttlSecondsAfterEmpty 설정을 통해 사용되지 않는 노드의 종료 시점을 설정하여 빠르게 노드를 종료할 수 있다. 또한 현재 리소스의 상황보다 더 효율적인 상황을 인지하여 배포되어 있는 Pod를 조정하고 노드를 교체할 수 있는 Consolidation라는 기능의 메커니즘을 사용할 수 있다.
단점
- AWS Auto Scaling Group 기능 사용 불가능
Karpenter는 Auto Scaling Group을 사용하지 않기 때문에 ASG의 기능을 활용할 수 없다.
예를 들어, 노드의 최소 최대 개수를 지정할 수 없고 Karpenter의 cpu, memory 제한 설정으로 스케일링을 제한해야 한다. 또한 Lifecycle hooks 설정을 통해 Scale-in 되어 노드가 종료되기 전 처리해야 할 작업을 마무리할 추가 시간(최대 7200초)을 줄 수 있는 기능을 사용할 수 없다. - 구성 및 운영에 대한 추가 학습 필요
Karpenter는 오픈 소스 도구이므로 사용자들은 운영 환경에 적용하고 구성하기 위해 Karpenter의 설정 및 작동 방식을 이해하기 위한 시간과 노력을 투자해야 한다.
이제 Karpenter를 구성해 보고 AutoScaling이 잘 동작하는지 실습을 통해 확인해 보자.
실습
전제 조건
- AWS EKS 클러스터
- Helm CLI 도구
- AWS CLI 도구
설치 환경
- AWS EKS : 1.24.17
- Helm CLI : 3.8.2
- AWS CLI : 2.13.25
설치 버전
- Karpenter : 0.31.0
실습 절차
1. Karpenter 설정 및 배포1-1. Karpenter가 사용할 IAM role 생성
1-2. 서브넷 및 보안 그룹에 Tag 추가
1-3. aws-auth ConfigMap 업데이트
1-4. Karpenter 배포
2. AutoScaling 동작 확인
2-1. Scale out 테스트
2-2. Scale in 테스트
1. Karpenter 설정 및 배포
1-1. Karpenter가 사용할 IAM role 생성
KarpenterNodeRole이라는 IAM Role을 생성 후 아래와 같이 필요한 정책을 추가한다.
해당 Role은 Scale-out 된 노드가 사용할 IAM Role이다.
$ cat << EOF > node-trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
$ aws iam create-role --role-name "KarpenterNodeRole-wlsdn-eks" \
--assume-role-policy-document file://node-trust-policy.json
$ aws iam attach-role-policy --role-name "KarpenterNodeRole-wlsdn-eks" \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
$ aws iam attach-role-policy --role-name "KarpenterNodeRole-wlsdn-eks" \
--policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
$ aws iam attach-role-policy --role-name "KarpenterNodeRole-wlsdn-eks" \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
$ aws iam attach-role-policy --role-name "KarpenterNodeRole-wlsdn-eks" \
--policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
위에서 생성한 IAM Role에 인스턴스 프로파일을 생성 및 추가한다.
$ aws iam create-instance-profile \
--instance-profile-name "KarpenterNodeInstanceProfile-wlsdn-eks"
$ aws iam add-role-to-instance-profile \
--instance-profile-name "KarpenterNodeInstanceProfile-wlsdn-eks" \
--role-name "KarpenterNodeRole-wlsdn-eks"
KarpenterNodeRole이 잘 생성되었는지 AWS Console에서 확인한다.
KarpenterControllerRole이라는 IAM Role을 생성 후 아래와 같이 필요한 정책 및 신뢰관계를 추가한다.
해당 Role은 Karpenter Pod가 사용할 IAM Role이다. (IRSA)
$ cat << EOF > controller-trust-policy.json
{
"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"
}
}
}
]
}
EOF
$ aws iam create-role --role-name KarpenterControllerRole-wlsdn-eks \
--assume-role-policy-document file://controller-trust-policy.json
$ cat << EOF > controller-policy.json
{
"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:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-wlsdn-eks",
"Sid": "PassNodeIAMRole"
},
{
"Effect": "Allow",
"Action": "eks:DescribeCluster",
"Resource": "arn:aws:eks:ap-northeast-2:${AWS_ACCOUNT_ID}:cluster/wlsdn-eks",
"Sid": "EKSClusterEndpointLookup"
}
],
"Version": "2012-10-17"
}
EOF
$ aws iam put-role-policy --role-name KarpenterControllerRole-wlsdn-eks \
--policy-name KarpenterControllerPolicy-wlsdn-eks \
--policy-document file://controller-policy.json
KarpenterControllerRole이 잘 생성되었는지 AWS Console에서 확인한다.
1-2. 서브넷 및 보안 그룹에 Tag 추가
karpenter가 사용할 서브넷에 아래와 같이 tag를 추가한다.
- karpenter.sh/discovery : ${eks-cluster-name}
karpenter가 사용할 보안 그룹에 아래와 같이 tag를 추가한다.
- karpenter.sh/discovery : ${eks-cluster-name}
1-3. aws-auth ConfigMap 업데이트
Scale-out 된 노드가 EKS 클러스터에 조인할 수 있게 위에서 생성한 KarpenterNodeRole IAM role을 aws-auth ConfigMap에 추가한다.
$ kubectl edit configmap aws-auth -n kube-system
...
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-wlsdn-eks
username: system:node:{{EC2PrivateDNSName}}
...
1-4. Karpenter 배포
Helm을 이용하여 설치에 필요한 karpenter.yaml 파일을 추출한다.
$ helm template karpenter oci://public.ecr.aws/karpenter/karpenter --version v0.31.0 --namespace karpenter \
--set settings.aws.defaultInstanceProfile=KarpenterNodeInstanceProfile-wlsdn-eks \
--set settings.aws.clusterName=wlsdn-eks \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole-wlsdn-eks" \
--set controller.resources.requests.cpu=1 \
--set controller.resources.requests.memory=1Gi \
--set controller.resources.limits.cpu=1 \
--set controller.resources.limits.memory=1Gi > karpenter.yaml
Karpenter가 배포될 namespace를 생성하고 버전에 맞게 관련 CRD를 배포하고 Karpenter를 배포한다.
$ kubectl create ns karpenter
$ kubectl create -f \
https://raw.githubusercontent.com/aws/karpenter/v0.31.0/pkg/apis/crds/karpenter.sh_provisioners.yaml
kubectl create -f \
https://raw.githubusercontent.com/aws/karpenter/v0.31.0/pkg/apis/crds/karpenter.k8s.aws_awsnodetemplates.yaml
kubectl create -f \
https://raw.githubusercontent.com/aws/karpenter/v0.31.0/pkg/apis/crds/karpenter.sh_machines.yaml
$ kubectl apply -f karpenter.yaml
정상 설치 되었는지 확인한다.
$ kubectl get po -n karpenter
NAME READY STATUS RESTARTS AGE
karpenter-667f7bdc89-6c8rb 1/1 Running 0 3h56m
karpenter-667f7bdc89-cfhj7 1/1 Running 0 3h34m
2. AutoScaling 동작 확인
2-1. Scale out 테스트
Karpenter가 생성할 수 있는 노드와 노드에서 실행될 수 있는 Pod에 대한 제약 조건을 설정한다.
## provisioner.yaml
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: default
spec:
requirements:
- key: "node.kubernetes.io/instance-type"
operator: In
values: ["c5.large", "c5.xlarge"]
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: "topology.kubernetes.io/zone"
operator: In
values: [ "ap-northeast-2b", "ap-northeast-2c" ]
providerRef:
name: test
ttlSecondsAfterEmpty: 30
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
name: test
spec:
subnetSelector:
karpenter.sh/discovery: "wlsdn-eks"
securityGroupSelector:
karpenter.sh/discovery: "wlsdn-eks"
instanceProfile: KarpenterNodeInstanceProfile-wlsdn-eks
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 20Gi
volumeType: gp3
encrypted: true
위 설정을 배포 후 Nginx Pod를 배포하여 scale-out/in 동작을 확인해 보자.
배포할 Pod 정보는 아래와 같다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
name: http
protocol: TCP
resources:
requests:
cpu: "1.6"
memory: "2.8Gi"
nginx Pod의 Events를 확인해 보면 karpenter가 자원이 부족한 Pod를 인식하고, scale out 하여 확장된 노드에 할당시킨 것을 확인할 수 있다.
$ kubectl describe po nginx-xxxxx
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 42s default-scheduler 0/1 nodes are available: 1 Insufficient cpu, 1 Insufficient memory. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.
Normal Nominated 41s karpenter Pod should schedule on: machine/default-2cfs6
Normal Scheduled 9s default-scheduler Successfully assigned default/nginx-6477478df8-kfl7c to ip-192-168-7-146.ap-northeast-2.compute.internal
Normal Pulling 9s kubelet Pulling image "nginx:latest"
Normal Pulled 2s kubelet Successfully pulled image "nginx:latest" in 6.488098916s
Normal Created 2s kubelet Created container nginx
Normal Started 2s kubelet Started container nginx
Karpenter 로그를 살펴보자.
$ kubectl logs -n karpenter deploy/karpenter -f
2023-10-23T02:27:30.127Z INFO controller.provisioner found provisionable pod(s) {"commit": "322822a", "pods": "default/nginx-6477478df8-kfl7c"}
2023-10-23T02:27:30.127Z INFO controller.provisioner computed new machine(s) to fit pod(s) {"commit": "322822a", "machines": 1, "pods": 1}
2023-10-23T02:27:30.140Z INFO controller.provisioner created machine {"commit": "322822a", "provisioner": "default", "machine": "default-2cfs6", "requests": {"cpu":"1755m","memory":"3132306227200m","pods":"4"}, "instance-types": "c5.large, c5.xlarge"}
2023-10-23T02:27:32.449Z INFO controller.machine.lifecycle launched machine {"commit": "322822a", "machine": "default-2cfs6", "provisioner": "default", "provider-id": "aws:///ap-northeast-2c/i-0ef7895898e7ed62c", "instance-type": "c5.large", "zone": "ap-northeast-2c", "capacity-type": "on-demand", "allocatable": {"cpu":"1930m","ephemeral-storage":"17Gi","memory":"3114Mi","pods":"29"}}
provisioning이 필요한 pod를 감지 후 Pod에 필요한 스펙을 확인한 뒤 스펙에 맞는 instance type을
Pod가 필요로 하는 스펙은 cpu: 1755m(약 1.6) / mem: 3132306227200m(약 2.8Gib) 이기에 "cpu : 4 / mem : 8"인 c5.xlarge 타입보다는 "cpu : 2 / mem : 4"인 c5.large 타입을 선택 후 노드를 scale out 한 걸 확인할 수 있다.
AWS Console에서 확인해 보자.
노드 이름은 "karpenter,sh/provisioner-name/{Provisioner 이름}"으로 생성되었고 Provisioner와 AWSNodeTemplate 설정에 맞게 노드가 생성된 것을 확인할 수 있다.
2-2. Scale in 테스트
새로 배포했던 nginx Pod를 삭제 후 karpenter의 로그를 확인해 보자.
$ kubectl logs -n karpenter deploy/karpenter -f
2023-10-23T02:45:36.005Z DEBUG controller.machine.disruption marking empty {"commit": "322822a", "machine": "default-2cfs6"}
2023-10-23T02:46:15.855Z INFO controller.deprovisioning deprovisioning via emptiness delete, terminating 1 machines ip-192-168-7-146.ap-northeast-2.compute.internal/c5.large/on-demand {"commit": "322822a"}
2023-10-23T02:46:15.962Z INFO controller.termination cordoned node {"commit": "322822a", "node": "ip-192-168-7-146.ap-northeast-2.compute.internal"}
2023-10-23T02:46:16.328Z INFO controller.termination deleted node {"commit": "322822a", "node": "ip-192-168-7-146.ap-northeast-2.compute.internal"}
2023-10-23T02:46:16.703Z INFO controller.machine.termination deleted machine {"commit": "322822a", "machine": "default-2cfs6", "provisioner": "default", "node": "ip-192-168-7-146.ap-northeast-2.compute.internal", "provider-id": "aws:///ap-northeast-2c/i-0ef7895898e7ed62c"}
"ttlSecondsAfterEmpty: 30" 설정에 의해 비어있는 노드 감지 후 약 30초 후에 노드를 바로 삭제하는 것을 확인할 수 있다.
마치며
AWS EKS 환경에서 Auto Scaling 기능을 지원하는 Cluster Autoscaler와 Karpenter에 대해 알아보았다. Auto Scaling 제품은 상황과 요구 사항에 따라 어떤 제품이 더 적합한지를 고려해야 한다. 각 제품의 장단점을 고려하여 상황에 맞는 제품을 선택하여 구성한다면 더 안정적인 운영 환경을 구성할 수 있을 것이다.
'Orchestration > Kubernetes' 카테고리의 다른 글
K8sGPT 사용하여 Kubernetes 문제 해결하기 (0) | 2024.07.03 |
---|---|
AWS EKS AutoScaling 속도 비교(Cluster Autoscaler vs Kerpenter) (0) | 2023.10.22 |
AWS EKS 노드그룹 자동 시작, 중지하기 (0) | 2023.10.17 |
EKS add-on(vpc-cni) 업그레이드 이슈 (0) | 2023.05.24 |
EKS Pod 전용 Security group (SecurityGroupPolicy) (0) | 2023.05.18 |
댓글