[Istio] 트래픽 제어

xgro·2025년 4월 25일
0

Istio

목록 보기
5/17
post-thumbnail

📌 Notice

Istio Hands-on Study (=Istio)
직접 실습을 통해 Isito를 배포 및 설정하는 내용을 정리한 블로그입니다.

CloudNetaStudy 그룹에서 스터디를 진행하고 있습니다.

Gasida님께 다시한번 🙇 감사드립니다.

EKS 관련 이전 스터디 내용은 아래 링크를 통해 확인할 수 있습니다.



📌 Overview

이번 주 스터디에서는 Istio 서비스 메시의 핵심 기능 중 하나인 트래픽 제어에 대해 중점적으로 살펴보았습니다.

특히 "Istio in Action" 책의 5장 내용을 바탕으로, 실제 마이크로서비스 환경에서 트래픽을 세밀하게 제어하는 방법에 대해 깊이 있게 다루었습니다.

마이크로서비스 아키텍처에서는 서비스 간 통신이 복잡해지면서 트래픽 제어의 중요성이 더욱 커지고 있습니다.

이번 스터디에서는 배포와 릴리스의 차이점부터 시작해 VirtualService, DestinationRule 등의 Istio 리소스를 활용한 라우팅 설정, 카나리 배포, 트래픽 미러링까지 다양한 고급 배포 전략을 실습해 보았습니다.

특히 흥미로웠던 점은 Flagger라는 도구를 사용해 카나리 릴리스를 자동화하는 방법과, 실제 사용자에게 영향을 주지 않으면서 새 버전을 테스트할 수 있는 트래픽 미러링 기법이었습니다.

이러한 기술들을 활용하면 프로덕션 환경에서의 위험을 최소화하면서 새로운 기능을 안전하게 출시할 수 있습니다.



📌 Istio를 활용한 마이크로서비스 트래픽 제어

👉 Step 00. 실습 환경 구성

[실습 환경 구성] k8s(1.23.17) 배포 : NodePort(30000 HTTP, 30005 HTTPS)

먼저, 실습 환경을 구성하기 위해 Kubernetes 클러스터와 Istio를 설치합니다.

실습에서는 Kind를 사용하여 로컬에 Kubernetes 클러스터를 생성합니다.

# kind 클러스터 생성
kind create cluster --name myk8s --image kindest/node:v1.23.17 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000 # Sample Application (istio-ingrssgateway) HTTP
    hostPort: 30000
  - containerPort: 30001 # Prometheus
    hostPort: 30001
  - containerPort: 30002 # Grafana
    hostPort: 30002
  - containerPort: 30003 # Kiali
    hostPort: 30003
  - containerPort: 30004 # Tracing
    hostPort: 30004
  - containerPort: 30005 # Sample Application (istio-ingrssgateway) HTTPS
    hostPort: 30005
  - containerPort: 30006 # TCP Route
    hostPort: 30006
  - containerPort: 30007 # kube-ops-view
    hostPort: 30007
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
EOF

# 설치 확인
docker ps

# 노드에 기본 툴 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bridge-utils net-tools dnsutils tcpdump ngrep iputils-ping git vim -y'

[실습 환경 구성] istio 1.17.8 설치 (addon 필수)

# myk8s-control-plane 진입 후 설치 진행
docker exec -it myk8s-control-plane bash
-----------------------------------
# (옵션) 코드 파일들 마운트 확인
tree /istiobook/ -L 1
혹은
git clone ... /istiobook

# istioctl 설치
export ISTIOV=1.17.8
echo 'export ISTIOV=1.17.8' >> /root/.bashrc

curl -s -L https://istio.io/downloadIstio | ISTIO_VERSION=$ISTIOV sh -
cp istio-$ISTIOV/bin/istioctl /usr/local/bin/istioctl
istioctl version --remote=false

# default 프로파일 컨트롤 플레인 배포
istioctl install --set profile=default -y

# 설치 확인 : istiod, istio-ingressgateway, crd 등
kubectl get istiooperators -n istio-system -o yaml
kubectl get all,svc,ep,sa,cm,secret,pdb -n istio-system
kubectl get cm -n istio-system istio -o yaml
kubectl get crd | grep istio.io | sort

# 보조 도구 설치
kubectl apply -f istio-$ISTIOV/samples/addons
kubectl get pod -n istio-system

# 빠져나오기
exit
-----------------------------------

# 실습을 위한 네임스페이스 설정
kubectl create ns istioinaction
kubectl label namespace istioinaction istio-injection=enabled
kubectl get ns --show-labels

# istio-ingressgateway 서비스 : NodePort 변경 및 nodeport 지정 변경 , externalTrafficPolicy 설정 (ClientIP 수집)
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort", "ports": [{"port": 80, "targetPort": 8080, "nodePort": 30000}]}}'
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 8443, "nodePort": 30005}]}}'
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec":{"externalTrafficPolicy": "Local"}}'
kubectl describe svc -n istio-system istio-ingressgateway

# NodePort 변경 및 nodeport 30001~30003으로 변경 : prometheus(30001), grafana(30002), kiali(30003), tracing(30004)
kubectl patch svc -n istio-system prometheus -p '{"spec": {"type": "NodePort", "ports": [{"port": 9090, "targetPort": 9090, "nodePort": 30001}]}}'
kubectl patch svc -n istio-system grafana -p '{"spec": {"type": "NodePort", "ports": [{"port": 3000, "targetPort": 3000, "nodePort": 30002}]}}'
kubectl patch svc -n istio-system kiali -p '{"spec": {"type": "NodePort", "ports": [{"port": 20001, "targetPort": 20001, "nodePort": 30003}]}}'
kubectl patch svc -n istio-system tracing -p '{"spec": {"type": "NodePort", "ports": [{"port": 80, "targetPort": 16686, "nodePort": 30004}]}}'

# Prometheus 접속 : envoy, istio 메트릭 확인
open http://127.0.0.1:30001

# Grafana 접속
open http://127.0.0.1:30002

# Kiali 접속 1 : NodePort
open http://127.0.0.1:30003

# (옵션) Kiali 접속 2 : Port forward
kubectl port-forward deployment/kiali -n istio-system 20001:20001 &
open http://127.0.0.1:20001

# tracing 접속 : 예거 트레이싱 대시보드
open http://127.0.0.1:30004


# 내부 접속 테스트용 netshoot 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: netshoot
spec:
  containers:
  - name: netshoot
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

👉 Step 01. 배포 위험 최소화를 위한 전략 이해

✅ Reducing the risk of deploying new code

1장에서 소개된 ACME 회사는 클라우드 플랫폼 전환을 통해 코드 배포 위험을 줄이기 위한 다양한 관행을 채택했습니다. 그중 하나가 블루/그린(Blue/Green) 배포 전략이었습니다.

블루/그린 배포에서 ACME는 변경하고자 하는 서비스의 v2(그린)를 v1(블루) 옆에 프로덕션 환경에 배포했습니다.

고객에게 새 버전을 출시할 준비가 되면 트래픽을 v2로 전환했습니다. 이 접근법은 문제 발생 시 서비스의 v1으로 빠르게 되돌릴 수 있어 배포 중 서비스 중단을 최소화하는 데 도움이 되었습니다.

그러나 블루/그린 배포에도 한계가 있습니다. v1에서 v2로 전환할 때 모든 코드 변경사항이 한 번에 적용되는 "빅뱅(Big Bang)" 방식의 위험은 여전히 존재합니다. 이제 배포 위험을 더욱 줄일 수 있는 방법을 살펴보겠습니다.

✅ 배포(Deployment)와 릴리스(Release)의 차이

가상의 카탈로그 서비스를 예로 들어 배포와 릴리스의 차이점을 설명해 보겠습니다.

catalog 서비스 v1이 현재 운영 중이라고 가정해봅시다. 이 서비스에 코드 변경사항을 도입하려면 다음과 같은 과정을 거칩니다:
1. CI 시스템으로 빌드
2. 새 버전(v1.1)으로 태그 지정
3. 스테이징 환경에 배포 및 테스트
4. 검증 및 승인 후 운영 환경으로 이동


출처 - 이제원님 @IstioStudy


배포(Deployment)
새 코드를 운영 환경 리소스(서버, 컨테이너 등)에 설치하는 과정이지만, 이 단계에서는 아직 실제 사용자 트래픽을 받지 않습니다.

배포 후에는 운영 환경에서 스모크 테스트를 실행하여 새 버전이 기대한 대로 작동하는지 검증할 수 있습니다.

메트릭과 로그를 통해 새 배포의 안정성을 확인할 수 있습니다.

릴리스(Release)
실제 사용자 트래픽을 새 배포로 전환하는 과정입니다.

중요한 점은 릴리스가 반드시 한 번에 모든 트래픽을 전환해야 하는 것은 아니라는 것입니다.

새로운 코드를 운영 환경에 안전하게 도입하기 위해서는 배포와 릴리스를 분리하는 것이 중요합니다.

예를 들어, 새 버전을 내부 직원에게만 먼저 공개할 수 있습니다.

내부 직원들은 새 버전을 사용하면서 로그와 메트릭을 통해 코드 변경의 효과가 의도대로인지 관찰하고 검증할 수 있습니다.

이 방식을 카나리 릴리스(Canary Release)라고 부릅니다.

소수의 사용자 그룹에게 새 버전을 노출시키고 어떻게 작동하는지 관찰하는 방법입니다.

만약 새 버전이 의도치 않게 동작한다면, 트래픽을 이전 버전으로 되돌려 릴리스를 롤백할 수 있습니다.


출처 - 이제원님 @IstioStudy

배포와 릴리스를 분리함으로써 새로운 변화를 어떤 사용자에게, 어떻게 보여줄지 세밀하게 제어할 수 있으며, 이를 통해 새 코드를 운영 환경에 도입하는 위험을 크게 줄일 수 있습니다.

이제 Istio가 시스템에 들어오는 요청을 기반으로 트래픽을 제어함으로써 릴리스 위험을 낮추는 데 어떻게 도움이 되는지 살펴보겠습니다.



👉 Step 02. Istio를 활용한 트래픽 라우팅

✅ Routing requests with Istio

2장에서는 Istio를 사용하여 카탈로그 서비스로의 트래픽을 제어했습니다.

특히 Istio VirtualService 리소스를 사용하여 트래픽을 라우팅하는 방법을 지정했습니다. 이제 어떻게 작동하는지 자세히 살펴보겠습니다.

Istio는 요청의 내용(헤더 등)을 평가하여 요청의 경로를 제어합니다. 이러한 방식으로 다크 런치(Dark Launch)라는 기술을 사용하여 특정 사용자에게만 새로운 배포를 제공할 수 있습니다.

다크 런치에서는 대부분의 사용자는 검증된 서비스 버전으로 보내지며, 특정 사용자 그룹만 최신 버전으로 보내집니다. 따라서 모든 사용자에게 영향을 주지 않고 통제된 방식으로 특정 그룹에게만 새로운 기능을 노출할 수 있습니다.


✅ Deploying v1 of the catalog service

먼저 카탈로그 서비스의 v1을 배포하고 Istio Gateway 및 VirtualService를 설정하여 외부에서 접근할 수 있도록 합니다.

# Let’s deploy v1 of our catalog service. From the root of the book’s source code, run the following command
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction

# 확인
kubectl get pod -n istioinaction -owide
NAME                     READY   STATUS    RESTARTS   AGE   IP           NODE                  NOMINATED NODE   READINESS GATES
catalog-6cf4b97d-ftl77   2/2     Running   0          42s   10.10.0.14   myk8s-control-plane   <none>           <none>

# 도메인 질의를 위한 임시 설정 : 실습 완료 후에는 삭제 해둘 것
echo "127.0.0.1       catalog.istioinaction.io" | sudo tee -a /etc/hosts
cat /etc/hosts | tail -n 3

# netshoot로 내부에서 catalog 접속 확인
kubectl exec -it netshoot -- curl -s http://catalog.istioinaction/items | jq

# 외부 노출을 위해 Gateway 설정
cat ch5/catalog-gateway.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: catalog-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "catalog.istioinaction.io"
    
kubectl apply -f ch5/catalog-gateway.yaml -n istioinaction

# 트래픽을 catalog 서비스로 라우팅하는 VirtualService 리소스 설정
cat ch5/catalog-vs.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog-vs-from-gw
spec:
  hosts:
  - "catalog.istioinaction.io"
  gateways:
  - catalog-gateway
  http:
  - route:
    - destination:
        host: catalog

kubectl apply -f ch5/catalog-vs.yaml -n istioinaction

# 확인
kubectl get gw,vs -n istioinaction

NAME                                          AGE
gateway.networking.istio.io/catalog-gateway   95s

NAME                                                    GATEWAYS              HOSTS                          AGE
virtualservice.networking.istio.io/catalog-vs-from-gw   ["catalog-gateway"]   ["catalog.istioinaction.io"]   3s


# 도메인 질의를 위한 임시 설정 : 실습 완료 후에는 삭제 해둘 것
echo "127.0.0.1       catalog.istioinaction.io" | sudo tee -a /etc/hosts
cat /etc/hosts | tail -n 3


# istio-ingressgateway Service(NodePort)에 포트 정보 확인
kubectl get svc -n istio-system istio-ingressgateway -o jsonpath="{.spec.ports}" | jq
[
  {
    "name": "status-port",
    "nodePort": 31674,
    "port": 15021,
    "protocol": "TCP",
    "targetPort": 15021
  },
  {
    "name": "http2",
    "nodePort": 30000, # 순서1
    "port": 80,        
    "protocol": "TCP",
    "targetPort": 8080 # 순서2
  },
  {
    "name": "https",
    "nodePort": 30005,
    "port": 443,
    "protocol": "TCP",
    "targetPort": 8443
  }
]

# 호스트에서 NodePort(Service)로 접속 확인
curl -v -H "Host: catalog.istioinaction.io" http://localhost:30000
kubectl stern -l app=catalog -n istioinaction

open http://localhost:30000
open http://catalog.istioinaction.io:30000
open http://catalog.istioinaction.io:30000/items


# 신규 터미널 : 반복 접속 실행 해두기
while true; do curl -s http://catalog.istioinaction.io:30000/items/ ; sleep 1; echo; done
while true; do curl -s http://catalog.istioinaction.io:30000/items/ -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done
while true; do curl -s http://catalog.istioinaction.io:30000/items/ -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 0.5; echo; done

#
docker exec -it myk8s-control-plane istioctl proxy-status
NAME                                                  CLUSTER        CDS        LDS        EDS        RDS        ECDS         ISTIOD                      VERSION
catalog-6cf4b97d-ftl77.istioinaction                  Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-fl492     1.17.8
istio-ingressgateway-996bc6bb6-zvtdc.istio-system     Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-fl492     1.17.8

# istio-ingressgateway
## LDS - Listener Discovery Service
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system --port 8080
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system --port 8080 -o json

## RDS - Route Discovery Service
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system --name http.8080 
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system --name http.8080 -o json

## CDS - Cluseter Discovery Service
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local -o json

## EDS - Endpoint Discovery Service
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80||catalog.istioinaction.svc.cluster.local'
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80||catalog.istioinaction.svc.cluster.local' -o json

# catalog
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/catalog.istioinaction

✅ Deploying v2 of the catalog service

이제 카탈로그 서비스의 v2를 배포합니다. v2 버전에는 imageUrl 필드가 추가되어 있습니다.

# catalog 서비스 v2 를 배포 : v2에서는 imageUrl 필드가 추가
kubectl apply -f services/catalog/kubernetes/catalog-deployment-v2.yaml -n istioinaction

#
kubectl get deploy -n istioinaction --show-labels
NAME         READY   UP-TO-DATE   AVAILABLE   AGE   LABELS
catalog      1/1     1            1           30m   app=catalog,version=v1
catalog-v2   1/1     1            1           34s   app=catalog,version=v2

kubectl get pod -n istioinaction -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP           NODE                  NOMINATED NODE   READINESS GATES
catalog-6cf4b97d-ftl77        2/2     Running   0          43m   10.10.0.14   myk8s-control-plane   <none>           <none>
catalog-v2-6df885b555-6hmcl   2/2     Running   0          13m   10.10.0.15   myk8s-control-plane   <none>           <none>

docker exec -it myk8s-control-plane istioctl proxy-status
NAME                                                  CLUSTER        CDS        LDS        EDS        RDS        ECDS         ISTIOD                      VERSION
catalog-6cf4b97d-ftl77.istioinaction                  Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-fl492     1.17.8
catalog-v2-6df885b555-6hmcl.istioinaction             Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-fl492     1.17.8
istio-ingressgateway-996bc6bb6-zvtdc.istio-system     Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-fl492     1.17.8


# 호출 테스트 : v1 , v2 호출 확인
for i in {1..10}; do curl -s http://catalog.istioinaction.io:30000/items/ ; printf "\n\n"; done


# istio-ingressgateway proxy-config 확인
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local -o json
...
        "name": "outbound|80||catalog.istioinaction.svc.cluster.local",
        "type": "EDS",
        "edsClusterConfig": {
            "edsConfig": {
                "ads": {},
                "initialFetchTimeout": "0s",
                "resourceApiVersion": "V3"
            },
            "serviceName": "outbound|80||catalog.istioinaction.svc.cluster.local"
        },
        "connectTimeout": "10s",
        "lbPolicy": "LEAST_REQUEST",
        "circuitBreakers": {
            "thresholds": [
                {
                    "maxConnections": 4294967295,
                    "maxPendingRequests": 4294967295,
                    "maxRequests": 4294967295,
                    "maxRetries": 4294967295,
                    "trackRemaining": true
                }
            ]
        },
        "commonLbConfig": {
            "localityWeightedLbConfig": {}
        },
...

docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80||catalog.istioinaction.svc.cluster.local'
ENDPOINT            STATUS      OUTLIER CHECK     CLUSTER
10.10.0.16:3000     HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local
10.10.0.17:3000     HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local

docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80||catalog.istioinaction.svc.cluster.local' -o json
...
   {
        "name": "outbound|80||catalog.istioinaction.svc.cluster.local",
        "addedViaApi": true,
        "hostStatuses": [
            {
                "address": {
                    "socketAddress": {
                        "address": "10.10.0.14",
                        "portValue": 3000
                    }
                },
                "stats": [
                    {
                        "name": "cx_connect_fail"
                    },
                    {
                        "value": "8",
                        "name": "cx_total"
                    },
                    {
                        "name": "rq_error"
                    },
                    {
                        "value": "315",
                        "name": "rq_success"
                    },
                    {
                        "name": "rq_timeout"
                    },
                    {
                        "value": "315",
                        "name": "rq_total"
                    },
                    {
                        "type": "GAUGE",
                        "value": "8",
                        "name": "cx_active"
                    },
                    {
                        "type": "GAUGE",
                        "name": "rq_active"
                    }
                ],
                "healthStatus": {
                    "edsHealthStatus": "HEALTHY"
                },
                "weight": 1,
                "locality": {}
            },
            {
                "address": {
                    "socketAddress": {
                        "address": "10.10.0.15",
                        "portValue": 3000
                    }
                },
                "stats": [
                    {
                        "name": "cx_connect_fail"
                    },
                    {
                        "value": "8",
                        "name": "cx_total"
                    },
                    {
                        "name": "rq_error"
                    },
                    {
                        "value": "308",
                        "name": "rq_success"
                    },
                    {
                        "name": "rq_timeout"
                    },
                    {
                        "value": "308",
                        "name": "rq_total"
                    },
                    {
                        "type": "GAUGE",
                        "value": "8",
                        "name": "cx_active"
                    },
                    {
                        "type": "GAUGE",
                        "name": "rq_active"
                    }
                ],
                "healthStatus": {
                    "edsHealthStatus": "HEALTHY"
                },
                "weight": 1,
                "locality": {}
            }
        ],
        "circuitBreakers": {
            "thresholds": [
                {
                    "maxConnections": 4294967295,
                    "maxPendingRequests": 4294967295,
                    "maxRequests": 4294967295,
                    "maxRetries": 4294967295
                },
                {
                    "priority": "HIGH",
                    "maxConnections": 1024,
                    "maxPendingRequests": 1024,
                    "maxRequests": 1024,
                    "maxRetries": 3
                }
            ]
        },
        "observabilityName": "outbound|80||catalog.istioinaction.svc.cluster.local",
        "edsServiceName": "outbound|80||catalog.istioinaction.svc.cluster.local"
    },
...

# catalog proxy-config 도 직접 확인해보자


✅ Routing all traffic to v1 of the catalog service

Istio를 사용하여 모든 트래픽을 catalog v1 버전으로만 라우팅하도록 설정하겠습니다.

이를 위해 먼저 DestinationRule을 생성하여 서비스의 여러 버전(subset)을 정의합니다.

#
kubectl get pod -l app=catalog -n istioinaction --show-labels
NAME                          READY   STATUS    RESTARTS   AGE   LABELS
catalog-6cf4b97d-ftl77        2/2     Running   0          56m   app=catalog,...,version=v1
catalog-v2-6df885b555-6hmcl   2/2     Running   0          26m   app=catalog,...,version=v2

#
cat ch5/catalog-dest-rule.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: catalog
spec:
  host: catalog.istioinaction.svc.cluster.local
  subsets:
  - name: version-v1
    labels:
      version: v1
  - name: version-v2
    labels:
      version: v2

kubectl apply -f ch5/catalog-dest-rule.yaml -n istioinaction

# 확인
kubectl get destinationrule -n istioinaction
NAME      HOST                                      AGE
catalog   catalog.istioinaction.svc.cluster.local   8s


# catalog proxy-config 확인 : SUBSET(v1, v2, -) 확인
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local
SERVICE FQDN                                PORT     SUBSET         DIRECTION     TYPE     DESTINATION RULE
catalog.istioinaction.svc.cluster.local     80       -              outbound      EDS      catalog.istioinaction
catalog.istioinaction.svc.cluster.local     80       version-v1     outbound      EDS      catalog.istioinaction
catalog.istioinaction.svc.cluster.local     80       version-v2     outbound      EDS      catalog.istioinaction

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --subset version-v1 -o json
...
        "name": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
        "type": "EDS",
        "edsClusterConfig": {
            "edsConfig": {
                "ads": {},
                "initialFetchTimeout": "0s",
                "resourceApiVersion": "V3"
            },
            "serviceName": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local"
        },
        "connectTimeout": "10s",
        "lbPolicy": "LEAST_REQUEST",
...

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --subset version-v2 -o json
...
        "name": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local",
        "type": "EDS",
        "edsClusterConfig": {
            "edsConfig": {
                "ads": {},
                "initialFetchTimeout": "0s",
                "resourceApiVersion": "V3"
            },
            "serviceName": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local"
        },
        "connectTimeout": "10s",
        "lbPolicy": "LEAST_REQUEST",
...

#
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local -o json
...
        "name": "outbound|80||catalog.istioinaction.svc.cluster.local",
        "type": "EDS",
        "edsClusterConfig": {
            "edsConfig": {
                "ads": {},
                "initialFetchTimeout": "0s",
                "resourceApiVersion": "V3"
            },
            "serviceName": "outbound|80||catalog.istioinaction.svc.cluster.local"
        },
...

# 
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system | egrep 'ENDPOINT|istioinaction'
ENDPOINT                                                STATUS      OUTLIER CHECK     CLUSTER
10.10.0.16:3000                                         HEALTHY     OK                outbound|80|version-v1|catalog.istioinaction.svc.cluster.local
10.10.0.16:3000                                         HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local
10.10.0.17:3000                                         HEALTHY     OK                outbound|80|version-v2|catalog.istioinaction.svc.cluster.local
10.10.0.17:3000                                         HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local

docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80|version-v1|catalog.istioinaction.svc.cluster.local'
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80|version-v1|catalog.istioinaction.svc.cluster.local' -o json
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80|version-v2|catalog.istioinaction.svc.cluster.local'
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80|version-v2|catalog.istioinaction.svc.cluster.local' -o json
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80||catalog.istioinaction.svc.cluster.local'
ENDPOINT            STATUS      OUTLIER CHECK     CLUSTER
10.10.0.16:3000     HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local
10.10.0.17:3000     HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local

# VirtualService 수정 (subset 추가)
cat ch5/catalog-vs-v1.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog-vs-from-gw
spec:
  hosts:
  - "catalog.istioinaction.io"
  gateways:
  - catalog-gateway
  http:
  - route:
    - destination:
        host: catalog
        subset: version-v1

kubectl apply -f ch5/catalog-vs-v1.yaml -n istioinaction

# 호출 테스트 : v1
for i in {1..10}; do curl -s http://catalog.istioinaction.io:30000/items/ ; printf "\n\n"; done


# 세부 정보 확인
# routes 에 virtualHosts 항목에 routes.route 에 cluster 부분이 ...version-v1... 설정 확인
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system --name http.8080 -o json
...
        "virtualHosts": [
            {
                "name": "catalog.istioinaction.io:80",
                "domains": [
                    "catalog.istioinaction.io"
                ],
                "routes": [
                    {
                        "match": {
                            "prefix": "/"
                        },
                        "route": {
                            "cluster": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
                            "timeout": "0s",
                            "retryPolicy": {
                                "retryOn": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes",
                                "numRetries": 2,
...

# cluster 
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --subset version-v1
SERVICE FQDN                                PORT     SUBSET         DIRECTION     TYPE     DESTINATION RULE
catalog.istioinaction.svc.cluster.local     80       version-v1     outbound      EDS      catalog.istioinaction

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --subset version-v1 -o json
...
        "name": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
        "type": "EDS",
        "edsClusterConfig": {
            "edsConfig": {
                "ads": {},
                "initialFetchTimeout": "0s",
                "resourceApiVersion": "V3"
            },
            "serviceName": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local"
        },
        ...
        "metadata": {
            "filterMetadata": {
                "istio": {
                    "config": "/apis/networking.istio.io/v1alpha3/namespaces/istioinaction/destination-rule/catalog",
                    "default_original_port": 80,
                    "services": [
                        {
                            "host": "catalog.istioinaction.svc.cluster.local",
                            "name": "catalog",
                            "namespace": "istioinaction"
                        }
                    ],
                    "subset": "version-v1"
...

# endpoint 
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system | egrep 'ENDPOINT|istioinaction'
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80|version-v1|catalog.istioinaction.svc.cluster.local'
ENDPOINT            STATUS      OUTLIER CHECK     CLUSTER
10.10.0.16:3000     HEALTHY     OK                outbound|80|version-v1|catalog.istioinaction.svc.cluster.local

docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80|version-v1|catalog.istioinaction.svc.cluster.local' -o json
...

# istio-proxy (catalog)
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/catalog.istioinaction --subset version-v1 -o json
...
        "metadata": {
            "filterMetadata": {
                "istio": {
                    "config": "/apis/networking.istio.io/v1alpha3/namespaces/istioinaction/destination-rule/catalog",
                    "default_original_port": 80,
                    "services": [
                        {
                            "host": "catalog.istioinaction.svc.cluster.local",
                            "name": "catalog",
                            "namespace": "istioinaction"
                        }
                    ],
                    "subset": "version-v1"
                }
            }
        },
...

DestinationRule은 catalog 서비스의 두 가지 버전을 version-v1version-v2라는 서브셋으로 정의합니다.

VirtualService는 모든 트래픽을 version-v1 서브셋으로 라우팅합니다.

이제 테스트하면 모든 요청이 catalog v1으로만 라우팅되는 것을 확인할 수 있습니다.


✅ Routing specific requests to v2

특정 HTTP 헤더를 가진 요청을 v2로 라우팅하도록 설정해 보겠습니다. 이는 다크 런치 패턴을 구현하는 방법입니다.


출처 - 이제원님 @IstioStudy

#
cat ch5/catalog-vs-v2-request.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog-vs-from-gw
spec:
  hosts:
  - "catalog.istioinaction.io"
  gateways:
  - catalog-gateway
  http:
  - match:
    - headers:
        x-istio-cohort:
          exact: "internal"
    route:
    - destination:
        host: catalog
        subset: version-v2
  - route:
    - destination:
        host: catalog
        subset: version-v1

kubectl apply -f ch5/catalog-vs-v2-request.yaml -n istioinaction

# 호출 테스트 : 여전히 v1
for i in {1..10}; do curl -s http://catalog.istioinaction.io:30000/items/ ; printf "\n\n"; done

# 요청 헤더 포함 호출 테스트 : v2!
curl http://catalog.istioinaction.io:30000/items -H "x-istio-cohort: internal"

# (옵션) 신규 터미널 : v2 반복 접속
while true; do curl -s http://catalog.istioinaction.io:30000/items/ -H "x-istio-cohort: internal" -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 2; echo; done


# 상세 확인
# route 추가 : routes 에 2개의 route 확인 - 위에서 부터 적용되는 순서 중요!
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system --name http.8080
NAME          DOMAINS                      MATCH     VIRTUAL SERVICE
http.8080     catalog.istioinaction.io     /*        catalog-vs-from-gw.istioinaction
http.8080     catalog.istioinaction.io     /*        catalog-vs-from-gw.istioinaction

docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system --name http.8080 -o json
...
        "virtualHosts": [
            {
                "name": "catalog.istioinaction.io:80",
                "domains": [
                    "catalog.istioinaction.io"
                ],
                "routes": [
                    {
                        "match": {
                            "prefix": "/",
                            "caseSensitive": true,
                            "headers": [
                                {
                                    "name": "x-istio-cohort",
                                    "stringMatch": {
                                        "exact": "internal"
                                    }
                                }
                            ]
                        },
                        "route": {
                            "cluster": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local",
                            "timeout": "0s",
                ...
                   {
                        "match": {
                            "prefix": "/"
                        },
                        "route": {
                            "cluster": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
...

#
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system | egrep 'ENDPOINT|istioinaction'

# istio-proxy (catalog)에는 routes 정보가 아래 cluster 로 보내는 1개만 있다. 즉 istio-proxy(istio-ingressgateway)가 routes 분기 처리하는 것을 알 수 있다.
## "cluster": "outbound|80||catalog.istioinaction.svc.cluster.local"
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction --name 80 -o json
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction | grep catalog
80                                                            catalog, catalog.istioinaction + 1 more...          /*


✅ Routing deep within a call graph

지금까지는 외부에서 들어오는 트래픽에 대한 라우팅 규칙을 설정했습니다.

이제 서비스 메시 내부에서의 서비스 간 통신에도 동일한 트래픽 제어를 적용해 보겠습니다.


출처 - 이제원님 @IstioStudy

# 초기화
kubectl delete gateway,virtualservice,destinationrule --all -n istioinaction

# webapp 기동
kubectl apply -n istioinaction -f services/webapp/kubernetes/webapp.yaml
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction # 이미 배포 상태
kubectl apply -f services/catalog/kubernetes/catalog-deployment-v2.yaml -n istioinaction # 이미 배포 상태

# 확인
kubectl get deploy,pod,svc,ep -n istioinaction
NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/catalog      1/1     1            1           55m
deployment.apps/catalog-v2   1/1     1            1           48m
deployment.apps/webapp       1/1     1            1           42s

NAME                              READY   STATUS    RESTARTS   AGE
pod/catalog-6cf4b97d-jxpb8        2/2     Running   0          55m
pod/catalog-v2-6df885b555-rg9f5   2/2     Running   0          48m
pod/webapp-7685bcb84-2q7rg        2/2     Running   0          42s

NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/catalog   ClusterIP   10.200.1.254   <none>        80/TCP    55m
service/webapp    ClusterIP   10.200.1.61    <none>        80/TCP    42s

NAME                ENDPOINTS                         AGE
endpoints/catalog   10.10.0.16:3000,10.10.0.17:3000   55m
endpoints/webapp    10.10.0.18:8080                   42s

먼저 webapp 서비스를 배포하고 외부에서 접근할 수 있도록 Gateway와 VirtualService를 설정합니다:

# Now, set up the Istio ingress gateway to route to the webapp service
cat services/webapp/istio/webapp-catalog-gw-vs.yaml
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: coolstore-gateway
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "webapp.istioinaction.io"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: webapp-virtualservice
spec:
  hosts:
  - "webapp.istioinaction.io"
  gateways:
  - coolstore-gateway
  http:
  - route:
    - destination:
        host: webapp
        port:
          number: 80

kubectl apply -f services/webapp/istio/webapp-catalog-gw-vs.yaml -n istioinaction

# 확인
kubectl get gw,vs -n istioinaction
NAME                                            AGE
gateway.networking.istio.io/coolstore-gateway   3s

NAME                                                       GATEWAYS                HOSTS                         AGE
virtualservice.networking.istio.io/webapp-virtualservice   ["coolstore-gateway"]   ["webapp.istioinaction.io"]   3s


# 도메인 질의를 위한 임시 설정 : 실습 완료 후에는 삭제 해둘 것
echo "127.0.0.1       webapp.istioinaction.io" | sudo tee -a /etc/hosts
cat /etc/hosts | tail -n 3


# 호출테스트 : 외부(web, curl) → ingressgw → webapp → catalog (v1, v2)
curl -s http://webapp.istioinaction.io:30000/api/catalog | jq

# 반복 호출테스트 : 신규터미널 2개에 아래 각각 실행 해두기
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -H "x-istio-cohort: internal" -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 2; echo; done


# proxy-config : istio-ingressgateway
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system --name http.8080 
NAME          DOMAINS                     MATCH     VIRTUAL SERVICE
http.8080     webapp.istioinaction.io     /*        webapp-virtualservice.istioinaction
=> route."cluster": "outbound|80||webapp.istioinaction.svc.cluster.local"

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system | egrep 'webapp|catalog'
catalog.istioinaction.svc.cluster.local                 80        -          outbound      EDS            
webapp.istioinaction.svc.cluster.local                  80        -          outbound      EDS

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn webapp.istioinaction.svc.cluster.local -o json
...
        "name": "outbound|80||webapp.istioinaction.svc.cluster.local",
        "type": "EDS",
...

docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80||webapp.istioinaction.svc.cluster.local'
ENDPOINT            STATUS      OUTLIER CHECK     CLUSTER
10.10.0.18:8080     HEALTHY     OK                outbound|80||webapp.istioinaction.svc.cluster.local


# proxy-config : webapp
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/webapp.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/webapp.istioinaction

# proxy-config : catalog
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/catalog.istioinaction


# webapp istio-proxy 로그 활성화
# 신규 터미널
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f

# webapp istio-proxy 로그 활성화 적용
cat << EOF | kubectl apply -f -
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
  name: webapp
  namespace: istioinaction
spec:
  selector:
    matchLabels:
      app: webapp
  accessLogging:
  - providers:
    - name: envoy #2 액세스 로그를 위한 프로바이더 설정
    disabled: false #3 disable 를 false 로 설정해 활성화한다
EOF

# webapp → catalog 는 k8s service(clusterIP) 라우팅 사용 확인!
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
[2025-04-18T13:27:57.178Z] "HEAD /api/catalog HTTP/1.1" 200 - via_upstream - "-" 0 0 8 8 "172.18.0.1" "curl/8.7.1" "8d425652-17a9-4b41-a21c-874acab3b1f4" "webapp.istioinaction.io:30000" "10.10.0.18:8080" inbound|8080|| 127.0.0.6:51809 10.10.0.18:8080 172.18.0.1:0 outbound_.80_._.webapp.istioinaction.svc.cluster.local default
=> 이 로그는 webapp 서비스의 사이드카 프록시가 클라이언트로부터 직접 HTTP 요청을 받은 장면이고, 이 요청을 10.10.0.18:8080 (즉, webapp 서비스의 실제 컨테이너)으로 보냄을 의미
[2025-04-18T13:27:58.237Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 2 2 "172.18.0.1" "beegoServer" "49b55b86-2505-4a5c-aadf-950d03705b87" "catalog.istioinaction:80" "10.10.0.16:3000" outbound|80||catalog.istioinaction.svc.cluster.local 10.10.0.18:45152 10.200.1.254:80 172.18.0.1:0 - default
=>  이 로그는 webapp 서비스가 catalog 서비스로 HTTP 요청을 보낸 상황이에요. Envoy는 catalog.istioinaction이라는 Kubernetes catalog 서비스(clusterIp 10.200.1.254:80)로 라우팅하고, 실제 Pod IP는 10.10.0.16:3000으로 연결되었어요.
...

이제 서비스 메시 내부에서 webapp이 catalog를 호출할 때의 트래픽도 제어해 보겠습니다.

이를 위해 mesh 게이트웨이를 사용한 VirtualService를 생성합니다:

#
ch5/catalog-dest-rule.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: catalog
spec:
  host: catalog.istioinaction.svc.cluster.local
  subsets:
  - name: version-v1
    labels:
      version: v1
  - name: version-v2
    labels:
      version: v2
      
kubectl apply -f ch5/catalog-dest-rule.yaml -n istioinaction

# istio-proxy
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local
SERVICE FQDN                                PORT     SUBSET         DIRECTION     TYPE     DESTINATION RULE
catalog.istioinaction.svc.cluster.local     80       -              outbound      EDS      catalog.istioinaction
catalog.istioinaction.svc.cluster.local     80       version-v1     outbound      EDS      catalog.istioinaction
catalog.istioinaction.svc.cluster.local     80       version-v2     outbound      EDS      catalog.istioinaction

docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system | egrep 'ENDPOINT|istioinaction'
ENDPOINT                                                STATUS      OUTLIER CHECK     CLUSTER
10.10.0.16:3000                                         HEALTHY     OK                outbound|80|version-v1|catalog.istioinaction.svc.cluster.local
10.10.0.16:3000                                         HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local
10.10.0.17:3000                                         HEALTHY     OK                outbound|80|version-v2|catalog.istioinaction.svc.cluster.local
10.10.0.17:3000                                         HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local
10.10.0.18:8080                                         HEALTHY     OK                outbound|80||webapp.istioinaction.svc.cluster.local

#
cat ch5/catalog-vs-v1-mesh.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog
spec:
  hosts:
  - catalog
  gateways: # 만약, gateways 부분을 제외하고 배포하면 암묵적으로 mesh gateways가 적용됨.
    - mesh  # VirtualService는 메시 내의 모든 사이드카(현재 webapp, catalog)에 적용된다. edge는 제외.
  http:
  - route:
    - destination:
        host: catalog
        subset: version-v1

kubectl apply -f ch5/catalog-vs-v1-mesh.yaml -n istioinaction

# VirtualService 확인 : GATEWAYS 에 mesh 확인
kubectl get vs -n istioinaction
NAME                    GATEWAYS                HOSTS                         AGE
catalog                 ["mesh"]                ["catalog"]                   12s
webapp-virtualservice   ["coolstore-gateway"]   ["webapp.istioinaction.io"]   28s


# 반복 호출테스트 : 신규터미널 2개에 아래 각각 실행 해두기 >> 현재는 v1만 라우팅 처리
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -H "x-istio-cohort: internal" -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 2; echo; done


# webapp → catalog 호출도 istio 의 DestinationRule 라우팅 전달 처리! : 신규터미널
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
[2025-04-18T13:52:54.772Z] "GET /items HTTP/1.1" 200 - via_upstream - "-" 0 502 2 2 "172.18.0.1" "beegoServer" "2035962f-144f-4d07-9102-4e3ab7ea3484" "catalog.istioinaction:80" "10.10.0.16:3000" outbound|80|version-v1|catalog.istioinaction.svc.cluster.local 10.10.0.18:52458 10.200.1.254:80 172.18.0.1:0 - -
=> 이 로그는 webapp이 내부적으로 catalog 서비스를 호출하는 로그이고, version-v1이라는 **서브셋(subset)**으로 요청이 라우팅되었어요. 이는 DestinationRule에서 subset: version-v1으로 정의된 엔드포인트로 라우팅이 잘 되었다는 뜻이에요. 


# proxy-config (webapp) : 기존에 webapp 에서 catalog 로 VirtualService 정보는 없었는데, 추가됨을 확인
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction | egrep 'NAME|catalog'
NAME                                                          DOMAINS                                             MATCH                  VIRTUAL SERVICE
80                                                            catalog, catalog.istioinaction + 1 more...          /*                     catalog.istioinaction

docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 -o json > webapp-routes.json
cat webapp-routes.json | jq
...
    "virtualHosts": [
      {
        "name": "catalog.istioinaction.svc.cluster.local:80",
        "domains": [
          "catalog.istioinaction.svc.cluster.local",
          "catalog",
          "catalog.istioinaction.svc",
          "catalog.istioinaction",
          "10.200.1.254" # 해당 IP는 catalog service(clusterIP)
        ],
        "routes": [
          {
            "match": {
              "prefix": "/"
            },
            "route": {
              "cluster": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
              "timeout": "0s",
...


docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction --fqdn catalog.istioinaction.svc.cluster.local
SERVICE FQDN                                PORT     SUBSET         DIRECTION     TYPE     DESTINATION RULE
catalog.istioinaction.svc.cluster.local     80       -              outbound      EDS      catalog.istioinaction
catalog.istioinaction.svc.cluster.local     80       version-v1     outbound      EDS      catalog.istioinaction
catalog.istioinaction.svc.cluster.local     80       version-v2     outbound      EDS      catalog.istioinaction

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction --subset version-v1 -o json
...
        "name": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
        "type": "EDS",
...

docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/webapp.istioinaction | egrep 'ENDPOINT|catalog'
ENDPOINT                                                STATUS      OUTLIER CHECK     CLUSTER
10.10.0.16:3000                                         HEALTHY     OK                outbound|80|version-v1|catalog.istioinaction.svc.cluster.local
10.10.0.16:3000                                         HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local
10.10.0.17:3000                                         HEALTHY     OK                outbound|80|version-v2|catalog.istioinaction.svc.cluster.local
10.10.0.17:3000                                         HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local

# proxy-config (catalog) : gateway.mesh 이므로, 메시 내에 모든 사이드카에 VirtualService 적용됨을 확인. 아래 routes 부분
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction | egrep 'NAME|catalog'
NAME                                                          DOMAINS                                             MATCH                  VIRTUAL SERVICE
80                                                            catalog, catalog.istioinaction + 1 more...          /*                     catalog.istioinaction

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/catalog.istioinaction

이제 서비스 메시 내에서 webapp이 catalog를 호출할 때, x-istio-cohort: internal 헤더가 있으면 catalog v2로, 없으면 catalog v1으로 라우팅됩니다.

#
cat ch5/catalog-vs-v2-request-mesh.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog
spec:
  hosts:
  - catalog
  gateways:
    - mesh
  http:
  - match:
    - headers:
        x-istio-cohort:
          exact: "internal"
    route:
    - destination:
        host: catalog
        subset: version-v2
  - route:
    - destination:
        host: catalog
        subset: version-v1

kubectl apply -f ch5/catalog-vs-v2-request-mesh.yaml -n istioinaction


# 반복 호출테스트 : 신규터미널 2개에 아래 각각 실행 >> v1, v2 각기 라우팅 확인
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -H "x-istio-cohort: internal" -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 2; echo; done


# proxy-config (webapp) : 기존에 webapp 에서 catalog 로 VirtualService 추가된 2개 항목 확인
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction | egrep 'NAME|catalog'
NAME                                                          DOMAINS                                             MATCH                  VIRTUAL SERVICE
80                                                            catalog, catalog.istioinaction + 1 more...          /*                     catalog.istioinaction
80                                                            catalog, catalog.istioinaction + 1 more...          /*                     catalog.istioinaction

docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 -o json > webapp-routes.json
cat webapp-routes.json | jq
...
        "virtualHosts": [
            {
                "name": "catalog.istioinaction.svc.cluster.local:80",
                "domains": [
                    "catalog.istioinaction.svc.cluster.local",
                    "catalog",
                    "catalog.istioinaction.svc",
                    "catalog.istioinaction",
                    "10.200.1.254"
                ],
                "routes": [
                    {
                        "match": {
                            "prefix": "/",
                            "caseSensitive": true,
                            "headers": [
                                {
                                    "name": "x-istio-cohort",
                                    "stringMatch": {
                                        "exact": "internal"
                                    }
                                }
                            ]
                        },
                        "route": {
                            "cluster": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local",
                     ....
                    {
                        "match": {
                            "prefix": "/"
                        },
                        "route": {
                            "cluster": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
...


docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction --fqdn catalog.istioinaction.svc.cluster.local
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/webapp.istioinaction | egrep 'ENDPOINT|catalog'

# proxy-config (catalog) : mesh 이므로 VS가 아래 routes(catalog)에도 적용됨
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/catalog.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/catalog.istioinaction



👉 Step 03. 트래픽 분산 및 카나리 릴리스 자동화

✅ Traffic shifting

가중치 기반으로 트래픽을 점진적으로 이동시키는 카나리 릴리스(canary release) 방법에 대해 알아보겠습니다.

가중치 기반 트래픽 분산은 전체 트래픽 중 일정 비율을 새로운 버전으로 보내는 방법입니다.

예를 들어 catalog 서비스 v2로 10%의 트래픽을 보내고, 나머지 90%는 v1으로 유지할 수 있습니다.

이를 통해 새 버전의 영향 범위를 제한하면서 점진적으로 릴리스할 수 있어 위험을 최소화할 수 있습니다.

다크 런치와 마찬가지로 서비스 상태를 지속적으로 모니터링하면서 문제가 발생하면 트래픽 가중치를 조정하여 쉽게 롤백할 수 있습니다.

먼저 현재 실행 중인 서비스들을 확인하고, 모든 트래픽을 v1으로 설정합니다:

# 이전 절부터 다음 서비스가 실행 중이다 : 파드에 labels 에 버전 정보 없을 경우 latest 설정되며, kiali 에 Workloads Details 에 'Missing Version' 표기됨
kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction # 이미 배포 상태
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction # 이미 배포 상태
kubectl apply -f services/catalog/kubernetes/catalog-deployment-v2.yaml -n istioinaction # 이미 배포 상태
kubectl get deploy,rs,pod -n istioinaction --show-labels
NAME                          READY   STATUS    RESTARTS   AGE     LABELS
catalog-6cf4b97d-q4xv5        2/2     Running   0          19m     app=catalog,...,service.istio.io/canonical-revision=v1,version=v1
catalog-v2-6df885b555-df7g4   2/2     Running   0          19m     app=catalog,...,service.istio.io/canonical-revision=v2,version=v2
webapp-7685bcb84-skzgg        2/2     Running   0          2m48s   app=webapp,...,service.istio.io/canonical-revision=latest

# 반복 호출테스트 : 신규터미널
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done

# 모든 트래픽을 catalog service v1 으로 재설정하자
cat ch5/catalog-vs-v1-mesh.yaml
...
  http:
  - route:
    - destination:
        host: catalog
        subset: version-v1

kubectl apply -f ch5/catalog-vs-v1-mesh.yaml -n istioinaction

# 호출테스트
curl -s http://webapp.istioinaction.io:30000/api/catalog | jq

이제 catalog 서비스 트래픽의 10%를 v2로 보내 보겠습니다:

#
cat ch5/catalog-vs-v2-10-90-mesh.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog
spec:
  hosts:
  - catalog
  gateways:
  - mesh
  http:
  - route:
    - destination:
        host: catalog
        subset: version-v1
      weight: 90
    - destination:
        host: catalog
        subset: version-v2
      weight: 10 
kubectl apply -f ch5/catalog-vs-v2-10-90-mesh.yaml -n istioinaction

#
kubectl get vs -n istioinaction catalog
NAME      GATEWAYS   HOSTS         AGE
catalog   ["mesh"]   ["catalog"]   112s

# 호출 테스트 : v2 호출 비중 확인
for i in {1..10};  do curl -s http://webapp.istioinaction.io:30000/api/catalog | grep -i imageUrl ; done | wc -l
for i in {1..100}; do curl -s http://webapp.istioinaction.io:30000/api/catalog | grep -i imageUrl ; done | wc -l

# proxy-config(webapp) : mesh 이므로 메시 내 모든 사이드카에 VS 적용
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 -o json
...
                        "route": {
                            "weightedClusters": {
                                "clusters": [
                                    {
                                        "name": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
                                        "weight": 90
                                    },
                                    {
                                        "name": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local",
                                        "weight": 10
                                    }
                                ],
                                "totalWeight": 100
...

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction --fqdn catalog.istioinaction.svc.cluster.local
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/webapp.istioinaction | grep catalog


# proxy-config(catalog) : mesh 이므로 메시 내 모든 사이드카에 VS 적용
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction --name 80 -o json
...

이번에는 트래픽을 50:50으로 분산시켜 보겠습니다:

#
cat ch5/catalog-vs-v2-50-50-mesh.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog
spec:
  hosts:
  - catalog
  gateways:
  - mesh
  http:
  - route:
    - destination:
        host: catalog
        subset: version-v1
      weight: 50
    - destination:
        host: catalog
        subset: version-v2
      weight: 50
kubectl apply -f ch5/catalog-vs-v2-50-50-mesh.yaml -n istioinaction

# 호출 테스트 : v2 호출 비중 확인
for i in {1..10};  do curl -s http://webapp.istioinaction.io:30000/api/catalog | grep -i imageUrl ; done | wc -l
for i in {1..100}; do curl -s http://webapp.istioinaction.io:30000/api/catalog | grep -i imageUrl ; done | wc -l

# proxy-config(webapp) 
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 -o json
...
                      "route": {
                            "weightedClusters": {
                                "clusters": [
                                    {
                                        "name": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
                                        "weight": 50
                                    },
                                    {
                                        "name": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local",
                                        "weight": 50
                                    }
                                ],
                                "totalWeight": 100
...


✅ Flagger를 활용한 카나리 릴리스 자동화

지금까지는 트래픽 분산을 수동으로 수행했습니다. 그러나 실제 운영 환경에서는 수백 개의 서비스를 관리해야 하므로, 이러한 작업을 자동화하는 것이 바람직합니다.

Flagger는 이런 카나리 릴리스 프로세스를 자동화해주는 도구입니다.


출처 - https://github.com/stefanprodan/gitops-istio

Flagger는 다음과 같은 기능을 제공합니다:

  • 릴리스 진행 방식 정의
  • 성공/실패 판단을 위한 메트릭 지정
  • 자동 롤백 조건 설정
  • 프로메테우스와 연동하여 서비스 상태 모니터링

먼저 기존 리소스를 정리합니다.

# catalog-v2 와 트래픽 라우팅을 명시적으로 제어하는 VirtualService를 제거
kubectl delete virtualservice catalog -n istioinaction
kubectl delete deploy catalog-v2 -n istioinaction
kubectl delete service catalog -n istioinaction
kubectl delete destinationrule catalog -n istioinaction

# 남은 리소스 확인
kubectl get deploy,svc,ep -n istioinaction
NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/catalog   1/1     1            1           77m
deployment.apps/webapp    1/1     1            1           78m

NAME             TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
service/webapp   ClusterIP   10.200.1.73   <none>        80/TCP    78m

NAME               ENDPOINTS         AGE
endpoints/webapp   10.10.0.19:8080   78m

kubectl get gw,vs -n istioinaction
NAME                                            AGE
gateway.networking.istio.io/coolstore-gateway   73m

NAME                                                       GATEWAYS                HOSTS                         AGE
virtualservice.networking.istio.io/webapp-virtualservice   ["coolstore-gateway"]   ["webapp.istioinaction.io"]   73m        

Flagger를 설치합니다:

# CRD 설치 
kubectl apply -f https://raw.githubusercontent.com/fluxcd/flagger/main/artifacts/flagger/crd.yaml
kubectl get crd | grep flagger
alertproviders.flagger.app                 2025-04-19T03:11:50Z
canaries.flagger.app                       2025-04-19T03:11:50Z
metrictemplates.flagger.app                2025-04-19T03:11:50Z

# Helm 설치
helm repo add flagger https://flagger.app
helm install flagger flagger/flagger \
  --namespace=istio-system \
  --set crd.create=false \
  --set meshProvider=istio \
  --set metricServer=http://prometheus:9090

# 디플로이먼트 flagger 에 의해 배포된 파드 확인
kubectl get pod -n istio-system -l app.kubernetes.io/name=flagger
NAME                       READY   STATUS    RESTARTS   AGE
flagger-6d4ffc5576-q78ls   1/1     Running   0          2m54s

# 시크릿
kubectl get secret -n istio-system | grep flagger-token
flagger-token-v2f5z                                kubernetes.io/service-account-token   3      4m11s

# 시크릿 확인 : ca.crt 는 k8s 루프 인증서
kubectl view-secret -n istio-system flagger-token-v2f5z --all
ca.crt='-----BEGIN CERTIFICATE-----
MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTI1MDQxOTAxNDEyMVoXDTM1MDQxNzAxNDEyMVowFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMzN
onEbSSXBHfHhJICwREU4EX4D0K2Bwg7SXNwZNl3QwwPOpjFoaRbr6Hdog88jmo8A
Mo/RDKDj+Lvr0FE3hBvm5igLndWgnjYqpIHfDq31AYvWCoJvbBQ/omDIal4u1HHI
8XNqEpxl3JhsV9M9pMEx2+Gvlp1og8qjbB3B5amutriNQom6VOG0HBzJQuvNG8op
2GhWD4IOQf3vfKltGE9Y/KzbBLajtPueMkHZr/kH4Qy/Xu9kSGc8lhdsxrRSqoTX
ytyr2rOe83vliKhGKYtkiWESIm35BcVF1rp+jl0nLGs8IMhmR5Ll9A9pZ5xsqFzN
ndg7DjpdcKKwCxzw9BsCAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
/wQFMAMBAf8wHQYDVR0OBBYEFPm+n2+NblRv8ZWaoVW4fMvmFzuNMBUGA1UdEQQO
MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBAFkDbnnWS+9u9AKnlih0
Cltedk01oId5I31SWzvfI1owgudBH6pGxs3dGeij5iNDlV2StDzG5mqUIW6Y5iXR
hVMUUl/GvscaoSqFb5cIEgfmzdDSsNm1eBON8HpoN4VuEyuRZn1O39JAYIzVQcwD
LgO/dTCZwqM6hs6LC2Qx/PlxlQLt3agT3sZWXkztbOjLpLCuqVrz0NIRzFS3M2hA
b1+ACPllYGIRiEpSNxzbybgjui4W8bt8W2AjTPuqXIB/TuEcQrgAzrVcQsVf2XID
nC+WEpmUURKxQ51dLpLLQgFfojz+LXrdrlGJ4hpcfj0aN5j221+rjHTnI6PrsQdT
qME=
-----END CERTIFICATE-----
'
namespace='istio-system'
token='eyJhbGciOiJSUzI1NiIsImtpZCI6InFIUnV5blBfSUVDaDQ0MUxyOXR2MFRqV1g5ekVjaU1wdWZvSDFXZXl6Z3cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJpc3Rpby1zeXN0ZW0iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiZmxhZ2dlci10b2tlbi12MmY1eiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJmbGFnZ2VyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNWIxZWM4MjUtODU4My00OGViLWI4MGMtNmYyNzEzZTBlMzA3Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmlzdGlvLXN5c3RlbTpmbGFnZ2VyIn0.Eb14h5EKU9FfYZa3XkydrFFYUSk4gPYUM0j76Cbsb4XTtAL0U54-RfMiNcX5rfyK6WFOUhU5W6yuAChRhsl7TEzZCpgj3aVRNNe5TRsy-mYpG89FfBSpU0H6wmZyJnvHDcweo1eh-BLIThH6-_1GuUeJDc18WsapllkcHNIXiR_7gudgY7tfn29KoKxlv72K_HPYIerIsTGZe9tHr7K__lvl0Yz779yKNXKUlSerqho0-z2cPsmhFRR1KvPwrhi6UQck70s_snMlaecVJvkrYXCnEvsMkUwpaa6JmDmamKC3NNm9zWJYKtEt0fHHomZoJQFHHQCiYDTVkjYi8ErE2A'

# token 을 jtw.io 에서 Decoded 확인
{
  "iss": "kubernetes/serviceaccount",
  "kubernetes.io/serviceaccount/namespace": "istio-system",
  "kubernetes.io/serviceaccount/secret.name": "flagger-token-v2f5z",
  "kubernetes.io/serviceaccount/service-account.name": "flagger",
  "kubernetes.io/serviceaccount/service-account.uid": "5b1ec825-8583-48eb-b80c-6f2713e0e307",
  "sub": "system:serviceaccount:istio-system:flagger"
}

Flagger의 핵심은 Canary 리소스입니다.

이 리소스에서 카나리 릴리스의 모든 파라미터를 정의합니다:

#  cat ch5/flagger/catalog-release.yaml        
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: catalog-release
  namespace: istioinaction
spec:   
  targetRef: #1 카나리 대상 디플로이먼트 https://docs.flagger.app/usage/how-it-works#canary-target
    apiVersion: apps/v1
    kind: Deployment
    name: catalog  
  progressDeadlineSeconds: 60
  # Service / VirtualService Config
  service: #2 서비스용 설정 https://docs.flagger.app/usage/how-it-works#canary-service
    name: catalog
    port: 80
    targetPort: 3000
    gateways:
    - mesh    
    hosts:
    - catalog
  analysis: #3 카니리 진행 파라미터 https://docs.flagger.app/usage/how-it-works#canary-analysis
    interval: 45s
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    match: 
    - sourceLabels:
        app: webapp
    metrics: # https://docs.flagger.app/usage/metrics , https://docs.flagger.app/faq#metrics
    - name: request-success-rate # built-in metric 요청 성공률
      thresholdRange:
        min: 99
      interval: 1m
    - name: request-duration # built-in metric 요청 시간
      thresholdRange:
        max: 500
      interval: 30s
# 반복 호출테스트 : 신규터미널
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done


# flagger (operator) 가 catalog를 위한 canary 배포환경을 구성
kubectl apply -f ch5/flagger/catalog-release.yaml -n istioinaction

# flagger 로그 확인 : Service, Deployment, VirtualService 등을 설치하는 것을 확인할 수 있습니다.
kubectl logs -f deploy/flagger -n istio-system
...

# 확인
kubectl get canary -n istioinaction -w
NAME              STATUS        WEIGHT   LASTTRANSITIONTIME
catalog-release   Initializing  0        2025-04-19T05:10:00Z
catalog-release   Initialized   0        2025-04-19T05:15:54Z

kubectl get canary -n istioinaction -owide
NAME              STATUS        WEIGHT   SUSPENDED   FAILEDCHECKS   INTERVAL   MIRROR   STEPWEIGHT   STEPWEIGHTS   MAXWEIGHT   LASTTRANSITIONTIME
catalog-release   Initialized   0                    0              45s                 10                         50          2025-04-19T05:15:54Z

# flagger Initialized 동작 확인
## catalog-primary deployment/service 가 생성되어 있음, 기존 catalog deploy/service 는 파드가 0으로 됨
kubectl get deploy,svc,ep -n istioinaction -o wide

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE     CONTAINERS   IMAGES                         SELECTOR
deployment.apps/catalog           0/0     0            0           3h34m   catalog      istioinaction/catalog:latest   app=catalog,version=v1
deployment.apps/catalog-primary   1/1     1            1           6m41s   catalog      istioinaction/catalog:latest   app=catalog-primary
deployment.apps/webapp            1/1     1            1           3h36m   webapp       istioinaction/webapp:latest    app=webapp

NAME                      TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE     SELECTOR
service/catalog           ClusterIP   10.200.1.168   <none>        80/TCP    5m56s   app=catalog-primary
service/catalog-canary    ClusterIP   10.200.1.242   <none>        80/TCP    6m41s   app=catalog
service/catalog-primary   ClusterIP   10.200.1.119   <none>        80/TCP    6m41s   app=catalog-primary
service/webapp            ClusterIP   10.200.1.73    <none>        80/TCP    3h36m   app=webapp

NAME                        ENDPOINTS         AGE
endpoints/catalog           10.10.0.23:3000   5m56s
endpoints/catalog-canary    <none>            6m41s
endpoints/catalog-primary   10.10.0.23:3000   6m41s
endpoints/webapp            10.10.0.19:8080   3h36m

## VS catalog 생성되었음
kubectl get gw,vs -n istioinaction
NAME                                            AGE
gateway.networking.istio.io/coolstore-gateway   137m

NAME                                                       GATEWAYS                HOSTS                         AGE
virtualservice.networking.istio.io/catalog                 ["mesh"]                ["catalog"]                   8m17s
virtualservice.networking.istio.io/webapp-virtualservice   ["coolstore-gateway"]   ["webapp.istioinaction.io"]   137m

## VS catalog 확인
kubectl get vs -n istioinaction catalog -o yaml | kubectl neat
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  annotations:
    helm.toolkit.fluxcd.io/driftDetection: disabled
    kustomize.toolkit.fluxcd.io/reconcile: disabled
  name: catalog
  namespace: istioinaction
spec:
  gateways:
  - mesh
  hosts:
  - catalog
  http:
  - match:
    - sourceLabels:
        app: webapp
    route:
    - destination:
        host: catalog-primary
      weight: 100
    - destination:
        host: catalog-canary
      weight: 0
  - route:
    - destination:
        host: catalog-primary
      weight: 100

# destinationrule 확인
kubectl get destinationrule -n istioinaction
NAME              HOST              AGE
catalog-canary    catalog-canary    15m
catalog-primary   catalog-primary   15m

kubectl get destinationrule -n istioinaction catalog-primary -o yaml | kubectl neat
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: catalog-primary
  namespace: istioinaction
spec:
  host: catalog-primary

kubectl get destinationrule -n istioinaction catalog-canary -o yaml | kubectl neat
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: catalog-canary
  namespace: istioinaction
spec:
  host: catalog-canary

#
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 -o json
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 
NAME     DOMAINS                                                        MATCH     VIRTUAL SERVICE
80       catalog-canary, catalog-canary.istioinaction + 1 more...       /*        
80       catalog-primary, catalog-primary.istioinaction + 1 more...     /*        
80       catalog, catalog.istioinaction + 1 more...                     /*        catalog.istioinaction
...


docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction | egrep 'RULE|catalog'
SERVICE FQDN                                            PORT      SUBSET     DIRECTION     TYPE             DESTINATION RULE
catalog-canary.istioinaction.svc.cluster.local          80        -          outbound      EDS              catalog-canary.istioinaction
catalog-primary.istioinaction.svc.cluster.local         80        -          outbound      EDS              catalog-primary.istioinaction
catalog.istioinaction.svc.cluster.local                 80        -          outbound      EDS 

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction --fqdn catalog.istioinaction.svc.cluster.local -o json
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction --fqdn catalog-primary.istioinaction.svc.cluster.local -o json
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction --fqdn catalog-canary.istioinaction.svc.cluster.local -o json

docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/webapp.istioinaction | grep catalog
10.10.0.23:3000                                         HEALTHY     OK                outbound|80||catalog-primary.istioinaction.svc.cluster.local
10.10.0.23:3000                                         HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local

# 해당 EDS에 메트릭 통계 값 0.
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/webapp.istioinaction --cluster 'outbound|80||catalog.istioinaction.svc.cluster.local' -o json
...

# 현재 EDS primary 에 메트릭 통계 값 출력 중. 해당 EDS 호출.
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/webapp.istioinaction --cluster 'outbound|80||catalog-primary.istioinaction.svc.cluster.local' -o json
...

이제 catalog v2를 배포하면 Flagger가 자동으로 카나리 릴리스를 시작합니다:

# 반복 호출테스트 : 신규터미널1 - 부하 만들기
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done

# flagger 로그 확인 : 신규터미널2
kubectl logs -f deploy/flagger -n istio-system
{"level":"info","ts":"2025-04-19T05:15:54.453Z","caller":"controller/events.go:33","msg":"Initialization done! catalog-release.istioinaction","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:09:09.442Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:09:09.444Z","caller":"controller/events.go:33","msg":"New revision detected! Scaling up catalog.istioinaction","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:09:54.441Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:09:54.446Z","caller":"controller/events.go:33","msg":"Starting canary analysis for catalog.istioinaction","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:09:54.461Z","caller":"controller/events.go:33","msg":"Advance catalog-release.istioinaction canary weight 10","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:10:39.443Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:10:39.469Z","caller":"controller/events.go:33","msg":"Advance catalog-release.istioinaction canary weight 20","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:11:24.437Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:11:24.461Z","caller":"controller/events.go:33","msg":"Advance catalog-release.istioinaction canary weight 30","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:12:09.445Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:12:09.472Z","caller":"controller/events.go:33","msg":"Advance catalog-release.istioinaction canary weight 40","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:12:54.429Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:12:54.445Z","caller":"controller/events.go:33","msg":"Advance catalog-release.istioinaction canary weight 50","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:13:39.444Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:13:39.453Z","caller":"controller/events.go:33","msg":"Copying catalog.istioinaction template spec to catalog-primary.istioinaction","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:14:24.438Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:14:24.440Z","caller":"controller/events.go:33","msg":"Routing all traffic to primary","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-19T06:15:09.436Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-19T06:15:09.642Z","caller":"controller/events.go:33","msg":"Promotion completed! Scaling down catalog.istioinaction","canary":"catalog-release.istioinaction"}

# flagger 상태 확인 : 신규터미널3
## 카나리는 Carary 오브젝트에 설정한 대로 45초마다 진행될 것이다.
## 트래픽의 50%가 카나리로 이동할 때까지는 단계별로 10%씩 증가한다.
## flagger가 메트릭에 문제가 없고 기준과 차이가 없다고 판단되면, 모든 트래픽이 카나리로 이동해 카나리가 기본 서비스로 승격 될 때까지 카나리가 진행된다.
## 만약 문제가 발생하면 flagger는 자동으로 카나리 릴리스를 롤백할 것이다.
kubectl get canary -n istioinaction -w
NAME              STATUS        WEIGHT   LASTTRANSITIONTIME
catalog-release   Initialized   0        2025-04-19T05:15:54Z
catalog-release   Progressing   0        2025-04-19T06:09:09Z
catalog-release   Progressing   10       2025-04-19T06:09:54Z # 45초 간격
catalog-release   Progressing   20       2025-04-19T06:10:39Z
catalog-release   Progressing   30       2025-04-19T06:11:24Z
catalog-release   Progressing   40       2025-04-19T06:12:09Z
catalog-release   Progressing   50       2025-04-19T06:12:54Z
catalog-release   Promoting     0        2025-04-19T06:13:39Z
catalog-release   Finalising    0        2025-04-19T06:14:24Z
catalog-release   Succeeded     0        2025-04-19T06:15:09Z


# imageUrl 출력 (v2)을 포함하는 catalog deployment v2 배포
cat ch5/flagger/catalog-deployment-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: catalog
    version: v1
  name: catalog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: catalog
      version: v1
  template:
    metadata:
      labels:
        app: catalog
        version: v1
    spec:
      containers:
      - env:
        - name: KUBERNETES_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: SHOW_IMAGE
          value: "true"
        image: istioinaction/catalog:latest
        imagePullPolicy: IfNotPresent
        name: catalog
        ports:
        - containerPort: 3000
          name: http
          protocol: TCP
        securityContext:
          privileged: false

kubectl apply -f ch5/flagger/catalog-deployment-v2.yaml -n istioinaction
	kubectl get vs -n istioinaction catalog -o yaml -w # catalog vs 에 가중치 변경 모니터링
	---
    route:
    - destination:
        host: catalog-primary
      weight: 90
    - destination:
        host: catalog-canary
      weight: 10
  - route:
    - destination:
        host: catalog-primary
      weight: 90
---
    route:
    - destination:
        host: catalog-primary
      weight: 80
    - destination:
        host: catalog-canary
      weight: 20
  - route:
    - destination:
        host: catalog-primary
      weight: 80
---
    route:
    - destination:
        host: catalog-primary
      weight: 70
    - destination:
        host: catalog-canary
      weight: 30
  - route:
    - destination:
        host: catalog-primary
      weight: 70
---
    route:
    - destination:
        host: catalog-primary
      weight: 60
    - destination:
        host: catalog-canary
      weight: 40
  - route:
    - destination:
        host: catalog-primary
      weight: 60
---
    route:
    - destination:
        host: catalog-primary
      weight: 50
    - destination:
        host: catalog-canary
      weight: 50
  - route:
    - destination:
        host: catalog-primary
      weight: 50
---
    route:
    - destination:
        host: catalog-primary
      weight: 100
    - destination:
        host: catalog-canary
      weight: 0
  - route:
    - destination:
        host: catalog-primary
      weight: 100
---
 
# canary CRD 이벤트 확인
kubectl describe canary -n istioinaction catalog-release | grep Events: -A20
Events:
  Type    Reason  Age    From     Message
  ----    ------  ----   ----     -------
  Normal  Synced  10m    flagger  New revision detected! Scaling up catalog.istioinaction
  Normal  Synced  9m54s  flagger  Starting canary analysis for catalog.istioinaction
  Normal  Synced  9m54s  flagger  Advance catalog-release.istioinaction canary weight 10
  Normal  Synced  9m9s   flagger  Advance catalog-release.istioinaction canary weight 20
  Normal  Synced  8m24s  flagger  Advance catalog-release.istioinaction canary weight 30
  Normal  Synced  7m39s  flagger  Advance catalog-release.istioinaction canary weight 40
  Normal  Synced  6m54s  flagger  Advance catalog-release.istioinaction canary weight 50
  Normal  Synced  6m9s   flagger  Copying catalog.istioinaction template spec to catalog-primary.istioinaction
  Normal  Synced  5m24s  flagger  Routing all traffic to primary
  Normal  Synced  4m39s  flagger  (combined from similar events): Promotion completed! Scaling down catalog.istioinaction

# 최종 v2 접속 확인
for i in {1..100}; do curl -s http://webapp.istioinaction.io:30000/api/catalog | grep -i imageUrl ; done | wc -l
     100

카나리 과정 중 kiali 확인

Flagger는 다음과 같은 프로세스로 작동합니다:

  1. 원본 디플로이먼트의 변경을 감지
  2. 카나리 디플로이먼트(catalog-canary)와 프라이머리 디플로이먼트(catalog-primary) 생성
  3. 메트릭을 모니터링하면서 단계적으로 트래픽 가중치 조정
  4. 모든 검증이 통과되면 프라이머리를 업데이트하고 카나리 제거

프로메테우스에서 카나리 진행 상황을 확인할 수 있습니다:

  • flagger_canary_weight: 트래픽 가중치 변화
  • flagger_canary_metric_analysis{metric="request-success-rate"}: 요청 성공률
  • flagger_canary_metric_analysis{metric="request-duration"}: 응답 시간

Flagger를 사용하면 수작업으로 인한 실수를 줄이고, 메트릭 기반의 객관적인 기준으로 릴리스를 진행할 수 있습니다. 또한 문제 발생 시 자동 롤백되므로 안정적인 배포가 가능합니다.


👉 Step 04. 트래픽 미러링으로 위험 최소화

✅ Reducing risk even further: Traffic mirroring

앞서 살펴본 요청 수준 라우팅과 트래픽 분산은 릴리스의 위험을 줄이는 효과적인 방법이지만, 여전히 실제 사용자에게 영향을 줄 수 있다는 한계가 있습니다.

트래픽 미러링(Traffic Mirroring, 또는 섀도잉 Shadowing)은 이러한 위험을 더욱 줄일 수 있는 방법입니다.


출처 - 이제원님 @IstioStudy

트래픽 미러링이란?
트래픽 미러링은 운영 환경의 트래픽을 복사하여 새로운 버전으로 전송하는 기술입니다.

핵심은 미러링된 트래픽의 응답이 실제 사용자에게 영향을 주지 않는다는 점입니다.

이를 통해 사용자 경험에 영향을 주지 않으면서 새 버전의 동작을 실제 운영 환경의 트래픽으로 테스트할 수 있습니다.

# catalog 디플로이먼트를 초기 상태로 돌리고, catalog-v2 를 별도의 디플로이먼트로 배포
kubectl apply -f services/catalog/kubernetes/catalog-svc.yaml -n istioinaction
kubectl apply -f services/catalog/kubernetes/catalog-deployment.yaml -n istioinaction
kubectl apply -f services/catalog/kubernetes/catalog-deployment-v2.yaml -n istioinaction
kubectl apply -f ch5/catalog-dest-rule.yaml -n istioinaction
kubectl apply -f ch5/catalog-vs-v1-mesh.yaml -n istioinaction

# 확인
kubectl get deploy -n istioinaction -o wide
kubectl get svc,ep -n istioinaction -owide
kubectl get gw,vs -n istioinaction

# 반복 접속
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done

# catalog v1 으로만 접속 확인
for i in {1..100}; do curl -s http://webapp.istioinaction.io:30000/api/catalog | grep -i imageUrl ; done | wc -l
0

이제 트래픽 미러링을 구현해보겠습니다. VirtualService에 미러링 규칙을 추가합니다:

# cat ch5/catalog-vs-v2-mirror.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog
spec:
  hosts:
  - catalog
  gateways:
    - mesh
  http:
  - route:
    - destination:
        host: catalog
        subset: version-v1
      weight: 100
    mirror:
      host: catalog
      subset: version-v2
# 반복 접속
while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; don

# catalog istio-proxy 로그 활성화
cat << EOF | kubectl apply -f -
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
  name: catalog
  namespace: istioinaction
spec:
  accessLogging:
  - disabled: false
    providers:
    - name: envoy
  selector:
    matchLabels:
      app: catalog
EOF

kubectl get telemetries -n istioinaction
NAME      AGE
catalog   16s
webapp    5h49m

# istio-proxy 로그 확인 : 신규 터미널
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
kubectl logs -n istioinaction -l app=catalog -c istio-proxy -f
kubectl logs -n istioinaction -l version=v1 -c istio-proxy -f
kubectl logs -n istioinaction -l app=catalog -l version=v2 -c istio-proxy -f
혹은
kubectl stern -n istioinaction -l app=catalog -c istio-proxy

# proxy-config : webapp
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction | grep catalog 
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction | grep catalog
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/webapp.istioinaction | grep catalog

# proxy-config : catalog
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction | grep catalog 
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/catalog.istioinaction | grep catalog 
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/catalog.istioinaction | grep catalog 


# 미러링 VS 설정
kubectl apply -f ch5/catalog-vs-v2-mirror.yaml -n istioinaction

# v1 으로만 호출 확인
for i in {1..100}; do curl -s http://webapp.istioinaction.io:30000/api/catalog | grep -i imageUrl ; done | wc -l
       0

# v1 app 로그 확인
kubectl logs -n istioinaction -l app=catalog -l version=v1 -c catalog -f
request path: /items
blowups: {}
number of blowups: 0
GET catalog.istioinaction:80 /items 200 502 - 0.375 ms
GET /items 200 0.375 ms - 502
...

# v2 app 로그 확인 : 미러링된 트래픽이 catalog v2로 향할때, Host 헤더가 수정돼 미러링/섀도잉된 트래픽임을 나타낸다.
## 따라서 Host:catalog:8080 대신 Host:catalog-shadow:8080이 된다.
## -shadow 접미사가 붙은 요청을 받는 서비스는 그 요청이 미러링된 요청임을 식별할 수 있어, 요청을 처리할 때 고려할 수 있다
## 예를 들어, 응답이 버려질 테니 트랜잭션을 롤백하지 않거나 리소스를 많이 사용하는 호출을 하지 않는 것 등.
kubectl logs -n istioinaction -l app=catalog -l version=v2 -c catalog -f
request path: /items
blowups: {}
number of blowups: 0
GET catalog.istioinaction-shadow:80 /items 200 698 - 0.503 ms
GET /items 200 0.503 ms - 698


#
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 -o json > webapp-routes.json
cat webapp-routes.json
...
                        "route": {
                            "cluster": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
                            "timeout": "0s",
                            "retryPolicy": {
                                "retryOn": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes",
                                "numRetries": 2,
                                "retryHostPredicate": [
                                    {
                                        "name": "envoy.retry_host_predicates.previous_hosts",
                                        "typedConfig": {
                                            "@type": "type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate"
                                        }
                                    }
                                ],
                                "hostSelectionRetryMaxAttempts": "5",
                                "retriableStatusCodes": [
                                    503
                                ]
                            },
                            "requestMirrorPolicies": [
                                {
                                    "cluster": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local",
                                    "runtimeFraction": {
                                        "defaultValue": {
                                            "numerator": 100
                                        }
                                    },
                                    "traceSampled": false
...


# 위 webapp과 상동 : 그거슨 mesh(gateway)이니까...
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/catalog.istioinaction --name 80 -o json > webapp-routes.json
cat catalog-routes.json
...

주목할 점은 v2 로그의 호스트명이 catalog.istioinaction-shadow라는 것입니다.

-shadow 접미사는 미러링된 트래픽임을 나타내며, 이를 통해 서비스는 해당 요청이 미러링된 것임을 인식하고 특별히 처리할 수 있습니다(예: 트랜잭션 롤백 방지, 리소스 집약적인 작업 제외 등).


👉 Step 05. 클러스터 외부 서비스와의 통신 제어

✅ Routing to services outside your cluster by using Istio's service discovery

서비스 메시를 운영하다 보면 외부 서비스와의 통신이 필요한 경우가 많습니다.

Istio는 기본적으로 서비스 메시 외부로의 트래픽을 허용하지만, 보안 강화를 위해 이를 제어할 수 있는 기능을 제공합니다.

✅ 외부 트래픽 차단 설정

Istio는 기본적으로 외부 트래픽을 허용하지만(ALLOW_ANY 모드), 이를 차단함으로써 보안을 강화할 수 있습니다.

외부 트래픽 차단은 다음과 같은 보안 위협을 방지하는 데 도움이 됩니다:

  • 악성코드가 외부 서버와 통신하는 것을 차단
  • 내부 데이터 유출 방지
  • 침입자의 C&C(Command and Control) 서버 접근 차단

외부 트래픽을 차단하도록 설정해보겠습니다:

# 현재 istiooperators meshConfig 설정 확인
kubectl get istiooperators -n istio-system -o json
...
                "meshConfig": {
                    "defaultConfig": {
                        "proxyMetadata": {}
                    },
                    "enablePrometheusMerge": true
                },
...

# webapp 파드에서 외부 다운로드
kubectl exec -it deploy/webapp -n istioinaction -c webapp -- wget https://raw.githubusercontent.com/gasida/KANS/refs/heads/main/msa/sock-shop-demo.yaml

# webapp 로그 : 신규 터미널
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
[2025-04-19T09:55:34.851Z] "- - -" 0 UH - - "-" 0 0 0 - "-" "-" "-" "-" "-" BlackHoleCluster - 185.199.109.133:443 10.10.0.19:34868 - -

# 다음 명령을 실행해 이스티오의 기본값을 ALLOW_ANY 에서 REGISTRY_ONLY 로 바꾸자.
# 이느 서비스 메시 저장소에 명시적으로 허용된 경우(화이트 리스트)에만 트래픽이 메시를 떠나도록 허용하겠다는 의미다.
# 아래 설정 방법 이외에도 IstioOperator 로 설정을 변경하거나, istio-system 의 istio configmap 을 변경해도 됨.
# outboundTrafficPolicy 3가지 모드 : ALLOW_ANY (default) , REGISTRY_ONLY , ALLOW_LIST
docker exec -it myk8s-control-plane bash
----------------------------------------
istioctl install --set profile=default --set meshConfig.outboundTrafficPolicy.mode=REGISTRY_ONLY
y 

exit
----------------------------------------

# 배포 확인
docker exec -it myk8s-control-plane istioctl proxy-status
NAME                                                   CLUSTER        CDS        LDS        EDS        RDS          ECDS         ISTIOD                    VERSION
catalog-6d5b9bbb66-vzg4j.istioinaction                 Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-8d74787f-6w77x     1.17.8
catalog-v2-6df885b555-n9nxw.istioinaction              Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-8d74787f-6w77x     1.17.8
istio-ingressgateway-6bb8fb6549-s4pt8.istio-system     Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-8d74787f-6w77x     1.17.8
webapp-7685bcb84-skzgg.istioinaction                   Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-8d74787f-6w77x     1.17.8


# webapp 파드에서 외부 다운로드
kubectl exec -it deploy/webapp -n istioinaction -c webapp -- wget https://raw.githubusercontent.com/gasida/KANS/refs/heads/main/msa/sock-shop-demo.yaml
Connecting to raw.githubusercontent.com (185.199.109.133:443)
wget: error getting response: Connection reset by peer
command terminated with exit code 1

# webapp 로그 : BlackHoleCluster 차단
# UH : NoHealthyUpstream - No healthy upstream hosts in upstream cluster in addition to 503 response code.
# https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage
kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f
[2025-04-19T09:55:34.851Z] "- - -" 0 UH - - "-" 0 0 0 - "-" "-" "-" "-" "-" BlackHoleCluster - 185.199.109.133:443 10.10.0.19:34868 - -

# proxy-config : webapp
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/webapp.istioinaction --fqdn BlackHoleCluster -o json
[
    {
        "name": "BlackHoleCluster",
        "type": "STATIC",
        "connectTimeout": "10s"
    }
]

# 현재 istiooperators meshConfig 설정 확인
kubectl get istiooperators -n istio-system -o json
...
                "meshConfig": {
                    "accessLogFile": "/dev/stdout",
                    "defaultConfig": {
                        "proxyMetadata": {}
                    },
                    "enablePrometheusMerge": true,
                    "extensionProviders": [
                        {
                            "envoyOtelAls": {
                                "port": 4317,
                                "service": "opentelemetry-collector.istio-system.svc.cluster.local"
                            },
                            "name": "otel"
                        },
                        {
                            "name": "skywalking",
                            "skywalking": {
                                "port": 11800,
                                "service": "tracing.istio-system.svc.cluster.local"
                            }
                        }
                    ],
                    "outboundTrafficPolicy": {
                        "mode": "REGISTRY_ONLY"
                    }
                },

outboundTrafficPolicy의 모드:

  • ALLOW_ANY (기본값): 모든 외부 트래픽 허용
  • REGISTRY_ONLY: 서비스 레지스트리에 등록된 외부 서비스만 허용
  • ALLOW_LIST: 명시적으로 허용된 서비스만 접근 가능

로그에서 BlackHoleCluster는 Istio가 외부 트래픽을 차단하고 있음을 나타냅니다.

✅ ServiceEntry를 통한 외부 서비스 접근

외부 서비스에 대한 접근이 필요한 경우, ServiceEntry를 사용하여 Istio의 서비스 레지스트리에 외부 서비스를 등록할 수 있습니다.

Istio는 내부 서비스 레지스트리를 구축하여 메시 내 모든 서비스를 관리합니다:

  • Kubernetes에서는 Service 리소스를 통해 자동으로 레지스트리 구축
  • 외부 서비스는 ServiceEntry를 통해 수동으로 등록

이스티오 ServiceEntry 리소스는 이스티오의 서비스 저장소에 항목을 삽입하는 데 사용 할 수 있는 저장소 메타데이터를 캡슐화합니다.

# cat ch5/forum-serviceentry.yaml                                        
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: jsonplaceholder
spec:
  hosts:
  - jsonplaceholder.typicode.com
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS
  location: MESH_EXTERNAL

✅ ServiceEntry (실습)

JSONPlaceholder API를 사용하는 forum 애플리케이션을 통해 ServiceEntry 사용법을 알아보겠습니다.

# forum 설치
cat services/forum/kubernetes/forum-all.yaml
kubectl apply -f services/forum/kubernetes/forum-all.yaml -n istioinaction

# 확인
kubectl get deploy,svc -n istioinaction -l app=webapp
docker exec -it myk8s-control-plane istioctl proxy-status


# webapp 웹 접속
open http://webapp.istioinaction.io:30000/

이 호출을 허용하기 위해 jsonplaceholder.typicode.com 호스트에 이스티오 ServiceEntry 리소스를 만들 수 있습니다.

ServiceEntry 리소스를 만들면 이스티오의 서비스 저장소에 항목이 삽입되고, 서비스 메시가 이를 알 수 있습니다.

# istio proxy (forum)
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/forum.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/forum.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/forum.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config all deploy/forum.istioinaction -o short > forum-1.json

#
cat ch5/forum-serviceentry.yaml
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: jsonplaceholder
spec:
  hosts:
  - jsonplaceholder.typicode.com
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS
  location: MESH_EXTERNAL
  
kubectl apply -f ch5/forum-serviceentry.yaml -n istioinaction

#
docker exec -it myk8s-control-plane istioctl proxy-config all deploy/forum.istioinaction -o short > forum-2.json
diff forum-1.json forum-2.json
25a26
> jsonplaceholder.typicode.com                            80        -              outbound      STRICT_DNS       
96a98
> 80                                                            jsonplaceholder.typicode.com                       /*  

#
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/forum.istioinaction | grep json
80                                                            jsonplaceholder.typicode.com                       /* 

docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/forum.istioinaction --fqdn jsonplaceholder.typicode.com -o json
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/forum.istioinaction | grep json
jsonplaceholder.typicode.com                            80        -              outbound      STRICT_DNS  

# 목적 hosts 의 도메인 질의 응답 IP로 확인된 엔드포인트들 확인
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/forum.istioinaction --cluster 'outbound|80||jsonplaceholder.typicode.com'
ENDPOINT            STATUS      OUTLIER CHECK     CLUSTER
104.21.112.1:80     HEALTHY     OK                outbound|80||jsonplaceholder.typicode.com
104.21.16.1:80      HEALTHY     OK                outbound|80||jsonplaceholder.typicode.com
104.21.32.1:80      HEALTHY     OK                outbound|80||jsonplaceholder.typicode.com
104.21.48.1:80      HEALTHY     OK                outbound|80||jsonplaceholder.typicode.com
104.21.64.1:80      HEALTHY     OK                outbound|80||jsonplaceholder.typicode.com
104.21.80.1:80      HEALTHY     OK                outbound|80||jsonplaceholder.typicode.com
104.21.96.1:80      HEALTHY     OK                outbound|80||jsonplaceholder.typicode.com


# 메시 안에서 새로운 포럼 서비스를 호출 : 사용자 목록 반환 OK
curl -s http://webapp.istioinaction.io:30000/api/users


# 반복 접속
while true; do curl -s http://webapp.istioinaction.io:30000/ ; date "+%Y-%m-%d %H:%M:%S" ; sleep 2; echo; done


# webapp 웹 접속
open http://webapp.istioinaction.io:30000/



📌 Conclusion

이번 Istio in Action 5장 스터디를 통해 마이크로서비스 환경에서 트래픽 제어가 얼마나 중요한지 깊이 있게 이해할 수 있었습니다.

특히 배포와 릴리스를 분리하는 개념부터 시작해 다양한 트래픽 제어 전략을 실습해보며, Istio가 제공하는 강력한 기능들을 직접 체험할 수 있었습니다.

주요 학습 내용 정리

  1. 배포와 릴리스의 분리

    • 새 버전의 안전한 배포를 위한 기본 개념
    • 배포는 코드를 운영 환경에 설치하는 과정, 릴리스는 실제 트래픽을 전환하는 과정
  2. 트래픽 라우팅 전략

    • 헤더 기반 라우팅을 통한 다크 런치
    • VirtualService와 DestinationRule을 활용한 세밀한 트래픽 제어
    • 서비스 메시 내부에서의 깊은 라우팅 구현
  3. 점진적 릴리스 방법론

    • 가중치 기반 트래픽 분산으로 카나리 릴리스 구현
    • Flagger를 활용한 카나리 릴리스 자동화
    • 메트릭 기반의 자동 롤백 시스템
  4. 트래픽 미러링

    • 실사용자에게 영향 없이 새 버전 테스트
    • "Fire-and-forget" 방식의 안전한 테스팅 환경
    • 섀도우 트래픽을 통한 실제 환경 검증
  5. 외부 서비스 통신 제어

    • 보안을 위한 외부 트래픽 차단 설정
    • ServiceEntry를 통한 외부 서비스 명시적 관리
    • 레거시 시스템과의 안전한 통합

이번 실습을 통해 가장 인상 깊었던 점은 Istio가 복잡한 트래픽 제어를 얼마나 직관적이고 강력하게 구현하는지 확인할 수 있었다는 것입니다.

특히 다음 세 가지 측면에서 큰 깨달음을 얻었습니다:

  1. 위험 최소화: 트래픽 미러링을 통해 실제 사용자에게 영향을 주지 않고 테스트할 수 있다는 점이 매우 인상적이었습니다. 이는 프로덕션 환경에서의 안정성을 크게 향상시킬 수 있는 핵심 기능입니다.

  2. 자동화의 중요성: Flagger를 통한 카나리 릴리스 자동화는 휴먼 에러를 줄이고 객관적인 메트릭 기반 의사결정을 가능하게 해줍니다. 이는 DevOps 문화에서 매우 중요한 요소입니다.

  3. 보안과 거버넌스: ServiceEntry를 통한 외부 서비스 접근 제어는 단순한 트래픽 제어를 넘어 보안과 거버넌스 측면에서도 큰 가치를 제공합니다.

마이크로서비스 아키텍처에서 안정적인 서비스 운영을 위해서는 이러한 트래픽 제어 기능들이 필수적입니다.

Istio는 이러한 요구사항을 매우 효과적으로 충족시키며, 실제 운영 환경에서 발생할 수 있는 다양한 시나리오에 대응할 수 있는 강력한 도구임을 확인할 수 있었습니다.

앞으로 이러한 기능들을 실제 프로젝트에 적용하면서, 더 안정적이고 효율적인 마이크로서비스 운영이 가능할 것으로 기대됩니다.



🔗 Reference

profile
안녕하세요! DevOps 엔지니어 이재찬입니다. 블로그에 대한 피드백은 언제나 환영합니다! 기술, 개발, 운영에 관한 다양한 주제로 함께 나누며, 더 나은 협업과 효율적인 개발 환경을 만드는 과정에 대해 인사이트를 나누고 싶습니다. 함께 여행하는 기분으로, 즐겁게 읽어주시면 감사하겠습니다! 🚀

0개의 댓글