IBM Kubernetes에서 스트랭글러 피그: 프로덕션을 중단하지 않고 모놀리스를 현대화하기
Source: Dev.to
Strangler Fig on IBM Kubernetes
Modernizing a Monolith without Breaking Production
소개
우리는 수년간 운영해 온 레거시 모놀리식 애플리케이션을 마이그레이션 해야 하는 상황에 직면했습니다. 기존 시스템은 수천 명의 사용자가 동시에 사용하고 있었고, 다운타임 없이 새로운 기능을 도입해야 했습니다.
이러한 요구사항을 충족시키기 위해 Strangler Fig 패턴을 선택했고, IBM Cloud Kubernetes Service(IKS)를 기반으로 단계적으로 전환을 진행했습니다.
왜 Strangler Fig인가?
- 점진적 전환: 전체 시스템을 한 번에 교체하지 않고, 작은 서비스 단위로 점차 교체합니다.
- 리스크 최소화: 기존 기능이 그대로 동작하는 동안 새로운 서비스만 별도로 테스트할 수 있습니다.
- 비즈니스 연속성: 트래픽 라우팅을 통해 기존 모놀리식과 새로운 마이크로서비스를 동시에 운영할 수 있습니다.
아키텍처 개요
graph LR
A[Client] --> B[Ingress (IBM Cloud Load Balancer)]
B --> C[Istio Envoy Proxy]
C --> D[Legacy Monolith]
C --> E[New Microservice 1]
C --> F[New Microservice 2]
- Ingress: IBM Cloud Load Balancer를 사용해 외부 트래픽을 받아옵니다.
- Istio: 서비스 메쉬 역할을 하며, 라우팅 규칙을 통해 트래픽을 기존 모놀리식 또는 새로운 마이크로서비스로 전달합니다.
- Legacy Monolith: 기존 Java EE 애플리케이션 (Tomcat 기반).
- New Microservice: Spring Boot, Node.js 등으로 구현된 독립 서비스.
단계별 전환 과정
| 단계 | 설명 | 핵심 작업 |
|---|---|---|
| 1️⃣ 분석 | 모놀리식 내부의 비즈니스 도메인을 식별합니다. | - 코드베이스 탐색 - DB 스키마 매핑 |
| 2️⃣ 경계 정의 | 각 도메인을 Bounded Context 로 분리합니다. | - API 계약 정의 - 데이터 복제 전략 수립 |
| 3️⃣ 서비스 구현 | 새로운 마이크로서비스를 컨테이너화합니다. | - Dockerfile 작성 - CI/CD 파이프라인 구축 |
| 4️⃣ Istio 라우팅 | 트래픽을 점진적으로 새로운 서비스로 전환합니다. | - VirtualService, DestinationRule 설정 |
| 5️⃣ 데이터 마이그레이션 | 기존 DB와 새로운 서비스 DB 간 동기화를 구현합니다. | - CDC (Change Data Capture) 도구 사용 |
| 6️⃣ 폐기 | 모든 트래픽이 새로운 서비스로 이동하면 모놀리식을 종료합니다. | - 리소스 정리 - 비용 최적화 |
1️⃣ 분석 예시
# 코드베이스에서 주요 패키지와 의존성을 시각화
$ jdeps -verbose:class -recursive target/*.jar > deps.txt
4️⃣ Istio 라우팅 예시
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order.example.com
http:
- match:
- uri:
prefix: /v1/orders
route:
- destination:
host: order-legacy
subset: v1
- destination:
host: order-microservice
subset: v2
weight: 20
- destination:
host: order-microservice
subset: v2
weight: 80
위 설정은 점진적 트래픽 이동을 보여줍니다. 처음에는 20%만 새로운 마이크로서비스로 라우팅하고, 검증이 끝나면 비중을 80%까지 올립니다.
CI/CD 파이프라인
# .github/workflows/deploy.yml
name: Deploy to IKS
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: |
docker build -t ${{ secrets.REGISTRY }}/order-service:${{ github.sha }} .
- name: Push image
run: |
echo ${{ secrets.REGISTRY_PASSWORD }} | docker login ${{ secrets.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin
docker push ${{ secrets.REGISTRY }}/order-service:${{ github.sha }}
- name: Deploy to IKS
uses: ibm-cloud/kubernetes-action@v1
with:
cluster: ${{ secrets.IKS_CLUSTER }}
namespace: production
manifest: k8s/deployment.yaml
GitHub Actions를 이용해 코드 푸시 → 이미지 빌드 → 레지스트리 푸시 → IKS 배포까지 자동화했습니다.
모니터링 & 로깅
- IBM Cloud Monitoring (Prometheus + Grafana)
- LogDNA 혹은 IBM Log Analysis를 활용해 로그를 중앙집중식으로 수집
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: order-service-monitor
spec:
selector:
matchLabels:
app: order-service
endpoints:
- port: http
interval: 30s
결과
| 항목 | 이전 | 현재 |
|---|---|---|
| 평균 응답 시간 | 850 ms | 420 ms |
| 다운타임 | 월 2 시간 | < 5 분 (배포 시) |
| 개발 속도 | 2 주/피처 | 1 주/피처 |
| 인프라 비용 | $12,000/월 | $9,500/월 |
점진적인 전환 덕분에 서비스 가용성을 유지하면서 성능과 비용을 크게 개선할 수 있었습니다.
교훈
- 작게 시작하라 – 가장 위험도가 낮은 도메인부터 마이크로서비스화하면 빠르게 피드백을 얻을 수 있다.
- Istio 라우팅을 적극 활용 – 트래픽 비중을 조절하면서 새로운 서비스의 안정성을 검증한다.
- 데이터 동기화 전략을 명확히 – CDC 도구와 이벤트 기반 복제를 사용하면 데이터 일관성을 유지하기 쉽다.
- 모니터링을 사전에 구축 – 문제 발생 시 즉시 원인을 파악할 수 있도록 메트릭과 로그를 모두 수집한다.
Strangler Fig 패턴과 IBM Kubernetes를 결합하면 레거시 시스템을 안전하게 현대화할 수 있습니다. 여러분도 비슷한 상황이라면 위 과정을 참고해 보시기 바랍니다.
이 가이드를 마치면 다음을 할 수 있습니다:
- 기존 모놀리식 애플리케이션을 컨테이너화하기
- IBM Cloud Kubernetes Service에 배포하기
- Ingress 뒤에 배치하기
- 새로운 “edge” 서비스 배포하기
- 경로 기반 라우팅을 사용하여 트래픽을 점진적으로 라우팅하기
- 롤백을 간단하고 안전하게 유지하기
사전 요구 사항
| 항목 | 세부 정보 |
|---|---|
| IBM Cloud 계정 | – |
| 기존 IKS 클러스터 | – |
| 로컬 도구 | ibmcloud, kubectl, docker |
| 로그인 | bash<br>ibmcloud login -a https://cloud.ibm.com<br>ibmcloud target -r <region> -g <resource-group><br> |
1. 깨끗한 네임스페이스 설정
kubectl create namespace monolith-demo
kubectl config set-context --current --namespace=monolith-demo
kubectl get ns
Goal: 행동 변화 없음 – 단지 모놀리스를 패키징합니다.
2. 모놀리스를 컨테이너화하기
Dockerfile (Node.js 모놀리스)
# ---- Build stage ----
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# ---- Runtime stage ----
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app /app
EXPOSE 8080
CMD ["npm","start"]
최소한의 헬스 엔드포인트 추가 (이미 없을 경우)
// Example endpoints
app.get("/health", (req, res) => res.status(200).send("ok"));
app.get("/ready", (req, res) => res.status(200).send("ready"));
이미지 빌드
docker build -t monolith:1.0.0 .
IBM Cloud Container Registry에 푸시
# 로그인 (한 번만)
ibmcloud cr login
ibmcloud cr namespace-add <your-namespace>
# 태그 및 푸시
docker tag monolith:1.0.0 <registry>/<namespace>/monolith:1.0.0
docker push <registry>/<namespace>/monolith:1.0.0
# 검증
ibmcloud cr images | grep monolith
3. 모놀리식 배포
3.1 배포 매니페스트 (deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: monolith
spec:
replicas: 2
selector:
matchLabels:
app: monolith
template:
metadata:
labels:
app: monolith
spec:
containers:
- name: monolith
image: <registry>/<namespace>/monolith:1.0.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
kubectl apply -f deployment.yaml
kubectl rollout status deploy/monolith
kubectl get pods -l app=monolith
3.2 서비스 매니페스트 (service.yaml)
apiVersion: v1
kind: Service
metadata:
name: monolith-svc
spec:
selector:
app: monolith
ports:
- name: http
port: 80
targetPort: 8080
type: ClusterIP
kubectl apply -f service.yaml
kubectl get svc monolith-svc
빠른 로컬 테스트
kubectl port-forward svc/monolith-svc 8080:80
curl -i http://localhost:8080/health
4. Ingress를 통해 노출하기 (라우팅 컨트롤 플레인)
Ingress 매니페스트 (ingress.yaml)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
spec:
rules:
- host: <your-host>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: monolith-svc
port:
number: 80
kubectl apply -f ingress.yaml
kubectl get ingress app-ingress -o wide
현재 시점: 트래픽의 100 %가 여전히 모놀리쓰로 향합니다.
5. 위험이 낮은 첫 번째 슬라이스를 “목 조르기” 선택
좋은 첫 후보: /api/auth/*.
이번 워크스루에서는 auth API를 추출합니다.
최소 예제 엔드포인트 (모놀리쓰에 추가)
app.get("/api/auth/ping", (req, res) => {
res.json({ service: "auth-service", status: "pong" });
});
6. 새로운 Auth 서비스 빌드
Dockerfile (Dockerfile)
FROM node:20-alpine
WORKDIR /app
COPY . .
EXPOSE 8081
CMD ["node","server.js"]
Build & push
docker build -t auth-service:1.0.0 .
docker tag auth-service:1.0.0 <registry>/<namespace>/auth-service:1.0.0
docker push <registry>/<namespace>/auth-service:1.0.0
7. Auth 서비스 배포
7.1 배포 매니페스트 (auth-deploy.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-service
spec:
replicas: 2
selector:
matchLabels:
app: auth-service
template:
metadata:
labels:
app: auth-service
spec:
containers:
- name: auth-service
image: <registry>/<namespace>/auth-service:1.0.0
ports:
- containerPort: 8081
readinessProbe:
httpGet:
path: /ready
port: 8081
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8081
initialDelaySeconds: 15
periodSeconds: 10
kubectl apply -f auth-deploy.yaml
kubectl rollout status deploy/auth-service
kubectl get pods -l app=auth-service
7.2 서비스 매니페스트 (auth-svc.yaml)
apiVersion: v1
kind: Service
metadata:
name: auth-svc
spec:
selector:
app: auth-service
ports:
- name: http
port: 80
targetPort: 8081
type: ClusterIP
kubectl apply -f auth-svc.yaml
kubectl get svc auth-svc
8. 새 서비스로 /api/auth/* 라우팅하도록 Ingress 업데이트
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
spec:
rules:
- host: <your-host>
http:
paths:
# New route for auth
- path: /api/auth
pathType: Prefix
backend:
service:
name: auth-svc
port:
number: 80
# Fallback to monolith for everything else
- path: /
pathType: Prefix
backend:
service:
name: monolith-svc
port:
number: 80
kubectl apply -f ingress.yaml
kubectl get ingress app-ingress -o wide
이제 /api/auth/*에 대한 트래픽은 새로운 auth‑service가 처리하고, 다른 모든 요청은 계속해서 모놀리쓰(monolith)로 전달됩니다.
9. 점진적 롤아웃 및 롤백
-
새 엔드포인트 검증
curl -i https://<your-host>/api/auth/ping -
트래픽 증가 (트래픽 분할 컨트롤러를 사용하는 경우) 또는 새 서비스의 메트릭과 로그를 단순히 모니터링합니다.
-
롤백 (필요한 경우)
# Ingress에서 auth 경로 제거 kubectl edit ingress app-ingress # /api/auth 블록 삭제 # 또는 auth 배포/서비스 삭제 kubectl delete -f auth-deploy.yaml kubectl delete -f auth-svc.yaml
모놀리식이 Ingress 뒤에서 그대로 유지되기 때문에 언제든지 즉시 되돌릴 수 있습니다.
10. 정리 (준비가 되면)
kubectl delete -f ingress.yaml
kubectl delete -f service.yaml
kubectl delete -f deployment.yaml
kubectl delete namespace monolith-demo
🎉 실제 IBM Cloud Kubernetes 환경에서 Strangler Fig 패턴을 적용했습니다!
같은 방식을 사용하여 추가 기능 슬라이스(예: /api/orders/*, /api/payments/*)를 계속 추출하고, 모놀리스를 안전하게 폐기할 수 있을 때까지 진행하세요.
롤백을 지루하고 빠르게 유지하기
옵션 A – 모놀리스로 라우팅 되돌리기
Ingress를 편집하고 /api/auth 경로를 제거하거나 monolith-svc 로 지정한 뒤 다시 적용합니다:
kubectl apply -f ingress.yaml
옵션 B – 배포 롤아웃 되돌리기
kubectl rollout undo deploy/auth-service
Process
- 첫 번째 추출된 기능을 안정화하기
- 다음 제한된 도메인을 선택하기
- 별도의 서비스로 구축하기
- 배포하기
- Ingress로 라우팅하기
- 각 단계마다 롤백을 가능하게 유지하기
시간이 지나면서
- 모놀리식이 축소됩니다.
- 현대화가 “대규모 마이그레이션”이 아니라 일상적인 작업이 됩니다.
- 다운타임이 없습니다.
Strangler Fig 패턴은 현실을 존중하기 때문에 작동합니다 – 과거를 삭제하지 않고 현대화합니다.
현재 모놀리식에 머물러 있다면, 이 접근 방식은 이미 작동하고 있는 것을 깨뜨리지 않고 앞으로 나아갈 수 있게 해줍니다.