
발자국 3주차: 우아하게 종료하기, 우아하게 새로 켜기
해당 글은 인프런 워밍업 클럽 스터디 4기 - DevOps (쿠버네티스)를 진행하며 깊게 고민해 보려고 한 내용을 담습니다. 개괄적 정리보다 아는 것과의 비교를 통한 이해를 추구합니다.
우아하게 종료하기, 우아하게 새로 켜기
# 정상적인 프로세스 종료 (권장)
kill 1234
kill -15 1234
# 강제 종료 (SIGTERM이 안 될 때)
kill -9 1234 # SIGKILL
내가 서버와 관련된, 특히 프로세스와 관련된 생각을 할 때 가장 먼저 떠올리는 것은 [생명 주기]다.
잘 시작하는 것, 잘 살아 있는 것은 무엇보다 중요하지만, 내가 가장 중요하다고 생각하는 나의 가장 큰 관심사는.
바로 종료다.
잘 돌아가던 것은 잘못된 종료로 인해 한 순간에 와르르 실패할 수 있다.
따라서 배포는 기존의 것을 죽이고 → 새로운, 심지어 달라진 것을 올린다는 점에서 매우 중요한 작업이다.
지난주에 서버가 "살아있다"는 것의 의미를 깊이 고민했다면, 이번 주는 그 살아있는 서버를 어떻게 종료하고, 어떻게 새로운 코드를 안전하게 배포할 것인가를 파헤쳐봤다.
DevOps, 거대한 파이프라인
DevOps는 개발(Dev)과 운영(Ops)의 경계를 허물고 자동화를 통해 빠르고 안정적인 배포를 추구하는 문화이자 방법론.
개발 소스 → 커밋 → GitHub → CI/CD 환경 → 빌드 → 실행 파일 → 컨테이너 이미지 → k8s 배포
이 과정의 가장 처음 확인해야 하는 핵심은 무엇일까? 결국 실행 파일을 만드는 것이다.
DevOps가 아무리 복잡해 보여도, 개발 → 빌드 → 실행 파일이라는 본질은 변하지 않는다.
왜 환경을 나눠야 하는가
이에 따라 빌드 환경을 고려하는 것을 잊어서는 안 된다.
어디에서 실행하기 위해 빌드하는가? 무엇을 위해 분리하는가?
단일 환경으로 모든 것을 처리하려는 시도는 위험하다. 각 환경은 명확한 목적을 가지고 분리되어야 한다.
개발 환경: 개발자들의 통합 테스트 공간. 자유로운 실험이 가능해야 한다.
검증 환경: QA팀의 영역. 운영과 최대한 동일하되 보안 제약은 완화된 환경.
운영 환경: 실제 사용자를 위한 공간. 안정성이 최우선이다.
운영 환경에서 오픈소스를 도입할 때 이중화 가능 여부를 확인해야 하는 이유는 명확하다.
SPOF. 단일 장애점(Single Point of Failure)은 전체 서비스 중단으로 이어질 수 있기 때문이다.
예를 들어, 메시지 큐 시스템이 이중화를 지원하지 않는다면, 해당 시스템 장애 시 전체 서비스가 마비될 수 있다.
이제 상황에 따른 빌드 파일이 완료되었다면, 실제로 서버에 올릴 시간이다.
그냥 말고, 쿠버네티스랑 같이.
안 무서워?
쿠버네티스의 자동화된 배포는 분명 편리하지만, 나에게는 중요한 질문이 있었다.
"자동화된 배포가 정상적인 통신을 보장할 수 있는가?"
이번 주 강의에서는 그 안전장치에 대한 설정값을 보고, [가능성]을 확인하는 과정을 거쳤다.
쿠버네티스가 제공하는 안전장치
Readiness Probe의 역할
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
트래픽 자체에 대한 [통신 가능성] 확인이다.
새 Pod가 트래픽을 받기 전에 정말 준비되었는지 확인한다.
단순히 프로세스가 떴는지가 아니라, DB 연결, 캐시 워밍업, 외부 API 연결 등 실제 서비스에 필요한 모든 것이 준비되었는지 검증할 수 있다.
점진적 롤아웃
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
한 번에 하나씩만 교체하므로, 문제 발생 시 영향 범위를 최소화한다.
자동 롤백 메커니즘 새 버전의 Pod가 계속 실패하면 쿠버네티스가 자동으로 이전 버전을 유지한다.
그럼에도 남는 나의 두려움
물론 스프링 서버 단계에서 많은 걸 보완할 수 있다. 그리고 위의 것들로 많은 걸 보완할 수 있다.
하지만 하나하나 톺아 보면서 조금 더 든든하게 정리해 보자.
데이터 일관성 문제
해결 방안
기존 서버를 프로세스 종료까지 유지
데이터베이스 마이그레이션을 배포와 분리
이벤트 소싱 패턴으로 변경 이력 관리
두 버전이 공존할 수 있도록 하위 호환성 유지
이미 DB에 쓰인 데이터는 어떻게 할 것인가? 쿠버네티스는 애플리케이션 레벨의 트랜잭션을 관리하지 않는다.
이에 따라 생각한 나의 해결 방안은, 기존 서버를 [기존 프로세스가 실행될 때까지만] 살아 있게 하는 것이었다.
그리고 신규 서버로 인입되는 데이터에 대해서 확실한 표시를 하는 것.
그 부분을 쿠버네티스 매핑과 연결해 달라고, 클로드한테 부탁했는데.
기존 서버를 기존 프로세스가 실행될 때까지만 살아있게 유지
terminationGracePeriodSeconds
설정과 PreStop Hook을 활용해 진행 중인 프로세스가 완료될 때까지 Pod 종료를 지연시킬 수 있다.
데이터베이스 마이그레이션을 배포와 분리:
Helm Job이나 Init Container를 사용해 마이그레이션을 배포와 독립적으로 실행할 수 있다.
이벤트 소싱 패턴으로 변경 이력 관리
ConfigMap을 통해 이벤트 스토어 설정을 관리하고, StatefulSet으로 이벤트 저장소의 순서를 보장할 수 있다.
두 버전이 공존할 수 있도록 하위 호환성 유지
롤링 업데이트의
maxSurge
와maxUnavailable
설정으로 두 버전이 동시에 실행되는 기간을 제어할 수 있다.
신규 서버로 인입되는 데이터에 대해서 확실한 표시
라벨과 어노테이션을 활용해 Pod 버전을 명시하고, 애플리케이션에서 이를 참조할 수 있다.
로컬 캐시 불일치 :
구 버전과 신 버전이 동시에 실행되면서 캐시 데이터가 불일치할 수 있다.
해결 방안
배포 시 캐시 무효화 전략 수립
버전별 캐시 키 분리
기존 캐시에 대해서 소멸 시간을 설정한 만큼 기존 서버를 살려두기
새로운 캐시에 대해서는 버전 처리
다음으로 떠오른 것은 로컬 캐시. 우리는 서버의 가용성을 위해 어떠한 정보들을 저장해 둔다.
이런 부분에 대해서 떠오른 해결 방안 또한 무효화와 버저닝이었다.
기존 캐시에 대해서 소멸 시간을 설정한 만큼 기존 서버를 살려 둘 수도 있다.
또한 새로운 캐시에 대해서는 버전 처리를 할 수 있다.
사실 가장 좋은 건 로컬 캐시에 이런 [사용자성] 데이터를 넣지 않는 편이 아닐까 싶지만.
이번 부분도 쿠버네티스 매핑과 연결해 보자면.
배포 시 캐시 무효화 전략 수립
ConfigMap 업데이트를 통해 캐시 무효화 신호를 전달하고, Rolling Restart를 트리거해서 캐시를 초기화할 수 있다.
버전별 캐시 키 분리
Pod의 라벨이나 환경변수를 통해 버전 정보를 주입하고, 이를 캐시 키 접두어로 사용해서 버전별로 캐시를 분리할 수 있다.
기존 캐시에 대해서 소멸 시간을 설정한 만큼 기존 서버를 살려두기
terminationGracePeriodSeconds
를 캐시 TTL과 동일하게 설정해서 캐시가 자연스럽게 만료될 때까지 기존 Pod를 유지할 수 있다.
새로운 캐시에 대해서는 버전 처리
Deployment의
metadata.labels
에 버전 정보를 포함하고, 이를 애플리케이션에서 참조해서 새로운 캐시 키에 버전을 포함시킬 수 있다.
세션 문제 사용자가 구 버전에서 신 버전으로 넘어갈 때 세션이 유실될 수 있다.
해결 방안
Redis 같은 외부 세션 스토어 사용
Sticky Session 설정 (단, 롤링 업데이트 효과 감소)
그리고 세션. 아무래도 서버가 사라질 때 기존 서버에 붙어 있던 세션들도 함께 끊겨 버릴 수 있으니까.
이 부분도 똑같이 해당 프로세스가 마칠 때까지의 Sticky Session이 떠올랐다.
또는 세션 자체의 Store를 밖에 두는 것. 상황에 따른 의사결정이 필요한 시점 같다.
이번에도 클로드의 힘을 빌려 보자면.
Redis 같은 외부 세션 스토어 사용
StatefulSet으로 Redis 클러스터를 구성하고 Service로 엔드포인트를 관리할 수 있다.
Sticky Session 설정
Ingress Controller의 어노테이션(
nginx.ingress.kubernetes.io/affinity: "cookie"
)을 사용해 쉽게 설정할 수 있다.
이제 이 프로세스의 장점을 담아, 자동 배포를 위한 젠킨스를 실행해 보자.
Jenkins 파이프라인 구축 여정
1단계: 기본 구성
처음에는 Jenkins UI를 통해 각 단계를 개별 Job으로 구성할 수 있다.
소스 코드 체크아웃
Gradle 빌드
Docker 이미지 생성
kubectl 배포
처음엔 Jenkins UI에서 클릭클릭하며 Job을 만든다.
GitHub에서 소스 가져오고, Gradle로 빌드하고, Docker 이미지 만들고, kubectl로 배포하고.
각 단계마다 "Build Now" 버튼을 눌러야 했다. 수동으로 트리거해야 했고, 전체 흐름을 파악하기 어려웠다.
2단계: Pipeline으로의 전환
그러다 Pipeline의 시작. Jenkins Pipeline은 코드로 파이프라인을 정의하는 방식이다.
Stage View로 진행 상황 시각화
Jenkinsfile로 버전 관리 가능
병렬 처리 및 조건부 실행 지원
시각적인 Stage View가 제공되고, 각 단계별 소요 시간이 보이고, 무엇보다 Jenkinsfile로 버전 관리가 가능하다.
3단계: Blue/Green 배포 구현
다음부터는 배포 방식.
Blue/Green 배포는 두 개의 동일한 환경을 준비하고 트래픽을 한 번에 전환하는 방식이다.
메모리: 두 배의 리소스가 필요하다. CPU는 기동 시에만 피크를 치지만, 메모리는 지속적으로 점유한다.
네이밍 전략: blue-v1, green-v2와 같은 일관된 네이밍이 필요하다.
Label 관리: Service의 Selector를 변경하기 위한 적절한 Label 설계가 중요하다.
Blue/Green 배포 - 쿠버네티스와 전통적 방식의 차이
사실 이 배포 방식에 대해서는 이해하고 있었지만, k8s에서 어떻게 진행하는지를 한 번 더 비교해 보고 싶었다.
그리고 정리한 것은 다음과 같았다.
전통적인 Blue/Green
기존의 블루/그린 전략은 다음과 같다.
L4 로드밸런서에서 수동으로 트래픽 전환
물리 서버나 VM 단위로 환경 분리
스크립트로 복잡한 전환 로직 구현
롤백 시 다시 수동으로 전환
쿠버네티스의 Blue/Green
# Service의 selector만 변경
selector:
app: myapp
version: green # blue → green
그리고 쿠버네티스의 블루/그린.
핵심 차이점
이제 전체 배포 방식에 대해 비교해 보고 싶었고, 그 내용이 다음과 같았다.
내가 기존에 알고 있던 카나리를 포함해서.
배포 전략의 선택 기준
Jenkins 파이프라인으로 배포 자동화를 구축했지만, 곧 새로운 문제를 마주한다.
개발, 검증, 운영 환경마다 다른 설정값들. 서비스가 늘어날수록 관리해야 할 YAML 파일이 기하급수적으로 증가한다.
Helm과 Kustomize
Helm
Helm은 쿠버네티스의 패키지 매니저다. apt나 yum처럼 복잡한 애플리케이션을 쉽게 설치하고 관리할 수 있게 해준다.
핵심 개념
Chart: Helm 패키지. 쿠버네티스 리소스를 정의하는 템플릿 모음
Values: 차트에 주입할 설정 값
Release: 차트의 인스턴스. 같은 차트로 여러 릴리즈 생성 가능
Helm은 이건 마치 프로그래밍의 함수와 같았다.
템플릿이라는 함수에 values라는 매개변수를 전달하면, 원하는 YAML이 생성되는 방식.
Kustomize란?
Kustomize는 YAML 파일을 직접 수정하지 않고 패치를 통해 커스터마이징하는 도구다.
kubectl에 내장되어 있어 별도 설치가 불필요하다.
핵심 개념
Base: 기본 리소스 정의
Overlay: 환경별 커스터마이징
Patch: 특정 필드만 수정하는 파일
Kustomize의 접근법은 더 직관적이었다. "기본 YAML은 그대로 두고, 환경별로 다른 부분만 덮어쓰자"는.
이 두 가지를 언제 선택해야 할까?
선택 기준
Helm을 선택해야 할 때
복잡한 의존성을 가진 애플리케이션
여러 환경에 동일한 애플리케이션을 다른 설정으로 배포
커뮤니티 차트를 활용하고 싶을 때 (Artifact Hub)
팀원들과 표준화된 배포 방식을 공유하고 싶을 때
Kustomize를 선택해야 할 때
간단한 YAML 커스터마이징만 필요한 경우
템플릿 문법 없이 순수 YAML을 유지하고 싶을 때
kubectl과의 네이티브 통합을 선호할 때
작은 규모의 프로젝트
특히 와닿았던 부분은 "대부분의 오픈소스가 Helm 차트로 제공된다"는 점이었다.
실제로 Prometheus, Grafana, Redis 등을 설치할 때 Helm을 사용하지 않으면 수십 개의 YAML을 직접 관리해야 한다.
반면 작은 마이크로서비스 하나를 여러 환경에 배포할 때는 Kustomize의 단순함이 빛을 발할 것이다.
템플릿 문법을 배울 필요 없이 바로 사용할 수 있으니까.
이처럼 배포 방식이 단순해질수록, 그 배포를 자동화하는 CI/CD 도구의 선택도 함께 고민해야 한다.
어떤 도구를 선택하느냐에 따라 개발 생산성과 운영 효율이 크게 달라지기 때문이다.
CI/CD 도구 선택의 지혜
커뮤니티 활성도: Google Trends, GitHub Stars, Stack Overflow 질문 수
유지보수 지원: 상용 지원이 필요한가? 내부 전문가가 있는가?
인프라 요구사항: 온프레미스 필수인가? 클라우드 네이티브가 가능한가?
보안 요구사항: 외부 서비스 사용이 가능한가? 에어갭 환경인가?
예를 들어, 금융권처럼 보안이 중요한 환경에서는 GitHub Actions보다 Jenkins나 Tekton같은 온프레미스 솔루션이 적합하다.
반면 스타트업처럼 빠른 구축이 중요하다면 GitHub Actions가 더 나은 선택일 수 있다.
마무리하며
이번 주를 통해 배포는 더 이상 두려운 작업이 아닌, 체계적인 프로세스임을 배웠다.
하지만 쿠버네티스의 자동화된 배포가 만능은 아니다. 데이터 일관성, 버전 호환성, 상태 관리 등 여전히 개발자가 신경 써야 할 부분들이 많다.
적절한 도구와 전략을 선택하되, 그 한계를 어떻게 극복할지 명확히 인지하고 있어야 한다.
다음 주에는 ArgoCD를 통한 GitOps, 그리고 더 고도화된 배포 자동화를 다룬다고 한다.
배포의 여정은 계속된다!
댓글을 작성해보세요.
이번에도 잘 봤어요^^