AWS

Karpenter로 EKS에서 Spot 인스턴스 자동 프로비저닝하기

황동리 2025. 9. 3. 11:16
반응형

사전 준비

  • EKS 클러스터
  • EKS의 노드그룹
  • Helm

1. Karpenter 설치

먼저 Karpenter를 설치하기 전, Karpenter-Controller가 사용할 IRSA가 필요합니다.

1-1. IRSA 생성을 위한 IAM Role 생성

먼저 EKS의 ID 제공업체 확인해줍니다.

# 클러스터 정보 확인
aws eks describe-cluster \
  --name <CLUSTER_NAME> \
  --query "cluster.identity.oidc.issuer" \
  --output text

이제 Role 생성할 때 필요한 신뢰 관계를 json 파일로 만들어 줍니다.

cat > role.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>:sub": "system:serviceaccount:karpenter:karpenter"
        }
      }
    }
  ]
}
EOF

이제, Role을 생성 해줍니다.

aws iam create-role \
  --role-name KarpenterControllerRole \
  --assume-role-policy-document file://role.json

그리고 정책을 붙여주면 되는데 저는 아래 사진과 같이 붙여주었습니다.


KarpenterController-EC2NodeClass 를 제외한 Policy는 원래 노드 그룹에서 사용하던 정책을 그대로 가져왔습니다.


KarpenterController-EC2NodeClass 정책은 아래와 같습니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "EC2ReadForDiscoveryAndPricing",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeImages",
                "ec2:DescribeInstanceTypes",
                "ec2:DescribeInstanceTypeOfferings",
                "ec2:DescribeSpotPriceHistory",
                "ec2:DescribeSubnets",
                "ec2:DescribeVpcs",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeAvailabilityZones",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DescribeLaunchTemplates",
                "ec2:DescribeLaunchTemplateVersions"
            ],
            "Resource": "*"
        },
        {
            "Sid": "SSMParameterReadForAMIResolution",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameter",
                "ssm:GetParameters",
                "ssm:GetParametersByPath"
            ],
            "Resource": "*"
        },
        {
            "Sid": "AWSPriceListRead",
            "Effect": "Allow",
            "Action": [
                "pricing:GetProducts"
            ],
            "Resource": "*"
        },
        {
            "Sid": "InstanceProfileManageForKarpenter",
            "Effect": "Allow",
            "Action": [
                "iam:GetInstanceProfile",
                "iam:CreateInstanceProfile",
                "iam:DeleteInstanceProfile",
                "iam:AddRoleToInstanceProfile",
                "iam:RemoveRoleFromInstanceProfile",
                "iam:TagInstanceProfile"
            ],
            "Resource": [
                  "arn:aws:iam::<ACCOUNT_ID>:instance-profile/<CLUSTER_NAME>_*",
                    "arn:aws:iam::<ACCOUNT_ID>:instance-profile/karpenter-*"
            ]
        },
        {
            "Sid": "PassNodeRoleToEC2",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "arn:aws:iam::308910977993:role/dev-outcode-eks-node-group-role",
            "Condition": {
                "StringEquals": {
                    "iam:PassedToService": "ec2.amazonaws.com"
                }
            }
        },
        {
            "Sid": "ProvisionInstances",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateFleet",
                "ec2:RunInstances",
                "ec2:CreateTags",
                "ec2:DeleteTags",
                "ec2:TerminateInstances"
            ],
            "Resource": "*"
        },
        {
            "Sid": "CreateSpotServiceLinkedRoleIfMissing",
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:AWSServiceName": "spot.amazonaws.com"
                }
            }
        },
        {
            "Sid": "EC2LaunchTemplateRead",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeLaunchTemplates",
                "ec2:DescribeLaunchTemplateVersions"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ManageLaunchTemplates",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateLaunchTemplate",
                "ec2:DeleteLaunchTemplate",
                "ec2:CreateLaunchTemplateVersion",
                "ec2:DeleteLaunchTemplateVersions"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ManageENI",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateNetworkInterface",
                "ec2:AttachNetworkInterface",
                "ec2:DetachNetworkInterface",
                "ec2:DeleteNetworkInterface",
                "ec2:AssignPrivateIpAddresses",
                "ec2:UnassignPrivateIpAddresses"
            ],
            "Resource": "*"
        }
    ]
}

이렇게 정책을 넣어주면 Role 생성은 완료 입니다.

1-2. Karpenter 설치

저는 우선 Karpenter 공식 홈페이지에 나와있는 방법대로 EKS 클러스터 에서 Helm을 사용해서 설치를 하니, Controller의 이미지 Pull 하는 과정에서 자꾸 401 에러가 나왔습니다.


그래서 values.yaml 파일에 나와있는 public.ecr.aws/karpenter/controller:1.6.3 이미지를 제 로컬 노트북에 설치한 후, ECR에 Push 한 후 ECR에서 이미지를 Pull 하는 식으로 설치하였습니다.


Karpenter-provider-aws 주소


위 주소에서 values.yaml 파일을 확인 하실 수 있습니다.


앞서 생성한 Role의 ARN과 ECR 이미지 주소를 values.yaml 파일에 추가해서 설치를 진행하였습니다.


  • values.yaml 파일에서 수정한 부분
    serviceAccount:
    create: true
    name: "karpenter"
    annotations: 
      eks.amazonaws.com/role-arn: <앞서 생성한 Role의 ARN>
    controller:
    image:
      repository: <자신이 사용하는 이미지 레포지터리 주소>
      tag: <변경한 태그>

이제 설치를 진행 해주면 됩니다.

helm upgrade --install --namespace karpenter --create-namespace \
  karpenter oci://public.ecr.aws/karpenter/karpenter -f values.yaml

정상적으로 파드가 생긴 것을 확인 할 수 있습니다.



2. Spot 인스턴스 생성을 위한 EC2NodeClass 생성

EC2NodeClass 란?


Karpenter가 EC2 인스턴스를 띄울 때, 어떤 AWS 리소스(Subnet, SecurityGroup, AMI, IAM Role 등)를 사용할 지 정의하는 리소스 입니다.

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: <원하는 이름>
spec:
  amiFamily: AL2023 # Amazon Linux 2023, 해당 부분은 사용자가 원하는 OS 타입을 사용하면 됩니다.
  amiSelectorTerms:
    - alias: al2023@v20250821 # 저는 기존 EKS에서 동작하던 워커노드와 버전을 맞추었습니다.
  role: <기존 EKS의 노드 그룹에서 사용하던 Role의 이름>
  subnetSelectorTerms:
    - tags:
        kubernetes.io/cluster/dev-outcode-eks-cluster: shared # Karpenter가 새로 EC2 노드를 만들 때 어떤 Subnet에 노드를 띄울지 결정하는 조건, 해당 태그가 존재하는 서브넷에 EC2 생성함 
  securityGroupSelectorTerms:
    - tags:
        kubernetes.io/cluster/dev-outcode-eks-cluster: owned # EC2 인스턴스가 사용할 Security Group 선택하는 조건, 해당 태그가 존재하는 SG를 생성된 EC2가 사용함
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 100Gi
  metadataOptions:
    httpTokens: optional
    httpPutResponseHopLimit: 2
    httpEndpoint: enabled
  tags:
    Name: <원하는 태그 명>

3. 2. Spot 인스턴스 생성을 위한 NodePool 생성

NodePool 이란 ?


arpenter에서 클러스터의 노드 확장 정책을 정의하는 리소스입니다.


즉, “어떤 Pod이 들어오면 어떤 조건의 노드를 띄워줄지”를 결정하는 스케줄링 단위 입니다.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: <원하는 이름>
spec:
  template:
    metadata:
      # 생성될 노드에 추가할 라벨입니다. 'kubectl get nodes -l node-group-type=cache-nodes' 와 같이 노드를 선택할 때 유용합니다.
      labels:
        node-group-type: spot
    spec:
      # expireAfter: 노드가 생성된 후 지정된 시간이 지나면 자동으로 교체되도록 설정합니다.
      # 'Never'로 설정하면 시간 경과에 따른 자동 교체 기능을 사용하지 않습니다.
      expireAfter: 'Never'

      # nodeClassRef: 노드의 AWS 관련 상세 설정(AMI, 인스턴스 프로파일, 서브넷, 보안 그룹 등)을 정의한 EC2NodeClass를 연결합니다.
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: <앞서 생성한 EC2NodeClass 이름>

      # requirements: 생성될 노드가 충족해야 할 요구사항을 정의합니다. 파드의 요구사항과 조합하여 최종 노드 스펙이 결정됩니다.
      requirements:
        # 인스턴스 타입을 t3a.large로 설정.
        - key: node.kubernetes.io/instance-type
          operator: In
          values: ["t3a.large"]
        # 스팟 인스턴스만 사용하도록 설정합니다. (온디맨드도 가능)
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]

      # taints: 생성될 노드에 특정 테인트(taint)를 적용합니다.
      # 이 테인트를 용인(toleration)하는 파드만 이 노드에 스케줄링될 수 있습니다.
      taints:
        - key: spot
          value: "only"
          effect: "NoSchedule"

  # disruption: 노드를 축소하거나 교체하는 '중단(disruption)' 관련 정책을 정의합니다.
  disruption:
    # consolidationPolicy: 노드 통합(축소) 정책을 정의합니다.
    # 'WhenEmptyOrUnderutilized': 노드가 비어있거나(Empty) 또는 충분히 활용되지 않을 때(Underutilized) Karpenter가 노드를 통합(삭>제)하려고 시도합니다.
    # 'WhenEmpty': 노드가 완전히 비었을 때만 삭제합니다.
    consolidationPolicy: WhenEmptyOrUnderutilized
    # consolidateAfter: 노드가 통합 대상으로 고려되기까지 대기하는 시간입니다.
    # 예를 들어, 워크로드가 잠시 줄었다가 다시 늘어나는 경우를 대비해 너무 빨리 노드를 삭제하지 않도록 합니다.
    consolidateAfter: 30s

  # limits: 이 NodePool이 생성할 수 있는 전체 리소스의 양을 제한합니다. 비용 관리 및 안전장치 역할을 합니다.
  limits:
    cpu: "16" # 이 NodePool은 최대 16개의 vCPU까지만 생성할 수 있습니다.

이렇게 리소스들을 생성하고 파드의 toleration, nodeSelector에 아래와 같이 조건을 주면 karpenter가 알아서 노드를 생성하게 됩니다.

nodeSelector:
  "node-group-type": spot
tolerations:
  - key: "spot"
    operator: "Equal"
    value: "only"
    effect: "NoSchedule"

이상 입니다.

반응형