새소식

기술/AWS

Karpenter 설치 및 후기

  • -
Karpenter 동작 구조

기존에는 EKS에서 노드 그룹을 관리 위해서는 Cluster-Autoscaler를 이용하여 Autoscaling 그룹을 조정하여 운영해왔습니다.
 
이럴 경우 몇가지 문제가 발생 됩니다.
 

1. Autoscaling 그룹은 인스턴스의 생성/삭제가 느리다.
2. Topology 이슈
3. 무조건 1대 이상의 인스턴스가 실행되어 있어야 한다.

 
1번의 경우 Autoscaling 그룹의 원하는 용량(desire)을 조정하여 스케일링 하므로, 동작이 매우 느린 단점이 있습니다.
 
2번의 경우 Autoscaling 그룹이 가용영역 별로 인스턴스의 수량을 균등하게 유지하려는 특성이 있기 때문에, PV가 있는 가용영역에 생성되지 않는 불상사(?)가 발생될 수 있습니다.
 
3번의 경우 빌드 서버로 예를 들면, 빌드 서버는 상시로 떠있을 이유가 없는데 CA(Cluster-Autoscaler 이하 CA)는 Autoscaling 그룹의 정책 상 1대 이상의 인스턴스가 유지되어야 운영이 되기 때문에 사용하지 않더라도 1대 이상을 상시로 유지해야 하는 문제가 있습니다.
 
 
대표적으로 이런 문제들로 인해 Karpenter를 이용하기로 마음 먹었습니다.
 
https://karpenter.sh/

Karpenter

Just-in-time Nodes for Any Kubernetes Cluster

karpenter.sh

 

Karpenter의 장점

Karpenter는 Just In Time 프로비저닝을 목표로 하며, Autoscaling 그룹을 이용하지 않습니다.
 
Pod, CRD를 이용하여, 인스턴스의 생성/삭제/관리를 합니다.
 
Provisioner 는 Node Family Type을 비롯한 프로비저닝할 Node의 크기를 지정할 수 있습니다.
AWSNodeTemplate 은 AMI를 비롯한 Node 설정에 대한 대부분을 지정할 수 있습니다.
 
조금 더 자세한 설정은 <Provisioner>와 <AWSNodeTemplate>를 참고해주세요.
 
 

CA 사용자의 Karpenter 설치

#주의 코드 박스에 보이는 것보다 많은 코드들이 있기 때문에 꼭 스크롤해서 보셔야 합니다.

대체로 <매뉴얼 페이지>에 잘 설명되어 있지만, 한글이 없어서 저의 경험을 녹여보겠습니다.

CLUSTER_NAME=<eks-cluster-name> #업그레이드에 사용할 클러스터 이름을 입력합니다.

AWS_PARTITION="aws" #그대로 둡니다.

#이하 부터는 aws cli를 사용하기 때문에 멀티 클러스터를 운영 중이라면 --profile을 이용하여 사용할 credentials을 지정합니다.
AWS_REGION="$(aws configure list | grep region | tr -s " " | cut -d" " -f3)" 

OIDC_ENDPOINT="$(aws eks describe-cluster --name ${CLUSTER_NAME} \
    --query "cluster.identity.oidc.issuer" --output text)"

AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' \
    --output text)

주석에도 적었지만 멀티 클러스터를 이용할 경우 aws cli 사용 시 --profile 옵션을 써서 사용할 credentials을 지정해야 합니다.
 
 
이제 Karpenter와 Node에서 사용할 IAM Policy를 작성합니다.

#다음 아래 두개의 IAM Policy를 생성합니다.
#Node에 부여할 IAM Policy를 생성합니다.

echo '{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}' > node-trust-policy.json

aws iam create-role --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --assume-role-policy-document file://node-trust-policy.json


aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --policy-arn arn:${AWS_PARTITION}:iam::aws:policy/AmazonEKSWorkerNodePolicy

aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --policy-arn arn:${AWS_PARTITION}:iam::aws:policy/AmazonEKS_CNI_Policy

aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --policy-arn arn:${AWS_PARTITION}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly

aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
    --policy-arn arn:${AWS_PARTITION}:iam::aws:policy/AmazonSSMManagedInstanceCore

#IAM Policy에 정책을 추가 완료한 후 EC2 Instance Profile 만들어 줍니다.

aws iam create-instance-profile \
    --instance-profile-name "KarpenterNodeInstanceProfile-${CLUSTER_NAME}"

aws iam add-role-to-instance-profile \
    --instance-profile-name "KarpenterNodeInstanceProfile-${CLUSTER_NAME}" \
    --role-name "KarpenterNodeRole-${CLUSTER_NAME}"



#Controller가 사용할 Policy를 생성합니다.
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-${CLUSTER_NAME} \
    --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_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"
}
EOF

aws iam put-role-policy --role-name KarpenterControllerRole-${CLUSTER_NAME} \
    --policy-name KarpenterControllerPolicy-${CLUSTER_NAME} \
    --policy-document file://controller-policy.json​

 
이제 VPC Subnet에서 Karpenter가 사용할 Subnet을 discovery할 수 있도록 태그를 삽입합니다.
 
다음 코드를 사용하면 됩니다.

for NODEGROUP in $(aws eks list-nodegroups --cluster-name ${CLUSTER_NAME} \
    --query 'nodegroups' --output text); do aws ec2 create-tags \
        --tags "Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}" \
        --resources $(aws eks describe-nodegroup --cluster-name ${CLUSTER_NAME} \
        --nodegroup-name $NODEGROUP --query 'nodegroup.subnets' --output text )
done

 
다음은 Security Group 입니다.

NODEGROUP=$(aws eks list-nodegroups --cluster-name ${CLUSTER_NAME} \
    --query 'nodegroups[0]' --output text)

LAUNCH_TEMPLATE=$(aws eks describe-nodegroup --cluster-name ${CLUSTER_NAME} \
    --nodegroup-name ${NODEGROUP} --query 'nodegroup.launchTemplate.{id:id,version:version}' \
    --output text | tr -s "\t" ",")

# EKS 클러스터에서 사용하는 기본 보안그룹만 사용하면 아래 코드를 사용합니다.

SECURITY_GROUPS=$(aws eks describe-cluster \
    --name ${CLUSTER_NAME} --query "cluster.resourcesVpcConfig.clusterSecurityGroupId" --output text)

# EC2 Lunch Template에서 따로 보안그룹을 사용하고 있다면 아래 코드를 사용합니다.

SECURITY_GROUPS=$(aws ec2 describe-launch-template-versions \
    --launch-template-id ${LAUNCH_TEMPLATE%,*} --versions ${LAUNCH_TEMPLATE#*,} \
    --query 'LaunchTemplateVersions[0].LaunchTemplateData.[NetworkInterfaces[0].Groups||SecurityGroupIds]' \
    --output text)



aws ec2 create-tags \
    --tags "Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}" \
    --resources ${SECURITY_GROUPS}

 

이제 aws-auth에서 새로 생성된 노드가 EKS에서 사용할 퍼미션을 부여합니다.

#kubectl cli를 이용하여 kube-system 네임스페이스에 있는 aws-auth를 수정합니다.
kubectl edit configmap aws-auth -n kube-system


#다음 내용을 추가합니다.
#변수로 지정되어 있는 rolearn 부분은 꼭 수정하여 입력합니다.
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}
  username: system:node:{{EC2PrivateDNSName}}
 

주석에 달린 것처럼 그룹 퍼미션을 추가할 때 변수로 선언되어 있는 부분을 꼭 실제 값으로 지정해주셔야 동작합니다.
 
Helm Template를 이용하여 설치에 사용할 manifest를 불러옵니다.

#Karpenter의 버전을 입력합니다.
#2023-07-14 기준 v0.29.0이 가장 최신 버전입니다.
export KARPENTER_VERSION=v0.29.0


#helm tempate를 이용하여 Karpenter를 설치할 Template를 불러옵니다.
helm template karpenter oci://public.ecr.aws/karpenter/karpenter --version ${KARPENTER_VERSION} --namespace karpenter \
    --set settings.aws.defaultInstanceProfile=KarpenterNodeInstanceProfile-${CLUSTER_NAME} \
    --set settings.aws.clusterName=${CLUSTER_NAME} \
    --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole-${CLUSTER_NAME}" \
    --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

제 운영 경험상 노드그룹을 엄청 많이 사용하고 있지 않다면...
 
 
cpu는 0.5, memory는 512Mi 정도 주어도 운영에 지장은 없었습니다.
 
다음은 저장된 karpenter.yaml에서 Karpenter Pod이 운영될 NodeAffinity를 수정합니다.

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: karpenter.sh/provisioner-name
          operator: DoesNotExist
      - matchExpressions:
        - key: eks.amazonaws.com/nodegroup
          operator: In
          values:
          - ${NODEGROUP}
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - topologyKey: "kubernetes.io/hostname"

저는 취향적으로 nodeSelector를 선호해서 주석처리 후 nodeSelector 옵션을 사용했습니다.
 
이제 대망의 Karpenter 배포입니다.

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

CRD와 함께 karpenter manifest를 이용하여 배포합니다.
 
Karpenter Pod가 정상적으로 동작중이라면, 처음엔 Erorr가 나오게 됩니다.
 
왜냐하면 Provisinor와 AWSNodeTemplate이 없기 때문 입니다.
 
다음은 Karpenter에서 제공하는 기본 Template 입니다.
 
인스턴스를 수정하고 싶으시면 Provisioner에서 다음 문서를 참고해서 requirements를 수정합니다. <Requirements>

cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  requirements:
    - key: karpenter.k8s.aws/instance-category
      operator: In
      values: [c, m, r]
    - key: karpenter.k8s.aws/instance-generation
      operator: Gt
      values: ["2"]
  labels:
   node_role: test
  providerRef:
    name: default
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    karpenter.sh/discovery: "${CLUSTER_NAME}"
  securityGroupSelector:
    karpenter.sh/discovery: "${CLUSTER_NAME}"
EOF

 
해당 내용을 배포 후 정상 작동하는지에 대한 여부는 아래 manifest를 이용하여 테스트하여 봅니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
  namespace: karpenter
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.2
          resources:
            requests:
              cpu: 1
      nodeSelector:
        node_role: test

위에 Provisinor에서 labels에 node_role: test라는 label을 추가하였기 때문에 해당 manifest에 포함되어 있습니다.
 
이제 해당 CLI를 이용하여 정상적으로 Scale Out이 되는지 확인 해봅니다.

kubectl scale -n karpenter deploy/inflate --replicas 1
kubectl scale -n karpenter deploy/inflate --replicas 2
kubectl scale -n karpenter deploy/inflate --replicas 3
kubectl scale -n karpenter deploy/inflate --replicas 4
kubectl scale -n karpenter deploy/inflate --replicas 5

정상적으로 Scale Out이 되는 것을 확인했다면 다음은 CA를 중지합니다.

kubectl scale deploy/cluster-autoscaler -n kube-system --replicas=0

이후 AutoScaling 그룹은 최소 수량을 유지하여 Karpenter가 동작하도록 운영하거나, Deprecate 시킵니다.
 
주의) Karpenter를 Karpenter가 생성한 노드에 배포/운영은 주의하라고 작성되어 있습니다. 잘 생각해서 배포/운영해야 합니다.
 
 

느낀 점

확실히 Karpenter를 사용하면 Scale Out이 엄청 빨라졌습니다.
 
기존에는 3분 정도 기다려야 노드 1대가 생성되는데, 요청하고 50초 정도면 노드가 생성되서 Ready 상태로 바뀌는 것 같습니다.
 
그리고 매번 필요할 때마다 노드 그룹을 새로 배포하기 보다 Provisinor를 통해서 노드 그룹에 대한 정의를 진행하니 노드 그룹 배포에 대한 부담감이 많이 줄었습니다.
 
다만 단점으로 느껴지는 부분도 적진 않은데요.
 
Spot 인스턴스가 삭제될 경우 기존 CA는 Spot 인스턴스를 새로 배포하여 드레인 시켜주지만, Karpenter는 별도 도구를 이용해서 운영해야 합니다.
 
Node Termination Handler라는 도구인데, SQS와 연동하거나 Daemonset으로 이용하거나 해야 합니다.
 
요건 아직 노하우가 적어서 조만간에 정리하도록 하겠습니다.
 
또 한가지는 topologySpreadConstraints 옵션을 사용하지 않으면 특정 가용영역에만 노드가 프로비저닝 되는 문제가 있습니다.
 
CA는 Autoscaling 그룹 특성이 반영되어 가용영역에 대한 밸런싱을 자동으로 진행했지만, Karpenter는 해당 옵션이 없기 때문에 Deployment manifest에서 옵션을 추가해야 합니다.
 
 
 
이상으로 Karpenter에 대한 소감을 마칩니다.
 
궁금하신 점은 댓글 달아주시면 최대한 답변해드리도록 하겠습니다.
 
감사합니다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.