스터디/Istio

Istio 스터디 3주차 - <Traffic Control>

황동리 2025. 4. 23. 10:56
반응형

CloudNet@ 가시다님이 진행하는 Istio 스터디 1기 - 3주차 정리 내용 입니다.

 

앞서 진행했던 주차를 보고 오시면 이해가 빠릅니다.

 

2025.04.07 - [스터디/Istio] - Istio 스터디 1주차 - <서비스 메시와 Istio>

2025.04.09 - [스터디/Istio] - Istio 스터디 1주차 -

2025.04.16 - [스터디/Istio] - Istio 스터디 2주차 -

2025.04.18 - [스터디/Istio] - Istio 스터디 2주차 - <Envoy와 Istio Gateway 실습>

 

이번 글에서는 Traffic Control 에 대해서 알아보고 실습 해보도록 하겠습니다.


✅ Traffic Control 이란?

Istio에서의 트래픽 제어는 서비스 간 통신 흐름을 세밀하게 조정하고, 특정 조건에 따라 트래픽을 분산하거나 전환할 수 있게 도와주는 기능 입니다.

 

주로 아래와 같은 목적에 사용됩니다.

  • 새로운 버전 배포 시, 점진적으로 트래픽 전달 (Canary / Blue-Green 배포)
  • 특정 사용자만 테스트 기능을 사용하도록 설정 (A/B 테스트)
  • 문제 발생 시 특정 서비스로 트래픽 우회
  • 요청 헤더, URI 등 다양한 조건에 따라 라우팅

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

Istio의 트래픽 제어 기능을 이해하기 위해서는 "배포와 릴리스의 차이"를 이해하는 것이 중요합니다.

배포(Deployment) 란?:

  • 배포는 코드가 운영 환경(Production Cluster)에 설치되는 행위입니다.
  • 하지만 이 시점에서는 사용자 트래픽은 전혀 흐르지 않습니다.
  • 대신 스모크 테스트, 모니터링, 메트릭 수집 등을 통해 사전 검증이 이루어집니다.
  • 이 방식은 문제가 발생해도 실제 사용자에게 영향을 주지 않기 때문에 안전합니다.

릴리스(Release) 란?:

  • 릴리스는 배포된 코드에 실제 트래픽을 전달하는 단계입니다.
  • 릴리스는 반드시 전체 사용자에게 한 번에 진행되지 않아도 됩니다.
  • 예를 들어, 내부 직원이나 테스트 사용자만을 대상으로 먼저 릴리스할 수 있습니다.
  • 이를 통해 문제 발생 시 빠르게 감지하고 롤백할 수 있습니다.

실제로 운영 환경에서 서비스를 사용자들에게 릴리스 할 때, 배포 과정을 거쳐 릴리스 과정 까지 진행이 됩니다.

 

예시를 통해 자세히 알아보겠습니다.

배포 -> 릴리스 예시

1️⃣ 배포(Deployment) 준비

 

현재 catalog 서비스의 v1 버전이 운영 중이라고 가정해봅시다.


이 서비스에 코드 변경 사항이 발생하면, 우리는 CI 시스템을 통해 코드를 빌드하고 새로운 버전(예: v2.0)으로 태깅한 후, 스테이징 환경에 먼저 배포하여 충분히 테스트를 진행합니다.

 

테스트와 승인이 완료되면 이 새로운 버전을 운영 환경으로 옮기게 됩니다.

 

2️⃣ 배포(Deployment) 완료

 

운영 환경에 v2.0 버전이 배포(deploy)되었다고 해서, 곧바로 사용자 요청을 받는 것은 아닙니다.


아직은 어떤 사용자도 이 새로운 버전에 접근할 수 없습니다.


이 단계에서는 새로운 버전의 서비스가 운영 환경의 리소스(컨테이너, 서버 등)에 설치만 된 상태 입니다.

 

이때 우리는 스모크 테스트, 로그 및 메트릭 수집, 모니터링 등을 통해 v1.1이 기대한 대로 작동하는지 확인할 수 있습니다.

 

이처럼 운영 환경에서 배포만 이루어지고 트래픽을 받지 않는 단계는 매우 안전하며, 문제가 발생해도 사용자에게 영향을 주지 않습니다.

 

3️⃣ 릴리스, 트래픽을 전달하는 시점

새로운 코드를 실제 운영에 릴리스(Release) 한다는 것은 트래픽을 새로운 배포로 전달하기 시작하는 것을 의미합니다.

릴리스는 꼭 모든 사용자에게 동시에 이루어질 필요는 없습니다.

예를 들어, user == internal 조건을 만족하는 내부 직원에게만 먼저 트래픽을 보내도록 할 수 있습니다.

이런 방식으로 운영자는 새 버전이 실제 운영 환경에서 어떻게 동작하는지, 로그와 메트릭을 통해 관찰하고 검증할 수 있습니다.

이렇게 하면 실 트래픽의 대부분은 소프트웨어의 구 버전이 받고, 신 버전은 일부만을 받게되고
해당 방식을 카나리한다(canarying) 고 말하거나 카나리 릴리스(canary release)라고 부릅니다.

해당 방식을 사용하면, 신 버전이 정상 동작 하지 않을 때 릴리스를 철회하고 트래픽을 구 버전으로 되돌리기도 편합니다.

4️⃣ 카나리 릴리스(Canary Release)의 확장

 

이제 점진적으로 더 넓은 사용자에게 릴리스를 확장할 수 있습니다.


예를 들어, 내부 사용자뿐만 아니라, silver 등급의 고객에게도 트래픽을 전달할 수 있습니다.

 

이를 통해 실 사용자 반응을 수집하고, 여전히 이상이 없다면 다음 단계를 진행할 수 있습니다.

 

트래픽을 점진적으로 확장하고 모니터링하면서, 릴리스 전략을 더 세밀하게 조정할 수 있게 됩니다.

 

5️⃣ 전체 릴리스와 롤백 전략

 

마지막 단계에서는 전체 사용자 트래픽을 새 버전인 v2.0으로 전환합니다.


이제 v1.0은 운영 환경에 배포된 상태이지만 트래픽은 받지 않는 상태가 됩니다.

 

만약 이 과정 중 예상치 못한 오류나 성능 저하가 발생한다면, 트래픽을 다시 v1.0으로 돌리는 롤백도 빠르게 가능합니다.

 

릴리스는 “트래픽의 흐름”에 대한 결정이고, 배포는 “코드 설치”에 관한 작업임을 명확히 이해해야 합니다.

Traffic Control 실습

이전 글에서 Istio VirtualService를 사용하여 트래픽을 라우팅 하는 방법에 대해 알아보았습니다.

 

해당 내용을 다시 복기 할 겸 좀 더 자세히 알아보도록 하겠습니다.

요청에 따른 Routing 실습

🔧 catalog-v1 POD, Service 배포
$ kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction

🔧 도메인 질의를 위한 임시 설정 추가
$ echo "127.0.0.1       catalog.istioinaction.io" | sudo tee -a /etc/hosts

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

결과
[
  {
    "id": 1,
    "color": "amber",
    "department": "Eyewear",
    "name": "Elinor Glasses",
    "price": "282.00"
  },
  {
    "id": 2,
    "color": "cyan",
    "department": "Clothing",
    "name": "Atlas Shirt",
    "price": "127.00"
  },
  {
    "id": 3,
    "color": "teal",
    "department": "Clothing",
    "name": "Small Metal Shoes",
    "price": "232.00"
  },
  {
    "id": 4,
    "color": "red",
    "department": "Watches",
    "name": "Red Dragon Watch",
    "price": "232.00"
  }
]

🔧 외부 노출을 위한 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

🔧 이제 istio-gateway에서 Service(NodePort)에 포트 정보 확인 
$ kubectl get svc -n istio-system istio-ingressgateway -o jsonpath="{.spec.ports}" | jq

결과
[
  {
    "name": "status-port",
    "nodePort": 30913,
    "port": 15021,
    "protocol": "TCP",
    "targetPort": 15021
  },
  {
    "name": "http2",
    "nodePort": 30000,
    "port": 80,
    "protocol": "TCP",
    "targetPort": 8080
  },
  {
    "name": "https",
    "nodePort": 30005,
    "port": 443,
    "protocol": "TCP",
    "targetPort": 8443
  }
]

🔧 이제 30000 NodePort로 요청이 들어오면 Service 에서는 80 포트로 요청을 받고 POD 안의 컨테이너 포트에서 설정해둔 8080 포트로 요청이 들어오는지 확인해보겠습니다.
$ kubectl logs -f -l app=catalog -n istioinaction
$ curl -v -H "Host: catalog.istioinaction.io" http://localhost:30000

 

이 내용을 이제 kiali 그래프에서도 확인할 수 있습니다.

🔧 반복 호출
$ 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

🔧 URL에 localhost:30003 입력 -> Graph에서 확인

 

 

특정 요청 시, 새로운 버전의 서비스로만 라우팅 되도록 해보기

이제 catalog-v2 서비스를 배포를 하고 특정 요청들만 v2 버전으로 라우팅 하는 방식을 실습해보겠습니다.

 

먼저 catalog-v2 서비스를 배포해보겠습니다.

🔧 catalog-v2 서비스 배포
$ kubectl apply -f services/catalog/kubernetes/catalog-deployment-v2.yaml -n istioinaction

🔧 배포한 deploy들의 라벨 확ㅇ니
$ kubectl get deploy -n istioinaction --show-labels

결과
NAME         READY   UP-TO-DATE   AVAILABLE   AGE   LABELS
catalog      1/1     1            1           55m   app=catalog,version=v1
catalog-v2   1/1     1            1           24s   app=catalog,version=v2

🔧 배포한 POD들의 Endpoint 확인
$ kubectl get pod -n istioinaction -o wide

결과
NAME                          READY   STATUS    RESTARTS   AGE   IP           NODE                  NOMINATED NODE   READINESS GATES
catalog-6cf4b97d-7b6v9        2/2     Running   0          56m   10.10.0.14   myk8s-control-plane   <none>           <none>
catalog-v2-6df885b555-9b78z   2/2     Running   0          61s   10.10.0.15   myk8s-control-plane   <none>           <none>

🔧 생성된 Envoy 프록시 확인
$ docker exec -it myk8s-control-plane istioctl proxy-status

결과
NAME                                                  CLUSTER        CDS        LDS        EDS        RDS        ECDS         ISTIOD                      VERSION
catalog-6cf4b97d-7b6v9.istioinaction                  Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-jd7lt     1.17.8
catalog-v2-6df885b555-9b78z.istioinaction             Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-jd7lt     1.17.8
istio-ingressgateway-996bc6bb6-htbqq.istio-system     Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-jd7lt     1.17.8

🔧 istio-gateway에서 catalog 서비스로 트래픽 보내는 Endpoint 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system --cluster 'outbound|80||catalog.istioinaction.svc.cluster.local'

# 결과를 보면 앞서 생생했던 POD의 Endpoint와 컨테이너 포트로 가는 것을 확인할 수 있습니다.
결과
ENDPOINT            STATUS      OUTLIER CHECK     CLUSTER
10.10.0.14:3000     HEALTHY     OK                outbound|80||catalog.istioinaction.svc.cluster.local
10.10.0.15: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
                    }
                }

 

이제 kiali Graph 에서 확인해보면 정상적으로 배포 릴리스 된것을 확인 할 수 있습니다.

 

이제 모든 트래픽을 catalog-v1 버전으로 라우팅 해주도록 합니다.

 

앞서 확인 했던 레이블 대로, DestinationRule을 만들어서 라우팅 해보도록 하겠습니다.

🔧 각 파드 별 레이블 확인
$ kubectl get pod -l app=catalog -n istioinaction --show-labels

결과
NAME                          READY   STATUS    RESTARTS   AGE   LABELS
catalog-6cf4b97d-7b6v9        2/2     Running   0          73m   app=catalog,pod-template-hash=6cf4b97d,security.istio.io/tlsMode=istio,service.istio.io/canonical-name=catalog,service.istio.io/canonical-revision=v1,version=v1
catalog-v2-6df885b555-9b78z   2/2     Running   0          18m   app=catalog,pod-template-hash=6df885b555,security.istio.io/tlsMode=istio,service.istio.io/canonical-name=catalog,service.istio.io/canonical-revision=v2,version=v2

🔧 DestionationRule 설정
$ 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   7s

🔧 CDS 정보 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system --fqdn catalog.istioinaction.svc.cluster.local

# 결과를 살펴보면 배포된 Catalog 서비스들의 Enovy 프록시의 CDS 정보 입니다.
# 동일한 도메인을 가지고 있지만, 버전 별로 SUBSET이 다릅니다. 
결과
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

🔧 VirtualService에 subset 설정을 추가해주도록 하겠습니다.
# VS에 subset: version-v1 설정을 추가해주면, catalog-v1 파드로만 트래픽이 전달되게 됩니다.
$ 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

 

kiali 대시보드에서 확인해보면 아래와 같이 v2 쪽으로 가던 트래픽이 사라진 것을 확인할 수 있습니다.

이제 HTTP 요청 헤더에 x-istio-cohort: internal 을 포함한 트래픽은 catalog-v2 로 보내도록 해보겠습니다.

🔧 VirtualService 내용 수정
$ 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:							# http 필드는 위에서부터 순차적으로 match 합니다.
  - match:
    - headers:
        x-istio-cohort:
          exact: "internal"
    route:						# 따라서 헤더 값이 x-istio-cohort: internal 이면 catalog-v2 로 트래픽이 라우팅 됩니다.
    - destination:
        host: catalog
        subset: version-v2
  - route:			
    - destination:
        host: catalog
        subset: version-v1
        
$ kubectl apply -f ch5/catalog-vs-v2-request.yaml -n istioinaction

🔧 istio-gateway의 Envoy 프록시에서 route 결과 확인
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system --name http.8080

# 동일한 도메인의 route 결과가 2개가 나오는 이유는 앞서 설정한 VirtualService에서 route 경로가 2개여서 입니다.
결과
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

 

이제 kiali 대시보드에서 확인해보면 v2 파드로 트래픽이 가는 것을 확인할 수 있습니다.

라우팅 수행 위치 변경 실습

지금까지 Istio를 사용해 요청을 라우팅하는 방법을 살펴봤지만, 라우팅 수행 위치가 에지/게이트웨이뿐 이였습니다.

 

앞서 진행했던 내용과 동일하게 header에 internal 요청이 들어올 때만 catalog-v2 버전으로 라우팅하는 방식을 구현을 해볼건데,

 

이번엔 게이트웨이가 아닌, 파드 내부의 사이드카에서 트래픽을 조정해보도록 하겠습니다.

🔧 istioinaction 네임스페이스에 있는 gateway, virtualservice, destinationrule 삭제
$ kubectl delete gateway,virtualservice,destinationrule --all -n istioinaction

🔧 webapp 애플리케이션 설치
# catalog-v1,v2는 이미 앞선 과정에서 설치 완료
$ kubectl apply -n istioinaction -f services/webapp/kubernetes/webapp.yaml

🔧 gateway, virtualService 세팅
# "webapp.istioinaction.io" 도메인 이름으로 들어오는 요청을 GW에서 받아서 webapp 이름의 서비스의 80 포트로 요청을 보내도록 설정
$ 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

🔧 도메인 질의를 위한 임시 설정 추가
$ echo "127.0.0.1       webapp.istioinaction.io" | sudo tee -a /etc/hosts

🔧 호출 테스트 외부(curl) -> ingressgw -> webapp -> catalog(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

 

kiali Graph에서 보면 실제 트래픽이 가는 경로를 볼 수 있습니다.

🔧 Envoy 프록시에서 라우팅 정보를 확인 해봅니다.
$ 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

🔧 Envoy 프록시에서 가지고 있는 클러스터의 서비스 엔드포인트를 확인해줍니다.
$ 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

🔧 Envoy 프록시에서 가지고 있는 Endpoint를 확인해줍니다.
$ 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.14:8080     HEALTHY     OK                outbound|80||webapp.istioinaction.svc.cluster.local

🔧 위에서 확인한 webapp.istioinaction.svc.cluster.local 서비스 도메인과 연결된 파드의 엔드포인트를 확인해보겠습니다.
$ k get pods -o wide -n istioinaction

# 결과를 보면 webapp 파드의 IP가 10.10.0.14 으로 앞서 확인했던 Endpoint와 동일한 것을 확인 할 수 있습니다.
결과
NAME                          READY   STATUS    RESTARTS   AGE   IP           NODE                  NOMINATED NODE   READINESS GATES
catalog-6cf4b97d-6mscs        2/2     Running   0          71m   10.10.0.12   myk8s-control-plane   <none>           <none>
catalog-v2-6df885b555-smwn5   2/2     Running   0          69m   10.10.0.13   myk8s-control-plane   <none>           <none>
webapp-7685bcb84-rskhw        2/2     Running   0          18m   10.10.0.14   myk8s-control-plane   <none>           <none>

🔧 새로운 터미널에서 webapp 파드 앞에 있는 Envoy 프록시의 로그를 실시간으로 확인해줍니다.
$ kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f

🔧 webapp에 대한 엑세스 로그 활성화 적용
$ 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

 

Telemetry를 적용하면, webapp 파드 앞에 있는 Envoy 프록시에서 현재 요청 받고 있는 트래픽에 대한 로그가 나오는 것을 확인할 수 있습니다.

 

이제 메시 내부 트래픽(사이드카 간 통신)을 조정해보도록 하겠습니다.

🔧 DestionationRule 적용
# catalog.istioinaction.svc.cluster.local 서비스 도메인으로 요청이 들어오면 version-v1,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

🔧 메시 내부에서 catalog 서비스로 향하는 모든 트래픽이 version-v1 파드로만 라우팅 되도록 설정
$ 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

 

적용시키고 kiail Graph 에서 확인해보면 v1 쪽으로만 트래픽이 가는 것을 확인할 수 있습니다.

 

좀 더 자세히 살펴보겠습니다.

🔧 webapp 파드 앞의 Envoy 프록시에서 라우팅 경로를 살펴보겠습니다.
$ docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction | egrep 'NAME|catalog'

# 결과를 보면 webapp에서 catalog 관련된 정보가 나오는 것을 확인할 수 있습니다.
결과
NAME                                                          DOMAINS                                            MATCH                  VIRTUAL SERVICE
80                                                            catalog, catalog.istioinaction + 1 more...         /*                     catalog.istioinaction

🔧 좀 더 자세하게 확인하기 위해 json 형식으로 Envoy 프록시 라우팅 정보를 확인해보겠습니다.
$ docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 -o json

# 결과를 보면 webapp 파드의 Envoy 프록시는 catalog 관련 도메인으로 들어오는 모든 요청(/)을
# catalog 서비스의 version-v1 파드로만 라우팅하도록 설정되어 있습니다.
# 이는 앞서 적용한 VirtualService와 DestinationRule의 조합 결과입니다.
결과
{
        "name": "80",
        "virtualHosts": [
            {
                "name": "catalog.istioinaction.svc.cluster.local:80",
                "domains": [
                    "catalog.istioinaction.svc.cluster.local",
                    "catalog",
                    "catalog.istioinaction.svc",
                    "catalog.istioinaction",
                    "10.200.1.62"	# 해당 IP는 catalog 파드와 연결된 Service의 ClusterIP
                ],
                "routes": [
                    {
                        "match": {
                            "prefix": "/"
                        },
                        "route": {
                            "cluster": "outbound|80|version-v1|catalog.istioinaction.svc.cluster.local",
                            "timeout": "0s"
                            

 

이제 헤더에 x-istio-cohort: internal 요청이 포함된 트래픽을 메쉬 단계에서 catalog-v2 로만 라우팅 되도록 설정하겠습니다.

🔧 VirtualService 설정 수정
$ 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

🔧 반복 호출을 시켜줍니다.
$ 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 파드 앞의 Envoy 프록시에서 라우팅 새롭게 추가된 라우팅 경로를 확인 해줍니다.
$ docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/webapp.istioinaction --name 80 -o json

결과
{
        "name": "80",
        "virtualHosts": [
            {
                "name": "catalog.istioinaction.svc.cluster.local:80",
                "domains": [
                    "catalog.istioinaction.svc.cluster.local",
                    "catalog",
                    "catalog.istioinaction.svc",
                    "catalog.istioinaction",
                    "10.200.1.62"
                ],
                "routes": [
                    {
                        "match": {
                            "prefix": "/",
                            "caseSensitive": true,
                            "headers": [
                                {
                                    "name": "x-istio-cohort",	
                                    "stringMatch": {
                                        "exact": "internal"		
                                    }
                                }
                            ]
                        },
                        "route": {
                            "cluster": "outbound|80|version-v2|catalog.istioinaction.svc.cluster.local" # 헤더가 x-istio-cohort: internal 일 때, version-v2로 라우트 되는 것을 확인 할 수 있다.

 

그리고 kiail Graph에서 확인하면 아래 그림 처럼 트래픽이 흐르는 것을 확인 할 수 있습니다.

✅ Traffic shifing

Istio의 Traffic Shifting 은 동일한 서비스의 여러 버전(예: v1, v2) 사이에서 트래픽을 특정 비율로 나눠 보내는 기능입니다.

 

주로, Canary 배포, A/B 테스트, 블루그린 배포, 버전 테스트 등에 매우 유용하게 사용됩니다.

예를 들어, catalog-v2를 내부 직원들에게만 다크 런치하고 일반 사용자들에게는 천천히 릴리스 하고 싶을 때, 라우팅 가중치를 10% 정도만 지정해서 릴리스 할 수 있습니다.

이렇게 하면, v1은 90% v2는 10% 트래픽이 분산되면서 새로 배포된 v2가 문제가 있을 때 대처하기 더 쉽습니다.

 

이전 실습에서는 주로 헤더 기반으로 다크 런치(dark launch)를 수행하는 라우팅 방식에 대해 살펴보았습니다.

 

이번에는 가중치 기반으로 특정 서비스에 여러 버전에 라이브 트래픽을 분배해보겠습니다.

🔧 앞서 실행 시켰던 deployment 들 확인
$ kubectl get deploy,rs,pod -n istioinaction --show-labels

결과
NAME                         READY   UP-TO-DATE   AVAILABLE   AGE     LABELS
deployment.apps/catalog      1/1     1            1           5h6m    app=catalog,version=v1
deployment.apps/catalog-v2   1/1     1            1           5h4m    app=catalog,version=v2
deployment.apps/webapp       1/1     1            1           4h13m   app=webapp

🔧 반복 호출을 실행 시켜줍니다.
$ while true; do curl -s http://webapp.istioinaction.io:30000/api/catalog ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done

🔧 모든 트래픽을 catlog-v1으로 재설정
$ cat ch5/catalog-vs-v1-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

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

 

kiail Graph를 확인해보면 v1으로만 트래픽이 가는 것을 확인할 수 있습니다.

 

이제 catalog-v2로 10%만 트래픽을 보내보도록 하겠습니다.

🔧 앞서 구성 했던 VirtualService 내용에서 catalog-version-v2 destination 추가
$ 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

 

kiali Graph에서 확인해보면 9:1 비율로 트래픽이 가는 것을 확인 할 수 있습니다.

🔧 webapp 파드의 Envoy 프록시의 라우팅 정보를 확인해보면 90:10으로 적용되있는 것을 확인할 수 있습니다.
$ 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

 

이제 트래픽 비율을 50:50으로 변경해보겠습니다.

🔧 VirtualService 설정 내용에서 비율 변경
$ 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

 

kiail Graph에서 확인해보면 50:50 비율로 트래픽이 분산되는 것을 확인할 수 있습니다.

Flagger를 사용해보기

앞서 트래픽 비율 변경은 운영자가 CLI를 통해 수동으로 변경을 해야하지만, Flagger를 사용하면 서비스 릴리스를 자동화 할 수 있습니다.

Flagger란?

Flagger는 스테판 프로단(Stefan Prodan) 이 만든 카나리 자동화 도구로, 릴리스를 어떻게 수행할지, 언제 더 많은 사용자에게 릴리스를 개방할지, 릴리스가 문제를 일으킬 경우 언제 롤백할지 등에 관련된 파라미터를 지정할 수 있습니다.

Flagger는 릴리스를 수행하는 데 필요한 작절한 설정을 모두 만들 수 있습니다.

 

이번엔 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

 

Flagger 설치에 앞서 동작 방식을 잠깐 설명 드리자면,

  • Flagger는 서비스 상태를 판단할 때 메트릭에 의존하며, 카나리 릴리스를 사용할 때 특히 그렇습니다.
  • Flagger가 성공 메트릭을 사용하려면 프로메테우스를 설치해 이스티오 데이터 플레인 수집해야 합니다.

이제 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-22T06:12:13Z
canaries.flagger.app                       2025-04-22T06:12:13Z
metrictemplates.flagger.app                2025-04-22T06:12:13Z

🔧 Helm 차트를 사용하여 flagger 설치
$ 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-vj5ww   1/1     Running   0          65s

🔧 flagger canary 리소스를 사용하여 카나리 릴리스의 파라미터를 지정하고, flagger가 적절한 리소스를 만들어서 릴리스를 주관하도록 설정
$ 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
      
위 설정 파일에 설명을 덧붙이자면,
- 이 Canary 리소스에서는 어떤 쿠버네티스 Deployment가 카나리 대상인지, 어떤 쿠버네티스 Service와 이스티오 VirtualService가 자동으로 만들어져야 하는지, 카나리를 어떻게 진행해야 하는지 등을 지정한다.
- Canary 리소스의 마지막 부분은 카나리를 얼마나 빨리 진행할지, 생존을 판단하기 위해 지켜볼 메트릭은 무엇인지, 성공을 판단할 임계값은 얼마인지를 기술하고 있다.
- 45초마다 카나리의 각 단계를 평가하고, 단계별로 트래픽을 10%씩 늘린다. 트래픽이 50%에 도달하면 100%로 바꾼다.
- 성공률 메트릭의 경우 1분 동안의 성공률이 99% 이상이어야 한다. 또한 P99(상위 99%) 요청 시간은 500ms까지 허용한다.
- 이 메트릭들이 연속으로 5회를 초과해 지정한 범위와 다르면, 롤백한다.

 

이제 해당 Canary 리소스를 적용하고 catalog-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

🔧 Canary 리소스 설정 적용
$ kubectl apply -f ch5/flagger/catalog-release.yaml -n istioinaction

🔧 Canary 리소스 설치 확인
# 해당 명령어를 사용하면 Service, Deployment, VirtualService 등을 설치하는 로그가 보입니다.
$ kubectl logs -f deploy/flagger -n istio-system
$ kubectl get canary -n istioinaction

결과
NAME              STATUS        WEIGHT   LASTTRANSITIONTIME
catalog-release   Initialized   0        2025-04-22T06:28:06Z

🔧 Deployment, Service, VirtualService, Gateway 리소스 확인
$ 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           6h43m   catalog      istioinaction/catalog:latest   app=catalog,version=v1
deployment.apps/catalog-primary   1/1     1            1           35m     catalog      istioinaction/catalog:latest   app=catalog-primary
deployment.apps/webapp            1/1     1            1           5h50m   webapp       istioinaction/webapp:latest    app=webapp

NAME                      TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE     SELECTOR
service/catalog           ClusterIP   10.200.1.13    <none>        80/TCP    34m     app=catalog-primary
service/catalog-canary    ClusterIP   10.200.1.110   <none>        80/TCP    35m     app=catalog
service/catalog-primary   ClusterIP   10.200.1.171   <none>        80/TCP    35m     app=catalog-primary
service/webapp            ClusterIP   10.200.1.114   <none>        80/TCP    5h50m   app=webapp

NAME                        ENDPOINTS         AGE
endpoints/catalog           10.10.0.16:3000   34m
endpoints/catalog-canary    <none>            35m
endpoints/catalog-primary   10.10.0.16:3000   35m
endpoints/webapp            10.10.0.14:8080   5h50m

$ kubectl get gw,vs -n istioinaction

결과
NAME                                            AGE
gateway.networking.istio.io/coolstore-gateway   5h43m

NAME                                                       GATEWAYS                HOSTS                         AGE
virtualservice.networking.istio.io/catalog                 ["mesh"]                ["catalog"]                   35m
virtualservice.networking.istio.io/webapp-virtualservice   ["coolstore-gateway"]   ["webapp.istioinaction.io"]   5h43m


🔧 생성된 VirtualService 내용 확인
# 내용 확인해보면, app: webapp 라벨이 붙은 파드에서 오는 트래픽은 catalog-primary로 가도록 설정되어있음
$ kubectl get vs -n istioinaction catalog -o yaml
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

 

kiail Graph에서 확인해보면 catalog-primary로만 트래픽이 흐르는 것을 확인 할 수 있습니다.

 

현재는 catalog-primary 서비스로 100%, catalog-canary 로는 0% 라우팅 되는 것을 확인 할 수 있습니다.

 

지금까지는 기본 설정만 준비 했을 뿐, 카나리 배포는 실행하지 않았습니다.

 

이제 catalog-v2를 설치하고 Flagger가 어떻게 릴리스에서 자동화하는지, 메트릭 기반해서 의사결정을 내리는 지 알아보겠습니다.

 

추가적으로 Flagger가 정상 메트릭 기준선을 가져올 수 있도록 Istio를 통해 서비스에 대한 부하를 만들어 보겠습니다.

간략한 시나리오

  • 카나리는 Carary 오브젝트에 설정한 대로 45초마다 진행될 것
  • 트래픽의 50%가 카나리로 이동할 때까지는 단계별로 10%씩 증가할 것
  • flagger가 메트릭에 문제가 없고 기준과 차이가 없다고 판단되면, 모든 트래픽이 카나리로 이동해 카나리가 기본 서비스로 승격 될 때까지 카나리가 진행 될 것
  • 만약 문제가 발생하면 flagger는 자동으로 카나리 릴리스를 롤백할 것
🔧 새로운 터미널에서 flagger 로그를 실시간으로 확인해줍니다.
$ kubectl logs -f deploy/flagger -n istio-system

결과
{"level":"info","ts":"2025-04-22T07:30:21.210Z","caller":"controller/events.go:33","msg":"New revision detected! Scaling up catalog.istioinaction","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-22T07:31:06.187Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-22T07:31:06.193Z","caller":"controller/events.go:33","msg":"Starting canary analysis for catalog.istioinaction","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-22T07:31:06.214Z","caller":"controller/events.go:33","msg":"Advance catalog-release.istioinaction canary weight 10","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-22T07:31:51.199Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-22T07:31:51.233Z","caller":"controller/events.go:33","msg":"Advance catalog-release.istioinaction canary weight 20","canary":"catalog-release.istioinaction"}
{"level":"info","ts":"2025-04-22T07:32:36.182Z","caller":"router/istio.go:414","msg":"Canary catalog-release.istioinaction uses HTTP service"}
{"level":"info","ts":"2025-04-22T07:32:36.215Z","caller":"controller/events.go:33","msg":"Advance catalog-release.istioinaction canary weight 30","canary":"catalog-release.istioinaction"}

🔧 새로운 터미널에서 flagger 상태확인 하도록 명령어 입력
$ kubectl get canary -n istioinaction -w

결과
NAME              STATUS        WEIGHT   LASTTRANSITIONTIME
catalog-release   Initialized   0        2025-04-22T06:28:06Z
catalog-release   Progressing   0        2025-04-22T07:30:21Z
catalog-release   Progressing   10       2025-04-22T07:31:06Z
catalog-release   Progressing   20       2025-04-22T07:31:51Z
catalog-release   Progressing   30       2025-04-22T07:32:36Z

🔧 이제 catalog-v2 deployment를 배포해보도록 하겠습니다.
$ 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

결과
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

 

위 점점 catalog-canary 쪽으로 트래픽 비율이 늘어나는 것을 확인 할 수 있습니다.

 

Prometheus에서도 확인해볼 수 있습니다.

 

이제 다음 실습에 앞서 설치 했던 것들을 삭제 하도록 하겠습니다.

🔧 canary, catalog, Flagger 삭제
$ kubectl delete canary catalog-release -n istioinaction
$ kubectl delete deploy catalog -n istioinaction
$ helm uninstall flagger -n istio-system

✅ Traffic Mirroring

앞서 실습한 요청 기준 라우팅, 트래픽 전환 기술을 사용하면 릴리스 할 때 위험을 낮출 수 있습니다.

 

다만, 두 기술 모두 라이브 트래픽과 요청을 사용하므로 사용자에게 100% 영향이 없지는 않습니다.

 

따라서 또 다른 기술인 Traffic Mirroring을 사용하면 실 서비스에는 영향 없이 트래픽 테스트를 진행할 수 있습니다.

 

Istio의 Traffic Mirroring(트래픽 미러링) 은 실제 사용자 요청을 기존 서비스로 처리하면서, 동시에 복사된 요청을 새 버전 서비스로 보내는 기능입니다.

 

이제 실습을 통해 자세히 알아보겠습니다.

🔧 catalog Deployment를 초기 상태로 돌리고, catalog-v2 를 별도의 Deployment로 배포
$ 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

🔧 반복 접속 호출
$ 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

🔧 미러링 수행을 위한 VS 확인
$ cat ch5/catalog-vs-v2-mirror.yaml
# 해당 VS는 catalog-v1으로 트래픽을 전부 보내지만, 동시에 v2 에도 미러링 해줍니다.
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
      
🔧 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

🔧 telemetries 확인
$ kubectl get telemetries -n istioinaction

결과
NAME      AGE
catalog   16s
webapp    5h49m

🔧 미러링 VS 설치
$ kubectl apply -f ch5/catalog-vs-v2-mirror.yaml -n istioinaction

🔧 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.244 ms
GET /items 200 0.244 ms - 502

🔧 v2 app 로그 확인
$ kubectl logs -n istioinaction -l app=catalog -l version=v2 -c catalog -f

# 결과를 확인해보면, 미러링 된 트래픽인 것을 확인 할 수 있도록
# v1의 결과와 다르게 GET catalog.istioinaction-shadow:80 shadow가 붙은 것을 확인 할 수 있습니다.
결과
request path: /items
blowups: {}
number of blowups: 0
GET catalog.istioinaction-shadow:80 /items 200 698 - 0.268 ms
GET /items 200 0.268 ms - 698

 

kiail Graph에서도 50:50 비율로 트래픽이 가는 것을 확인 할 수 있습니다.

 

이제 심화 과정으로 webapp과 catalog-v2 파드에서 패킷 덤프로 확인 해보겠습니다.

🔧 Istio 메시 내부망에서 모든 mTLS 통신 기능 끄기
$ cat < 10.10.0.20:3000 [AP] #346
GET /items HTTP/1.1.
host: catalog.istioinaction-shadow:80.
user-agent: beegoServer.
x-envoy-attempt-count: 1.
x-forwarded-for: 172.18.0.1,10.10.0.14.
x-forwarded-proto: http.
x-request-id: c9fb06ee-6a0f-4eff-a105-6ea244c0a44a.
accept-encoding: gzip.
x-envoy-internal: true.
x-envoy-decorator-operation: catalog.istioinaction.svc.cluster.local:80/*.
x-envoy-peer-metadata: ChoKDkFQUF9DT05UQUlORVJTEggaBndlYmFwcAoaCgpDTFVTVEVSX0lEEgwaCkt1YmVybmV0ZXMKHAoMSU5TVEFOQ0VfSVBTEgwaCjEwLjEwLjAuMTQKGQoNSVNUSU9fVkVSU0lPThIIGgYxLjE3LjgKowEKBkxBQkVMUxKYASqVAQoPCgNhcHASCBoGd2ViYXBwCiQKGXNlY3VyaXR5LmlzdGlvLmlvL3Rsc01vZGUSBxoFaXN0aW8KKwofc2VydmljZS5pc3Rpby5pby9jYW5vbmljYWwtbmFtZRIIGgZ3ZWJhcHAKLwojc2VydmljZS5pc3Rpby5pby9jYW5vbmljYWwtcmV2aXNpb24SCBoGbGF0ZXN0ChoKB01FU0hfSUQSDxoNY2x1c3Rlci5sb2NhbAogCgROQU1FEhgaFndlYmFwcC03Njg1YmNiODQtcnNraHcKHAoJTkFNRVNQQUNFEg8aDWlzdGlvaW5hY3Rpb24KUAoFT1dORVISRxpFa3ViZXJuZXRlczovL2FwaXMvYXBwcy92MS9uYW1lc3BhY2VzL2lzdGlvaW5hY3Rpb24vZGVwbG95bWVudHMvd2ViYXBwChcKEVBMQVRGT1JNX01FVEFEQVRBEgIqAAoZCg1XT1JLTE9BRF9OQU1FEggaBndlYmFwcA==.
x-envoy-peer-metadata-id: sidecar~10.10.0.14~webapp-7685bcb84-rskhw.istioinaction~istioinaction.svc.cluster.local.
x-b3-traceid: 49042ff58a94033c98bff28b1679e771.
x-b3-spanid: 4598f646005ba1f6.
x-b3-parentspanid: ceef50eb5239bfe7.
x-b3-sampled: 0.

응답 내용
T 2025/04/22 08:10:32.770098 10.10.0.20:3000 -> 10.10.0.14:49160 [AP] #347
HTTP/1.1 200 OK.
x-powered-by: Express.
vary: Origin, Accept-Encoding.
access-control-allow-credentials: true.
cache-control: no-cache.
pragma: no-cache.
expires: -1.
content-type: application/json; charset=utf-8.
content-length: 698.
etag: W/"2ba-8igEisu4O69h8jWIFgUqgmp7D5o".
date: Tue, 22 Apr 2025 08:10:32 GMT.
x-envoy-upstream-service-time: 1.
x-envoy-peer-metadata: ChsKDkFQUF9DT05UQUlORVJTEgkaB2NhdGFsb2cKGgoKQ0xVU1RFUl9JRBIMGgpLdWJlcm5ldGVzChwKDElOU1RBTkNFX0lQUxIMGgoxMC4xMC4wLjIwChkKDUlTVElPX1ZFUlNJT04SCBoGMS4xNy44CrIBCgZMQUJFTFMSpwEqpAEKEAoDYXBwEgkaB2NhdGFsb2cKJAoZc2VjdXJpdHkuaXN0aW8uaW8vdGxzTW9kZRIHGgVpc3RpbwosCh9zZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1uYW1lEgkaB2NhdGFsb2cKKwojc2VydmljZS5pc3Rpby5pby9jYW5vbmljYWwtcmV2aXNpb24SBBoCdjIKDwoHdmVyc2lvbhIEGgJ2MgoaCgdNRVNIX0lEEg8aDWNsdXN0ZXIubG9jYWwKJQoETkFNRRIdGhtjYXRhbG9nLXYyLTZkZjg4NWI1NTUtdnhuemwKHAoJTkFNRVNQQUNFEg8aDWlzdGlvaW5hY3Rpb24KVAoFT1dORVISSxpJa3ViZXJuZXRlczovL2FwaXMvYXBwcy92MS9uYW1lc3BhY2VzL2lzdGlvaW5hY3Rpb24vZGVwbG95bWVudHMvY2F0YWxvZy12MgoXChFQTEFURk9STV9NRVRBREFUQRICKgAKHQoNV09SS0xPQURfTkFNRRIMGgpjYXRhbG9nLXYy.
x-envoy-peer-metadata-id: sidecar~10.10.0.20~catalog-v2-6df885b555-vxnzl.istioinaction~istioinaction.svc.cluster.local.
server: istio-envoy.
.
[
  {
    "id": 1,
    "color": "amber",
    "department": "Eyewear",
    "name": "Elinor Glasses",
    "price": "282.00",
    "imageUrl": "http://lorempixel.com/640/480"
  },
  {
    "id": 2,
    "color": "cyan",
    "department": "Clothing",
    "name": "Atlas Shirt",
    "price": "127.00",
    "imageUrl": "http://lorempixel.com/640/480"
  },
....

 

해당 내용을 살펴보자면,

  • 요청은 webapp(10.10.0.14)에서 보냄
  • 답장은 catalog-v2(10.10.0.20)에서 하지만 해당 답장은 사용자로 보내지지 않음
  • 즉, 사용자는 미러링된 파드에서 응답을 받는 것이 아닌, Primary 파드에서 응답을 받음

Istio의 Service Discovery

이번에 알아 볼 것은 Istio의 Service Discovery 입니다.

 

Istio의 Service Discovery를 자세히 알아보기 전, 왜 알아야 하는지 먼저 살펴보겠습니다.

Service Discovery를 알아야 하는 이유

기본적으로, 이스티오는 트래픽이 서비스 메시 밖으로 향하는 것을 허용합니다.

예를 들어, 애플리케이션이 서비스 메시가 관리하지 않는 외부의 웹 사이트나 서비스와 통신하려고 시도하면 이스티오는 트래픽이 나가도록 허용합니다.

모든 트래픽은 먼저 서비스 메시 사이드카 프록시(이스티오 프록시)를 거치므로 트래픽 라우팅을 제어할 수 있고, 이스티오의 기본 정책을 바꿔 어떤 트래픽도 메시를 떠날 수 없게 거부할 수 있습니다.

어떤 트래픽도 메시를 떠날 수 없게 막는 것은, 메시 내 서비스나 애플리케이션이 손상됐을 때 악의적인 공격자가 자신의 집으로 연락하는 것을 방지하기 위한 기본적인 심층 방어 태세 입니다.

그렇지만 외부 트래픽이 이스티오를 사용할 수 없게 하는 것만으로는 충분하지 않습니다.

손상된 파드는 프록시를 우회할 수 있기 때문입니다.

그러므로 3계층 및 4계층 보호 같은 추가적인 트래픽 차단 매커니즘을 갖춘 심층 방어 접근법이 필요합니다.

예를 들어 취약점 때문에 공격자가 특정 서비스를 제어할 수 있다면, 공격자는 자신이 제어하는 서버에 도달할 수 있도록 코드 주입이나 서비스 조작을 시도할 수 있습니다.

공격자가 자신이 제어하는 서버에 도달할 수 있고 손상된 서비스를 더 제어할 수 있다면, 회사의 민감 데이터와 지적 재산권을 탈취할 수 있습니다.

이러한 일들을 방지하기 위해 프록시 우회를 고려한 심층 방어 전략이 함께 필요합니다.

 

이제 간단한 실습을 해보도록 하겠습니다.

 

위 그림 처럼 외부 트래픽을 차단하도록 Istio를 설정해 메시에 간단한 보호 계층을 더해보겠습니다.

🔧 현재 istiooperators meshConfig 설정 확인
$ kubectl get istiooperators -n istio-system -o json

결과
"meshConfig": {
                    "defaultConfig": {
                        "proxyMetadata": {}
                    },
                    "enablePrometheusMerge": true
                }
                
                
🔧 새로운 터미널에서 webapp Envoy 프록시 로그 확인
$ kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f

webapp 파드에서 외부 파일 다운로드 시 아래 로그 발생
[2025-04-22T08:44:54.207Z] "- - -" 0 - - - "-" 556 11690 81 - "-" "-" "-" "-" "185.199.108.133:443" PassthroughCluster 10.10.0.14:43518 185.199.108.133:443 10.10.0.14:43506 - -

🔧 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.108.133:443)
saving to 'sock-shop-demo.yaml'
sock-shop-demo.yaml  100% |********************************************************************************************************************************| 17782  0:00:00 ETA
'sock-shop-demo.yaml' saved

🔧 Istio의 outboundTrafficPolicy.mode를 REGISTRY_ONLY 변경
$ docker exec -it myk8s-control-plane bash
-------------
istioctl install --set profile=default --set meshConfig.outboundTrafficPolicy.mode=REGISTRY_ONLY
y

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

🔧 다시 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.108.133:443)
wget: error getting response: Connection reset by peer
command terminated with exit code 1

 

이제 Istio의 서비스 디스커버리 기능 중 하나인 ServiceEntry 에 대해 알아보겠습니다.

🔍 ServiceEntry란?

ServiceEntry는 Istio 서비스 메시 내부에서 외부 서비스(클러스터 외부 혹은 메시 외부)에 대한 접근 경로를 등록해주는 리소스입니다.

등록된 외부 서비스는 메시 내부에서 일반적인 Kubernetes 서비스처럼 인식되고 제어 가능해집니다.

 

이제 실습을 통해서 좀 더 자세히 알아보겠습니다.

🔧 forum 설치
$ kubectl apply -f services/forum/kubernetes/forum-all.yaml -n istioinaction

🔧 forum 설치 확인
$ kubectl get deploy,svc -n istioinaction -l app=webapp
$ docker exec -it myk8s-control-plane istioctl proxy-status

결과
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/webapp   1/1     1            1           7h42m

NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/webapp   ClusterIP   10.200.1.114   <none>        80/TCP    7h42m
NAME                                                  CLUSTER        CDS        LDS        EDS        RDS        ECDS         ISTIOD                      VERSION
catalog-6d5b9bbb66-c9p5f.istioinaction                Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-5kcq4     1.17.8
catalog-v2-6df885b555-vxnzl.istioinaction             Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-5kcq4     1.17.8
istio-ingressgateway-996bc6bb6-hmtnj.istio-system     Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-5kcq4     1.17.8
webapp-7685bcb84-rskhw.istioinaction                  Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-5kcq4     1.17.8

🛠️ 메시 안에서 새로운 포럼 서비스 호출
$ curl -s http://webapp.istioinaction.io:30000/api/users

결과
error calling Forum service

🛠️ forum 로그 확인
$ kubectl logs -n istioinaction -l app=webapp -c istio-proxy -f

결과
[2025-04-23T00:43:37.918471Z] "GET /users HTTP/1.1" 502 - direct_response - "-" 0 0 0 - "172.18.0.1" "Go-http-client/1.1" "04bef923-b182-94e9-a58d-e2d9f957693b" "jsonplaceholder.typicode.com" "-" - - 104.21.48.1:80 172.18.0.1:0 - block_all
# 클러스터 내부 서비스에서 외부 도메인(jsonplaceholder.typicode.com) 으로 나가려 했지만, Istio가 요청을 막아서 502 오류와 함께 직접 응답 처리한 상황
## direct_response : Envoy가 요청을 외부로 보내지 않고 자체적으로 차단 응답을 반환했음을 의미
## block_all : Istio에서 egress(외부) 요청이 전면 차단됨을 나타내는 메시지
[2025-04-23T00:43:38.331092Z] "GET /api/users HTTP/1.1" 500 - via_upstream - "-" 0 28 0 0 "172.18.0.1" "beegoServer" "04bef923-b182-94e9-a58d-e2d9f957693b" "forum.istioinaction:80" "10.10.0.31:8080" inbound|8080|| 127.0.0.6:60487 10.10.0.31:8080 172.18.0.1:0 - default

 

kiail Graph에서 보면 아래와 같습니다.

 

이제 외부 도메인(jsonplaceholder.typicode.com)으로 나가는 트래픽을 허용하기 위해 Istio에 ServiceEntry 리소스를 생성 해줍니다.

 

위 그림처럼 ServiceEntry 리소스를 만들면,

  • Istiod(컨트롤 플레인) 가 알게되고
  • Istiod가 해당 정보를 xDS API를 통해 Envoy Proxy(사이드카) 에게 전달
  • Envoy Proxy는 route, cluster, endpoint 정보에 이를 반영하여 외부 트래픽을 허용하고 라우팅 가능하게 설정 해줍니다.
🛠️ ServiceEntry 리소스 생성
# jsonplaceholder.typicode.com 해당 도메인에 대해 외부로 트래픽을 허용해줍니다.
$ 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

🛠️ forum 의 Envoy 프록시에 잘 적용되었는지 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config all deploy/forum.istioinaction -o short

결과
SERVICE FQDN                                            PORT      SUBSET         DIRECTION     TYPE             DESTINATION RULE
jsonplaceholder.typicode.com                            80        -              outbound      STRICT_DNS

🛠️ 목적 외부 도메인에 대한 응답 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

 

그리고 URL에 해당 IP를 입력해보면 웹 페이지가 나옵니다.

🛠️ 또한 curl 명령어로 확인해보면 앞서 에러 결과와 다르게 사용자 목록을 정상적으로 호출합니다.
$ curl -s http://webapp.istioinaction.io:30000/api/users

결과
[{"id":1,"name":"Leanne Graham","username":"Bret","email":"Sincere@april.biz","address":{"street":"Kulas Light","suite":"Apt. 556","city":"Gwenborough","zipcode":"92998-3874"},"phone":"1-770-736-8031 x56442","website":"hildegard.org","company":{"name":"Romaguera-Crona","catchPhrase":"Multi-layered client-server neural-net","bs":"harness real-time e-markets"}},{"id":2,"name":"Ervin Howell","username":"Antonette","email":"Shanna@melissa.tv","address":{"street":"Victor Plains","suite":"Suite 879","city":"Wisokyburgh","zipcode":"90566-7771"},"phone":"010-692-6593 x09125","website":"anastasia.net","company":{"name":"Deckow-Crist","catchPhrase":"Proactive didactic contingency","bs":"synergize scalable supply-chains"}},{"id":3,"name":"Clementine Bauch","username":"Samantha","email":"Nathan@yesenia.net","address":{"street":"Douglas Extension","suite":"Suite 847","city":"McKenziehaven","zipcode":"59590-4157"},"phone":"1-463-123-4447","website":"ramiro.info","company":{"name":"Romaguera-Jacobson","catchPhrase":"Face to face bifurcated interface","bs":"e-enable strategic applications"}},{"id":4,"name":"Patricia Lebsack","username":"Karianne","email":"Julianne.OConner@kory.org","address":{"street":"Hoeger Mall","suite":"Apt. 692","city":"South Elvis","zipcode":"53919-4257"},"phone":"493-170-9623 x156","website":"kale.biz","company":{"name":"Robel-Corkery","catchPhrase":"Multi-tiered zero tolerance productivity","bs":"transition cutting-edge web services"}},{"id":5,"name":"Chelsey Dietrich","username":"Kamren","email":"Lucio_Hettinger@annie.ca","address":{"street":"Skiles Walks","suite":"Suite 351","city":"Roscoeview","zipcode":"33263"},"phone":"(254)954-1289","website":"demarco.info","company":{"name":"Keebler LLC","catchPhrase":"User-centric fault-tolerant solution","bs":"revolutionize end-to-end systems"}},{"id":6,"name":"Mrs. Dennis Schulist","username":"Leopoldo_Corkery","email":"Karley_Dach@jasper.info","address":{"street":"Norberto Crossing","suite":"Apt. 950","city":"South Christy","zipcode":"23505-1337"},"phone":"1-477-935-8478 x6430","website":"ola.org","company":{"name":"Considine-Lockman","catchPhrase":"Synchronised bottom-line interface","bs":"e-enable innovative applications"}},{"id":7,"name":"Kurtis Weissnat","username":"Elwyn.Skiles","email":"Telly.Hoeger@billy.biz","address":{"street":"Rex Trail","suite":"Suite 280","city":"Howemouth","zipcode":"58804-1099"},"phone":"210.067.6132","website":"elvis.io","company":{"name":"Johns Group","catchPhrase":"Configurable multimedia task-force","bs":"generate enterprise e-tailers"}},{"id":8,"name":"Nicholas Runolfsdottir V","username":"Maxime_Nienow","email":"Sherwood@rosamond.me","address":{"street":"Ellsworth Summit","suite":"Suite 729","city":"Aliyaview","zipcode":"45169"},"phone":"586.493.6943 x140","website":"jacynthe.com","company":{"name":"Abernathy Group","catchPhrase":"Implemented secondary concept","bs":"e-enable extensible e-tailers"}},{"id":9,"name":"Glenna Reichert","username":"Delphine","email":"Chaim_McDermott@dana.io","address":{"street":"Dayna Park","suite":"Suite 449","city":"Bartholomebury","zipcode":"76495-3109"},"phone":"(775)976-6794 x41206","website":"conrad.com","company":{"name":"Yost and Sons","catchPhrase":"Switchable contextually-based project","bs":"aggregate real-time technologies"}},{"id":10,"name":"Clementina DuBuque","username":"Moriah.Stanton","email":"Rey.Padberg@karina.biz","address":{"street":"Kattie Turnpike","suite":"Suite 198","city":"Lebsackbury","zipcode":"31428-2261"},"phone":"024-648-3804","website":"ambrose.net","company":{"name":"Hoeger LLC","catchPhrase":"Centralized empowering task-force","bs":"target end-to-end models"}}]

🛠️ 반복 접속 명령어 실행
$ while true; do curl -s http://webapp.istioinaction.io:30000/ ; date "+%Y-%m-%d %H:%M:%S" ; sleep 2; echo; done

 

반복 접속 명령어를 실행하고 kiail Graph에서 확인해보면 정상적으로 트래픽이 가는 것을 확인 할 수 있습니다.

 

다음 글에서는 Istio의 Resilience에 대해 알아보겠습니다.

 

감사합니다.

반응형