Theme:

Docker 컨테이너 하나 띄우는 건 쉽다. 근데 그게 수십, 수백 개가 되면? 죽은 컨테이너를 누가 다시 살리고, 트래픽은 어떻게 분산하고, 새 버전 배포는 어떻게 할 건지. 이 글에서는 Kubernetes가 왜 필요한지부터 핵심 개념들을 쭉 정리해봤습니다.


Docker만으로는 왜 부족한가

Docker는 애플리케이션을 컨테이너로 패키징하고 실행하는 도구예요. 개발 환경과 운영 환경의 차이를 없애주고, 이미지 하나로 어디서든 동일하게 돌릴 수 있다는 점에서 혁신적이었습니다.

그런데 실제 서비스를 운영하다 보면 이런 문제가 생겨요.

  • 컨테이너가 죽었을 때 자동으로 재시작 해주는 주체가 없습니다
  • 여러 서버에 컨테이너를 분산 배치 하려면 직접 해야 합니다
  • 트래픽이 몰리면 컨테이너를 자동으로 늘려야 하는데, Docker 자체로는 안 돼요
  • 새 버전을 배포할 때 ** 무중단 롤링 업데이트 **가 안 됩니다
  • 컨테이너 간 ** 네트워크 통신 **, ** 서비스 디스커버리 **를 일일이 구성해야 해요

결국 컨테이너가 많아지면 "컨테이너를 관리하는 시스템", 즉 ** 컨테이너 오케스트레이션 **이 필요합니다. Kubernetes(이하 K8s)가 바로 그 역할을 해요. Docker Swarm이나 Apache Mesos 같은 대안도 있었지만, 사실상 K8s가 표준으로 자리 잡았습니다.


K8s 아키텍처

K8s 클러스터는 크게 Control Plane 과 Worker Node 로 나뉩니다.

Control Plane (마스터)

클러스터 전체를 관리하는 두뇌 역할입니다.

컴포넌트역할
API Server (kube-apiserver)모든 요청의 진입점. kubectl 명령이나 다른 컴포넌트들이 여기로 통신한다. RESTful API를 제공하고, 인증/인가 처리도 담당
etcd클러스터의 모든 상태 정보를 저장하는 분산 키-값 저장소. Pod 정보, 설정, 시크릿 등 전부 여기 들어있다. 이게 날아가면 클러스터 자체가 복구 불능이라 백업이 필수
Scheduler (kube-scheduler)새로 생성된 Pod을 어느 노드에 배치할지 결정한다. 노드의 리소스 상황, 어피니티 규칙, 테인트/톨러레이션 등을 고려해서 최적의 노드를 선택
Controller Manager (kube-controller-manager)클러스터의 상태를 원하는 상태(desired state)로 유지하는 컨트롤러들의 집합. ReplicaSet 컨트롤러, Node 컨트롤러, Job 컨트롤러 등이 있다

Worker Node

실제 컨테이너가 돌아가는 서버예요.

컴포넌트역할
kubelet각 노드에서 실행되는 에이전트. API Server로부터 Pod 스펙을 받아서 컨테이너를 실행하고, 상태를 보고한다
kube-proxy노드 내 네트워크 규칙을 관리. Service로 들어오는 트래픽을 적절한 Pod으로 라우팅해주는 역할
Container Runtime실제 컨테이너를 실행하는 런타임. Docker, containerd, CRI-O 등이 있다. 참고로 K8s 1.24부터 Docker를 직접 지원하지 않고 containerd를 사용한다

흐름을 간단히 보면 이렇습니다:

  1. 사용자가 kubectl apply -f deployment.yaml을 실행
  2. API Server 가 요청을 받아서 etcd 에 저장
  3. Controller Manager 가 변경을 감지하고 필요한 Pod을 생성
  4. Scheduler 가 각 Pod을 어떤 노드에 배치할지 결정
  5. 해당 노드의 kubelet 이 컨테이너를 실행

Pod — 최소 배포 단위

K8s에서 배포할 수 있는 가장 작은 단위가 Pod입니다. 컨테이너 하나만 들어가는 경우가 대부분이지만, 여러 컨테이너가 하나의 Pod 안에 들어갈 수도 있어요.

같은 Pod 안에 있는 컨테이너들은 이런 특징이 있습니다:

  • 같은 네트워크 네임스페이스 를 공유합니다 (localhost로 통신 가능)
  • 같은 스토리지 볼륨 을 공유할 수 있어요
  • 같은 노드에서 항상 함께 스케줄링됩니다

사이드카 패턴

메인 컨테이너 옆에 보조 역할을 하는 컨테이너를 붙이는 패턴이에요. 실무에서 꽤 자주 씁니다.

  • **로그 수집 **: 메인 앱이 파일로 로그를 쓰면 사이드카가 그걸 읽어서 로그 시스템으로 전송
  • ** 프록시 **: Envoy 같은 프록시를 사이드카로 붙여서 서비스 메시 구현
  • ** 설정 동기화 **: 외부 설정 저장소에서 주기적으로 설정을 가져오는 컨테이너

Pod 생명주기

Pod은 다음 상태를 거칩니다:

상태설명
Pending스케줄링 대기 중이거나 이미지를 받고 있는 상태
Running컨테이너가 실행 중
Succeeded모든 컨테이너가 정상 종료 (주로 Job에서)
Failed컨테이너 중 하나 이상이 실패로 종료
Unknown노드 통신 문제로 상태를 알 수 없음

주의할 점은 Pod은 ** 일회성 이라는 거예요. Pod이 죽으면 같은 Pod이 되살아나는 게 아니라, Deployment 같은 상위 오브젝트가 ** 새로운 Pod을 만듭니다. IP도 바뀌어요.


Service — Pod에 안정적으로 접근하기

Pod은 생성될 때마다 IP가 바뀝니다. 그러면 다른 서비스에서 어떻게 안정적으로 접근할 수 있을까요? 여기서 Service가 등장해요. Service는 Pod 집합에 대한 ** 고정 진입점 **을 제공합니다.

Service 타입

타입설명
ClusterIP클러스터 내부에서만 접근 가능한 가상 IP. 기본값이다. 프론트 -> 백엔드 API 호출 같은 내부 통신에 쓴다
NodePort각 노드의 특정 포트를 열어서 외부 접근을 허용. 30000~32767 범위의 포트를 쓴다. 개발/테스트 용도로 적합
LoadBalancer클라우드 프로바이더의 로드밸런서를 생성해서 외부 트래픽을 받는다. AWS면 ELB, GCP면 Cloud Load Balancing이 붙는 식

Ingress

Service가 L4 레벨이라면 Ingress는 L7 레벨 의 라우팅을 제공합니다. 도메인 기반 라우팅, 경로 기반 라우팅, TLS 종료 같은 기능을 쓸 수 있어요.

YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-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

Ingress 리소스를 만들었다고 바로 동작하는 건 아니고, Ingress Controller(nginx-ingress, traefik 등)를 별도로 설치해야 합니다.


Deployment — 선언적 배포 관리

실무에서 Pod을 직접 만들 일은 거의 없어요. 대신 Deployment를 통해 Pod의 원하는 상태를 선언합니다.

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: my-app:1.2.0
        ports:
        - containerPort: 8080

ReplicaSet

Deployment가 내부적으로 관리하는 오브젝트입니다. 지정된 수만큼의 Pod 복제본을 유지해요. 직접 만들어 쓸 일은 거의 없고, Deployment가 알아서 ReplicaSet을 만들고 관리합니다.

롤링 업데이트

Deployment의 이미지 버전을 바꾸면 K8s가 자동으로 롤링 업데이트를 수행합니다.

  1. 새 ReplicaSet을 만들어서 새 버전 Pod을 하나씩 띄워요
  2. 새 Pod이 Ready 상태가 되면 기존 Pod을 하나씩 내립니다
  3. 이 과정을 반복해서 무중단 배포를 달성해요

maxSurgemaxUnavailable 값으로 업데이트 속도를 조절할 수 있습니다.

롤백

배포 후 문제가 발견되면 즉시 롤백 가능합니다.

BASH
# 이전 버전으로 롤백
kubectl rollout undo deployment/my-app

# 특정 리비전으로 롤백
kubectl rollout undo deployment/my-app --to-revision=2

# 배포 히스토리 확인
kubectl rollout history deployment/my-app

ConfigMap과 Secret — 설정 분리

애플리케이션 코드와 설정값을 분리하는 게 12-Factor App의 원칙이기도 합니다. K8s에서는 ConfigMap과 Secret으로 이걸 구현해요.

ConfigMap

민감하지 않은 설정값을 저장합니다.

YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "db.example.com"
  LOG_LEVEL: "INFO"
  MAX_CONNECTIONS: "100"

Pod에서 환경 변수로 주입하거나 볼륨으로 마운트해서 쓸 수 있어요.

Secret

비밀번호, API 키, 인증서 같은 민감한 데이터를 저장합니다. Base64로 인코딩되어 있긴 한데 이건 암호화가 아니에요. etcd 수준에서 Encryption at Rest를 설정하거나, Vault 같은 외부 시크릿 관리 도구를 연동하는 게 실무에서의 일반적인 접근 방식입니다.

YAML
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  username: YWRtaW4=      # admin
  password: cEBzc3dvcmQ=  # p@ssword

PersistentVolume과 PersistentVolumeClaim — 스토리지

Pod은 기본적으로 Ephemeral입니다. Pod이 죽으면 안에 있던 데이터도 날아가요. DB처럼 데이터가 보존돼야 하는 워크로드에는 PersistentVolume(PV)을 사용합니다.

  • PersistentVolume (PV): 클러스터 관리자가 프로비저닝한 실제 스토리지예요. NFS, AWS EBS, GCE PD 같은 백엔드를 쓸 수 있습니다
  • PersistentVolumeClaim (PVC): 사용자(개발자)가 필요한 스토리지를 요청하는 오브젝트입니다. 용량이나 접근 모드를 지정하면 K8s가 매칭되는 PV를 찾아서 바인딩해요
YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: gp2

StorageClass를 설정해두면 PVC가 요청될 때 Dynamic Provisioning 으로 PV가 자동 생성돼요. 클라우드 환경에서는 이 방식이 일반적입니다.


HPA — Horizontal Pod Autoscaler

트래픽에 따라 Pod 수를 자동으로 조절하는 오브젝트예요. CPU 사용률이나 메모리, 커스텀 메트릭을 기준으로 스케일 아웃/인을 수행합니다.

YAML
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

위 설정이면 CPU 사용률이 70%를 넘으면 Pod을 늘리고, 떨어지면 줄여요. 단, HPA가 동작하려면 Pod에 resources.requests가 설정되어 있어야 하고, Metrics Server가 클러스터에 설치되어 있어야 합니다.

VPA(Vertical Pod Autoscaler)는 Pod 수를 늘리는 대신 개별 Pod의 리소스를 늘리는 방식인데, HPA에 비해 덜 쓰입니다.


Helm — K8s 패키지 매니저

K8s 리소스를 하나하나 YAML로 관리하다 보면 파일이 수십 개가 됩니다. Helm은 이걸 Chart 라는 패키지 단위로 묶어서 관리할 수 있게 해줘요.

  • Chart: K8s 리소스 템플릿의 묶음
  • Release: Chart를 클러스터에 설치한 인스턴스
  • values.yaml: Chart의 설정값을 오버라이드하는 파일
BASH
# 차트 저장소 추가
helm repo add bitnami https://charts.bitnami.com/bitnami

# 설치
helm install my-redis bitnami/redis --set auth.password=mypassword

# 업그레이드
helm upgrade my-redis bitnami/redis --set auth.password=newpassword

# 삭제
helm uninstall my-redis

직접 Chart를 만들면 환경별(dev, staging, prod) values 파일만 바꿔서 동일한 구조를 배포할 수 있어요. CI/CD 파이프라인과 궁합이 좋습니다.


주의할 점

Pod이 죽으면 어떻게 되는가? (셀프 힐링)

Deployment의 Controller가 현재 상태와 원하는 상태를 지속적으로 비교합니다. Pod이 죽으면 ReplicaSet Controller가 감지해서 새 Pod을 생성해요. 같은 Pod이 되살아나는 게 아니라 새 Pod이 만들어지는 겁니다. IP도 바뀌기 때문에 Service를 통해 접근해야 해요.

Liveness Probe vs Readiness Probe

둘 다 컨테이너의 상태를 확인하는 헬스체크인데, 목적이 다릅니다.

구분Liveness ProbeReadiness Probe
목적컨테이너가 살아있는지 확인트래픽을 받을 준비가 됐는지 확인
실패 시컨테이너를 ** 재시작**Service 엔드포인트에서 ** 제외** (트래픽 안 보냄)
사용 예시데드락에 빠진 프로세스 감지초기화 중인 앱에 트래픽 보내지 않기

추가로 Startup Probe 도 있어요. 컨테이너가 시작되는 데 오래 걸리는 경우, Startup Probe가 성공할 때까지 Liveness/Readiness 체크를 유예시켜줍니다. 무거운 Java 앱 같은 경우에 쓸 수 있어요.

StatefulSet vs Deployment

구분DeploymentStatefulSet
Pod 이름랜덤 (my-app-7d4f8b-xk2js)순서 보장 (my-db-0, my-db-1)
스토리지Pod이 죽으면 볼륨 바인딩 해제각 Pod에 고유 PVC가 유지됨
네트워크매번 IP 변경Headless Service로 고정 DNS 제공
순서동시에 생성/삭제 가능순차적으로 생성/삭제
용도무상태 앱 (API 서버 등)유상태 앱 (DB, 캐시, 카프카 등)

왜 DB를 Deployment 대신 StatefulSet으로 배포하는 걸까요? Pod 재시작 후에도 동일한 PVC에 다시 연결 되어야 하고, 안정적인 네트워크 ID 가 필요하기 때문입니다.

Service Mesh (Istio)

마이크로서비스 간 통신이 복잡해지면 서비스 메시가 등장합니다. Istio는 대표적인 서비스 메시 솔루션으로, 각 Pod에 Envoy 프록시를 사이드카로 주입 해서 다음 기능을 제공해요:

  • **트래픽 관리 **: 카나리 배포, A/B 테스트, 서킷 브레이커
  • ** 보안 **: 서비스 간 mTLS 자동 적용
  • ** 관측성(Observability)**: 분산 트레이싱, 메트릭 수집

앱 코드를 건드리지 않고 인프라 레벨에서 이런 기능을 추가할 수 있다는 게 핵심이에요. 다만 복잡도가 높아서 소규모 팀에서는 오버엔지니어링이 될 수 있습니다.


파생 개념 정리

개념설명비고
Docker컨테이너 이미지 빌드 및 실행K8s가 오케스트레이션하는 대상
CI/CD코드 변경 -> 빌드 -> 테스트 -> 배포 자동화Jenkins, GitHub Actions, ArgoCD 등. ArgoCD는 GitOps 기반으로 K8s와 궁합이 특히 좋다
** 클라우드 K8s**AWS EKS, GCP GKE, Azure AKSControl Plane을 클라우드가 관리해주므로 운영 부담이 줄어든다
** 모니터링**Prometheus(메트릭 수집) + Grafana(시각화)K8s 모니터링의 사실상 표준 조합. kube-state-metrics, node-exporter를 함께 설치한다

마무리

Kubernetes는 범위가 넓어서 한 글로 다 담기는 어렵습니다. 그래도 핵심만 정리하면 이 정도로 정리할 수 있어요:

  1. ** 왜 필요한가 **: Docker만으로는 대규모 컨테이너 관리가 안 됩니다
  2. ** 아키텍처 **: Control Plane이 결정하고, Worker Node가 실행해요
  3. ** 핵심 오브젝트 **: Pod(최소 단위), Service(접근점), Deployment(배포 관리)
  4. ** 운영 **: HPA로 오토스케일링, Helm으로 패키징, Probe로 헬스체크

실제 운영 경험이 없더라도 이 개념들을 이해하고 있으면 충분히 활용할 수 있습니다. 나중에 직접 클러스터를 구축해보면서 손에 익히면 더 좋고요.

댓글 로딩 중...