Kubernetes(k8s)는 Container 오케스트레이션 플랫폼이다. 다수의 Container를 클러스터 위에서 배포·확장·복구하는 작업을 자동화한다. 단일 서버에서 Docker로 Container를 관리하는 단계를 넘어 수십, 수백 개 Container를 다뤄야 할 때, Container가 종료되면 누가 재시작하는지, 트래픽이 몰리면 어떻게 늘리는지, Container 간 통신은 어떻게 하는지 같은 문제를 선언적 모델로 푼다.
클러스터 아키텍처
k8s 클러스터는 Control Plane과 Worker Node로 구성된다.
flowchart TB
subgraph cp["Control Plane"]
API["API Server"]
ETCD["etcd"]
SCHED["Scheduler"]
CM["Controller Manager"]
end
subgraph wn1["Worker Node"]
KL1["kubelet"]
KP1["kube-proxy"]
CR1["Container Runtime"]
P1["Pod"]
P2["Pod"]
end
subgraph wn2["Worker Node"]
KL2["kubelet"]
KP2["kube-proxy"]
CR2["Container Runtime"]
P3["Pod"]
end
API --> SCHED
API --> CM
API --> ETCD
API --> KL1
API --> KL2
Control Plane
클러스터 전체를 관리하는 컴포넌트 집합이다.
API Server는 모든 요청의 진입점이다. kubectl이든 내부 컴포넌트든, k8s에 무언가를 요청하면 API Server를 거친다. etcd는 클러스터의 상태를 저장하는 분산 키-값 저장소다. 어떤 Pod이 어디서 실행 중인지, 어떤 Deployment가 존재하는지 같은 정보가 모두 여기 있다.
Scheduler는 새로 생성된 Pod을 어느 Node에 배치할지 결정한다. Node의 자원 여유, affinity 규칙 등을 고려한다. Controller Manager는 클러스터의 현재 상태가 선언된 상태와 일치하는지 감시한다. Deployment에 replicas: 3이라고 적었는데 Pod이 2개뿐이면, Controller가 하나를 더 생성한다.
Worker Node
실제로 Container가 실행되는 서버다.
kubelet은 각 Node에서 Pod의 생명주기를 관리한다. API Server로부터 “이 Pod을 실행하라"는 지시를 받아 Container Runtime을 통해 Container를 시작한다. kube-proxy는 Node 레벨의 네트워크 규칙을 관리해서 Service로 들어온 트래픽을 적절한 Pod으로 전달한다.
전체 흐름
kubectl apply -f deployment.yaml을 실행하면 API Server가 요청을 받는다. etcd에 원하는 상태를 저장하고, Scheduler가 Pod을 배치할 Node를 정한다. 해당 Node의 kubelet이 Container를 생성한다. Controller Manager는 이후에도 선언된 상태와 실제 상태의 차이를 계속 감시하고 보정한다.
백엔드 개발자가 직접 다루는 건 kubectl과 YAML 매니페스트다. 나머지는 k8s가 내부적으로 처리한다.
핵심 오브젝트
Pod
Pod은 k8s에서 배포할 수 있는 가장 작은 단위다. Container를 직접 배포하지 않고 Pod으로 감싸는 건, 같은 Pod 안의 Container끼리 네트워크 네임스페이스와 스토리지를 공유하기 때문이다. 사이드카 패턴처럼 메인 Container 옆에 로그 수집기나 프록시를 함께 배치할 때 유용하다.
대부분의 경우 Pod 하나에 Container 하나다. Pod을 직접 만드는 일은 드물고, Deployment를 통해 관리한다.
Deployment
Deployment는 Pod의 선언적 관리자다. “이 Image로 Pod을 3개 유지하라"고 선언하면 k8s가 자동으로 3개를 생성하고, Pod이 종료되면 재시작한다. 백엔드 서비스를 배포할 때 가장 자주 쓰는 오브젝트다.
배포 전략도 Deployment가 처리한다. 기본 전략은 롤링 업데이트다. 새 버전의 Pod을 하나씩 생성하면서 이전 버전을 하나씩 제거한다. 전체 서비스가 중단되지 않으면서 점진적으로 교체된다. 문제가 생기면 kubectl rollout undo로 이전 버전으로 돌아간다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: api-server:1.2.0
ports:
- containerPort: 8080
Service
Pod은 생성과 삭제가 수시로 일어나고, IP가 바뀐다. Pod IP를 직접 사용하면 안 된다. Service는 Pod 집합에 안정적인 접근점을 제공한다.
ClusterIP는 클러스터 내부에서만 접근 가능한 가상 IP를 부여한다. 백엔드 서비스 간 통신에 주로 사용한다. NodePort는 각 Node의 특정 포트를 열어 외부 접근을 허용한다. LoadBalancer는 클라우드 환경에서 외부 로드밸런서를 자동으로 생성한다.
백엔드 개발에서 가장 자주 쓰는 건 ClusterIP다. 다른 서비스를 호출할 때 http://service-name:port로 접근하면 k8s DNS가 해당 Service의 ClusterIP로 해석한다. 서비스 디스커버리를 별도로 구현하지 않아도 된다.
Namespace
Namespace는 하나의 클러스터를 논리적으로 분리하는 단위다. dev, staging, production 같은 환경을 같은 클러스터 안에서 격리할 때 사용한다. 리소스 이름은 Namespace 안에서만 고유하면 된다.
네트워크
Ingress
Service가 클러스터 내부의 접근점이라면, Ingress는 클러스터 외부에서 내부 Service로 트래픽을 라우팅한다. 도메인이나 경로 기반으로 여러 Service에 분배할 수 있다. API Gateway 없이도 경로별 라우팅이 가능해서 백엔드 서비스 구성에 자주 사용된다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
spec:
rules:
- host: api.example.com
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
- path: /orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
api.example.com/users는 user-service로, /orders는 order-service로 전달된다. Ingress 자체는 규칙 정의이고, 실제 트래픽 처리는 Ingress Controller(nginx, traefik 등)가 담당한다.
설정과 저장소
ConfigMap과 Secret
애플리케이션 설정을 Container Image에 포함하면 설정이 바뀔 때마다 Image를 다시 빌드해야 한다. ConfigMap은 설정 데이터를 별도 오브젝트로 분리한다. 환경 변수로 주입하거나 볼륨으로 마운트해서 Container에 전달한다.
Secret은 ConfigMap과 구조가 같지만, 패스워드나 API 키 같은 민감 정보를 저장한다. base64로 인코딩되어 저장되므로 암호화는 아니지만, RBAC과 결합하면 접근 제어가 가능하다.
백엔드 개발에서 DB 접속 정보, 외부 API 키 같은 설정을 코드에서 분리할 때 ConfigMap과 Secret이 그 역할을 한다.
PersistentVolume
Pod이 삭제되면 내부 데이터도 사라진다. 데이터베이스처럼 영속 저장이 필요한 워크로드에는 PersistentVolume(PV)을 사용한다. PV는 클러스터 관리자가 미리 프로비저닝한 저장소이고, PersistentVolumeClaim(PVC)은 Pod이 PV를 요청하는 방법이다. Pod은 PVC만 알면 되고, 실제 저장소가 어디인지는 몰라도 된다.
헬스 체크
k8s가 Pod의 상태를 자동으로 판단하려면 애플리케이션이 건강한지 확인할 방법이 필요하다. 이 부분은 백엔드 개발자가 직접 구현해야 한다.
readinessProbe는 Pod이 트래픽을 받을 준비가 됐는지 확인한다. 준비되지 않은 Pod에는 Service가 트래픽을 보내지 않는다. 서버 시작 후 캐시 워밍업이 끝나야 요청을 받을 수 있는 경우에 사용한다.
livenessProbe는 Pod이 정상적으로 동작하는지 확인한다. 실패하면 k8s가 Pod을 재시작한다. 데드락에 빠지거나 응답 불능 상태에 빠진 경우를 감지한다.
startupProbe는 시작이 느린 애플리케이션에 사용한다. 시작이 완료될 때까지 liveness/readiness 체크를 유예한다.
containers:
- name: api
image: api-server:1.2.0
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
백엔드 서버에 /health/ready와 /health/live 엔드포인트를 구현하면 된다. readiness는 DB 연결, 외부 의존성 확인을 포함하고, liveness는 서버 프로세스 자체의 생존 여부만 확인하는 것이 일반적이다.
스케일링
수동 스케일링
kubectl scale deployment api-server --replicas=5
Deployment의 replicas를 직접 변경한다. 트래픽 패턴이 예측 가능하거나, 이벤트에 맞춰 사전에 증설할 때 사용한다.
HPA
트래픽이 불규칙하면 수동 스케일링으로는 대응이 어렵다. HPA(Horizontal Pod Autoscaler)는 메트릭을 기반으로 Pod 수를 자동 조절한다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
이 설정은 “cpu 사용률이 평균 70%를 넘으면 Pod을 늘리고, 낮아지면 줄여라. 최소 2개, 최대 10개"를 의미한다.
metrics-server가 각 Pod의 자원 사용량을 주기적으로 수집한다. HPA Controller가 현재 평균 사용률과 목표 사용률을 비교해서 필요한 Pod 수를 계산한다. 현재 Pod 3개의 CPU 평균이 90%이고 목표가 70%이면, 90/70 × 3 ≈ 4개로 확장한다.
HPA가 동작하려면 Deployment에 resource requests가 반드시 설정되어 있어야 한다. requests가 없으면 “70%“의 기준이 되는 분모가 없다.
containers:
- name: api
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
requests는 Pod이 보장받는 최소 자원이다. Scheduler가 Node에 Pod을 배치할 때 이 값을 기준으로 여유 공간을 판단한다. limits는 Pod이 사용할 수 있는 최대 자원이다. CPU limits를 초과하면 스로틀링되고, 메모리 limits를 초과하면 OOMKill로 Pod이 종료된다. 백엔드 서비스의 메모리 사용량을 모니터링해서 적절한 값을 설정하는 것이 중요하다.
CPU 외에 메모리, 커스텀 메트릭(요청 수, 큐 길이 등)도 HPA 기준으로 사용할 수 있다. Pod 수가 아니라 개별 Pod의 리소스를 자동 조정하는 VPA(Vertical Pod Autoscaler)도 있지만, HPA와 같은 메트릭으로 동시에 사용하면 충돌할 수 있다.
운영
kubectl 기본
kubectl get pods # Pod 목록
kubectl get pods -o wide # Node 배치 정보 포함
kubectl describe pod <name> # Pod 상세 + 이벤트
kubectl logs <pod-name> # 로그 확인
kubectl logs <pod-name> -f # 실시간 로그
kubectl exec -it <pod-name> -- sh # Container 내부 접속
디버깅 흐름
kubectl get pods로 Pod 상태를 본다. CrashLoopBackOff, ImagePullBackOff 같은 상태가 원인을 알려준다. kubectl describe pod <name>으로 이벤트를 확인하면 Scheduler가 Node에 배치하지 못했는지, 리소스가 부족한지, Image를 가져오지 못했는지 파악할 수 있다. kubectl logs로 애플리케이션 로그를 확인한다. 로그만으로 안 되면 kubectl exec로 Container 안에 들어가서 직접 확인한다.
배포 후 Pod이 뜨지 않는 상황은 백엔드 개발자가 가장 자주 마주치는 k8s 문제다. 이 흐름을 익혀두면 대부분의 상황에서 원인을 파악할 수 있다.
정리
k8s는 “원하는 상태를 선언하면 시스템이 그 상태를 유지한다"는 선언적 모델로 동작한다. Deployment에 replicas: 3을 적으면 k8s가 3개를 유지하고, HPA에 목표 CPU 사용률을 적으면 Pod 수를 자동으로 조절한다. 백엔드 개발자가 직접 챙겨야 할 부분은 헬스 체크 엔드포인트 구현, resource requests 설정, 그리고 디버깅을 위한 kubectl 사용법이다.