CloudNet@ Team Study/EKS Workshop 4th Cohort

Introduction to EKS Managed Node Groups

"Everything about infra" 2026. 4. 1. 03:17
포스팅은 CloudNet@팀 서종호(Gasida)님이 진행하시는
AWS EKS Workshop Study 내용을 참고하여 작성합니다.

 

안녕하세요!

오늘은 파드를 관리하는 EKS 관리형 노드 그룹의

종류에 대해서 학습하는 시간을 갖도록 하겠습니다.

 

 

 

01. On-Demand 노드 그룹

먼저 온디맨드가 기준선이라서 먼저 소개하도록 하겠습니다.

EKS 관리형 노드 그룹의 기본 동작 방식을 이해해야 ARM과 Spot의 차이점을 알 수 있습니다.

 

📌 EKS 관리형 노드 그룹이란?

  • 노드 프로비저닝 / 업그레이드 / 교체를 AWS가 대신 해줍니다 → ASG(AutoScalingGroup) 위에서 동작
  • vanilla k8s 에서는 직접 EC2를 띄우고 kubelet을 설정해야 했던 것을 AWS가 흡수함.
  • 'eksctl get nodegroup' 출력의 TYPE: managed가 이걸 증명하는 근거입니다.

주목할 컬럼은 아래와 같습니다.

MIN SIZE / MAX SIZE / DESIRED : ASG 스케일링 범위 입니다. 현재 노드 2개가 떠있는 이유죠.
INSTANCE TYPE : t3.medium을 사용중이며, 단일 인스턴스 타입이 지정됨 (Spot과 달리 하나만)
TYPE : managed라고 적혀있는 것이 AWS 관리형 노드 그룹임을 명시하는 것입니다.

 

1) 노드 레이블 확인

'--label-columns' 옵션은 보통 'kubectl get nodes'에서는 보이지 않는 레이블 값을 컬럼으로 출력하는 옵션임.

NODEGROUP: myeks-ng-1 → 두 노드 모두 같은 노드 그룹에 속함
ARCH: amd64 → x86_64 계열 인텔/AMD 프로세서
CAPACITYTYPE: ON_DEMAND → 온디맨드 인스턴스

이 세 레이블은 EKS가 노드 프로비저닝 시 자동으로 붙여주는 것이고 사용자가 직접 label로 붙이는 것이 아님.

EKS관리형 노드 그룹이 노드를 등록할 때 노드 그룹 메타데이터를 읽어서 자동으로 주입합니다.

 

2) 노드 그룹 상세 확인

aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-ng-1 | jq
{
  "nodegroup": {
    "nodegroupName": "myeks-ng-1",
    "nodegroupArn": "arn:aws:eks:ap-northeast-2:093359840099:nodegroup/myeks/myeks-ng-1/a4cea252-6d58-f9df-a79e-39a0c756e086",
    "clusterName": "myeks",
    "version": "1.35",
    "releaseVersion": "1.35.2-20260318",
    "createdAt": "2026-04-01T01:05:50.776000+09:00",
    "modifiedAt": "2026-04-01T01:27:13.304000+09:00",
    "status": "ACTIVE",
    "capacityType": "ON_DEMAND",
    "scalingConfig": {
      "minSize": 1,
      "maxSize": 4,
      "desiredSize": 2
    },
    "instanceTypes": [
      "t3.medium"
    ],
    "subnets": [
      "subnet-0bf7b24c86d4a7117",
      "subnet-039388558cb980c15",
      "subnet-0b17ff49d2a15c150"
    ],
    "amiType": "AL2023_x86_64_STANDARD",
    "nodeRole": "arn:aws:iam::093359840099:role/myeks-ng-1",
    "labels": {
      "tier": "primary"
    },
    "resources": {
      "autoScalingGroups": [
        {
          "name": "eks-myeks-ng-1-a4cea252-6d58-f9df-a79e-39a0c756e086"
        }
      ]
    },
    "health": {
      "issues": []
    },
    "updateConfig": {
      "maxUnavailablePercentage": 33
    },
    "launchTemplate": {
      "name": "primary-20260331160542409100000009",
      "version": "1",
      "id": "lt-0ff041fe3f5d26d86"
    },
    "tags": {
      "Terraform": "true",
      "Environment": "cloudneta-lab",
      "Name": "myeks-ng-1"
    }
  }
}

위에서 설명드렸던 필드들은 생략하고 포인트를 짚어보겠습니다.

capacityType: ON_DEMAND → Spot이 아님을 명시하는 필드이며, Terraform으로 배포할 때 capacity_type을
지정하지 않으면 기본값이 온디맨드 입니다.

updateConfig: maxUnavailablePercentage: 33 → 노드 업그레이드 시 동시에 최대 33%의 노드까지
unavailable 상태를 허용한다는 설정이며, 노드가 2개라면 최대 1개씩 교체하는 롤링 업그레이드 전략입니다.

launchTemplate → Terraform이 생성한 런치 템플릿입니다.
userdata, security group, hop limit 설정 등이 여기에 담기며, EKS가 새 노드를 프로비저닝할 때 이 템플릿을 참조함.

 

즉 요약하자면, 온디맨드는 EKS 관리형 노드 그룹이며 EC2 ASG + AWS 관리 라이프 사이클 중 기본값 입니다.

(온디맨드 특성 상 비용이 고정적이고 중단이 없습니다 → Stateful 서비스나 중단 허용이 안되는 워크로드의 기본 값)

 


 

02. AWS Graviton (ARM) 노드 그룹

이 섹션에서 두 가지 레이어를 설명할 예정입니다.

온디맨드와 가격을 비교하는 다이어그램과, ARM에서 어떻게 격리를 시키는지에 대한 다이어그램으로 나뉩니다.

 

 

📌 왜 온디맨드보다 ARM이 저렴할까?

  • Graviton = AWS가 직접 설계한 64-bit ARM 칩 → x86 라이선스 비용 없음 + 전력 효율
  • 컨테이너 워크로드는 단순 반복 연산이 많아 RISC 코어에 더 잘 맞음
  • 가격 대비 성능 20~40% 향상이라는 수치의 근거: 인텔 라이선스 제거 + 칩 설계 최적화

뭔가 잘 와닿지 않을 수 있기 때문에 딥하게 한번 말씀드려보겠습니다.

x86(인텔/AMD)은 수십 년간 하위 호환성을 유지하면서 명령어가 계속 쌓여 있습니다.
칩 안에 이 복잡한 명령어들을 해석하는 디코더가 커야 하고, 그만큼 전력을 많이 씁니다. 여기에 인텔/AMD 라이선스 비용도 포함

하지만, AWS Graviton은 처음부터 서버 워크로드만을 위해서 설계되어서 하위 호환 부담이 없고,
"컨테이너가 주로 뭘 하나"를 분석해서 그 연산에 최적화된 명령어 세트로만 구현이 되었습니다. + 라이선스 비용 X

컨테이너 워크로드가 주로 하는 일은 네트워크 I/O 처리, HTTP 요청 파싱, JSON 직렬화/역직렬화, 메모리 복사 같은
단순 반복 연산입니다. x86의 복잡한 명령어가 필요한 순간이 거의 없습니다.
그래서 단순한 Graviton 코어가 더 낮은 전력으로 같은 일을 처리할 수 있는 것입니다.

 

ARM 노드에 x86 파드가 잘못 스케줄되면 실행 자체가 안되기 때문에 'taint로 강제 격리' 합니다.

 

📌 Taint / Toleration 원리

  • taint는 노드에 "이 조건을 수용 못하면 오지 마"라는 표시이며, 3가지 effect가 있습니다.
    • NoSchedule : 새 파드 배치 불가
    • PreferNoSchedule : 가급적 파드 배치 배제
    • NoExecute : 배치 불가 + 기존 파드 즉시 축출
  • nodeSelector + toleration 조합
    • toleration만 있으면 "갈 수 있다"는 허가일 뿐, ARM 노드로 반드시 가도록 강제하지는 않음.
    • nodeSelector: kubernetes.io/arch: arm64까지 함께 써야 ARM 전용으로 핀 가능.

 

ARM 바이너리와 x86 바이너리는 서로 실행이 안됩니다.

x86용으로 빌드된 컨테이너 이미지가 ARM 노드에 스케줄되면 파드가 그냥 crash 합니다.

 

그래서 위 원리로 taint를 걸어서 "이 노드는 ARM이니까, ARM을 명시적으로 인지하고 toleration을 선언한

파드만 와라"라고 일종의 실수 방지 장치를 거는 것 입니다.

 

1) ARM 노드그룹 추가.

ARCH: arm64의 'myeks-ng-2' 노드 그룹을 추가해보았습니다.

인스턴스 타입이 t4g.medium으로 t4g의 g가 Graviton을 의미합니다.
IMAGE ID도 ARM 전용 AMI가 올라가 있습니다.

 

2) 온디맨드와 다른점

노드그룹 1번 출력에는 taints 필드 자체가 없었습니다.

해당 노드 그룹에 effect가 NoExecute로 걸려있습니다.
위에서 말씀드렸다 싶이, ARM 바이너리와 x86은 서로 실행 불가하여 파드의 크래시 상태를 방지하기 위한 강제 격리 조치.

 

Taint 확인 방법은 총 세 가지가 있습니다.

# 방법 1: AWS API로 노드 그룹 레벨 확인
aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-ng-2 \
  | jq .nodegroup.taints

# 방법 2: 레이블로 ARM 노드만 필터링
kubectl get node -l kubernetes.io/arch=arm64

# 방법 3: 노드에 실제 적용된 taint 확인
kubectl describe node -l tier=secondary | grep -i taint

방법 1은 "AWS가 관리하는 노드 그룹 설정"을 보는거고,

방법 3은 "실제 쿠버네티스 노드 오브젝트에 반영된 상태"를 보는 것 입니다.

 


 

Taint/Toleration 동작 확인 -1

먼저 sample-app 디플로이먼트를 배포해봅시다.

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
  labels:
    app: sample-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      nodeSelector:
        kubernetes.io/arch: arm64
      containers:
      - name: sample-app
        image: nginx:alpine
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
EOF

 

배포를 했는데 어디에도 배치가 안되고 Pending 상태입니다. 왜일까요?

nodeSelector로 ARM 노드를 지정했지만 tolerations이 없습니다.

스케줄러 입장에서 본다면 아래와 같습니다.
 - arm64 노드로 가야해서 ng-2 노드를 찾았음.
 - ng-2 노드에 'cpuarch=arm64:NoExecute' taint가 있음.
 - 파드에 이 taint를 수용하는 toleration이 없음.

위 과정을 거쳐 결국 배치가 거부된 것입니다.

(ng-1 그룹의 노드들은 arm64가 아닌, amd64(x64 바이너리) 이므로 당연히 조건이 불만족합니다)

 

다시 정리를 해보자면 아래와 같습니다.

nodeSelector 단독 → Pending (목적지는 알지만 진입 불가)
tolerations 단독 → 진입은 가능한데 ARM 노드로 핀되지 않음 (다른 노드에 배치될 수 있음)
nodeSelector + tolerations 둘 다 → ARM 노드에 정확히 배치

 

그렇다면, 파드에 tolerations 설정으로 배치를 실행해봅시다.

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
  labels:
    app: sample-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      nodeSelector:
        kubernetes.io/arch: arm64
      tolerations:
      - key: "cpuarch"
        operator: "Equal"
        value: "arm64"
        effect: "NoExecute"
      containers:
      - name: sample-app
        image: nginx:alpine
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
EOF

 

아래와 같이 파드가 tolerations의 값을 참조하여 ng-2 그룹의 노드에 배치가 되었습니다.

 


 

Taint/Toleration 동작 확인 -2

이번에도 아래와 같이 샘플 애플리케이션을 배포해봅시다.

cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mario
  labels:
    app: mario
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mario
  template:
    metadata:
      labels:
        app: mario
    spec:
      nodeSelector:
        kubernetes.io/arch: arm64
      tolerations:
      - key: "cpuarch"
        operator: "Equal"
        value: "arm64"
        effect: "NoExecute"
      containers:
      - name: mario
        image: pengbai/docker-supermario
EOF

 

이번에는 Status가 Error로 나왔습니다. 왜그럴까요?

exec format error는 리눅스 커널이 바이너리를 실행하려 했을 때 ELF 헤더의 아키텍처가

현재 CPU와 맞지 않을 때 뱉는 에러 입니다.

즉, Kubernetes나 containerd가 아니라 OS 커널 레벨에서 실행을 거부했다는 의미입니다.

그렇다면, pengbai/docker-supermario 이미지 안의 catalina.sh가 호출하는 JVM 바이너리가
x86_64용으로 컴파일 되어 있어서 ARM64 커널이 "이 바이너리 형식을 모르겠다"고 거부한 것 입니다.

왜? nodeSelector와 tolerations는 arm64 바이너리의 파드를 받도록 설계되어 있기 때문에.

 


 

03. Spot 노드 그룹

 

Spot이 싼 이유

  • AWS 데이터센터의 남는 용량을 경매 방식으로 판매 → 최대 90% 할인
  • AWS에 용량이 필요해지면 2분 알림 후 회수 → 이게 리스크이자 활용 조건 입니다.

여러 인스턴스 타입을 지정하여 풀을 넓혀서 가용 용량 확보 확률을 높입니다.

특정 타입의 Spot 풀이 소진되어도 다른 타입에서 용량을 가져올 수 있도록 구성합니다.

(단일 타입만 지정하면 풀 소진 시 프로비저닝 실패함)

 

AWSServiceRoleForEC2Spot

aws iam create-service-linked-role --aws-service-name spot.amazonaws.com || true

Spot 요청을 AWS 내부 시스템이 처리하려면 이 역할이 계정에 반드시 존재해야 합니다.

(한 번만 만들면 계정 전체에서 공유됨.)

 

1) 신규 Spot 노드그룹 확인

현재 capacityType이 SPOT인 노드 그룹이 추가된 것을 확인할 수 있고,

인스턴스 타입 부분도 풀로 4가지 타입이 있는 것도 확인이 가능합니다.

 

2) 실제 과금 단가

eks-node-viewer --extra-labels eks-node-viewer/node-age

스팟 인스턴스가 온디맨드 대비 훨씬 저렴한 것을 확인할 수도 있습니다.

 

3) Spot 요청 확인

aws ec2 describe-spot-instance-requests \
  --query "SpotInstanceRequests[].{ID:SpotInstanceRequestId,State:State,Type:Type,InstanceId:InstanceId}" \
  --output table

Spot 인스턴스는 EC2를 직접 띄우는게 아니라 "Spot 용량 요청"을 먼저 넣고 AWS가 수락하면 인스턴스가 뜸.

이 명령이 그 요청 레코드를 보는 것이에요.

(EKS 관리형 노드 그룹은 one-time으로 요청합니다 왜? 인스턴스가 중단되면 ASG가 새 요청을 알아서 넣음)

 

스팟 요청을 AWS Console - EC2 카테고리에서 확인할 수도 있습니다.

 


 

Spot 인스턴스 타겟팅 샘플 파드 배포

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: busybox
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: busybox
    image: busybox
    command:
    - "/bin/sh"
    - "-c"
    - "while true; do date >> /home/pod-out.txt; cd /home; sync; sync; sleep 10; done"
  nodeSelector:
    eks.amazonaws.com/capacityType: SPOT
EOF

현재 nodeSelector로 SPOT 인스턴스를 타겟팅 하였고, 확인해봅시다.

실습 파드가 SPOT 인스턴스에 배치가 된 것을 확인할 수 있습니다.

 

여기서 잠깐, nodeSelector만 있고 tolerations은 없는데 배치가 됐네요?

Spot 노드에는 taint가 없습니다. 그래서 tolerations 없이 nodeSelector만으로도 배치가 됩니다.
ARM은 잘못 배치되면 실행이 안되니까 taint로 강제 격리가 필요했지만, Spot 아키텍처는 문제가 X

 

 


긴 글 읽어주셔서 감사합니다

 

다음 포스팅은 스케일링 관련 기술에 대해서 포스팅 할 예정이며,

EKS Best Practices Guids를 참고하여 정리해보도록 하겠습니다.