Kubernetes 기본: Pod, Deployment, Service
1. 개요
Kubernetes에는 수십 가지의 리소스 유형이 있지만, 일일 작업의 80%는 세 가지 개념 중심으로 회전합니다: Pod, Deployment, Service. 이 글은 각 리소스의 본질을 이해하고, 이들이 어떻게 연결되는지, 그리고 실제로 클러스터에서 실행되는 애플리케이션을 배포하기 위한 manifest YAML 세트를 작성할 수 있도록 도와줍니다.
이 글을 읽고 나면 Spring Boot 애플리케이션을 Kubernetes에 배포하고, 여러 replica로 확장하며, Service를 통해 외부에 노출할 수 있습니다 - 모두 kubectl과 몇 개의 YAML 파일만으로.
참고: 이 글은 이미 클러스터(Minikube, Kind, 또는 클라우드)가 있고
kubectl이 올바른 context를 가리키고 있다고 가정합니다.
2. Pod - 배포의 최소 단위
2.1. Pod이란
Pod은 network namespace를 공유하는 하나 이상의 컨테이너 그룹입니다(동일 IP, 동일 port space) 및 volume. 실제로는 대부분의 Pod은 단 하나의 애플리케이션 컨테이너만 포함합니다.
apiVersion: v1
kind: Pod
metadata:
name: order-service-pod
labels:
app: order-service
spec:
containers:
- name: order-service
image: nonladev/order-service:1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
설명:
metadata.labels는app: order-service레이블을 붙입니다 - 이것이 Kubernetes가 Pod을 “인식”하는 방식이며, Service와 Deployment는 이 레이블을 사용하여 연결된 객체를 선택합니다.resources.requests는 scheduler가 Pod을 노드에 배치하기 위해 필요한 최소 리소스이고;limits는 상한선으로, 이를 초과하면 throttle(CPU) 또는 종료됩니다(메모리 - OOMKilled).containerPort: 8080은 단순히 정보 선언일 뿐 자동으로 포트를 외부에 열지 않습니다.
적용 및 확인:
kubectl apply -f order-service-pod.yaml
kubectl get pods
NAME READY STATUS RESTARTS AGE
order-service-pod 1/1 Running 0 5s
2.2. Pod을 직접 생성하지 말아야 하는 이유
위와 같이 “단순” Pod을 만드는 것은 큰 문제가 있습니다: Pod이 포함된 Node가 죽거나 Pod이 충돌하면 아무도 자동으로 다시 생성하지 않습니다. Pod에는 self-healing이나 scaling 메커니즘이 없습니다.
참고: 프로덕션 환경에서는 거의 Pod을 직접 생성하지 않습니다. 항상 Deployment(또는 StatefulSet, Job)를 사용하여 Pod을 관리하세요.
3. Deployment - Pod의 생명 주기 관리
3.1. Deployment 선언
Deployment은 ReplicaSet을 통해 Pod 세트를 생성하고 관리하며, 항상 올바른 수의 replica가 실행되도록 보장하고, 다운타임이 없는 rolling update를 지원합니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: nonladev/order-service:1.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
설명:
replicas: 3은 Deployment가 항상 3개의 Pod을 동시에 실행하도록 요구합니다 - Pod 하나가 죽으면 Deployment이 자동으로 새 Pod을 생성하여 대체합니다.selector.matchLabels는template.metadata.labels과 일치해야 합니다. 이것이 Deployment과 그것이 관리하는 Pod 사이의 필수 연결입니다.readinessProbe는 Pod이 traffic을 받을지 말지 결정합니다(Service에서 사용);livenessProbe는 Pod을 다시 시작해야 하는지 결정합니다. 이 두 probe를 혼동하는 것은 Kubernetes를 배울 때 매우 흔한 실수입니다.
적용 및 관찰:
kubectl apply -f order-service-deployment.yaml
kubectl get deployments
kubectl get pods -l app=order-service
NAME READY UP-TO-DATE AVAILABLE AGE
order-service 3/3 3 3 12s
NAME READY STATUS RESTARTS AGE
order-service-7d4f9c8b6d-2kx9p 1/1 Running 0 12s
order-service-7d4f9c8b6d-7h2mn 1/1 Running 0 12s
order-service-7d4f9c8b6d-xq1lw 1/1 Running 0 12s
3.2. Scale 및 rolling update
YAML을 수정하지 않고 replica 수를 늘리거나 줄일 수 있습니다:
kubectl scale deployment order-service --replicas=5
이미지를 변경할 때의 rolling update - Deployment이 rolling 전략에 따라 이전 Pod을 새 Pod으로 자동 교체하며, 다운타임이 없습니다:
kubectl set image deployment/order-service order-service=nonladev/order-service:1.1
kubectl rollout status deployment/order-service
Waiting for deployment "order-service" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "order-service" successfully rolled out
새 버전에 버그가 있으면 즉시 이전 버전으로 롤백합니다:
kubectl rollout undo deployment/order-service
3.3. Rolling update 전략 구성
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
maxUnavailable: 1은 업데이트 중에 최대 1개의 Pod이 “사용 불가”가 되도록 허용합니다; maxSurge: 1은 선언된 replica 수를 초과하여 최대 1개의 새 Pod을 생성하도록 허용합니다. replicas: 3의 경우 전환 과정 중 클러스터는 2-4개의 Pod을 실행하게 되지만, 절대 2개 이하로 내려가지 않습니다.
4. Service - Pod을 안정적으로 노출
4.1. Service가 필요한 이유
Pod은 자신의 IP를 가지고 있지만, Pod이 다시 생성될 때마다(crash, rolling update, scale) 이 IP는 변경됩니다. Service는 Pod 그룹 앞에 안정적인 주소(ClusterIP + DNS name)를 제공하고 레이블 selector를 기반으로 traffic을 자동으로 load-balance합니다 - 기본 Pod이 얼마나 자주 변경되든 상관없습니다.
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP
설명:
selector.app: order-service는 Pod의 레이블과 일치합니다(Deployment가template.metadata.labels에 붙인) - Service는 Endpoints라는 중간 객체를 통해 자동으로 이들 Pod을 찾고 추적합니다.port: 80은 Service가 클러스터 내부에서 수신 대기하는 포트이고;targetPort: 8080은 컨테이너가 실제로 노출하는 포트입니다.- 이 두 숫자가 반드시 같을 필요는 없습니다.
type: ClusterIP(기본값)은 클러스터 내에서만 접근할 수 있습니다 - microservice 간의 내부 service에 적합합니다.
Service와 Endpoints 확인:
kubectl apply -f order-service-service.yaml
kubectl get svc order-service
kubectl get endpoints order-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
order-service ClusterIP 10.96.142.201 <none> 80/TCP 4s
NAME ENDPOINTS AGE
order-service 10.244.1.5:8080,10.244.1.6:8080,+1 more 4s
4.2. Service type 종류
| Type | 접근 범위 | 사용 시기 |
|---|---|---|
ClusterIP |
클러스터 내부만 | 내부 microservice 간 통신 |
NodePort |
모든 노드에서 고정 포트(30000-32767) 개방 | 빠른 테스트, dev/on-prem 환경 |
LoadBalancer |
클라우드 공급자를 통해 공개 IP 할당 | 클라우드에서 service를 인터넷에 노출 |
Minikube에서 빠른 테스트를 위해 외부에 노출하는 예:
apiVersion: v1
kind: Service
metadata:
name: order-service-external
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
nodePort: 30080
type: NodePort
minikube service order-service-external --url
참고: 프로덕션에서
NodePort를 사용하는 것은 anti-pattern입니다 - 포트 범위가 제한되어 있고, Ingress처럼 TLS termination이나 health-based routing이 없습니다. 클라우드의 프로덕션에서는LoadBalancer또는 Ingress Controller를 우선시하세요.
5. 실제 애플리케이션에서 세 가지 결합
전체 파이프라인: 클라이언트가 Service를 호출 → Service가 Deployment이 관리하는 Pod들에 load-balance
Client
│
▼
Service (order-service, ClusterIP 10.96.142.201:80)
│ (label selector: app=order-service)
▼
┌─────────────┬─────────────┬─────────────┐
│ Pod #1 │ Pod #2 │ Pod #3 │
│ :8080 │ :8080 │ :8080 │
└─────────────┴─────────────┴─────────────┘
▲ 관리하는
Deployment (order-service, replicas: 3)
한 번에 배포하기 편하게 하려면 Deployment과 Service를 하나의 파일에 합치고 ---로 분리할 수 있습니다:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: nonladev/order-service:1.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
kubectl apply -f order-service-all.yaml
6. 배포 전에 readiness endpoint를 위한 테스트 작성
Kubernetes probe가 /actuator/health/readiness에 의존하기 전에, 이 endpoint가 Spring Boot 코드에서 올바른 상태를 반환하는지 확인해야 합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoint.health.probes.enabled=true
management.health.readiness-state.enabled=true
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderServiceHealthTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void readinessProbe_shouldReturnUp() {
ResponseEntity<String> response =
restTemplate.getForEntity("/actuator/health/readiness", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("\"status\":\"UP\"");
}
}
이 테스트는 실제 readiness endpoint에 TestRestTemplate을 호출하고 HTTP 200과 body에 UP이 포함되어 있는지 확인합니다. 이것이 바로 Deployment의 readinessProbe이 주기적으로 호출할 endpoint입니다 - 테스트가 여기서 실패하면 Pod은 클러스터에서도 절대 “Ready”가 되지 않습니다.
7. 흔한 실수
- selector가 label과 일치하지 않음: Service 또는 Deployment이 Pod을 찾지 못합니다(Endpoints가 비어 있음)
selector와labels를 잘못 작성했거나 불일치하기 때문입니다. 항상kubectl get endpoints <svc-name>으로 확인하세요. - readinessProbe 누락: Service는 여전히 애플리케이션이 아직 시작되지 않은 Pod으로 traffic을 라우팅하여 배포 직후 502/503 오류가 발생합니다.
- Resource requests/limits 누락: Pod이 scheduler에 의해 잘못된 노드에 배치되거나 traffic이 증가할 때 예상치 못하게 OOMKilled될 수 있습니다.
- 프로덕션을 위해 단순 Pod 사용: self-healing이 없고, Pod이 손실되면 수동으로 다시 생성해야 합니다.
latestimage 태그: rolling update가 변경을 감지하지 못하고, Kubernetes는 이미지가 변경되지 않았다고 생각하여 다시 pull하지 않습니다. 항상 특정 버전 태그를 사용하세요.
8. 결론
Pod은 컨테이너를 실행하는 최소 단위이고, Deployment는 Pod 세트의 생명 주기와 scaling을 관리하며, Service는 기본 Pod이 계속 변경되더라도 이들에 액세스할 수 있는 안정적인 주소를 제공합니다. 이 세 가지 구성 요소를 확실히 이해하면 나중에 Ingress, ConfigMap/Secret 또는 Helm chart로 진행하기 위한 기초가 됩니다. 다음 단계에서 시도해야 할 사항: 실제 Spring Boot 애플리케이션을 배포하고 rolling update를 관찰한 후 Pod을 수동으로 종료하여 Deployment이 어떻게 자동으로 복구되는지 확인하세요.