Press / to search, Esc to close, ↑↓ to navigate

Kubernetes 핵심 개념 — Pod부터 Deployment까지

Docker만으로는 부족할 때 — 쿠버네티스로 컨테이너 오케스트레이션 시작하기

Posted by DoYoon Kim on April 1, 2026 | 20 min read

왜 Kubernetes인가

이전 Docker 포스트에서 컨테이너를 만들고 docker-compose로 멀티 컨테이너 환경을 구성하는 법을 다뤘다. 로컬 개발이나 소규모 서비스에서는 충분하지만, 프로덕션 환경에서는 금방 한계에 부딪힌다.

  • 컨테이너가 죽으면 누가 다시 띄우는가?
  • 트래픽이 급증하면 컨테이너를 어떻게 늘리는가?
  • 새 버전을 배포할 때 다운타임 없이 교체할 수 있는가?
  • 서버가 여러 대일 때 어떤 서버에 컨테이너를 배치할 것인가?

Kubernetes(이하 k8s)는 이 문제들을 자동으로 해결해 주는 컨테이너 오케스트레이션 플랫폼이다.

문제 k8s가 제공하는 해법
컨테이너 장애 Self-healing — 컨테이너가 죽으면 자동 재시작
트래픽 급증 Auto Scaling — HPA로 Pod 수를 자동 조절
무중단 배포 Rolling Update — 순차적으로 새 버전 교체
서버 분산 배치 Scheduling — 리소스 상태에 따라 최적 노드 배치

핵심 아키텍처

Cluster

k8s의 최상위 단위. Control Plane(마스터)과 하나 이상의 Worker Node로 구성된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────── Cluster ───────────────────┐
│                                                │
│  ┌──── Control Plane ────┐                     │
│  │  API Server           │                     │
│  │  etcd                 │                     │
│  │  Scheduler            │                     │
│  │  Controller Manager   │                     │
│  └───────────────────────┘                     │
│                                                │
│  ┌── Worker Node 1 ──┐  ┌── Worker Node 2 ──┐ │
│  │  kubelet           │  │  kubelet           │ │
│  │  kube-proxy        │  │  kube-proxy        │ │
│  │  [Pod] [Pod]       │  │  [Pod] [Pod]       │ │
│  └────────────────────┘  └────────────────────┘ │
└────────────────────────────────────────────────┘
  • API Server: 모든 요청의 진입점. kubectl 명령이 여기로 간다.
  • etcd: 클러스터 상태를 저장하는 분산 key-value 저장소.
  • Scheduler: 새 Pod를 어느 노드에 배치할지 결정.
  • Controller Manager: Desired State와 현재 상태를 비교해 차이를 메꾼다.

Node

실제 컨테이너가 실행되는 물리/가상 머신. 각 노드에는 kubelet(Pod 관리 에이전트)과 kube-proxy(네트워크 규칙 관리)가 동작한다.

Pod

k8s에서 배포 가능한 가장 작은 단위. 하나 이상의 컨테이너를 포함하며, 같은 Pod 안의 컨테이너들은 네트워크(localhost)와 스토리지를 공유한다.

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:1.0
      ports:
        - containerPort: 8080

실무에서는 Pod를 직접 생성하는 일은 거의 없다. 항상 Deployment 같은 상위 오브젝트를 통해 관리한다.

Namespace

클러스터 안에서 리소스를 논리적으로 격리하는 단위. 팀별, 환경별로 나눠 사용한다.

1
2
3
4
5
6
kubectl get namespaces
# NAME              STATUS   AGE
# default           Active   10d
# kube-system       Active   10d
# production        Active   5d
# staging           Active   5d

핵심 오브젝트

Deployment

Pod의 원하는 상태(Desired State)를 선언하면, k8s가 그 상태를 유지해 준다. Pod 개수, 이미지 버전 등을 명시하면 Controller가 알아서 관리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3            # Pod 3개를 유지
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: my-app:1.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"

Rolling Update는 Deployment의 기본 배포 전략이다. image: my-app:2.0으로 바꾸면 k8s가 새 Pod를 하나씩 띄우고 기존 Pod를 하나씩 종료한다. 다운타임이 없다.

1
2
3
4
5
6
7
8
# 이미지 업데이트 → Rolling Update 자동 실행
kubectl set image deployment/my-app app=my-app:2.0

# 배포 상태 확인
kubectl rollout status deployment/my-app

# 문제 시 롤백
kubectl rollout undo deployment/my-app

Service

Pod는 생성/삭제될 때마다 IP가 바뀐다. Service는 Pod 집합에 대한 안정적인 네트워크 엔드포인트를 제공한다. Label selector로 대상 Pod를 지정하고, 고정 IP(ClusterIP)와 DNS 이름을 부여한다.

타입 접근 범위 사용 시나리오
ClusterIP (기본) 클러스터 내부만 마이크로서비스 간 통신
NodePort 외부 (노드IP:포트) 개발/테스트 환경
LoadBalancer 외부 (클라우드 LB) 프로덕션 외부 노출
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  type: ClusterIP
  selector:
    app: my-app          # 이 label을 가진 Pod에 트래픽 전달
  ports:
    - port: 80           # Service가 받는 포트
      targetPort: 8080   # Pod에 전달하는 포트

ConfigMap & Secret

환경 설정과 민감 정보를 코드에서 분리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ConfigMap — 일반 설정
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  SPRING_PROFILES_ACTIVE: "production"
  LOG_LEVEL: "INFO"
---
# Secret — 민감 정보 (Base64 인코딩)
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  DB_PASSWORD: cGFzc3dvcmQxMjM=    # echo -n 'password123' | base64

Pod에서 참조:

1
2
3
4
5
6
7
8
9
spec:
  containers:
    - name: app
      image: my-app:1.0
      envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: db-secret

⚠️ Secret은 Base64일 뿐 암호화가 아니다. 프로덕션에서는 Sealed Secrets, Vault 등 별도 암호화 솔루션을 사용해야 한다.


자주 쓰는 kubectl 명령어

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 클러스터 정보
kubectl cluster-info
kubectl get nodes

# Pod 관리
kubectl get pods                          # Pod 목록
kubectl get pods -o wide                  # 노드 배치 정보 포함
kubectl describe pod <pod-name>           # Pod 상세 정보
kubectl logs <pod-name>                   # 로그 확인
kubectl logs <pod-name> -f                # 실시간 로그
kubectl exec -it <pod-name> -- /bin/sh    # Pod에 접속

# Deployment 관리
kubectl apply -f deployment.yaml          # 리소스 생성/업데이트
kubectl get deployments
kubectl scale deployment/my-app --replicas=5  # 스케일링
kubectl delete deployment my-app

# 디버깅
kubectl get events --sort-by='.lastTimestamp'
kubectl top pods                          # 리소스 사용량

실전 예제: Spring Boot 앱 k8s 배포

Docker 포스트에서 만든 Spring Boot 앱을 k8s에 배포해 보자. 하나의 파일에 Deployment와 Service를 함께 정의한다.

k8s-deployment.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-app
  labels:
    app: spring-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: spring-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1           # 업데이트 시 최대 1개 추가 Pod
      maxUnavailable: 0     # 업데이트 중 사용 불가 Pod 0개 → 무중단
  template:
    metadata:
      labels:
        app: spring-app
    spec:
      containers:
        - name: spring-app
          image: my-registry/spring-app:1.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "250m"
              memory: "512Mi"
            limits:
              cpu: "1000m"
              memory: "1Gi"
          livenessProbe:
            httpGet:
              path: /actuator/health
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          envFrom:
            - configMapRef:
                name: app-config
            - secretRef:
                name: db-secret
---
apiVersion: v1
kind: Service
metadata:
  name: spring-app-service
spec:
  type: LoadBalancer
  selector:
    app: spring-app
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP

핵심 포인트:

  • livenessProbe: 컨테이너가 살아있는지 확인. 실패하면 재시작한다.
  • readinessProbe: 트래픽을 받을 준비가 되었는지 확인. 실패하면 Service에서 제외한다.
  • resources: 리소스 요청(requests)과 제한(limits)을 반드시 설정해야 Scheduler가 올바르게 배치한다.
  • maxUnavailable: 0: 업데이트 중에도 항상 3개 Pod가 가용하므로 무중단 배포가 보장된다.

배포 실행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ConfigMap, Secret 먼저 생성
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml

# Deployment + Service 배포
kubectl apply -f k8s-deployment.yaml

# 확인
kubectl get all -l app=spring-app
# NAME                              READY   STATUS    RESTARTS   AGE
# pod/spring-app-6d4f8b7c9-abc12   1/1     Running   0          30s
# pod/spring-app-6d4f8b7c9-def34   1/1     Running   0          30s
# pod/spring-app-6d4f8b7c9-ghi56   1/1     Running   0          30s
#
# NAME                         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)
# service/spring-app-service   LoadBalancer   10.96.123.45    34.56.78.90     80:31234/TCP
#
# NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
# deployment.apps/spring-app   3/3     3            3           30s

Docker Compose vs Kubernetes

기준 Docker Compose Kubernetes
용도 로컬 개발, 단일 호스트 프로덕션, 멀티 호스트
스케일링 docker-compose up --scale app=3 (수동) HPA로 자동 스케일링
Self-healing 없음 (restart 정책만 존재) Pod 자동 재시작 + 재배치
네트워킹 단일 호스트 내 브리지 클러스터 전체 Service Discovery
배포 전략 없음 (stop → start) Rolling Update, Blue-Green, Canary
설정 관리 .env 파일 ConfigMap, Secret
학습 곡선 낮음 높음
설정 파일 docker-compose.yml 여러 YAML 매니페스트

이 둘은 경쟁 관계가 아니라 보완 관계다. 로컬에서는 Docker Compose로 빠르게 개발하고, 프로덕션에서는 k8s로 운영하는 것이 일반적인 패턴이다.


언제 k8s를 쓰고, 언제 과한가

k8s가 적합한 경우

  • 마이크로서비스 아키텍처로 서비스가 5개 이상
  • 트래픽 변동이 크고 오토스케일링이 필요
  • 무중단 배포(Rolling Update, Canary)가 필수
  • 멀티 클라우드 또는 하이브리드 클라우드 환경
  • 팀 규모가 크고 여러 서비스를 독립적으로 배포

k8s가 과한 경우

  • 모놀리식 앱 하나를 운영하는 경우
  • 팀원이 1-3명이고 운영 인력이 부족한 경우
  • 트래픽이 예측 가능하고 안정적인 경우
  • 단순히 “이력서에 넣고 싶어서” (가장 흔한 이유…)

대안 선택지:

  • 단일 서버: Docker Compose + Nginx
  • 서버리스: AWS Lambda, Google Cloud Run
  • 매니지드 PaaS: AWS ECS, Google App Engine
  • 간소화된 k8s: k3s (경량 Kubernetes)

k8s를 도입하기 전에 “이 복잡도를 감당할 운영 역량이 있는가?”를 먼저 물어보자. 도구가 문제를 해결하는 것이 아니라, 도구를 운영할 수 있는 팀이 문제를 해결한다.


마무리

Kubernetes는 단순한 배포 도구가 아니라 선언적 인프라 관리 플랫폼이다. “컨테이너 3개를 유지하라”고 선언하면 k8s가 알아서 상태를 맞춰주는 것이 핵심 철학이다.

이번 포스트에서 다룬 내용을 정리하면:

  1. Pod — 배포의 최소 단위, 직접 생성하지 않는다
  2. Deployment — Pod의 Desired State를 선언하고 Rolling Update를 관리
  3. Service — Pod에 안정적인 네트워크 접근을 제공
  4. ConfigMap/Secret — 설정과 민감 정보를 코드에서 분리

다음 포스트에서는 Helm Chart를 활용한 패키지 관리와 Ingress를 통한 외부 라우팅을 다룰 예정이다.


관련 포스트

Share


댓글을 불러오는 중...
CATALOG