Search

Service Mesh: Istio 실습

Tags
k8s
network
service-mesh
istio
Created
2024/10/17 19:01
Created time
2024/10/17 10:01
category
kubernetes

개요

Istio 실습을 수행하며 이론에서 학습한 내용을 구체적으로 이해
실습 환경 사전 구축은 여기를 참고
반드시 실습을 마치면 AWS의 CloudFormation으로 배포한 인스턴스를 삭제하고, /etc/hosts에 추가한 값들을 정리

Istio 간단 구축

실습 환경은 아래와 같으며, Istio Operator로 설치하는 방식은 Deprecated여서 Sail Operator를 이용
k3s 1.30.4
Ubuntu 22.04
Flannel CNI
iptables proxy mode
istio 1.23.2 (Envoy 1.32.2)

istioctl 폴더 구조

istio |____ bin | |____ istioctl | |____ manifests | |____ examples | |____ profiles | | |____ default.yaml | | |____ demo.yaml | | |____ empty.yaml | | |____ minimal.yaml | | |____ openshift.yaml | | |____ preview.yaml | | |____ remote.yaml | |____ charts | |____ tools | |____ samples | |____ manifest.yaml | |____ README.md
Plain Text
복사
demo 프로필은 기본적으로 상세하게 로그 출력을 지원

istioctl 설치

컨트롤 플레인에서 istioctl을 설치
# 설치 export ISTIOV=1.23.2 echo "export ISTIOV=1.23.2" >> /etc/profile curl -s -L https://istio.io/downloadIstio | ISTIO_VERSION=$ISTIOV TARGET_ARCH=x86_64 sh - cp istio-$ISTIOV/bin/istioctl /usr/local/bin/istioctl # 확인 tree istio-$ISTIOV -L 2 istioctl version --remote=false
Shell
복사

프로필 수정

실습 환경에서 복잡성을 낮취기 위해 프로필을 수정
# 프로필 목록 조회 istioctl profile list # manifest를 생성 istioctl profile dump # 특정 프로필의 manifest를 생성 # istioctl profile dump <profile> istioctl profile dump default istioctl profile dump demo # 특정 config 경로만 확인 # istioctl profile dump --configu-path <path> istioctl profile dump --config-path components.ingressGateways istioctl profile dump --config-path values.gateways.istio-ingressgateway # --- # 프로필에 해당하는 manifest를 작성 istioctl profile dump demo > demo-profile.yaml # egressGateways를 비활성화 # -------------------- # egressGateways: # - enabled: false # -------------------- vi demo-profile.yaml # 수정된 프로필을 설치 istioctl install -f demo-profile.yaml -y
Shell
복사
# istio-ingressgateway를 NodePort로 변경 kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec":{"type":"NodePort"}}'
Shell
복사

설치 확인

# istio-system 네임스페이스 리소스 확인 kubectl get all,svc,ep,sa,cm,secret,pdb -n istio-system
Shell
복사
# istio CRD 확인 kubectl get crd | grep istio.io | sort
Shell
복사
# istio-ingressgateway의 Envoy 버전 확인 kubectl exec -it deploy/istio-ingressgateway -n istio-system -c istio-proxy -- envoy --version
Shell
복사
# istio-ingressgateway Service 확인 kubectl get svc,ep -n istio-system istio-ingressgateway ## istio-ingressgateway Service 포트 정보 확인 kubectl get svc -n istio-system istio-ingressgateway -o jsonpath={.spec.ports[*]} | jq
Shell
복사
# istiod의 컨트롤 플레인 디플로이먼트 정보 확인 kubectl exec -it deployment.apps/istiod -n istio-system -- ss -tnlp kubectl exec -it deployment.apps/istiod -n istio-system -- ss -tnp kubectl exec -it deployment.apps/istiod -n istio-system -- ps -ef # istio-ingressgateway 디플로이먼트 정보 확인 kubectl exec -it deployment.apps/istio-ingressgateway -n istio-system -- ss -tnlp kubectl exec -it deployment.apps/istio-ingressgateway -n istio-system -- ss -tnp kubectl exec -it deployment.apps/istio-ingressgateway -n istio-system -- ps -ef
Shell
복사

변수 지정

# kans-s 컨트롤 플레인 노드 # istio-ingressgateway의 NodePort 변수 지정 export IGWHTTP=$(kubectl get service -n istio-system istio-ingressgateway -o jsonpath='{.spec.ports[1].nodePort}') echo $IGWHTTP # 도메인 이름 변수 지정 # www.jseo.dev는 적절한 도메인으로 교체 # 192.168.10.102는 istio-ingressgateway Pod가 존재하는 워커 노드의 ip 주소 export MYDOMAIN=www.jseo.dev echo -e "192.168.10.102 $MYDOMAIN" >> /etc/hosts echo -e "export MYDOMAIN=$MYDOMAIN" >> /etc/profile # 접속 시도 시 아직 설정이 없어서 실패 curl -v -s $MYDOMAIN:$IGWHTTP
Shell
복사
# testpc 노드 # 컨트롤 플레인에서 확인했던 istio-ingressgateway의 NodePort 변수 지정 export IGWHTTP=<nodeport> echo $IGWHTTP # 도메인 이름 변수 지정 # www.jseo.dev는 적절한 도메인으로 교체 # 192.168.10.102는 istio-ingressgateway Pod가 존재하는 워커 노드의 ip 주소 export MYDOMAIN=www.jseo.dev echo -e "192.168.10.102 $MYDOMAIN" >> /etc/hosts echo -e "export MYDOMAIN=$MYDOMAIN" >> /etc/profile # 접속 시도 시 아직 설정이 없어서 실패 curl -v -s $MYDOMAIN:$IGWHTTP
Shell
복사
# 로컬 pc # EC2 인스턴스 퍼블릭 IP 주소 확인 aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text # IP주소와 NodePort를 변수 지정 IGWHTTP=<nodeport> echo $IGWHTTP ISTIONODEIP=<control-plane-public-ip> echo $ISTIONODEIP # 도메인 이름 변수 지정 MYDOMAIN=www.jseo.dev echo "$ISTIONODEIP $MYDOMAIN" | sudo tee -a /etc/hosts # 접속 시도 시 아직 설정이 없어서 실패 curl -v -s $MYDOMAIN:$IGWHTTP
Shell
복사

외부 접속 설정

Auto Injection

# Auto Injection with Namespace Label # Mutating Admission Webhook Controller를 사용하여 특정 네임스페이스 생성되는 모든 Pod들은 istio의 Sidecar가 자동으로 주입되도록 설정 # 아래 설정을 해주지 않으면 실습에서 Deployment를 생성하더라도 Pod 내애 컨테이너가 주입되지 않으므로 반드시 설정 kubectl label namespace default istio-injection=enabled # 레이블 설정 확인 kubectl get ns -L istio-injection
Shell
복사
# ServiceAccount 및 Service manifest 생성 cat << EOT | kubectl create -f - apiVersion: v1 kind: ServiceAccount metadata: name: kans-nginx --- apiVersion: apps/v1 kind: Deployment metadata: name: deploy-websrv spec: replicas: 1 selector: matchLabels: app: deploy-websrv template: metadata: labels: app: deploy-websrv spec: serviceAccountName: kans-nginx terminationGracePeriodSeconds: 0 containers: - name: deploy-websrv image: nginx:alpine ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: svc-clusterip spec: ports: - name: svc-webport port: 80 targetPort: 80 selector: app: deploy-websrv type: ClusterIP EOT
Shell
복사
# Sidecar 확인 # Init 컨테이너가 istio-init 과정에서 Pod 내에 iptables 세팅 후 종료 # Pod 내에 istio-proxy라는 컨테이너가 생성 kubectl get po,svc,ep,sa -o wide kc describe po/<deployment-pod-name> | grep "Container ID" -B3 -A3 | grep istio-proxy -A3
Shell
복사

Istio Gateway 및 VirtualService 설정

Gateway : 지정한 Ingress Gateway로부터 트래픽이 인입되도록 Protocol, Port, Hosts, Proxy 등 설정 가능
VirtualService : 인입 처리할 Hosts 설정하여 L7 PATH 별 라우팅을 지원하고, Envoy Route Config를 통해 목적지에 대한 정책을 설정 가능
Istio v1 API는 여기를 참고
# Gateway 및 VirtualService 생성 cat << EOT | kubectl create -f - apiVersion: networking.istio.io/v1 kind: Gateway metadata: name: test-gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - "*" --- apiVersion: networking.istio.io/v1 kind: VirtualService metadata: name: nginx-service spec: hosts: - "$MYDOMAIN" gateways: - test-gateway http: - route: - destination: host: svc-clusterip port: number: 80 EOT
Shell
복사
# Istio의 Gateway와 VirtualService 설명 확인 kc explain gateways.networking.istio.io kc explain virtualservices.networking.istio.io # istio api-resources를 확인 kubectl api-resources | grep istio # gw, vs 조회 # vs는 특이하게도 다른 네임스페이스에 있는 Service를 참조할 수도 있음 kubectl get gw,vs
Shell
복사
# Mesh에 존재하는 각 Envoy와 istiod간의 xDS 동기화 관련 last sent, last acknowledged를 확인 가능 istioctl proxy-status istioctl ps
Shell
복사

접속 확인

# 이전에는 curl 접속 실패했었지만 로컬 pc와 testpc노드에서 curl 시도 시 성공 curl -s $MYDOMAIN:$IGWHTTP | grep -o "<title>.*</title>" # 퍼블릭 IP로 접속해도 정상 동작 curl -v -s <public-ip>:$IGWHTTP
Shell
복사
# proxy 이름 확인 istioctl proxy-status | grep deploy # Envoy Config (all, cluster, endpoint, listener) 등을 dump istioctl proxy-config all <proxy-name> -o json | jq
Shell
복사
# Deployment 내부에 istio-proxy 사용자가 존재함을 확인 kubectl exec -it deploy/deploy-websrv -c istio-proxy -- tail -n 3 /etc/passwd # istiod와 istio-proxy의 커넥션 확인 # istiod와 istio-proxy가 사용하는 포트 정보는 아래 링크 확인 # https://istio.io/latest/docs/ops/deployment/application-requirements/ kubectl exec -it deploy/istiod -n istio-system -- netstat -antp kubectl exec -it deploy/deploy-websrv -c istio-proxy -- netstat -antp
Shell
복사
istiod와 istio-proxy 포트
istiod / istio-proxy 포트 사용 테이블
istiod / istio-proxy의 구조적 관점 포트 사용 정보

UDS 통신 확인

istio-proxy와 Envoy 프로세스 간 UDS 통신을 수행하게 되는데, 이에 대해 확인
UDS는 여기를 참고 / xDS는 여기를 참고
xDS는 CDS(Clusters), LDS(Listeners), EDS(Endpoints), RDS(Routes)이고, DS는 Discovery Service를 의미
ADS는 xDS들을 종합하는 개념으로 각 영역의 설정을 동적 로드하여 istiod와 통신하는데, 이 때 사용되는 xDS는 소켓 타입
# Deployment 접속 kubectl exec -it deploy/deploy-websrv -c istio-proxy -- bash # XDS는 소켓 타입 ls -al /etc/istio/proxy # UDS의 Listener와 ESTAB 상태 정보 확인 ss -xpl ss -xp
Shell
복사

Pod 디버깅

Pod 디버깅은 Ephemeral 컨테이너를 확인

리소스 삭제

kubectl delete gw,vs,deploy,svc --all
Shell
복사

기능 확인

기능 확인은 istio의 공식 문서에서 제공하는 실습을 이용
Product Page에서 요청을 받고 Reviews와 Details 결과를 사용자에게 반환
요청은 도서 리뷰를 보여주는 Reviews Service와 도서 상세 정보를 보여주는 Details Service로 전달
Reviews Service는 v1, v2, v3 세 개의 버전이 있고 v2, v3 버전의 경우 추가로 Ratings Service에 접속하여 도서에 대한 5단계 평가를 읽음
Reviews Service의 차이는, v1은 Rating 이 없고, v2는 검은색 별로 Ratings 가 표시되며, v3는 색깔이 있는 별로 Ratings를 표시

서버 구조

Service 구조에서 Istio를 적용하게 되면 이론과 실습으로 이해한 바대로 위와 같은 그림의 구조를 갖게 됨

Service 셋업

# istioctl 버전 변수 지정 export ISTIOV=1.23.2 echo $ISTIOV # 실습 환경에 내장되어 있는 리소스 확인 cat ~/istio-$ISTIOV/samples/bookinfo/platform/kube/bookinfo.yaml # 리소스 생성 kubectl apply -f ~/istio-$ISTIOV/samples/bookinfo/platform/kube/bookinfo.yaml
Shell
복사

Service 확인

# 리소스 및 ServiceAccount 조회 kubectl get all,sa # ProductPage 접속 확인 kubectl exec "$(kubectl get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}')" -c ratings -- curl -sS productpage:9080/productpage | grep -o "<title>.*</title>"
Shell
복사

외부 접속 설정

# Istio의 Gateway와 VirtualService 설정 # manifest 확인 cat ~/istio-$ISTIOV/samples/bookinfo/networking/bookinfo-gateway.yaml # manifest 적용 kubectl apply -f ~/istio-$ISTIOV/samples/bookinfo/networking/bookinfo-gateway.yaml # 리소스 확인 kubectl get gw,vs
Shell
복사
서버 구조의 Ingress Gateway에서 Product Page로 트래픽이 가기 전에 Gateway와 VirtualService를 거치게 되는데, 위 도표에 기재된 흐름으로 처리
bookinfo-gateway는 모든 호스트를 처리하고, 경로 매칭으로는 /productpage, /static, /login, /logout, /api/v1/products를 지원하며, 경로가 매칭되면 일단 productpage라는 Service의 9080 포트로 라우팅
# 로컬 pc # 각 노드의 퍼블릭 주소를 확인 aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
Shell
복사
# k3s-s 컨트롤 플레인에서 접속 확인 # NodePort를 변수에 할당 export IGWHTTP=$(kubectl get service -n istio-system istio-ingressgateway -o jsonpath='{.spec.ports[1].nodePort}') echo $IGWHTTP # 도메인 이름을 변수에 할당 export MYDOMAIN=www.jseo.dev echo $MYDOMAIN # /etc/hosts를 열고 클러스터의 각 노드를 퍼블릭 IP로 변경 # $MYDOMAIN의 엔트리도 istio-ingressgateway가 있는 워커 노드의 주소로 변경해야 정상 동작 vi /etc/hosts # 클러스터의 각 노드로 접속 확인 # 로컬 호스트와 istio-ingressgateway가 존재하는 주소로는 응답이 정상적으로 오고, 그 외 주소로는 응답 실패 curl -s http://localhost:$IGWHTTP/productpage | grep "Book Details" curl -s --max-time 3 http://192.168.10.101:$IGWHTTP/productpage | grep "Book Details" curl -s --max-time 3 http://192.168.10.102:$IGWHTTP/productpage | grep "Book Details" # 도메인 이름으로 접속 확인 # 설정해둔 퍼블릭 IP로 정상 요청되어 응답 확인 curl -s http://$MYDOMAIN:$IGWHTTP/productpage | grep "Book Details"
Shell
복사
# testpc 노드에서 접속 확인 # NodePort를 변수에 할당 export IGWHTTP=<nodeport> echo $IGWHTTP # 도메인 이름을 변수에 할당 export MYDOMAIN=www.jseo.dev echo $MYDOMAIN # /etc/hosts를 열고 클러스터의 각 노드를 퍼블릭 IP로 변경 # $MYDOMAIN의 엔트리도 istio-ingressgateway가 있는 워커 노드의 주소로 변경해야 정상 동작 vi /etc/hosts # 도메인 이름으로 접속 확인 # 설정해둔 퍼블릭 IP로 정상 요청되어 응답 확인 curl -s http://$MYDOMAIN:$IGWHTTP/productpage | grep "Book Details"
Shell
복사
# 로컬 pc에서 접속 확인 # NodePort를 변수에 할당 export IGWHTTP=<nodeport> echo $IGWHTTP # 도메인 이름을 변수에 할당 export MYDOMAIN=www.jseo.dev echo $MYDOMAIN # /etc/hosts를 열고 클러스터의 각 노드를 퍼블릭 IP로 변경 # $MYDOMAIN의 엔트리도 istio-ingressgateway가 있는 워커 노드의 주소로 변경해야 정상 동작 vi /etc/hosts # 도메인 이름으로 접속 확인 # 설정해둔 퍼블릭 IP로 정상 요청되어 응답 확인 curl -s http://$MYDOMAIN:$IGWHTTP/productpage | grep "Book Details"
Shell
복사
<node-ip>:<nodeport> 접속 시 버전에 따라 Reviews의 Ratings가 다른 것도 확인 가능

모니터링

모니터링에서는 주로 Kiali를 사용하며, 이에 대한 주 데이터 소스는 Prometheus 혹은 Jaeger를 사용 (대시보드는 Grafana)
Jaeger를 이용하는 경우 분산 트레이싱이 가능하며, Pod 자체의 헬스 체크는 Kiali가 직접 istiod에 TCP 15014 포트를 통해 수행
# 실습 확녕에 있는 애드온 파일을 확인하고, 이를 생성 및 배포 tree ~/istio-$ISTIOV/samples/addons/ kubectl apply -f ~/istio-$ISTIOV/samples/addons kubectl rollout status deployment/kiali -n istio-system
Shell
복사
# 리소스 확인 kubectl get all,sa,cm -n istio-system kubectl get svc,ep -n istio-system # Kiali Service를 NodePort로 변경 kubectl patch svc -n istio-system kiali -p '{"spec":{"type":"NodePort"}}' # Grafana Service를 Nodeport로 변경 kubectl patch svc -n istio-system grafana -p '{"spec":{"type":"NodePort"}}' # Prometheus Service를 NodePort로 변경 kubectl patch svc -n istio-system prometheus -p '{"spec":{"type":"NodePort"}}'
Shell
복사
# Kiali 웹 접속 주소 확인 KIALINodePort=$(kubectl get svc -n istio-system kiali -o jsonpath={.spec.ports[0].nodePort}) echo -e "KIALI UI URL = http://$(curl -s ipinfo.io/ip):$KIALINodePort" # Grafana 웹 접속 주소 확인 GRAFANANodePort=$(kubectl get svc -n istio-system grafana -o jsonpath={.spec.ports[0].nodePort}) echo -e "Grafana URL = http://$(curl -s ipinfo.io/ip):$GRAFANANodePort" # Prometheus 웹 접속 주소 확인 PROMENodePort=$(kubectl get svc -n istio-system prometheus -o jsonpath={.spec.ports[0].nodePort}) echo -e "Prometheus URL = http://$(curl -s ipinfo.io/ip):$PROMENodePort"
Shell
복사
Kiali의 트래픽 그래프에서 Traffic Distribution과 Traffic Animation, Security를 켠 상태로 아래 명령어를 날려서 확인해보면 첨부된 스크린샷과 동일한 결과를 얻을 수 있음
while true; do curl -s $MYDOMAIN:$IGWHTTP/productpage | grep -o "<title>.*</title>" ; echo "--------------" ; sleep 0.1; done
Shell
복사
Application과 Service 측면에서의 정보 획득 가능
Workloads에서 istio-proxy와 app의 로그와 Envoy (Clusters, Listeners, Routes, Config) 정보 확인 가능
Istio Config에서 Istio 관련 설정 정보 확인 가능하며, Action으로 Istio 관련 오브젝트를 설정 및 삭제 가능

트래픽 컨트롤

트래픽 컨트롤은 VirtualService와 DestinationRule을 통해 동작
클라이언트 → Istio Ingress Gateway → Gateway, VirtualService + DestinationRule → Cluster (Endpoint to Pod)
DestinationRule은 실제 도착지 (Service와 1:1 연결)의 부하 분산, 커넥션 옵션, 서킷 브레이킹 등 정교한 정책 설정 가능
Gateway와 VirtualService 설명은 여기를 참고

Request Routing

Request Routing은 Service로 들어오는 트래픽을 적절히 라우팅 하는 기능을 의미
# 샘플 파일 확인 cd ~/istio-$ISTIOV/samples/bookinfo/networking # 기본 DestinationRule 적용 kubectl apply -f destination-rule-all.yaml # DestinationRule 확인 kubectl get dr
Shell
복사
# 각 Service에 대해 v1의 서브셋이 되도록 VirtualService 적용 kubectl apply -f virtual-service-all-v1.yaml # 아래 VirtualService에 따라 Kiali에서 Reviews의 v2, v3가 사라지는 것 확인 kubectl get vs
Shell
복사
# 특정 유저가 접속 시 v2로 라우팅 되도록 VirtualService 적용 kubectl apply -f virtual-service-reviews-test-v2.yaml # 아래 VirtualService에 따라 ProductPage에서 jason으로 접속한 사용자는 v2로만 접속 kubectl get vs
Shell
복사
Fault Injection은 의도적으로 통신에 지연을 발생 시키거나 중단하는 것
# VirtualService 적용하여 jason 사용자 접속 시 7초 지연 발생 kubectl apply -f virtual-service-ratings-test-delay.yaml
Shell
복사
# VirtualService를 적용하여 jason 사용자 접속 시 500 응답 반환 kubectl apply -f virtual-service-ratings-test-abort.yaml
Shell
복사
# 리소스 삭제 kubectl delete -f virtual-service-all-v1.yaml
Shell
복사
Traffic Shifting은 카나리 배포처럼 비율 별 트래픽 전달을 수행
# 기본 VirtualService kubectl apply -f virtual-service-all-v1.yaml
Shell
복사
# v1 50 - v3 50 kubectl apply -f virtual-service-reviews-50-v3.yaml
Shell
복사
# v1 80 - v2 20 kubectl apply -f virtual-service-reviews-80-20.yaml
Shell
복사
# v1 90 - v2 10 kubectl apply -f virtual-service-reviews-90-10.yaml
Shell
복사
# 리소스 삭제 kubectl delete -f virtual-service-all-v1.yaml
Shell
복사
Traffic Shifting은 TCP에 대해서도 지원하는데, 실습에서는 별도의 네임스페이스 만들어서 진행
# Istio 버전 설정 export ISTIO_VERSION=1.23.2 # 네임스페이스 생성 kubectl create namespace istio-io-tcp-traffic-shifting # Auto Injection 설정 kubectl label namespace istio-io-tcp-traffic-shifting istio-injection=enabled # 트래픽 전송에 Sleep이 걸려 있는 앱을 배포 kubectl apply -f ~/istio-$ISTIO_VERSION/samples/sleep/sleep.yaml -n istio-io-tcp-traffic-shifting # v1, v2가 존재하는 TCP Echoing Micro Service kubectl apply -f ~/istio-$ISTIO_VERSION/samples/tcp-echo/tcp-echo-services.yaml -n istio-io-tcp-traffic-shifting
Shell
복사
# 모든 TCP 트래픽이 v1으로 가도록 설정 kubectl apply -f ~/istio-$ISTIO_VERSION/samples/tcp-echo/tcp-echo-all-v1.yaml -n istio-io-tcp-traffic-shifting # Ingress Host와 TCP Ingress Port를 변수 지정 export INGRESS_HOST=$(kubectl get po -l istio=ingressgateway -n istio-system -o jsonpath='{.items[0].status.hostIP}') export TCP_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="tcp")].nodePort}') # 20개의 접속 시도 시 모든 트래픽이 v1으로 전송됨을 확인 for i in {1..20}; do \ kubectl exec "$(kubectl get pod -l app=sleep -n istio-io-tcp-traffic-shifting -o jsonpath={.items..metadata.name})" \ -c sleep -n istio-io-tcp-traffic-shifting -- sh -c "(date; sleep 1) | nc $INGRESS_HOST $TCP_INGRESS_PORT"; \ done
Shell
복사
# 20% 트래픽이 v2로 가도록 설정 kubectl apply -f ~/istio-$ISTIO_VERSION/samples/tcp-echo/tcp-echo-20-v2.yaml -n istio-io-tcp-traffic-shifting # Ingress Host와 TCP Ingress Port를 변수 지정 export INGRESS_HOST=$(kubectl get po -l istio=ingressgateway -n istio-system -o jsonpath='{.items[0].status.hostIP}') export TCP_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="tcp")].nodePort}') # 20개의 접속 시도 시 모든 트래픽이 v1으로 전송됨을 확인 for i in {1..20}; do \ kubectl exec "$(kubectl get pod -l app=sleep -n istio-io-tcp-traffic-shifting -o jsonpath={.items..metadata.name})" \ -c sleep -n istio-io-tcp-traffic-shifting -- sh -c "(date; sleep 1) | nc $INGRESS_HOST $TCP_INGRESS_PORT"; \ done
Shell
복사
# 리소스 삭제삭제 kubectl delete -f ~/istio-$ISTIO_VERSION/samples/tcp-echo/tcp-echo-all-v1.yaml -n istio-io-tcp-traffic-shifting kubectl delete -f ~/istio-$ISTIO_VERSION/samples/tcp-echo/tcp-echo-services.yaml -n istio-io-tcp-traffic-shifting kubectl delete -f ~/istio-$ISTIO_VERSION/samples/sleep/sleep.yaml -n istio-io-tcp-traffic-shifting kubectl delete namespace istio-io-tcp-traffic-shifting
Shell
복사
Requests Timeouts는 타임아웃을 제어하는 기능
x-envoy-upstream-rq-timeout-ms가 헤더에 있다면 경로가 아니더라도 요청 별로 타임아웃 설정 가능
# 기본 VirtualService 설정 cd ~/istio-$ISTIOV/samples/bookinfo/networking kubectl apply -f virtual-service-all-v1.yaml # Reviews Service는 v2만 호출되도록 설정 kubectl apply -f - << EOT apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews spec: hosts: - reviews http: - route: - destination: host: reviews subset: v2 EOT # 가장 끝단인 Ratings Service 호출에 2초 지연을 설정함으로써 앞단의 모든 Service에 장애 영향 kubectl apply -f - << EOT apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings spec: hosts: - ratings http: - fault: delay: percent: 100 fixedDelay: 2s route: - destination: host: ratings subset: v1 EOT
Shell
복사
# HTTP 요청의 타임아웃은 기본적으로 비활성화이지만 Override하여 설정 가능 kubectl apply -f - << EOT apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews spec: hosts: - reviews http: - route: - destination: host: reviews subset: v2 timeout: 0.5s EOT
Shell
복사
0.5초로 타임아웃 설정했는데, 1초가 소요된 것은 Reviews 호출에 타임아웃 되었을 때 재시도가 설정되어 있기 때문
# 리소스 삭제 kubectl delete -f virtual-service-all-v1.yaml
Shell
복사
일반적으로 지칭하는 Circuit Breaking은 장애가 발생했을 때 전체 시스템에 영향을 주지 않도록 특정 Service를 제외하는 기능인데, k8s 관점에서의 Circuit Breaking은 Pod 안정성을 추구하는 기능
요청들이 대체로 Pod가 처리할 수 있는 수보다 높게 들어오게 되면 Pod는 재실행될 수 있는데, Circuit Breaking을 통해 Pod가 죽지 않도록 Pod에 들어오는 요청을 DestinationRule을 통해 정해두고 거절하는 것이 가능
# httpbin 배포 kubectl apply -f ~/istio-$ISTIOV/samples/httpbin/httpbin-nodeport.yaml # 웹 접속 주소 확인 HTTPBHostIP=$(kubectl get pod -l app=httpbin -o jsonpath='{.items[0].status.hostIP}') HTTPBNodePort=$(kubectl get svc httpbin -o jsonpath={.spec.ports[0].nodePort}) echo -e "HTTPBIN UI URL = http://$HTTPBHostIP:$HTTPBNodePort"
Shell
복사
# Circuit Breaking을 위한 DestinationRule 생성 # TCP 최대 연결 1개, HTTP 연결 당 요청 1개, 요청 대기 최대 1개 kubectl apply -f - << EOT apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: httpbin spec: host: httpbin trafficPolicy: connectionPool: tcp: maxConnections: 1 http: http1MaxPendingRequests: 1 maxRequestsPerConnection: 1 # keepalive disable outlierDetection: consecutive5xxErrors: 1 interval: 1s baseEjectionTime: 3m maxEjectionPercent: 100 EOT
Shell
복사
# httpbin으로 부하를 주입할 fortio 배포 kubectl apply -f ~/istio-$ISTIO_VERSION/samples/httpbin/sample-client/fortio-deploy.yaml kubectl patch svc fortio -p '{"spec":{"type":"NodePort"}}' # curl로 부하 주입 export FORTIO_POD=$(kubectl get pods -l app=fortio -o 'jsonpath={.items[0].metadata.name}') kubectl exec "$FORTIO_POD" -c fortio -- /usr/bin/fortio curl -quiet http://httpbin:8000/get # fortio control UI 웹 접속 주소 확인 FORTIOHostIP=$(kubectl get pod -l app=fortio -o jsonpath='{.items[0].status.hostIP}') FORTIONodePort=$(kubectl get svc fortio -o jsonpath={.spec.ports[0].nodePort}) echo -e "FORTIO UI URL = http://$FORTIOHostIP:$FORTIONodePort/fortio"
Shell
복사
# 2 x 10회 반복 kubectl exec "$FORTIO_POD" -c fortio -- /usr/bin/fortio load -c 2 -qps 0 -n 20 -loglevel Warning http://httpbin:8000/get Code 200 : 13 (65.0 %) Code 503 : 7 (35.0 %) # 3 x10회 반복 kubectl exec "$FORTIO_POD" -c fortio -- /usr/bin/fortio load -c 3 -qps 0 -n 30 -loglevel Warning http://httpbin:8000/get Code 200 : 14 (46.7 %) Code 503 : 16 (53.3 %)
Shell
복사
# 리소스 삭제 kubectl delete destinationrule httpbin kubectl delete deploy httpbin fortio-deploy kubectl delete svc httpbin fortio
Shell
복사

보안

요구 사항

중간 공격을 방지하기 위해 트래픽의 암호화가 필요
Service의 유연한 접근 제어를 위해 mTLS와 Policy 필요
시간마다 어떤 일들이 있었는지 감지 수단 필요

목표

기본적으로 애플리케이션이나 인프라 변화 없이 적용
기존에 존재하는 여러 계층의 보안 시스템과 결합 가능
신뢰할 수 없는 네트워크에서도 보안 솔루션을 구축 (ZT)
아무것도 신뢰할 수 없다는 가정하에, 사용자 및 다양한 정보를 바탕으로 최소한의 권한과 세밀한 통제를 지속적으로 수행하는 보안 활동

구성 요소

키와 인증서를 위한 CA (Ceritificate Authority)
Proxy에 배포를 수행하는 Configuration API Server
Telemetry와 감지를 위한 Envoy Proxy Extensions
클라이언트와 서버 사이 통신을 보호하기 위한 PEP (Policy Enhancement Points)로 동작하는 Proxy

아키텍쳐

컨트롤 플레인이 설정 파일을 API Server로부터 관리하고, 데이터 플레인의 PEP를 설정 (PEP는 Envoy를 이용하여 구현)

TLS vs mTLS

TLS는 네트워크로 통신하는 과정에서 도청, 간섭, 위조를 방지하기 위해 설계되어, 암호화를 통해 인증하여 통신 기밀성을 제공
TLS는 3단계의 기본 절차로 이뤄지는데, 1) 지원 가능한 알고리즘을 서로 교환, 2) 키 교환 및 인증, 3) 대칭키 암호로 암호화하여 메시지를 인증
SSL은 Deprecated 되었는데, 용도 자체는 TLS와 비슷하니 참고
mTLS는 TLS와 달리 서버 측도 클라이언트 측에 대한 인증서를 확인하고, 접근 권한을 확인하는 방식

SPIFFE

SPIFFE는 워크로드 식별을 위한 표준 집합을 의미하는데, 이기종 환경에서 Service ID를 발급할 수 있는 프레임워크
Istio와 SPIFFE는 Service ID를 공유하게 되는데, 이를 통해 Istio Service들은 다른 SPIFFE 호환 시스템과 연결 설정 및 수락이 가능
Kiali에서도 Traffic Graph에서 Security 항목을 체크하여 커넥션에서 자물쇠를 클릭하면 Source / Destination Principal 정보를 확인 가능

Authentication, Authorization (mTLS)

Istio는 X.509 인증서를 이용하여 모든 워크로드에 강력한 Identity를 안전하게 프로비저닝 가능
Envoy Proxy와 함께 실행되는 istio-agent는 istiod와 함께 동작하여 키와 인증서 교체를 대규모로 자동화
1.
istiod는 CSR을 수행하기 위해 gRPC Service를 제공
2.
Envoy는 SDS (Secret Service Discovery) API를 이용하여 키와 인증서 요청을 전송
3.
istio-agent는 SDS 요청을 받았을 때 비공개 키와 CSR을 생성한 후, 자격증명과 함께 CSR을 istiod에 전송하여 서명을 요청
4.
CA는 CSR에 포함된 자격증명의 유효성을 검사하고 CSR에 서명하여 인증서 생성
5.
istio-agent는 istiod로부터 받은 인증서와 개인 키를 Envoy SDS API를 통해 Envoy에 전달
전반적으로 SSL 발급 과정을 Envoy 동작과 엮은 형태이며, 위 프로세스는 키와 인증서 교체를 위해 주기적으로 반복
Authentication에는 2가지 유형이 존재하는데, Peer Authentication과 Request Authentication으로 나뉨
Peer Authentication
클라이언트가 연결을 생성할 수 있도록 검증하기 위해 Service 간의 인증에 사용
Istio는 Service 코드 변경 없이 mTLS를 제공
Request Authentication
Request에 첨부된 자격증명을 확인하기 위해 엔드 사용자 인증에 사용
Istio는 JWT 유효성 검사를 통해 요청 수준의 인증을 지원
Authorization은 Mesh 내에 있는 워크로드를 대상으로 Mesh, 네임스페이스, 워크로드 범위의 접근 제어를 제공하고, 아래와 같은 장점이 존재
워크로드 - 워크로드 혹은 엔드 사용자 - 워크로드 간의 인증을 수행
사용과 관리가 쉬운 AuthorizationPolicy라는 CRD가 단순 API로 제공
Operator가 Istio 속성에 대해 커스텀 조건 (Custom Conditions)을 정의할 수 있고, CUSTOM, DENY, ALLOW 사용 가능
Envoy에서는 네이티브하게 Istio Authorization (ALLOW, DENY)를 적용하여 높은 성능을 보장
TCP, gRPC, HTTP, HTTPS, HTTP/2 등을 네이티브 하게 제공하여 높은 호환성을 지님
서버 측의 Envoy Proxy는 Inbound 트래픽에 대해 접근 제어 가능
HTTP Traffic을 이용하여 실습 가능
암묵적 활성화 사항으로는 1) 인증 미적용 시 모든 요청을 허용, 2) 권한 부여 정책은 ALLOW, DENY, CUSTOM로 동작, 3) 정책의 우선 순위가 있어서 CUSTOM, DENY, ALLOW 순서로 매칭 규칙을 확인
JWT는 X.509 인증서의 경량화된 JSON 버전으로, X.509 인증서와 마찬가지로 비공개 키를 이용하여 Token을 서명하고 공개 키를 이용하여 서명된 메시지를 검증
JWT는 JSON 형태로 Token의 형식을 정의한 스펙으로써 Header, Payload, Signature로 나뉘게 되는데, 각 파트들은 base64로 인코딩 되어 . 뒤에 합쳐지게 됨
Header - Token 형식과 암호화 아록리즘 선언
Payload - 전송하려는 데이터를 JSON 형태로 기입
Signature - Header와 Payload의 변조 가능성을 검증

트래픽 흐름 분석

외부 클라이언트에서 k8s로 접속하는 과정은 아래와 같은데, 클라이언트가 Nginx로 요청을 보내고, Nginx로부터 응답을 받으려고 하는 상황을 가정
간단하게는 호스트의 tcp/ip와 iptables를 거쳐 Pod 내의 iptables와 Envoy를 경유하는 식으로 동작하는데, 이를 Inbound / Outbound의 2가지 형태로 단계별 세세하게 탐색
Istio의 Life of Packet 글을 참고
(몇 없긴 하지만) 트래픽 흐름을 확인하기 위한 실습 환경

InTraffic - Inbound (클라이언트 → Pod)

sNAT + dNATdNATsNAT + dNAT

클라이언트 → istio-ingressgateway

클라이언트가 Nginx로 요청을 보내기 위해선 가장 먼저 Ingress Gateway로 인입
그림 상 1번 과정에 해당되며 일반적인 GET 요청을 보낸다고 가정했을 때, <client-ip>:<client-port> | <public-ip>:<public-port>와 같은 형태로 패킷을 구성
<public-port>의 경우 NodePort가 될 수도 있고, 그 외에 별도로 노출시킨 포트일 수 있음

istio-ingressgateway → 노드

Ingress Gateway에 요청이 인입되면, (externalTrafficPolicy: Local이 설정되었단 전제하에) Nginx Pod가 존재하는 노드로 전달 필요
이에 따라 Ingress Gateway에서는 클라이언트의 IP 주소를 XFF (X-Forwarded-For)에 담게 되고, sNAT과 dNAT되에 <ingress-gateway-ip>:<random-port> | <pod-ip>:<nginx-port> 형태로 노드에 전달
해당 과정에서의 패킷 캡쳐 시 예시로 아래와 같이 확인 가능

노드 → istio-proxy 컨테이너

노드로 전달되면 호스트 네임스페이스의 iptables를 경유하게 되는데, 이 때는 별도의 규칙 매칭 없이 바로 Pod의 네임스페이스로 전달
Pod의 네임스페이스는 Pause 컨테이너가 생성한 네트워크 네임스페이스이며, Nginx와 해당 네임스페이스를 공유하는 istio-proxy가 트래픽을 가로챌 수 있도록 Istio의 Init 컨테이너가 iptables 규칙을 수정
Pod 내부의 NAT 테이블을 통해 dNAT되어 <ingress-gateway-ip>:<random-port> | <pod-ip>:<istio-proxy-port> 형태로 패킷을 구성하게 되고, istio-proxy 컨테이너로 인입
istio-proxy에서 인입될 때 사용되는 필터들은 아래와 같은 과정으로 이뤄짐
PREROUTING → ISTIO_BOUNDED → ISTIO_REDIRECT
Istio의 Init 컨테이너가 설정하는 iptables 규칙은 구성된 실습 환경에서 아래 명령어로 확인 가능
kubectl logs nginx-pod -c istio-init

istio-proxy 컨테이너 → Nginx 컨테이너

istio-proxy에서는 요청을 전달 받게 되면 iptables 규칙에 의해 sNAT와 dNAT 되어 Nginx 컨테이너에 전달
sNAT되는 주소는 istio-proxy와 Nginx가 동일한 네트워크 네임스페이스를 사용하기 때문에 루프백으로 설정되므로, 127.0.0.6:<random-port> | <pod-ip>:<nginx-port>의 형태로 패킷을 구성
Nginx 컨테이너에는 클라이언트가 요청한 트래픽이 도착하게 되고, (externalTrafficPolicy: Local이므로) XFF 헤더에서 클라이언트의 주소를 확인 가능
istio-proxy에서 빠져나가면서 Nginx로 향할 때는 출발지 IP가 127.0.0.6임에 따라 최상단 규칙이 매칭되어 ISTIO_OUTPUT이 적용되는데, 전체적으로 아래와 같은 과정으로 Nginx에 전달
OUTPUT → ISTIO_OUTPUT → POSTROUTING
해당 과정에서의 패킷 캡쳐 시 예시로 아래와 같이 확인 가능

InTraffic - Outbound (Pod → 클라이언트)

Nginx 컨테이너에 도달한 요청은 리턴으로 200 응답을 클라이언트에 전달하는데, 마찬가지로 iptables의 규칙을 참고하여 Inbound와 동일한 동작 방식의 역순으로 트래픽을 전달
이 때 규칙은 별도 엔트리가 존재하는 것이 아니라, 기존에 존재하는 정보를 참고해서 역변환하는 방식으로 동작
sNAT + dNATsNATsNAT + dNAT

OutTraffic - Outbound (Pod → 웹 서버)

해당 예시는 외부의 요청을 받아내는 방식 보다는, Nginx가 업데이트 혹은 패치 등의 다운로드 과정에서 외부 웹 서버와의 연결을 위해 요청을 보내는 상황의 트래픽을 가정
나가는 트래픽도 들어오는 트래픽의 Outbound와 크게 다르지 않고 동일한 방식으로, Ingress Gateway를 거치지 않는다는 점만 빼면 거의 비슷하게 동작
dNATsNAT + dNATsNAT

Nginx 컨테이너 → istio-proxy 컨테이너

초기엔 <pod-ip>:<nginx-port> | <webserv-ip>:<webserv-port> 형태로 패킷이 구성되어 있는데, iptables의 OUTPUT → ISTIO_OUTPUT → ISTIO_REDIRECT 체인을 지나면서 istio-proxy로 접근하기 위해 <pod-ip>:<random-port> | 127.0.0.1:<istio-proxy-port> 형태로 패킷을 dNAT
해당 과정에서의 패킷 캡쳐 시 예시로 아래와 같이 확인 가능

istio-proxy 컨테이너 → 노드

외부로 트래픽을 전달하기 위해선 istio-proxy 컨테이너에서 나온 트래픽을 호스트 네트워크 네임스페이스가 있는 노드로 전달해야 하는데, 이 과정에서 Pause 컨테이너가 설정한 네트워크 네임스페이스를 거침
Pod의 네임스페이스에서는 iptables에 설정된 규칙에 따라 sNAT 및 dNAT 되어 <pod-ip>:<random-port + 2> | <webserv-ip>:<webserv-port> 형태로 패킷을 구성
InTraffic 때는 Pod의 네임스페이스를 지날 때 그냥 거쳐만 갔는데, OutTraffic에서는 iptables가 한 번 더 적용되는 것은 UID 1337 때문이고 자세한 내용은 여기를 참고 (따라서 Pod에서 동작하는 애플리케이션의 UID로 1337을 이용해선 안 됨)
해당 과정에서의 패킷 캡쳐 시 예시로 아래와 같이 확인 가능하고, 포트는 이전 그림과 달리 +2된 것도 확인 가능

노드 → 웹 서버

Pod의 네임스페이스를 거쳐 호스트의 네임스페이스까지 올라오면, 외부에 있는 웹 서버로 트래픽을 내보내기 위해 iptables의 규칙을 적용
<node-ip>:<random-port> | <webserv-ip>:<webserv-port>의 형태로 sNAT되어 패킷을 구성

OutTraffic - Inbound (웹 서버 → Pod)

웹 서버에서 반환되는 응답을 Pod에서 받기 위해 Inbound되면, Outbound의 역순으로 패킷을 처리
위에서 훑었던 흐름대로 Nginx로 도착하기 전에는랜덤 포트 + 2 값되었던 것도 인식하여 정상적으로 처리하여 istio-proxy를 거침