-
이코에코(Eco²) Service Mesh #1: Istio Sidecar 마이그레이션이코에코(Eco²)/Kubernetes Cluster+GitOps+Service Mesh 2025. 12. 8. 12:26

이전 포스팅에서 확인한 Scan API의 리소스 사용량, Fail rate 87%인 상황에서도 리소스 사용량은 미미하다.
공모전을 마치고, 프론트 측에서 웹앱에서 앱 배포로 전환하고자 React에서 React Native로 이관하려했다.
현재 이코에코의 Auth는 JWT를 쿠키에 담아 인증 로직을 진행하기에 브라우저 보안 모델에 의존하는 형태다.
프론트 측에서 React Native로 전환하기 전에, Auth를 쿠키 대신 헤더에 담아 전송하는 방식으로 리팩토링을 진행할 계획이었다.
Scan API의 성능 측정에서 밝혔듯, 도메인별 컴퓨팅 리소스가 부하에도 넉넉한 상태라 Auth 리팩토링에 맞춰 이전에 고려했던 Istio 도입을 진행해봐도 무리가 없다는 판단이 섰다. 클러스터 + API 개발기 때 Auth의 공통 모듈에 꽤 골머리를 앓았기도 해서 이 역할을 분리해 위임할 피쳐가 필요했다.

Istio란 무엇이고, 왜 도입하나
Istio는 애플리케이션 코드를 수정하지 않고도 마이크로서비스 간의 통신을 제어, 보안, 관측할 수 있게 해주는 오픈소스 Service Mesh 플랫폼이다. 애플리케이션 파드(Pod) 옆에 경량 프록시인 Envoy를 사이드카(Sidecar)로 배치하여 모든 트래픽을 가로채고 제어한다.
이렇게 되면 보안, 관측성, 트래픽 제어 등 비즈니스 로직 외 부가 플랫폼성 기능을 앱에서 분리해 사이드카 패턴으로 운용할 수 있다. 현재 모든 서비스에 공통으로 묶여있는 security 모듈을 모두 제거할 수 있으며, 보다 정밀한 트래픽 관측 및 제어가 가능하기에 차후 배포 전략을 구현하는데 있어 선택지가 넓어진다. 예를 들면 Istio를 도입한 이후론 클러스터 구성만으로 Canary 배포와 A/B 테스트가 가능해 진다. 기존 구조는 ArgoCD Roll-out이 최선으로, Canary를 적용할 경우 클러스터 외부 ALB의 기능에 의존하게 된다.
서비스메쉬, Istio에 대한 개념은 전직장에서 처음 접했으며, 직접 운용한 경험은 없고 CNP 파트에서 운용 중인 상태여서 해당 파트의 Onboarding 과정에 Istio in Action이 존재하는 걸 인지한 정도가 다다.
언제나 직접 다루고 싶던 피쳐였지만, 기능이 많은 만큼 리소스 사용량이 상대적으로 많기 때문에(0.5 - 2.0 vCPU) 기존 이코에코 클러스터에선 도입을 반려했었다. 그러나 모든 기능이 완성되고 최종 e2e 및 부하 테스트까지 진행해 본 결과, 리소스의 여유분이 꽤 되기도 하고, 프론트 측에 발맞춰 Auth 리팩토링도 들어가야 하기에 현 시점이 Istio를 도입하기 좋은 타이밍이라 판단했다.Istio Ambient Mesh vs Sidecar: 아키텍처 적합성

mTLS 암호화 , TCP 라우팅, L4 인가를 처리하는 경량 프록시인 ztunnel
Istio 도입을 결정하면서 고려한 사안 중 하나는 Sidecar vs Ambient Mesh였다. Istio v1.15(2022년 9월에서 처음 소개된 Ambient Mesh는, 기존 Service Mesh의 핵심이었던 Sidecar를 파드에서 분리(Sidecar-less)하여 인프라 레이어와 애플리케이션 레이어를 명확히 구분하는 새로운 데이터 플레인 모델이다.Ambient Mesh의 주요 특징

네임스페이스 단위로 배포되어 HTTP 라우팅, 헤더 조작, 정교한 보안 정책을 수행하는 프록시인 waypoint
Ambient Mesh는 데이터 플레인 기능을 단일 Sidecar 컨테이너에 통합하던 기존 Istio와는 달리, L4(보안/전송) 계층과 L7(기능) 계층을 물리적으로 분리(Decoupled Data Plane)하여 필요한 기능을 선택적으로 적용할 수 있도록 설계되었다.
1. Secure L4 Overlay Layer (ztunnel)
노드 단위로 실행되는 경량 프록시인 ztunnel (Zero Trust Tunnel)이 담당하는 기반 계층이다. Rust로 작성되어 리소스 오버헤드를 최소화했으며, 애플리케이션 파드 수정 없이 CNI 레벨에서 트래픽을 처리한다. 주로 mTLS 암호화, TCP 라우팅, L4 레벨의 인증/인가, 기본적인 텔레메트리 수집을 전담하여 메시(Mesh) 내 모든 통신의 보안을 기본적으로 보장한다.2. L7 Processing Layer (Waypoint Proxy)
HTTP 라우팅, 헤더 조작, 재시도, 정교한 보안 정책 등 풍부한 L7 기능이 요구될 때만 선택적으로 활성화되는 계층이다. 이를 위해 표준 Envoy 프록시인 Waypoint를 네임스페이스 또는 서비스 계정 단위로 배포하여 트래픽을 경유시킨다.
ztunnel을 통해 기본 보안 계층을 확보하고, L7 처리가 필요한 워크로드에 한해 Waypoint를 추가하여 기능을 확장하는 계층형 접근 방식을 취하고 유연한 아키텍처를 지향한다.Eco² 아키텍처와의 불일치 (Trade-off)
1. Namespace 격리 전략과 Waypoint의 비효율성
Eco²는 보안과 관리 효율을 위해 `auth`, `scan` 등 도메인별로 네임스페이스를 1:1로 분리하는 격리 전략을 취하고 있다. Ambient Mesh에서 L7 기능을 쓰려면 각 네임스페이스마다 별도의 Waypoint Proxy 파드를 띄워야 하는데, 이는 서비스 개수만큼 프록시 파드가 추가됨을 의미한다. 다수의 서비스가 한 네임스페이스에 모여 Waypoint를 공유하는 환경이 아니기에, 오히려 Sidecar 방식보다 리소스와 스케줄링 오버헤드가 증가하는 역효과가 발생한다.
2. 노드 격리 전략과 Ztunnel 효율성의 딜레마
Eco²의 인프라 구성(terraform/main.tf)을 보면, 각 도메인 서비스는 t3.small/medium 규모의 전용 노드에 Taint와 Affinity를 통해 물리적으로 격리되어 배포된다. 즉, "하나의 노드에 소수의 특정 서비스 파드만 실행되는 저밀도(Low-density) 환경"이다. ztunnel은 노드 내 다수 파드 트래픽을 통합 처리하여 효율을 내는 구조인데, 우리처럼 노드당 파드 수가 적은 환경에서는 그 '공유 효과'가 미미하다. 오히려 모든 노드에 ztunnel 데몬셋을 띄우는 고정 오버헤드가, 필요한 파드에만 붙는 Sidecar 컨테이너보다 비효율적이다.
3. Calico CNI와의 역할 중첩 및 복잡성
현재 Eco²는 Calico CNI(VXLAN)를 통해 East-West 네트워크 정책과 트래픽 제어를 수행하고 있다. Ambient Mesh는 자체적으로 트래픽을 가로채기 위해 CNI 레벨에 깊숙이 개입하는데, 이는 기존 Calico의 데이터 플레인과 역할이 중첩되거나 충돌할 소지가 있다. 안정성이 최우선인 상황에서 검증되지 않은 네트워크 스택 복잡도를 감수할 이유는 없었다.
4. GA(General Availability) 상태와 안정성
Ambient Mesh는 Istio v1.22(2024년 5월)에서 공식적으로 GA 상태가 되었지만, Sidecar 모델에 비해 프로덕션 운영 사례(Use Case)와 트러블슈팅 데이터가 절대적으로 부족하다. 특히 우리가 핵심적으로 사용하려는 EnvoyFilter나 일부 커스텀 인증 로직의 경우, Sidecar 모델에서의 동작이 훨씬 잘 문서화되어 있고 레퍼런스가 풍부하다. 안정적인 디벨롭을 위해 성숙하고 검증된 기술을 선택하는 것이 합리적이라 판단했다. 결론적으로, 네임스페이스/노드 격리 구조에서의 리소스 효율성과 운영 안정성(Stability)을 고려하여 Sidecar 패턴을 택했다.
Istiod / Istio Gateway는 어디에 배치할 건가

현재 Master node의 리소스 사용량, 아직 여유분이 존재한다. Istio는 각 서비스 파드에 붙어 Envoy 프록시를 주입하고(Data-plane), 이를 중앙에서 관리하는 control plane이 존재한다.
현재 이코에코의 control-plane을 맡고 있는 master-node에 두는 게 적합해 보였다. 해당 노드의 리소스를 확인하니 CPU 사용량 3.04%, 메모리 사용량 18.7%로 널널하기에 Control plane(master node)에 Istio Controller를 추가로 붙여도 운용에 무리가 없다.
Ingress Gateway의 경우, Control-plane이 아닌 별도의 GW 노드를 띄워 운용하는 편이 안전하지만 추가 노드를 띄우기 보다는 Master에 밀집시키는 방법을 택했다. terraform으로 새 노드를 추가하다가 클러스터가 망가질 수도 있어서.. 일단은 Master에 배치한 후 여유가 생기면 분리하는 방향이 안전하다.Istio는 언제, 어떻게 구축하나
Wave 0: CRDs Wave 2: Namespaces Wave 3: RBAC & Storage + Wave 4: Istio-base + Wave 5: Istiod + Wave 6: Istio Ingress Gateway Wave 7: Network Policies Wave 10: Secrets Operator Wave 11: Secrets CRs Wave 15: ALB Controller Wave 16: External DNS Wave 20: Monitoring Operator Wave 21: Grafana Wave 27: PostgreSQL Wave 40: APIs (ApplicationSet) + Wave 50: Istio routes (Virtual Service) - Wave 70: IngressesMaster node에 붙어 Istio CLI로 수동 설치도 가능하지만,GW, 트래픽 관리, 메트릭, Auth 등의 기능들을 개별로 설정하기엔 품이 너무 많이 든다. GitOps의 이점을 살려 코드베이스로 관리하는 편이 보다 커스텀 및 버전 관리에 용이하기에 ArgoCD Sync-wave에 합류시키는 방안이 합리적이라 판단했다.
Sync-wave로 구축이 결정되면 다음 판단 사례는 언제나 그랬듯이 Helm-charts vs Operator인데 Istio의 경우 Helm-charts의 지원이 상대적으로 활발하다. Platform의 성격을 가진 피쳐라 세밀한 CR 보다는 간편히 패키징된 Helm-charts를 통채로 가져와 사용하는 편이 운용 난이도를 낮추기도 한다. (비슷한 케이스로는 prometheus-grafana 스택, Postgres/Redis Data stack이 있다.)
구축/운용 방안을 정했다면 다음은 'Sync-wave 중 몇 번째에 뜨는 게 합리적이냐'를 판단해야 한다. Istio의 경우 네트워크 스택의 기반이 되는 컴포넌트로, 네트워크 피쳐들의 하위 의존성에 위치해야 한다. 때문에 네트워크 관련 피쳐(ALB, Network Policies, External DNS 등)보다 앞선 wave에 배치하는 것이 합당하다. 현재 Sync-wave 중 가장 앞선 네트워크 컴포넌트인 Network Policies의 Wave는 6으로 Istio는 wave 4, Istio GW는 wave 5에 놓는 걸로 결정했다. Istio GW를 단일 진입점으로 잡고 트래픽을 받되, 패스 기반 라우팅은 Virtual Service로 수행하도록 구성했다. 먼저 서비스가 올라온 뒤, 해당 라우팅이 붙는 편이 sync-wave의 안정성이 높기에 virtual service는 sync-wave 50에 배치했다. (시행착오를 꽤 겪었다.)
인그레스 파트만 따로 한 번 더 정리하면 기존 9개 k8s ingress 구성에서 수행하던 패스 기반 라우팅을 단일 Bridge Ingress({api | argocd | grafana}.dev.growbin.app) + 서비스별 Virtual Service로 마이그레이션하는 과정이라 보면 좋다. 네트워크 토폴로지의 변경에 대해선 다이어그램과 함께 설명하겠다.Network Topology w. Istio
AS-IS


좌: Ingress의 상태를 ALB Controller가 감지, 파드의 주소를 TG에 등록 / 우: TG를 기반으로 ALB->Pod로 트래픽이 전달 기존 네트워크 토폴로지다. Ingress에서 변경이 감지되면 watch 중이던 ALB Controller가 노출된 Ingress의 IP:Port를 TG에 반영,
ALB는 TG를 읽어 Ingress로, Ingress는 NodePort로 노출된 Pod에 트래픽을 전달하는 구조다.
자세한 사안은 이코에코(Eco²) 인프라 구축 #04를 참고하면 트래픽 흐름을 파악할 수 있다.TO-BE

Istio를 도입한 이코에코 네트워크 토폴로지, {ALB Controller ❘ External DNS} -watch-> Ingress 까진 동일.
Istio가 도입되면 기존 토폴로지에 Istio Ingress GW, VirtualService Routing, Envoy Sidecar가 추가되며, 기존 K8s Ingress -> Service(NodePort) 라우팅 방식이 Istio Ingress Gateway -> VirtualService -> Envoy(Sidecar) 흐름으로 고도화된다.
1. Entry Point: AWS ALB Controller는 istio-ingressgateway 서비스를 백엔드로 하는 단일 진입점(Single Entry Point)을 구성하여 모든 외부 트래픽을 Istio Gateway로 집중시킨다.
2. Dynamic Routing: Istio Ingress Gateway는 Control Plane(Istiod)으로부터 xDS 프로토콜을 통해 동적으로 주입받은 라우팅 테이블을 참조하여, 트래픽을 적절한 서비스의 Envoy Sidecar로 전달한다.
3. Sidecar Proxying: 목적지 파드의 Envoy Sidecar는 인증/인가(AuthN/AuthZ), 메트릭 수집, mTLS 복호화 등을 수행한 후, 최종적으로 localhost의 애플리케이션 컨테이너로 트래픽을 넘겨준다.
4. Bridge Ingress: 기존처럼 ALB Controller와 ExternalDNS가 K8s Ingress를 Watch하는 구성은 유지하나, 서비스별 Ingress에서 호스팅과 path를 모두 담은 후, ALB -Nodeport-> Pod로 전달했던 구성에서 Istio Ingress Gateway -> Virstual Service(path route) -> Envoy Sidecar -> API Pods로 트래픽 흐름을 변경했다. 이제 서비스가 추가될 때마다 별도의 ingress를 생성하는 게 아닌, Virtual Service를 명시한 후 Bridge ingress에 얹으면 된다.Istio 기반 Network Topology로 얻는 Trade-off
Istio를 도입할 경우 얻을 수 있는 이점은 크게 4가지다.
1. Zero-Trust Security
GW <-> Sidecar, Sidecar<-> Sidecar의 모든 통신이 mTLS로 암호화 가능하다. 내부 네트워크 침입 시에도 패킷 감청이 불가능하다.
2. Traffic Control
클러스터 내부에서 L7 기반의 정교한 트래픽 제어가 가능해진다. (etc. 특정 유저만 v2로 보내는 Canary 배포)
3. Decoupling
인증, 로깅, 재시도, 타임아웃 로직을 애플리케이션 코드에서 걷어내고 인프라 레벨(Sidecar)로 위임하여 비즈니스 로직만 유지할 수 있다.
4. Observability
애플리케이션 수정 없이 표준 HTTP 트래픽 메트릭(RPS, Latency, Error Rate)을 자동으로 수집해 이코에코(Eco²) Scan API 성능 측정 및 시각화 파트에서 주입했던 metrics.py의 보일러플레이트를 제거할 수 있는 환경이 마련된다.
Istio 도입 시 고려해야할 사이드 이펙트는 다음과 같다.
1. Resource Overhead
모든 파드마다 Envoy Sidecar 컨테이너가 추가된다. 사이드카 하나당 최소 100MB의 메모리와 일정량의 CPU를 상시 점유한다.
2. Network Latency
기존 ALB -> Pod 경로에서 ALB -> Gateway -> Sidecar -> Pod로 Hop이 2단계 늘어난다. 또한 mTLS 암호화/복호화 과정을 더한다면 CPU 연산 시간이 추가된다. 그렇지만 영향은 ms 단위로 미미할 듯싶다.
3. Operational Complexity
쿠버네티스 리소스 외에도 VirtualService, DestinationRule, Gateway 등 Istio CRD를 추가로 관리해야 한다. 코드베이스로 트러블슈팅을 진행하는 현재 GitOps 환경에서 운영 난이도가 올라갈 수 있다.Istio Controller / Gateway / VirtualService 스펙
--- apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: dev-istio-base namespace: argocd annotations: argocd.argoproj.io/sync-wave: '4' spec: project: dev source: repoURL: https://istio-release.storage.googleapis.com/charts/ chart: base targetRevision: 1.24.1 destination: server: https://kubernetes.default.svc namespace: istio-system syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - ServerSideApply=true --- apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: dev-istiod namespace: argocd annotations: argocd.argoproj.io/sync-wave: '4' spec: project: dev source: repoURL: https://istio-release.storage.googleapis.com/charts/ chart: istiod targetRevision: 1.24.1 helm: valuesObject: pilot: autoscaleEnabled: false replicaCount: 1 resources: requests: cpu: 100m memory: 512Mi limits: cpu: 500m memory: 1Gi # Master 노드에 배치 nodeSelector: role: control-plane tolerations: - key: role operator: Equal value: control-plane effect: NoSchedule destination: server: https://kubernetes.default.svc namespace: istio-system syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - ServerSideApply=true- file(05-istio.yaml): ArgoCD ApplicationSet으로 정의, Helm Chart로 istio-base, istiod, istio-ingressGW를 순차 배포.
- instance: Master 노드에 Control Plane(istiod)을 격리.


- ApplicationSet(cluster/{dev|prod}/05-istio.yaml: 70-ingress.yaml 제거, istio-base/istiod/istio-gw Application 추가
- Routing Structure (workloads/routing/): Ingress 리소스를 제거, Istio의 Gateway와 VirtualService로 전환.
Istio Ingress Gateway(Data Plane Entry Point)
## /terraform/main.tf (ec2 node spec) ... "k8s-ingress-gateway" = "--node-labels=role=ingress-gateway,domain=gateway,infra-type=istio,workload=gateway,tier=network,phase=5 --register-with-taints=role=ingress-gateway:NoSchedule" } .. # Ingress Gateway (Phase 5) output "ingress_gateway_instance_id" { description = "Ingress Gateway Instance ID" value = module.ingress_gateway.instance_id } output "ingress_gateway_public_ip" { description = "Ingress Gateway Public IP" value = module.ingress_gateway.public_ip } # EC2 Instances - Istio Ingress Gateway module "ingress_gateway" { source = "./modules/ec2" instance_name = "k8s-ingress-gateway" instance_type = "t3.medium" # 2GB ami_id = data.aws_ami.ubuntu.id subnet_id = module.vpc.public_subnet_ids[0] security_group_ids = [module.security_groups.cluster_sg_id] key_name = aws_key_pair.k8s.key_name iam_instance_profile = aws_iam_instance_profile.k8s.name root_volume_size = 20 root_volume_type = "gp3" user_data = templatefile("${path.module}/user-data/common.sh", { hostname = "k8s-ingress-gateway" kubelet_extra_args = local.kubelet_profiles["k8s-ingress-gateway"] }) tags = { Role = "worker" Workload = "gateway" Phase = "5" } } ## terraform/templates/hosts.tpl ... [ingress_gateway] k8s-ingress-gateway ansible_host=${ingress_gateway_public_ip} private_ip=${ingress_gateway_private_ip} workload=gateway instance_type=t3.medium phase=5 --- ## /clusters/{dev|prod}/05-istio.yaml ... apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: dev-istio-ingressgateway namespace: argocd annotations: argocd.argoproj.io/sync-wave: '5' spec: project: dev source: repoURL: https://istio-release.storage.googleapis.com/charts/ chart: gateway targetRevision: 1.24.1 helm: valuesObject: service: type: NodePort ports: - name: status-port port: 15021 protocol: TCP targetPort: 15021 - name: http2 port: 80 protocol: TCP targetPort: 80 - name: https port: 443 protocol: TCP targetPort: 443 destination: server: https://kubernetes.default.svc namespace: istio-system syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - ServerSideApply=true --- # workloads/routing/gateway/base/gateway.yaml --- apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: eco2-gateway namespace: istio-system spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - api.growbin.app - argocd.growbin.app - grafana.growbin.app --- # ALB Controller와 연동하기 위한 Bridge Ingress apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: istio-ingress namespace: istio-system annotations: alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/target-type: instance alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]' alb.ingress.kubernetes.io/certificate-arn: {arn} alb.ingress.kubernetes.io/group.name: ecoeco-main alb.ingress.kubernetes.io/group.order: '1' alb.ingress.kubernetes.io/backend-protocol: HTTP alb.ingress.kubernetes.io/healthcheck-path: /healthz/ready alb.ingress.kubernetes.io/success-codes: '200' external-dns.alpha.kubernetes.io/managed-by: external-dns external-dns.alpha.kubernetes.io/hostname: api.growbin.app,argocd.growbin.app,grafana.growbin.app spec: ingressClassName: alb rules: - http: paths: - path: / pathType: Prefix backend: service: name: istio-ingressgateway port: number: 80 --- # workloads/routing/gateway/base/health-check-vs.yaml apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: gateway-health-check namespace: istio-system spec: gateways: - eco2-gateway hosts: - '*' http: - match: - uri: exact: /healthz/ready - uri: exact: / headers: User-Agent: regex: ELB-HealthChecker/.* directResponse: status: 200 body: string: Healthy --- # /workloads/routing/gateway/dev/patch-gateway.yaml apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: eco2-gateway namespace: istio-system spec: servers: - port: number: 80 name: http protocol: HTTP hosts: - '*'- file: ArgoCD ApplicationSet(05-istio.yaml)에 포함.
- chart: gateway: Istio 공식 게이트웨이 차트를 사용.
- type: AWS ALB Controller와 연동하기 위해 서비스 타입을 NodePort로 설정.
- ports: 80(HTTP), 443(HTTPS), 15021(Health Check) 포트를 엽니다.
- instance: t3.medium으로 이원화. 트래픽이 몰리면 GW발 CPU 연산 부하가 발생하기에 별도의 노드(k8s-ingress-gw)로 분리.
- health-check.yaml: ALB Health check 수행, 엔드포인트 개설
- gateway.yaml:
- Listen port: HTTPS / Backend Protocol: HTTP
- Ingress Controller로 ALB Controller를 지정
- ExternalDNS managed 라벨 부착, ExternalDNS가 Istio-GW를 바라봄
- ALB helath check 및 {argocd | grafana | api}.dev.growbin.app Ingress/Egress 허용 (patch).
Istio VirtualService (ApplicationSet + Kustomize)
## /clusters/{dev|prod}/50-istio-routes.yaml apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: dev-istio-routes namespace: argocd spec: generators: - list: elements: - name: gateway namespace: istio-system path: workloads/routing/gateway/dev - name: auth namespace: auth path: workloads/routing/auth/dev - name: chat namespace: chat path: workloads/routing/chat/dev - name: character namespace: character path: workloads/routing/character/dev - name: my namespace: my path: workloads/routing/my/dev - name: scan namespace: scan path: workloads/routing/scan/dev - name: image namespace: image path: workloads/routing/image/dev - name: location namespace: location path: workloads/routing/location/dev - name: argocd namespace: argocd path: workloads/routing/argocd/dev - name: grafana namespace: grafana path: workloads/routing/grafana/dev template: metadata: name: dev-route-{{name}} namespace: argocd annotations: argocd.argoproj.io/sync-wave: '50' spec: project: dev source: repoURL: https://github.com/eco2-team/backend.git targetRevision: develop path: '{{path}}' destination: server: https://kubernetes.default.svc namespace: '{{namespace}}' syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true # workloads/routing/{domain}/{base | dev | prod}/virtual-service.yaml apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: {domain}-vs namespace: {domain} spec: hosts: - '*' gateways: - istio-system/eco2-gateway http: - match: - uri: prefix: /api/v1/{domain} route: - destination: host: {domain}-api.{domain}.svc.cluster.local port: number: 8000- file(virtual-service.yaml): ArgoCD ApplicaitonSet으로 정의, 각 도메인(auth, scan 등)별 workloads 기반으로 배포.
- 기존 70-ingress.yaml을 대체 (40-apis-appset.yaml 이후 배포)
- file(50-istio-routes.yaml): 각 도메인별 라우팅 규칙 정의
- 기존 workloads/ingress 관련 kustomize를 대체
Istio 구축 결과


k8s-ingress-gateway 노드 클러스터 합류 

istiod, istio-base, istio-ingressgateway application sync 완료 
K8s ingress -> Istio-ingress 교체 완료 
istiod(control-plane) 및 ingress gateway(gateway-node) pod 배포 완료 

virtaul services(7-apis, argocd, grafana, gateway) 배포 및 Sync 확인 
Istio GW(Bridge Ingress) 라우트 테이블 확인 
각 API Pod 외 Envoy Sidecar Pod(Istio-proxy) 정상동작 
Each API Pods: 1/1 -> 2/2 
Envoy Sidecar pod 내부 로그, 지속적으로 xDS 프로토콜 기반의 설정 Sync가 이뤄지는 걸 확인할 수 있다. https://dewble.tistory.com/entry/istio-traffic-management-understand-envoy-xDS-Sync
[Istio]Traffic Management - 무슨 일이 발생하는 건가?(envoy xDS Sync 이해하기)
Concept envoy xDS Sync는 Envoy 프록시가 동적으로 구성을 업데이트하고, 서비스 메시에서의 트래픽 관리를 최적화하는 과정을 의미한다. 여기서 xDS는 eXtensible Discovery Service의 약자로, Envoy 프록시와 구
dewble.tistory.com

Istio 마이그레이션 후 도메인 서버 간 호출 성공 (api/v1/scan/classify -> api/v1/internal/character) Troubleshooting

1. Domain not found- 마이그레이션 직후, 모든 도메인이 호스팅되지 않는 이슈가 발생했다. 시스템을 살피며 원인을 파악하니 ExternalDNS에서 Bridge Ingress를 감지하지 못하는 상황이라 Ingress가 ALB ARN을 가지고 있어도 외부로 빠져나가지 못했다. Bridge Ingress에 ExternalDNS manged 라벨을 부착하는 방식으로 해결했다.
2. Failed ALB Health checks
- ALB 대시보드로 확인한 결과, 모든 도메인 그룹 및 GW에서 heath 체크가 성공하지 못하고 있었다. 각 파드는 정상동작 중이었기에 스코프를 단일 진입점인 GW로 한정하고 시스템 디버깅을 이어간 결과, 호스트에 {api | argocd | grafana}.dev.growbin.app만 명시된 걸 확인했다. 이 경우 ALB 호스트 +/healthz/ready를 받지 못하기에 와일드카드를 추가해 ALB health check를 안정화시켰다. (+ health check용 virtual service 추가)
3. Network Policy
- Envoy Sidecar <-> istiod: API Server, DB 포트의 ingress/egress만 명시한 상태였기에 envoy 관련 연결이 모두 차단돼, sidecar pod가 degraded에 빠져있었다. 명시적으로 허용해 관련 오류를 해결했다. (podSelector: {})
- Scan -> Character: istiod 및 여타 podSelector:{} 로 명시된 Network Policy가 White list로 동작하면서 명시된 Netpol 외 도메인 간 통신이 모두 차단됐다. Scan에서 보상 함수로 호출하는 Character API 트래픽 흐름을 명시하는 netpol을 추가해 관련 문제를 해결했다.
- ingress(character): character의 character-api가 scan 네임스페이스로부터 들어오는 8000포트를 허용
- egress(scan): scan의 scan-api가 character 네임스페이스로 나가는 8000포트를 허용
Istio Offloading
다음 포스팅은 도메인 서버 내 플랫폼성 기능을 Envoy Sidecar로 위임하는 과정에 관해 서술할 예정이다.
Istio Offloading은 Istio Migration(Sidecar + Gateway)과 함께 작업했으며 이미 진행이 완료된 상태다. 검토까지 개발 기간에 포함하면 3-4일정도가 소요됐다.
Ingress를 건드리는 만큼 서버가 외부 연결과 끊어지는 경우도 많았기 때문에 주로 새벽에 작업해야 했다. (dev/prod 배포 환경 분리가 없으면 이렇게 된다.) 다음날 아침까지 끝내지 못하면 이전 버전으로 코드베이스를 롤백하며 클러스터를 관리했으나, 12월 10일엔 "한 번에 끝내자."는 마음에 클러스터를 얼린 상태로 작업했다.
Auth Offloading은 인증 로직을 포함한 Service 패킷 플로우가 모두 변경되기에 별도의 포스팅이 필요하다고 판단했다. AuthN/AuthZ 의존성 코드를 모든 도메인에서 제거한 만큼 코드베이스의 변경도 많아 신경을 꽤 썼다.
그만큼 비즈니스 로직의 순수성이 유지되게 됐으니 도메인 서버 개발 및 유지보수는 수월해질 듯 싶다.
대신 Auth가 인프라 레이어에 위임되면서 인프라 트러블슈팅은 더 하드해질 예정이다.
이제 마이크로서비스 간 통신엔 mTLS 복호화까지 붙은 상태라 클러스터 디버깅에 품이 꽤 많이 들 거라 예상한다.
물리적인 코드의 측면에선 auth에서 해방된 것처럼 보이나.. 논리적으론 여전히 Istio의 AuthZ가 이코에코 Auth 서버의 블랙리스트 로직에 의존하는 상황이다.
AuthN(JWT 토큰 유효성 검증)은 온전히 Istio에서 수행하기에 연산 부담은 분산됐다고 볼 수 있으나..
Auth 서버가 다운되면 클러스터가 얼어버리는 점은 여전하다. 비용이 감당이 된다는 전제 하에 트래픽이 몰릴 경우 HPA, 오토스케일링으로 대응 가능하니 이전보다 관리 지점이 제한되었다는 점에서 이점으로 작용할 듯 싶다.
Istio 이후의 이코에코 아키텍처에서 Auth와 유사한 파트가 있다면 ingress-gw 노드일 거다. 모든 트래픽을 라우팅하는 파트라 현재 구성처럼 단일 노드로 스케줄링을 제한하면 단일 장애 지점으로 꼽힐 가능성이 높다. 이 역시 이미 Deployment로 파드 풀을 관리하는 구조니 비용의 여유가 된다면 파드의 수를 늘려 ingress 전용 노드들 어디든 분산 배치시키면 된다.
반면 정책, 트래픽 제어 및 관측성은 Ingress GW 단일 코드 스펙으로 중앙화해서 관리가 가능하니 매 서비스별로 인그레스를 파고 Nodeport로 ALB->Pod로 보내던 방식에 비해 다룰 품이 줄어든다.- Auth Offloading (Succeed)
- Authentication 공통 모듈 dependecy로 수행하던 JWT 서명 인증을 각 도메인 서버의 Envoy Proxy에서 수행
- ext_authz로 모든 서비스 도메인 엔드포인트로 향하는 요청은 Auth 서버로 리다이렉팅 후 인증/인가 (gRPC)
- 클라이언트(프론트) 측 쿠키->헤더 변환 작업이 마무리되기 전까지 Istio Envoy에서 Cookie->Header 변환 필터 적용
- Auth를 제외한 도메인 서버에서 인증/인가 로직 제거, Cookie -> Header 기반 로직으로 변경
- Observability Offloading
- HTTP 메트릭 수집을 Envoy Proxy에서 수행 (Succeed)
- 로깅 시스템 구축이 완료되면 로깅 또한 Envoy Proxy로 이관 (In-Progress)
Istio Observability


Istio의 트래픽 제어 기능에 발맞추는 Observability 툴이 많다. 일단 파둔 건 kaili와 jaeger다.
이코에코의 app 라벨은 이미 도메인별로 분류가 되어있는 상황이니 따로 건드릴 건 없다. (version 라벨링을 추가 중이다.)
모니터링 툴이 갖춰진 상황이니 OpenTelemetry, label, metrics를 보강해 두면 이코에코 클러스터를 온전하게 시각화해준다.
드디어 mermaid 노동에서 탈출할 듯한 희망이 보여 상당히 기쁘다.. metrics 보강과 Observability 강화는 로깅 노드 구축과 함께 별도의 포스팅으로 기록하겠다. 당분간 이코에코의 kaili와 jaeger는 annonymous로 설정해서 별도 로그인 없이도 확인할 수 있도록 링크를 열어둔 상태니 궁금하다면 방문해도 좋다.
'이코에코(Eco²) > Kubernetes Cluster+GitOps+Service Mesh' 카테고리의 다른 글
이코에코(Eco²) Service Mesh #2: 내부 통신을 위한 gRPC 마이그레이션 (0) 2025.12.12 이코에코(Eco²) GitOps #06 - Namespace · RBAC · NetworkPolicy를 한 뿌리에서 (3) 2025.11.25 이코에코(Eco²) GitOps #05 Sync Wave (0) 2025.11.25 이코에코(Eco²) GitOps #04 Operator와 Helm-charts를 오가며 겪은 시행착오들 (0) 2025.11.24 이코에코(Eco²) GitOps #03 네트워크 안정화 (0) 2025.11.24