Kubernetes 배포를 몇 분에서 초 단위로 줄인 12가지 실전 기법

|Platform Decision|15분 읽기

배포 시간이 슬금슬금 늘어날 때

쿠버네티스를 한동안 운영하다 보면 비슷한 패턴을 만나게 됩니다. 처음엔 5분이면 끝나던 배포가 어느 순간 10분, 15분으로 늘어나죠. 그런데 그게 한 번에 늘어나는 게 아니라 슬금슬금 늘어난다는 게 문제입니다. 이미지에 의존성 하나 추가하고, 프로브 설정 한 줄 바꾸고, 레플리카 늘리고. 각각은 사소한데 누적되면 핫픽스를 올려야 하는 순간에 손이 떨립니다.

결국 배포 시간은 단일 원인의 문제가 아니라 파이프라인 전 구간에 흩어진 비효율의 합입니다. 이미지 빌드, 레지스트리 풀, 스케줄링, 프로브 통과, 롤아웃. 어느 한 단계만 잡아서는 체감이 안 됩니다. 지난 몇 달간 여러 프로젝트에서 구간별로 뜯어봤던 것들을 정리해봤습니다.

1. 컨테이너 이미지, 무자비하게 줄이기

배포 시간을 줄이려고 할 때 가장 먼저 손대는 건 이미지입니다. 이유는 단순합니다. 이미지가 크면 풀이 느리고, 풀이 느리면 노드마다 그 비용을 다 치러야 하니까요. 노드 10대에 새 파드를 띄운다면 그 큰 이미지를 10번 내려받는 셈입니다.

멀티스테이지 빌드Alpine 기반 이미지의 조합은 여기서 효과가 큽니다.

# Before: 1GB 크기의 무거운 이미지
# After: 100MB 이하로 압축
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["npm", "start"]

실제로 한 프로젝트에서 Ubuntu 기반 이미지를 Alpine으로 바꿨더니 풀 시간이 70% 가까이 줄었습니다. dive 같은 도구로 레이어를 한 겹씩 뜯어보면, 빌드 과정에서 딸려 들어온 불필요한 파일이 생각보다 많다는 걸 알게 됩니다. 빌드 캐시, 테스트 산출물, 문서까지 최종 이미지에 그대로 박혀 있는 경우가 흔하죠.

2. 헬스체크 프로브 정교하게 튜닝하기

Readiness와 Liveness 프로브는 배포 안정성의 핵심인데, 의외로 기본값 그대로 쓰는 곳이 많습니다. 여기서 어긋나면 정상인 앱을 죽이거나, 죽은 앱에 트래픽을 보냅니다.

포인트는 두 가지입니다. 초기 지연 시간을 앱의 실제 워밍업 시간에 맞추고, 체크 주기는 짧게 가져가는 것.

spec:
  containers:
  - name: app
    image: myapp:v1
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 30  # 앱 시작 시간 고려
      periodSeconds: 10        # 빠른 감지
      timeoutSeconds: 5
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      initialDelaySeconds: 5   # 준비 상태 빠른 체크
      periodSeconds: 5

Liveness와 Readiness를 같은 엔드포인트로 묶어두는 실수도 자주 봅니다. 둘은 역할이 다릅니다. Readiness는 "트래픽 받아도 되냐"를, Liveness는 "죽었냐 살았냐"를 묻습니다. JVM처럼 워밍업이 긴 앱은 initialDelaySeconds를 잘못 잡으면 멀쩡한 파드가 계속 재시작되면서 배포가 끝나질 않습니다. 한 전자상거래 서비스에서 프로브 튜닝만으로 배포 실패율을 절반으로 줄였다는 얘기도 들었는데, 충분히 그럴 만합니다.

3. 리소스 요청과 제한, 현실적으로 설정하기

kubectl top으로 실제 사용량을 먼저 재고 나서 리소스를 잡으면 스케줄링이 빨라집니다. requests를 과하게 잡으면 스케줄러가 자리를 못 찾아 헤매고, 너무 적게 잡으면 노드가 과밀해져서 다 같이 느려지죠.

resources:
  requests:
    cpu: "250m"      # 실제 평균 사용량 기준
    memory: "256Mi"
  limits:
    cpu: "500m"      # 버스트 트래픽 고려
    memory: "512Mi"

requests는 스케줄링의 기준이고 limits는 런타임의 상한이라는 걸 구분해서 봐야 합니다. 둘을 같은 값으로 막아두면 안정적이긴 한데 자원을 빡빡하게 쓰게 됩니다. 적절히 잡은 뒤 파드 배치가 40% 빨라졌다는 보고가 여러 팀에서 나옵니다.

4. HPA로 자동 스케일링 활용하기

Horizontal Pod Autoscaler는 트래픽이 튈 때 사람이 손으로 레플리카를 늘리던 일을 대체합니다. CPU 사용률 50~70%를 목표로 두는 게 보통이에요.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2    # 콜드 스타트 방지
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60

다만 HPA가 만능은 아닙니다. 스케일 아웃에는 새 파드가 뜨고 프로브를 통과하는 시간이 걸립니다. 트래픽이 이미 들이닥친 뒤에야 파드가 준비되는 거죠. minReplicas를 2 이상으로 둬서 콜드 스타트를 피하는 건 그래서입니다. CPU만으로 부족하다면 커스텀 메트릭이나 KEDA 쪽도 한번 봐둘 만합니다.

5. 롤링 업데이트 전략 최적화

무중단 배포에서 핵심은 maxUnavailable: 0입니다. 기존 파드를 죽이기 전에 새 파드를 먼저 띄우라는 뜻이죠. 속도가 더 중요하면 maxSurge를 높여서 한 번에 더 많은 파드를 교체합니다.

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0  # 무중단 보장

둘은 트레이드오프 관계입니다. maxUnavailable: 0은 안전하지만 그만큼 교체가 한 칸씩 진행돼서 느립니다. maxSurge를 키우면 빠르지만 그 순간 자원을 더 점유하죠. 무중단이 절대 조건인 금융권 쪽이면 안전을, 내부 서비스면 속도를 택하는 식으로 서비스 성격에 맞춰 잡는 게 맞습니다.

6. GitOps로 배포 자동화하기

ArgoCD나 Flux 같은 GitOps 도구를 들이면 PR 병합만으로 배포가 돌아갑니다. 사람이 직접 kubectl apply를 치는 단계를 없애는 거죠.

이게 단순히 편해지는 문제가 아닙니다. 수동 명령은 누가 언제 무엇을 바꿨는지가 사람 머릿속과 터미널 히스토리에만 남습니다. GitOps는 클러스터의 상태가 Git에 선언적으로 박혀 있어서, 배포 이력이 곧 커밋 로그가 됩니다. 롤백도 이전 커밋으로 돌리면 끝이고요. GitOps 도입 후 MTTR이 크게 줄었다는 사례가 많은데, 빨라져서라기보다 "무엇을 되돌릴지"가 명확해진 덕이 큽니다.

7. 노드 타입과 친화성 최적화

ARM Graviton 인스턴스로 옮기면 비용을 아끼면서 성능도 20~40% 올라가는 경우가 있습니다.

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: node-type
          operator: In
          values: ["graviton"]

다만 ARM으로 가려면 이미지를 멀티아키텍처로 빌드해둬야 합니다. amd64 이미지를 그대로 올리면 스케줄링은 되는데 파드가 못 뜨죠. 비용 절감만 보고 넘어갔다가 빌드 파이프라인을 손봐야 하는 일이 생깁니다. 친화성 자체는 스케줄링을 제약하는 수단이라, 너무 빡빡하게 걸면 자리가 없어서 파드가 Pending에 걸리는 역효과도 있습니다.

8. 이미지 풀 속도 개선하기

1번에서 이미지를 줄였다면, 여기선 받아오는 경로를 손봅니다. DaemonSet으로 노드에 이미지를 미리 받아두거나, 지역별 레지스트리 미러링을 쓰면 풀 시간이 확 줄어듭니다. imagePullPolicy: IfNotPresent로 이미 있는 이미지를 또 받지 않게 막는 것도 기본이고요.

레지스트리가 리전 바깥에 있으면 풀 시간의 상당 부분이 네트워크 왕복으로 새어 나갑니다. 글로벌 서비스를 굴리는 팀에서 ECR 미러링으로 풀 시간을 2분에서 20초로 줄였다는 얘기가 그래서 나오는 거죠. 이미지를 줄이는 것과 가까이 두는 것은 따로 노는 게 아니라 같은 문제의 양면입니다.

9. 네임스페이스와 리소스 쿼터로 격리하기

개발/스테이징/프로덕션을 네임스페이스로 나누고 ResourceQuota로 자원 경쟁을 막으면 배포 성능이 안정됩니다.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 20Gi
    limits.cpu: "20"
    limits.memory: 40Gi

쿼터는 속도를 직접 올리는 도구라기보다, 한 팀의 폭주가 다른 팀 배포를 끌어내리는 걸 막는 안전장치에 가깝습니다. 같은 클러스터를 여러 팀이 쓰면 누군가의 부하 테스트가 옆 팀 배포를 멈추는 일이 실제로 생기니까요.

10. 네트워킹과 스토리지 간소화

Calico나 Cilium 같은 고성능 CNI를 쓰고, 상태가 없는 앱에서는 불필요한 PV 마운트를 걷어내는 것만으로도 배포가 빨라집니다. 스토리지 어태치는 의외로 시간이 걸리는 단계라, 안 써도 되는 볼륨이 붙어 있으면 파드 시작이 그만큼 지연됩니다. 상태 없는 앱에 습관적으로 PVC를 달아두지 않았는지 한번 점검해볼 만합니다.

11. ConfigMap/Secret으로 설정 외부화

환경별 설정을 ConfigMap으로 빼두면 이미지를 다시 빌드하지 않고 배포할 수 있습니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  DB_HOST: "postgres.cluster"

설정값이 이미지 안에 박혀 있으면 로그 레벨 하나 바꾸려고 전체 빌드 파이프라인을 다시 돌려야 합니다. 그 차이가 누적되면 배포 사이클이 몇 배씩 벌어지죠. 설정이 복잡한 앱일수록 외부화의 체감이 큽니다.

12. 모니터링으로 지속적 개선하기

결국 측정하지 않으면 느려지는 걸 모릅니다. 앞서 말한 "슬금슬금 늘어나는" 배포 시간을 잡으려면 숫자로 추적하는 수밖에 없어요. Prometheus와 Grafana로 배포 시간을 보고, 2분을 넘는 배포에 알림을 걸어두면 성능 저하를 빨리 알아챕니다.

메트릭 목표값 모니터링 도구
배포 시간 < 2분 Grafana
이미지 풀 시간 < 30초 kubectl describe
파드 시작 시간 < 15초 Prometheus
롤아웃 성공률 > 95% ArgoCD/Flux

한번 줄여놓은 배포 시간은 가만 두면 다시 늘어납니다. 정기적으로 들여다보는 팀이 그 속도를 유지하는 건 그래서 당연한 결과죠.

작은 변화가 쌓여서 만드는 차이

이 기법들을 하나씩 적용하면서 느낀 건, 배포 속도는 한 방의 최적화가 아니라 구간마다 쌓인 비효율을 걷어내는 일이라는 점입니다. 이미지 1번과 풀 8번이 결국 같은 문제였던 것처럼, 따로 보이던 항목들이 사실 한 파이프라인 위에 얹혀 있더라고요.

그리고 가장 크게 달라진 건 배포 시간 숫자 자체가 아니라, 핫픽스를 올려야 하는 순간에 손이 덜 떨린다는 것이었습니다. 빠른 배포의 진짜 가치는 속도가 아니라 그 여유에 있는 게 아닐까 싶어요.

#Kubernetes#DevOps#Container#배포최적화#GitOps