Kubernetes Object

HamJina·2026년 9월 9일

Kubernetes

목록 보기
2/3

2. Kubernetes Object

2-1. Pod

Pod는 쿠버네티스에서 애플리케이션을 배포하는 가장 작은 단위다. Pod를 이해하려면 Container, Label, Node Schedule 세 가지를 알아야 한다.


2-1-1. Container

구조

┌──────────── Pod (IP: 10.244.1.7) ────────────┐
│                                               │
│   ┌────────────────┐   ┌────────────────┐    │
│   │  container1    │   │  container2    │    │
│   │  port 8000     │   │  port 8080     │    │
│   └────────────────┘   └────────────────┘    │
│                                               │
│      하나의 IP를 공유 → 포트는 달라야 한다    │
└───────────────────────────────────────────────┘

포트

각 컨테이너는 Service가 연결될 수 있도록 포트를 가진다. Service는 이 포트를 통해 컨테이너에 트래픽을 전달한다.

Pod 안의 컨테이너들은 하나의 IP를 공유하므로 같은 Pod 안에서 동일한 포트를 사용할 수 없다. 두 컨테이너가 모두 8080을 사용하면 포트 충돌이 발생한다. 위 구조처럼 8000과 8080으로 다르게 지정해야 한다.

Pod의 IP

특성내용
할당 방식Pod 생성 시 자동으로 할당된다
변경 여부Pod가 재생성되면 IP가 변경된다
접근 범위클러스터 내부에서만 이 IP로 접근할 수 있다
외부 접근클러스터 외부에서는 이 IP로 접근할 수 없다

Pod IP는 고정되지 않으므로 다른 애플리케이션이 Pod IP를 직접 지정해 통신하면 안 된다. 이 문제를 해결하기 위해 뒤에서 다룰 Service를 사용한다.

YAML

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.namePod의 이름. 같은 네임스페이스 안에서 중복될 수 없다
spec.containersPod 안에서 실행할 컨테이너 목록. 여러 개를 정의할 수 있다
image컨테이너가 실행할 이미지
containerPort컨테이너가 사용하는 포트. Pod 안에서 중복될 수 없다

더 알아두면 좋은 것

containerPort는 실제로 포트를 열어주는 설정이 아니다. 애플리케이션이 어떤 포트를 사용하는지 명시하는 문서 역할에 가깝다. 실제 포트는 컨테이너 내부의 애플리케이션이 연다. 다만 이 값을 적어두면 Service를 연결하거나 다른 사람이 YAML을 볼 때 구조를 파악하기 쉬워지므로 작성하는 것이 좋다.


2-1-2. Label

개념

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)

Selector로 골라내기

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만 선택된다.

Label을 붙이는 이유

이유설명
연결 대상 지정Service가 어떤 Pod에 트래픽을 보낼지 Label로 결정한다
환경 분리개발용과 운영용 Pod를 섞이지 않게 관리할 수 있다
일괄 조회kubectl get pods -l lo=dev 처럼 조건에 맞는 Pod만 조회할 수 있다
확장성Pod가 늘어나도 Label만 맞으면 Service가 자동으로 인식한다

마지막 항목이 특히 중요하다. Service는 특정 Pod를 이름으로 기억하지 않는다. 조건에 맞는 Pod를 그때그때 찾아서 연결한다. 그래서 Pod가 재생성되어 IP가 바뀌어도, 새 Pod에 같은 Label만 붙어 있으면 Service는 문제없이 동작한다.

YAML

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.labelsPod에 붙는 Label. key: value 형태로 여러 개 지정 가능
spec.selectorService가 연결할 Pod의 조건. 지정한 Label을 모두 가진 Pod가 선택된다
ports.portService가 사용하는 포트

더 알아두면 좋은 것

Label과 비슷하게 생겼지만 목적이 다른 Annotation이 있다. Label은 오브젝트를 선택하기 위한 정보이고, Annotation은 선택에는 쓰이지 않는 부가 정보를 기록하는 용도다. 배포 일시, 담당자, 관련 문서 링크 같은 것을 Annotation에 적는다.


2-1-3. Node Schedule

개념

Pod는 반드시 어느 한 Node 위에서 실행된다. 어떤 Node에서 실행할지 결정하는 과정을 Node Schedule이라고 한다.

방법은 두 가지다.

방법설명
직접 지정사용자가 특정 Node를 지목한다
스케줄러 판단쿠버네티스 스케줄러가 상황을 보고 결정한다

방법 1 — 직접 지정 (nodeSelector)

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
항목설명
nodeSelectorPod를 배치할 Node의 조건. 해당 Label을 가진 Node에만 배치된다

주의할 점: 조건에 맞는 Node가 없거나, 그 Node에 자원이 부족하면 Pod는 배치되지 못하고 Pending 상태로 대기한다. 다른 Node로 알아서 옮겨가지 않는다.

사용하는 경우: GPU가 달린 Node에서만 실행해야 하거나, SSD가 장착된 Node에 데이터베이스를 배치해야 하는 경우처럼 특정 Node의 조건이 필요할 때 사용한다.

방법 2 — 스케줄러 판단 (resources)

Node를 지정하지 않으면 스케줄러가 결정한다. 이때 판단 기준이 되는 것이 resources다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-4
spec:
  containers:
    - name: container
      image: tmkube/init
      resources:
        requests:
          memory: 2Gi
        limits:
          memory: 3Gi
항목설명
requestsPod가 최소한 필요로 하는 자원. 스케줄러가 Node를 고를 때 이 값을 기준으로 삼는다
limitsPod가 최대로 사용할 수 있는 자원. 실행 중 이 값을 넘지 못하도록 제한한다

스케줄러의 판단 과정

Pod-4가 requests: memory 2Gi를 요구하는 상황이다. 클러스터에 Node가 세 개 있다.

  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
  │   Node1      │  │   Node2      │  │   Node3      │
  │              │  │              │  │              │
  │ 전체 8Gi     │  │ 전체 8Gi     │  │ 전체 8Gi     │
  │ 사용중 7Gi   │  │ 사용중 2Gi   │  │ 사용중 5Gi   │
  │ 여유  1Gi    │  │ 여유  6Gi    │  │ 여유  3Gi    │
  └──────────────┘  └──────────────┘  └──────────────┘
        ✗                 ✓                 ✓
     2Gi 불가          배치 가능         배치 가능

1단계 — 필터링 (Filtering)

배치가 불가능한 Node를 먼저 제외한다.

  • Node1: 여유 메모리가 1Gi뿐이라 2Gi를 확보할 수 없다 → 제외
  • Node2, Node3: 조건 충족 → 후보로 남는다

2단계 — 스코어링 (Scoring)

남은 후보 중 가장 적합한 Node를 점수로 고른다. 자원 여유가 많은 Node가 높은 점수를 받는다.

  • Node2: 여유 6Gi → 높은 점수
  • Node3: 여유 3Gi → 낮은 점수

3단계 — 배치

점수가 가장 높은 Node2에 Pod-4를 배치한다.

  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
  │   Node1      │  │   Node2      │  │   Node3      │
  │              │  │  [Pod-4]     │  │              │
  │ 여유  1Gi    │  │ 여유  4Gi    │  │ 여유  3Gi    │
  └──────────────┘  └──────────────┘  └──────────────┘
                        ↑ 배치됨

이렇게 스케줄러는 자원 여유가 부족한 Node를 걸러내고, 남은 Node 중 가장 여유 있는 곳에 Pod를 배치해 클러스터 전체의 자원 사용을 고르게 유지한다.

requests와 limits의 동작 차이

두 값의 역할이 다르므로 구분해서 이해해야 한다.

requestslimits
시점Pod를 배치할 때Pod가 실행 중일 때
역할스케줄러가 Node를 고르는 기준자원 사용량의 상한
초과 시메모리 초과 시 컨테이너 종료 후 재시작

메모리가 limits를 초과하면 컨테이너가 강제 종료된다. 이 상태를 OOMKilled(Out Of Memory Killed)라고 한다. CPU는 초과해도 종료되지 않고 사용량이 제한되기만 한다.

더 알아두면 좋은 것

nodeSelector는 조건이 맞지 않으면 Pod가 배치되지 않는 단순하고 엄격한 방식이다. 실무에서는 "가능하면 이 Node에, 안 되면 다른 Node에라도" 처럼 유연한 조건이 필요한 경우가 많다. 이런 조건은 Node Affinity로 표현하며, 이후 단계에서 다루는 개념이다. 처음에는 nodeSelector로 Node 지정의 개념을 이해하고, 이후에 Affinity로 확장해 나가면 된다.

2-2. Service

Service란 무엇이고 왜 필요한가

문제 상황 — Pod IP는 신뢰할 수 없다

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는 사용자가 직접 삭제하지 않는 한 삭제되거나 재생성되지 않는다. 따라서 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가 여러 개면 요청을 나눠서 전달한다

Service의 종류

종류접근 범위주요 용도
ClusterIP클러스터 내부만내부 통신, 디버깅, 대시보드
NodePort클러스터 외부 가능 (Node IP)데모, 임시 연결
LoadBalancer클러스터 외부 가능 (전용 IP)실제 외부 서비스 노출

세 가지는 별개의 기능이 아니라 누적되는 구조다. NodePort는 ClusterIP의 기능을 포함하고, LoadBalancer는 NodePort의 기능을 포함한다.

  ┌───────────────────────────────────────────┐
  │  LoadBalancer                             │
  │  ┌─────────────────────────────────────┐  │
  │  │  NodePort                           │  │
  │  │  ┌───────────────────────────────┐  │  │
  │  │  │  ClusterIP                    │  │  │
  │  │  │  (기본 - 내부 접근)           │  │  │
  │  │  └───────────────────────────────┘  │  │
  │  │  + 모든 Node에 포트 개방            │  │
  │  └─────────────────────────────────────┘  │
  │  + 외부 로드밸런서와 전용 IP              │
  └───────────────────────────────────────────┘

2-2-1. ClusterIP

특징

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        │
        └───────────────┘
항목의미
portService가 열어두는 포트. 요청을 받는 입구
targetPortPod의 컨테이너가 사용하는 포트. 요청을 전달할 목적지

Service IP로 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는 외부에 노출되지 않기 때문에 안전하다. 다음과 같은 경우에 사용한다.

  • 내부 트래픽: 백엔드 애플리케이션이 데이터베이스나 캐시 서버에 접근할 때
  • 서비스 디버깅: 개발자가 특정 Pod의 동작을 확인할 때
  • 대시보드, 모니터링: 내부 관리 도구에 접근할 때
  • 인증된 사용자 연결: 별도의 인증을 거친 사용자만 내부 경로로 연결할 때

YAML

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: podapp: pod Label을 가진 Pod를 찾아 연결한다
port: 9000Service가 요청을 받는 포트
targetPort: 8080Pod의 컨테이너로 전달할 포트
type: ClusterIP클러스터 내부에서만 접근 가능. 생략 시 기본값
labels: app: podService의 selector와 일치해야 연결된다

연결의 핵심은 Pod의 labels와 Service의 selector가 일치하는 것이다. 둘 중 하나라도 다르면 Service는 Pod를 찾지 못한다.


2-2-2. NodePort

특징

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   │
            └───────────────┘
포트위치역할
nodePortNode외부에서 접근하는 입구 (30000~32767)
portServiceService가 받는 포트
targetPortPod컨테이너가 사용하는 포트

YAML

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 범위만 지정 가능하다. 생략하면 범위 안에서 자동 할당된다

externalTrafficPolicy

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를 사용한다.


2-2-3. LoadBalancer

특징

LoadBalancer는 NodePort의 기능을 그대로 가지면서, 외부 로드밸런서를 통해 하나의 접속 주소를 제공한다.

특성내용
외부 IP로드밸런서 전용 IP가 할당된다
포트원하는 포트를 사용할 수 있다 (80, 443 등)
장애 대응로드밸런서가 정상 Node로만 트래픽을 보낸다
제약로드밸런서를 제공하는 환경이 필요하다

구조

              ┌────────────┐
              │  External  │
              └──────┬─────┘
                     │  접속 주소 하나만 알면 된다
                     ▼
        ┌────────────────────────────┐
        │      Load Balancer         │  ← 클라우드가 제공
        │      (외부 IP 할당)        │
        └──────┬──────────────┬──────┘
               │              │
┌──────────────┼──────────────┼─────────────────┐
│  Cluster     │              │                 │
│  ┌───────────▼──────┐ ┌─────▼────────────┐   │
│  │ Node1            │ │ Node2            │   │
│  │  ┌──────────┐    │ │  ┌──────────┐    │   │
│  │  │   Pod    │    │ │  │   Pod    │    │   │
│  │  └──────────┘    │ │  └──────────┘    │   │
│  └──────────────────┘ └──────────────────┘   │
└───────────────────────────────────────────────┘

NodePort에서는 사용자가 Node의 IP를 직접 알아야 했지만, LoadBalancer는 로드밸런서 주소 하나만 알면 된다. 로드밸런서가 정상 동작하는 Node를 골라서 트래픽을 전달하므로, Node 하나가 죽어도 서비스가 계속된다.

YAML

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를 함께 사용해 로드밸런서 하나로 여러 서비스를 처리하는 구성을 많이 쓴다.


세 가지 Service 비교

   ClusterIP              NodePort                LoadBalancer
   ─────────              ────────                ────────────
                                                  ┌──────────┐
                                                  │    LB    │
                                                  └────┬─────┘
                          ┌────┐  ┌────┐          ┌────┴─────┐
     ┌──────┐             │30001│ │30001│          │30001│30001│
     │ Svc  │             └──┬─┘  └──┬─┘          └──┬──┘└──┬─┘
     └──┬───┘             ┌──┴────┬──┴──┐          ┌──┴────┬─┴───┐
     ┌──┴───┐             │ Node1 │Node2│          │ Node1 │Node2│
     │ Pod  │             │  Pod  │ Pod │          │  Pod  │ Pod │
     └──────┘             └───────┴─────┘          └───────┴─────┘

   내부만 접근           Node IP로 접근          전용 IP 하나로 접근
구분ClusterIPNodePortLoadBalancer
외부 접근불가가능가능
접속 주소Service IP (내부)Node IP : 30000~32767로드밸런서 전용 IP
포트 제약없음30000~32767없음
Node 장애 시해당 Node 접속 끊김다른 Node로 자동 전환
별도 환경 필요없음없음로드밸런서 제공 환경 필요
주요 용도내부 통신, 디버깅, 대시보드데모, 임시 연결실제 외부 서비스 노출

2-3. Volume

Volume이란 무엇이고 왜 필요한가

컨테이너 안에 저장한 데이터는 컨테이너가 재시작되면 사라진다. 컨테이너는 이미지를 기반으로 매번 새로 만들어지기 때문에, 실행 중에 쓴 파일은 남지 않는다.

   ┌──────────────┐          ┌──────────────┐
   │ Container    │          │ Container    │
   │              │  재시작  │  (새로 생성) │
   │ /data/a.txt  │  ──────▶ │              │  ← 파일 사라짐
   └──────────────┘          └──────────────┘

Volume은 이 문제를 해결하기 위해 컨테이너 외부에 데이터를 저장하는 공간을 컨테이너에 연결해 주는 오브젝트다.

Volume에는 세 가지 방식이 있으며, 데이터가 얼마나 오래 살아남는지가 다르다.

방식데이터가 유지되는 범위
emptyDirPod가 살아있는 동안만
hostPathNode가 살아있는 동안
PVC / PV클러스터와 무관하게 영구 보존
   emptyDir          hostPath           PVC / PV
   ────────          ────────           ────────
   Pod 삭제 시       Node 삭제 시       외부 저장소에 보관
   사라짐            사라짐             계속 남음

   [ 짧음 ] ──────────────────────────────▶ [ 김 ]
                    데이터 수명

2-3-1. emptyDir

특징

emptyDir은 Pod 안에 생성되는 볼륨이다.

특성내용
생성 시점Pod가 생성될 때 만들어진다
삭제 시점Pod가 삭제될 때 함께 사라진다
초기 상태이름 그대로 최초에는 비어 있다
용도Pod 안의 컨테이너끼리 데이터를 공유한다

Pod와 생명주기가 같기 때문에, 영구 보관이 필요 없는 일시적 목적의 데이터를 담는다.

구조

┌─────────────────── Pod ───────────────────┐
│                                            │
│                    ┌──────────────────┐   │
│                ┌──▶│  Container1      │   │
│  ┌──────────┐  │   │  mountPath:      │   │
│  │  Volume  │──┤   │   /mount1        │   │
│  │(emptyDir)│  │   └──────────────────┘   │
│  └──────────┘  │   ┌──────────────────┐   │
│                └──▶│  Container2      │   │
│                    │  mountPath:      │   │
│                    │   /mount2        │   │
│                    └──────────────────┘   │
│                                            │
│   Pod 생성 시 만들어지고, 삭제 시 없어짐   │
└────────────────────────────────────────────┘

같은 Volume을 두 컨테이너가 각각 다른 경로에 마운트했다. Container1이 /mount1에 파일을 쓰면 Container2는 /mount2에서 그 파일을 읽을 수 있다. 경로 이름은 달라도 실체는 같은 저장 공간이다.

어떤 데이터를 넣는가

Pod가 사라지면 없어져도 괜찮은 데이터를 넣는다.

예시설명
로그 파일애플리케이션이 로그를 쓰고, 사이드카 컨테이너가 읽어서 외부로 전송
파일 변환 작업업로드된 이미지를 한 컨테이너가 받아두고, 다른 컨테이너가 썸네일로 변환
캐시 데이터다시 계산하면 되는 임시 연산 결과
컨테이너 간 신호 파일초기화 완료 여부를 파일로 표시해 다른 컨테이너에 알림

반대로 주문 내역, 회원 정보, 업로드된 원본 파일처럼 사라지면 안 되는 데이터는 절대 emptyDir에 두면 안 된다.

YAML

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: {}
항목설명
volumesPod 수준에서 사용할 볼륨을 정의한다
emptyDir: {}빈 볼륨을 만든다. 별도 설정이 필요 없어 {} 로 비워둔다
volumeMounts.name위에서 정의한 볼륨의 이름과 일치해야 한다
mountPath컨테이너 내부에서 이 볼륨이 보일 경로

volumes에서 볼륨을 정의하고, volumeMounts에서 각 컨테이너에 연결하는 두 단계 구조다.


2-3-2. hostPath

특징

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에 로그 수집기를 하나씩 배치하는 구성이 대표적이다.

YAML

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를 사용한다.


2-3-3. PVC / PV

왜 필요한가

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) 목적으로 보존해야 하는 기록

YAML

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의 이름

accessModes

PV와 PVC를 연결할 때 반드시 일치해야 하는 값이다.

의미
ReadWriteOnce (RWO)하나의 Node에서 읽기·쓰기 가능
ReadOnlyMany (ROX)여러 Node에서 읽기만 가능
ReadWriteMany (RWX)여러 Node에서 읽기·쓰기 가능

ReadWriteOnce가 가장 일반적이며, 데이터베이스처럼 하나의 Pod만 데이터를 쓰는 경우에 사용한다. 여러 Pod가 동시에 같은 파일에 써야 한다면 ReadWriteMany를 지원하는 저장소(NFS 등)가 필요하다.


세 가지 Volume 비교

   emptyDir                hostPath                 PVC / PV
   ────────                ────────                 ────────
  ┌──────────┐          ┌──────────────┐        ┌──────────────┐
  │   Pod    │          │    Node      │        │    Pod       │
  │ ┌──────┐ │          │  ┌────────┐  │        │      │       │
  │ │Volume│ │          │  │  Pod   │  │        │     PVC      │
  │ └──────┘ │          │  └───┬────┘  │        │      │       │
  └──────────┘          │  ┌───▼────┐  │        │      PV      │
                        │  │ /path  │  │        └──────┼───────┘
                        │  └────────┘  │               ▼
                        └──────────────┘        ┌──────────────┐
                                                │ 외부 저장소  │
                                                │ NFS / AWS    │
                                                └──────────────┘
구분emptyDirhostPathPVC / PV
저장 위치Pod 내부Node의 파일시스템외부 저장소
Pod 삭제 시데이터 소멸데이터 유지데이터 유지
Node 변경 시데이터 접근 불가데이터 유지
설정 난이도매우 간단간단PV·PVC를 각각 정의해야 함
관리 주체개발자개발자Admin(PV) + User(PVC)
주요 용도컨테이너 간 임시 데이터 공유Node 로그·모니터링 정보 접근DB, 업로드 파일 등 영구 데이터

선택 기준은 다음과 같이 정리할 수 있다.

  • 데이터가 Pod와 함께 사라져도 되는가 → emptyDir
  • Node 자체의 파일에 접근해야 하는가 → hostPath
  • 데이터가 절대 사라지면 안 되는가 → PVC / PV

2-4. ConfigMap과 Secret

왜 필요한가

문제 상황 — 환경마다 값이 다르다

같은 애플리케이션이라도 개발 환경과 운영 환경에서 설정 값이 달라야 하는 경우가 많다.

설정 항목DevProduction
SSHFalseTrue
UserDevProd
KeyLS0tLs..MII3Ld..

이 값들을 컨테이너 이미지 안에 넣어두면 어떻게 될까.

   ┌──────────────────────┐    ┌──────────────────────┐
   │ Container for Dev    │    │ Container for Prod   │
   │  ┌────────────────┐  │    │  ┌────────────────┐  │
   │  │  A Service     │  │    │  │  A Service     │  │
   │  │  SSH  : False  │  │    │  │  SSH  : True   │  │
   │  │  User : Dev    │  │    │  │  User : Prod   │  │
   │  │  Key  : LS0tLs.│  │    │  │  Key  : MII3Ld.│  │
   │  └────────────────┘  │    │  └────────────────┘  │
   └──────────────────────┘    └──────────────────────┘

        환경마다 이미지를 따로 만들어야 한다

이 방식에는 두 가지 문제가 있다.

  • 이미지가 환경 수만큼 늘어난다. 개발·스테이징·운영이 있다면 이미지가 3개다. 코드는 완전히 같은데 설정만 다른 이미지를 각각 관리해야 한다.
  • 개발 환경에서 검증한 이미지가 운영에 그대로 가지 않는다. 운영용 이미지는 별도로 빌드된 것이므로, 테스트한 것과 다른 이미지가 배포된다.

해결 — 설정을 이미지 밖으로 분리한다

이미지는 값을 비워둔 채 하나만 만들고, 실제 값은 배포 시점에 외부에서 주입한다.

   ┌──────────────────────┐
   │  Container (하나)    │
   │  ┌────────────────┐  │
   │  │  A Service     │  │
   │  │  SSH  : [    ] │  │  ← 값이 비어 있음
   │  │  User : [    ] │  │
   │  │  Key  : [    ] │  │
   │  └────────────────┘  │
   └──────────────────────┘
              │
      ┌───────┴────────┐
      ▼                ▼
   [ Dev 값 주입 ]  [ Prod 값 주입 ]

이때 값을 담아두는 오브젝트가 ConfigMapSecret이다.

ConfigMap과 Secret의 구분

구분ConfigMapSecret
담는 값일반 상수보안 관리가 필요한 값
예시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이 메모리에 저장되는 이유

Secret은 Node의 디스크가 아니라 메모리(tmpfs) 에 저장된다. 보안상의 이유 때문이다.

   [ 디스크에 저장한다면 ]              [ 메모리에 저장하면 ]

   ┌──────────────────┐               ┌──────────────────┐
   │  Node 디스크     │               │  Node 메모리     │
   │  password=1234   │               │  password=1234   │
   └──────────────────┘               └──────────────────┘
            │                                   │
   Node 종료 후에도 파일이 남음          Node 종료 시 함께 사라짐
   디스크를 확보하면 값을 볼 수 있음     흔적이 남지 않음
이유설명
흔적이 남지 않는다디스크에 쓰면 파일 삭제 후에도 복구가 가능하다. 메모리는 전원이 꺼지면 내용이 사라진다
물리적 유출에 대비한다디스크를 물리적으로 확보당해도 Secret 값을 읽을 수 없다
백업에 포함되지 않는다디스크 백업이나 스냅샷을 뜰 때 Secret 값이 함께 복사되지 않는다
사용 범위를 최소화한다Secret은 그 값을 필요로 하는 Pod가 있는 Node에만 전달되고, Pod가 삭제되면 즉시 제거된다

Secret에는 크기 제한이 있다. 하나의 Secret은 1MByte를 넘을 수 없다. 메모리에 저장되기 때문에, 큰 데이터를 담으면 Node의 메모리를 잠식할 수 있어서다.

Base64 인코딩

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

2-4-1. Env (Literal) — 상수를 환경변수로

개념

가장 기본적인 방식이다. 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에서 원본으로 디코딩되어 전달된다.

YAML

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
항목설명
datakey: value 형태의 값 목록
envFromConfigMap이나 Secret의 모든 key를 한 번에 환경변수로 가져온다
configMapRef.name가져올 ConfigMap의 이름
secretRef.name가져올 Secret의 이름

envFrom을 쓰면 안에 있는 key가 전부 환경변수가 된다. 특정 key만 골라서 가져오고 싶다면 env를 사용한다.

사용 예시

애플리케이션에서 다음과 같이 값을 읽는다.

// Spring Boot
@Value("${SSH}")
private boolean sshEnabled;

애플리케이션 코드는 이 값이 ConfigMap에서 왔는지 Secret에서 왔는지 알 필요가 없다. 그냥 환경변수를 읽을 뿐이다.


2-4-2. Env (File) — 파일 내용을 환경변수로

개념

설정 값이 길거나 이미 파일로 존재하는 경우, 파일 자체를 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은 인코딩까지 자동으로 해준다.

YAML

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.keyConfigMap 안에서 가져올 key. 여기서는 파일 이름

앞의 envFrom이 모든 key를 가져왔다면, env + valueFrom특정 key 하나만 골라서 원하는 이름의 환경변수로 만든다. 위 예시에서는 file.txt라는 key의 값을 file이라는 환경변수로 받는다.

한계

환경변수는 컨테이너가 시작될 때 한 번 주입되고 끝난다. ConfigMap의 값을 나중에 수정해도 이미 실행 중인 컨테이너의 환경변수는 바뀌지 않는다. 새 값을 반영하려면 Pod를 재생성해야 한다.


2-4-3. Volume Mount (File) — 파일을 파일로 마운트

개념

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가 들어간다.

YAML

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-filekubectl create --from-file
값 수정 시Pod 재생성 필요Pod 재생성 필요파일 자동 갱신
적합한 경우짧은 설정 값파일 내용을 변수로 쓸 때설정 파일, 인증서

선택 기준

  • 값이 짧고 단순한 상수인가 → Env (Literal)
  • 파일에 있는 값을 환경변수로 받고 싶은가 → Env (File)
  • 애플리케이션이 파일 형태로 읽어야 하는가 → Volume Mount (File)

2-5. Namespace, ResourceQuota, LimitRange

왜 필요한가

문제 상황 — 자원 독점

쿠버네티스 클러스터가 사용할 수 있는 자원(Memory, CPU)은 정해져 있다. 그런데 아무런 제약이 없다면 한 팀이 자원을 전부 가져가는 상황이 발생할 수 있다.

┌──────────────── Kubernetes Cluster ────────────────┐
│                                                     │
│   Memory  ┌─┐┌─┐┌─┐┌─┐┌─┐                          │
│   CPU     └─┘└─┘└─┘└─┘└─┘   ← 클러스터 전체 자원   │
│              │                                      │
│              │ A팀이 전부 사용                      │
│              ▼                                      │
│   ┌──────────────────┐      ┌──────────────────┐  │
│   │  A팀 Pod         │      │  B팀 Pod         │  │
│   │  전체 자원 점유  │      │  자원 부족 ✗     │  │
│   └──────────────────┘      └──────────────────┘  │
│                                                     │
└─────────────────────────────────────────────────────┘

한쪽에서 자원을 독점하면 다른 쪽은 Pod를 아예 생성하지 못한다. 개발 환경의 테스트 Pod가 메모리를 다 써버려서 운영 서비스가 배포되지 못하는 상황이 벌어질 수 있다.

이를 방지하기 위해 세 가지 오브젝트를 사용한다.

오브젝트역할비유하자면
Namespace클러스터를 논리적으로 나눈다자원을 나눠 쓸 구역을 만든다
ResourceQuotaNamespace가 쓸 수 있는 총량을 제한한다구역 전체의 한도를 정한다
LimitRangeNamespace에 들어오는 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마다 각각 만들어야 한다.


2-5-1. Namespace

개념

Namespace는 하나의 클러스터를 여러 개의 논리적인 공간으로 나누는 오브젝트다. 물리적으로 서버를 나누는 것이 아니라, 같은 클러스터 안에서 오브젝트를 그룹으로 구분하는 것이다.

용도예시
환경 분리dev, staging, production
팀 분리team-a, team-b
서비스 분리frontend, backend, monitoring

특성 1 — 이름의 중복 허용

같은 Namespace 안에서는 오브젝트 이름이 유일해야 한다. 반대로 말하면, Namespace가 다르면 같은 이름을 써도 된다.

┌──── Namespace-1 ────┐      ┌──── Namespace-2 ────┐
│                      │      │                      │
│   ┌──────────────┐   │      │   ┌──────────────┐   │
│   │   Pod1       │   │      │   │   Pod1       │   │
│   │   nm: pod1   │   │      │   │   nm: pod1   │   │
│   └──────────────┘   │      │   └──────────────┘   │
│                      │      │                      │
│   같은 이름을 또 만들면 ✗  │      │  다른 Namespace라 가능 ✓ │
└──────────────────────┘      └──────────────────────┘

dev 네임스페이스와 production 네임스페이스에 각각 api-server라는 이름의 Pod를 둘 수 있다. 이름을 dev-api-server, prod-api-server처럼 구분해서 지을 필요가 없다.

특성 2 — 다른 Namespace의 자원과 연결되지 않는다

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라는 별도의 오브젝트가 필요하다.

특성 3 — Namespace에 속하지 않는 오브젝트가 있다

모든 오브젝트가 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 위에서 실행된다.

특성 4 — Namespace를 삭제하면 안의 오브젝트가 모두 삭제된다

Namespace를 지우면 그 안에 있던 Pod, Service, ConfigMap이 전부 함께 삭제된다. 정리할 때는 편리하지만 실수하면 되돌릴 수 없으므로 주의해야 한다.

YAML

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를 지정하지 않아서인 경우가 많다.


2-5-2. ResourceQuota

개념

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 Resourcecpu, memory, storage
Objects CountPod, Service, ConfigMap, PVC, Secret 등의 개수

Pod 개수를 10개로 제한해두면 그 Namespace에서는 11번째 Pod를 만들 수 없다.

중요한 제약 — Pod에 반드시 resources를 명시해야 한다

ResourceQuota가 설정된 Namespace에서는 모든 Pod가 requests와 limits를 반드시 작성해야 한다.

   ┌──────────────┐
   │ Pod2         │
   │              │  ← resources 항목이 없음
   │ (설정 없음)  │
   └──────┬───────┘
          │
          ✗ 생성 거부

이유는 단순하다. ResourceQuota는 "현재 사용량의 합계"를 계산해서 한도와 비교하는데, Pod가 자기 사용량을 밝히지 않으면 계산이 불가능하기 때문이다.

이 제약 때문에 실무에서는 ResourceQuota를 걸어두면 개발자가 resources를 깜빡해 Pod 생성이 실패하는 일이 자주 발생한다. 뒤에 나오는 LimitRange의 default 설정으로 이 문제를 해결할 수 있다.

YAML

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.memoryNamespace 안 모든 Pod의 requests 합계 한도
limits.memoryNamespace 안 모든 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를 운영할 수 있다. 아무리 많이 배포해도 다른 팀의 자원을 침범할 수 없다.


2-5-3. LimitRange

개념

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를 문 앞에서 검사해, 조건에 맞지 않으면 들여보내지 않는 방식이다.

검사 항목

항목의미
minPod가 요청할 수 있는 최소 크기
maxPod가 요청할 수 있는 최대 크기
maxLimitRequestRatiolimits ÷ requests 의 최대 비율

각 Pod가 거부되는 이유

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     ✓

default와 defaultRequest — 값을 자동으로 채워준다

LimitRange에는 검사 외에 값을 대신 넣어주는 기능도 있다.

   ┌──────────────┐                    LimitRange
   │ Pod3         │                    ┌──────────────────────┐
   │ (resources   │────▶ Namespace ────│ defaultRequest       │
   │  미작성)     │                    │   memory : 1Gi       │
   └──────────────┘                    │ default              │
                                       │   memory : 2Gi       │
          │                            └──────────────────────┘
          ▼
   ┌──────────────┐
   │ Pod3         │
   │ requests 1Gi │  ← 자동으로 채워짐
   │ limits   2Gi │
   └──────────────┘
항목설명
defaultRequestPod가 requests를 작성하지 않았을 때 자동으로 넣어줄 값
defaultPod가 limits를 작성하지 않았을 때 자동으로 넣어줄 값

앞서 ResourceQuota를 걸면 모든 Pod가 resources를 반드시 작성해야 한다고 했다. LimitRange의 이 설정을 함께 걸어두면, 개발자가 작성하지 않아도 기본값이 자동으로 들어가므로 Pod 생성이 실패하지 않는다. 두 오브젝트를 함께 사용하는 이유가 여기에 있다.

YAML

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
maxLimitRequestRatiolimits ÷ requests 의 최대 비율

type에는 Container 외에 Pod(Pod 안 컨테이너들의 합계 기준)와 PersistentVolumeClaim(요청 가능한 저장 용량 기준)도 지정할 수 있다.


세 오브젝트 비교

   Namespace              ResourceQuota           LimitRange
   ─────────              ─────────────           ──────────
   ┌──────────┐          ┌──────────────┐        ┌──────────────┐
   │          │          │  총합 3Gi    │        │ Pod 하나당   │
   │  구역을  │          │  ┌────┐┌────┐│        │ 1Gi ~ 4Gi    │
   │  나눈다  │          │  │Pod ││Pod ││        │              │
   │          │          │  └────┘└────┘│   ✗◀──│ [ 5Gi Pod ]  │
   └──────────┘          │  합계 초과 ✗ │        └──────────────┘
                         └──────────────┘

   논리적 분리            총량 제한               개별 크기 제한
구분NamespaceResourceQuotaLimitRange
역할클러스터를 논리적으로 분리Namespace의 자원 총량 제한Pod 하나의 크기 제한
검사 기준Namespace 안 모든 Pod의 합계Pod(Container) 개별 값
적용 범위클러스터NamespaceNamespace
제한 대상cpu, memory, storage, 오브젝트 개수min, max, 비율
값 자동 설정없음default, defaultRequest 제공
위반 시Pod 생성 거부Pod 생성 거부

함께 사용하는 이유

세 오브젝트는 하나씩 쓰기보다 조합해서 쓸 때 효과가 있다.

  • Namespace로 팀이나 환경별 구역을 나눈다
  • ResourceQuota로 각 구역이 클러스터 자원을 독점하지 못하게 막는다
  • LimitRange로 구역 안에서 하나의 Pod가 지나치게 커지는 것을 막고, 값을 빠뜨린 Pod에는 기본값을 채워준다

0개의 댓글