한 줄 정의
핵심 메시지
클라우드 네이티브 환경에서의 자바 배포는 로컬 컨테이너 → 오케스트레이션 클러스터 → 점진적 릴리스 전략이라는 세 층의 추상화를 동시에 다루는 일입니다. 로컬에서는 도커 컴포즈와 틸트로 멀티 컨테이너 워크플로를 만들고, 클러스터에서는 쿠버네티스의 Pod·서비스·인그레스로 트래픽과 라우팅을 추상화하며, 릴리스는 블루/그린, 카나리아, 기능 플래그로 배포와 릴리스를 분리해 변경 위험을 통제합니다.
자바 특유의 함정도 있습니다. JVM이 컨테이너의 cgroups 힌트를 제대로 읽지 못하면 호스트 전체 메모리를 가정해 OOMKilled가 발생하고, 단일 코어 컨테이너에서는 G1이 직렬 컬렉터로 떨어져 일시 정지가 길어집니다. 최신 LTS(17/21) + 2코어 이상 +
-XX:MaxRam이 컨테이너 환경 자바의 합리적 디폴트입니다.
쉽게 말하면
옛날 배포는 “운영팀이 서버에 jar 올리고 java -jar 실행”이었습니다. 클라우드 네이티브 배포는 “개발팀이 컨테이너 이미지를 만들고, 오케스트레이터가 어디서 몇 개 띄울지 알아서 결정”하는 방식입니다. 이 과정에서 데브옵스라는 개념이 등장했습니다.
로컬에서는 도커 컴포즈로 5개 마이크로서비스를 한 번에 띄우고, 틸트로 코드 변경을 자동 재배포합니다. 운영에서는 쿠버네티스가 Pod를 노드에 스케줄링하고, 서비스가 DNS 엔트리를 만들어주며, 인그레스가 외부 트래픽을 받아들입니다.
새 버전을 내보낼 때는 한 번에 다 바꾸지 않습니다. 블루/그린은 환경 두 벌을 두고 스위치를 전환하고, 카나리아는 트래픽의 일부만 새 버전으로 보내며 문제가 없을 때 점진적으로 비율을 늘립니다. 기능 플래그는 코드 한 줄로 기능을 켜고 끄는 방식으로 릴리스를 배포에서 완전히 분리합니다.
왜 중요한가?
빌드와 실행이 한 팀이 되어야 효율이 오른다
전통적으로 개발팀이 만들고 운영팀에 넘기는 모델이었지만, 클라우드 환경에서는 이 경계가 흐려졌습니다. ‘빌드와 실행’(build and run) 팀으로 운영 방식을 통합하면 개발과 운영 간 오해·좌절·오류가 줄고, 팀이 스택 전체에 대한 전문성과 책임감을 갖게 됩니다. 결과적으로 업무 만족도가 올라가고 참여도가 증가합니다.
로컬에서 클러스터와 비슷한 환경을 만들 수 있어야 한다
개발자 머신의 OS와 실제 런타임 환경 간 차이는 오랜 골칫거리였습니다. 컨테이너는 로컬에서 배포 시점과 더 유사한 환경을 만들어주고, 도커 컴포즈는 5개 마이크로서비스의 DNS 의존성까지 로컬에서 재현할 수 있게 해줍니다. 이게 없으면 “내 로컬에서는 됐는데”가 반복됩니다.
배포 ≠ 릴리스를 분리해야 변경 위험이 통제된다
배포는 시스템에 코드를 올리는 일이고, 릴리스는 최종 사용자에게 기능을 노출하는 일입니다. 이 둘을 같은 작업으로 묶으면 변경마다 사용자 경험에 직접 영향을 줍니다. 둘을 분리하면 배포 빈도를 자유롭게 가져가면서도 릴리스는 통제할 수 있고, 카나리아·블루/그린·기능 플래그가 그 분리의 기술적 도구입니다.
자바는 컨테이너 환경에서 추가로 신경 쓸 게 있다
대다수 자바 애플리케이션이 컨테이너에 배포된다는 조사가 있지만(2024년 시점 70% 이상), 단순히 jar를 이미지에 넣는다고 끝이 아닙니다. 콜드 스타트, cgroups 인식, 가비지 컬렉터 선택, 메모리 한계 모두 컨테이너 환경에서 추가로 고려해야 할 변수입니다. 옛 JVM은 호스트 메모리를 자기 것으로 착각해 OOMKilled를 일으켰고, 단일 CPU에서는 동시 컬렉터가 동작하지 않습니다.
핵심 내용
9.1 로컬 환경에서 컨테이너 작업하기
컨테이너의 장점 중 하나는 로컬 머신에서 배포 시점과 더 유사한 환경을 만든다는 점입니다. 이를 통해 개발자의 머신 운영체제와 실제 런타임 환경 간의 차이가 문제가 되는 일을 방지합니다.
단일 컨테이너 실행의 한계
Fighting Animals 데모에서 mammal_demo 하나만 빌드·실행하는 흐름은 다음과 같습니다.
git clone https://github.com/kittylyst/fighting-animals.git .
git checkout main
mvn clean package
docker build -t mammal_demo -f src/main/docker/mammal/Dockerfile .
docker run -p 8081:8081 -t mammal_demo
curl localhost:8081/getAnimal
{"timestamp":"2024-04-29T17:18:00.170+00:00","status":500,
"error":"Internal Server Error","path":"/getAnimal"}500 에러가 발생하는 이유는 MammalController가 다른 서비스를 호출하기 때문입니다. 서비스 맵은 코드 수준에서 DNS 의존성을 그대로 드러냅니다.
private static final Map<String, String> SERVICES =
Map.of(
"mustelids", "http://mustelid-service:8084/getAnimal",
"felines", "http://feline-service:8085/getAnimal");mustelid-service, feline-service는 URL에 등장하는 명명된 서비스(named service)입니다. 오케스트레이션 플랫폼에서는 DNS가 이 이름을 해석해주지만, 로컬에서도 이를 재현해야 합니다.
9.1.1 도커 컴포즈
도커 컴포즈(Docker Compose)는 멀티 컨테이너 도커 애플리케이션을 로컬에서 정의하고 실행하는 도구입니다. docker-compose.yml에 서비스 구성과 의존성을 적고, docker-compose up 한 번으로 전부 띄울 수 있습니다.
Fighting Animals 예제의 docker-compose.yml은 5개 서비스(animal, mammal, mustelid, feline, fish)와 각자의 포트·depends_on 토폴로지를 정의합니다.
# Fish service
fish-service:
image: fish_demo:latest
ports:
- "8083:8083"
# Mustelid service
mustelid-service:
image: mustelid_demo:latest
ports:
- "8084:8084"
# Feline service
feline-service:
image: feline_demo:latest
ports:
- "8085:8085"
# Mammal service
mammal-service:
image: mammal_demo:latest
ports:
- "8081:8081"
depends_on:
- feline-service
- mustelid-service
# Animal service
animal-service:
image: animals_demo:latest
ports:
- "8080:8080"
depends_on:
- fish-service
- mammal-service명명된 서비스가 동작하는 원리
depends_on 절은 서비스의 토폴로지를 정의하며, 서비스 이름(예: mustelid-service)은 다른 컨테이너가 참조할 수 있는 경량 DNS 엔트리가 됩니다. curl localhost:8081/getAnimal은 mammal 서비스의 응답을 받을 수 있지만, 컨테이너 외부에서는 명명된 서비스가 보이지 않는다는 점이 중요합니다.
명명된 서비스 추상화는 로컬 서비스 이름을 생성하는 데 유용하며, 오케스트레이션 시스템에서도 일관되게 작동합니다. 즉 로컬과 클러스터가 같은 DNS 모델을 공유합니다.
9.1.2 틸트
틸트(Tilt, tilt.dev)는 마이크로서비스를 사용한 로컬 워크플로를 만들기 위해 다양한 도구를 효율적으로 조율하는 툴킷입니다. Tiltfile을 작성해 컴파일·실행·배포 레시피를 정의하고, local_resource로 식별된 파일이 변경되면 애플리케이션의 일부를 자동으로 다시 배포합니다.
local_resource(
'monorepo-java-compile',
'mvn clean package',
deps=['src', 'pom.xml'])
docker_build(
'animals_demo',
'.',
dockerfile='./src/main/docker/animal/Dockerfile')
// ... All other docker_build tasks elided
docker_compose("deploy/docker-compose.yml")틸트 UI의 가치
monorepo-java-compile 작업은 코드가 변경되면 재컴파일하고, docker_build가 모든 이미지를 빌드하며, docker_compose가 설정에 따라 다시 배포합니다. 틸트 UI는 빌드·실행 중인 컨테이너 상태에 대한 시각적 피드백을 제공하고, 실행 중인 컨테이너의 로그를 빠르게 확인할 수 있어 로컬 인너 루프(inner loop)의 마찰을 크게 줄여줍니다.
9.2 컨테이너 오케스트레이션
컨테이너를 오케스트레이션 하는 방법은 여러 가지지만, 이 절에서는 가장 널리 사용되는 쿠버네티스(Kubernetes)에 초점을 맞춥니다.
'쿠버네티스'의 어원
그리스어로 ‘조타수’ 또는 ‘부 파일럿’을 의미합니다. 헬름(Helm), 키얼(kubectl) 등 관련 도구 상당수가 이 주제를 따라 이름을 지었습니다.
제어 플레인과 데이터 플레인
클라우드 네이티브 컨테이너 오케스트레이션은 일반적으로 두 가지 주요 구성 요소로 구현됩니다.
| 구성 요소 | 역할 |
|---|---|
| 제어 플레인 (control plane) | 데이터 플레인의 상태를 조정하기 위해 작업이 수행되는 곳 |
| 데이터 플레인 (data plane) | 작업이 실제로 수행되는 곳. Fighting Animals 5개 서비스가 실행되는 곳 |
개발자 관점에서는 서비스가 실행되는 위치와 이를 참조하는 방식이 제어 플레인이 처리하는 플랫폼 문제로 추상화됩니다.
일관성 ≠ 즉시성
제어 플레인의 작업은 결국 일관성을 가지며, 작업이 데이터 플레인에 적용되기까지 시간이 걸립니다. 쿠버네티스는 설계상 일관성과 가용성 간의 균형에서 가용성을 우선시합니다(CAP 정리의 AP).
쿠버네티스 작업의 핵심 명령은 kubectl이며, 실무에서는 k라는 별칭을 자주 사용합니다.
9.2.1 배포
배포(Deployment)는 데이터 플레인에서 실행 중인 워크로드에 대해 어떤 작업이 수행되어야 하는지를 정의합니다. deployment-mammal.yaml 예제는 다음과 같습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: mammal-service
spec:
replicas: 1
selector:
matchLabels:
app: mammal-service
template:
metadata:
labels:
app: mammal-service
spec:
containers:
- name: mammaldemo
image: mammal_demo
ports:
- containerPort: 8081kubectl apply -f deployment-mammal.yaml을 실행하면 클러스터에 해당 배포가 적용됩니다. Deployment가 적용되면 쿠버네티스 스케줄러는 Pod를 생성한 뒤 이를 데이터 플레인 노드에 배포합니다.
kubectl get Pods
NAME READY STATUS RESTARTS AGE
mammal-service-79b4ccb9bb-4bqrj 1/1 Running 0 3m56s
1/1은 “예상된 1개 컨테이너 중 1개 실행 중”을 의미합니다. 즉 이 Pod는 안정적인 상태에 도달한 것입니다.
9.2.2 Pod 내부 공유
Pod는 쿠버네티스 클러스터 노드에 하나 이상의 컨테이너가 함께 배포되는 단위입니다. Pod를 사용한 개발은 강력한 추상화를 제공하며, 여러 일반적인 배포 아키텍처의 핵심 요소가 됩니다.
Pod의 세 가지 공유 특성
- Pod 내부에서 실행되는 컨테이너는 공유 저장소 볼륨과 네트워크에 대한 접근 권한을 공유합니다
- Pod에 함께 배포된 컨테이너는 서로 긴밀한 연결 또는 구성적 의존성을 가집니다
- 단일 Pod에 배포된 컨테이너는
localhost를 공유합니다. 이는 네트워크 스택을 물리적 네트워크에 도달하기 전에 트래픽을 가로채는 루프백 어댑터(loopback adapter) 덕분입니다
단일 Pod 내부의 localhost 공유는 프로세스 외부에서 통신하는 지연에 민감한 서비스를 함께 배포할 수 있게 합니다. 인프라 수준 프로젝트에서 자주 사용됩니다. 다른 Pod로 데이터를 송신할 때는 추가 네트워크 지연이 발생하며, 연결된 서비스가 다른 노드에 있다면 영향이 더 커집니다.
서비스 메시: Pod 패턴의 응용
서비스 메시(service mesh)는 Pod를 기반으로 구성되며, 네트워크 트래픽과 라우팅을 제어할 수 있도록 설정된 CNCF 프로젝트들의 집합입니다. Istio(istio.io) 같은 프로젝트는 사용자의 애플리케이션 컨테이너와 동일한 Pod에 엔보이 프록시(envoy proxy)를 배포합니다. 애플리케이션 코드 변경 없이 프록시를 통한 라우팅이 가능해 보이지만, 실제로는 Pod 내부 IP 테이블(라우팅과 방화벽 규칙)을 조작해 로컬 Pod 수준 네트워크 동작을 변경합니다.
이 설정은 Istio가 제공하는 init-container 프로세스가 Pod 내부에서 실행됩니다. 해당 프로세스는 Pod의 IP 테이블에 필요한 상태와 구성을 설정한 뒤 종료되는 방식으로 동작합니다.
서비스 메시가 제공하는 추가 기능
- TLS/mTLS 강제: 프록시가 클러스터 통과 트래픽에 TLS 또는 mTLS를 강제할 수 있습니다. Pod 수준의 인터셉트 덕분에 개발자에게 투명하게 처리됩니다
- 트래픽 가시화: 트래픽이 프록시에서 암호화되므로 프록시는 암호화되지 않은 페이로드에 접근할 수 있어, 네트워크 수준에서 텔레메트리 데이터를 통합할 수 있는 지점이 생깁니다
- 세분화된 라우팅: 대상 서비스로 가는 트래픽을 세분화해 라우팅 가능. 예를 들어 실제 사용자 트래픽을 배치 작업이나 장시간 실행되는 프로세스보다 우선 처리하는 라우팅이 가능
Pod는 더 복잡한 시스템의 구성 요소로 일관된 추상화를 제공합니다. 공유 자원을 일관되게 조작·구성할 수 있는 기능 덕분에, Pod는 물리적 노드 위에서 유연하고 일관된 추상화 계층을 제공합니다.
9.2.3 컨테이너와 Pod의 수명 주기
분산되고 스케줄링된 환경에서 실행되는 애플리케이션은 추가적인 비기능적 요구 사항을 충족해야 합니다.
생존성 / 준비 상태 검사
| 검사 | 의미 |
|---|---|
| 생존성 (liveness) | 애플리케이션이 정상적으로 실행 중인지 평가 |
| 준비 (readiness) | 프로세스가 요청을 처리할 준비가 되었는지 표시 |
준비 상태를 정의하는 것은 구현자나 설계자의 몫이며, 모든 의존성이 요청을 성공적으로 처리할 준비가 되었는지를 고려해야 합니다. 독립적인 컨테이너 상태와 시스템 전체의 건강 상태를 관찰할 수 있어야 합니다.
Pod 수명 주기 단계
Pod는 컨테이너 수준의 생존성 및 준비 상태 검사와 직접 연결된 현재 수명 주기 단계를 통해 실행됩니다. Pod의 조건은 다음을 포함합니다.
- PodScheduled
- ContainersReady
- Initialized
- Ready
애플리케이션에서 정확한 생존성 및 준비 상태 검사를 구현하면, Pod가 준비 상태가 되기 전에는 서비스에 연결되거나 로테이션에 포함되지 않습니다. 잘못 구현된 헬스체크는 트래픽 흐름 시스템이 신뢰성과 회복력을 제공하지 못하게 만듭니다.
9.2.4 서비스
Service는 클러스터에 배포된 Pod에 대한 추상화 계층을 제공하며, 경량 DNS 엔트리를 광고하고 클러스터 내에서 라우팅하는 기본 역할을 합니다.
apiVersion: v1
kind: Service
metadata:
name: mammal-service
spec:
selector:
app: mammal-service
ports:
- protocol: TCP
port: 8081
targetPort: 8081다음 예제에서는 mammal-service 서비스를 배포합니다. 이를 통해 클러스터 수준에서 DNS 엔트리가 생성되며, 이는 도커 컴포즈에서 사용된 것과 유사합니다.
Pod는 휘발성, 서비스는 안정적
Pod는 휘발성이기 때문에 내부에서 실행 중인 컨테이너에 일관되게 연결하려면 클러스터 전체에서 여러 Pod로 라우팅하는 기능이 필요합니다. 이것이 바로 서비스가 필요한 이유입니다.
헬름과 커스터마이즈
간단한 배포에서는 문자열 메타데이터를 수동으로 입력하고 재입력하는 것이 괜찮을 수 있습니다. 하지만 이는 금방 복잡해질 수 있습니다. 헬름(Helm, helm.sh)이나 커스터마이즈(Kustomize, kustomize.io)와 같은 도구는 데이터 타입, 템플릿, 변수 등을 도입해 이러한 문제를 해결합니다.
kubectl get services를 실행하면 기본 네임스페이스에서 실행 중인 서비스가 출력됩니다.
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 16d
mammal-service ClusterIP 10.105.113.150 <none> 8081/TCP 57s
9.2.5 클러스터의 서비스에 연결하기
지금까지 생성된 Pod와 서비스는 클러스터 내부에서만 보입니다. 외부 진입점을 제공하려면 외부에서 접근할 수 있는 IP 주소를 가진 LoadBalancer를 만드는 방식을 일반적으로 사용합니다.
Fighting Animals 예제에서는 animal service만 클러스터 외부에 노출하면 됩니다. 다른 서비스들은 내부용으로만 참조됩니다.
apiVersion: v1
kind: Service
metadata:
name: animal-service
spec:
type: LoadBalancer
selector:
app: animal-service
ports:
- protocol: TCP
port: 8080
targetPort: 8080kubectl get services 결과에서 animal-service는 EXTERNAL-IP를 가지며(20.108.87.2), http://20.108.87.2:8080으로 외부에서 접근할 수 있습니다.
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
animal-service LoadBalancer 10.106.223.136 20.108.87.2 8080:31762/TCP
feline-service ClusterIP 10.101.233.232 <none> 8085/TCP
fish-service ClusterIP 10.101.40.193 <none> 8083/TCP
mammal-service ClusterIP 10.111.138.65 <none> 8081/TCP
mustelid-service ClusterIP 10.98.142.128 <none> 8084/TCP
LoadBalancer 구현은 사용하는 쿠버네티스 배포판에 따라 다릅니다. 클라우드 제공자 환경에 배포할 경우, 제공자의 네트워크에서 제공하는 로드 밸런싱 솔루션을 사용하게 됩니다.
9.2.6 컨테이너와 스케줄링의 도전 과제
로컬에서의 설정과 실행은 비교적 간단합니다. 하지만 대규모 운영 환경에서는 다양한 문제에 직면할 수 있습니다.
관측성 3축의 필수성
쿠버네티스 워크로드를 대규모로 운영하려면 3가지 관측성(3-pillar observability)이 필수입니다. 클러스터의 애플리케이션 수와 노드 수가 증가하면 현재 문제나 과거 문제의 근본 원인을 찾는 것이 어려워질 수 있으며, 여러 서비스를 통과한 오류 요청의 경로를 파악하기도 까다롭습니다. 로그를 수동으로 살펴볼 수도 있지만, 실행 중인 여러 인스턴스가 있는 경우 어떤 Pod와 컨테이너가 오류 요청에 관여했는지 찾는 데 시간이 많이 소요됩니다.
콜드 스타트와 이미지 크기
콜드 스타트(cold start)는 로컬 컨테이너 레지스트리에 없는 컨테이너를 요청할 때 발생하는 상황입니다. 이미지가 클수록 다운로드와 프로세스 시작에 걸리는 시간이 길어집니다. 장시간 실행되는 프로세스에서는 시작 비용이 긴 실행 시간에 비례해 상쇄되지만, 몇 초만 실행되는 애플리케이션에서는 이 비용이 매우 크게 작용합니다.
이미지 크기는 문제의 일부일 뿐입니다. 특정 프레임워크에서는 자바의 시작 시간이 느리다는 문제가 있습니다. 그러나 AOT 컴파일(6.4.1절)을 활용하면 자바 애플리케이션은 약 0.029초 만에 시작할 수 있고 이미지 크기도 크게 줄일 수 있습니다.
imagePullPolicy
쿠버네티스는 노드 수준에서 로컬 컨테이너 레지스트리를 유지·관리하며, 캐시된 이미지를 저장함으로써 콜드 스타트 영향을 줄입니다. Deployment 객체에는 로컬과 원격 레지스트리 간의 관계를 어떻게 처리할지 결정하는 imagePullPolicy가 설정되어 있습니다.
| 정책 | 동작 |
|---|---|
| IfNotPresent | 노드에 이미지가 없는 경우만 가져옴. 이미지에 :latest 태그를 사용하지 않은 경우 기본값 |
| Always | 항상 원격 레지스트리에서 새 이미지를 가져옴. 필요한 새 레이어만 가져오는 검사는 수행됨. 레지스트리 태그가 불변하지 않거나 기존 이미지 태그를 업데이트할 수 있는 경우 필요할 수 있음 |
| Never | 원격 레지스트리를 절대 참조하지 않음. 컨테이너 레지스트리에 이미지를 로드할 기타 메커니즘이 구성되어 있어야 함 |
:latest 태그 회피
프로덕션 클러스터에서 이미지를 지정할 때 :latest 태그를 사용하는 것은 피해야 합니다. 두 가지 이유가 있습니다. 첫째, Always 정책을 설정하는 것과 같아 불필요한 네트워크 트래픽을 유발합니다. 둘째, 더 중요한 점은 버전화된 이미지를 사용하지 않으면 프로덕션 환경에 통제되지 않은 변경이 일어날 수 있다는 것입니다.
최악의 경우 이는 호환성을 깨는 변경사항(breaking changes)을 의미할 수 있습니다. 더 위험한 점은 배포가 의도하지 않게 변경될 가능성입니다. 예를 들어, 애플리케이션이 자동 스케일링되어 이전과 새로운 버전이 혼합될 수 있습니다. 규제된 산업이나 감사 산업에서는 심지어 법적 책임을 초래할 수 있습니다.
태그는 불변하지 않다
태그는 불변하지 않으므로, 이미지 의존성이 어떻게 작동하는지 조사하는 것은 클러스터 운영 시 중요한 고려 사항입니다.
노드 배치와 보안
프로덕션 클러스터는 항상 여러 노드를 포함해야 하며, 노드 배치는 중요한 고려 사항입니다. 노드는 다른 데이터 센터나 공개 클라우드 가용성 영역에 위치해야 합니다. 쿠버네티스는 일반적으로 클러스터의 여러 노드에 컨테이너를 원활하게 분배합니다. 그러나 운영자는 노드 친화성(node affinity)을 사용하여 노드에 대한 컨테이너 배치/비배치를 제어할 수도 있습니다.
쿠버네티스 클러스터의 보안은 중요한 도전 과제로, 신중하게 고려해야 합니다. 제어 플레인은 해커의 주요 타깃입니다. 제어 플레인을 조작하면 전체 클러스터를 위험에 빠뜨릴 수 있기 때문입니다. OWASP 보안 차트 시트는 유용한 출발점입니다. 인그레스 포인트는 처음부터 보안을 설정해야 하며, 이들은 종종 널리 공개됩니다(저자 워크숍에서 보안이 취약한 데모는 15분 후 활발한 공격을 받음).
namespaces 사용은 큰 주제지만, 애플리케이션 그룹이 커짐에 따라 운영 위험을 줄이기 위한 핵심 메커니즘입니다. 네임스페이스는 본질적으로 클러스터에서 리소스를 그룹화하는 것이며, 격리와 네임스페이스 내에서만 적용되는 설정을 제공합니다. 제어 플레인에서 네임스페이스로의 접근을 차단할 수 있으므로, 한 팀의 운영자만 해당 팀의 네임스페이스에 리소스를 배포할 수 있습니다.
9.2.7 ‘Remocal’ 개발을 활용한 원격 컨테이너 작업
또 다른 방법은 로컬 컨테이너를 원격 클러스터의 일부처럼 보이게 하는 것입니다. remocal 개발(remote + local)의 장점은 로컬에서 IDE와 컨테이너를 사용하여 디버깅 또는 프로파일링 도구를 활용할 수 있다는 점입니다. 복잡한 아키텍처에서는 애플리케이션을 완전히 테스트하기 위해 모든 서비스를 로컬에서 실행할 필요가 없다는 장점이 있습니다.
텔레프레즌스(Telepresence, telepresence.io)는 로컬 머신과 클러스터 간에 프록시를 생성하여 이러한 기능을 제공하는 도구입니다. 어떤 서비스는 로컬에서 해결하고, 어떤 서비스는 원격 클러스터에서 해결할지 설정할 수 있습니다.
9.3 배포 기술
클라우드 네이티브 환경에서 작업할 때, 배포와 릴리스의 차이를 이해하면 소프트웨어 변경 사항을 적용하는 새로운 기술을 활용할 수 있습니다.
| 용어 | 의미 |
|---|---|
| 배포 (deployment) | 애플리케이션 구성 요소(코드/설정)나 인프라를 변경 |
| 릴리스 (release) | 변경된 기능이나 사항이 최종 사용자에게 제공됨 |
배포와 릴리스를 별개의 작업으로 구분하면 두 가지 주요 결과를 얻을 수 있습니다.
- 배포는 기능을 릴리스하지 않고도 프로덕션 시스템을 변경할 수 있으므로, 배포 빈도가 더 잦아질 수 있습니다.
- 릴리스는 사용자에게 보이는 동작을 변경하지만, 배포는 릴리스로 이어질 수도 있고 그렇지 않을 수도 있습니다.
예를 들어, 새 fish_demo 버전을 프로덕션 환경에 배포했더라도 새로운 기능은 프로덕션 시스템과의 상호작용에 의해 활성화되거나 실행되지 않을 수 있습니다.
이 절에서 논의할 문제를 진단하는 데 유용한 세 가지 배포 기술은 다음과 같습니다.
- 블루/그린 배포
- 카나리아 배포
- 기능 플래그와 이를 활용한 진화적 아키텍처
9.3.1 블루/그린 배포
블루/그린 배포(blue/green deployment)는 이해하기 쉬운 기술 중 하나이며, 릴리스 분리를 처음 고려할 때 좋은 출발점이 됩니다. 대부분의 경우, 완전한 클라우드 네이티브 플랫폼을 사용하지 않고도 배포 패턴으로 활용할 수 있습니다.
결정 지점
이를 구현하려면 아키텍처 내에서 블루와 그린 환경을 전환할 수 있는 결정 지점(decision point)이 필요합니다. 결정 지점은 트래픽을 다양한 대상으로 전달하도록 설정된 구성 요소입니다. 예를 들어, 로드 밸런서가 결정 지점의 역할을 할 수 있습니다.
결정 지점 뒤에는 소프트웨어 스택의 전체 복사본이 실행되며, 이를 블루 환경(blue environment)이라고 부릅니다. 또 다른 복사본이 결정 지점 뒤에 준비되며, 이는 그린 환경(green environment)이라고 불리며 개념적으로 플랫폼의 다음 버전을 나타냅니다.
쿠버네티스에서의 모델링
쿠버네티스에서 블루/그린을 모델링하는 여러 방법이 있습니다. 하나의 접근 방식은 새로운 서비스를 생성하는 것으로, 예를 들어 fighting-animals-blue와 fighting-animals-green 서비스를 생성하는 것입니다.
Ingress는 쿠버네티스 리소스로, 로드 밸런서에서 서비스를 직접 노출하는 것보다 더 풍부한 구성 옵션을 제공하는 진입 지점 역할을 합니다. Ingress의 구성을 변경하여 블루 서비스와 그린 서비스 간 전환을 수행할 수 있습니다.
graph LR LiveIngress[라이브<br/>인그레스] -->|블루 릴리스에서 활성화| BlueSvc[블루<br/>서비스] --> BlueApp[Fighting Animals<br/>버전 1] LiveIngress -->|그린 릴리스에서 활성화| GreenSvc[그린<br/>서비스] --> GreenApp[Fighting Animals<br/>버전 2]
빅뱅 vs 점진적 전환
블루와 그린 환경 간 전환은 빅뱅 방식으로 수행될 수 있으며, 이는 롤아웃 전략을 개발하기 위한 첫 단계가 될 수 있습니다. 트래픽이 블루에서 그린으로 완전히 전환되면, 블루 프로세스는 대기 상태(롤백이 필요한 경우)로 전환될 수 있습니다. 이후 릴리스는 다시 블루 환경으로 돌아가며, 새로운 배포는 이 환경에서 생성되고 구성됩니다. 블루와 그린 간 수동으로 인그레스와 서비스를 관리하며 전환하는 작업은 다소 까다로울 수 있습니다.
저하 테스트의 함정
저하 테스트(degraded testing)를 위해 블루/그린 환경에 직접 접근하는 것은 환경을 검증하는 데 필수적입니다. 이는 최종 사용자가 환경에 접근하는 방식과 다릅니다. 최종 사용자는 블루 또는 그린 환경이 활성화되어 있는지 인지하지 못합니다. 이 때문에 미묘한 버그가 발생할 수 있습니다. 예를 들어, URL 경로에 따라 접근이 제어되고 경로 평가 로직에 버그가 있는 경우, 저하 테스트 중에는 나타나지 않다가 실제 라이브 상태에서 문제가 발생할 수 있습니다.
블루/그린 배포의 잠재적인 단점 중 하나는 모든 서비스가 복제되어야 한다는 점입니다. 비용이 많이 들 수 있습니다.
9.3.2 카나리아 배포
카나리아 배포(canary deployment)는 실행 중인 서비스를 개별적으로 교체하며, 단계적 릴리스 시퀀스의 일부로 프로덕션 트래픽의 일부를 카나리아 환경으로 흘려보냅니다. ‘카나리아’라는 용어는 채굴 작업에서 유래되었으며, 환경 내 유독 가스의 존재를 테스트하기 위해 카나리아를 먼저 보내던 관행에서 비롯되었습니다.
아르고 CD를 활용한 자동화
아르고 CD(Argo CD)와 같은 도구는 쿠버네티스 클러스터 내에서 카나리아 릴리스를 자동화하기 위한 풍부한 도구 세트를 제공합니다.
git checkout k8s-with-argo
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts \
-f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml전략을 생성하려면 롤아웃 정의를 클러스터에 적용해야 합니다. kubectl apply -f operations/k8s-canary-rollout-demo.yaml을 실행하면 클러스터에 설치된 아르고 CD에 의해 처리됩니다.
Rollout 정의
Rollout은 5개의 복제본을 요구하며, 초기에는 mammal_demo 이미지로 설정됩니다. 전략은 카나리로 정의되며, 처음 20%를 릴리스하도록 설정됩니다. pause: {}는 롤아웃이 사용자 입력을 기다리도록 지시합니다. pause: {duration: 10}은 사용자 입력 없이 10초 동안 대기한 후 다음 단계로 진행합니다.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: rollouts-demo
spec:
replicas: 5
strategy:
canary:
steps:
- setWeight: 20
- pause: {}
- setWeight: 40
- pause: {duration: 10}
- setWeight: 60
- pause: {duration: 10}
- setWeight: 80
- pause: {duration: 10}
revisionHistoryLimit: 2
selector:
matchLabels:
app: mammal-service
template:
metadata:
labels:
app: mammal-service
spec:
containers:
- name: mammal-service
image: mammal_demo
ports:
- name: http
containerPort: 8081
protocol: TCP
resources:
requests:
memory: 32Mi
cpu: 5m새 버전 승격 명령
v2 컨테이너를 프로덕션에 릴리스하려면 다음 명령을 실행합니다.
kubectl argo rollouts set image rollouts-demo mammal-service=mammal_demo:v2이 명령은 이전 롤아웃 정의에 따라 전략을 실행하며, 20% 카나리아 배포를 시작합니다. 한 개의 컨테이너가 mammal_demo:v2로 설정됩니다. kubectl argo rollouts dashboard로 아르고 CD 대시보드를 통해 진행 중인 롤아웃을 시각적으로 확인할 수 있습니다.
자동화된 분석 단계
수동 승격 외에도, 신호를 사용하여 릴리스를 진행할 수도 있습니다. 프로메테우스 지표를 사용해 서비스의 성공적인 요청 수를 확인하는 AnalysisTemplate 예제입니다.
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
- name: prometheus-port
value: 9090
metrics:
- name: success-rate
successCondition: result[0] >= 0.95
provider:
prometheus:
address: "http://prometheus.example.com:{{args.prometheus-port}}"이 분석 단계는 롤아웃 정의의 일부로 적용됩니다. 성공률이 95% 미만으로 떨어지면 자동으로 롤백되는 식으로 동작합니다.
9.3.3 진화적 아키텍처와 기능 플래깅
블루/그린 배포와 카나리아 배포는 훌륭한 배포 메커니즘이지만, 이를 기존 시스템에 어떻게 적용할 수 있을까요? 복잡한 시스템을 한 번에 변경하는 것은 너무 많은 위험을 초래하기 때문에 현실적이지 않습니다.
진화적 아키텍처(evolutionary architecture)는 시간이 지남에 따라 시스템이 변경될 것을 예상하며, 이러한 변화에 열려 있도록 설계됩니다. 진화적 아키텍처의 핵심 아이디어는 목표 상태 아키텍처로 이동하기 위한 실행 계획입니다.
진화적 아키텍처는 여정
진행 과정에서 많은 것을 배우게 됩니다. 새로운 기술에 대해 더 많이 배우면서 목표 상태도 변화할 가능성이 높습니다.
AWS의 6R 마이그레이션 접근법
2016년 AWS는 클라우드 마이그레이션을 위한 ‘6R’(six R’s) 접근법을 발표했습니다.
| R | 의미 |
|---|---|
| 유지 (retain) 또는 재검토 (revisit) | 이동이 어렵거나 현재 가치가 높음 |
| 재호스팅 (rehost) | 리프트 앤 시프트, 클라우드로 그대로 이동 |
| 플랫폼 재구축 (replatform) | 작은 조정(예: 관리형 DB로 전환) |
| 재구매 (repurchase) | SaaS 제품으로 대체 |
| 리팩터링 (refactor) / 재구조화 (re-architect) | 클라우드 플랫폼을 최대 활용하도록 적응 |
| 폐기 (retire) | 더 이상 필요하지 않은 구성 요소 제거 |
리팩터링 또는 재구조화는 기술적 관점에서 가장 흥미로운 접근법입니다. 이는 쿠버네티스와 같은 플랫폼을 최대한 활용할 수 있도록 소프트웨어를 적응시킬 기회를 제공합니다.
한 번에 모두 옮기지 마라
모든 것을 한 번에 이동하고 변경해야 한다고 생각하는 것은 클라우드 네이티브 기술을 도입할 때 안티 패턴에 해당합니다. 비즈니스 사례에 실질적인 차이를 가져오거나 아키텍처의 비기능적 요구 사항을 개선할 수 있는 요소만 이동하는 것이 중요합니다.
기능 플래그의 역할
기능 플래그는 실행 중인 시스템의 흐름을 어떤 조건에 따라 조작할 수 있도록 플래그를 외부 저장소에 저장하고 이를 기반으로 확인하는 코드 레벨 검사를 제공합니다. 자바에서 널리 사용되는 도구로 런치다클리(LaunchDarkly)가 있습니다.
LDUser user = new LDUser("authors");
boolean mammalService =
launchDarklyClient.boolVariation("user.enabled.mammals", user, false);
if (mammalService) {
// Retrieves the mammal from the modern environment
} else {
// Retrieves the mammal from the existing monolithic codebase
}이러한 기능 플래그를 도입하면 필요한 경우 새 기능을 유연하게 추가하고 활성화/비활성화할 수 있습니다. 카나리아 배포 또는 블루/그린 배포와 결합하면 애플리케이션 마이그레이션에 효과적인 접근 방식을 제공합니다.
폴백을 설정하라
기능 플래그는 아키텍처 내에서 높은 가용성을 유지해야 하지만, 실패 시 기본값으로 돌아가는 폴백(fallback)을 설정하는 것이 합리적입니다.
기능 플래그는 배포를 롤아웃하는 주요 메커니즘으로도 널리 사용됩니다. 블루/그린 배포가 너무 복잡해 적용하기 어려운 대규모 시스템의 핵심입니다. 365일 운영되는 많은 애플리케이션에서, 기능 플래그는 시간 소모적인 롤백 없이 변경 사항을 릴리스하는 몇 안 되는 포괄적인 방법 중 하나입니다.
나이트 캐피털의 교훈
나이트 캐피털(Knight Capital)의 사례는 기능 플래그를 재사용하고 정리하지 않아 몇 시간 만에 5억 달러 이상의 전자 거래 손실을 초래한 극단적인 예입니다. 시스템 내에서 기능 플래그의 명명과 사용 정책을 설정하는 것이 중요합니다.
기능 플래그는 반드시 정리해야 하며, 신중한 네이밍 전략이 필요합니다. 정확한 수명 주기를 가져야 하며, 코드베이스에 무기한으로 남아 있어서는 안 됩니다.
9.4 자바 특화 고려 사항
다른 언어로 작성된 애플리케이션에서는 흔하지 않은 문제들이, 컨테이너 환경에서 자바 애플리케이션을 배포할 때 여러 차례 발생했습니다. 이를 방지하기 위해 고려해야 할 사항들을 살펴봅니다.
9.4.1 컨테이너와 가비지 컬렉션
렐릭(Relic) 연구에 따르면, 현재 자바 애플리케이션의 70% 이상이 컨테이너화된 환경에 배포되고 있습니다(수천만 개의 프로덕션 JVM 데이터 기반).
이 자체로는 문제가 되지 않지만, 이러한 컨테이너에서 사용되는 CPU의 분포를 살펴보면 문제가 발생할 수 있습니다. 이 컨테이너들 중 약 절반은 단일 CPU만 사용할 수 있도록 설정되어 있습니다.
단일 코어 컨테이너의 함정
자바 가상 머신은 시작 시 가상 머신의 런타임 동작을 제어하는 몇 가지 속성을 동적으로 설정하는데, 여기에는 가비지 컬렉션 설정도 포함됩니다.
기본적으로 G1 컬렉터는 부분적으로 동시성을 지원합니다. 하지만 동시 컬렉터를 실행하려면 여러 CPU가 필요합니다. 단일 CPU 머신(단일 코어 컨테이너가 이에 해당)에서는 자바 가상 머신이 자동으로 G1이 효과적으로 작동할 수 없다고 판단하여 Serial이나 SerialOld 컬렉터를 대신 선택합니다.
예를 들어 CPU: 1로 설정하면 런타임 환경은 단 1개의 CPU만 사용할 수 있게 됩니다. 이러한 제약은 자바 가상 머신이 가비지 컬렉션을 직렬로 실행하도록 만들어 애플리케이션의 일시 정지 시간이 길어지고 처리량이 줄어들며 애플리케이션 중단이 더 빈번하게 발생합니다.
작고 많은 vs 크고 적은 컨테이너
레드햇과 마이크로소프트의 연구에 따르면, 단일 코어 컨테이너의 큰 클러스터를 사용하는 것보다 더 적은 수의 컨테이너에 더 많은 CPU를 할당하는 것이 더 효과적일 수 있습니다.
즉, 많은 단일 코어 컨테이너를 배포하는 것은 자원의 낭비일 뿐 아니라 클라우드 인프라 비용을 불필요하게 증가시킬 수 있습니다. 따라서 특별한 증거가 없는 한, 기본적인 가정은 자바 애플리케이션을 2개 이상의 코어를 사용하는 컨테이너에 배포하는 것이 적합하다는 것입니다.
9.4.2 메모리와 OOMEs
메모리 문제는 자바 애플리케이션이 컨테이너를 도입하는 과정에서 주요 문제로 작용해 왔습니다.
옛 JVM이 호스트 메모리를 본 문제
첫 번째 문제는 이전 버전의 자바 가상 머신이 컨트롤 그룹(cgroups) 힌트를 제대로 인식하지 못했다는 점입니다. 이는 자바의 초기 버전이 컨트롤 그룹 기술 개발 이전에 만들어졌기 때문입니다. 대신 자바 가상 머신은 호스트 머신의 전체 메모리 정보를 참조했습니다. 이 때문에 자바 가상 머신이 실제로 사용할 수 있는 메모리보다 더 많은 메모리를 사용하려고 시도했고, 이는 운영 체제가 애플리케이션을 종료시키는 결과를 초래할 수 있었습니다.
이는 중요한 문제입니다. 왜냐하면 컨트롤 그룹 제한을 위반하면 프로세스가 종료될 가능성이 있기 때문입니다(커널이 컨트롤 그룹을 통해 완전한 격리를 강제하지 않기 때문).
컨트롤 그룹 기능은 자바 8에 역이식 되었지만, 컨테이너를 사용하는 경우 더 최신 자바 가상 머신 버전을 사용하는 것이 좋습니다. 최신 버전에는 추가적인 최적화가 포함되어 있기 때문입니다. 예를 들어, 자바 17에는 cgroups v2 지원과 OperatingSystemMxBean에서의 컨테이너 인식이라는 두 가지 주요 추가 기능이 포함되어 있습니다.
cgroups v2와 구버전 JVM
구 버전의 자바 가상 머신을 cgroups v2만 지원하는 머신에서 실행하면, 자바 가상 머신이 컨테이너의 제약 조건이 아닌 호스트 수준의 세부 정보를 참조하게 되는 문제가 발생할 수 있습니다.
권장: 최신 LTS
일반적으로 컨테이너화된 환경에서는 가능한 최신 LTS 자바 가상 머신(이상적으로는 17 또는 21)을 실행하는 것이 항상 유리합니다. 자바의 각 LTS 버전마다 성능이 향상될 뿐만 아니라, 자바 11보다 낮은 버전을 실행하면 특정 하드웨어에서 애플리케이션에 예상치 못한 영향을 미칠 수 있습니다. 이러한 문제는 로컬 개발 환경에서는 잘 드러나지 않을 수 있습니다.
메모리 할당과 자바 힙의 자동 설정
두 번째 문제는 컨테이너와 자바 가상 머신 간의 설정과 구성에 관한 것입니다. 예를 들어, 컨테이너에 1GB의 메모리 할당량을 지정하면 자바 가상 머신 최대 힙 크기는 자동으로 256MB로 설정됩니다. 컨테이너 내부 또는 다른 프로세스 실행에 필요한 공간을 확보해야 하지만, 이는 자원의 과소 활용을 초래할 가능성이 있습니다.
-Xmx 오버라이드와 -XX:MaxRam
최대 힙 크기 -Xmx를 오버라이드하고 성능 테스트를 실행하는 것은 자원 활용도를 극대화하기 위한 좋은 방법입니다. 대안으로는 -XX:MaxRam 옵션을 설정하는 것이 있습니다. 이 옵션은 프로세스에서 사용할 수 있는 물리적 RAM 크기를 선언하며, 자바 가상 머신이 힙 크기를 어떻게 설정할지 결정할 수 있도록 합니다.
힙 크기를 고려하는 것 외에도, 스택 크기와 애플리케이션에서 사용하는 직접 메모리 또는 오프 힙 메모리의 크기도 함께 고려하는 것이 중요합니다.
튜닝 옵션의 다층 구조
현실적으로는 구성·제약·튜닝을 위한 다양한 옵션이 존재합니다. 자바 가상 머신 구성 옵션뿐만 아니라 오케스트레이션 계층의 런타임 구성 옵션도 포함됩니다. 이 구성 옵션들은 항상 독립적으로 고려하거나 테스트할 수 있는 것은 아닙니다. 아무 제약 없이 실행하는 것도 가능하며, 초기 리프트 앤 시프트 단계에서는 이러한 접근이 유용할 수 있습니다. 하지만 이는 최종 상태로 적합하지 않을 가능성이 높습니다. 성능과 복원력 목표를 달성하지 못할 수 있기 때문입니다.
9.5 요약
이 챕터는 자바 애플리케이션 개발과 배포 간의 관계가 더 긴밀해지는 복잡한 변화의 일부를 다룹니다. 로컬 환경에서 컨테이너를 활용하는 방법과 도커 컴포즈를 사용해 경량화된 DNS 엔트리를 생성하는 방법, 쿠버네티스 작업의 시작점과 Pod·컨테이너의 수명 주기에 대한 주요 개념과 일반적인 함정, 아르고 CD와 같은 도구를 활용한 릴리스 자동화, 마지막으로 배포와 릴리스 개념을 분리하는 접근 방식으로 블루/그린, 카나리아, 피처 플래그를 활용한 진화적 아키텍처를 살펴봅니다.
이 챕터에서 소개한 도구만으로는 프로덕션 환경에서 서비스를 효과적으로 관리하기 어렵습니다. 다음 단계로 다룰 주제는 관측성이며, 이는 이 챕터에서 다룬 배포 옵션을 활용하기 위해 필수적으로 고려해야 할 요소입니다.
비교 / 트레이드오프
도커 컴포즈 vs 틸트 vs 로컬 쿠버네티스
| 항목 | 도커 컴포즈 | 틸트 | 로컬 쿠버네티스 (k3s/minikube) |
|---|---|---|---|
| 학습 곡선 | 낮음 | 중간 | 높음 |
| 코드 변경 자동 재배포 | 수동 | 자동 (local_resource) | 추가 도구 필요 (skaffold 등) |
| 프로덕션 환경 유사성 | 낮음 | 중간 | 높음 |
| 멀티 컨테이너 정의 방식 | YAML | Python(Tiltfile) | YAML |
| UI/로그 통합 | 별도 | 통합 대시보드 | kubectl + 별도 도구 |
| 적합한 단계 | 학습/POC | 일상 개발 | 프로덕션 준비 |
블루/그린 vs 카나리아 vs 기능 플래그
| 항목 | 블루/그린 | 카나리아 | 기능 플래그 |
|---|---|---|---|
| 전환 단위 | 환경 전체 | Pod 일부 | 코드 분기 |
| 자원 비용 | 2배 (전 환경 복제) | 점진적 증가 | 거의 없음 |
| 롤백 속도 | 빠름 (스위치 전환) | 빠름 (가중치 조정) | 매우 빠름 (플래그 토글) |
| 부분 사용자 노출 | 어려움 | 가능 (트래픽 %) | 가능 (사용자/그룹 단위) |
| 운영 복잡도 | 낮음 | 중간 (아르고 CD 등 필요) | 중간 (플래그 관리 필요) |
| 적합 시나리오 | 단순 v1→v2 교체 | 점진적 검증 | A/B 테스트, 점진적 마이그레이션 |
블루/그린, 카나리아 전환 흐름
graph TB subgraph BG[블루/그린] BGUsers[사용자] --> BGLB[로드 밸런서] BGLB -.활성.-> BGBlue[블루 v1] BGLB -.대기.-> BGGreen[그린 v2] end subgraph Canary[카나리아] CUsers[사용자] --> CLB[로드 밸런서] CLB -->|80%| CV1[v1 4개 Pod] CLB -->|20%| CV2[v2 1개 Pod] end
imagePullPolicy 선택 가이드
| 정책 | 적합한 상황 | 위험 |
|---|---|---|
| IfNotPresent | 버전 태그 사용, 일반 프로덕션 | 없음 (디폴트) |
| Always | 가변 태그 사용, 개발 환경 | 네트워크 비용, 콜드 스타트 지연 |
| Never | 에어갭, 사전 로드 환경 | 이미지 누락 시 시작 실패 |
자바 컨테이너 환경 권장 디폴트
| 항목 | 권장 |
|---|---|
| JVM 버전 | LTS 17 또는 21 |
| CPU 할당 | 2 코어 이상 |
| 가비지 컬렉터 | G1 (기본), ZGC (대용량 힙) |
| 메모리 설정 | -XX:MaxRam 또는 -Xmx 명시 |
| cgroups | v2 지원 JVM 확인 |
| 이미지 베이스 | 최소 UBI/distroless, 가능하면 GraalVM 네이티브 |
내 생각
배포와 릴리스 분리가 진짜 컬처 변화다
기술적으로는 기능 플래그 라이브러리 하나 도입하면 끝나는 일이지만, 실제로는 “오늘 배포했지만 다음주에 릴리스한다” 라는 사고방식 자체가 팀에 자리잡아야 합니다. 이게 되면 금요일 오후 배포 공포가 사라지고, 마이그레이션이나 큰 리팩터링이 한 번의 빅뱅 PR이 아닌 수십 번의 작은 배포로 나뉩니다. 나이트 캐피털의 5억 달러 손실은 기술적 사고처럼 보이지만, 실제로는 플래그 라이프사이클 거버넌스가 부재했던 운영 사고에 가깝습니다.
단일 코어 컨테이너 함정은 비용 최적화 안티 패턴의 대표
“작은 인스턴스 많이 띄우면 비용이 싸진다”는 직관은 자바에서는 틀린 경우가 많습니다. G1이 직렬 GC로 떨어지면 GC pause가 길어지고 처리량이 줄어, 결과적으로 더 많은 Pod가 필요해집니다. 2 코어 이상이 자바 디폴트라는 권고는 정말 중요한 실무 가이드입니다. 쿠버네티스 리소스 리퀘스트를 cpu: 100m 같이 설정하면 자바는 사실상 1 코어로 인식한다는 점에 주의해야 합니다.
카나리아의 진짜 가치는 자동화된 분석 단계
수동 승격으로 카나리아를 운영하면 결국 사람이 대시보드를 들여다보는 시간이 늘어 의미가 반감됩니다. 프로메테우스 + 아르고 롤아웃의 AnalysisTemplate 조합처럼, 성공률·지연·에러율을 자동으로 평가해 승격/롤백을 결정하는 파이프라인이 들어와야 카나리아가 비로소 작동합니다. 그렇지 않으면 그냥 “느린 블루/그린”이 됩니다.
remocal 개발은 마이크로서비스 시대의 필수
20개 마이크로서비스를 노트북에 다 띄우는 건 현실적으로 불가능합니다. 텔레프레즌스 같은 도구로 내가 작업 중인 한 서비스만 로컬에서 돌리고 나머지는 원격 클러스터에서 가져오는 방식이 표준이 되어야 합니다. IDE 디버거를 그대로 쓸 수 있다는 점이 가장 큰 가치이고, 도커 컴포즈로는 5개 서비스가 한계라는 점을 인정해야 합니다.
Jib는 챕터 8의 합리적 디폴트, 9장은 그 위의 운영 디폴트
8장에서 Jib가 빌드 시점의 디폴트라면, 9장의 운영 디폴트는 :latest 금지 + cgroups v2 인식 JVM + 2 코어 + 헬름 차트입니다. 이 조합 없이 쿠버네티스에 자바를 올리면 OOMKilled, GC pause spike, “왜 갑자기 다른 버전이 돌고 있지?” 같은 사고가 반복됩니다.
:latest는 보안 감사에서도 문제
규제 산업에서 :latest 태그를 쓰면 “프로덕션에서 무엇이 돌고 있는지 정확히 알 수 없다” 는 컴플라이언스 위반이 됩니다. SBOM(소프트웨어 빌즈 오브 머티리얼)이나 공급망 보안이 화두인 지금, 이미지 다이제스트(sha256:…)까지 고정하는 것이 best practice입니다. 단순한 운영 편의의 문제가 아니라 보안 거버넌스의 문제입니다.
더 알아볼 것
- 로컬에 minikube/k3s를 설치하고 Fighting Animals의
k8s-with-argo브랜치 배포 실습 - 아르고 CD 대시보드 띄우고
kubectl argo rollouts set image로 카나리아 트리거해 보기 - AnalysisTemplate로 프로메테우스 지표 기반 자동 승격/롤백 구성
- 동일 자바 앱을 단일 코어 vs 2 코어 컨테이너에서 실행해 GC 로그(
-Xlog:gc*) 비교 - JVM 17 vs 11의 cgroups v2 인식 차이를
MemoryMXBean/OperatingSystemMxBean으로 측정 - 텔레프레즌스 설치 후 로컬 IDE에서 원격 클러스터 서비스에 디버거 attach
- 헬름 차트로 mammal-service 배포 매니페스트를 템플릿화하고 values.yaml로 환경별 분리
- 런치다클리 또는 OpenFeature SDK로 기능 플래그 도입, 카나리아와 결합한 점진적 롤아웃 시나리오 작성
- 이스티오 또는 Linkerd 설치 후 mTLS와 트래픽 분할 정책 실험
-
imagePullPolicy: AlwaysvsIfNotPresent의 Pod 시작 지연을 측정해 콜드 스타트 영향 정량화
관련 개념
- Ch08 클라우드 스택의 구성 요소 — OCI, CNCF, 컨테이너 기본 (이 챕터의 전제)
- 쿠버네티스
- 도커 컴포즈
- 블루-그린 배포
- 카나리아 배포
- 기능 플래그
- 아르고 CD
- 헬름
- 서비스 메시
- Istio
- 12팩터 앱
- GraalVM 네이티브 이미지
- G1 가비지 컬렉터
- cgroups
출처
자바 최적화 2판 (오라일리), Ben Evans 외 저, Chapter 9: 클라우드에서의 자바 배포