Certified Kubernetes Administrator (CKA) #13 Scheduling 1: nodeSelector, nodeAffinity, podAffinity/antiAffinity

#12 ConfigMap과 Secret 깊이까지로 워크로드와 그 설정을 다뤘다면, 이번 글부터는 그 워크로드를 어느 노드에 둘지를 통제합니다. 기본값으로는 kube-scheduler가 알아서 적당한 노드를 고릅니다. 하지만 운영에서는 “GPU가 달린 노드에만”, “같은 가용 영역의 캐시 옆에”, “복제본은 서로 다른 노드에 흩어서” 같은 요구가 끊임없이 생깁니다. 이런 배치 의도를 매니페스트로 표현하는 것이 스케줄링입니다.

이번 #13에서는 nodeSelector, nodeAffinity, podAffinity/podAntiAffinity 네 가지를 다루겠습니다. 모두 “Pod가 어떤 노드를 좋아하는가"를 표현하는 도구입니다. 다음 #14에서 다룰 taints/tolerations가 반대로 “노드가 어떤 Pod를 밀어내는가"를 다루므로, 두 글을 묶어서 보면 스케줄링의 양면이 완성됩니다.

스케줄러는 무슨 일을 하는가 #

먼저 큰 그림을 잡겠습니다. Pod를 만들면 그 매니페스트에는 nodeName 필드가 비어 있습니다. kube-scheduler는 노드가 지정되지 않은 Pod를 발견하면 두 단계로 노드를 고릅니다.

  1. 필터링(filtering). 이 Pod를 받을 수 없는 노드를 걸러냅니다. 리소스가 모자란 노드, nodeSelector 조건에 맞지 않는 노드, 견딜 수 없는 taint가 붙은 노드를 제외합니다.
  2. 스코어링(scoring). 남은 후보 노드에 점수를 매겨 가장 높은 노드를 고릅니다. preferred 규칙의 가중치, 리소스 여유, 이미지 캐시 보유 여부 등이 점수에 반영됩니다.

스케줄러가 노드를 정하면 그 결과를 Pod의 nodeName에 써 넣습니다(이를 바인딩이라고 합니다). 그 노드의 kubelet이 자기 앞으로 바인딩된 Pod를 보고 컨테이너를 띄웁니다. 이번 글에서 다루는 도구는 모두 이 필터링과 스코어링 단계에 개입하는 방법입니다.

배치 의도를 표현하는 도구를 강도 순으로 정리하면 다음 같습니다.

도구기준강제력
nodeSelector노드 라벨강제(맞지 않으면 Pending)
nodeAffinity (required)노드 라벨강제(맞지 않으면 Pending)
nodeAffinity (preferred)노드 라벨선호(점수만, 안 맞아도 배치)
podAffinity / podAntiAffinity다른 Pod의 위치required와 preferred 둘 다 가능

nodeSelector: 라벨 단순 매칭 #

가장 단순한 도구입니다. Pod의 spec.nodeSelector에 라벨 키와 값을 적으면, 그 라벨을 모두 가진 노드에만 배치됩니다. 조건을 만족하는 노드가 하나도 없으면 Pod는 Pending에 머뭅니다.

먼저 노드에 라벨을 붙입니다.

# 노드에 라벨 부여
k label node node01 disktype=ssd

# 라벨 확인
k get nodes --show-labels
k get nodes -l disktype=ssd

그다음 Pod에서 그 라벨을 지정합니다.

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  nodeSelector:
    disktype: ssd
  containers:
    - name: web
      image: nginx:1.27

nodeSelector는 AND 매칭만 됩니다. 여러 키를 적으면 그 모두를 가진 노드여야 하고, “둘 중 하나"나 “이 값이 아닌” 같은 조건은 표현할 수 없습니다. 그런 표현력이 필요하면 nodeAffinity로 넘어갑니다.

nodeAffinity: required와 preferred #

nodeAffinity는 nodeSelector의 확장판입니다. 같은 “노드 라벨로 고른다"는 목적이지만, 연산자와 강제력을 세밀하게 표현할 수 있습니다. 두 종류가 있습니다.

  • requiredDuringSchedulingIgnoredDuringExecution. 강제 규칙입니다. 조건을 만족하는 노드가 없으면 Pod는 Pending에 머뭅니다. nodeSelector와 같은 강제력이되 연산자를 쓸 수 있습니다.
  • preferredDuringSchedulingIgnoredDuringExecution. 선호 규칙입니다. 조건을 만족하는 노드에 가산점을 주되, 그런 노드가 없어도 다른 노드에 그냥 배치합니다.

이름이 긴 이유는 두 부분으로 읽으면 됩니다. 앞의 DuringScheduling은 스케줄링 시점에 이 규칙을 본다는 뜻이고, 뒤의 IgnoredDuringExecution은 이미 떠 있는 Pod는 나중에 노드 라벨이 바뀌어도 쫓아내지 않는다는 뜻입니다.

연산자 #

nodeAffinity의 matchExpressions에서 쓰는 연산자입니다.

연산자의미
In값이 목록 안에 있음
NotIn값이 목록 안에 없음
Exists키가 존재(값 무관)
DoesNotExist키가 존재하지 않음
Gt / Lt값이 큼 / 작음(정수)

NotInDoesNotExist가 nodeSelector에는 없던 부정 조건입니다. 이 덕분에 “이 라벨이 없는 노드에만” 같은 배치가 가능합니다.

nodeAffinity 예제 #

다음은 disktypessd 또는 nvme인 노드에 반드시 배치하고, 그중 zone=ap-northeast-1a인 노드를 선호하는 예제입니다.

apiVersion: v1
kind: Pod
metadata:
  name: db
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: disktype
                operator: In
                values:
                  - ssd
                  - nvme
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 50
          preference:
            matchExpressions:
              - key: zone
                operator: In
                values:
                  - ap-northeast-1a
  containers:
    - name: db
      image: postgres:16

구조에서 헷갈리기 쉬운 두 곳을 짚겠습니다.

  • required는 nodeSelectorTerms 리스트입니다. 리스트 항목들끼리는 OR로 묶입니다. 한 항목 안의 여러 matchExpressions는 AND로 묶입니다.
  • preferred는 가중치를 가진 리스트입니다. 각 항목에 weight(1〜100)가 붙고, 만족하는 노드는 그 가중치만큼 점수를 더 받습니다. 여러 preferred 규칙을 만족하면 가중치가 합산됩니다.

podAffinity와 podAntiAffinity: 다른 Pod를 기준으로 #

nodeAffinity가 노드 라벨을 기준으로 한다면, podAffinity와 podAntiAffinity는 이미 떠 있는 다른 Pod의 위치를 기준으로 합니다.

  • podAffinity. 특정 라벨을 가진 Pod와 가까이 둡니다. 예를 들어 앱 Pod를 캐시 Pod와 같은 노드에 붙여 네트워크 지연을 줄입니다.
  • podAntiAffinity. 특정 라벨을 가진 Pod와 떨어뜨려 둡니다. 예를 들어 같은 Deployment의 복제본을 서로 다른 노드에 흩어 한 노드 장애가 전체를 끊지 않게 합니다.

topologyKey가 “같은 곳"을 정의한다 #

podAffinity의 핵심은 topologyKey입니다. “같은 노드"인지 “같은 가용 영역"인지 같은 “가까움의 단위"를 노드 라벨 키로 정합니다.

topologyKey“같은 곳"의 의미
kubernetes.io/hostname같은 노드
topology.kubernetes.io/zone같은 가용 영역
topology.kubernetes.io/region같은 리전

동작은 이렇게 읽습니다. podAffinity는 “라벨이 맞는 Pod가 떠 있는 노드와 같은 topologyKey 값을 가진 노드에 나를 배치하라"는 뜻이고, podAntiAffinity는 그 반대로 “그런 노드를 피하라“는 뜻입니다. topologyKeykubernetes.io/hostname으로 두면 “같은 노드/다른 노드” 단위가 되고, zone으로 두면 “같은 영역/다른 영역” 단위가 됩니다.

podAntiAffinity 예제: 복제본을 노드마다 하나씩 #

다음은 app=web 라벨을 가진 Pod들을 서로 다른 노드에 두는 예제입니다. Deployment의 Pod 템플릿에 넣으면 복제본이 한 노드에 몰리지 않습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: web
              topologyKey: kubernetes.io/hostname
      containers:
        - name: web
          image: nginx:1.27

labelSelector로 “어떤 Pod를 기준으로 할지"를 고르고, topologyKey로 “어느 단위로 떨어뜨릴지"를 정합니다. 여기서는 같은 app=web Pod끼리 같은 노드를 피하므로, 노드가 3대 미만이면 남는 복제본은 Pending에 머뭅니다. required 대신 preferred를 쓰면 노드가 부족해도 일단 같은 노드에 겹쳐 띄웁니다.

preferred로 바꾸는 형태도 같은 구조에 가중치만 더해집니다.

      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: web
                topologyKey: kubernetes.io/hostname

preferred에서는 labelSelectortopologyKeypodAffinityTerm 아래로 한 단계 들어가고, 그 위에 weight가 붙는다는 점만 기억하면 됩니다.

수동 배치: nodeName으로 스케줄러 우회 #

지금까지의 도구는 모두 스케줄러에게 힌트를 줄 뿐, 최종 결정은 스케줄러가 합니다. 반면 Pod에 spec.nodeName을 직접 적으면 스케줄러를 완전히 건너뛰고 그 노드에 곧장 바인딩됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: pinned
spec:
  nodeName: node01
  containers:
    - name: app
      image: nginx:1.27

이 방식은 필터링도 스코어링도 거치지 않으므로, 그 노드에 리소스가 없거나 taint가 있어도 강제로 바인딩됩니다. 결과적으로 노드가 받지 못하면 Pod가 영영 안 뜨는 위험이 있어 실무에서는 거의 쓰지 않습니다. 다만 스케줄러 자체가 죽은 상황에서 control plane 컴포넌트를 띄워야 할 때 같은 예외에서는 유용합니다. static Pod가 바로 이 방식으로, kubelet이 매니페스트 디렉터리의 Pod를 스케줄러 없이 직접 띄웁니다.

디버깅: 왜 Pending에 머무는가 #

affinity 규칙은 너무 빡빡하게 걸면 Pod가 Pending에 갇히기 쉽습니다. 원인은 describe로 바로 보입니다.

# Pending 원인 확인
k describe pod web

# 노드 라벨이 조건과 맞는지 재확인
k get nodes --show-labels

describe 출력의 Events에 스케줄러 메시지가 찍힙니다. nodeAffinity가 안 맞으면 didn't match Pod's node affinity/selector, podAntiAffinity로 노드가 모자라면 didn't match pod anti-affinity rules 같은 문구가 나옵니다. 이 문구만 읽어도 어느 규칙이 발목을 잡는지 알 수 있습니다.

시험 포인트 #

CKA 시험에서 스케줄링은 Workloads and Scheduling도메인(15%)의 한 축입니다. 이 글 범위에서 자주 나오는 작업을 정리하겠습니다.

  • 노드 라벨 부여. k label node <노드> <키>=<값>을 손에 익히기. 문제에서 요구하는 라벨을 먼저 붙여야 nodeSelector/nodeAffinity가 동작합니다.
  • nodeSelector vs nodeAffinity 구분. “이 라벨이 있는 노드"는 nodeSelector로 충분하고, “둘 중 하나” 또는 “이 라벨이 없는 노드"는 nodeAffinity의 In/NotIn/Exists가 필요합니다.
  • required와 preferred의 강제력 차이. 문제가 “반드시"라고 하면 required, “가능하면"이라고 하면 preferred입니다. preferred에는 weight가 필수입니다.
  • podAntiAffinity로 복제본 흩기. topologyKey: kubernetes.io/hostnamelabelSelector를 자기 Pod 라벨로 거는 패턴을 외워 두기. 복제본 수가 노드 수보다 많고 required면 일부가 Pending에 머문다는 점을 기억합니다.
  • YAML 구조 함정. required nodeAffinity는 nodeSelectorTerms, preferred는 weight+preference, preferred podAffinity는 weight+podAffinityTerm입니다. 이 세 형태의 들여쓰기 차이가 가장 흔한 실수입니다.
  • Pending 디버깅. k describe pod의 Events 한 줄로 어느 규칙 때문인지 즉시 판단합니다.

시험에서는 affinity YAML을 손으로 쓰는 시간이 아깝습니다. kubectl create deployment ... $do로 골격을 만든 뒤 affinity 블록만 끼워 넣고, 공식 문서의 Assigning Pods to Nodes 예제를 복사해 값만 바꾸는 것이 가장 빠릅니다.

정리 #

이번 글에서 잡은 것:

  • 스케줄러는 필터링과 스코어링 두 단계로 노드를 고른 뒤 Pod의 nodeName에 결과를 바인딩합니다.
  • nodeSelector. 노드 라벨 AND 매칭. 가장 단순하고 강제. 표현력이 부족하면 nodeAffinity로 넘어갑니다.
  • nodeAffinity. required(강제)와 preferred(선호+가중치). In/NotIn/Exists 등 연산자로 부정 조건까지 표현합니다.
  • podAffinity/podAntiAffinity. 다른 Pod의 위치를 기준으로 topologyKey 단위에서 같은 곳에 붙이거나 다른 곳으로 흩습니다.
  • nodeName. 스케줄러를 우회하는 수동 배치. static Pod의 동작 방식이지만 일반 워크로드에는 권하지 않습니다.
  • 디버깅. Pending이면 k describe pod의 Events로 어느 규칙이 막는지 확인합니다.

다음: Scheduling 2 #

이번 글의 도구는 모두 “Pod가 어떤 노드를 좋아하는가"를 표현했습니다. 다음 #14 Scheduling 2: Taints/tolerations, Priority/PriorityClass, preemption에서는 반대 방향을 다루겠습니다. taints/tolerations로 노드가 어떤 Pod를 밀어내는지, PriorityClass로 자원이 모자랄 때 어떤 Pod가 먼저 자리를 차지하는지, preemption으로 낮은 우선순위 Pod가 어떻게 쫓겨나는지를 직접 매니페스트로 다뤄 스케줄링의 나머지 절반을 채우겠습니다.

X