Kubernetes(K8s)는 컨테이너 오케스트레이션 플랫폼이다. 수백 개의 컨테이너를 자동으로 배포, 스케일링, 복구한다. 개발자가 “3개의 인스턴스를 실행해”라고 선언하면, K8s는 그 상태를 항상 유지하려고 동작한다.

비유: 대형 물류 센터 관리자와 같다. 수백 명의 작업자(컨테이너)가 있고, 관리자(K8s)가 일을 배분한다. 작업자가 쓰러지면 다른 작업자가 즉시 대체하고, 주문량이 많아지면 인력을 더 투입(스케일 아웃)하고, 줄어들면 퇴근(스케일 인)시킨다.


아키텍처

K8s 클러스터는 Control Plane(마스터)과 Worker Node로 구성된다. Control Plane이 전체 클러스터를 조율하고, Worker Node가 실제 컨테이너를 실행한다.

graph LR
    API["API Server"] --> ETCD[("etcd")]
    API --> SCHED["스케줄러"]
    API -->|"명령"| KL1["Node1 kubelet"]
    API -->|"명령"| KL2["Node2 kubelet"]
    KL1 --> POD1["Pod들"]

Control Plane 컴포넌트

컴포넌트 역할
API Server 모든 요청의 진입점. kubectl 명령을 받아 etcd에 저장
etcd 클러스터 상태를 저장하는 분산 Key-Value DB. etcd 장애 = 클러스터 마비
Scheduler 새로운 Pod를 어느 Node에 배치할지 결정 (리소스, 어피니티 기반)
Controller Manager 실제 상태가 원하는 상태와 일치하도록 지속 감시 및 교정

Worker Node 컴포넌트

컴포넌트 역할
kubelet API Server와 통신하며 Pod를 실행/관리. Node의 에이전트
kube-proxy Service의 네트워크 라우팅 규칙 관리 (iptables/ipvs)
Container Runtime containerd, CRI-O — 실제 컨테이너 실행

핵심 리소스

Pod — 가장 작은 배포 단위

하나 이상의 컨테이너가 같은 네트워크/스토리지를 공유하는 단위다. Pod 내 컨테이너들은 localhost로 서로 통신한다.

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp
    image: myapp:1.0
    ports:
    - containerPort: 8080
    resources:
      requests:          # 스케줄링 시 보장되는 최소 리소스
        memory: "256Mi"
        cpu: "250m"      # 250 밀리코어 = 0.25 CPU
      limits:            # 이 이상 사용 시 OOM Kill (메모리) 또는 Throttle (CPU)
        memory: "512Mi"
        cpu: "500m"
    livenessProbe:       # 실패 시 컨테이너 재시작
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
    readinessProbe:      # 실패 시 Service 트래픽에서 제외 (재시작 안 함)
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      initialDelaySeconds: 10
      periodSeconds: 5
    env:
    - name: SPRING_PROFILES_ACTIVE
      value: "prod"
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:    # Secret에서 값 주입 (평문 하드코딩 금지)
          name: db-secret
          key: password

livenessProbereadinessProbe의 차이를 혼동하면 안 된다. liveness는 앱이 죽었는지 확인해 재시작하고, readiness는 앱이 트래픽을 받을 준비가 됐는지 확인해 Service에서 제외한다. 배포 중 앱이 초기화 중이면 readiness가 실패해 트래픽을 차단하므로 무중단 배포가 가능하다.

Deployment — Pod 선언적 관리

원하는 상태(replicas, image 등)를 선언하면 Controller Manager가 실제 상태를 맞춘다. Pod는 직접 배포하지 않고 Deployment를 통해 관리한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 배포 중 추가로 생성할 수 있는 최대 Pod 수
      maxUnavailable: 0  # 배포 중 사용 불가 Pod 수 (0 = 무중단 배포)
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:1.1
# 배포
kubectl apply -f deployment.yaml

# 배포 진행 상태 확인
kubectl rollout status deployment/myapp-deployment

# 롤백
kubectl rollout undo deployment/myapp-deployment

# 특정 버전으로 롤백
kubectl rollout undo deployment/myapp-deployment --to-revision=2

Service — 안정적인 접근 엔드포인트

Pod IP는 재시작마다 바뀐다. Service는 고정 IP와 DNS 이름을 제공해 Pod 집합에 안정적으로 접근할 수 있게 한다.

# ClusterIP: 클러스터 내부 통신 전용 (기본값)
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: ClusterIP
  selector:
    app: myapp          # 이 라벨의 Pod들로 트래픽 분산
  ports:
  - port: 80
    targetPort: 8080
---
# NodePort: 외부에서 노드 IP:포트로 직접 접근 (개발/테스트)
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30080     # 30000-32767 범위
---
# LoadBalancer: 클라우드 LB와 연동 (AWS ELB, GCP LB 등)
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080

ConfigMap & Secret — 설정 외부화

# ConfigMap: 일반 설정값
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  LOG_LEVEL: "INFO"
  application.properties: |
    server.port=8080
    spring.datasource.url=jdbc:mysql://mysql-service:3306/mydb
---
# Secret: 민감 정보 (base64 인코딩 저장)
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:
  password: "mypassword"    # stringData는 자동으로 base64 인코딩
  api-key: "my-api-key"

Secret은 base64로 인코딩되지만 암호화는 아니다. 실제 암호화를 위해서는 etcd 암호화 또는 외부 비밀 관리자(AWS Secrets Manager, Vault)를 사용해야 한다.


스케줄링

Scheduler가 Pod를 어느 Node에 배치할지 결정하는 과정이다.

graph LR
    POD["새 Pod 배치 요청"] --> F["1️⃣ Filtering"]
    F --> S["2️⃣ Scoring"]
    S --> B["3️⃣ Binding"]

Node Affinity — 특정 Node에만 배치

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: node-type
            operator: In
            values: ["high-memory"]   # high-memory 라벨의 Node에만 배치

Pod Anti-Affinity — 같은 Node에 중복 배치 방지

가용성을 위해 같은 앱의 Pod가 서로 다른 Node에 분산되도록 강제한다.

spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: myapp
        topologyKey: kubernetes.io/hostname
        # 같은 Node에 app=myapp Pod 두 개 배치 불가

Taint & Toleration — Node 접근 제한

특정 Node에 특수 작업만 배치할 때 사용한다.

# GPU Node에 Taint 설정 (GPU 작업만 허용)
kubectl taint nodes gpu-node1 gpu=true:NoSchedule
# GPU 작업 Pod에 Toleration 설정
spec:
  tolerations:
  - key: "gpu"
    operator: "Equal"
    value: "true"
    effect: "NoSchedule"

HPA (Horizontal Pod Autoscaler) — 자동 스케일링

부하에 따라 Pod 수를 자동으로 조절한다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp-deployment
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70   # CPU 평균 70% 초과 시 스케일 아웃
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30   # 30초 안정화 후 스케일 아웃 (급격한 증가 방지)
    scaleDown:
      stabilizationWindowSeconds: 300  # 5분 안정화 후 스케일 인 (조기 축소 방지)

stabilizationWindowSeconds는 메트릭이 임계치를 넘었다고 바로 스케일하지 않고 일정 시간 관찰 후 결정한다. 순간적인 스파이크로 인한 불필요한 스케일 아웃/인을 방지한다.


Ingress — L7 외부 트래픽 라우팅

비유: Ingress는 건물 안내 데스크다. 외부에서 들어오는 방문객(요청)을 보고 “A팀을 찾으시면 3층, B팀은 5층”처럼 목적지 Service로 안내한다. 건물 입구(LoadBalancer)는 하나지만 내부 목적지는 여럿이다.

graph LR
    EXT["외부 요청"] --> LB["LoadBalancer"]
    LB --> ING["Ingress Controller"]
    ING -->|"/api/*"| SVC1["API Service"]
    ING -->|"/web/*"| SVC2["Web Service"]
    SVC1 --> POD1["API Pod들"]
    SVC2 --> POD2["Web Pod들"]

클러스터 외부에서 내부 Service로 URL 경로/호스트 기반 라우팅하는 규칙이다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls-secret
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /api/orders
        pathType: Prefix
        backend:
          service:
            name: order-service
            port:
              number: 80
      - path: /api/products
        pathType: Prefix
        backend:
          service:
            name: product-service
            port:
              number: 80

Helm — K8s 패키지 매니저

K8s 리소스를 패키지(Chart)로 관리한다. 환경별 값만 바꿔서 배포할 수 있다.

# Chart 구조
myapp-chart/
├── Chart.yaml          # 차트 메타데이터 (이름, 버전)
├── values.yaml         # 기본값 (환경별 override 가능)
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   └── hpa.yaml
└── charts/             # 의존 차트
# values.yaml
replicaCount: 3
image:
  repository: myregistry/myapp
  tag: "1.0.0"
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
# 배포
helm install myapp ./myapp-chart -f values-prod.yaml

# 이미지 태그만 변경해서 업그레이드
helm upgrade myapp ./myapp-chart --set image.tag=1.1.0

# 롤백
helm rollback myapp 1

# 배포된 릴리즈 목록
helm list -A

실무 운영 명령어

# 전체 리소스 확인
kubectl get all -n production

# Pod 상태 상세 확인 (이벤트 포함)
kubectl describe pod myapp-xxx -n production

# 로그 확인 (이전 컨테이너 포함)
kubectl logs myapp-xxx -n production -f --previous

# Pod 내부 접속
kubectl exec -it myapp-xxx -n production -- /bin/sh

# 로컬 포트 포워딩 (디버깅)
kubectl port-forward pod/myapp-xxx 8080:8080 -n production

# 리소스 사용량
kubectl top pods -n production
kubectl top nodes

# 강제 재시작 (이미지 재다운로드 없이)
kubectl rollout restart deployment/myapp-deployment -n production

극한 시나리오

시나리오 1: 배포 중 서비스 다운 — Pod 0개 순간 발생

maxUnavailable: 1이고 replicas: 1이면 구 Pod 종료 후 신 Pod 시작 전 순간적으로 서비스가 다운된다.

strategy:
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0  # 배포 중 절대 Pod 수를 줄이지 않음

maxUnavailable: 0이면 신 Pod가 Ready 상태가 되기 전까지 구 Pod를 종료하지 않는다. readinessProbe와 함께 사용해야 완전한 무중단 배포가 된다.

시나리오 2: Node 장애 시 Pod 재배치 5분 대기

기본 tolerationSeconds가 300초(5분)이므로 Node 장애 후 Pod 재배치까지 5분이 걸린다.

spec:
  tolerations:
  - key: "node.kubernetes.io/unreachable"
    effect: "NoExecute"
    tolerationSeconds: 30  # 기본 300초 → 30초로 단축
  - key: "node.kubernetes.io/not-ready"
    effect: "NoExecute"
    tolerationSeconds: 30

시나리오 3: HPA가 스케일 아웃 안 됨

kubectl top pods 명령이 동작하지 않으면 Metrics Server가 설치되지 않은 것이다. HPA는 Metrics Server에서 CPU/메모리 사용량을 가져온다.

# Metrics Server 설치
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# HPA 상태 확인
kubectl describe hpa myapp-hpa

시나리오 4: Secret 평문 노출

ConfigMap과 Secret의 차이를 모르고 DB 비밀번호를 ConfigMap에 저장하거나, Secret을 kubectl get secret -o yaml로 노출하는 경우다.

# Secret 값 확인 (base64 디코딩)
kubectl get secret db-secret -o jsonpath='{.data.password}' | base64 --decode

운영 환경에서는 RBAC으로 Secret 조회 권한을 제한하고, Sealed Secrets 또는 AWS Secrets Manager 연동을 사용해야 한다.


왜 Kubernetes인가? (vs Docker Compose vs Nomad vs ECS)

항목 Docker Compose HashiCorp Nomad AWS ECS Kubernetes
학습 곡선 낮음 중간 중간 높음
멀티노드 제한적 지원 지원 완전 지원
자가 복구 기본 수준 지원 지원 고급 (liveness/readiness probe)
오토스케일링 없음 지원 ECS Auto Scaling HPA/VPA/KEDA
에코시스템 제한 중간 AWS 종속 압도적 (Helm, Istio, ArgoCD 등)
멀티클라우드 불가 가능 AWS 종속 가능
적합 규모 단일 서버, 로컬 개발 혼합 워크로드 AWS 종속 팀 대규모 마이크로서비스

언제 K8s를 선택하지 않는가: 서비스가 5개 미만이고 단일 서버면 Docker Compose가 훨씬 단순하다. K8s는 운영 오버헤드가 크다 — etcd 백업, 노드 업그레이드, 네트워크 플러그인(CNI) 관리 등이 따라온다.


실무에서 자주 하는 실수

실수 1: resource request/limit 미설정

requestslimits를 설정하지 않으면 Scheduler가 노드 배치를 제대로 못 하고, 한 Pod가 노드 전체 CPU를 독점할 수 있다.

# 나쁜 예 — resource 없음
spec:
  containers:
  - name: app
    image: myapp:latest

# 좋은 예
spec:
  containers:
  - name: app
    image: myapp:latest
    resources:
      requests:
        cpu: "250m"
        memory: "256Mi"
      limits:
        cpu: "500m"
        memory: "512Mi"

requests는 Scheduler가 노드 선택 기준으로 사용하고, limits는 실제 사용 상한이다. limits만 설정하고 requests를 생략하면 requests = limits로 자동 설정된다.

실수 2: liveness probe와 readiness probe 혼동

livenessProbe는 “컨테이너를 재시작할 것인가”를 결정하고, readinessProbe는 “트래픽을 보낼 것인가”를 결정한다. 두 가지를 같은 엔드포인트로 설정하면 DB 연결이 끊어졌을 때 무한 재시작 루프가 발생한다.

livenessProbe:
  httpGet:
    path: /healthz        # 프로세스 자체가 살아있는가
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready          # 실제 요청 처리 가능한가 (DB 연결 포함)
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

원칙: livenessProbe는 단순하게 (프로세스 생존만), readinessProbe는 의존성 포함해서.

실수 3: imagePullPolicy: Always를 프로덕션에서 사용

imagePullPolicy: Always는 Pod가 시작할 때마다 레지스트리에서 이미지를 pull한다. 레지스트리 장애 시 Pod 재시작이 불가능해진다.

# 프로덕션에서 위험
image: myapp:latest
imagePullPolicy: Always

# 안전한 방식 — 구체적 태그 + IfNotPresent
image: myapp:v1.2.3
imagePullPolicy: IfNotPresent

latest 태그를 사용하면 어떤 이미지가 실행 중인지 추적이 불가능하다. 항상 SHA256 또는 시맨틱 버전 태그를 사용해야 한다.

실수 4: Deployment 롤링 업데이트 중 다운타임 발생

terminationGracePeriodSeconds를 설정하지 않으면 진행 중인 요청이 강제 종료된다.

spec:
  terminationGracePeriodSeconds: 60  # 기본값 30초
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]  # 로드밸런서 업데이트 대기

preStop 훅에서 5초 sleep을 주면, 로드밸런서가 해당 Pod를 엔드포인트 목록에서 제거할 시간을 확보할 수 있다.

실수 5: namespace를 default만 사용

모든 리소스를 default 네임스페이스에 배포하면, 팀/환경별 RBAC 적용과 kubectl 명령 실수로 인한 사고 위험이 높아진다.

# 환경별 네임스페이스 분리
kubectl create namespace production
kubectl create namespace staging
kubectl create namespace monitoring

# 기본 네임스페이스 변경 (실수 방지)
kubectl config set-context --current --namespace=staging

면접 포인트

Q. Pod와 Container의 차이는?

Pod는 하나 이상의 컨테이너를 감싸는 K8s의 최소 배포 단위다. 같은 Pod 내 컨테이너는 네트워크 네임스페이스(IP, 포트)와 볼륨을 공유한다. 사이드카 패턴(로그 수집, 프록시)이 이 구조를 활용한다. 실제 운영에서는 대부분 컨테이너 1개 = Pod 1개지만, Envoy 사이드카(Istio), Fluentd 로그 수집기는 메인 앱과 같은 Pod에 배치된다.

Q. Deployment, StatefulSet, DaemonSet의 차이는?

  • Deployment: 무상태 앱. Pod 이름이 랜덤(app-abc123). 어느 노드에 배치되든 상관없음. 롤링 업데이트 기본 지원.
  • StatefulSet: 상태 있는 앱(DB, Kafka). Pod 이름 고정(mysql-0, mysql-1). 순서대로 시작/종료. 각 Pod에 고정된 PersistentVolume 연결.
  • DaemonSet: 모든 노드에 1개씩 배포. 로그 수집기(Fluentd), 모니터링 에이전트(Prometheus Node Exporter)에 사용.

Q. Service의 ClusterIP, NodePort, LoadBalancer 차이는?

  • ClusterIP: 클러스터 내부에서만 접근 가능한 가상 IP. 기본값. 마이크로서비스 간 통신에 사용.
  • NodePort: 모든 노드의 특정 포트(30000~32767)로 외부 접근. 테스트/개발용.
  • LoadBalancer: 클라우드 프로바이더의 외부 로드밸런서를 자동 프로비저닝. 프로덕션 외부 노출.

실무에서는 외부 트래픽을 Ingress → ClusterIP Service → Pod 순으로 라우팅하고, LoadBalancer는 Ingress Controller 하나에만 사용한다.

Q. HPA가 스케일 아웃을 안 하는 이유는?

세 가지를 확인한다. 첫째, Metrics Server 미설치 (kubectl top pods 실패). 둘째, Pod의 resources.requests.cpu가 미설정 (HPA는 requests 대비 사용률로 계산). 셋째, minReplicasmaxReplicas가 같거나 현재 Pod 수가 이미 최대. kubectl describe hpa 명령으로 ConditionsEvents 항목을 확인하면 원인이 나온다.

Q. K8s에서 무중단 배포를 보장하려면?

다섯 가지를 조합해야 한다: (1) readinessProbe — 준비된 Pod에만 트래픽 전달, (2) PodDisruptionBudget — 동시에 내려가는 Pod 수 제한, (3) rollingUpdate.maxUnavailable: 0 — 항상 기존 Pod 유지 후 신규 시작, (4) preStop 훅 — 로드밸런서 제거 대기, (5) terminationGracePeriodSeconds — 진행 중 요청 완료 대기. 이 다섯 가지 중 하나라도 빠지면 다운타임이 발생할 수 있다.


왜 이 기술인가

Kubernetes를 선택하는 이유는 수십~수백 개의 컨테이너를 수동으로 관리하는 것이 불가능하기 때문이다.

대안 문제점 Kubernetes의 해결
직접 Docker 실행 장애 시 수동 재시작, 배포 자동화 없음 Self-healing, 자동 재시작
Docker Compose 단일 호스트 제한, 오토스케일링 없음 멀티 노드 클러스터, HPA 자동 확장
수동 로드밸런싱 새 인스턴스 추가 시 수동 등록 Service가 Pod 등록/해제 자동화

트래픽 급증 시 HPA가 CPU 메트릭 기반으로 Pod를 자동 증가시키고, 장애 Pod는 Kubelet이 감지해 자동으로 재시작한다. Rolling Update로 무중단 배포가 가능하고, Rollback도 명령 한 줄로 처리된다.


함께 읽으면 좋은 글

댓글

이 글이 도움이 됐다면?

같은 카테고리의 다른 글도 확인해보세요

더 많은 글 보기 →