0. Contents
1. Overview
Stateless Service 패턴의 핵심: 세 가지 추상화가 하나씩 맡는다
패턴의 정의와 적합한 환경
- Stateless Service 패턴은 동일한 일시적(ephemeral) 복제본들로 구성된 애플리케이션을 만들고 운영하는 방법을 설명한다
- 빠른 확장과 높은 가용성이 필요한 동적 클라우드 환경에 가장 적합한 패턴이다
2. Problem
일시적 서비스의 조건
- 마이크로서비스 아키텍처에서 서비스를 일시적(ephemeral) 으로 만들려면
- 서비스가 부작용 없이 생성·확장·삭제될 수 있어야 한다
- 이 특성은 서비스가 무상태(Stateless) 일 때 훨씬 쉽게 달성된다
| Stateful (상태 있음) | Stateless (상태 없음) | |
|---|---|---|
| 비유 | 단골 카페 | 자판기 |
| 요청 처리 | 사장님이 내 취향을 기억 | 누가 눌렀는지 모르고 버튼만 보고 음료를 줌 |
| 교체 시 | 사장님이 바뀌면 다시 알려야 함 | 고장나도 옆 걸로 바꾸면 됨 |
무상태 서비스의 요청 처리
- 내부적으로 어떤 상태도 유지하지 않는다
- 과거 요청에 대한 저장된 지식이나 참조가 없다
- 그래서 각 요청은 마치 처음부터 새로 시작하는 것처럼 처리된다
무상태 판별 사고 실험
실험 가정
- 서비스 인스턴스들이 서로 다른 노드에 배포된다
- 로드 밸런서가 sticky session 없이 요청을 무작위로 분배한다
아무 서버나 받아도 되면 Stateless, 특정 서버여야 하면 Stateful
판별 기준
- 이 설정에서도 서비스가 제 역할을 다할 수 있다면, 그것은 무상태 서비스다
3. Solution
애플리케이션 = 여러 요소의 집합
- Kubernetes에는 “애플리케이션(Application)“이나 “컨테이너(Container)“를 최상위 개체로 갖는 개념이 없다
- 대신 애플리케이션은 여러 요소들의 집합으로 구성된다
하나의 애플리케이션
- Deployment / ReplicaSet → Pod 생성 및 유지
- ConfigMap / Secret → 설정 및 민감 정보
- Service → 네트워크 진입점
- PersistentVolumeClaim → 영구 저장소
Deployment와 ReplicaSet의 관계
- 무상태 Pod를 관리하는 컨트롤러는 ReplicaSet이다
- 하지만 이것은 Deployment가 내부적으로 사용하는 하위 레벨 구조다
- Deployment가 사용자가 직접 다루는 권장 추상화다
사용자는 Deployment 만 다루고, 그 아래는 알아서 만들어진다
ReplicaSet 직접 사용: 두 경우로 한정
- Deployment의 업데이트 전략이 맞지 않을 때
- 커스텀 메커니즘이 필요할 때
4. Instances: ReplicaSet
ReplicaSet의 주요 목적
- 언제든지 지정된 수의 동일한 Pod 복제본이 실행되도록 보장하는 것이다
ReplicaSet의 3가지 핵심 구성요소
| 구성요소 | 역할 | 비유 |
|---|---|---|
replicas | 유지할 Pod 수 지정 | “직원은 항상 3명이어야 해” |
selector | 관리할 Pod를 식별 | “이름표에 ‘my-app’ 붙은 직원만 관리” |
template | 새 Pod 생성 설계도 | “새 직원 채용 공고문” |
Example 11-1. ReplicaSet 명세
apiVersion: apps/v1 # ReplicaSet은 apps API 그룹 소속 (core v1이 아님)
kind: ReplicaSet
metadata:
name: rg # ReplicaSet 자체의 이름 — Pod 이름은 이 뒤에 랜덤 접미사가 붙어 생성됨(rg-xxxxx)
labels:
app: random-generator # ReplicaSet 리소스 자체에 붙는 레이블(관리용). selector와는 별개
spec:
replicas: 3 # ① 유지할 Pod 복제본의 목표 수 — 컨트롤러가 이 값을 계속 감시
selector:
matchLabels:
app: random-generator # ② 관리할 Pod를 찾는 레이블 조건 — template.metadata.labels와 반드시 일치해야 함
template: # ③ 새 Pod를 만들 때 사용할 템플릿 (이하는 일반 Pod spec과 동일)
metadata:
labels:
app: random-generator # selector가 찾는 바로 그 레이블 — 여기서 Pod가 실제로 부여받음
spec:
containers:
- name: random-generator # 컨테이너 이름 (Pod 안에서 로그/exec 시 식별자로 사용)
image: k8spatterns/random-generator:1.0 # 3개 Pod 모두 동일한 이미지로 복제됨
자동 복구 동작
ReplicaSet 은 죽은 Pod 를 살리지 않고, 새 Pod 로 개수를 다시 맞춘다
selector와 template label이 일치해야 하는 이유
| 위치 | 레이블 | 의미 |
|---|---|---|
selector.matchLabels | app: random-generator | “이 레이블을 가진 Pod를 관리하겠다” |
template.metadata.labels | app: random-generator | “새로 만드는 Pod에 이 레이블을 붙인다” |
이 둘이 반드시 같아야 함!
- 다르면 ReplicaSet이 자신이 만든 Pod를 관리 대상으로 인식 못함 ❌
ReplicaSet의 범위: 클러스터 전체가 아닌 네임스페이스
동작 범위와 자원 소비
- ReplicaSet은 클러스터 전체가 아니라 특정 네임스페이스 안에서만 동작한다
- 단, Pod들이 실행되면서 클러스터 전체 노드의 CPU/메모리 자원을 소비한다
클러스터 전체
namespace: default← ReplicaSet “rg” 여기 있음- [Pod1] [Pod2] [Pod3]
namespace: production← 여기엔 영향 없음namespace: staging← 여기도 영향 없음
5. Networking: Service
Pod의 근본적인 문제
Pod의 수명: 일시적
- ReplicaSet이 생성한 Pod는 일시적(ephemeral) 이며 언제든지 사라질 수 있다
| 기존 Pod | 새 Pod | |
|---|---|---|
| 이름 | pod-abc | pod-xyz (바뀜) |
| IP | 10.0.0.1 | 10.0.0.5 (바뀜!) |
| hostname | pod-abc | pod-xyz (바뀜) |
핵심 문제: 찾아오는 방법
- IP가 바뀌면 클라이언트가 이전 IP로 요청을 보내도 찾을 수 없다
- Stateless라면 어떤 Pod가 요청을 받아도 동일하게 처리할 수 있다
- 그러므로 문제는 “찾아오는 방법"에 있다
Kubernetes Service가 해결하는 것
Pod IP 는 바뀌어도 클라이언트는 Service IP 하나만 안다
Service가 필요한 경우 vs 불필요한 경우
연결을 누가 먼저 거느냐가 Service 필요 여부를 가른다
Example 11-2. 무상태 서비스 노출하기
apiVersion: v1 # Service는 core v1 API 그룹 소속
kind: Service
metadata:
name: random-generator # ① Pod에 접근할 때 사용하는 서비스 이름 — 클러스터 내부 DNS도 이 이름으로 생성됨
# (예: random-generator.default.svc.cluster.local)
spec:
# type을 생략하면 기본값은 ClusterIP — 클러스터 내부에서만 접근 가능(외부 노출은 NodePort/LoadBalancer 필요)
selector:
app: random-generator # ② ReplicaSet의 Pod 레이블과 매칭되는 셀렉터
# 이 레이블을 가진 Pod들이 자동으로 이 Service의 엔드포인트가 됨
ports:
- port: 80 # ③ Service가 클러스터 내부에 노출하는 포트 (클라이언트가 호출하는 포트)
targetPort: 8080 # ④ Pod 내부 컨테이너가 실제로 listening하는 포트 (컨테이너 spec의 포트와 일치해야 함)
protocol: TCP # ⑤ 기본값이 TCP라 생략 가능하지만 명시적으로 적어두면 의도가 분명해짐
포트 개념: 건물 비유
- 클라이언트가 보내는 곳 →
port: 80 - 실제 Pod 안에서 받는 곳 →
targetPort: 8080 - “안내데스크(80번 창구)로 오세요 → 실제 담당자는 8080호 사무실에 있어요”
clusterIP: 클러스터 내부 전용 고정 IP
clusterIP 할당과 특성
- Service가 생성되면 clusterIP가 자동으로 할당된다
- 이 IP는 Kubernetes 클러스터 내부에서만 접근 가능하다
- Service가 삭제될 때까지 절대 바뀌지 않는다
clusterIP 는 클러스터 안에서만 통하는 대표 번호다
비유: 대표 전화번호
- clusterIP는 서로 다른 서비스끼리 통신하는 대표 전화번호 같은 개념이다
- 라벨만으로 직접 찾으려 하면
- 로드밸런싱이 없다
- 죽은 Pod의 IP를 추적하기 어렵다
Service의 3가지 핵심 기능
| 기능 | 설명 |
|---|---|
| 고정 IP (clusterIP) | Pod가 바뀌어도 Service IP는 그대로 |
| 로드 밸런싱 | 요청을 여러 Pod에 자동으로 분산 |
| 헬스 체크 | readiness probe 통과한 정상 Pod에만 요청 전달 |
Deployment/ReplicaSet과 Service는 서로 모른다
| Deployment/ReplicaSet | Service | |
|---|---|---|
| 하는 일 | “Pod 3개만 유지하면 됨” | “app=random-generator Pod로 트래픽 보내면 됨” |
| 상대에 대한 관심 | “Service가 있는지 없는지 전혀 관심 없음” | “Deployment가 있는지 없는지 전혀 관심 없음” |
연결 고리: label/selector
- 이 둘은 label/selector라는 공통 언어를 통해서만 간접적으로 연결된다
Deployment 와 Service 사이에 직접 선은 없다 — 공통 언어는 label
6. Storage: PersistentVolumeClaim
PVC란?
PV와 PVC
- PV (PersistentVolume) = 실제 저장 공간 (창고)
- PVC (PersistentVolumeClaim) = 창고 사용 신청서
신청서 방식의 동작
- 개발자가 직접 창고를 만드는 게 아니다
- “이런 창고가 필요해요” 라고 신청서를 내면
- Kubernetes가 알아서 맞는 창고를 연결해줌
Example 11-3. PersistentVolume 요청(claim)
apiVersion: v1 # PVC도 core v1 API 그룹 소속
kind: PersistentVolumeClaim
metadata:
name: random-generator-log # ① Pod 템플릿에서 참조할 claim 이름 — volumes.persistentVolumeClaim.claimName에 그대로 사용됨
spec:
storageClassName: "manual" # 어떤 StorageClass(=어떤 종류의 스토리지 프로비저너)를 쓸지 지정
# "manual"은 관리자가 PV를 미리 만들어둔 정적 프로비저닝을 의미
# 클라우드 환경이면 보통 "standard", "gp3" 같은 동적 프로비저닝용 클래스를 씀
accessModes:
- ReadWriteOnce # ② 단일 노드에서만 읽기/쓰기 가능 (노드 단위 제한, Pod 단위 아님 — 아래 RWO vs RWOP 참고)
resources:
requests:
storage: 1Gi # ③ 1GiB 저장 공간 요청 — 실제 PV의 용량이 이보다 크거나 같아야 바인딩됨
# 이 PVC는 template.spec.volumes에서 이름으로 참조되고,
# containers[].volumeMounts에서 컨테이너 내부 경로에 마운트된다.
accessModes 4가지
accessModes의 역할
- PVC의 핵심 필드인
accessModes는 저장소가 노드에 어떻게 마운트되고 Pod에 의해 어떻게 사용되는지를 제어한다
accessModes: 누가(노드? Pod?) 볼륨을 읽고 쓸 수 있나
RWO vs RWOP: 미묘하지만 중요한 차이
제한 단위: 노드 vs Pod
- RWO는 노드 단위 제한이라 같은 노드의 여러 Pod가 동시에 쓸 수 있다
- 오직 RWOP만이 단 하나의 Pod만 볼륨에 접근함을 보장한다
| 모드 | 제한 단위 | Node A의 Pod1 | Node A의 Pod2 |
|---|---|---|---|
| ReadWriteOnce | 노드 단위 | ✅ 쓰기 가능 | ✅ 쓰기 가능 ← 같은 노드면 여러 Pod도 OK |
| ReadWriteOncePod | Pod 단위 | ✅ 쓰기 가능 | ❌ 접근 불가 ← 같은 노드여도 다른 Pod는 NO |
RWOP 주의점: 스케일 아웃
replicas: 3으로 늘리려 했더니…- Pod1 ✅ 실행 중 (PVC 점유 중)
- Pod2 ❌ Pending! 스케줄러가 노드에 배치하지 못함
- Pod3 ❌ Pending! 같은 이유
kubectl describe pod이벤트에FailedScheduling과 함께 같은 RWOP PVC를 쓰는 Pod가 이미 있다는 메시지가 남음
- → 사실상 싱글톤(1개)으로 고정됨
- → Stateless의 장점인 스케일 아웃이 불가능해짐!
RWOP 주의점: 선점(preemption)
- 처음(알파)에는 RWOP PVC를 쓰는 Pod에 선점이 적용되지 않았다
- Kubernetes 1.27(베타)부터 스케줄러가 RWOP PVC를 쓰는 Pod의 선점을 지원한다 (1.29에서 RWOP와 함께 GA)
- 우선순위 높은 Pod가 같은 PVC를 필요로 하면, 그 PVC를 쓰던 낮은 우선순위 Pod를 밀어낼 수 있다
상황별 추천 모드
| 상황 | 추천 모드 |
|---|---|
| 일반적인 단일 서버 디스크 | RWO |
| 여러 Pod가 같은 파일 읽기 (설정파일 등) | ROX |
| 여러 Pod가 공유 파일 읽기/쓰기 (NFS 등) | RWX |
| 반드시 하나의 Pod만 써야 하는 중요 데이터 | RWOP (주의!) |
ReplicaSet vs StatefulSet의 저장소 차이
같은 창고 하나 vs 각자 창고 — 무상태와 상태 저장의 큰 차이
무상태 vs 상태 저장: 저장소 방식
- ReplicaSet의 Pod들은 PVC 하나를 함께 쓴다
- 단, 예제 PVC는 ReadWriteOnce라서 같은 노드에 뜬 Pod끼리만 공유할 수 있다
- 여러 노드의 Pod가 함께 쓰려면 RWX를 지원하는 스토리지가 필요하다
- 이것이 Kubernetes에서 무상태와 상태 저장 워크로드를 처리하는 방식의 가장 큰 차이점 중 하나다
7. 전체 구조 한눈에 보기
Deployment · ReplicaSet · Service · PVC 가 한 앱을 이루는 모습
추상화별 해결 문제
| 추상화 | 해결하는 문제 | 핵심 동작 |
|---|---|---|
| Pod | 컨테이너가 죽으면? | liveness probe → 재시작 |
| ReplicaSet | Pod 수가 부족하면? | 자동으로 새 Pod 생성 |
| Deployment | 버전 업데이트/실패하면? | 롤링업데이트/롤백 |
| Service | 어떤 Pod가 정상인지? | readiness probe → 정상만 연결 |
| PVC | 데이터를 영구 보관하려면? | 저장소 요청 및 마운트 |
8. Discussion
복잡한 분산 시스템의 구성
- 일반적으로 여러 서비스들로 구성된다
| 구성 | 예 | 특징 |
|---|---|---|
| Stateless 서비스 (이번 챕터) | API 서버, 웹 서버 | 복제 쉬움, 빠른 확장 |
| Stateful 서비스 (다음 챕터) | DB, 메시지 브로커 | 각자 고유 데이터 보유 |
| 단기 작업 (Job/CronJob) | 배치 처리, 데이터 변환 | 실행 후 종료 |
무상태 서비스의 4가지 특성
- 동일함 (Identical): Pod1 = Pod2 = Pod3
- 교체 가능함 (Swappable): Pod1이 Pod2로 교체되어도 문제 없음
- 일시적임 (Ephemeral): 언제든 사라져도 괜찮음
- 대체 가능함 (Replaceable): 죽으면 새걸로 바로 대체
조합의 책임: 개발자
- Kubernetes는 이러한 구성 요소들을 제공한다
- 하지만 이들 사이의 직접적인 관계를 강제하지는 않는다
- 이것들을 애플리케이션의 특성에 맞게 조합하는 것은 개발자의 책임이다
확장 프레임워크와의 결합
- Kubernetes 기본 요소만으로 부족할 때는 Knative Serving, KEDA 같은 프레임워크와 결합할 수 있다
| 프레임워크 | 역할 | 특징 |
|---|---|---|
| Knative Serving | 서버리스 워크로드 실행 | 트래픽 없으면 Pod를 0으로 축소 |
| KEDA | 이벤트 기반 자동확장 | 메시지 큐 길이 등 외부 지표로 확장 |
Knative의 구성
- Knative는 Serving(요청 기반 자동확장), Eventing(이벤트 라우팅) 등을 포함하는 서버리스 프로젝트다
- 스케일-투-제로는 그중 Knative Serving의 역할이다
자동확장 기준 비교
| 구성 | 자동확장 방식 |
|---|---|
| 기본 Kubernetes | HPA로 자동확장: 기본은 CPU/메모리, 메트릭 어댑터를 붙이면 커스텀·외부 지표도 사용 (autoscaling/v2) |
| KEDA 추가 시 | 메시지 큐 길이, DB 쿼리 수 등으로도 자동확장 |
| Knative Serving 추가 시 | 트래픽 0 → Pod 0개로 축소 (완전한 서버리스) |
관련 장
- 이러한 프레임워크들은 29장 “탄력적 확장(Elastic Scale)“에서 다룬다
Stateless Service는 동일하고 교체 가능한 인스턴스로 구성되며, Deployment + ReplicaSet + Service + PVC의 조합으로 생성·유지·확장·업데이트·복구 전 과정을 자동화할 수 있다. 각 구성 요소는 서로를 직접 알지 못하고 label이라는 공통 언어로만 연결된다.
