infra
Platform

모듈 맵

[Kubernetes] Istio 서비스 메시가 제공하는 가시성과 mTLS 보안

0 / 29 완료

펼치기
0 / 29 완료0%

쿠버네티스 & GitOps · 27 / 29

[Kubernetes] Istio 서비스 메시가 제공하는 가시성과 mTLS 보안

Envoy 사이드카 인젝션으로 mTLS 자동 적용, 트래픽 가중치 기반 카나리 배포를 구현합니다

🚨INCIDENT ALERT
HIGH

마이크로서비스가 늘어나자 어느 호출이 느린지, 어떤 서비스 간 통신을 암호화해야 하는지 파악하기 어려워졌습니다. 애플리케이션 코드마다 재시도와 인증을 넣으면 일관성이 깨집니다. Service Mesh는 트래픽 제어, 관측성, mTLS를 인프라 레이어에서 다루게 해줍니다.

Istio 서비스 메시

마이크로서비스 아키텍처를 운영하다 보면 서비스 간 통신 보안, 트래픽 관찰, 장애 격리라는 세 가지 문제가 동시에 찾아옵니다. 보안팀은 "서비스 간 통신도 암호화해야 한다"고 요구하는데 수십 개 서비스 각각에 TLS 코드를 추가하는 것은 현실적이지 않습니다. 트래픽 분배는 L7 라우팅 없이는 헤더 기반 카나리를 구현할 수 없고, 특정 서비스 장애가 연쇄 장애로 번지는 것을 막을 서킷브레이커가 없습니다. Istio는 애플리케이션 코드를 전혀 수정하지 않고 이 세 가지를 모두 해결합니다. 각 파드에 Envoy 프록시를 사이드카로 자동 주입해서 모든 트래픽이 Envoy를 통하도록 만들고, mTLS 인증서 발급과 갱신, 트래픽 라우팅, 메트릭 수집을 대신 처리합니다. 이 모듈을 마치면 Istio를 클러스터에 설치하고, 서비스 간 mTLS를 자동 적용하며, 트래픽 10%를 신규 버전으로 보내는 카나리 배포를 구성할 수 있습니다.

이번 챕터에서 배울 것
  • 1Istio 아키텍처(istiod Control Plane과 Envoy 사이드카 Data Plane)를 설명할 수 있다
  • 2iptables, istio-init, istio-proxy로 이뤄지는 사이드카 인젝션 메커니즘을 설명할 수 있다
  • 3PeerAuthentication으로 mTLS 모드를 PERMISSIVE에서 STRICT로 설정할 수 있다
  • 4VirtualService로 트래픽 가중치를 분배해 카나리 배포를 수행할 수 있다
  • 5DestinationRule로 subset을 정의하고 로드밸런싱 정책을 설정할 수 있다
  • 6mTLS STRICT 모드 전환 후 통신 실패를 진단할 수 있다
실습 환경 준비
Istio 다운로드 및 CLI 설치
공식 릴리스 파일을 다운로드한 뒤 체크섬·서명을 검증하고 설치합니다(curl | sh 방식은 사용하지 않음).
클러스터 Istio 호환성 확인
istioctl x precheck
Istio 설치 (demo 프로파일)
istioctl install --set profile=demo -y
설치 확인
kubectl get pods -n istio-system
실습용 네임스페이스에 사이드카 인젝션 활성화
kubectl label namespace default istio-injection=enabled

Istio 아키텍처

Istio Service Mesh — Control Plane(istiod: Pilot 트래픽 규칙·Citadel 인증서·Galley 설정 배포)가 xDS API/인증서로, 각 Pod의 istio-proxy(Envoy)가 앱 컨테이너 트래픽을 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 규칙을 설정하기 때문입니다.

사이드카 프록시와 투명 리다이렉트 — istio-init 컨테이너가 파드에 iptables 규칙을 심어 앱의 모든 인바운드·아웃바운드 트래픽을 같은 파드의 Envoy 사이드카로 우회시킴. 앱 코드 변경 없이 mTLS·메트릭·라우팅이 적용되며, 사이드카 주입이 안 된 파드는 트래픽이 Envoy를 안 거쳐 mTLS 미적용확대

Kubernetes
# 사이드카가 주입된 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 핸드쉐이크, 메트릭 수집, 트래픽 라우팅을 처리하고 실제 목적지로 전달합니다.

Kubernetes
# 사이드카 인젝션 활성화된 네임스페이스 확인
kubectl get namespace -L istio-injection

# 특정 Pod에 사이드카 인젝션 비활성화 (모니터링 에이전트 등)
# Pod annotations에 추가:
# sidecar.istio.io/inject: "false"
💡개념

요청 한 번이 두 파드의 Envoy를 거쳐 가는 길 — 인터셉트부터 전달까지 6단계

앞에서 iptables가 트래픽을 Envoy로 우회시킨다는 것까지 봤습니다. 그런데 앱이 http://payment:8080 한 줄을 호출했을 때, 그 요청이 실제로 어느 지점들을 거쳐 상대 앱에 도착하는지를 단계로 그려두면 통신 실패가 났을 때 "어느 단계가 끊겼나"를 바로 좁힐 수 있습니다. 요청 하나는 보내는 파드의 Envoy와 받는 파드의 Envoy를 모두 거칩니다 — 즉 사이드카는 둘이 한 쌍으로 일합니다.

TEXT
[Pod A: app]  http://payment:8080 로 요청
   │
   ① iptables OUTPUT 가로채기     (istio-init 규칙이 app 아웃바운드를 → A의 Envoy 15001)
   │
   ② A Envoy: 라우팅 결정         (VirtualService·DestinationRule로 목적지 subset 선택)
   │
   ③ A Envoy: mTLS 클라이언트      (Citadel 발급 인증서로 상대 Envoy와 상호 TLS로 암호화)
   │            (파드 경계를 넘는 이 구간만 암호화됨)
   ▼
[Pod B]
   ④ iptables PREROUTING 가로채기  (인바운드를 → B의 Envoy 15006)
   │
   ⑤ B Envoy: mTLS 검증 + 인가 + 메트릭  (AuthorizationPolicy 확인, 요청 기록)
   │
   ⑥ B Envoy → 로컬 app           (127.0.0.1:8080 의 실제 컨테이너로 전달)
   ▼
[응답]  같은 경로를 역방향 — 양쪽 Envoy에 메트릭·트레이스가 남음

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① OUTPUT 가로채기istio-init이 심은 iptables가 app의 모든 아웃바운드를 같은 파드 Envoy(15001)로 우회사이드카 미주입이면 우회가 없어 평문 직행 → STRICT 상대가 거부
② A Envoy 라우팅VirtualService(어디로·가중치)·DestinationRule(subset·정책)로 목적지 결정subset 레이블이 Pod 레이블과 불일치면 NR(No Route) → 503
③ A Envoy mTLSCitadel 발급 워크로드 인증서로 상대 Envoy와 상호 TLS 수립한쪽만 mTLS면 CONFLICT·URX(핸드셰이크 실패)로 연결 리셋
④ PREROUTING 가로채기목적지 파드의 인바운드를 그 파드 Envoy(15006)로 우회목적지에 사이드카 없으면 인증서를 못 줘 STRICT에서 거부
⑤ B Envoy 검증·인가mTLS 검증 + AuthorizationPolicy 평가 + 요청 메트릭·트레이스 기록정책이 거부하면 403·UAEX(RBAC access denied)
⑥ 로컬 전달통과분만 127.0.0.1:8080의 실제 app 컨테이너로 넘김app이 아직 기동 전이면 UF(upstream failure)·503

즉 앱은 http://payment:8080 한 줄만 아는데, 실제로는 두 파드의 Envoy를 ①~⑥으로 거치며 라우팅·암호화·인가·관측이 얹힙니다. 통신이 실패하면 kubectl logs -c istio-proxy의 Envoy 코드(NR/URX/UAEX/UF)로 어느 단계인지 좁히고, 그 전에 양쪽 파드에 사이드카가 주입됐는지(①④)부터 확인합니다 — 사이드카 쌍 중 하나만 빠져도 mTLS는 성립하지 않습니다.

네임스페이스 레이블로 자동 인젝션 설정

위험 명령어모든 Pod가 순차 재시작되어 readinessProbe나 복제본 설정이 부족하면 일시 장애가 발생할 수 있습니다.

운영 Deployment 재시작

안전한 실행 조건: 무중단 배포 설정과 모니터링을 확인한 뒤 변경 창구에서 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
  • 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
  • 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl rollout restart deployment -n production

위 항목을 모두 확인한 후 복사할 수 있습니다

Kubernetes
# 네임스페이스에 자동 인젝션 활성화
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 모드 설정

YAML
# PERMISSIVE 모드: mTLS와 평문 트래픽 모두 허용 (기본값, 마이그레이션 단계)
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: PERMISSIVE
YAML
# STRICT 모드: mTLS 인증서 없는 트래픽은 거부
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT
Kubernetes
# 적용
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 설정

YAML
# 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
Kubernetes
kubectl apply -f destination-rule.yaml

# DestinationRule 적용 확인
kubectl get destinationrule -n production
istioctl proxy-config cluster payment-7d9b4c8f6-xkp2n.production | grep payment

트래픽 가중치 기반 카나리 배포

Istio 트래픽 관리 — VirtualService의 weight로 v1/v2에 트래픽 비율(예: 90/10)을 분배해 카나리 배포. Deployment 레플리카 수와 무관하게 정확한 비율을 유지하며, 지표 확인 후 10→50→100%로 점진 전환하거나 이상 시 즉시 v1으로 롤백확대

실습: v1 → v2 점진적 트래픽 전환

YAML
# 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
YAML
# 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
YAML
# 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
Kubernetes
# 적용
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 체험)

YAML
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
위험 명령어Git이나 매니페스트와 다른 임시 상태가 생기고 잘못된 패치가 즉시 운영 트래픽에 영향을 줄 수 있습니다.

운영 리소스 직접 패치

안전한 실행 조건: 변경 내용을 코드에 반영할 계획이 있고 영향 범위를 검토했을 때만 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 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단계: 사이드카 인젝션 여부 확인

Kubernetes
# 통신 실패 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 접근 로그로 실제 에러 확인

Kubernetes
# 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단계: 단계적 해결

Kubernetes
# 해결 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

심화 — 사이드카는 앱과 '동시에' 뜨지 않는다

💡개념

심화: 사이드카의 시작 순서와 종료 — 설정이 맞아도 타이밍이 어긋나면 실패한다

인젝션이 Envoy를 앱 옆에 놓아준다는 것까지는 배웠지만, 두 컨테이너의 시작·종료 타이밍이 어긋나면 mTLS 설정이 완벽해도 요청이 실패합니다.

  • 시작은 레이스입니다: 파드가 뜰 때 앱 컨테이너와 istio-proxy(Envoy)는 병렬로 시작됩니다. iptables는 istio-init이 미리 심어 앱의 아웃바운드를 전부 Envoy로 우회시키는데, 앱이 Envoy보다 먼저 요청을 쏘면 아직 리스닝하지 않는 Envoy로 리다이렉트돼 connection refused나 503이 납니다. DB 마이그레이션·초기화 API 호출을 기동 즉시 하는 앱에서 잘 터집니다. holdApplicationUntilProxyStarts를 켜면 Envoy가 준비될 때까지 앱 시작을 지연시켜 막을 수 있습니다.
  • istio-proxy는 스스로 종료하지 않습니다: Envoy는 파드가 사는 동안 계속 떠 있는 상주 프로세스입니다. 그래서 Job/CronJob 파드는 앱이 끝나도 istio-proxy가 계속 Running이라 파드가 Completed로 전이하지 못하고 영원히 남습니다. 그런 배치 파드는 사이드카 주입을 제외(sidecar.istio.io/inject: false)하거나, 앱 종료 후 Envoy를 종료시키는 방식(K8s 네이티브 사이드카 컨테이너 등)으로 처리합니다.

정리하면 사이드카는 공짜가 아니라 파드 생애주기에 새 참여자를 더한 것입니다. mTLS·라우팅 설정이 맞아도 컨테이너 수명주기가 어긋나면 장애가 나므로, 사이드카 파드는 시작·종료 순서까지 설계해야 합니다.

상황: payment 앱이 부팅 직후 DB와 다른 서비스로 초기화 호출을 하는데, 파드가 새로 뜬 직후 그 호출들이 connection refused나 503으로 실패합니다. 앱 로그엔 기동 시점에만 에러가 몰리고, 5~10초 뒤 요청은 멀쩡합니다. mTLS·VirtualService·DestinationRule은 모두 맞게 설정돼 있습니다.

원인: 앱 컨테이너와 istio-proxy가 병렬로 시작되는데, istio-init이 심은 iptables가 앱의 모든 아웃바운드를 Envoy로 우회시킵니다. 앱이 Envoy가 리스닝을 시작하기 전에 요청을 보내면, 갈 곳(Envoy 15001)이 아직 안 열려 connection refused가 납니다. Envoy가 준비되면 정상화됩니다. 설정 오류가 아니라 시작 순서 레이스입니다.

진단: 실패가 파드 기동 직후에만 몰리는 패턴인지 확인합니다(정상 운영 중엔 없음). kubectl logs <pod> -c istio-proxy로 Envoy가 언제 준비됐는지와 앱 로그의 첫 요청 시각을 비교합니다. istioctl proxy-config로 라우팅·mTLS 설정 자체는 정상임을 확인해 설정 문제를 배제합니다.

해결: 앱 시작을 Envoy 준비까지 미루도록 holdApplicationUntilProxyStarts를 켭니다(전역 MeshConfig 또는 파드 어노테이션 proxy.istio.io/config). 그러면 Envoy가 준비된 뒤 앱이 시작돼 초기 요청 실패가 사라집니다. 아울러 배치성 Job 파드는 istio-proxy가 끝나지 않아 Completed가 안 되므로, 그런 파드는 사이드카 주입에서 제외하거나 네이티브 사이드카 방식을 씁니다.

💼
실무 맥락
현업 패턴

시나리오: v2 결제 모듈 카나리 배포 — 에러 감지 시 즉시 롤백

새로운 결제 로직이 포함된 payment:2.0.0을 프로덕션에 배포합니다. 장애 없이 점진적으로 트래픽을 전환하고, 문제 감지 시 즉시 롤백하는 절차를 준비했습니다.

위험 명령어Git이나 매니페스트와 다른 임시 상태가 생기고 잘못된 패치가 즉시 운영 트래픽에 영향을 줄 수 있습니다.

운영 리소스 직접 패치

안전한 실행 조건: 변경 내용을 코드에 반영할 계획이 있고 영향 범위를 검토했을 때만 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 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
AuthorizationPolicyL7 접근 제어principals, methods, paths

마이그레이션 안전 순서: 전체 PERMISSIVE → 각 서비스 사이드카 확인 → 서비스별 STRICT 전환 → 네임스페이스 STRICT 전환

실습 단계
1

Istio 설치 및 istiod 상태 확인

istioctl install --set profile=demo -y kubectl get pods -n istio-system

예상 출력

NAME                                   READY   STATUS    RESTARTS
istiod-xxx                             1/1     Running   0
istio-ingressgateway-xxx               1/1     Running   0
istio-egressgateway-xxx                1/1     Running   0
2

네임스페이스에 사이드카 인젝션 활성화 및 검증

kubectl label namespace default istio-injection=enabled kubectl get namespace default --show-labels | grep istio-injection

예상 출력

default   Active   10d   istio-injection=enabled,kubernetes.io/metadata.name=default
3

파드 배포 후 사이드카 주입 확인

kubectl run nginx --image=nginx:1.25 kubectl get pod nginx -o jsonpath='{.spec.containers[*].name}'

예상 출력

nginx istio-proxy
4

PeerAuthentication STRICT 모드 적용

kubectl apply -f - <<EOF apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: default namespace: default spec: mtls: mode: STRICT EOF kubectl get peerauthentication -n default

예상 출력

NAME      MODE     AGE
default   STRICT   5s
5

mTLS 연결 상태 확인

istioctl authn tls-check nginx.default.svc.cluster.local 2>/dev/null | head -5

예상 출력

HOST:PORT                                STATUS  SERVER  CLIENT
nginx.default.svc.cluster.local:80    OK      STRICT  STRICT
🔍실행 후 확인할 것
  • kubectl get pods -n istio-system 에서 istiod Pod이 Running 상태인지 먼저 확인 — istiod가 CrashLoopBackOff 이면 이후 사이드카 인젝션 및 mTLS 설정이 모두 실패함
  • kubectl get pod -n <namespace> -o yaml | grep -c "istio-proxy" 결과가 Pod 수와 일치하면 모든 Pod에 사이드카 인젝션 성공 — 숫자가 적으면 레이블 또는 어노테이션 설정 확인
  • istioctl authn tls-check <pod>.<ns> <service>.<ns>.svc.cluster.local 출력에서 STATUS=OK + SERVER=STRICT + CLIENT=STRICT 조합이면 mTLS 정상 동작 — CONFLICT 이면 한쪽에 사이드카 없음
  • VirtualService weight 합산이 100이어야 함: kubectl get virtualservice -o yaml 에서 route weight 합계가 100이 아니면 503 오류 발생. 0%일 때도 weight: 0으로 명시 필요
  • 카나리 트래픽 분배 확인: for i in $(seq 1 20); do curl -s http://svc/version; done | sort | uniq -c 결과에서 v1/v2 분포가 설정한 weight(90/10)에 근사하면 정상. 편차가 크면 DestinationRule subset 레이블과 Pod 레이블 불일치 확인
  • Envoy 에러코드 해석: kubectl logs <pod> -c istio-proxy | grep -E "NR|UF|URX" — NR(No Route)이면 VirtualService 미적용, UF(Upstream Failure)이면 서비스 자체 장애, URX이면 mTLS 핸드쉐이크 실패

명령어·단축키 빠른 참조

이 모듈에서 다룬 istioctl과 사이드카·mTLS·카나리 관련 kubectl 명령을 실전 옵션과 함께 모았습니다.

명령어/단축키용도자주 쓰는 예
istioctl install컨트롤 플레인 설치(프로파일)istioctl install --set profile=demo -y
istioctl x precheck설치 전 클러스터 호환성 점검istioctl x precheck
istioctl authn tls-check서비스 간 mTLS 상태(STRICT/CONFLICT)istioctl authn tls-check payment.prod svc.prod.svc.cluster.local
istioctl proxy-configEnvoy 실제 설정 덤프(route/cluster/endpoint)istioctl proxy-config route <pod>.<ns>, istioctl proxy-config cluster <pod>.<ns>
istioctl dashboardKiali·Prometheus 대시보드 열기istioctl dashboard kiali
kubectl label namespace네임스페이스 자동 사이드카 주입kubectl label namespace default istio-injection=enabled
kubectl get pod -o jsonpath사이드카(istio-proxy) 주입 확인kubectl get pod X -o jsonpath='{.spec.containers[*].name}'app istio-proxy
kubectl logs -c istio-proxyEnvoy 접근 로그·에러코드 확인kubectl logs <pod> -c istio-proxy | grep -E 'UF|URX|NR'
kubectl apply -f (PeerAuth/DR/VS)mTLS·subset·가중치 라우팅 적용kubectl apply -f peer-auth-strict.yaml
kubectl patch virtualservice카나리 트래픽 가중치 조정kubectl patch virtualservice payment-vs --type=json -p='[...]'

관련 모듈로 더 깊이:

다음 모듈 monitoring-prometheus에서는 Istio의 사이드카가 자동으로 노출하는 Envoy 메트릭을 Prometheus로 수집하고, 서비스 메시 레이어의 에러율과 레이턴시를 Grafana 대시보드로 가시화하는 방법을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

Istio의 사이드카 인젝션 방식에서 Envoy 프록시가 모든 트래픽을 가로채는 메커니즘은?

Q2

VirtualService와 DestinationRule의 역할 구분으로 올바른 것은?

Q3

Istio mTLS STRICT 모드로 전환 후 일부 서비스 간 통신이 실패하는 상황에서 가장 가능성 높은 원인은?

Q4

카나리 배포에서 VirtualService의 weight: 10을 v2에 설정했을 때 실제 동작은?

Q5

Istio가 애플리케이션 코드를 바꾸지 않고도 mTLS·트래픽 라우팅·관측을 제공할 수 있는 이유는?

Q6

모니터링 에이전트 파드처럼 특정 파드만 Istio 사이드카 주입에서 제외하려면?

Q7

[심화] 사이드카가 주입된 파드가 새로 뜬 직후, 앱이 기동하자마자 보내는 아웃바운드 요청만 connection refused로 실패하고 몇 초 뒤부터는 정상이 된다. 원인은?

Q8

[심화] Istio 사이드카가 주입된 네임스페이스에서 배치 작업 Job의 파드가 앱 프로세스는 끝났는데도 Completed가 되지 않고 계속 Running으로 남는다. 이유와 해결로 옳은 것은?

0 / 8 답변

🧪 실습으로 확인하기

K8s 기초 — Pod/Deployment/Service 생성

초급

kubectl로 nginx Pod를 생성하고 Deployment와 Service를 차례로 만들어 클러스터 외부에서 접근 가능한 상태까지 구성한다. K8s 3대 리소스의 역할과 관계를 직접 손으로 익힌다.

55📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요