목차
14 장

RBAC / NetworkPolicy / ResourceQuota

한 클러스터에 여러 팀 · 환경이 같이 사는 멀티테넌트 운영의 격리를 만드는 세 정책 객체를 다룹니다. RBAC의 Role · ClusterRole · ServiceAccount · RoleBinding 모델, NetworkPolicy의 default-deny 패턴과 CNI 의존성, ResourceQuota와 LimitRange의 짝 관계까지 한 사이클로 정리하며 2부를 마무리합니다.

2부의 마지막 챕터입니다. 8장 StatefulSet / DaemonSet / Job / CronJob부터 13장 오토스케일링까지 워크로드를 운영하는 모델을 한 층씩 쌓아 왔습니다. 컨트롤러 4종, 영속 데이터, 외부 진입점, 자원 요청 · 상한, 헬스 체크, 오토스케일링까지 — Pod 한 개를 안정적으로 띄우고 부하에 맞춰 늘리고 줄이는 한 사이클이 손에 들어왔습니다. 이번 챕터에서는 그 위에 한 층 더 얹는 주제를 다룹니다 — 한 클러스터에 여러 팀 · 환경이 같이 사는 상황의 보안과 자원 통제입니다. 키워드는 셋입니다. RBAC (누가 무엇을 할 수 있는가), NetworkPolicy (어떤 트래픽이 통하는가), ResourceQuota (얼마나 만들 수 있는가). 세 객체 모두 네임스페이스 단위 정책이라는 공통점이 있고, 7장 Namespace와 라벨에서 “Namespace 자체는 보안 경계가 아니다"라고 짚어 둔 그 빈 부분을 이 세 객체가 채웁니다.

이번 챕터의 끝에서는 한 클러스터 위에 여러 환경 · 팀이 안전하게 같이 살 수 있는 격리의 세 차원이 손에 들어옵니다. 2부 회고와 다음 부 (3부 깊이) 안내도 함께 담습니다.

세 정책의 공통 좌표 — 네임스페이스 단위 #

7장에서 Namespace를 다룰 때 한 줄을 남겨 두었습니다 — Namespace 자체는 보안 경계가 아닙니다. 객체 이름을 갈라 주는 논리 칸일 뿐이고, 진짜 격리는 그 위에 얹히는 정책 객체들이 만든다는 이야기였습니다. 그 위에 얹히는 정책의 본체가 이번 챕터의 주제 셋입니다.

차원객체설명
권한Role / ClusterRole / RoleBinding / ClusterRoleBinding누가 어떤 객체에 어떤 동사를 쓸 수 있는가
트래픽NetworkPolicy어느 Pod가 어느 Pod와 통신할 수 있는가
자원ResourceQuota / LimitRange한 네임스페이스가 얼마나 만들 수 있는가

세 객체 모두 네임스페이스 단위로 적용되거나 (NetworkPolicy, ResourceQuota, LimitRange, Role / RoleBinding), 네임스페이스에 결합해서 적용됩니다 (ClusterRole + RoleBinding 조합). 한 클러스터 위에 dev / staging / prod, 또는 팀 A / 팀 B가 같이 사는 멀티테넌트 운영의 격리는 이 세 차원이 합쳐졌을 때 비로소 완성됩니다.

이번 챕터의 흐름은 단순합니다. 권한부터 — RBAC을 가장 자세히 다루고, 그 다음 트래픽 (NetworkPolicy), 그 다음 자원 (ResourceQuota / LimitRange) 순서로 갑니다.

RBAC — 누가 무엇을 할 수 있는가 #

RBAC (Role-Based Access Control)은 K8s API 호출 단위로 권한을 표현하는 모델입니다. kubectl get pods, kubectl create deployment, kubectl delete secret 같은 모든 동작은 결국 K8s API 서버에 대한 HTTP 요청입니다. RBAC은 그 요청 한 건 한 건에 대해 “이 주체가 이 동사를 이 자원에 쓸 수 있는가"를 검사합니다.

모델은 네 객체로 표현됩니다. 권한 묶음 (Role / ClusterRole)과, 그 권한과 주체를 잇는 객체 (RoleBinding / ClusterRoleBinding)입니다.

객체무엇인가스코프
Role권한 묶음 (verbs + resources)네임스페이스
ClusterRole권한 묶음 (verbs + resources)클러스터 (모든 네임스페이스 공유)
RoleBindingRole 또는 ClusterRole을 주체에게 부여네임스페이스
ClusterRoleBindingClusterRole을 주체에게 부여클러스터 전역

여기서 한 가지 미묘한 부분이 있습니다. RoleBinding은 ClusterRole을 참조할 수도 있습니다. ClusterRole은 권한 묶음이고 RoleBinding은 그 묶음을 어떤 네임스페이스에서 누구에게 줄지 결정하는 객체이기 때문에, “표준 ClusterRole (예: view, edit) 하나를 만들어 두고 네임스페이스마다 RoleBinding으로 다른 사람에게 부여” 하는 패턴이 운영에서 가장 흔합니다.

본 챕터는 RBAC의 매니페스트와 운영 패턴까지를 다룹니다. Aggregated ClusterRole, impersonation, EKS의 IRSA · GKE의 Workload Identity 같은 외부 IAM 매핑, 토큰 라이프사이클 같은 깊이는 16장 RBAC / ServiceAccount 깊이에서 본격적으로 다룹니다.

Subject — 권한을 받는 쪽 #

권한이 부여되는 주체 (Subject)는 셋입니다.

  • User — 사람 사용자입니다. K8s 자체에는 사용자 데이터베이스가 없고, 외부 인증 (OIDC, x509 클라이언트 인증서, 클라우드 IAM 매핑 등)이 사용자 이름을 알려 줍니다.
  • Group — 사용자의 묶음입니다. User와 마찬가지로 외부 인증이 그룹 정보를 K8s에 전달합니다.
  • ServiceAccount — Pod가 K8s API에 접근할 때 쓰는 ID입니다. 사람이 아니라 워크로드의 정체성입니다.

이 셋 중 K8s 매니페스트로 직접 만들고 관리하는 것은 ServiceAccount가 거의 유일합니다. User와 Group은 외부 인증의 산물이라 K8s 안에 객체로 존재하지 않습니다. RoleBinding의 subjects 필드에 적힐 뿐입니다.

ServiceAccount — Pod의 ID #

모든 Pod는 어떤 ServiceAccount의 ID를 들고 클러스터에 살고 있습니다. 매니페스트에 spec.serviceAccountName을 적지 않으면 그 네임스페이스의 default ServiceAccount가 자동으로 묶입니다. Pod 안에서 kubectl 같은 도구로 K8s API에 접근하면, 그 호출은 그 ServiceAccount의 권한으로 수행됩니다.

네임스페이스의 ServiceAccount 확인
kubectl get serviceaccounts -n default
출력 예시
NAME      SECRETS   AGE
default   0         3d

기본 default ServiceAccount는 어떤 권한도 부여되어 있지 않습니다. RBAC을 적용한 운영 클러스터에서 Pod 안에서 kubectl get pods를 시도하면 다음 메시지가 나오는 것이 정상입니다.

권한 없음 메시지
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default"

이 메시지를 보고 RoleBinding으로 권한을 묶어 주면 같은 명령이 통합니다. 다음 예시로 그 흐름을 따라갑니다.

RBAC 매니페스트 한 묶음 #

가장 단순한 시나리오를 매니페스트 한 장으로 정리해 봅니다. dev 네임스페이스에 pod-reader라는 ServiceAccount를 만들고, 그 SA에 “Pod를 읽을 수 있는 권한"을 부여합니다. 그리고 그 SA를 사용하는 Pod 안에서 kubectl get pods가 통하는지 확인하는 흐름입니다.

rbac-pod-reader.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: pod-reader
  namespace: dev
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: dev
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-reader
  namespace: dev
subjects:
  - kind: ServiceAccount
    name: pod-reader
    namespace: dev
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

세 객체의 책임을 한 줄씩 짚어 둡니다.

  • ServiceAccount pod-readerdev 네임스페이스의 새 ID입니다. 아직 어떤 권한도 없습니다.
  • Role pod-readerpods 자원에 대해 get, list, watch 동사를 허용하는 권한 묶음입니다. apiGroups: [""]은 코어 API 그룹 (Pod, Service, ConfigMap 등)을 가리킵니다.
  • RoleBinding pod-reader — Role과 ServiceAccount를 잇는 객체입니다. subjects가 받는 쪽, roleRef가 주는 쪽입니다.

이 세 객체를 한 번에 적용한 뒤 그 ServiceAccount를 쓰는 Pod를 띄워 봅니다.

reader-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: reader
  namespace: dev
spec:
  serviceAccountName: pod-reader
  containers:
    - name: kubectl
      image: bitnami/kubectl:1.32
      command: ["sleep", "3600"]
Pod 안에서 kubectl 실행
kubectl apply -f rbac-pod-reader.yaml
kubectl apply -f reader-pod.yaml
kubectl exec -n dev reader -- kubectl get pods -n dev
출력 예시
NAME     READY   STATUS    RESTARTS   AGE
reader   1/1     Running   0          30s

같은 Pod 안에서 권한이 없는 동작을 시도하면 막히는 것도 확인할 수 있습니다.

허용되지 않은 동작
kubectl exec -n dev reader -- kubectl create deployment nginx --image=nginx -n dev
출력 예시 — 거부
error: failed to create deployment: deployments.apps is forbidden: User "system:serviceaccount:dev:pod-reader" cannot create resource "deployments" in API group "apps" in the namespace "dev"

pod-reader Role은 Pod의 get / list / watch만 줬고 Deployment의 create는 주지 않았기 때문에 정확히 그 부분에서 거부됩니다.

verbs와 resources의 핵심 #

Role / ClusterRole의 rules에서 자주 쓰이는 verbs를 표로 정리하면 다음과 같습니다.

verb의미
get단일 객체 조회
list객체 목록 조회
watch변경 이벤트 구독
create객체 생성
update객체 업데이트 (전체)
patch객체 부분 업데이트
delete객체 삭제
deletecollection매칭되는 객체 일괄 삭제

읽기만 허용하려면 get, list, watch 셋을 같이 줍니다. kubectl get 같은 명령이 내부적으로 list를 쓰고 watch는 informer 캐시의 갱신에 쓰이기 때문입니다. 쓰기까지 허용하려면 create, update, patch, delete를 추가합니다.

resourcespods, services, configmaps처럼 복수형 이름을 적습니다. apiGroups는 그 자원이 속한 API 그룹입니다. 코어 그룹 (Pod / Service / ConfigMap / Secret 등)은 [""] (빈 문자열), Deployment / StatefulSet / DaemonSet은 ["apps"], Job / CronJob은 ["batch"], Ingress는 ["networking.k8s.io"]입니다. 자원이 어느 그룹에 속하는지는 kubectl api-resources로 한 번에 확인할 수 있습니다.

자원과 그룹 확인
kubectl api-resources

kubectl auth can-i — 권한 검증 #

RBAC을 만져 두면 다음 질문이 곧 따라옵니다 — “그래서 이 사용자가 진짜 이 동작을 할 수 있는가?” 매니페스트를 한참 들여다봐도 결과가 한눈에 들어오지 않을 때가 많습니다. K8s가 이 질문에 직접 답해 주는 명령이 kubectl auth can-i입니다.

현재 본인의 권한 확인
kubectl auth can-i create pods -n dev
kubectl auth can-i delete deployments -n prod

yes / no 둘 중 하나가 출력됩니다. 다른 사용자나 ServiceAccount의 입장에서 묻고 싶다면 --as 옵션으로 impersonation을 씁니다.

다른 사용자 · SA로 가정해 묻기
kubectl auth can-i create pods --as=alice -n dev
kubectl auth can-i list secrets --as=system:serviceaccount:dev:pod-reader -n dev

--as 옵션 자체에도 권한이 필요합니다 (impersonation 권한). 운영 클러스터의 admin이 새로 만든 RBAC 정책이 의도대로 동작하는지 확인할 때 가장 자주 쓰이는 패턴입니다. 모든 권한을 한 번에 보고 싶다면 --list를 붙입니다.

한 사용자의 모든 권한 나열
kubectl auth can-i --list --as=alice -n dev

진단의 완성된 흐름과 일반적인 권한 거부 트러블슈팅 트리는 27장 kubectl 디버깅 패턴에서 정리합니다.

흔한 함정 — 너무 넓은 ClusterRole #

RBAC을 처음 적용할 때 가장 자주 저지르는 실수가 귀찮음에 못 이겨 ClusterRole cluster-admin을 ServiceAccount에 묶는 패턴입니다.

안티패턴 — 절대 운영에 두지 말 것
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: app-admin
subjects:
  - kind: ServiceAccount
    name: app
    namespace: dev
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

cluster-admin은 K8s API 전체에 대한 무제한 권한입니다. 이 ClusterRoleBinding이 적용된 ServiceAccount의 토큰이 Pod 한 개라도 외부로 새면, 그 토큰으로 클러스터의 모든 네임스페이스의 모든 Secret을 읽고 모든 객체를 지울 수 있습니다. 컨테이너 이미지 안의 라이브러리에 취약점 하나만 있어도 그 한 점이 클러스터 전체의 사고로 번집니다.

운영의 표준 원칙은 **최소 권한 (least privilege)**입니다. 워크로드가 진짜 필요한 verbs와 resources만 정확히 추려서 Role로 묶고, 그 Role을 RoleBinding으로 부여합니다. K8s가 미리 만들어 두는 표준 ClusterRole 셋이 있는데, 이것을 RoleBinding으로 네임스페이스에 한정해 쓰는 패턴이 일반적인 시작점입니다.

ClusterRole설명
view네임스페이스의 객체를 읽기. Secret은 제외
editview + 대부분 객체의 쓰기. Role / RoleBinding 등 RBAC 객체는 제외
adminedit + 그 네임스페이스 안의 RBAC 객체 관리
cluster-admin클러스터 전체에 무제한

view / edit / admin을 RoleBinding으로 특정 네임스페이스에 한정하면 사람 · 팀 · 서비스에 적당한 권한 묶음을 빠르게 부여할 수 있고, cluster-admin클러스터 운영자 한 줌 한테만 ClusterRoleBinding으로 부여하는 모양이 표준입니다. 매니페스트 자체를 admission 단계에서 막는 정책 (예: “cluster-admin 묶음 금지”)은 17장 Admission Controller의 OPA Gatekeeper · Kyverno가 다루는 영역입니다.

automountServiceAccountToken #

ServiceAccount의 토큰은 기본적으로 Pod 안의 /var/run/secrets/kubernetes.io/serviceaccount/token에 자동으로 마운트됩니다. K8s API를 호출할 일이 없는 Pod에는 그 토큰조차 마운트하지 않는 편이 안전합니다. 매니페스트의 automountServiceAccountToken: false로 끌 수 있습니다.

API 호출 안 하는 Pod — 토큰 안 마운트
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  automountServiceAccountToken: false
  containers:
    - name: nginx
      image: nginx:1.27

이 한 줄을 넣어 두면 그 Pod가 만에 하나 침해당해도 K8s API에 직접 접근할 토큰이 없습니다. 보안 가이드의 단골 권장 사항입니다. 토큰을 외부 비밀 저장소 · IRSA와 결합해 “비밀번호 0"으로 운영하는 본격적인 패턴은 29장 시크릿 운영에서 다룹니다.

NetworkPolicy — Pod 사이 트래픽 통제 #

RBAC이 K8s API에 대한 권한을 다룬다면, NetworkPolicy는 Pod 사이의 IP 트래픽을 다룹니다. 같은 클러스터의 두 Pod가 서로의 IP를 알고 통신하려고 할 때, 그 트래픽이 허용되는지 검사하는 정책입니다.

기본은 모두 통과 #

K8s 네트워크 모델의 기본은 단순합니다 — NetworkPolicy가 없으면 모든 Pod가 서로 통신할 수 있습니다. 같은 네임스페이스든 다른 네임스페이스든, Pod의 IP만 알면 자유롭게 패킷을 보낼 수 있습니다. 멀티테넌트 클러스터에서 이 기본값이 그대로 두어지면, 한 네임스페이스의 Pod가 다른 네임스페이스의 DB Pod에 자유롭게 접근하는 모양이 됩니다.

NetworkPolicy의 동작 규칙은 다음 두 줄로 정리됩니다.

  • NetworkPolicy가 한 개라도 매칭되는 Pod의 트래픽은, 그 정책의 policyTypes에 적힌 방향에 대해 default-deny가 적용됩니다.
  • 그리고 그 정책의 ingress / egress 규칙에 명시적으로 허용된 트래픽만 통과합니다.

매칭되는 정책이 한 장도 없는 Pod는 위 규칙이 발동되지 않으므로 모든 트래픽이 통합니다. 매칭되는 정책이 한 장이라도 있으면, 그 정책의 방향에 대해서는 화이트리스트 모델로 전환됩니다.

CNI가 NetworkPolicy를 지원해야 한다 #

NetworkPolicy는 K8s 매니페스트 차원에서는 표준이지만, 실제 트래픽을 막는 일은 CNI (Container Network Interface) 플러그인이 합니다. 그래서 CNI가 NetworkPolicy를 지원하지 않으면 매니페스트를 적용해도 아무 일도 일어나지 않습니다. 트래픽은 그대로 통합니다.

CNINetworkPolicy 지원
Calico지원
Cilium지원 (eBPF 기반)
Antrea지원
flannel미지원
EKS의 amazon-vpc-cni별도 옵션 활성화 필요 (Calico를 함께 깔거나 vpc-cni의 NetworkPolicy 옵션을 켜야 함)

운영 클러스터에서 NetworkPolicy를 쓸 계획이라면 클러스터 만들 때부터 CNI를 골라 두어야 합니다. 기본 EKS 클러스터에서 NetworkPolicy 매니페스트를 적용하고 “왜 트래픽이 안 막히지?“가 흔한 함정입니다 — CNI가 지원하지 않으면 매니페스트는 그저 etcd에 들어 있는 객체일 뿐입니다. CNI 데이터 플레인의 본격적인 모델 (iptables 기반 vs eBPF 기반, Calico vs Cilium)은 15장 CNI 깊이에서 다룹니다.

default-deny → allow 패턴 #

운영의 표준 패턴은 네임스페이스에 default-deny 정책을 한 장 깔고, 필요한 통신만 명시적으로 허용 하는 것입니다.

default-deny-all.yaml — 네임스페이스 안의 모든 Pod에 적용
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

podSelector: {}은 빈 셀렉터로 “이 네임스페이스의 모든 Pod"를 가리킵니다. policyTypesIngressEgress가 모두 적혀 있고 ingress / egress 규칙은 한 줄도 없으므로, 이 네임스페이스의 모든 Pod는 들어오는 트래픽도 나가는 트래픽도 모두 차단됩니다.

이 상태에서 그대로 두면 Pod가 DNS 조회조차 못 하게 되므로, 최소한 DNS 트래픽은 풀어 줘야 합니다.

allow-dns.yaml — kube-system의 CoreDNS로 나가는 53/UDP, 53/TCP 허용
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: prod
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

그 다음에 워크로드별로 필요한 통신을 한 장씩 추가합니다 — frontend가 backend로 가는 80/TCP, backend가 DB로 가는 5432/TCP 같은 식입니다.

NetworkPolicy 매니페스트 — frontend → backend #

가장 흔한 한 컷을 한 장으로 적어 두면 다음과 같습니다. backend Pod가 frontend Pod로부터 들어오는 8080/TCP 트래픽만 받도록 하는 ingress 정책입니다.

backend-allow-frontend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-allow-frontend
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

읽는 법은 다음과 같습니다.

  • spec.podSelector — 이 정책이 적용되는 Pod입니다. app=backend 라벨이 붙은 Pod에만 적용됩니다.
  • policyTypes: [Ingress] — 들어오는 방향에 대한 정책임을 명시합니다. egress는 이 정책으로는 제어하지 않습니다.
  • ingress[0].from — 어디서 오는 트래픽을 허용할지. app=frontend 라벨이 붙은 Pod에서 옵니다.
  • ingress[0].ports — 어느 포트에 대해. 8080 / TCP만 허용합니다.

from의 셀렉터는 셋 중에서 고를 수 있습니다 — 이 셋은 따로 또는 같이 쓸 수 있습니다.

셀렉터의미
podSelector같은 네임스페이스의 Pod 라벨 매칭
namespaceSelector다른 네임스페이스의 모든 Pod (또는 그 네임스페이스 + podSelector 조합)
ipBlockCIDR 표현으로 IP 범위 (외부 IP 또는 노드 IP)

namespaceSelectorpodSelector를 같은 from 항목에 같이 적으면 “특정 네임스페이스의 특정 라벨 Pod"가 됩니다. 두 개를 따로 적으면 “그 네임스페이스의 모든 Pod 또는 같은 네임스페이스의 그 라벨 Pod” 의미가 되어 버리므로, 의도한 모양이 무엇인지 정확히 보고 적어야 합니다.

namespaceSelector + podSelector 한 항목으로 — AND
ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            env: prod
        podSelector:
          matchLabels:
            app: frontend
따로 적은 경우 — OR
ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            env: prod
      - podSelector:
          matchLabels:
            app: frontend

이 두 매니페스트의 의미가 다르다는 점이 NetworkPolicy의 단골 함정입니다. 위 매니페스트는 “env=prod 네임스페이스의 frontend Pod만"이고, 아래 매니페스트는 “env=prod 네임스페이스의 모든 Pod 또는 같은 네임스페이스의 frontend Pod"입니다.

egress 규칙 — 나가는 방향 #

ingress의 거울이 egress입니다. backend Pod가 DB로 5432 포트로 나가는 통신만 허용하는 정책은 다음 모양입니다.

backend-egress-to-db.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-egress-to-db
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

이 정책 한 장이 적용되면 backend Pod는 postgres Pod의 5432 / TCP 외에는 어디로도 나갈 수 없습니다. 위에서 만든 default-deny + allow-dns 조합과 합치면, backend Pod의 나가는 통신은 “DNS + DB” 둘로 한정됩니다. 외부 인터넷으로 가는 통신을 워크로드마다 정확히 화이트리스트로 좁혀 두는 것이 운영 보안의 표준 모양입니다.

NetworkPolicy의 한계 #

NetworkPolicy는 L3 / L4 정책입니다. IP, 포트, 프로토콜만 봅니다. HTTP 메소드나 경로 같은 L7 정책은 NetworkPolicy의 범위를 벗어납니다 — Cilium의 L7 정책이나 Istio / Linkerd 같은 서비스 메시가 그 차원을 다룹니다 (후속 K8s 심화 책의 영역입니다). 그리고 NetworkPolicy는 클러스터 안의 트래픽 정책이고, 외부 인바운드는 10장 Ingress의 Ingress · LoadBalancer 차원에서, 외부 아웃바운드는 NAT 게이트웨이의 보안 그룹에서 별도로 통제됩니다. 한 클러스터의 보안 모양은 여러 층의 정책이 같이 만들어 갑니다.

ResourceQuota — 네임스페이스 자원 합계 상한 #

11장 resources.requests / limits에서 컨테이너 단위 자원 모델 — requestslimits — 을 다뤘습니다. 컨테이너 한 개의 보장과 상한을 정하는 객체였습니다. 그 위에 한 층 더 얹는 것이 네임스페이스 단위 합계 상한 인 ResourceQuota입니다.

운영 시나리오는 분명합니다. 한 클러스터에 dev / staging / prod, 또는 여러 팀이 같이 살 때, dev 네임스페이스가 모든 노드의 CPU를 잡아먹어 prod 워크로드의 자원이 부족해지는 사고를 막아야 합니다. ResourceQuota가 그 빈 부분을 채웁니다.

ResourceQuota 매니페스트 #

dev-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    pods: "50"
    services: "20"
    configmaps: "30"
    secrets: "30"
    persistentvolumeclaims: "10"
    requests.storage: 100Gi

이 ResourceQuota가 dev 네임스페이스에 적용되면 다음 합계가 한도를 넘지 못합니다.

  • 그 네임스페이스 안의 모든 Pod의 requests.cpu 합계 ≤ 4 코어
  • requests.memory 합계 ≤ 8Gi
  • limits.cpu 합계 ≤ 8 코어
  • limits.memory 합계 ≤ 16Gi
  • 객체 개수 — Pod 50개, Service 20개, ConfigMap · Secret 각 30개, PVC 10개
  • PVC의 requests.storage 합계 ≤ 100Gi

상한을 넘기는 객체 생성은 K8s API 서버에서 거부됩니다. dev 네임스페이스에 이미 4 코어가 할당된 상태에서 추가 Pod의 requests.cpu: 1을 만들려고 하면, 그 Pod 생성 요청은 admission 단계에서 거부되고 다음 메시지가 나옵니다.

ResourceQuota 초과 시 거부
Error from server (Forbidden): error when creating "...": pods "..." is forbidden: exceeded quota: dev-quota, requested: requests.cpu=1, used: requests.cpu=4, limited: requests.cpu=4

ResourceQuota 동작 확인 #

네임스페이스 ResourceQuota 사용량
kubectl get resourcequota -n dev
kubectl describe resourcequota dev-quota -n dev
describe 출력 예시
Name:            dev-quota
Namespace:       dev
Resource         Used    Hard
--------         ----    ----
configmaps       12      30
limits.cpu       6       8
limits.memory    12Gi    16Gi
pods             18      50
persistentvolumeclaims  3   10
requests.cpu     3       4
requests.memory  6Gi     8Gi
requests.storage 30Gi    100Gi
secrets          15      30
services         8       20

Used / Hard 표가 한 페이지에 나오는 모양입니다. 운영 클러스터에서 한 네임스페이스가 어디까지 차 있는지 확인할 때 describe가 가장 빠른 도구입니다.

LimitRange와 짝 — 컨테이너 단위 기본값 · 상한 #

ResourceQuota의 미묘한 함정 하나가 — ResourceQuota가 활성화된 네임스페이스에 Pod를 만들 때, 컨테이너에 requests / limits를 적지 않으면 거부됩니다. ResourceQuota가 합계를 계산하려면 모든 컨테이너의 자원 값이 필요한데 매니페스트에 적히지 않은 컨테이너는 합계 계산이 안 되기 때문입니다.

이 부분의 운영 안전망이 LimitRange입니다. LimitRange는 컨테이너 단위로 기본값과 최댓값 · 최솟값을 정하는 객체입니다. 11장에서 한 번 짚었던 객체이고, 본 챕터에서는 ResourceQuota와 짝으로 한 번 더 정리합니다.

dev-limits.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: dev-limits
  namespace: dev
spec:
  limits:
    - type: Container
      default:
        cpu: 200m
        memory: 256Mi
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      max:
        cpu: "2"
        memory: 2Gi
      min:
        cpu: 50m
        memory: 64Mi

이 LimitRange가 적용된 네임스페이스에 Pod 매니페스트가 들어오면 다음 일이 일어납니다.

  • 컨테이너에 requests가 없으면 defaultRequest (100m / 128Mi)가 자동으로 채워집니다.
  • 컨테이너에 limits가 없으면 default (200m / 256Mi)가 자동으로 채워집니다.
  • 컨테이너의 requestslimitsmax (2 / 2Gi)를 초과하면 거부됩니다.
  • min (50m / 64Mi) 미만이면 거부됩니다.

ResourceQuota와 LimitRange의 책임을 갈라 두면 한 줄씩 정리됩니다.

객체단위무엇을 정하나
LimitRange컨테이너기본값 · 최대 · 최소
ResourceQuota네임스페이스합계 상한, 객체 개수 상한

운영 모양은 둘을 같이 두는 것입니다. LimitRange가 깨진 매니페스트의 빈 부분을 채워 주고, ResourceQuota가 그 채워진 값들의 합계가 한도를 넘지 않도록 막습니다. 둘이 같이 있어야 멀티테넌트 운영의 자원 정책이 안정적으로 굴러갑니다.

scopes / scopeSelector — 일부 Pod에만 #

ResourceQuota는 기본적으로 네임스페이스의 모든 Pod에 적용되지만, scope를 좁힐 수 있습니다. 자주 쓰이는 패턴은 PriorityClass 별 분리입니다 — 예를 들어 high-priority 워크로드의 자원 합계만 따로 제한하거나, BestEffort QoS의 Pod만 별도로 제한하는 식입니다.

high-priority Pod에만 적용되는 quota
apiVersion: v1
kind: ResourceQuota
metadata:
  name: high-priority-quota
  namespace: dev
spec:
  hard:
    requests.cpu: "2"
    requests.memory: 4Gi
  scopeSelector:
    matchExpressions:
      - operator: In
        scopeName: PriorityClass
        values: ["high"]

운영의 기본 모양에서는 scope 없이 전체에 적용하는 한 장으로 시작하고, 필요할 때 scope를 가진 정책을 추가하는 방식이 무난합니다.

세 정책의 협업 — 멀티테넌트 클러스터의 격리 #

세 객체가 만드는 격리의 모양을 한 그림으로 정리해 두면 다음과 같습니다.

세 차원의 협업
[ RBAC ]                 누가 객체를 만들 수 있는가
   │                     (verbs × resources × namespace)
[ NetworkPolicy ]        만들어진 Pod 가 누구와 통신하는가
   │                     (podSelector × from/to × ports)
[ ResourceQuota ]        그 네임스페이스가 얼마나 만들 수 있는가
   │                     (cpu/memory 합계 + 객체 개수)
[ LimitRange ]           컨테이너 단위 기본값 · 상한
                         (default / max / min)

세 객체가 따로 굴러가는 것 같지만 실제로는 한 네임스페이스의 격리를 같이 만듭니다. dev 네임스페이스에 RBAC을 적용해 dev 팀만 그 안의 객체를 만질 수 있게 하고, NetworkPolicy로 dev의 Pod가 prod의 DB로 가는 트래픽을 막고, ResourceQuota로 dev가 잡아먹을 수 있는 CPU · 메모리 · 객체 개수를 한정합니다. 이 셋이 모두 적용되어 있을 때, dev에서 발생한 사고가 staging과 prod로 새지 않는 격리가 성립합니다.

이 격리가 깔끔하게 굴러가려면 한 가지 전제가 더 필요합니다 — 네임스페이스 자체를 잘 갈라 두는 것입니다. 환경별 (dev / staging / prod), 팀별 (team-a / team-b), 또는 서비스 단위로 네임스페이스를 어떻게 가를지의 정책은 클러스터를 처음 셋업할 때 잡아 둬야 하는 결정입니다. 이 결정을 흐릿하게 두고 RBAC / NetworkPolicy / ResourceQuota를 적용하려고 하면, 정책의 결을 어디에 맞춰야 할지 매번 흔들립니다.

2부 회고 — 7장으로 손에 들어온 것 #

2부의 마지막 챕터이므로 한 번 정리합니다. 1부가 매니페스트 한 장을 읽고 쓰는 단계까지 데려다줬다면, 2부는 그 위에 운영의 결을 한 층씩 더했습니다.

  • 8장StatefulSet / DaemonSet / Job / CronJob입니다. Deployment가 아닌 컨트롤러 4종으로, 정체성과 디스크가 필요한 워크로드, 노드마다 한 개씩 떠야 하는 워크로드, 일회성 작업, 주기 실행 작업의 네 패턴을 다룹니다.
  • 9장PV / PVC / StorageClass입니다. 영속 데이터 모델로서, 정적 · 동적 프로비저닝, accessModes, reclaimPolicy, volumeBindingMode, allowVolumeExpansion을 정리하고 StatefulSet의 volumeClaimTemplates가 무엇을 만드는지 설명합니다.
  • 10장Ingress와 Ingress Controller입니다. 외부 진입점을 한 곳에 모으는 객체와, 그 매니페스트를 실제 트래픽 라우팅으로 풀어 주는 컨트롤러를 다루고, HTTP / HTTPS, TLS 종단, 가상 호스트, 경로 기반 라우팅까지 정리합니다.
  • 11장resources.requests / limits입니다. 컨테이너 단위 자원 요청과 상한을 설명하고, requests가 스케줄링의 기준, limits가 OOM · CPU 스로틀링의 기준이라는 점과 QoS 세 등급이 evict 우선순위를 결정하는 흐름을 다룹니다.
  • 12장Health check입니다. liveness / readiness / startup probe 셋으로, 컨테이너의 살아 있음 · 서비스 준비됨 · 초기화 단계를 K8s가 어떻게 판정하는지 살펴봅니다.
  • 13장HPA / VPA / Cluster Autoscaler입니다. Pod 개수, Pod 자원, 노드 개수가 부하에 맞춰 자동으로 변하는 세 차원을 설명하고, metrics-server · custom metrics · HPA + VPA의 충돌 함정도 짚습니다.
  • 14장 (본 챕터)RBAC / NetworkPolicy / ResourceQuota입니다. 보안과 자원 정책을 통해 누가, 어떤 트래픽이, 얼마나 허용되는지 멀티테넌트 클러스터 격리의 세 차원을 다룹니다.

이 일곱 장을 다 따라온 시점이라면, 회사 클러스터의 매니페스트 디렉터리에서 어떤 종류의 객체를 만나도 그 의도와 운영상의 함정을 한 줄로 읽을 수 있는 단계입니다. kind: StatefulSet을 보면 volumeClaimTemplates와 headless Service를 자연스럽게 떠올리고, kind: Ingress를 보면 그 뒤의 Ingress Controller가 무엇인지를 묻고, resources 아래의 빈 limits를 보면 OOM 위험과 LimitRange의 부재를 동시에 의심하게 됩니다. 매니페스트 한 장이 클러스터 안에서 어떻게 굴러가는지의 모델이 머릿속에 정착한 단계입니다.

연습문제 #

  1. 위 본문의 rbac-pod-reader.yamlreader-pod.yaml을 차례로 적용한 뒤, kubectl exec -n dev reader -- kubectl get pods -n devkubectl exec -n dev reader -- kubectl create deployment nginx --image=nginx -n dev 두 명령의 결과를 기록합니다. 그 다음 같은 SA에 deploymentscreate 권한을 더 부여하려면 Role의 rules를 어떻게 추가해야 하는지 매니페스트 한 줄로 적고, kubectl auth can-i create deployments --as=system:serviceaccount:dev:pod-reader -n dev로 검증합니다.
  2. prod 네임스페이스에 §“default-deny → allow 패턴"의 default-deny-allallow-dns 두 정책을 적용한 뒤, frontend → backend 8080 / TCP만 허용하는 정책 한 장을 추가합니다. 그 다음 backend Pod 안에서 다른 네임스페이스의 임의 Service로 curl을 시도해 차단되는지, 허용된 frontend에서는 통하는지를 시간 순서대로 기록합니다. CNI가 NetworkPolicy를 지원하지 않는 클러스터 (예: 기본 flannel)에서는 같은 정책이 트래픽을 막지 못한다는 §“CNI가 NetworkPolicy를 지원해야 한다"의 모델과 어떻게 연결되는지 한 단락으로 메모합니다.
  3. dev 네임스페이스에 dev-quotadev-limits 두 객체를 같이 적용합니다. requests / limits를 빼먹은 Deployment 매니페스트를 apply 했을 때 LimitRange가 자동으로 채워 주는 값, max를 넘는 매니페스트가 거부되는 메시지, ResourceQuota의 합계가 한도에 도달했을 때 새 Pod 생성이 거부되는 메시지를 각각 기록합니다. 세 메시지가 어느 단계 (admission, validation)에서 나오는지를 §“세 정책의 협업"의 그림과 맞춰 한 단락으로 정리합니다.

한 줄 요약: 멀티테넌트 클러스터의 격리는 RBAC (K8s API 권한), NetworkPolicy (Pod 사이 IP 트래픽), ResourceQuota + LimitRange (네임스페이스 자원 합계 + 컨테이너 단위 기본값)의 세 차원이 합쳐졌을 때 성립한다. 운영의 표준 원칙은 RBAC의 최소 권한, NetworkPolicy의 default-deny + allow, ResourceQuota와 LimitRange의 짝 운영이다. Namespace 자체는 보안 경계가 아니라 이 세 정책이 얹혀야 진짜 격리가 만들어진다.

다음 챕터 #

2부가 끝났습니다. 3부 깊이의 첫 챕터인 15장 CNI 깊이부터는 시점을 한 단 더 옮깁니다 — 매니페스트의 동작을 떠받치는 데이터 플레인 · 정책 엔진 · API 확장의 깊이로 들어갑니다. 본 챕터의 NetworkPolicy가 실제로 어떻게 막히는지의 답이 15장의 Calico / Cilium / eBPF입니다.

3부 전체의 줄거리는 다음과 같습니다.

챕터주제
15장CNI 깊이 — Calico · Cilium · eBPF
16장RBAC · ServiceAccount 깊이 — Aggregated ClusterRole · Impersonation · IRSA · Workload Identity
17장Admission Controller — OPA Gatekeeper · Kyverno
18장CRD와 Operator 패턴 — controller-runtime
19장옵저버빌리티 — Prometheus · Grafana · Loki · OpenTelemetry
20장GitOps — ArgoCD · Flux

3부를 마치면 클러스터를 셋업하는 사람의 시야로 K8s를 볼 수 있는 단계에 도달합니다. 그 다음 4부 EKS 실전에서는 AWS EKS 위에 실제 서비스를 처음부터 올리고 운영하는 한 사이클을 따라갑니다.

X