K8s 중급 #7 RBAC / NetworkPolicy / ResourceQuota — 보안과 자원 정책
K8s 중급 시리즈의 마지막 글입니다. #1부터 #6까지 워크로드를 운영하는 모델을 한 층씩 쌓아 왔습니다. 컨트롤러 4종, 영속 데이터, 외부 진입점, 자원 요청,상한, 헬스 체크, 오토스케일링까지 — Pod 한 개를 안정적으로 띄우고 부하에 맞춰 늘리고 줄이는 한 사이클이 손에 들어왔습니다. 이번 글에서는 그 위에 한 층 더 얹는 주제를 다루겠습니다 — 한 클러스터에 여러 팀,환경이 같이 사는 상황의 보안과 자원 통제입니다. 키워드는 셋입니다. RBAC(누가 무엇을 할 수 있는가), NetworkPolicy(어떤 트래픽이 통하는가), ResourceQuota(얼마나 만들 수 있는가). 세 객체 모두 네임스페이스 단위 정책이라는 공통점이 있고, 기초 #7에서 “Namespace 자체는 보안 경계가 아니다"라고 짚어 둔 그 빈 부분을 이 세 객체가 채웁니다. 시리즈의 마지막 글이라 7편 전체 회고와 다음 트랙(K8s 고급) 예고도 같이 담겠습니다.
이번 시리즈는 K8s 중급 7편입니다.
- #1 StatefulSet / DaemonSet / Job / CronJob — Deployment가 아닌 다른 컨트롤러들
- #2 PV / PVC / StorageClass — 영속 데이터 모델
- #3 Ingress와 Ingress Controller — 외부 진입점
- #4 resources.requests / limits — Pod의 자원 요청과 상한
- #5 Health check — liveness / readiness / startup probe
- #6 오토스케일링 — HPA / VPA / Cluster Autoscaler
- #7 RBAC / NetworkPolicy / ResourceQuota — 보안과 자원 정책 ← 이번 글
세 정책의 공통 좌표 — 네임스페이스 단위 #
기초 #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) | 클러스터 (모든 네임스페이스 공유) |
RoleBinding | Role 또는 ClusterRole을 주체에게 부여 | 네임스페이스 |
ClusterRoleBinding | ClusterRole을 주체에게 부여 | 클러스터 전역 |
여기서 한 가지 미묘한 부분이 있습니다. RoleBinding은 ClusterRole을 참조할 수도 있습니다. ClusterRole은 권한 묶음이고 RoleBinding은 그 묶음을 어떤 네임스페이스에서 누구에게 줄지 결정하는 객체이기 때문에, “표준 ClusterRole(예: view, edit) 하나를 만들어 두고 네임스페이스마다 RoleBinding으로 다른 사람에게 부여"하는 패턴이 운영에서 가장 흔합니다.
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의 권한으로 수행됩니다.
kubectl get serviceaccounts -n defaultNAME 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가 통하는지 확인하는 흐름입니다.
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-reader—dev네임스페이스의 새 ID. 아직 어떤 권한도 없음. - Role
pod-reader—pods자원에 대해get,list,watch동사를 허용하는 권한 묶음.apiGroups: [""]은 코어 API 그룹(Pod, Service, ConfigMap 등)을 가리킴. - RoleBinding
pod-reader— Role과 ServiceAccount를 잇는 객체.subjects가 받는 쪽,roleRef가 주는 쪽.
이 세 객체를 한 번에 적용한 뒤 그 ServiceAccount를 쓰는 Pod를 띄워 봅니다.
apiVersion: v1
kind: Pod
metadata:
name: reader
namespace: dev
spec:
serviceAccountName: pod-reader
containers:
- name: kubectl
image: bitnami/kubectl:1.30
command: ["sleep", "3600"]kubectl apply -f rbac-pod-reader.yaml
kubectl apply -f reader-pod.yaml
kubectl exec -n dev reader -- kubectl get pods -n devNAME READY STATUS RESTARTS AGE
reader 1/1 Running 0 30s같은 Pod 안에서 권한이 없는 동작을 시도하면 막히는 것도 확인할 수 있습니다.
kubectl exec -n dev reader -- kubectl create deployment nginx --image=nginx -n deverror: 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를 추가합니다.
resources는 pods, services, configmaps처럼 복수형 이름을 적습니다. apiGroups는 그 자원이 속한 API 그룹입니다. 코어 그룹(Pod / Service / ConfigMap / Secret 등)은 [""](빈 문자열), Deployment / StatefulSet / DaemonSet은 ["apps"], Job / CronJob은 ["batch"], Ingress는 ["networking.k8s.io"]입니다. 자원이 어느 그룹에 속하는지는 kubectl api-resources로 한 번에 확인할 수 있습니다.
kubectl api-resourceskubectl auth can-i — 권한 검증 #
RBAC을 만져 두면 다음 질문이 곧 따라옵니다 — “그래서 이 사용자가 진짜 이 동작을 할 수 있는가?” 매니페스트를 한참 들여다봐도 결과가 한눈에 들어오지 않을 때가 많습니다. K8s가 이 질문에 직접 답해 주는 명령이 kubectl auth can-i입니다.
kubectl auth can-i create pods -n dev
kubectl auth can-i delete deployments -n prodyes / no 둘 중 하나가 출력됩니다. 다른 사용자나 ServiceAccount의 입장에서 묻고 싶다면 --as 옵션으로 impersonation을 씁니다.
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흔한 함정 — 너무 넓은 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.iocluster-admin은 K8s API 전체에 대한 무제한 권한입니다. 이 ClusterRoleBinding이 적용된 ServiceAccount의 토큰이 Pod 한 개라도 외부로 새면, 그 토큰으로 클러스터의 모든 네임스페이스의 모든 Secret을 읽고 모든 객체를 지울 수 있습니다. 컨테이너 이미지 안의 라이브러리에 취약점 하나만 있어도 그 한 점이 클러스터 전체의 사고로 번집니다.
운영의 표준 원칙은 최소 권한(least privilege) 입니다. 워크로드가 진짜 필요한 verbs와 resources만 정확히 추려서 Role로 묶고, 그 Role을 RoleBinding으로 부여합니다. K8s가 미리 만들어 두는 표준 ClusterRole 셋이 있는데, 이것을 RoleBinding으로 네임스페이스에 한정해 쓰는 패턴이 일반적인 시작점입니다.
| ClusterRole | 설명 |
|---|---|
view | 네임스페이스의 객체를 읽기. Secret은 제외 |
edit | view + 대부분 객체의 쓰기. Role/RoleBinding 등 RBAC 객체는 제외 |
admin | edit + 그 네임스페이스 안의 RBAC 객체 관리 |
cluster-admin | 클러스터 전체에 무제한 |
view / edit / admin을 RoleBinding으로 특정 네임스페이스에 한정하면 사람,팀,서비스에 적당한 권한 묶음을 빠르게 부여할 수 있고, cluster-admin은 클러스터 운영자 한 줌한테만 ClusterRoleBinding으로 부여하는 모양이 표준입니다.
automountServiceAccountToken #
ServiceAccount의 토큰은 기본적으로 Pod 안의 /var/run/secrets/kubernetes.io/serviceaccount/token에 자동으로 마운트됩니다. K8s API를 호출할 일이 없는 Pod에는 그 토큰조차 마운트하지 않는 편이 안전합니다. 매니페스트의 automountServiceAccountToken: false로 끌 수 있습니다.
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:1.27이 한 줄을 넣어 두면 그 Pod가 만에 하나 침해당해도 K8s API에 직접 접근할 토큰이 없습니다. 보안 가이드의 단골 권장 사항입니다.
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를 지원하지 않으면 매니페스트를 적용해도 아무 일도 일어나지 않습니다. 트래픽은 그대로 통합니다.
| CNI | NetworkPolicy 지원 |
|---|---|
| Calico | 지원 |
| Cilium | 지원 (eBPF 기반) |
| Antrea | 지원 |
| flannel | 미지원 |
| EKS의 amazon-vpc-cni | 별도 옵션 활성화 필요 (Calico를 함께 깔거나 vpc-cni의 NetworkPolicy 옵션을 켜야 함) |
운영 클러스터에서 NetworkPolicy를 쓸 계획이라면 클러스터 만들 때부터 CNI를 골라 두어야 합니다. 기본 EKS 클러스터에서 NetworkPolicy 매니페스트를 적용하고 “왜 트래픽이 안 막히지?“가 흔한 함정입니다 — CNI가 지원하지 않으면 매니페스트는 그저 etcd에 들어 있는 객체일 뿐입니다.
default-deny → allow 패턴 #
운영의 표준 패턴은 네임스페이스에 default-deny 정책을 한 장 깔고, 필요한 통신만 명시적으로 허용하는 것입니다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: prod
spec:
podSelector: {}
policyTypes:
- Ingress
- EgresspodSelector: {}은 빈 셀렉터로 “이 네임스페이스의 모든 Pod"를 가리킵니다. policyTypes에 Ingress와 Egress가 모두 적혀 있고 ingress / egress 규칙은 한 줄도 없으므로, 이 네임스페이스의 모든 Pod는 들어오는 트래픽도 나가는 트래픽도 모두 차단됩니다.
이 상태에서 그대로 두면 Pod가 DNS 조회조차 못 하게 되므로, 최소한 DNS 트래픽은 풀어 줘야 합니다.
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 정책입니다.
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 조합) |
ipBlock | CIDR 표현으로 IP 범위 (외부 IP 또는 노드 IP) |
namespaceSelector와 podSelector를 같은 from 항목에 같이 적으면 “특정 네임스페이스의 특정 라벨 Pod"가 됩니다. 두 개를 따로 적으면 “그 네임스페이스의 모든 Pod 또는 같은 네임스페이스의 그 라벨 Pod” 의미가 되어 버리므로, 의도한 모양이 무엇인지 정확히 보고 적어야 합니다.
ingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
podSelector:
matchLabels:
app: frontendingress:
- 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 포트로 나가는 통신만 허용하는 정책은 다음 모양입니다.
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 같은 서비스 메시가 그 차원을 다룹니다. 그리고 NetworkPolicy는 클러스터 안의 트래픽 정책이고, 외부 인바운드는 Ingress / LoadBalancer 차원에서, 외부 아웃바운드는 NAT 게이트웨이의 보안 그룹에서 별도로 통제됩니다. 한 클러스터의 보안 모양은 여러 층의 정책이 같이 만들어 갑니다.
ResourceQuota — 네임스페이스 자원 합계 상한 #
#4에서 컨테이너 단위 자원 모델 — requests와 limits — 을 다뤘습니다. 컨테이너 한 개의 보장과 상한을 정하는 객체였습니다. 그 위에 한 층 더 얹는 것이 네임스페이스 단위 합계 상한인 ResourceQuota입니다.
운영 시나리오는 분명합니다. 한 클러스터에 dev / staging / prod, 또는 여러 팀이 같이 살 때, dev 네임스페이스가 모든 노드의 CPU를 독점해 prod 워크로드의 자원이 부족해지는 사고를 막아야 합니다. 이 사고를 ResourceQuota가 막습니다.
ResourceQuota 매니페스트 #
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합계 ≤ 8Gilimits.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 단계에서 거부되고 다음 메시지가 나옵니다.
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=4ResourceQuota 동작 확인 #
kubectl get resourcequota -n dev
kubectl describe resourcequota dev-quota -n devName: 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 20Used / Hard 표가 한 페이지에 나오는 모양입니다. 운영 클러스터에서 한 네임스페이스가 어디까지 차 있는지 확인할 때 describe가 가장 빠른 도구입니다.
LimitRange와 짝 — 컨테이너 단위 기본값,상한 #
ResourceQuota의 미묘한 함정 하나가 — ResourceQuota가 활성화된 네임스페이스에 Pod를 만들 때, 컨테이너에 requests/limits를 적지 않으면 거부됩니다. ResourceQuota가 합계를 계산하려면 모든 컨테이너의 자원 값이 필요한데 매니페스트에 적히지 않은 컨테이너는 합계 계산이 안 되기 때문입니다.
이 부분의 운영 안전망이 LimitRange입니다. LimitRange는 컨테이너 단위로 기본값과 최댓값,최솟값을 정하는 객체입니다.
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)가 자동으로 채워짐 - 컨테이너의
requests나limits가max(2 / 2Gi)를 초과하면 거부 min(50m / 64Mi) 미만이면 거부
ResourceQuota와 LimitRange의 책임을 갈라 두면 한 줄씩 정리됩니다.
| 객체 | 단위 | 무엇을 정하나 |
|---|---|---|
LimitRange | 컨테이너 | 기본값,최대,최소 |
ResourceQuota | 네임스페이스 | 합계 상한, 객체 개수 상한 |
운영 모양은 둘을 같이 두는 것입니다. LimitRange가 깨진 매니페스트의 빈 부분을 채워 주고, ResourceQuota가 그 채워진 값들의 합계가 한도를 넘지 않도록 막습니다. 둘이 같이 있어야 멀티테넌트 운영의 자원 정책이 안정적으로 굴러갑니다.
scopes / scopeSelector — 일부 Pod에만 #
ResourceQuota는 기본적으로 네임스페이스의 모든 Pod에 적용되지만, scope를 좁힐 수 있습니다. 자주 쓰이는 패턴은 PriorityClass별 분리입니다 — 예를 들어 high-priority 워크로드의 자원 합계만 따로 제한하거나, BestEffort QoS의 Pod만 별도로 제한하는 식입니다.
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를 적용하려고 하면, 정책의 결을 어디에 맞춰야 할지 매번 흔들립니다.
시리즈 회고 — K8s 중급 7편으로 손에 들어온 것 #
마지막 글이므로 7편을 한 번 짚어 두겠습니다. 기초 시리즈가 매니페스트 한 장을 읽고 쓰는 단계까지 데려다줬다면, 중급 시리즈는 그 위에 운영의 결을 한 층씩 더했습니다.
- #1 — StatefulSet / DaemonSet / Job / CronJob입니다. Deployment가 아닌 컨트롤러 4종으로, 정체성과 디스크가 필요한 워크로드, 노드마다 한 개씩 떠야 하는 워크로드, 일회성 작업, 주기 실행 작업의 네 패턴을 다룹니다.
- #2 — PV / PVC / StorageClass입니다. 영속 데이터 모델로서, 정적,동적 프로비저닝, accessModes, reclaimPolicy, volumeBindingMode, allowVolumeExpansion을 정리하고 StatefulSet의 volumeClaimTemplates가 무엇을 만드는지 설명합니다.
- #3 — Ingress와 Ingress Controller입니다. 외부 진입점을 한 곳에 모으는 객체와, 그 매니페스트를 실제 트래픽 라우팅으로 풀어 주는 컨트롤러를 다루고, HTTP/HTTPS, TLS 종단, 가상 호스트, 경로 기반 라우팅까지 정리합니다.
- #4 — resources.requests / limits입니다. 컨테이너 단위 자원 요청과 상한을 설명하고, requests가 스케줄링의 기준, limits가 OOM,CPU 스로틀링의 기준이라는 점과 QoS 세 등급이 evict 우선순위를 결정하는 흐름을 다룹니다.
- #5 — Health check입니다. liveness / readiness / startup probe 셋으로, 컨테이너의 살아 있음,서비스 준비됨,초기화 단계를 K8s가 어떻게 판정하는지 살펴봅니다.
- #6 — HPA / VPA / Cluster Autoscaler입니다. Pod 개수, Pod 자원, 노드 개수가 부하에 맞춰 자동으로 변하는 세 차원을 설명하고, metrics-server,custom metrics,HPA + VPA의 충돌 함정도 짚습니다.
- #7 — RBAC / NetworkPolicy / ResourceQuota입니다. 보안과 자원 정책을 통해 누가, 어떤 트래픽이, 얼마나 허용되는지 멀티테넌트 클러스터 격리의 세 차원을 다룹니다.
이 일곱 편을 다 따라온 시점이라면, 회사 클러스터의 매니페스트 디렉터리에서 어떤 종류의 객체를 만나도 그 의도와 운영상의 함정을 한 줄로 읽을 수 있는 단계입니다. kind: StatefulSet을 보면 volumeClaimTemplates와 headless Service를 자연스럽게 떠올리고, kind: Ingress를 보면 그 뒤의 Ingress Controller가 무엇인지를 묻고, resources 아래의 빈 limits를 보면 OOM 위험과 LimitRange의 부재를 동시에 의심하게 됩니다. 매니페스트 한 장이 클러스터 안에서 어떻게 굴러가는지의 모델이 머릿속에 정착한 단계입니다.
다음 트랙 — K8s 고급 #
K8s 중급 시리즈에서 일부러 미뤄 둔 깊은 주제들이 K8s 고급 트랙의 줄거리입니다. 6편으로 묶을 예정이고, 표로 미리 정리해 두면 다음과 같습니다.
| 주제 | 설명 |
|---|---|
| CNI 깊이 — Calico / Cilium / eBPF | 클러스터 네트워크의 실제 데이터 플레인. iptables 기반과 eBPF 기반의 차이, NetworkPolicy의 실행 단의 모양. |
| RBAC / ServiceAccount 깊이 | Aggregated ClusterRole, impersonation, 외부 IAM 매핑(EKS의 IRSA, GKE의 Workload Identity), 토큰 lifecycle. |
| Admission Controller / OPA Gatekeeper / Kyverno | 정책 엔진. 매니페스트가 etcd에 들어가기 전에 검사,변형하는 단계. “limits 없는 컨테이너 거부”, “특정 라벨 강제” 같은 정책. |
| CRD와 Operator 패턴 | Kubernetes API 확장. CustomResourceDefinition으로 새 객체 종류를 정의, controller-runtime 기반의 Operator로 그 객체를 운영. |
| 옵저버빌리티 | Prometheus + Grafana + Loki, kube-state-metrics, OpenTelemetry. 클러스터,워크로드의 메트릭,로그,트레이스의 표준 스택. |
| GitOps — ArgoCD / Flux | 매니페스트의 source of truth를 git에 두는 운영 모델. drift detection, multi-cluster, sync policy. |
이 여섯 주제를 다 다루고 나면, 클러스터를 셋업하는 사람의 시야로 K8s를 볼 수 있는 단계까지 한 발 더 들어갑니다. 매니페스트를 잘 쓰는 단계에서, 어떤 정책 엔진을 깔지,어떤 옵저버빌리티 스택을 고를지,GitOps 파이프라인을 어떻게 짤지를 결정하는 단계로 넘어가는 트랙입니다.
그 다음 — K8s 실전 #
고급 트랙 다음에는 K8s 실전 6편을 준비하고 있습니다. 고급까지가 K8s의 객체 모델과 정책의 깊이를 다룬다면, 실전은 그 위에 진짜 서비스를 한 개 올리고 운영하는 한 사이클입니다.
| 주제 | 설명 |
|---|---|
| EKS 클러스터 셋업 | AWS EKS 클러스터를 처음부터, IAM, VPC, 노드 그룹, 애드온. |
| 앱 배포 골격 | Deployment + Service + Ingress + ConfigMap + Secret의 한 묶음, Helm 차트로 정리. |
| DB 연동 | RDS / Aurora를 Pod에서 안전하게 부르는 길, Secrets Manager 통합, 커넥션 풀. |
| CI/CD 파이프라인 | GitHub Actions에서 컨테이너 빌드 → ECR push → ArgoCD sync. |
| 모니터링,알람 | CloudWatch + Prometheus, 핵심 알람 룰셋, on-call 흐름. |
| 운영 체크리스트 | 업그레이드, 백업,복구, 비용 점검, 보안 점검의 정기 운영 사이클. |
이 두 트랙(고급 + 실전)을 거치면 K8s를 도입하고 운영하는 사람의 시야가 거의 다 들어옵니다. 중급 시리즈가 끝나는 지점이 그 큰 그림의 한가운데입니다 — 객체 모델은 손에 들어왔고, 그 위에 얹히는 정책,확장,운영의 깊이가 다음 트랙입니다.
마무리 #
K8s 중급 시리즈 7편을 마무리했습니다. 이번 글에서는 멀티테넌트 클러스터의 격리를 만드는 세 정책 객체 — RBAC, NetworkPolicy, ResourceQuota — 를 한 사이클로 정리했습니다. RBAC이 K8s API의 권한을, NetworkPolicy가 Pod 사이 트래픽을, ResourceQuota(와 짝꿍 LimitRange)가 네임스페이스의 자원 합계를 통제하고, 세 차원이 같이 적용되어 있을 때 비로소 한 클러스터 위에 여러 환경,팀이 안전하게 같이 살 수 있다는 모델을 따라갔습니다. 시리즈 전체로 보면, 기초 7편이 매니페스트 한 장을 읽고 쓰는 단계, 중급 7편이 그 매니페스트가 운영 클러스터에서 어떻게 굴러가는지의 깊이를 한 층씩 더한 단계였습니다. 다음 트랙인 K8s 고급에서는 CNI,RBAC의 깊이부터 정책 엔진, CRD/Operator, 옵저버빌리티, GitOps까지 — 클러스터를 셋업하고 운영하는 시야의 주제들을 차례로 다루겠습니다. 그 다음 K8s 실전에서는 EKS 위에 진짜 서비스를 올리는 한 사이클을 처음부터 끝까지 따라가겠습니다.