[Kubernetes] AWS EKS Karpenter 1.0 버전 기반 노드 Auto Scailing 최적화

들어가며

Karpenter는 AWS에서 개발한 EKS 환경에서 클러스터를 자동으로 조정하기 위한 프로비저닝 도구이다.

 

Karpenter 공식 문서

공식 문서를 따라 Karpenter 설치를 할 것이고 최적화를 위해 필요한 기능은 공식 문서를 참고하여 추가할 수 있다.

 

Getting Started with Karpenter

Set up a cluster and add Karpenter

karpenter.sh

 

설치 과정

Karpenter 및 Kubernetes 버전을 설정

KARPENTER_NAMESPACE, K8S_VERSION을 커스터마이징하면 된다.

export KARPENTER_NAMESPACE="karpenter-1"
export KARPENTER_VERSION="1.0.0"
export K8S_VERSION="1.29"

 

환경변수 설정

공식 문서는 Amazon Linux 2를 사용했지만 프로젝트 구성에 따라 Amazon Linux 2023을 사용하는 것으로 변경했다.

 

이 부분은 프로젝트의 아키텍처 요구사항에 따라 CLUSTER_NAME, AWS_DEFAULT_REGION, ARM_AMI_ID, AMD_AMI_ID를 커스터마이징하면 된다.

export AWS_PARTITION="aws"
export CLUSTER_NAME="cluster"
export AWS_DEFAULT_REGION="ap-northeast-1"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export TEMPOUT="templates"
export ARM_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2023/x86_64/standard/recommended/image_id --region ap-northeast-1 --query "Parameter.Value" --output text)"
export AMD_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2023/arm64/standard/recommended/image_id --region ap-northeast-1 --query "Parameter.Value" --output text)"

 

Karpenter 설치 전 사전 Role 생성하고 Policy 및 신뢰관계 생성

curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml  > "${TEMPOUT}" \
&& aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-file "${TEMPOUT}" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"

공식문서에서 제공하는 코드로 Cloudformation 스택으로 Karpenter에서 관리할 노드 그룹의 역할과 Karpenter 컨트롤러에서 사용할 정책이 생성된다.

 

--template-file 옵션으로 지정한 경로에 생성된 파일을 열어보면 KarpenterNodeRole-${ClusterName}, KarpenterControllerPolicy-${ClusterName}로 리소스가 생성될 것을 알 수 있다.

 

AWS 리소스를 직접 확인해본다.

공식 문서에서는 바로 EKS 클러스터를 생성하는 예제 코드를 제공하지만 우리는 이미 운영중인 클러스터에 적용해야 하므로 직접 연결해줄 것이다.

 

EKS 클러스터에 OIDC 제공자를 설정

eksctl utils associate-iam-oidc-provider --region ${AWS_DEFAULT_REGION} --cluster ${CLUSTER_NAME} --approve

 

IAM 역할 및 신뢰 관계 설정

aws iam create-role \
  --role-name ${CLUSTER_NAME}-karpenter \
  --assume-role-policy-document file://<(cat <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:oidc-provider/oidc.eks.${AWS_DEFAULT_REGION}.amazonaws.com/id/$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.identity.oidc.issuer" --output text | cut -d'/' -f5)"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.${AWS_DEFAULT_REGION}.amazonaws.com/id/$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.identity.oidc.issuer" --output text | cut -d'/' -f5):aud": "sts.amazonaws.com",
          "oidc.eks.${AWS_DEFAULT_REGION}.amazonaws.com/id/$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.identity.oidc.issuer" --output text | cut -d'/' -f5):sub": "system:serviceaccount:${KARPENTER_NAMESPACE}:karpenter"
        }
      }
    }
  ]
}
EOF
)

 

IAM 정책 연결

aws iam attach-role-policy --role-name ${CLUSTER_NAME}-karpenter --policy-arn arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}

 

Subnet, Security Group에 태그를 지정

이 부분은 어떻게 동작하는지 더 잘 이해하기 위해 아래의 기술 블로그를 참고했다.

 

Karpenter

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

techblog.gccompany.co.kr

 

EC2NodeClass을 생성하는 코드를 보면 Karpenter는 WorkerNode를 생성할 때 CR(Custom Resource)에 설정되어 있는 Tag값을 기반으로 동작한다.

 

우리가 Ingress를 생성할 때 Public Subnet에 태그를 설정하는 것과 동일한 것 같으므로 Worker Node가 생성될 모든 Subnet과 노드에 연결될 Security Group에 지정해주면 된다.

 

공식 예제 아래처럼 Key: karpenter.sh/discovery, Value: 클러스터 이름으로 지정되어 있는 것을 확인할 수 있다.

 

클러스터 엔드포인트와 카펜터 클러스터 Role

export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name "${CLUSTER_NAME}" --query "cluster.endpoint" --output text)"
export KARPENTER_IAM_ROLE_ARN="arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter"

echo "${CLUSTER_ENDPOINT} ${KARPENTER_IAM_ROLE_ARN}"

 

클러스터 엔드포인트와 우리가 Cloudformation로 생성한 KarpenterNodeRole이 아닌 이전에 직접 명령어로 생성한 Karpenter Controller Role Name이다.

 

우리는 Role을 두 개를 생성했으니 어디에 적용되는지 잘 구분해야한다.

 

다음은 선택저으로 보면 되는데 클러스터가 Private으로 운영된다면 인터넷 액세스가 불가능하므로 VPC 엔드포인트를 환경변수로 지정하는 것이고 이후에 추가적인 명령어가 필요하니 공식 문서를 찾아보면 좋을 것이다.

 

Karpenter 설치

# Logout of helm registry to perform an unauthenticated pull against the public ECR
helm registry logout public.ecr.aws

helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter --version "${KARPENTER_VERSION}" --namespace "${KARPENTER_NAMESPACE}" --create-namespace \
  --set "settings.clusterName=${CLUSTER_NAME}" \
  --set "settings.interruptionQueue=${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 \
  --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter" \
  --wait

 

기존에 운영하던 클러스터에 적용하는 거라 공식 문서엔 적혀있지 않았는데

--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter" \

이 부분을 추가하지 않아 엄청 헤매게 되었다.

이때 IRSA인 쿠버네티스의 ServiceAccount에 Role을 연결하고 이로 인증/인가하는 방식에 대해 자세히 공부하게 되었다.

 

 

Karpenter 기반 Node AutoScailing 최적화

NodePool과 NodeClass 생성

아래부터는 Karpenter 설치가 모두 완료된 것이고 이제 사용하면 된다.

 

상세한 옵션은 공식 문서 확인하면 된다.

 

NodePools

Configure Karpenter with NodePools

karpenter.sh

 

이 부분부턴 Karpenter는 설치가 완료된 이후이고 완벽하게 프로젝트 상황에 따라 커스터마이징하여 Node AutoScailing을 하면 된다.

 

다음은 아주 간단하게 NodePool과 EC2NodeClass를 생성하여 바로 적용하는 것이다.

Karpenter가 제공하는 옵션들을 아주 최소화한 것이니 틀만 참고하면 된다.

 

프로젝트에 적용한 NodePool의 옵션 소개

우리 프로젝트에선 오픈소스인 Harbor를 위한 인증서를 가져오도록 노드가 실행될 때 스크립트를 실행했고 우리는 CI/CD를 위한 오픈소스 소프트웨어만을 위한 워커 노드 그룹이 따로 존재했으므로 Pod가 특정 노드 그룹에서만 실행되도록 NodeAffinity를 설정했었다는 것을 보여주려는 것이다.

cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - key: pod
          operator: In
          values: ["app"] # NodeAffinity와 일치하는 레이블 요구
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["m"]
        - key: karpenter.k8s.aws/instance-generation
          operator: In
          values: ["7"]
        - key: karpenter.k8s.aws/instance-size
          operator: In
          values: ["large"] # M7i.large 인스턴스를 사용
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 2160h
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023 # Amazon Linux 2023
  userData: |
    #!/bin/bash
      mkdir -p /etc/containerd/certs.d/harbor.joon-test.shop
      curl -o /etc/containerd/certs.d/harbor.joon-test.shop/ca.crt https://harbor.joon-test.shop/certs/ca.crt
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  amiSelectorTerms:
    - id: "${AMD_AMI_ID}"
    - id: "${ARM_AMI_ID}"
EOF

 

아래처럼 서비스 파드가 특정 노드에서만 실행될 수 있도록 NodeAffinity를 설정되어있는 경우가 기본적일 것이다.

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
            - key: pod
              operator: In
              values:
                - app

 

다음과 같이 해당 부분을 추가해주면 된다.

 spec:
  template:
    spec:
      requirements:
        - key: pod
          operator: In
          values: ["app"] # NodeAffinity와 일치하는 레이블 요구

 

노드가 실행될 때 스크립트 명령어 실행

spec:
  amiFamily: AL2023 # Amazon Linux 2023
  userData: | # 노드가 실행될 때 스크립트 명령어 실행
    #!/bin/bash
      mkdir -p /etc/containerd/certs.d/harbor.joon-test.shop
      curl -o /etc/containerd/certs.d/harbor.joon-test.shop/ca.crt https://harbor.joon-test.shop/certs/ca.crt
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  amiSelectorTerms:
    - id: "${AMD_AMI_ID}"
    - id: "${ARM_AMI_ID}"
EOF

 

공식 문서 예제

공식 문서에선 배포 확장 및 축소해볼 수 있는 코드를 제공하고 수동으로 노드 삭제 시 적절한 노드에 파드 배치 등 다양한 에제를 제공하므로 반드시 운영하기 전에 테스트해봐야 한다.

 

Karpenter를 도입하여 프로젝트가 나아질 수 있는 부분

인스턴스를 스팟 인스턴스로부터 대여하는 옵션, 여러 인스턴스 타입을 후보로 두고 카펜터가 최적화하여 가장 효율적인 타입을 선택하는 옵션, 여러 노드의 여유 리소스가 존재하는 시점에 통합하여 하나의 노드로 운영하는 옵션, 노드가 실행될 때 스크립트를 실행하는 옵션 등 카펜터의 기능은 아주 많으니 참고하면 된다.