왜 격리가 필요한가
단일 클러스터를 여러 팀이나 서비스가 공유하면 한 워크로드가 노드의 CPU·메모리를 독점하는 "시끄러운 이웃(noisy neighbor)" 문제가 발생한다. 배치 잡 하나가 메모리를 잠식하면 같은 노드의 API 서버 Pod가 OOMKill되고, 무한히 생성되는 Pod가 스케줄러 큐를 막기도 한다. 네임스페이스는 이름 공간만 나눌 뿐 자원을 나누지 않으므로, 실제 격리는 ResourceQuota와 LimitRange, 그리고 네트워크·RBAC 정책을 함께 걸어야 성립한다.
ResourceQuota로 총량 제한
ResourceQuota는 네임스페이스 단위로 요청/제한 총합과 오브젝트 개수를 제한한다. 중요한 전제: 쿼터에 requests.cpu나 limits.memory가 걸려 있으면, 해당 네임스페이스의 모든 Pod는 그 리소스를 반드시 명시해야 한다. 명시하지 않은 Pod는 admission 단계에서 거부된다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
pods: "50"
count/deployments.apps: "20"
persistentvolumeclaims: "10"
requests.storage: 200Gi
LimitRange로 기본값과 상·하한
ResourceQuota가 "총량"이라면 LimitRange는 "개별 컨테이너"의 기본값과 범위를 담당한다. 개발자가 리소스를 빼먹어도 기본값이 주입되므로 쿼터 거부를 피할 수 있고, 극단적으로 큰 요청도 막는다. 두 오브젝트는 항상 함께 배포하는 것이 안전하다.
apiVersion: v1
kind: LimitRange
metadata:
name: team-a-limits
namespace: team-a
spec:
limits:
- type: Container
default: # limits 미지정 시 주입
cpu: 500m
memory: 512Mi
defaultRequest: # requests 미지정 시 주입
cpu: 100m
memory: 128Mi
max:
cpu: "4"
memory: 8Gi
min:
cpu: 50m
memory: 64Mi
두 오브젝트의 역할 비교
| 구분 | ResourceQuota | LimitRange |
|---|---|---|
| 적용 범위 | 네임스페이스 총합 | 개별 Pod/Container |
| 주 목적 | 총량 상한, 오브젝트 개수 제한 | 기본값 주입, 컨테이너별 상·하한 |
| 미지정 시 | Pod 생성 거부 | 기본값 자동 주입 |
| 대상 예 | cpu, memory, pvc, pods 수 | cpu, memory 범위 |
네트워크와 RBAC 격리 병행
리소스만 나눠서는 격리가 끝나지 않는다. 기본적으로 모든 네임스페이스는 서로 통신 가능하므로, 기본 거부(default-deny) NetworkPolicy를 깔고 필요한 흐름만 허용해야 한다. RBAC은 팀이 자기 네임스페이스만 다루도록 RoleBinding으로 묶는다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: team-a
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
# ingress/egress 미기재 = 전면 차단, 이후 허용 정책 추가
운영 중 확인과 흔한 함정
적용 후에는 사용량을 주기적으로 확인한다. 쿼터를 초과하면 새 Pod가 조용히 Pending이 아니라 아예 생성 거부되므로, CI/CD 파이프라인에서 배포 실패 원인을 여기서 찾는 경우가 많다.
# 현재 쿼터 사용량 확인
kubectl describe resourcequota team-a-quota -n team-a
# 거부 이벤트 추적
kubectl get events -n team-a --field-selector reason=FailedCreate
주의할 점 세 가지. 첫째, CNI가 NetworkPolicy를 지원하지 않으면(예: 일부 기본 설정) 정책이 무시되니 반드시 지원 여부를 확인한다. 둘째, 쿼터는 이미 실행 중인 Pod를 소급 제거하지 않으므로, 축소 시에는 기존 워크로드 정리 계획이 필요하다. 셋째, DaemonSet이나 시스템 Pod가 쿼터에 걸려 노드 초기화에 실패하지 않도록, 시스템 네임스페이스에는 쿼터를 걸지 않거나 PriorityClass로 예외를 둔다.
정리
실효성 있는 멀티테넌시는 ResourceQuota(총량) + LimitRange(개별 기본값·상한) + NetworkPolicy(통신) + RBAC(권한)의 네 축을 함께 채워야 완성된다. 리소스 명시를 강제하는 순간 개발자 워크플로가 바뀌므로, LimitRange 기본값을 먼저 깔아 마찰을 줄인 뒤 쿼터를 조이는 순서로 도입하는 것이 현실적이다.