Pod는 쿠버네티스에서 애플리케이션을 배포하는 가장 작은 단위다. Pod를 이해하려면 Container, Label, Node Schedule 세 가지를 알아야 한다.
┌──────────── Pod (IP: 10.244.1.7) ────────────┐
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ container1 │ │ container2 │ │
│ │ port 8000 │ │ port 8080 │ │
│ └────────────────┘ └────────────────┘ │
│ │
│ 하나의 IP를 공유 → 포트는 달라야 한다 │
└───────────────────────────────────────────────┘
각 컨테이너는 Service가 연결될 수 있도록 포트를 가진다. Service는 이 포트를 통해 컨테이너에 트래픽을 전달한다.
Pod 안의 컨테이너들은 하나의 IP를 공유하므로 같은 Pod 안에서 동일한 포트를 사용할 수 없다. 두 컨테이너가 모두 8080을 사용하면 포트 충돌이 발생한다. 위 구조처럼 8000과 8080으로 다르게 지정해야 한다.
| 특성 | 내용 |
|---|---|
| 할당 방식 | Pod 생성 시 자동으로 할당된다 |
| 변경 여부 | Pod가 재생성되면 IP가 변경된다 |
| 접근 범위 | 클러스터 내부에서만 이 IP로 접근할 수 있다 |
| 외부 접근 | 클러스터 외부에서는 이 IP로 접근할 수 없다 |
Pod IP는 고정되지 않으므로 다른 애플리케이션이 Pod IP를 직접 지정해 통신하면 안 된다. 이 문제를 해결하기 위해 뒤에서 다룰 Service를 사용한다.
apiVersion: v1
kind: Pod
metadata:
name: pod-1
spec:
containers:
- name: container1
image: tmkube/p8000
ports:
- containerPort: 8000
- name: container2
image: tmkube/p8080
ports:
- containerPort: 8080
| 항목 | 설명 |
|---|---|
kind: Pod | 생성할 오브젝트의 종류 |
metadata.name | Pod의 이름. 같은 네임스페이스 안에서 중복될 수 없다 |
spec.containers | Pod 안에서 실행할 컨테이너 목록. 여러 개를 정의할 수 있다 |
image | 컨테이너가 실행할 이미지 |
containerPort | 컨테이너가 사용하는 포트. Pod 안에서 중복될 수 없다 |
containerPort는 실제로 포트를 열어주는 설정이 아니다. 애플리케이션이 어떤 포트를 사용하는지 명시하는 문서 역할에 가깝다. 실제 포트는 컨테이너 내부의 애플리케이션이 연다. 다만 이 값을 적어두면 Service를 연결하거나 다른 사람이 YAML을 볼 때 구조를 파악하기 쉬워지므로 작성하는 것이 좋다.
Label은 오브젝트에 붙이는 key: value 형태의 이름표다. Pod뿐 아니라 Service, Node 등 모든 오브젝트에 붙일 수 있다.
쿠버네티스에서 오브젝트가 많아지면 이름만으로는 관리가 어렵다. Label을 붙여두면 원하는 조건의 오브젝트만 골라내는 것이 가능해진다. 이때 조건을 지정하는 쪽이 selector다.
개발 환경과 운영 환경에 각각 web, db, server를 운영한다고 하자. Pod가 총 6개다.
Label로 분류된 Pod들
┌──────────────────────────┬──────────────────────────┐
│ lo: dev │ lo: production │
├──────────────────────────┼──────────────────────────┤
│ ┌────────────────────┐ │ ┌────────────────────┐ │
│ │ Pod │ │ │ Pod │ │
│ │ type: web │ │ │ type: web │ │
│ │ lo: dev │ │ │ lo: production │ │
│ └────────────────────┘ │ └────────────────────┘ │
│ ┌────────────────────┐ │ ┌────────────────────┐ │
│ │ Pod │ │ │ Pod │ │
│ │ type: db │ │ │ type: db │ │
│ │ lo: dev │ │ │ lo: production │ │
│ └────────────────────┘ │ └────────────────────┘ │
│ ┌────────────────────┐ │ ┌────────────────────┐ │
│ │ Pod │ │ │ Pod │ │
│ │ type: server │ │ │ type: server │ │
│ │ lo: dev │ │ │ lo: production │ │
│ └────────────────────┘ │ └────────────────────┘ │
└──────────────────────────┴──────────────────────────┘
각 Pod는 두 개의 Label을 갖는다.
type : 어떤 역할인가 (web / db / server)lo : 어떤 환경인가 (dev / production)Service가 selector: type: web 을 지정하면, type이 web인 Pod에만 연결된다.
Service svc-1
selector:
type: web
│
│ type: web 인 Pod만 선택
▼
┌─────────────┐ ┌─────────────┐
│ type: web │ │ type: web │ ┌─────────────┐
│ lo: dev │ │ lo: │ │ type: db │ ← 선택 안 됨
│ │ │ production │ │ lo: dev │
└─────────────┘ └─────────────┘ └─────────────┘
여기서 lo는 조건에 없으므로 dev와 production의 web Pod가 모두 선택된다. 개발용 Service를 따로 만들고 싶다면 조건을 두 개로 지정하면 된다.
selector:
type: web
lo: dev
Label이 여러 개 지정되면 모두 만족하는 Pod만 선택된다.
| 이유 | 설명 |
|---|---|
| 연결 대상 지정 | Service가 어떤 Pod에 트래픽을 보낼지 Label로 결정한다 |
| 환경 분리 | 개발용과 운영용 Pod를 섞이지 않게 관리할 수 있다 |
| 일괄 조회 | kubectl get pods -l lo=dev 처럼 조건에 맞는 Pod만 조회할 수 있다 |
| 확장성 | Pod가 늘어나도 Label만 맞으면 Service가 자동으로 인식한다 |
마지막 항목이 특히 중요하다. Service는 특정 Pod를 이름으로 기억하지 않는다. 조건에 맞는 Pod를 그때그때 찾아서 연결한다. 그래서 Pod가 재생성되어 IP가 바뀌어도, 새 Pod에 같은 Label만 붙어 있으면 Service는 문제없이 동작한다.
Service — 조건을 지정하는 쪽
apiVersion: v1
kind: Service
metadata:
name: svc-1
spec:
selector:
type: web
ports:
- port: 8080
Pod — Label을 붙이는 쪽
apiVersion: v1
kind: Pod
metadata:
name: pod-2
labels:
type: web
lo: dev
spec:
containers:
- name: container
image: tmkube/init
| 항목 | 설명 |
|---|---|
metadata.labels | Pod에 붙는 Label. key: value 형태로 여러 개 지정 가능 |
spec.selector | Service가 연결할 Pod의 조건. 지정한 Label을 모두 가진 Pod가 선택된다 |
ports.port | Service가 사용하는 포트 |
Label과 비슷하게 생겼지만 목적이 다른 Annotation이 있다. Label은 오브젝트를 선택하기 위한 정보이고, Annotation은 선택에는 쓰이지 않는 부가 정보를 기록하는 용도다. 배포 일시, 담당자, 관련 문서 링크 같은 것을 Annotation에 적는다.
Pod는 반드시 어느 한 Node 위에서 실행된다. 어떤 Node에서 실행할지 결정하는 과정을 Node Schedule이라고 한다.
방법은 두 가지다.
| 방법 | 설명 |
|---|---|
| 직접 지정 | 사용자가 특정 Node를 지목한다 |
| 스케줄러 판단 | 쿠버네티스 스케줄러가 상황을 보고 결정한다 |
Node에도 Label을 붙일 수 있다. Pod에 nodeSelector를 지정하면 해당 Label을 가진 Node에만 배치된다.
Pod-3
nodeSelector:
hostname: node1
│
│ hostname: node1 인 Node를 찾는다
▼
┌──────────────────┐ ┌──────────────────┐
│ Node1 │ │ Node2 │
│ hostname: node1 │ │ hostname: node2 │
│ │ │ │
│ [Pod-3] ← 배치 │ │ │
└──────────────────┘ └──────────────────┘
apiVersion: v1
kind: Pod
metadata:
name: pod-3
spec:
nodeSelector:
hostname: node1
containers:
- name: container
image: tmkube/init
| 항목 | 설명 |
|---|---|
nodeSelector | Pod를 배치할 Node의 조건. 해당 Label을 가진 Node에만 배치된다 |
주의할 점: 조건에 맞는 Node가 없거나, 그 Node에 자원이 부족하면 Pod는 배치되지 못하고 Pending 상태로 대기한다. 다른 Node로 알아서 옮겨가지 않는다.
사용하는 경우: GPU가 달린 Node에서만 실행해야 하거나, SSD가 장착된 Node에 데이터베이스를 배치해야 하는 경우처럼 특정 Node의 조건이 필요할 때 사용한다.
Node를 지정하지 않으면 스케줄러가 결정한다. 이때 판단 기준이 되는 것이 resources다.
apiVersion: v1
kind: Pod
metadata:
name: pod-4
spec:
containers:
- name: container
image: tmkube/init
resources:
requests:
memory: 2Gi
limits:
memory: 3Gi
| 항목 | 설명 |
|---|---|
requests | Pod가 최소한 필요로 하는 자원. 스케줄러가 Node를 고를 때 이 값을 기준으로 삼는다 |
limits | Pod가 최대로 사용할 수 있는 자원. 실행 중 이 값을 넘지 못하도록 제한한다 |
Pod-4가 requests: memory 2Gi를 요구하는 상황이다. 클러스터에 Node가 세 개 있다.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Node1 │ │ Node2 │ │ Node3 │
│ │ │ │ │ │
│ 전체 8Gi │ │ 전체 8Gi │ │ 전체 8Gi │
│ 사용중 7Gi │ │ 사용중 2Gi │ │ 사용중 5Gi │
│ 여유 1Gi │ │ 여유 6Gi │ │ 여유 3Gi │
└──────────────┘ └──────────────┘ └──────────────┘
✗ ✓ ✓
2Gi 불가 배치 가능 배치 가능
1단계 — 필터링 (Filtering)
배치가 불가능한 Node를 먼저 제외한다.
2단계 — 스코어링 (Scoring)
남은 후보 중 가장 적합한 Node를 점수로 고른다. 자원 여유가 많은 Node가 높은 점수를 받는다.
3단계 — 배치
점수가 가장 높은 Node2에 Pod-4를 배치한다.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Node1 │ │ Node2 │ │ Node3 │
│ │ │ [Pod-4] │ │ │
│ 여유 1Gi │ │ 여유 4Gi │ │ 여유 3Gi │
└──────────────┘ └──────────────┘ └──────────────┘
↑ 배치됨
이렇게 스케줄러는 자원 여유가 부족한 Node를 걸러내고, 남은 Node 중 가장 여유 있는 곳에 Pod를 배치해 클러스터 전체의 자원 사용을 고르게 유지한다.
두 값의 역할이 다르므로 구분해서 이해해야 한다.
| requests | limits | |
|---|---|---|
| 시점 | Pod를 배치할 때 | Pod가 실행 중일 때 |
| 역할 | 스케줄러가 Node를 고르는 기준 | 자원 사용량의 상한 |
| 초과 시 | — | 메모리 초과 시 컨테이너 종료 후 재시작 |
메모리가 limits를 초과하면 컨테이너가 강제 종료된다. 이 상태를 OOMKilled(Out Of Memory Killed)라고 한다. CPU는 초과해도 종료되지 않고 사용량이 제한되기만 한다.
nodeSelector는 조건이 맞지 않으면 Pod가 배치되지 않는 단순하고 엄격한 방식이다. 실무에서는 "가능하면 이 Node에, 안 되면 다른 Node에라도" 처럼 유연한 조건이 필요한 경우가 많다. 이런 조건은 Node Affinity로 표현하며, 이후 단계에서 다루는 개념이다. 처음에는 nodeSelector로 Node 지정의 개념을 이해하고, 이후에 Affinity로 확장해 나가면 된다.
Pod는 언제든지 죽을 수 있다. 노드에 장애가 나거나, 새 버전을 배포하거나, 자원이 부족해 재배치되는 경우 Pod는 삭제되고 새로 생성된다.
이때 Pod가 재생성되면 IP가 바뀐다.
[ 배포 전 ] [ Pod 재생성 후 ]
┌────────────────┐ ┌────────────────┐
│ Pod-1 │ │ Pod-1 (새로) │
│ IP 10.244.1.7 │ │ IP 10.244.1.23 │ ← IP 변경
└────────────────┘ └────────────────┘
▲ ▲
│ 10.244.1.7 로 호출 │ 연결 끊김 ✗
┌────────────────┐ ┌────────────────┐
│ 다른 애플리케이션│ │ 다른 애플리케이션│
└────────────────┘ └────────────────┘
Pod IP를 직접 지정해 통신하면, Pod가 한 번 재생성되는 순간 연결이 끊긴다. 즉 Pod IP는 신뢰성이 없다.
Service는 사용자가 직접 삭제하지 않는 한 삭제되거나 재생성되지 않는다. 따라서 Service의 IP는 변하지 않는다.
┌─────────────────────────────────────────────┐
│ Service (IP 172.96.10.17) ← 변하지 않음 │
└──────────────────┬──────────────────────────┘
│ Label로 Pod를 찾아 연결
▼
┌────────────────────────────┐
│ Pod IP 10.244.1.7 │
│ ↓ 재생성 │
│ Pod IP 10.244.1.23 │ ← IP가 바뀌어도
└────────────────────────────┘ Service가 알아서 다시 연결
Service는 특정 Pod를 IP로 기억하지 않고 Label 조건으로 그때그때 찾아서 연결한다. 그래서 Pod가 재생성되어 IP가 바뀌어도 새 Pod에 같은 Label만 붙어 있으면 연결이 유지된다.
정리하면 Service의 역할은 다음과 같다.
| 역할 | 설명 |
|---|---|
| 고정 주소 제공 | 변하지 않는 IP와 이름으로 Pod에 접근할 수 있게 한다 |
| Pod 추적 | Label 조건에 맞는 현재 살아있는 Pod를 계속 추적한다 |
| 트래픽 분배 | 연결된 Pod가 여러 개면 요청을 나눠서 전달한다 |
| 종류 | 접근 범위 | 주요 용도 |
|---|---|---|
| ClusterIP | 클러스터 내부만 | 내부 통신, 디버깅, 대시보드 |
| NodePort | 클러스터 외부 가능 (Node IP) | 데모, 임시 연결 |
| LoadBalancer | 클러스터 외부 가능 (전용 IP) | 실제 외부 서비스 노출 |
세 가지는 별개의 기능이 아니라 누적되는 구조다. NodePort는 ClusterIP의 기능을 포함하고, LoadBalancer는 NodePort의 기능을 포함한다.
┌───────────────────────────────────────────┐
│ LoadBalancer │
│ ┌─────────────────────────────────────┐ │
│ │ NodePort │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ ClusterIP │ │ │
│ │ │ (기본 - 내부 접근) │ │ │
│ │ └───────────────────────────────┘ │ │
│ │ + 모든 Node에 포트 개방 │ │
│ └─────────────────────────────────────┘ │
│ + 외부 로드밸런서와 전용 IP │
└───────────────────────────────────────────┘
ClusterIP는 Service의 기본 타입이다. type을 지정하지 않으면 자동으로 ClusterIP가 된다.
| 특성 | 내용 |
|---|---|
| IP 할당 | Service 생성 시 클러스터 전용 IP가 자동 할당된다 |
| 접근 범위 | 클러스터 내부에서만 접근 가능하다 |
| 외부 접근 | 외부에서는 이 IP로 접근할 수 없다 |
| 수명 | 사용자가 삭제하지 않는 한 IP가 변하지 않는다 |
┌─────────────────── Cluster ───────────────────┐
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Service svc-1 │ │
│ │ IP: 172.96.10.17 │ │
│ │ port 9000 → targetPort 8080 │ │
│ │ selector: app: pod │ │
│ └───────────────┬──────────────────────┘ │
│ │ app: pod 인 Pod를 찾음 │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ Pod-1 │ │
│ │ labels: app: pod │ │
│ │ containerPort: 8080 │ │
│ └────────────────────────┘ │
│ │
└────────────────────────────────────────────────┘
▲
│ ✗ 외부에서 172.96.10.17 접근 불가
┌───────────┐
│ External │
└───────────┘
Service에는 포트가 두 개 등장한다. 처음 배울 때 가장 헷갈리는 부분이므로 흐름으로 이해하는 것이 좋다.
요청: 172.96.10.17 : 9000
│
│ ① port 9000 으로 Service에 도착
▼
┌───────────────┐
│ Service │
│ port: 9000 │
└───────┬───────┘
│ ② targetPort 8080 으로 전달
▼
┌───────────────┐
│ Pod │
│ 8080 │
└───────────────┘
| 항목 | 의미 |
|---|---|
port | Service가 열어두는 포트. 요청을 받는 입구 |
targetPort | Pod의 컨테이너가 사용하는 포트. 요청을 전달할 목적지 |
클러스터 내부의 다른 Pod에서 접근하는 방법이다.
# Service의 IP로 접근
curl 172.96.10.17:9000
# Service의 이름으로 접근 (권장)
curl svc-1:9000
IP 대신 Service 이름으로도 접근할 수 있다. 쿠버네티스에는 내부 DNS가 있어서 Service 이름을 IP로 변환해 준다. Service를 지우고 다시 만들면 IP는 바뀔 수 있지만 이름은 그대로이므로, 실무에서는 이름으로 접근하는 방식을 사용한다.
클러스터 외부에서 확인해야 할 때는 다음 명령으로 임시 터널을 열 수 있다.
kubectl port-forward svc/svc-1 9000:9000
ClusterIP는 외부에 노출되지 않기 때문에 안전하다. 다음과 같은 경우에 사용한다.
apiVersion: v1
kind: Service
metadata:
name: svc-1
spec:
selector:
app: pod
ports:
- port: 9000
targetPort: 8080
type: ClusterIP
apiVersion: v1
kind: Pod
metadata:
name: pod-1
labels:
app: pod
spec:
containers:
- name: container
image: tmkube/app
ports:
- containerPort: 8080
| 항목 | 설명 |
|---|---|
selector: app: pod | app: pod Label을 가진 Pod를 찾아 연결한다 |
port: 9000 | Service가 요청을 받는 포트 |
targetPort: 8080 | Pod의 컨테이너로 전달할 포트 |
type: ClusterIP | 클러스터 내부에서만 접근 가능. 생략 시 기본값 |
labels: app: pod | Service의 selector와 일치해야 연결된다 |
연결의 핵심은 Pod의 labels와 Service의 selector가 일치하는 것이다. 둘 중 하나라도 다르면 Service는 Pod를 찾지 못한다.
NodePort는 ClusterIP의 기능을 그대로 가지면서, 외부에서 접근할 수 있는 통로를 추가한다.
| ClusterIP에서 유지되는 것 | NodePort에서 추가되는 것 |
|---|---|
| Service IP가 할당된다 | 모든 Node에 동일한 포트가 열린다 |
| 클러스터 내부에서 접근 가능하다 | Node의 IP와 그 포트로 외부에서 접근 가능하다 |
| Label로 Pod를 찾아 연결한다 | 포트 범위는 30000~32767 로 제한된다 |
┌───────────────────── Cluster ─────────────────────┐
│ │
│ ┌──────────────────────────────┐ │
│ │ Service svc-2 │ │
│ │ IP 172.96.10.17 : 9000 │ │
│ └───┬──────────────────────┬───┘ │
│ │ │ │
│ ┌──────────▼─────────┐ ┌─────────▼──────────┐ │
│ │ Node1 │ │ Node2 │ │
│ │ 192.168.56.31 │ │ 192.168.56.32 │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ Pod │ │ │ │ Pod │ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │
│ │ [ 30001 ] │ │ [ 30001 ] │ │
│ └────────┬───────────┘ └─────────┬──────────┘ │
└───────────┼────────────────────────┼─────────────┘
│ │
└────────┬───────────────┘
│ ← 모든 Node에 같은 포트가 열린다
┌────────────┐
│ External │
└────────────┘
외부 접근: 192.168.56.31:30001 또는 192.168.56.32:30001
(어느 Node로 접근해도 동일하게 동작)
핵심은 "모든 Node에 Port 할당" 이다. Pod가 Node1에만 있어도 Node2의 30001로 접근할 수 있다. 요청을 받은 Node가 Pod가 있는 Node로 트래픽을 전달해 주기 때문이다.
외부 요청: 192.168.56.32 : 30001
│
│ ① Node2의 nodePort로 도착
▼
┌───────────────┐
│ Service │ ② port 9000
└───────┬───────┘
│ ③ targetPort 8080 으로 전달
▼
┌───────────────┐
│ Pod 8080 │
└───────────────┘
| 포트 | 위치 | 역할 |
|---|---|---|
nodePort | Node | 외부에서 접근하는 입구 (30000~32767) |
port | Service | Service가 받는 포트 |
targetPort | Pod | 컨테이너가 사용하는 포트 |
apiVersion: v1
kind: Service
metadata:
name: svc-2
spec:
selector:
app: pod
ports:
- port: 9000
targetPort: 8080
nodePort: 30000
type: NodePort
| 항목 | 설명 |
|---|---|
type: NodePort | 모든 Node에 포트를 열어 외부 접근을 허용한다 |
nodePort: 30000 | 열어둘 포트 번호. 30000~32767 범위만 지정 가능하다. 생략하면 범위 안에서 자동 할당된다 |
NodePort에는 트래픽 전달 방식을 정하는 옵션이 있다.
spec:
externalTrafficPolicy: Local
| 값 | 동작 |
|---|---|
Cluster (기본값) | 요청을 받은 Node에 Pod가 없으면 다른 Node의 Pod로 전달한다 |
Local | 요청을 받은 그 Node의 Pod에만 전달한다. 없으면 요청이 실패한다 |
[ Cluster ] [ Local ]
Node2로 요청 Node2로 요청
│ │
│ Node2에 Pod 없음 │ Node2에 Pod 없음
▼ ▼
Node1의 Pod로 전달 ✓ 요청 실패 ✗
(한 번 더 이동하므로 (이동이 없어 빠르고
클라이언트 IP가 사라짐) 클라이언트 IP가 유지됨)
Local은 불필요한 Node 간 이동을 없애고 클라이언트의 원래 IP를 알 수 있다는 장점이 있지만, Pod가 없는 Node로 요청이 오면 실패한다는 단점이 있다.
NodePort는 데모나 임시 연결용이며 실제 서비스 환경에는 적합하지 않다.
| 문제 | 설명 |
|---|---|
| 포트 번호가 제한된다 | 30000~32767만 사용 가능하다. 웹 서비스의 기본 포트인 80이나 443을 쓸 수 없다 |
| 주소가 지저분하다 | 사용자가 192.168.56.31:30001 형태로 접속해야 한다. 도메인을 연결하기 어렵다 |
| Node IP에 의존한다 | Node의 IP를 사용자가 알아야 한다. Node가 교체되거나 추가되면 주소가 달라진다 |
| 장애에 취약하다 | 사용자가 접속한 Node가 죽으면 접속이 끊긴다. 다른 Node로 자동 전환되지 않는다 |
| Node가 노출된다 | 클러스터를 구성하는 서버의 IP를 외부에 알려야 하므로 보안상 바람직하지 않다 |
이런 문제를 해결하기 위해 실제 서비스에서는 LoadBalancer를 사용한다.
LoadBalancer는 NodePort의 기능을 그대로 가지면서, 외부 로드밸런서를 통해 하나의 접속 주소를 제공한다.
| 특성 | 내용 |
|---|---|
| 외부 IP | 로드밸런서 전용 IP가 할당된다 |
| 포트 | 원하는 포트를 사용할 수 있다 (80, 443 등) |
| 장애 대응 | 로드밸런서가 정상 Node로만 트래픽을 보낸다 |
| 제약 | 로드밸런서를 제공하는 환경이 필요하다 |
┌────────────┐
│ External │
└──────┬─────┘
│ 접속 주소 하나만 알면 된다
▼
┌────────────────────────────┐
│ Load Balancer │ ← 클라우드가 제공
│ (외부 IP 할당) │
└──────┬──────────────┬──────┘
│ │
┌──────────────┼──────────────┼─────────────────┐
│ Cluster │ │ │
│ ┌───────────▼──────┐ ┌─────▼────────────┐ │
│ │ Node1 │ │ Node2 │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ Pod │ │ │ │ Pod │ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │
│ └──────────────────┘ └──────────────────┘ │
└───────────────────────────────────────────────┘
NodePort에서는 사용자가 Node의 IP를 직접 알아야 했지만, LoadBalancer는 로드밸런서 주소 하나만 알면 된다. 로드밸런서가 정상 동작하는 Node를 골라서 트래픽을 전달하므로, Node 하나가 죽어도 서비스가 계속된다.
apiVersion: v1
kind: Service
metadata:
name: svc-3
spec:
selector:
app: pod
ports:
- port: 9000
targetPort: 8080
type: LoadBalancer
| 항목 | 설명 |
|---|---|
type: LoadBalancer | 외부 로드밸런서를 생성하고 전용 외부 IP를 할당받는다 |
nodePort를 적지 않아도 내부적으로 자동 할당된다. LoadBalancer가 NodePort의 기능을 포함하고 있기 때문이다.
LoadBalancer는 로드밸런서를 제공해 주는 플랫폼이 있어야 동작한다. AWS, GCP, Azure 같은 클라우드 환경에서는 Service를 만드는 순간 클라우드가 로드밸런서를 자동으로 생성해 준다.
반면 개인 PC에 설치한 학습용 클러스터(minikube, kubeadm 등)에서는 로드밸런서를 만들어 줄 주체가 없다. 이 경우 Service의 상태를 조회하면 EXTERNAL-IP가 계속 <pending> 으로 남는다.
$ kubectl get svc
NAME TYPE EXTERNAL-IP PORT(S)
svc-3 LoadBalancer <pending> 9000:31234/TCP
또한 클라우드에서 LoadBalancer 타입 Service를 만들면 로드밸런서가 하나씩 생성되며, 이는 비용이 발생한다. Service를 여러 개 만들면 로드밸런서도 그만큼 생성되므로 실무에서는 Ingress를 함께 사용해 로드밸런서 하나로 여러 서비스를 처리하는 구성을 많이 쓴다.
ClusterIP NodePort LoadBalancer
───────── ──────── ────────────
┌──────────┐
│ LB │
└────┬─────┘
┌────┐ ┌────┐ ┌────┴─────┐
┌──────┐ │30001│ │30001│ │30001│30001│
│ Svc │ └──┬─┘ └──┬─┘ └──┬──┘└──┬─┘
└──┬───┘ ┌──┴────┬──┴──┐ ┌──┴────┬─┴───┐
┌──┴───┐ │ Node1 │Node2│ │ Node1 │Node2│
│ Pod │ │ Pod │ Pod │ │ Pod │ Pod │
└──────┘ └───────┴─────┘ └───────┴─────┘
내부만 접근 Node IP로 접근 전용 IP 하나로 접근
| 구분 | ClusterIP | NodePort | LoadBalancer |
|---|---|---|---|
| 외부 접근 | 불가 | 가능 | 가능 |
| 접속 주소 | Service IP (내부) | Node IP : 30000~32767 | 로드밸런서 전용 IP |
| 포트 제약 | 없음 | 30000~32767 | 없음 |
| Node 장애 시 | — | 해당 Node 접속 끊김 | 다른 Node로 자동 전환 |
| 별도 환경 필요 | 없음 | 없음 | 로드밸런서 제공 환경 필요 |
| 주요 용도 | 내부 통신, 디버깅, 대시보드 | 데모, 임시 연결 | 실제 외부 서비스 노출 |
컨테이너 안에 저장한 데이터는 컨테이너가 재시작되면 사라진다. 컨테이너는 이미지를 기반으로 매번 새로 만들어지기 때문에, 실행 중에 쓴 파일은 남지 않는다.
┌──────────────┐ ┌──────────────┐
│ Container │ │ Container │
│ │ 재시작 │ (새로 생성) │
│ /data/a.txt │ ──────▶ │ │ ← 파일 사라짐
└──────────────┘ └──────────────┘
Volume은 이 문제를 해결하기 위해 컨테이너 외부에 데이터를 저장하는 공간을 컨테이너에 연결해 주는 오브젝트다.
Volume에는 세 가지 방식이 있으며, 데이터가 얼마나 오래 살아남는지가 다르다.
| 방식 | 데이터가 유지되는 범위 |
|---|---|
| emptyDir | Pod가 살아있는 동안만 |
| hostPath | Node가 살아있는 동안 |
| PVC / PV | 클러스터와 무관하게 영구 보존 |
emptyDir hostPath PVC / PV
──────── ──────── ────────
Pod 삭제 시 Node 삭제 시 외부 저장소에 보관
사라짐 사라짐 계속 남음
[ 짧음 ] ──────────────────────────────▶ [ 김 ]
데이터 수명
emptyDir은 Pod 안에 생성되는 볼륨이다.
| 특성 | 내용 |
|---|---|
| 생성 시점 | Pod가 생성될 때 만들어진다 |
| 삭제 시점 | Pod가 삭제될 때 함께 사라진다 |
| 초기 상태 | 이름 그대로 최초에는 비어 있다 |
| 용도 | Pod 안의 컨테이너끼리 데이터를 공유한다 |
Pod와 생명주기가 같기 때문에, 영구 보관이 필요 없는 일시적 목적의 데이터를 담는다.
┌─────────────────── Pod ───────────────────┐
│ │
│ ┌──────────────────┐ │
│ ┌──▶│ Container1 │ │
│ ┌──────────┐ │ │ mountPath: │ │
│ │ Volume │──┤ │ /mount1 │ │
│ │(emptyDir)│ │ └──────────────────┘ │
│ └──────────┘ │ ┌──────────────────┐ │
│ └──▶│ Container2 │ │
│ │ mountPath: │ │
│ │ /mount2 │ │
│ └──────────────────┘ │
│ │
│ Pod 생성 시 만들어지고, 삭제 시 없어짐 │
└────────────────────────────────────────────┘
같은 Volume을 두 컨테이너가 각각 다른 경로에 마운트했다. Container1이 /mount1에 파일을 쓰면 Container2는 /mount2에서 그 파일을 읽을 수 있다. 경로 이름은 달라도 실체는 같은 저장 공간이다.
Pod가 사라지면 없어져도 괜찮은 데이터를 넣는다.
| 예시 | 설명 |
|---|---|
| 로그 파일 | 애플리케이션이 로그를 쓰고, 사이드카 컨테이너가 읽어서 외부로 전송 |
| 파일 변환 작업 | 업로드된 이미지를 한 컨테이너가 받아두고, 다른 컨테이너가 썸네일로 변환 |
| 캐시 데이터 | 다시 계산하면 되는 임시 연산 결과 |
| 컨테이너 간 신호 파일 | 초기화 완료 여부를 파일로 표시해 다른 컨테이너에 알림 |
반대로 주문 내역, 회원 정보, 업로드된 원본 파일처럼 사라지면 안 되는 데이터는 절대 emptyDir에 두면 안 된다.
apiVersion: v1
kind: Pod
metadata:
name: pod-volume-1
spec:
containers:
- name: container1
image: tmkube/init
volumeMounts:
- name: empty-dir
mountPath: /mount1
- name: container2
image: tmkube/init
volumeMounts:
- name: empty-dir
mountPath: /mount2
volumes:
- name: empty-dir
emptyDir: {}
| 항목 | 설명 |
|---|---|
volumes | Pod 수준에서 사용할 볼륨을 정의한다 |
emptyDir: {} | 빈 볼륨을 만든다. 별도 설정이 필요 없어 {} 로 비워둔다 |
volumeMounts.name | 위에서 정의한 볼륨의 이름과 일치해야 한다 |
mountPath | 컨테이너 내부에서 이 볼륨이 보일 경로 |
volumes에서 볼륨을 정의하고, volumeMounts에서 각 컨테이너에 연결하는 두 단계 구조다.
hostPath는 Pod가 올라가 있는 Node의 경로를 볼륨으로 사용한다.
| 특성 | 내용 |
|---|---|
| 저장 위치 | Node의 파일시스템 |
| 데이터 유지 | Pod가 죽어도 Node의 데이터는 사라지지 않는다 |
| 공유 범위 | 같은 Node에 있는 Pod들이 같은 데이터를 본다 |
| 제약 | Node마다 별도의 저장 공간이므로 Node가 바뀌면 데이터를 잃는다 |
┌─────────────── Node1 ────────────────┐
│ │
│ ┌──────────────┐ ┌───────────┐ │
│ │ Volume │───▶│ Pod1 │ │
│ │ /node-v1 │ │ /data │ │
│ │ │ └───────────┘ │
│ │ (Node의 │ ┌───────────┐ │
│ │ 실제 경로) │───▶│ Pod2 │ │
│ └──────────────┘ │ /data │ │
│ └───────────┘ │
└───────────────────────────────────────┘
│
│ Pod2가 죽고 Node2에 재생성되면?
▼
┌─────────────── Node2 ────────────────┐
│ │
│ ┌──────────────┐ ┌───────────┐ │
│ │ Volume │───▶│ Pod2 │ │
│ │ /node-v1 │ │ /data │ │
│ │ │ └───────────┘ │
│ │ ← 비어 있음 │ │
│ └──────────────┘ │
└───────────────────────────────────────┘
Node1의 데이터는 볼 수 없다
이것이 hostPath의 가장 큰 함정이다. Pod가 재생성될 때 다른 Node에 배치되면, 그 Node의 경로에는 이전 데이터가 없다. 같은 경로 이름을 썼더라도 물리적으로 다른 디스크이기 때문이다.
또한 Node를 추가할 때마다 해당 경로를 직접 만들고 마운트를 걸어줘야 한다. Node가 늘어날수록 관리 부담이 커진다.
hostPath는 애플리케이션 데이터 저장용이 아니라, Node 자체의 정보에 접근하기 위해 사용한다.
| 예시 | 설명 |
|---|---|
| Node 로그 수집 | /var/log 를 마운트해 Node의 시스템 로그를 읽음 |
| Node 모니터링 | /proc, /sys 를 마운트해 CPU·메모리 사용량 수집 |
| 컨테이너 런타임 접근 | /var/run/docker.sock 등에 접근 |
| Node 설정 파일 참조 | Node에 있는 인증서나 설정 파일을 읽음 |
이런 작업은 Node마다 하나씩 Pod를 띄우는 방식(DaemonSet) 과 함께 쓰인다. 모든 Node에 로그 수집기를 하나씩 배치하는 구성이 대표적이다.
apiVersion: v1
kind: Pod
metadata:
name: pod-volume-2
spec:
containers:
- name: container
image: tmkube/init
volumeMounts:
- name: host-path
mountPath: /mount1
volumes:
- name: host-path
hostPath:
path: /node-v
type: Directory
| 항목 | 설명 |
|---|---|
hostPath.path | 볼륨으로 사용할 Node의 실제 경로 |
type: Directory | 해당 경로가 디렉터리로 이미 존재해야 한다는 의미 |
type: Directory로 지정하면 사전에 그 경로가 Node에 없을 경우 Pod가 생성되지 않는다. 경로가 없을 때 자동으로 만들려면 DirectoryOrCreate를 사용한다.
emptyDir은 Pod와 함께 사라지고, hostPath는 Node에 묶여 있다. 데이터베이스 데이터처럼 어떤 상황에도 사라지면 안 되는 데이터는 두 방식 모두 적합하지 않다.
PV와 PVC는 클러스터 외부의 저장소(NFS, AWS EBS, iSCSI 등)를 연결해 영속성 있는 볼륨을 제공한다.
emptyDir hostPath PV / PVC
──────── ──────── ────────
Pod 안에 저장 Node에 저장 외부 저장소에 저장
│ │ │
Pod 삭제 → 소멸 Node 변경 → 소멸 Pod·Node와 무관하게 보존
PV와 PVC가 나뉘어 있는 이유는 관리자와 사용자의 역할을 분리하기 위해서다.
User (개발자) Admin (인프라 관리자)
─────────────────────── ────────────────────────────
"1Gi 저장 공간이 필요해요" "실제 저장소를 준비해뒀습니다"
│ │
PVC PV
(요청서 / 신청서) (실제 저장소 연결 정보)
| 오브젝트 | 담당 | 역할 |
|---|---|---|
| PV (PersistentVolume) | Admin | 실제 저장소를 클러스터에 등록한다. 용량, 접근 방식, 저장소 위치를 정의 |
| PVC (PersistentVolumeClaim) | User | 필요한 용량과 조건을 요청한다. 실제 저장소가 무엇인지 몰라도 된다 |
개발자는 저장소가 NFS인지 AWS EBS인지 알 필요 없이 "1Gi가 필요하다"고 요청만 하면 된다. 저장소 종류가 바뀌어도 PVC는 수정할 필요가 없다.
┌────────────── User ──────────────┐ ┌───────────── Admin ─────────────┐
│ │ │ │
│ │ │ ┌────────────────────┐ │
│ │ │ │ PersistentVolume │ ┌─────┐│
│ │ │ │ pv-01 │──│ 실제││
│ │ │ │ 용량 1Gi │ │저장소││
│ │ │ └─────────┬──────────┘ │ NFS ││
│ │ │ │ │ AWS ││
│ ┌──────────────────┐ │ │ │ │iSCSI││
│ │ PersistentVolume │◀─────┼───┼────────────┘ └─────┘│
│ │ Claim pvc-01 │ ③ │ │ ┌────────────────────┐ │
│ │ 요청 1Gi │ │ │ │ PersistentVolume │ │
│ └────────┬─────────┘ │ │ │ pv-02 │ │
│ │ ② │ │ └────────────────────┘ │
│ ┌────────▼─────────┐ │ │ ① │
│ │ Pod │ │ │ │
│ │ PVC 마운트 ④ │ │ │ │
│ └──────────────────┘ │ │ │
└───────────────────────────────────┘ └──────────────────────────────────┘
① PV 정의 생성 (Admin)
관리자가 실제 저장소를 PV로 등록한다. "1Gi 짜리 저장 공간이 이 위치에 있다"는 정보다.
② PVC 생성 (User)
개발자가 "1Gi가 필요하다"는 요청서를 만든다.
③ PV 연결 (자동)
쿠버네티스가 조건에 맞는 PV를 찾아 PVC와 연결한다. 이 연결을 바인딩(Binding) 이라고 한다. 조건에 맞는 PV가 없으면 PVC는 Pending 상태로 대기한다.
④ Pod 생성 시 PVC 마운트 (User)
Pod에서 PVC를 볼륨으로 지정하면, 연결된 PV의 실제 저장소를 사용하게 된다.
Pod는 PV를 직접 참조하지 않고 항상 PVC를 통해서만 접근한다.
사라지면 안 되는 모든 데이터를 넣는다.
| 예시 | 설명 |
|---|---|
| 데이터베이스 데이터 | MySQL, PostgreSQL의 실제 데이터 파일 |
| 사용자 업로드 파일 | 프로필 이미지, 첨부 문서 |
| 메시지 큐 데이터 | Kafka, RabbitMQ의 저장 데이터 |
| 애플리케이션 영구 로그 | 감사(audit) 목적으로 보존해야 하는 기록 |
PV — 관리자가 저장소를 등록
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-01
spec:
capacity:
storage: 1G
accessModes:
- ReadWriteOnce
local:
path: /node-v
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- {key: kubernetes.io/hostname, operator: In, values: [node1]}
| 항목 | 설명 |
|---|---|
capacity.storage | 이 PV가 제공하는 용량 |
accessModes | 접근 방식 (아래 표 참고) |
local.path | 저장소의 실제 경로 |
nodeAffinity | 이 PV를 사용하는 Pod가 해당 Node에 만들어지도록 지정 |
local 타입은 특정 Node의 디스크를 사용하므로, 그 Node에서만 접근 가능하다. 그래서 nodeAffinity로 Pod가 반드시 node1에 배치되도록 강제한다.
PVC — 사용자가 저장 공간을 요청
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-01
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1G
storageClassName: ""
| 항목 | 설명 |
|---|---|
resources.requests.storage | 요청하는 용량 |
accessModes | 요청하는 접근 방식. PV와 일치해야 연결된다 |
storageClassName: "" | 자동 생성을 사용하지 않고, 이미 만들어진 PV와 연결한다는 의미 |
Pod — PVC를 볼륨으로 사용
apiVersion: v1
kind: Pod
metadata:
name: pod-volume-3
spec:
containers:
- name: container
image: tmkube/init
volumeMounts:
- name: pvc-pv
mountPath: /volume
volumes:
- name: pvc-pv
persistentVolumeClaim:
claimName: pvc-01
| 항목 | 설명 |
|---|---|
persistentVolumeClaim.claimName | 사용할 PVC의 이름 |
PV와 PVC를 연결할 때 반드시 일치해야 하는 값이다.
| 값 | 의미 |
|---|---|
ReadWriteOnce (RWO) | 하나의 Node에서 읽기·쓰기 가능 |
ReadOnlyMany (ROX) | 여러 Node에서 읽기만 가능 |
ReadWriteMany (RWX) | 여러 Node에서 읽기·쓰기 가능 |
ReadWriteOnce가 가장 일반적이며, 데이터베이스처럼 하나의 Pod만 데이터를 쓰는 경우에 사용한다. 여러 Pod가 동시에 같은 파일에 써야 한다면 ReadWriteMany를 지원하는 저장소(NFS 등)가 필요하다.
emptyDir hostPath PVC / PV
──────── ──────── ────────
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Pod │ │ Node │ │ Pod │
│ ┌──────┐ │ │ ┌────────┐ │ │ │ │
│ │Volume│ │ │ │ Pod │ │ │ PVC │
│ └──────┘ │ │ └───┬────┘ │ │ │ │
└──────────┘ │ ┌───▼────┐ │ │ PV │
│ │ /path │ │ └──────┼───────┘
│ └────────┘ │ ▼
└──────────────┘ ┌──────────────┐
│ 외부 저장소 │
│ NFS / AWS │
└──────────────┘
| 구분 | emptyDir | hostPath | PVC / PV |
|---|---|---|---|
| 저장 위치 | Pod 내부 | Node의 파일시스템 | 외부 저장소 |
| Pod 삭제 시 | 데이터 소멸 | 데이터 유지 | 데이터 유지 |
| Node 변경 시 | — | 데이터 접근 불가 | 데이터 유지 |
| 설정 난이도 | 매우 간단 | 간단 | PV·PVC를 각각 정의해야 함 |
| 관리 주체 | 개발자 | 개발자 | Admin(PV) + User(PVC) |
| 주요 용도 | 컨테이너 간 임시 데이터 공유 | Node 로그·모니터링 정보 접근 | DB, 업로드 파일 등 영구 데이터 |
선택 기준은 다음과 같이 정리할 수 있다.
같은 애플리케이션이라도 개발 환경과 운영 환경에서 설정 값이 달라야 하는 경우가 많다.
| 설정 항목 | Dev | Production |
|---|---|---|
| SSH | False | True |
| User | Dev | Prod |
| Key | LS0tLs.. | MII3Ld.. |
이 값들을 컨테이너 이미지 안에 넣어두면 어떻게 될까.
┌──────────────────────┐ ┌──────────────────────┐
│ Container for Dev │ │ Container for Prod │
│ ┌────────────────┐ │ │ ┌────────────────┐ │
│ │ A Service │ │ │ │ A Service │ │
│ │ SSH : False │ │ │ │ SSH : True │ │
│ │ User : Dev │ │ │ │ User : Prod │ │
│ │ Key : LS0tLs.│ │ │ │ Key : MII3Ld.│ │
│ └────────────────┘ │ │ └────────────────┘ │
└──────────────────────┘ └──────────────────────┘
환경마다 이미지를 따로 만들어야 한다
이 방식에는 두 가지 문제가 있다.
이미지는 값을 비워둔 채 하나만 만들고, 실제 값은 배포 시점에 외부에서 주입한다.
┌──────────────────────┐
│ Container (하나) │
│ ┌────────────────┐ │
│ │ A Service │ │
│ │ SSH : [ ] │ │ ← 값이 비어 있음
│ │ User : [ ] │ │
│ │ Key : [ ] │ │
│ └────────────────┘ │
└──────────────────────┘
│
┌───────┴────────┐
▼ ▼
[ Dev 값 주입 ] [ Prod 값 주입 ]
이때 값을 담아두는 오브젝트가 ConfigMap과 Secret이다.
| 구분 | ConfigMap | Secret |
|---|---|---|
| 담는 값 | 일반 상수 | 보안 관리가 필요한 값 |
| 예시 | SSH 여부, 사용자명, 포트, 로그 레벨, API 주소 | 비밀번호, 인증서, API 키, DB 접속 정보 |
| 저장 형식 | 평문 | Base64 인코딩 |
| 저장 위치 | 일반 저장 | 메모리(tmpfs)에 저장 |
┌──────────── Dev ────────────┐ ┌──────── Production ─────────┐
│ │ │ │
│ ┌────────────┐ │ │ ┌────────────┐ │
│ │ ConfigMap │ │ │ │ ConfigMap │ │
│ │ SSH : False│──┐ │ │ │ SSH : True │──┐ │
│ │ User: Dev │ │ │ │ │ User: Prod │ │ │
│ └────────────┘ │ ┌──────┐ │ │ └────────────┘ │ ┌──────┐ │
│ ├─▶│ Pod │ │ │ ├─▶│ Pod │ │
│ ┌────────────┐ │ │ Env │ │ │ ┌────────────┐ │ │ Env │ │
│ │ Secret │ │ └──────┘ │ │ │ Secret │ │ └──────┘ │
│ │ Key:LS0tLs.│──┘ │ │ │ Key:MII3Ld.│──┘ │
│ └────────────┘ │ │ └────────────┘ │
└──────────────────────────────┘ └──────────────────────────────┘
같은 이미지 + 다른 ConfigMap/Secret = 다른 동작
Pod를 생성할 때 ConfigMap과 Secret의 값을 가져와 컨테이너의 환경변수에 세팅한다. 이미지는 하나지만 어떤 ConfigMap을 붙이느냐에 따라 Dev용 Pod가 되기도 하고 Production용 Pod가 되기도 한다.
Secret은 Node의 디스크가 아니라 메모리(tmpfs) 에 저장된다. 보안상의 이유 때문이다.
[ 디스크에 저장한다면 ] [ 메모리에 저장하면 ]
┌──────────────────┐ ┌──────────────────┐
│ Node 디스크 │ │ Node 메모리 │
│ password=1234 │ │ password=1234 │
└──────────────────┘ └──────────────────┘
│ │
Node 종료 후에도 파일이 남음 Node 종료 시 함께 사라짐
디스크를 확보하면 값을 볼 수 있음 흔적이 남지 않음
| 이유 | 설명 |
|---|---|
| 흔적이 남지 않는다 | 디스크에 쓰면 파일 삭제 후에도 복구가 가능하다. 메모리는 전원이 꺼지면 내용이 사라진다 |
| 물리적 유출에 대비한다 | 디스크를 물리적으로 확보당해도 Secret 값을 읽을 수 없다 |
| 백업에 포함되지 않는다 | 디스크 백업이나 스냅샷을 뜰 때 Secret 값이 함께 복사되지 않는다 |
| 사용 범위를 최소화한다 | Secret은 그 값을 필요로 하는 Pod가 있는 Node에만 전달되고, Pod가 삭제되면 즉시 제거된다 |
Secret에는 크기 제한이 있다. 하나의 Secret은 1MByte를 넘을 수 없다. 메모리에 저장되기 때문에, 큰 데이터를 담으면 Node의 메모리를 잠식할 수 있어서다.
Secret에 값을 넣을 때는 Base64로 인코딩해서 넣는다.
원본 값 Base64 인코딩 Secret에 저장
"1234" ──────▶ "MTIzNA==" ──────▶ data.Key
│
│ Pod에 주입 시
│ 자동 디코딩
▼
컨테이너 환경변수
PW = 1234
컨테이너에 전달될 때는 쿠버네티스가 자동으로 디코딩하므로, 애플리케이션은 원본 값을 그대로 받는다.
여기서 반드시 알아야 할 점이 있다. Base64는 암호화가 아니다. 누구나 되돌릴 수 있는 단순 변환이다.
$ echo "MTIzNA==" | base64 --decode
1234
Base64를 쓰는 이유는 보안이 아니라, 인증서처럼 줄바꿈이나 특수문자가 섞인 값을 YAML에 안전하게 담기 위해서다. 따라서 Secret이 담긴 YAML 파일을 Git에 그대로 올리면 안 된다. 실무에서는 별도의 비밀 관리 도구(Sealed Secrets, Vault 등)를 함께 사용한다.
ConfigMap과 Secret의 값을 컨테이너에 전달하는 방법은 세 가지다.
① Env (Literal) ② Env (File) ③ Volume Mount (File)
─────────────── ──────────── ─────────────────────
상수 값을 파일 내용을 파일을 파일 그대로
환경변수로 환경변수로 컨테이너에 마운트
SSH = False file = (파일 내용) /mount/file.txt
가장 기본적인 방식이다. ConfigMap과 Secret에 key: value 형태의 상수를 넣고, 이를 컨테이너의 환경변수로 주입한다.
┌─────────────────┐ ┌─────────────────┐
│ ConfigMap │ │ Secret │
│ cm-dev │ │ sec-dev │
│ │ │ │
│ Key : Value │ │ Key : Value │
│ SSH : False │ │ PW : Base64 │ ← 메모리 1Mbyte
│ user : dev │ │ │
└────────┬────────┘ └────────┬────────┘
│ │
│ │ Decoding
▼ ▼
┌──────────────────────────────────────────┐
│ Pod │
│ ┌────────────────────────────────────┐ │
│ │ Container │ │
│ │ env │ │
│ │ SSH : False │ │
│ │ user : dev │ │
│ │ PW : 1234 ← 자동 디코딩됨 │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
ConfigMap의 값은 그대로 전달되고, Secret의 값은 Base64에서 원본으로 디코딩되어 전달된다.
ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-dev
data:
SSH: "false"
User: dev
Secret
apiVersion: v1
kind: Secret
metadata:
name: sec-dev
data:
Key: MTIzNA==
Pod
apiVersion: v1
kind: Pod
metadata:
name: pod-1
spec:
containers:
- name: container
image: tmkube/init
envFrom:
- configMapRef:
name: cm-dev
- secretRef:
name: sec-dev
| 항목 | 설명 |
|---|---|
data | key: value 형태의 값 목록 |
envFrom | ConfigMap이나 Secret의 모든 key를 한 번에 환경변수로 가져온다 |
configMapRef.name | 가져올 ConfigMap의 이름 |
secretRef.name | 가져올 Secret의 이름 |
envFrom을 쓰면 안에 있는 key가 전부 환경변수가 된다. 특정 key만 골라서 가져오고 싶다면 env를 사용한다.
애플리케이션에서 다음과 같이 값을 읽는다.
// Spring Boot
@Value("${SSH}")
private boolean sshEnabled;
애플리케이션 코드는 이 값이 ConfigMap에서 왔는지 Secret에서 왔는지 알 필요가 없다. 그냥 환경변수를 읽을 뿐이다.
설정 값이 길거나 이미 파일로 존재하는 경우, 파일 자체를 ConfigMap에 넣을 수 있다. 이때 파일 이름이 key가 되고, 파일 안의 내용이 value가 된다.
file.txt
┌──────────────────┐
│ Content │ ConfigMap에 넣으면
│ (파일 내용) │ ──────────────────────▶ key : file.txt
└──────────────────┘ value : Content
▲ ▲
파일 이름 파일 내용
│ │
key value
file.txt
┌──────────────────┐
│ Content │
└────┬────────┬────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ ConfigMap │ │ Secret │
│ │ │ │
│ Key : Value │ │ Key : Value │
│ file.txt : │ │ file.txt : │
│ Content │ │ Base64 │
└──────┬──────┘ └──────┬──────┘
│ │ Decoding
▼ ▼
┌──────────────────────────────┐
│ Pod │
│ ┌────────────────────────┐ │
│ │ Container │ │
│ │ env │ │
│ │ file : Content │ │
│ └────────────────────────┘ │
└───────────────────────────────┘
이 방식은 YAML로 직접 작성하지 않고 명령어로 생성한다. 파일 내용을 사람이 옮겨 적을 필요가 없다.
# ConfigMap 생성
kubectl create configmap cm-file --from-file=./file.txt
# Secret 생성 (Base64 인코딩은 자동으로 처리됨)
kubectl create secret generic sec-file --from-file=./file.txt
--from-file 옵션을 쓰면 파일 이름이 key가 되고 내용이 value가 된다. Secret은 인코딩까지 자동으로 해준다.
apiVersion: v1
kind: Pod
metadata:
name: file
spec:
containers:
- name: container
image: tmkube/init
env:
- name: file
valueFrom:
configMapKeyRef:
name: cm-file
key: file.txt
| 항목 | 설명 |
|---|---|
env.name | 컨테이너에서 사용할 환경변수 이름 |
valueFrom | 값을 다른 오브젝트에서 가져온다는 의미 |
configMapKeyRef.name | 가져올 ConfigMap의 이름 |
configMapKeyRef.key | ConfigMap 안에서 가져올 key. 여기서는 파일 이름 |
앞의 envFrom이 모든 key를 가져왔다면, env + valueFrom은 특정 key 하나만 골라서 원하는 이름의 환경변수로 만든다. 위 예시에서는 file.txt라는 key의 값을 file이라는 환경변수로 받는다.
환경변수는 컨테이너가 시작될 때 한 번 주입되고 끝난다. ConfigMap의 값을 나중에 수정해도 이미 실행 중인 컨테이너의 환경변수는 바뀌지 않는다. 새 값을 반영하려면 Pod를 재생성해야 한다.
ConfigMap의 내용을 환경변수가 아니라 파일 형태 그대로 컨테이너 안에 넣는 방식이다. Volume에서 배운 마운트 방식을 그대로 사용한다.
file.txt
┌──────────────────┐
│ Content │
└────┬────────┬────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ ConfigMap │ │ Secret │
│ file.txt : │ │ file.txt : │
│ Content │ │ Base64 │
└──────┬──────┘ └──────┬──────┘
│ │ Decoding
▼ ▼
┌──────────────────────────────────┐
│ Pod │
│ ┌────────────────────────────┐ │
│ │ Container │ │
│ │ │ │
│ │ /mount │ │ ← mountPath
│ │ └── file.txt │ │ ← key가 파일명이 된다
│ │ Content │ │ ← value가 파일 내용이 된다
│ └────────────────────────────┘ │
└───────────────────────────────────┘
마운트한 경로 아래에 key 이름의 파일이 생성되고, 그 파일 안에 value가 들어간다.
apiVersion: v1
kind: Pod
metadata:
name: mount
spec:
containers:
- name: container
image: tmkube/init
volumeMounts:
- name: file-volume
mountPath: /mount
volumes:
- name: file-volume
configMap:
name: cm-file
| 항목 | 설명 |
|---|---|
volumes.configMap.name | 볼륨으로 사용할 ConfigMap의 이름 |
mountPath | 컨테이너 안에서 파일이 놓일 경로 |
Volume에서 emptyDir이나 hostPath를 쓰던 자리에 configMap을 넣는 구조다. Secret을 마운트할 때는 secret: secretName: sec-file 형태로 작성한다.
컨테이너 안에서 확인하면 다음과 같다.
$ ls /mount
file.txt
$ cat /mount/file.txt
Content
| 예시 | 설명 |
|---|---|
| 애플리케이션 설정 파일 | application.yml, nginx.conf 처럼 파일로 읽어야 하는 설정 |
| 인증서 | TLS 인증서와 키 파일 |
| 초기화 스크립트 | 컨테이너 시작 시 실행할 스크립트 파일 |
환경변수로는 여러 줄로 된 설정 파일을 다루기 어렵다. 이런 경우 Volume Mount 방식이 적합하다.
Volume Mount 방식은 ConfigMap을 수정하면 마운트된 파일도 자동으로 갱신된다. Pod를 재생성하지 않아도 된다.
[ Env 방식 ] [ Volume Mount 방식 ]
ConfigMap 수정 ConfigMap 수정
│ │
▼ ▼
컨테이너 환경변수 그대로 마운트된 파일 자동 갱신
│ │
Pod 재생성 필요 Pod 유지 가능
다만 파일 내용이 바뀌어도 애플리케이션이 자동으로 다시 읽어들이지는 않는다. 애플리케이션이 파일 변경을 감지해 재로딩하는 기능을 가지고 있어야 실제로 반영된다.
| 구분 | Env (Literal) | Env (File) | Volume Mount (File) |
|---|---|---|---|
| 전달 형태 | 환경변수 | 환경변수 | 파일 |
| 값의 출처 | YAML에 직접 작성한 상수 | 파일 이름(key) + 파일 내용(value) | 파일 이름(key) + 파일 내용(value) |
| 생성 방법 | YAML 작성 | kubectl create --from-file | kubectl create --from-file |
| 값 수정 시 | Pod 재생성 필요 | Pod 재생성 필요 | 파일 자동 갱신 |
| 적합한 경우 | 짧은 설정 값 | 파일 내용을 변수로 쓸 때 | 설정 파일, 인증서 |
선택 기준
쿠버네티스 클러스터가 사용할 수 있는 자원(Memory, CPU)은 정해져 있다. 그런데 아무런 제약이 없다면 한 팀이 자원을 전부 가져가는 상황이 발생할 수 있다.
┌──────────────── Kubernetes Cluster ────────────────┐
│ │
│ Memory ┌─┐┌─┐┌─┐┌─┐┌─┐ │
│ CPU └─┘└─┘└─┘└─┘└─┘ ← 클러스터 전체 자원 │
│ │ │
│ │ A팀이 전부 사용 │
│ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ A팀 Pod │ │ B팀 Pod │ │
│ │ 전체 자원 점유 │ │ 자원 부족 ✗ │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
한쪽에서 자원을 독점하면 다른 쪽은 Pod를 아예 생성하지 못한다. 개발 환경의 테스트 Pod가 메모리를 다 써버려서 운영 서비스가 배포되지 못하는 상황이 벌어질 수 있다.
이를 방지하기 위해 세 가지 오브젝트를 사용한다.
| 오브젝트 | 역할 | 비유하자면 |
|---|---|---|
| Namespace | 클러스터를 논리적으로 나눈다 | 자원을 나눠 쓸 구역을 만든다 |
| ResourceQuota | Namespace가 쓸 수 있는 총량을 제한한다 | 구역 전체의 한도를 정한다 |
| LimitRange | Namespace에 들어오는 Pod 하나의 크기를 제한한다 | 구역에 들어올 수 있는 개별 크기를 정한다 |
┌─────────────────── Kubernetes Cluster ───────────────────┐
│ │
│ Memory ┌─┐┌─┐┌─┐┌─┐┌─┐ │
│ CPU └─┘└─┘└─┘└─┘└─┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────────────────────────┐ │
│ │Resource│ │Resource│ │ ResourceQuota LimitRange │ │
│ │ Quota │ │ Quota │ │ │ │
│ │┌──────┐│ │┌──────┐│ │ ┌────────────┐ │ │
│ ││ Pod ││ ││ Pod ││ │ │ │ ┌─────┐ │ │
│ ││ Pod ││ ││ Pod ││ │ │ Pod │◀──│ Pod │ │ │
│ │└──────┘│ │└──────┘│ │ │ (큰 Pod) │ └─────┘ │ │
│ │Namespace│ │Namespace│ │ └────────────┘ ┌─────┐ │ │
│ └────────┘ └────────┘ │ 크기 초과 ✗ │ Pod │ │ │
│ │ └─────┘ │ │
│ │ Namespace │ │
│ └────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
ResourceQuota는 Namespace가 쓸 수 있는 총합을 막고, LimitRange는 Namespace에 들어오려는 Pod 하나하나의 크기를 막는다.
두 오브젝트 모두 Namespace에 설정하는 오브젝트다. 클러스터 전체에 한 번에 적용하는 방법은 없으며, 자원을 통제하려는 Namespace마다 각각 만들어야 한다.
Namespace는 하나의 클러스터를 여러 개의 논리적인 공간으로 나누는 오브젝트다. 물리적으로 서버를 나누는 것이 아니라, 같은 클러스터 안에서 오브젝트를 그룹으로 구분하는 것이다.
| 용도 | 예시 |
|---|---|
| 환경 분리 | dev, staging, production |
| 팀 분리 | team-a, team-b |
| 서비스 분리 | frontend, backend, monitoring |
같은 Namespace 안에서는 오브젝트 이름이 유일해야 한다. 반대로 말하면, Namespace가 다르면 같은 이름을 써도 된다.
┌──── Namespace-1 ────┐ ┌──── Namespace-2 ────┐
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ Pod1 │ │ │ │ Pod1 │ │
│ │ nm: pod1 │ │ │ │ nm: pod1 │ │
│ └──────────────┘ │ │ └──────────────┘ │
│ │ │ │
│ 같은 이름을 또 만들면 ✗ │ │ 다른 Namespace라 가능 ✓ │
└──────────────────────┘ └──────────────────────┘
dev 네임스페이스와 production 네임스페이스에 각각 api-server라는 이름의 Pod를 둘 수 있다. 이름을 dev-api-server, prod-api-server처럼 구분해서 지을 필요가 없다.
Service의 selector는 같은 Namespace 안의 Pod만 선택한다. Label이 완전히 같아도 Namespace가 다르면 연결되지 않는다.
┌──── Namespace-1 ────┐ ┌──── Namespace-2 ────┐
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ Pod1 │ │ │ │ Service1 │ │
│ │ nm: pod1 │◀──┼──✗───┼───│ selector: │ │
│ │ │ │ │ │ nm: pod1 │ │
│ └──────────────┘ │ │ └──────────────┘ │
│ │ │ │
└──────────────────────┘ └──────────────────────┘
Label이 같아도 Namespace가 다르면 선택되지 않는다
다만 이것은 오브젝트 연결이 안 된다는 의미이지, 네트워크가 차단된다는 의미는 아니다. Pod의 IP를 직접 알고 있다면 다른 Namespace의 Pod에도 접근할 수 있다.
Namespace-2의 Pod ──── 10.2.3.12 로 직접 호출 ────▶ Namespace-1의 Pod
접근 가능 ✓
네트워크까지 차단하려면 NetworkPolicy라는 별도의 오브젝트가 필요하다.
모든 오브젝트가 Namespace에 속하는 것은 아니다.
| 구분 | 오브젝트 |
|---|---|
| Namespace에 속함 | Pod, Service, ConfigMap, Secret, PVC, Deployment |
| 클러스터 전체에 속함 | Node, PersistentVolume(PV) |
┌──────────────┐ ┌──────────────┐
│ PV │ │ Node │ ← Namespace에 속하지 않음
└──────┬───────┘ └──────┬───────┘
│ │
┌─────────┴──────────────────┴─────────┐
│ │
┌────▼─────────────┐ ┌──────────▼───────┐
│ Namespace-1 │ │ Namespace-2 │
│ Pod, Service │ │ Pod, Service │
└──────────────────┘ └──────────────────┘
Node는 물리적인 서버이고 PV는 실제 저장소이므로, 논리적 구분인 Namespace에 종속될 수 없다. 여러 Namespace의 Pod가 같은 Node 위에서 실행된다.
Namespace를 지우면 그 안에 있던 Pod, Service, ConfigMap이 전부 함께 삭제된다. 정리할 때는 편리하지만 실수하면 되돌릴 수 없으므로 주의해야 한다.
Namespace 생성
apiVersion: v1
kind: Namespace
metadata:
name: nm-1
Pod를 특정 Namespace에 생성
apiVersion: v1
kind: Pod
metadata:
name: pod-1
namespace: nm-1
labels:
nm: pod1
spec:
containers:
- name: container
image: tmkube/init
| 항목 | 설명 |
|---|---|
metadata.namespace | 이 오브젝트가 속할 Namespace. 생략하면 default에 생성된다 |
다른 Namespace의 Service
apiVersion: v1
kind: Service
metadata:
name: svc-1
namespace: nm-2
spec:
selector:
nm: pod1
ports:
- port: 8080
이 Service는 nm-2에 있으므로 nm-1에 있는 Pod를 선택하지 못한다.
# Namespace 목록 조회
kubectl get namespace
# 특정 Namespace의 Pod 조회
kubectl get pods -n nm-1
# 모든 Namespace의 Pod 조회
kubectl get pods --all-namespaces
-n 옵션을 생략하면 default Namespace를 조회한다. Pod를 만들었는데 목록에 안 보인다면 Namespace를 지정하지 않아서인 경우가 많다.
ResourceQuota는 하나의 Namespace가 사용할 수 있는 자원의 총량을 제한한다.
┌────────────────── Namespace (nm-1) ──────────────────┐
│ │
│ ResourceQuota │
│ requests.memory : 3Gi ← 이 Namespace의 총 한도 │
│ limits.memory : 6Gi │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Pod1 │ │ Pod3 │ │
│ │ requests 2Gi │ │ requests 2Gi │ ← 합계 4Gi │
│ │ limits 4Gi │ │ limits 2Gi │ 한도 3Gi 초과│
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ✗ 생성 거부 │
└───────────────────────────────────────────────────────┘
Pod1이 이미 2Gi를 요청한 상태에서 Pod3가 2Gi를 추가로 요청하면 합계가 4Gi가 되어 한도인 3Gi를 넘는다. 이 경우 Pod3는 생성되지 않는다.
ResourceQuota는 자원의 양뿐 아니라 오브젝트의 개수도 제한할 수 있다.
| 구분 | 항목 |
|---|---|
| Compute Resource | cpu, memory, storage |
| Objects Count | Pod, Service, ConfigMap, PVC, Secret 등의 개수 |
Pod 개수를 10개로 제한해두면 그 Namespace에서는 11번째 Pod를 만들 수 없다.
ResourceQuota가 설정된 Namespace에서는 모든 Pod가 requests와 limits를 반드시 작성해야 한다.
┌──────────────┐
│ Pod2 │
│ │ ← resources 항목이 없음
│ (설정 없음) │
└──────┬───────┘
│
✗ 생성 거부
이유는 단순하다. ResourceQuota는 "현재 사용량의 합계"를 계산해서 한도와 비교하는데, Pod가 자기 사용량을 밝히지 않으면 계산이 불가능하기 때문이다.
이 제약 때문에 실무에서는 ResourceQuota를 걸어두면 개발자가 resources를 깜빡해 Pod 생성이 실패하는 일이 자주 발생한다. 뒤에 나오는 LimitRange의 default 설정으로 이 문제를 해결할 수 있다.
ResourceQuota
apiVersion: v1
kind: ResourceQuota
metadata:
name: rq-1
namespace: nm-1
spec:
hard:
requests.memory: 3Gi
limits.memory: 6Gi
| 항목 | 설명 |
|---|---|
namespace | 이 제한을 적용할 Namespace |
hard | 넘을 수 없는 한도. Namespace 전체의 합계 기준 |
requests.memory | Namespace 안 모든 Pod의 requests 합계 한도 |
limits.memory | Namespace 안 모든 Pod의 limits 합계 한도 |
Pod
apiVersion: v1
kind: Pod
metadata:
name: pod-2
namespace: nm-1
spec:
containers:
- name: container
image: tmkube/app
resources:
requests:
memory: 2Gi
limits:
memory: 2Gi
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
services: "5"
persistentvolumeclaims: "3"
개발 팀에 이런 Quota를 설정하면, 그 팀은 CPU 4코어와 메모리 8Gi 안에서 최대 20개의 Pod를 운영할 수 있다. 아무리 많이 배포해도 다른 팀의 자원을 침범할 수 없다.
ResourceQuota가 Namespace 전체 합계를 막는다면, LimitRange는 Pod 하나하나의 크기를 검사한다.
┌──────────────┐ LimitRange
│ Pod1 │ ┌──────────────────────┐
│ limits 5Gi │────✗──▶ Namespace │ min : 1Gi │
└──────────────┘ │ max : 4Gi │
│ maxLimitRequestRatio │
┌──────────────┐ │ : 3 │
│ Pod2 │ └──────────────────────┘
│ requests 1Gi │────✗──▶ Namespace
│ limits 4Gi │
└──────────────┘
┌──────────────┐
│ Pod3 │────✓──▶ Namespace
│ requests 1Gi │ 들어감
│ limits 2Gi │
└──────────────┘
Namespace에 들어오려는 Pod를 문 앞에서 검사해, 조건에 맞지 않으면 들여보내지 않는 방식이다.
| 항목 | 의미 |
|---|---|
min | Pod가 요청할 수 있는 최소 크기 |
max | Pod가 요청할 수 있는 최대 크기 |
maxLimitRequestRatio | limits ÷ requests 의 최대 비율 |
Pod1 — max 초과
limits: 5Gi > max: 4Gi ✗ 거부
한 Pod가 너무 큰 자원을 가져가는 것을 막는다.
Pod2 — 비율 초과
limits 4Gi ÷ requests 1Gi = 4 > maxLimitRequestRatio: 3 ✗ 거부
이 항목은 처음 보면 이해하기 어려우므로 의미를 짚고 넘어가는 것이 좋다.
requests는 스케줄러가 Node를 고를 때 쓰는 값이고, limits는 실제로 쓸 수 있는 상한이다. 둘의 차이가 너무 크면 문제가 생긴다.
requests 1Gi 로 Node에 배치됨
│
│ 스케줄러는 이 Pod가 1Gi만 쓸 것으로 예상
▼
실제로는 limits 4Gi 까지 사용
│
▼
Node의 메모리가 예상보다 빨리 고갈됨
→ 같은 Node의 다른 Pod가 영향을 받음
maxLimitRequestRatio: 3은 "실제 사용량이 신고한 값의 3배를 넘지 못하게 하라"는 뜻이다. 신고한 값과 실제 사용량의 격차를 줄여 Node의 자원 예측을 정확하게 만든다.
Pod3 — 통과
min 1Gi ≤ requests 1Gi ≤ max 4Gi ✓
min 1Gi ≤ limits 2Gi ≤ max 4Gi ✓
limits 2Gi ÷ requests 1Gi = 2 ≤ 3 ✓
LimitRange에는 검사 외에 값을 대신 넣어주는 기능도 있다.
┌──────────────┐ LimitRange
│ Pod3 │ ┌──────────────────────┐
│ (resources │────▶ Namespace ────│ defaultRequest │
│ 미작성) │ │ memory : 1Gi │
└──────────────┘ │ default │
│ memory : 2Gi │
│ └──────────────────────┘
▼
┌──────────────┐
│ Pod3 │
│ requests 1Gi │ ← 자동으로 채워짐
│ limits 2Gi │
└──────────────┘
| 항목 | 설명 |
|---|---|
defaultRequest | Pod가 requests를 작성하지 않았을 때 자동으로 넣어줄 값 |
default | Pod가 limits를 작성하지 않았을 때 자동으로 넣어줄 값 |
앞서 ResourceQuota를 걸면 모든 Pod가 resources를 반드시 작성해야 한다고 했다. LimitRange의 이 설정을 함께 걸어두면, 개발자가 작성하지 않아도 기본값이 자동으로 들어가므로 Pod 생성이 실패하지 않는다. 두 오브젝트를 함께 사용하는 이유가 여기에 있다.
apiVersion: v1
kind: LimitRange
metadata:
name: lr-1
namespace: nm-1
spec:
limits:
- type: Container
min:
memory: 1Gi
max:
memory: 4Gi
defaultRequest:
memory: 1Gi
default:
memory: 2Gi
maxLimitRequestRatio:
memory: 3
| 항목 | 설명 |
|---|---|
type: Container | 검사 대상. Container 단위로 검사한다 |
min / max | 허용되는 크기의 하한과 상한 |
defaultRequest / default | 값이 없을 때 자동으로 넣어줄 requests와 limits |
maxLimitRequestRatio | limits ÷ requests 의 최대 비율 |
type에는 Container 외에 Pod(Pod 안 컨테이너들의 합계 기준)와 PersistentVolumeClaim(요청 가능한 저장 용량 기준)도 지정할 수 있다.
Namespace ResourceQuota LimitRange
───────── ───────────── ──────────
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ 총합 3Gi │ │ Pod 하나당 │
│ 구역을 │ │ ┌────┐┌────┐│ │ 1Gi ~ 4Gi │
│ 나눈다 │ │ │Pod ││Pod ││ │ │
│ │ │ └────┘└────┘│ ✗◀──│ [ 5Gi Pod ] │
└──────────┘ │ 합계 초과 ✗ │ └──────────────┘
└──────────────┘
논리적 분리 총량 제한 개별 크기 제한
| 구분 | Namespace | ResourceQuota | LimitRange |
|---|---|---|---|
| 역할 | 클러스터를 논리적으로 분리 | Namespace의 자원 총량 제한 | Pod 하나의 크기 제한 |
| 검사 기준 | — | Namespace 안 모든 Pod의 합계 | Pod(Container) 개별 값 |
| 적용 범위 | 클러스터 | Namespace | Namespace |
| 제한 대상 | — | cpu, memory, storage, 오브젝트 개수 | min, max, 비율 |
| 값 자동 설정 | — | 없음 | default, defaultRequest 제공 |
| 위반 시 | — | Pod 생성 거부 | Pod 생성 거부 |
함께 사용하는 이유
세 오브젝트는 하나씩 쓰기보다 조합해서 쓸 때 효과가 있다.