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에 대해 알아보겠습니다.
감사합니다.
'스터디 > Istio' 카테고리의 다른 글
Istio 스터디 4주차 - <Observability> (0) | 2025.04.30 |
---|---|
Istio 스터디 3주차 - <Resilience> (0) | 2025.04.25 |
Istio 스터디 2주차 - <Envoy와 Istio Gateway 실습> (0) | 2025.04.18 |
Istio 스터디 2주차 - <Envoy 란?> (0) | 2025.04.16 |
Istio 스터디 1주차 - <Istio 실습 해보기> (1) | 2025.04.09 |