유성

Helm Chart로 Spring Boot 프로덕션 배포하기, 선언적 배포 본문

카테고리 없음

Helm Chart로 Spring Boot 프로덕션 배포하기, 선언적 배포

백엔드 유성 2026. 2. 11. 16:51

쿠버네티스에 Spring Boot 애플리케이션을 처음 올릴 때 막막한 것 중 하나가 "설정을 어디에, 어떻게 써야 하는가"다.

이번 글에서는 Helm Chart를 기준으로 Spring Boot를 프로덕션에 배포할 때 필요한 설정 항목들을 하나씩 살펴본다.

 

values.yaml 파일을 중심으로 각 설정이 왜 필요한지, 어떤 영향을 주는지를 함께 설명한다.

 

1. 초기 Replica 개수 설정

서비스를 기동할 때 몇 개의 Pod를 띄울지 지정한다.

replicaCount: 2

2로 설정하면 동일한 애플리케이션 Pod가 2개 생성된다. 단일 Pod로 운영하면 해당 노드에 장애가 발생했을 때 서비스가 완전히 중단되므로, 프로덕션 환경에서는 최소 2개 이상을 권장한다.

다만 replicaCount는 HPA(8번 섹션)가 활성화되어 있으면 자동으로 조정되므로, 초기 기동 수 정도로만 이해하면 된다.

 

2. 컨테이너 이미지 설정

image:
  repository: dockerhubid/application-name
  pullPolicy: IfNotPresent
  tag: ""

imagePullSecrets:
  - name: dockerhub-secret

repository 는 Docker Hub 또는 사설 레지스트리의 이미지 경로다. CI/CD 파이프라인에서 빌드한 이미지를 이 경로에 푸시하고, 여기서 참조한다.

pullPolicy 는 이미지를 언제 다시 받을지를 결정한다.

  • IfNotPresent: 노드에 이미지가 없을 때만 Pull
  • Always: 매번 레지스트리에서 최신 이미지 확인
  • Never: 로컬에 있는 이미지만 사용

 

프로덕션에서는 IfNotPresent를 쓰되, 이미지 태그를 latest 대신 빌드 번호나 커밋 해시로 고정하는 것이 안전하다.

필자는 커밋 해시를 사용한다. 안정성을 위해 직접 해시값을 넣지 않고, github-action에서 에이전트가 직접 수정하도록 한다.

 

latest 를 쓰면 동일한 태그로 다른 이미지가 배포될 수 있어 재현성이 떨어진다.

imagePullSecrets 는 프라이빗 레지스트리에 접근하기 위한 인증 정보다.

Docker Hub의 경우 Rate Limit 이슈도 있어, 인증 후 사용하는 것이 권장된다.

# Secret 생성 예시
kubectl create secret docker-registry dockerhub-secret \
  --docker-username=myid \
  --docker-password=mypassword \
  -n my-namespace

 

 

3. Pod 라벨링

podLabels:
  app-type: api
  team: backend
  environment: prod

라벨은 단순한 메타데이터처럼 보이지만, 운영에서 꽤 유용하게 쓰인다.

  • 'kubectl get pods -l team=backend' 처럼 팀 단위로 파드를 필터링할 수 있다.
  • Grafana나 Loki에서 라벨 기반 쿼리로 특정 환경, 팀의 로그와 메트릭만 분리해 볼 수 있다.
  • NetworkPolicy나 PodAffinity 규칙의 셀렉터로도 활용된다.

처음에는 귀찮아 보여도, 서비스가 많아지면 라벨 체계를 잘 잡아두는 것이 운영 편의성에 큰 차이를 만든다.

 

4. Service

service:
  type: ClusterIP
  port: 8080
  targetPort: 8080

Service는 Pod에 대한 고정된 접근 진입점을 제공한다. Pod는 재시작될 때마다 IP가 바뀌지만, Service의 IP는 고정되어 있어 클러스터 내부에서 안정적으로 통신할 수 있다.

 

ClusterIP 는 클러스터 내부에서만 접근 가능한 타입이다. 외부 트래픽은 다음 섹션에서 설명할 HTTPRoute(Gateway API)를 통해 받는다. 이렇게 내부 통신과 외부 노출을 분리하면 보안 경계를 명확하게 유지할 수 있다.

 

 

5. HTTPRoute — 외부 트래픽 라우팅

httpRoute:
  enabled: true
  parentRefs:
    - name: api-gateway
  hostnames:
    - www.api.mycompany.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api

전통적으로 외부 트래픽은 Ingress로 처리했지만, 최근에는 Gateway API가 표준으로 자리잡고 있다.

Istio, Envoy Gateway 등 대부분의 현대적인 게이트웨이 컨트롤러가 Gateway API를 지원한다.

 

설정의 흐름은 다음과 같다.

인터넷 → Gateway(로드밸런서) → HTTPRoute(라우팅 규칙) → Service → Pod
  • parentRefs: 트래픽이 들어오는 Gateway를 지정한다. 여러 서비스가 하나의 Gateway를 공유할 수 있다.
  • hostnames: 어떤 도메인으로 들어오는 요청을 이 라우트가 처리할지 결정한다.
  • PathPrefix: /api: /api로 시작하는 모든 경로를 이 서비스로 전달한다. 같은 도메인에 여러 서비스가 경로별로 나뉘는 구조에서 유용하다.

 

6. 컨테이너 리소스 제한

resources:
  requests:
    cpu: 300m
    memory: 512Mi
  limits:
    cpu: 500m
    memory: 1Gi

리소스 설정은 쿠버네티스 스케줄링과 안정성에 직접 영향을 미치는 중요한 항목이다.

 

requests 는 스케줄러가 노드를 선택할 때 기준이 되는 보장 리소스다.

이 값이 없으면 스케줄러가 적절한 노드를 찾지 못하거나, 리소스가 없는 노드에 배치되어 OOM이 발생할 수 있다.

 

limits 는 컨테이너가 사용할 수 있는 최대 리소스다. 초과하면

  • CPU: 강제로 쓰로틀링되어 응답이 느려진다.
  • Memory: 컨테이너가 OOMKill로 즉시 종료된다.

Spring Boot의 경우 JVM 특성상 초기 메모리 사용량이 낮더라도 GC 과정에서 순간적으로 치솟을 수 있다. limits.memory 는 JVM 힙 최대값보다 최소 1.5배 이상 여유 있게 잡는 것이 좋다.

 

7. Health Check — Liveness & Readiness Probe

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: actuator-port

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: actuator-port

두 Probe는 이름이 비슷하지만 역할이 완전히 다르다.

  Liveness Probe Readiness Probe
질문 살아있는가? 트래픽을 받을 수 있는가?
실패 시 컨테이너 재시작 Service 엔드포인트에서 제거
용도 데드락, 무한루프 감지 초기화 완료, DB 연결 확인

 


Spring Boot Actuator의 /actuator/health/liveness
와 /actuator/health/readiness 는 이 두 상태를 분리해서 노출해주므로, 그대로 연결하면 된다.

 

Actuator를 사용하려면 application.yml 에 아래 설정이 필요하다.

management:
  endpoint:
    health:
      probes:
        enabled: true
  health:
    livenessState:
      enabled: true
    readinessState:
      enabled: true

Readiness가 실패하면 재시작 없이 트래픽만 차단하므로, 배포 롤아웃 중 구버전 트래픽 처리 완료를 기다리는 데도 자연스럽게 활용된다.

 

 

8. HPA — 자동 스케일링

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 4
  targetCPUUtilizationPercentage: 50
  targetMemoryUtilizationPercentage: 50

HPA(Horizontal Pod Autoscaler)는 부하에 따라 Pod 수를 자동으로 조절한다.

 

위 설정은 "전체 Pod의 평균 CPU 또는 메모리가 50%를 넘으면 Pod를 추가하고, 줄어들면 다시 줄인다. 단, 최소 2개는 항상 유지하고 최대 4개를 넘지 않는다"는 의미다.

 

targetCPUUtilizationPercentage: 50 을 50%로 잡는 이유는 트래픽이 급증할 때 스케일 아웃까지 시간이 걸리기 때문이다. 80%에서 트리거하면, 새 Pod가 Ready 상태가 되기 전까지 기존 Pod가 과부하를 받는다. 여유 있게 50%로 설정하면 좀 더 선제적으로 대응할 수 있다.

HPA가 동작하려면 metrics-server가 클러스터에 설치되어 있어야 한다.

 

 

9. 노드 선택 — NodeSelector

nodeSelector:
  node-role: api

클러스터에 API 서버용 노드와 배치 작업용 노드가 섞여 있다면, 애플리케이션을 원하는 노드 그룹에만 배치할 수 있다.

노드에 미리 node-role=api  라벨을 붙여두면 해당 노드에만 스케줄링된다.

kubectl label node <node-name> node-role=api

 

10. Affinity — 세밀한 배치 제어

NodeSelector가 "이 노드 그룹에만" 배치하는 단순한 규칙이라면, Affinity는 더 세밀한 배치 전략을 표현한다.

affinity:
  podAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
              - key: app.kubernetes.io/name
                operator: In
                values:
                  - redis-cache
          topologyKey: kubernetes.io/hostname

  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 40
        podAffinityTerm:
          labelSelector:
            matchExpressions:
              - key: app.kubernetes.io/name
                operator: In
                values:
                  - my-app
          topologyKey: kubernetes.io/hostname

 

설정이 두 가지로 나뉜다.

 

podAffinity (같이 두기): redis-cache 와 같은 노드에 배치하도록 선호한다. 캐시 서버와 가까이 두면 네트워크 지연을 줄일 수 있다.weight: 100 으로 우선순위를 높게 설정했다.

 

podAntiAffinity (분산 배치): 같은 앱의 Pod끼리는 서로 다른 노드에 퍼지도록 유도한다. Pod가 한 노드에 몰리면 해당 노드 장애 시 전체 서비스가 중단될 수 있다. weight: 40 이 누적되면서 자연스럽게 균등 분산이 이루어진다.

 

preferred 방식이므로 강제는 아니다. 리소스가 부족한 상황에서는 규칙을 무시하고 배치된다.

강제로 분산하려면 requiredDuringSchedulingIgnoredDuringExecution을 쓰면 되지만, 노드가 부족하면 아예 스케줄링이 안 될 수 있어 주의가 필요하다.

 

11. 환경 변수 주입 — ConfigMap

config:
  SPRING_PROFILES_ACTIVE: "prod"

 

이 값은 Helm Chart가 ConfigMap으로 만들어 컨테이너에 환경 변수로 주입한다.

SPRING_PROFILES_ACTIVE=prod가 설정되면 Spring Boot는 application-prod.yml 을 자동으로 읽는다.

환경별로 다른 values 파일을 관리하면 동일한 Chart로 여러 환경을 대응할 수 있다.

 

# 개발 환경 배포
helm upgrade my-app ./chart -f values-dev.yaml

# 프로덕션 환경 배포
helm upgrade my-app ./chart -f values-prod.yaml

DB 패스워드나 API 키처럼 민감한 정보는 ConfigMap 대신 Secret에 넣고, 별도의 시크릿 관리 도구(Vault, Sealed Secrets 등)와 연계하는 것이 안전하다.

 

 

12. 메트릭 수집 — Actuator & Prometheus

actuator:
  port: 8081
  path: /actuator/prometheus
  interval: 30s

 

Spring Boot Actuator는 애플리케이션 내부 상태(JVM 메모리, HTTP 요청 수, GC 통계 등)를 Prometheus 형식으로 노출한다.

 

Prometheus가 이를 주기적으로 수집하고 Grafana로 시각화하면, 코드 한 줄 추가 없이 상세한 애플리케이션 대시보드를 얻을 수 있다.

Actuator 포트를 메인 포트(8080)와 분리하는 이유는 보안 때문이다. 메트릭 엔드포인트를 외부에 노출하지 않고, 클러스터 내부의 Prometheus만 접근할 수 있도록 제한할 수 있다.

HTTPRoute는 8080만 외부에 열고, Prometheus는 8081로 내부에서 수집하는 구조다.

 

application.yml 에서 관련 설정을 활성화한다.

management:
  server:
    port: 8081
  endpoints:
    web:
      exposure:
        include: health, prometheus
  metrics:
    export:
      prometheus:
        enabled: true

 

 

마치며

지금까지 Spring Boot를 쿠버네티스에 배포할 때 values.yaml 에서 다루는 주요 설정 12가지를 살펴보았다.

처음에는 항목이 많아 보이지만, 각 설정이 담당하는 역할이 명확히 분리되어 있다.

관심사 관련 설정
가용성 replicaCount, HPA, AntiAffinity
성능/비용 resources, NodeSelector
트래픽 제어 Service, HTTPRoute
안정성 Liveness/Readiness Probe
관측성 Actuator, podLabels
보안 imagePullSecrets, ConfigMap/Secret 분리

 

Helm Chart로 이 모든 설정을 values.yaml  한 파일로 관리하면, 환경별 차이를 코드로 추적할 수 있고 배포 재현성도 높아진다.