Search

CircuitBreaker & Canary — istio로 구현하기

Date
2026/02/23
Category
Devops
Tag
Kubernetes
목차

0. 소개글

Come2us 프로젝트에서 Istio를 도입하며 CircuitBreaker와 Canary 배포를 적용하고 싶었지만 수행하지 못하고 마무리하게 되었다. 그 미완성 부분을 작게라도 경험하고 싶어 이 포스트를 작성하게 되었다.
우선 Come2us 프로젝트에 대해 간단하게 설명하자면 Groom 부트캠프에서 수행한 프로젝트로, 총 3차례에 걸쳐 Monolithic 구조에서 MSA로 전환하며 ECS에서 EKS까지의 구조 전환을 가졌다.

1. 배경

1.1 ECS + Spring Cloud 환경에서 겪은 문제

Come2us 프로젝트는 2차 단계에서 ECS Fargate 기반으로 운영되었다. 당시 서비스 디스커버리를 위해 Spring Cloud Eureka를 사용했는데, ECS Fargate 환경에서 예상치 못한 문제가 발생했다.

1.2 Eureka IP 불일치 문제

ECS Fargate는 task 내부 IP와 외부로 노출되는 IP가 다르다. Eureka는 각 서비스가 자신의 IP를 등록하고, API Gateway가 이 IP로 라우팅하는 구조이다. 하지만 Fargate task가 내부 IP를 Eureka에 등록하면서 Gateway가 실제로 접근할 수 없는 IP로 라우팅을 시도하는 문제가 생겼다.
수정 전 서비스 별 eureka에 대한 property yaml은 다음과 같았다:
eureka: instance: prefer-ip-address: true
YAML
복사
prefer-ip-address: true는 Eureka에 task 내부 IP를 등록하게 된다. Fargate 환경에서는 이 IP로 외부에서 접근이 불가능하다. 임시 해결을 위해 hostname과 instance-id를 직접 주입하는 방식을 사용해보았다:
eureka: instance: prefer-ip-address: false hostname: ${SERVICE_HOSTNAME: servicename.come2us.local} ip-address: ${SERVICE_HOSTNAME} instance-id: ${SERVICE_HOSTNAME}:${SERVICE_PORT}
YAML
복사
환경변수로 hostname을 직접 주입하는 방식으로 해결은 되었지만, 서비스가 늘어날수록 서비스마다 hostname 환경변수를 관리해야 하는 이 방식은 근본적인 해결이 될 수 없다고 판단했다. 이 문제를 계기로 Spring Cloud 컴포넌트(Eureka, Gateway, ConfigServer)를 모두 제거하기로 결정했다.

1.3 EKS 전환 결정

Spring Cloud 컴포넌트를 제거하기로 결정한 뒤 가장 먼저 떠올린 것은 Kubernetes의 Ingress였다. Kubernetes는 서비스 디스커버리와 라우팅을 네이티브로 지원하기 때문에 Eureka와 Gateway를 대체할 수 있었다.
또한 ECS 환경에서는 Terraform으로 인프라를 관리하다 보니 로그 확인이나 서비스 상태 제어 시 콘솔에 의존해야 하는 불편함이 있었다(이 부분은 내 ECS 경험 부족의 문제일 수 있다). 하지만 Kubernetes는 kubectl로 선언적으로 클러스터 상태를 확인하고 제어할 수 있다.
EKS로 전환하면서 CI/CD도 Kubernetes 네이티브인 ArgoCD를 도입하기로 했고, 이와 함께 Canary 배포 적용을 목표로 세웠다.

1.4 Istio 도입 결정

Canary 배포를 고민하며 트래픽 제어 방식을 탐색했다. Kubernetes의 Ingress, Gateway API, HTTPRoute 등으로 L7 라우팅을 수행할 순 있지만, 여기에 더해 Spring Cloud Gateway가 수행하던 인증·인가·필터링까지 처리할 수 있는 방법이 필요했다.
이 과정에서 Istio를 발견했다. Istio는 다음 과제를 해결할 수 있었다:
인증·인가 플랫폼화: Spring Cloud Gateway의 JWT 인증·인가 로직을 RequestAuthentication, AuthorizationPolicy로 이관
라우팅 중앙화: VirtualService로 라우팅 정책을 선언적으로 관리
Canary 배포: VirtualService의 weight 기반 트래픽 분산
추가로 Istio와 Service Mesh를 공부하며 mTLS에 대해 알게 되었다. Come2us는 금전 거래가 포함된 E-Commerce 서비스이기 때문에 서비스 간 통신을 mTLS로 암호화하는 것이 보안 측면에서도 의미있다고 판단하여 Istio를 적극 도입하게 되었다.

1.5 이 글을 쓰게 된 이유

결과적으로 Istio 도입 자체의 학습 비용이 예상보다 높았다. RequestAuthentication, AuthorizationPolicy, VirtualService, DestinationRule, PeerAuthentication 등 핵심 리소스를 이해하고 실제 서비스에 적용하는 과정에서 상당한 시간이 소요되었고, 목표로 했던 Canary 배포와 Circuit Breaker는 손도 대보지 못하고 프로젝트를 끝마치게 되었다.
“Istio를 도입했다”고 말하면서 정작 Istio의 핵심 가치인 트래픽 제어를 구현하지 못했다는 것이 머릿속에 남게 되었고, 미완성 부분을 작게라도 완성해보고자 k3d 로컬 환경에서 구현한 내용을 기록해본다.

2. 환경 구성

2.1 k3d 설치 및 클러스터 생성

k3d는 k3s를 Docker 컨테이너 안에서 실행하는 경량 Kubernetes이다. kind보다 리소스 사용량이 적어 로컬 테스트 환경으로 선택했다.
# k3d 설치 (macOS) brew install k3d # 클러스터 생성 # traefik은 Istio와 충돌할 수 있어 비활성화 k3d cluster create istio-test \ --agents 2 \ --k3s-arg "--disable=traefik@server:0" # 클러스터 확인 kubectl cluster-info kubectl get nodes
Bash
복사

2.2 Istio 설치

come2us 프로젝트에서는 istio를 Helm Chart로 관리했지만, 로컬에서는 간단한 istioctl을 사용했다.
# istioctl 설치 (macOS) brew install istioctl # Istio 설치 (demo 프로파일) istioctl install --set profile=demo -y # 설치 확인 kubectl get pods -n istio-system
Bash
복사

2.3 sidecar injection 활성화

# 테스트용 네임스페이스 생성 kubectl create namespace test # sidecar injection 활성화 # 이 레이블이 있어야 Pod에 Envoy sidecar가 자동으로 주입됨 kubectl label namespace test istio-injection=enabled # 확인 kubectl get namespace test --show-labels
Bash
복사

2.4 테스트용 서비스(httpbin) 배포

httpbin은 HTTP 응답을 테스트할 수 있는 이미지이다. /status/500으로 5xx 오류를, /status/200으로 정상 응답을 반환할 수 있어 별도 코드 작성 없이 Circuit Breaker와 Canary를 테스트할 수 있다.
stable과 canary 두 버전의 Deployment를 배포하였으며, version 레이블로 두 버전을 구분하였다.
# httpbin.yaml apiVersion: apps/v1 kind: Deployment metadata: name: httpbin-stable namespace: test spec: replicas: 1 selector: matchLabels: app: httpbin version: stable template: metadata: labels: app: httpbin version: stable spec: containers: - name: httpbin image: kennethreitz/httpbin ports: - containerPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: httpbin-canary namespace: test spec: replicas: 1 selector: matchLabels: app: httpbin version: canary template: metadata: labels: app: httpbin version: canary spec: containers: - name: httpbin image: kennethreitz/httpbin ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: httpbin namespace: test spec: selector: app: httpbin ports: - port: 80 targetPort: 80
YAML
복사
kubectl apply -f httpbin.yaml # Pod 확인 (sidecar가 주입되어 2/2 상태여야 함) kubectl get pods -n test
YAML
복사

3. Circuit Breaker 구현

3.1 Circuit Breaker란 무엇인가

Circuit Breaker는 전기 회로의 차단기에서 따온 개념이다. 특정 서비스에 장애가 발생했을 때 해당 서비스로의 호출을 빠르게 실패(fail-fast)시키거나, 문제있는 인스턴스를 일시적으로 격리(ejection)하여 장애가 연쇄적으로 확산되는 Cascading Failure를 막는다.
Circuit Breaker가 없을 때
서비스 A → 서비스 B (장애) ↓ 타임아웃까지 대기 ↓ 서비스 A도 응답 지연 시작 ↓ 전체 시스템 장애로 확산
YAML
복사
Circuit Breaker가 있을 때
서비스 A → 서비스 B (장애) ↓ 연속 3번 5xx 오류 감지 ↓ Circuit Breaker 동작 ↓ 서비스 B로의 호출 즉시 차단 ↓ 장애 확산 차단
YAML
복사

3.2 DestinationRule outlierDetection

Istio에서 Circuit Breaker는 DestinationRule의 outlierDetection으로 구현한다. Envoy sidecar가 각 인스턴스의 응답을 모니터링하다가 조건을 초과하면 해당 인스턴스를 로드밸런싱 풀에서 일시적으로 제거한다.
# httpbin-dr.yaml apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: httpbin-dr namespace: test spec: host: httpbin trafficPolicy: outlierDetection: consecutive5xxErrors: 3 # 연속 3번 5xx 오류 시 차단 interval: 10s # 10초마다 상태 체크 baseEjectionTime: 30s # 30초 동안 해당 인스턴스 제거 maxEjectionPercent: 100 # 최대 100% 인스턴스 제거 가능
YAML
복사
consecutive5xxErrors: 몇 번 연속 실패하면 차단할지. 너무 낮으면 일시적 오류에도 차단 발생
interval: 상태를 체크하는 주기
baseEjectionTime: 첫 차단 시 격리 시간. 같은 인스턴스가 반복 실패하면 baseEjectionTime × 실패 횟수로 격리 시간이 누적
maxEjectionPercent: 전체 인스턴스 중 최대 몇 %까지 제거할지. 100%는 모든 인스턴스 제거 가능을 의미하지만, 장애 인스턴스만 선택적으로 제거함
kubectl apply -f httpbin-dr.yaml kubectl get destinationrule -n test
YAML
복사

3.3 테스트 시나리오 및 검증

# 테스트용 클라어언트 Pod 실행 kubectl run test-client -n test \ --image=curlimages/curl --command -- sleep 365d # Pod 생성 확인 kubectl get pod test-client -n test # 5xx 연속 실패 유도 -> 500에서 503으로 전환 확인 kubectl exec test-client -n test -- sh -c ' for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" http://httpbin/status/500 sleep 0.5 done '
Bash
복사
기대 결과
초반 500이 반복되다가 이후 503이 연속으로 나옴
httpbin 엔드포인트가 stable 1 + canary 1로 2개이기 때문에 트래픽이 분산되어 각 엔드포인트에 5xx가 3번씩 쌓여 둘 다 ejection 발생
503은 애플리케이션(httpbin)이 낸 값이 아닌 Envoy가 “보낼 healthy upstream이 없다”고 판단하여 반환한 값
[이미지 1] 5xx 실패 유도 스크린샷
# outlier ejection 때문에 503이 난 것을 검증 # test-client의 istio-proxy에서 httpbin 업스트림 엔드포인트 상태 확인 kubectl -n test exec test-client -c istio-proxy -- sh -c ' pilot-agent request GET clusters \ | sed -n "/outbound|80||httpbin.test.svc.cluster.local/,/^[^ ]/p" \ | grep -n "health_flags::/failed_outlier_check" || true '
Bash
복사
[이미지 2] outlier ejection 검증
출력을 보면 Pod IP(엔드포인트)가 outlierDetection에 의해 ejection 된 상태를 확인할 수 있다. 이로써 Circuit Breaker가 실제로 동작했다라고 검증했다.
# 복구 동작 관찰 # baseEjectionTime: 30s 가 지나면 엔드포인트가 다시 풀에 들어옴을 확인 kubectl -n test exec test-client -- sh -c ' sleep 35 for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code}\n" http://httpbin/status/500 sleep 0.5 done '
Bash
복사

3.5 동작 전/후 비교

구분
Circuit Breaker 없음
Circuit Breaker(outlierDetection) 적용
장애 서비스 호출
장애 엔드포인트로 트래픽이 계속 유입(5xx 지속)
문제 엔드포인트를 LB 풀에서 격리(ejection)
장애 확산
호출자 관점에서 불필요한 재시도/지연이 누적될 수 있음
unhealthy 엔드포인트로의 호출을 줄여 연쇄 영향 완화
복구
보통 앱/인프라 복구까지 5xx 지속
baseEjectionTime 이후 자동 재편입(계속 실패하면 재격리)

4. Canary 배포 구현

4.1 Canary 배포란 무엇인가

Canary 배포는 새 버전을 전체에 한 번에 배포하지 않고, 일부 트래픽에만 먼저 노출해 안정성을 검증한 뒤 점진적으로 비중을 늘리는 배포 방식이다.
1.
stable 90% / canary 10% → 문제 없을 시 다음 단계
2.
stable 70% / canary 30% → 문제 없을 시 다음 단계
3.
stable 0% / canary 100% → 완전 전환
Istio에서는 VirtualService의 weight 기반 라우팅으로 트래픽 비율 조절을 선언적으로 구현할 수 있다.

4.2 DestinationRule subset 설정

Canary 배포를 위해 stable과 canary 버전을 구분하는 subset을 DestinationRule에 정의한다. subset은 Pod의 레이블을 기준으로 트래픽을 구분하도록 구성했다.
# httpbin-dr.yaml ... subsets: - name: stable labels: version: stable # version: stable 레이블을 가진 Pod로 라우팅 - name: canary labels: version: canary # version: canary 레이블을 가진 Pod로 라우팅
YAML
복사
DestinationRule은 동일 host에 대해 한 개만 유지하는 것이 권장된다. 따라서 3장에서 사용된 dr을 재사용한다.
kubectl apply -f httpbin-dr.yaml kubectl get destinationrule -n test
YAML
복사

4.3 VirtualService weight 기반 트래픽 분산 설정

# httpbin-vs.yaml apiVersion: networking.istio.io/v1 kind: VirtualService metadata: name: httpbin-vs namespace: test spec: hosts: - httpbin http: - route: - destination: host: httpbin subset: stable weight: 90 headers: # 분산 검증을 위해 request header 추가 request: add: x-version: stable - destination: host: httpbin subset: canary weight: 10 headers: # 분산 검증을 위해 request header 추가 request: add: x-version: canary
YAML
복사
kubectl apply -f httpbin-vs.yaml kubectl get virtualservice -n test
YAML
복사

4.4 트래픽 분산 검증

# 테스트 클라이언트 생성 (3장에서 만들었다면 생략) kubectl -n test run test-client \ --image=curlimages/curl \ --command -- sleep 365d # Pod 생성 확인 kubectl -n test get pod test-client
YAML
복사
# 100번 호출하여 트래픽 분산 확인 kubectl -n test exec test-client -- sh -c ' for i in $(seq 1 100); do curl -s -H "Connection: close" http://httpbin/headers \ | grep -o "\"X-Version\": \"[^\"]*\"" done | sort | uniq -c '
YAML
복사
[이미지 3] 90/10 트래픽 분산 확인
1000번으로 테스트 한 결과 stable | canary가 약 90% 대 10% 비율로 카운트됨을 확인할 수 있었다.

4.5 점진적 전환 테스트 (90/10 → 70/30 → 100/0)

# weight 변경 (yaml 파일 변경 가능) kubectl patch virtualservice httpbin-vs -n test \ --type='json' \ -p='[ {"op":"replace","path":"/spec/http/0/route/0/weight","value":70}, {"op":"replace","path":"/spec/http/0/route/1/weight","value":30} ]' # 다시 100번 호출하여 분산 확인 kubectl -n test exec test-client -- sh -c ' for i in $(seq 1 100); do curl -s -H "Connection: close" http://httpbin/headers \ | grep -o "\"X-Version\": \"[^\"]*\"" done | sort | uniq -c '
YAML
복사
[이미지 4] 70/30 트래픽 분산 확인
[이미지 5] 100/0 트래픽 분산 확인
테스트 결과 stable | canary의 비율이 90/10 → 70/30 → 100/0 으로 변화하는 것을 확인할 수 있었다.

5. 마무리

이번 데모에서는 stable | canary가 각 1개인 환경에서 maxEjectionPercent: 100을 사용해 둘 다 격리되어 바로 전부 503이 되는 구간이 나왔다. 운영 환경에서는 이런 설정이 그대로 사용된다면 서비스가 쉽게 블랙아웃이 날 수 있을 것이다. 따라서 outlierDection 값을 트래픽 패턴과 장애 패턴을 모니터링하며 서비스 특성에 맞게 튜닝하는 것이 중요할 것이다.
Canary 배포는 수동 Canary를 적용해 VirtualService의 weight를 조정하는 것으로, 운영 환경에서는 Git에 Push하여 ArgoCD로 동기화시킬 수 있을 것이다.
Circuit Breaker와 Canary 모두 애플리케이션 코드 수정이 아닌 yaml을 통한 선언형으로 관리된다는 것이 Istio의 강점이었다. 정책을 플랫폼 레이어에서 관리한다는 것의 실질적 의미를 직접 확인하는 시간이었다.
반면 DestinationRule과 VirtualService 간의 관계, subset 레이블 매핑, outlierDetection 동작 방식 등 개념을 이해하지 못하면 설정이 의도대로 동작하지 않음을 확인하였다. Istio 자체의 높은 러닝 커브를 체감할 수 있었고, 오히려 짧은 기간의 Come2us 프로젝트에서 AI만을 사용해 적용했다면 이해가 덜 했을 것 같다.
# 테스트 환경 제거 k3d cluster delete istio-test
YAML
복사

* Reference