| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- NIO
- netty
- 성능 최적화
- prometheus
- RDBMS
- kafka
- mysql
- GitOps
- spring boot
- 데이터베이스
- jvm
- SpringBoot
- docker
- Kotlin
- selector
- grafana
- CloudNative
- JPA
- Java
- 트랜잭션
- DevOps
- monitoring
- 동시성제어
- helm
- 백엔드
- Kubernetes
- redis
- webflux
- 백엔드개발
- 성능최적화
- Today
- Total
유성
Helm Chart로 Spring Boot 프로덕션 배포하기, 선언적 배포 본문
쿠버네티스에 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 한 파일로 관리하면, 환경별 차이를 코드로 추적할 수 있고 배포 재현성도 높아진다.