마이크로서비스가 늘어나자 어느 호출이 느린지, 어떤 서비스 간 통신을 암호화해야 하는지 파악하기 어려워졌습니다. 애플리케이션 코드마다 재시도와 인증을 넣으면 일관성이 깨집니다. Service Mesh는 트래픽 제어, 관측성, mTLS를 인프라 레이어에서 다루게 해줍니다.
Istio 서비스 메시
마이크로서비스 아키텍처를 운영하다 보면 서비스 간 통신 보안, 트래픽 관찰, 장애 격리라는 세 가지 문제가 동시에 찾아옵니다. 보안팀은 "서비스 간 통신도 암호화해야 한다"고 요구하는데 수십 개 서비스 각각에 TLS 코드를 추가하는 것은 현실적이지 않습니다. 트래픽 분배는 L7 라우팅 없이는 헤더 기반 카나리를 구현할 수 없고, 특정 서비스 장애가 연쇄 장애로 번지는 것을 막을 서킷브레이커가 없습니다. Istio는 애플리케이션 코드를 전혀 수정하지 않고 이 세 가지를 모두 해결합니다. 각 파드에 Envoy 프록시를 사이드카로 자동 주입해서 모든 트래픽이 Envoy를 통하도록 만들고, mTLS 인증서 발급과 갱신, 트래픽 라우팅, 메트릭 수집을 대신 처리합니다. 이 모듈을 마치면 Istio를 클러스터에 설치하고, 서비스 간 mTLS를 자동 적용하며, 트래픽 10%를 신규 버전으로 보내는 카나리 배포를 구성할 수 있습니다.
- 1Istio 아키텍처: istiod Control Plane + Envoy 사이드카 Data Plane
- 2사이드카 인젝션 메커니즘 — iptables, istio-init, istio-proxy
- 3PeerAuthentication으로 mTLS 모드 설정 (PERMISSIVE → STRICT)
- 4VirtualService로 트래픽 가중치 분배 (카나리 배포)
- 5DestinationRule로 subset 정의 및 로드밸런싱 정책 설정
- 6TroubleCase: mTLS STRICT 모드 전환 후 통신 실패 진단
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.20.0 sh - && export PATH=$PWD/istio-1.20.0/bin:$PATHistioctl x precheckistioctl install --set profile=demo -ykubectl get pods -n istio-systemkubectl label namespace default istio-injection=enabledIstio 아키텍처
┌─────────────────────────────────────────────────────────────┐
│ Control Plane (istiod) │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Pilot │ │ Citadel │ │ Galley │ │
│ │(트래픽 규칙) │ │(인증서 관리) │ │(설정 검증/배포) │ │
│ └──────┬──────┘ └──────┬───────┘ └───────────────────┘ │
│ │ xDS API │ 인증서 │
└─────────┼─────────────────┼──────────────────────────────────┘
│ │
┌─────────┼─────────────────┼──────────────────────────────────┐
│ │ Data Plane │ │
│ Pod A │ │ Pod B │
│ ┌──────▼──────────────┐ │ ┌──────────────────────────┐ │
│ │ [App Container] │ │ │ [App Container] │ │
│ │ ← 포트 8080 │ │ │ ← 포트 8080 │ │
│ │─────────────────────│ │ │──────────────────────────│ │
│ │ [istio-proxy] │──┼─▶│ [istio-proxy] │ │
│ │ Envoy :15001/:15006│ └ │ Envoy :15001/:15006 │ │
│ │ mTLS + 메트릭 수집 │ │ mTLS + 메트릭 수집 │ │
│ └──────────────────── ┘ └──────────────────────────┘ │
│ (iptables가 모든 트래픽을 Envoy로 리다이렉트) │
└─────────────────────────────────────────────────────────────┘
istiod는 세 가지를 담당합니다:
- Pilot: VirtualService, DestinationRule 등 트래픽 규칙을 Envoy에 xDS API로 배포
- Citadel: 워크로드 인증서(SVID)를 자동 발급하고 24시간마다 갱신
- Galley: Istio 설정 유효성 검증 및 istiod에 전달
사이드카 인젝션 이해
iptables로 트래픽을 Envoy로 투명하게 리다이렉트하는 원리
Istio를 도입했는데 특정 파드의 트래픽이 Envoy를 통하지 않아 mTLS가 적용되지 않습니다. istioctl x check-inject를 실행해도 원인이 명확하지 않고, 애플리케이션 코드는 아무것도 바꾸지 않았는데 왜 어떤 파드는 메시에 포함되고 어떤 파드는 제외되는지 이해하기 어렵습니다. 사이드카 인젝션 메커니즘을 이해하지 못하면 인젝션 실패 원인을 찾지 못하고 무작정 파드를 재시작하게 됩니다. 이 CB에서는 istio-init init container가 iptables 규칙을 설정하는 원리와, 그 결과 앱 컨테이너가 투명하게 Envoy를 경유하는 구조를 다룹니다. 이것이 가능한 이유는 istio-init init container가 Pod 시작 전에 iptables 규칙을 설정하기 때문입니다.

# 사이드카가 주입된 Pod 내부 컨테이너 확인
kubectl get pod payment-7d9b4c8f6-xkp2n -o jsonpath='{.spec.containers[*].name}'
# 출력: payment istio-proxy ← 두 컨테이너
# istio-proxy 컨테이너 상세 정보
kubectl describe pod payment-7d9b4c8f6-xkp2n | grep -A20 "istio-proxy"
# iptables 규칙 확인 (nsenter로 Pod 네트워크 네임스페이스 진입)
# 실제 운영에서는 kubectl debug 사용
kubectl debug -n production \
-it payment-7d9b4c8f6-xkp2n \
--image=nicolaka/netshoot \
-- iptables -t nat -L ISTIO_REDIRECT
iptables 규칙 구조:
Chain ISTIO_REDIRECT (인바운드)
REDIRECT → TCP 15006 # 모든 인바운드 트래픽 → Envoy 15006
Chain ISTIO_OUTPUT (아웃바운드)
RETURN → uid 1337 # Envoy 자신의 트래픽은 제외 (루프 방지)
REDIRECT → TCP 15001 # 나머지 아웃바운드 → Envoy 15001
결과: 앱 컨테이너는 localhost:8080으로 요청을 수신하고 http://other-service:80으로 요청을 보내지만, 실제로는 모두 Envoy를 경유합니다. Envoy가 mTLS 핸드쉐이크, 메트릭 수집, 트래픽 라우팅을 처리하고 실제 목적지로 전달합니다.
# 사이드카 인젝션 활성화된 네임스페이스 확인
kubectl get namespace -L istio-injection
# 특정 Pod에 사이드카 인젝션 비활성화 (모니터링 에이전트 등)
# Pod annotations에 추가:
# sidecar.istio.io/inject: "false"
네임스페이스 레이블로 자동 인젝션 설정
운영 Deployment 재시작
안전한 실행 조건: 무중단 배포 설정과 모니터링을 확인한 뒤 변경 창구에서 실행하세요.
실행 전 반드시 확인
- 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
- 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
- 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl rollout restart deployment -n production위 항목을 모두 확인한 후 복사할 수 있습니다
# 네임스페이스에 자동 인젝션 활성화
kubectl label namespace production istio-injection=enabled
# 기존 Pod에 적용하려면 재배포 필요
kubectl rollout restart deployment -n production
# 사이드카 주입 확인
kubectl get pod -n production -o yaml | grep -c "istio-proxy"
# 숫자가 Pod 수와 일치해야 함
mTLS 설정
PeerAuthentication으로 mTLS 모드 설정
# PERMISSIVE 모드: mTLS와 평문 트래픽 모두 허용 (기본값, 마이그레이션 단계)
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: PERMISSIVE
# STRICT 모드: mTLS 인증서 없는 트래픽은 거부
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT
# 적용
kubectl apply -f peer-auth-strict.yaml
# mTLS 상태 확인
istioctl x check-inject -n production
istioctl authn tls-check payment.production.svc.cluster.local
# 출력 예시:
# HOST:PORT STATUS SERVER CLIENT
# payment.production.svc.cluster.local:8080 OK STRICT STRICT
DestinationRule로 아웃바운드 mTLS 설정
# mTLS를 클라이언트 측에서도 강제
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: payment-mtls
namespace: production
spec:
host: payment.production.svc.cluster.local
trafficPolicy:
tls:
mode: ISTIO_MUTUAL # Istio 발급 인증서로 mTLS
connectionPool:
tcp:
maxConnections: 100
http:
h2UpgradePolicy: UPGRADE
outlierDetection: # 서킷브레이커
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
kubectl apply -f destination-rule.yaml
# DestinationRule 적용 확인
kubectl get destinationrule -n production
istioctl proxy-config cluster payment-7d9b4c8f6-xkp2n.production | grep payment
트래픽 가중치 기반 카나리 배포
실습: v1 → v2 점진적 트래픽 전환
# 1. 두 버전의 Deployment 준비
# v1 Deployment (기존)
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-v1
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: payment
version: v1
template:
metadata:
labels:
app: payment
version: v1 # 버전 레이블 필수
spec:
containers:
- name: payment
image: payment:1.0.0
ports:
- containerPort: 8080
---
# v2 Deployment (신규)
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-v2
namespace: production
spec:
replicas: 1 # 처음엔 적게
selector:
matchLabels:
app: payment
version: v2
template:
metadata:
labels:
app: payment
version: v2
spec:
containers:
- name: payment
image: payment:2.0.0
ports:
- containerPort: 8080
# 2. DestinationRule로 v1/v2 subset 정의
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: payment-dr
namespace: production
spec:
host: payment # Service 이름
subsets:
- name: v1
labels:
version: v1 # 이 레이블을 가진 Pod를 v1 subset으로 그룹화
- name: v2
labels:
version: v2
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
# 3. VirtualService로 트래픽 분배 (처음: v1 90% / v2 10%)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: payment-vs
namespace: production
spec:
hosts:
- payment # Service 이름과 일치
http:
- route:
- destination:
host: payment
subset: v1
weight: 90 # v1으로 90%
- destination:
host: payment
subset: v2
weight: 10 # v2으로 10%
timeout: 5s # 타임아웃 설정
retries:
attempts: 3
perTryTimeout: 2s
retryOn: gateway-error,connect-failure,retriable-4xx
# 적용
kubectl apply -f destination-rule.yaml
kubectl apply -f virtual-service.yaml
# 트래픽 분배 확인 (부하 테스트)
for i in $(seq 1 100); do
kubectl exec -n production \
$(kubectl get pod -n production -l app=frontend -o name | head -1) \
-- curl -s http://payment:8080/version
done | sort | uniq -c
# 약 90개: "version: v1"
# 약 10개: "version: v2"
# v2 에러율 모니터링 (Grafana 또는 istioctl)
istioctl dashboard kiali # Kiali 트래픽 시각화 대시보드
헤더 기반 카나리 (특정 사용자만 v2 체험)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: payment-vs
namespace: production
spec:
hosts:
- payment
http:
# QA 팀은 X-Canary: true 헤더로 항상 v2로 라우팅
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: payment
subset: v2
weight: 100
# 나머지는 v1
- route:
- destination:
host: payment
subset: v1
weight: 100
운영 리소스 직접 패치
안전한 실행 조건: 변경 내용을 코드에 반영할 계획이 있고 영향 범위를 검토했을 때만 실행하세요.
실행 전 반드시 확인
- 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
- 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
- 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl patch virtualservice payment-vs -n production위 항목을 모두 확인한 후 복사할 수 있습니다
# v2 테스트 (헤더 포함)
curl -H "x-canary: true" http://payment.production.svc.cluster.local/api/pay
# 단계별 트래픽 전환 스크립트
for weight in 10 25 50 75 100; do
echo "v2 트래픽: $weight%"
kubectl patch virtualservice payment-vs -n production \
--type=json \
-p="[
{\"op\": \"replace\", \"path\": \"/spec/http/0/route/0/weight\", \"value\": $((100-weight))},
{\"op\": \"replace\", \"path\": \"/spec/http/0/route/1/weight\", \"value\": $weight}
]"
echo "에러율 모니터링 중... (5분 대기)"
sleep 300
done
트러블슈팅
PeerAuthentication을 STRICT 모드로 변경한 직후 일부 서비스에서 RBAC: access denied 또는 upstream connect error 오류가 발생합니다.
1단계: 사이드카 인젝션 여부 확인
# 통신 실패 Pod에 istio-proxy 사이드카가 있는지 확인
kubectl get pod -n production -o json | \
jq '.items[] | select(.spec.containers[].name == "istio-proxy") | .metadata.name'
# 사이드카가 없는 Pod 찾기
kubectl get pod -n production -o yaml | \
grep -B5 "istio-injection: false"
# 특정 Pod의 사이드카 상태 한 번에 확인
kubectl describe pod <pod-name> -n production | \
grep -E "istio-proxy|istio-init|Containers:"
2단계: mTLS 연결 상태 진단
# 서비스 간 mTLS 상태 확인
istioctl authn tls-check <pod-name>.<namespace> <service-name>.<namespace>.svc.cluster.local
# 예시:
istioctl authn tls-check payment-7d9b4c8f6-xkp2n.production \
order.production.svc.cluster.local
# 출력:
# HOST:PORT STATUS SERVER CLIENT
# order.production.svc.cluster.local:8080 OK STRICT STRICT ← 정상
# order.production.svc.cluster.local:8080 CONFLICT STRICT DISABLE ← 문제!
# CONFLICT: 서버는 STRICT를 요구하지만 클라이언트는 mTLS 없이 연결 시도
3단계: Envoy 접근 로그로 실제 에러 확인
# Envoy 접근 로그 활성화
kubectl apply -f - << 'EOF'
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
name: enable-access-log
namespace: production
spec:
accessLogging:
- providers:
- name: envoy
EOF
# 실패하는 Pod의 Envoy 로그 확인
kubectl logs -n production <pod-name> -c istio-proxy | \
grep -E "response_code=403|UF|URX|NR"
# 주요 에러 코드 해석:
# UF: Upstream connection failure
# URX: Upstream connection reset (mTLS 핸드쉐이크 실패)
# NR: No route found
# UAEX: Upstream application exception (AuthorizationPolicy 거부)
4단계: 단계적 해결
# 해결 1: 사이드카가 없는 네임스페이스에 인젝션 활성화
kubectl label namespace <namespace> istio-injection=enabled
kubectl rollout restart deployment -n <namespace>
# 해결 2: 특정 Pod만 PERMISSIVE 예외 처리 (임시)
kubectl apply -f - << 'EOF'
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: legacy-service-exception
namespace: production
spec:
selector:
matchLabels:
app: legacy-batch # 사이드카 없는 레거시 앱
mtls:
mode: PERMISSIVE # 이 Pod만 예외
EOF
# 해결 3: DestinationRule의 tls 모드 확인
kubectl get destinationrule -n production -o yaml | \
grep -A3 "tls:"
# mode: DISABLE 라면 ISTIO_MUTUAL로 변경
# 해결 4: AuthorizationPolicy 규칙 확인 (403 에러인 경우)
kubectl get authorizationpolicy -n production
kubectl describe authorizationpolicy <name> -n production
5단계: 프록시 설정 덤프로 최종 확인
# Envoy가 실제로 받은 라우팅 설정 확인
istioctl proxy-config route <pod-name>.production
istioctl proxy-config cluster <pod-name>.production | grep payment
istioctl proxy-config endpoint <pod-name>.production | grep payment
# 전체 설정 덤프 (고급 분석용)
istioctl proxy-config all <pod-name>.production -o json > /tmp/proxy-config.json
시나리오: v2 결제 모듈 카나리 배포 — 에러 감지 시 즉시 롤백
새로운 결제 로직이 포함된 payment:2.0.0을 프로덕션에 배포합니다. 장애 없이 점진적으로 트래픽을 전환하고, 문제 감지 시 즉시 롤백하는 절차를 준비했습니다.
운영 리소스 직접 패치
안전한 실행 조건: 변경 내용을 코드에 반영할 계획이 있고 영향 범위를 검토했을 때만 실행하세요.
실행 전 반드시 확인
- 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
- 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
- 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl patch virtualservice payment-vs -n production위 항목을 모두 확인한 후 복사할 수 있습니다
# 배포 전: 기준선 에러율 측정
istioctl dashboard prometheus
# PromQL: sum(rate(istio_requests_total{destination_service="payment.production.svc.cluster.local", response_code=~"5.."}[5m])) / sum(rate(istio_requests_total{destination_service="payment.production.svc.cluster.local"}[5m]))
# 기준값: 0.2% (0.002)
# Step 1: v2 Deployment 배포 (0% 트래픽)
kubectl apply -f payment-v2-deployment.yaml
# Step 2: 10% 트래픽 전환
kubectl apply -f - << 'EOF'
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: payment-vs
namespace: production
spec:
hosts:
- payment
http:
- route:
- destination:
host: payment
subset: v1
weight: 90
- destination:
host: payment
subset: v2
weight: 10
EOF
# Step 3: 5분간 v2 에러율 모니터링
watch -n 10 'kubectl exec -n monitoring \
$(kubectl get pod -n monitoring -l app=prometheus -o name | head -1) \
-- curl -sg "http://localhost:9090/api/v1/query" \
--data-urlencode "query=sum(rate(istio_requests_total{destination_workload=\"payment-v2\",response_code=~\"5..\"}[2m])) / sum(rate(istio_requests_total{destination_workload=\"payment-v2\"}[2m]))" \
| jq ".data.result[0].value[1]"'
# v2 에러율이 1%를 초과하면 즉시 롤백
V2_ERROR_RATE=$(kubectl exec -n monitoring ... | jq -r ...)
if (( $(echo "$V2_ERROR_RATE > 0.01" | bc -l) )); then
echo "에러율 임계값 초과, 롤백 시작"
# 롤백: 100% v1으로 되돌리기
kubectl patch virtualservice payment-vs -n production \
--type=json \
-p='[{"op":"replace","path":"/spec/http/0/route/0/weight","value":100},
{"op":"replace","path":"/spec/http/0/route/1/weight","value":0}]'
fi
# 안정적이면 25% → 50% → 100% 순으로 단계적 전환
# 각 단계 5분 관찰 후 이상 없으면 다음 단계 진행
Istio 없이 카나리 배포를 구현하려면 Deployment 레플리카 수로 트래픽을 조절해야 합니다(10%를 위해 v1 9개, v2 1개 유지). Istio의 weight 기반 라우팅은 각 Deployment의 레플리카 수와 무관하게 정확한 비율을 유지할 수 있어, 적은 v2 레플리카로도 세밀한 트래픽 분배가 가능합니다.
핵심 요약
| 리소스 | 역할 | 주요 설정 |
|---|---|---|
| PeerAuthentication | 인바운드 mTLS 정책 | PERMISSIVE / STRICT |
| DestinationRule | 아웃바운드 정책 + subset 정의 | tls.mode, subsets, outlierDetection |
| VirtualService | 라우팅 규칙 | weight, match, timeout, retries |
| AuthorizationPolicy | L7 접근 제어 | principals, methods, paths |
마이그레이션 안전 순서: 전체 PERMISSIVE → 각 서비스 사이드카 확인 → 서비스별 STRICT 전환 → 네임스페이스 STRICT 전환
다음 단계
- Kiali 대시보드로 서비스 메시 토폴로지 시각화
- Jaeger/Zipkin 트레이싱 연동으로 요청 추적
- AuthorizationPolicy로 서비스 간 접근 제어 (L7 RBAC)
- Istio Ingress Gateway로 외부 트래픽 진입 제어