본문 바로가기
Orchestration/Kubernetes

Karpenter로 AWS EKS 환경에 AutoScaling 구현하기

by wlsdn3004 2023. 10. 18.
728x90
반응형

 

AWS 환경에서 EKS를 운영하면서 자동으로 확장 및 축소되는 기능을 설정하려면 AutoScaling 기능이 필요하다.

이를 위해 일반적으로 Cluster Autoscaler(CA)를 사용한다. 그러나 최근에 Karpenter라는 제품에 대한 관심이 늘어나고 있어, Karpenter 기준으로 두 제품을 사용하여 비교해 보고 느낀 차이점을 작성해 보려 한다.

 

Karpenter란?

Karpenter는 Cluster Autoscaler와 유사하게 Kubernetes 클러스터에서 자동 스케일링을 관리하기 위한 오픈 소스 도구이다. 

 

아래 그림을 통해 두 제품의 실제 작동 방식을 비교해 보자.

Cluster Autoscaler(CA)의 동작은 아래와 같다.

  1. HPA에 의해 자동확장 또는 재배포하여 새로운 Pod가 생성된다.
  2. Kube-scheduler에 의해 기존에 존재하는 Worker node에 새로운 Pod를 할당하려 한다.
  3. 기존 Worker node에 새로운 Pod를 할당할 자원이 부족하여 Pod는 Pending 상태가 된다.
  4. Cluster Autoscaler는 Pending 상태의 Pod를 감지하고 Auto Scaling Group의 desired 값을 증가시킨다.
  5. Auto Scaling Group의 증가된 desired 값에 의해 새로운 Worker node가 생성된다.
  6. Kube-scheduler에 의해 새로 생성된 Worker node에 Pending 상태인 Pod가 배포된다.

 

Karpenter의 동작은 아래와 같다.

  1. HPA에 의해 자동확장 또는 재배포하여 새로운 Pod가 생성된다.
  2. Kube-scheduler에 의해 기존에 존재하는 Worker node에 새로운 Pod를 할당하려 한다.
  3. 기존 Worker node에 새로운 Pod를 할당할 자원이 부족하여 Pod는 Pending 상태가 된다.
  4. Karpenter는 Pending 상태의 Pod를 감지하고 새로운 Worker node를 생성한다.
  5. 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를 사용하면 아래와 같은 장단점이 있다.

장점

  1. 빠른 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 속도 비교] )
  2. 효율적인 자원 관리
    Cluster Autoscaler는 노드의 스펙 변경을 위해 Launch Template 수정, 노드를 다시 생성해야 하는 등 번거로움이 존재하며, 적은 리소스 부족으로 인해 scale-out이 진행되어 노드가 증가되면 기존 노드 타입과 동일한 instance type의 노드가 생성되어 자원 낭비가 발생할 수 있다. Karpenter는 사용할  instance type을 여러 개 지정할 수 있고 현재 자원 상황에 효율적인 instance type을 선택 후 노드를 생성한다. 그 외 스토리지 타입, 사이즈 등을 간단하게 설정할 수 있다.
  3. 다양한 기능을 손쉽게 설정
    Cluster Autoscaler는 설정 변경을 위해 배포되어 있는 Cluster Autoscaler Pod 재기동이 필요한데, Karpenter는 다양한 기능을 CRD(customresourcedefinition) 설정을 통해 Karpenter Pod 재기동 없이 손쉽게 설정할 수 있다.
    예를 들어, ttlSecondsUntilExpired 설정을 통해 최신 이미지(ami)로 노드를 자동 업데이트하는 노드 lifecycle를 관리할 수 있고, ttlSecondsAfterEmpty 설정을 통해 사용되지 않는 노드의 종료 시점을 설정하여 빠르게 노드를 종료할 수 있다. 또한 현재 리소스의 상황보다 더 효율적인 상황을 인지하여 배포되어 있는 Pod를 조정하고 노드를 교체할 수 있는 Consolidation라는 기능의 메커니즘을 사용할 수 있다.

단점

  1. AWS Auto Scaling Group 기능 사용 불가능
    Karpenter는 Auto Scaling Group을 사용하지 않기 때문에 ASG의 기능을 활용할 수 없다.
    예를 들어, 노드의 최소 최대 개수를 지정할 수 없고 Karpenter의 cpu, memory 제한 설정으로 스케일링을 제한해야 한다. 또한 Lifecycle hooks 설정을 통해
    Scale-in 되어 노드가 종료되기 전 처리해야 할 작업을 마무리할 추가 시간(최대 7200초)을 줄 수 있는 기능을 사용할 수 없다.
  2. 구성 및 운영에 대한 추가 학습 필요
    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 생성

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 제품은 상황과 요구 사항에 따라 어떤 제품이 더 적합한지를 고려해야 한다. 각 제품의 장단점을 고려하여 상황에 맞는 제품을 선택하여 구성한다면 더 안정적인 운영 환경을 구성할 수 있을 것이다.

반응형

댓글