Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Istio Sidecar로 서비스 구성 범위 좁히기

최근 Istio 스터디를 팀 동료들과 하고 있다. 업무에서 Istio를 어느 정도 알고는 있지만, 깊이 있는 내용은 알지 못한다. 업무 이해도를 높이려고 스터디를 하는데, 정해진 책의 진도를 나가는 방식이 아닌 자유 주제라 매번 주제를 정하기 어렵다. 그래서 최근에 이해도가 부족하다고 생각한 주제를 공부했다.

Istio Sidecar

Istio에는 Sidecar라는 리소스가 존재한다. 개인적으로 이 이름은 잘못 지은 용어라고 생각하고 있다. Kubernetes 환경에서 Sidecar라는 용어를 쓰면

  • Kubernetes에서 Pod에 앱 컨테이너 외에 추가 운영 기능을 위해 컨테이너를 추가하는 방법을 Sidecar 패턴이라고 부른다.
  • 위 패턴에 따라 Sidecar 패턴을 따르는 방식을 Istio에서는 Sidecar 모드라고 부르고 Sidecar가 없는 새로운 접근 방식을 Ambient Mode라고 부른다.
  • Sidecar 모드에서 Envoy proxy를 포함하고 있는 istio-proxy를 주입하는 걸 당연히 istio에서도 Sidecar Injection이라고 부른다. 그래서 의미상 istio-proxy를 Sidecar Proxy라고도 부를 수 있다.

여기서 얘기할 Sidecar는 Istio의 CRD인 Sidecar를 의미한다. 물론 이 Sidecar 리소스는 위에서 말한 istio-proxy와 관련 있어 이름이 틀린 것은 아니다. 다만 Sidecar의 의미가 너무 광범위해 실제로는 매우 헷갈리는 이름이라고 생각한다.

이 글에서는 이후 Sidecar는 Istio의 CRD를 의미하고, Sidecar Proxy는 istio-proxy로 지칭한다.

공식 문서는 다음과 같이 정의한다.

Sidecar는 연결된 워크로드 인스턴스의 인바운드 및 아웃바운드 통신을 중개하는 Sidecar Proxy의 구성을 설정한다. 기본적으로 Istio는 메쉬 내의 모든 Sidecar Proxy에, 메쉬 내의 모든 워크로드 인스턴스에 연결하는 데 필요한 구성과 워크로드와 관련된 모든 포트에서 트래픽을 수락하도록 설정합니다. Sidecar 구성을 사용하면 프록시가 워크로드로 향하거나 워크로드에서 오는 트래픽을 전달할 때 허용할 포트 및 프로토콜 집합을 세부적으로 조정할 수 있습니다.

xDS

Istio Sidecar 모드는 Envoy proxy를 기반으로 한다. Envoy proxy는 동적 구성 관리를 위한 API를 제공하며, 이러한 디스커버리 서비스 API를 xDS라고 부른다. xDS는 그 자체만으로 주제를 따로 잡아도 긴 글이 나올 정도로 큰 주제이므로 여기서는 간단히만 살펴본다.

Kubernetes와 Istio API를 감시한 Istiod가 xDS를 생성해 gRPC 스트림으로 istio-proxy에 전달한다.

Istiod가 이러한 정보를 수집해서 xDS로 각 istio-proxy에 전파한다. 그리고 Envoy는 Listener -> Route -> Cluster -> Endpoint 순으로 요청을 처리한다.

LDS - Listener Discovery Service

Envoy가 어떤 IP와 포트에서 트래픽을 받을지, 어떤 필터 체인을 적용할지 정의하며 다음과 같은 정보가 포함된다.

10.0.0.0:15001 virtualOutbound
20.0.0.0:15006 virtualInbound
310.96.20.30:8080 특정 서비스용 outbound listener

RDS - Route Discovery Service

HTTP Host, path, Header 등의 정보에 따라 어떤 클러스터로 보낼지를 정의한다.

1Host: catalog.catalog.svc.cluster.local
2Path: /v1/items 
3    -> outbound|8080|v1|catalog.catalog.svc.cluster.local

CDS - Cluster Discovery Service

Envoy에서 클러스터는 요청이 도달하는 업스트림 목적지를 정의한다.

1outbound|8080||catalog.catalog.svc.cluster.local outbound|8080|v1|catalog.catalog.svc.cluster.local
2PassthroughCluster
3BlackHoleCluster

EDS - Endpoint Discovery Service

각 클러스터의 실제 목적지 IP와 포트를 정의한다.

110.244.2.17:8080
210.244.3.21:8080

Configuration Scoping

Istio는 서비스 메시이므로 많게는 수백~수천 개의 마이크로서비스가 서로 통신한다. Istiod는 서비스 연결 방식과 적용 정책을 각 istio-proxy에 전파하고, 구성이 바뀌면 계속 업데이트한다.

Kubernetes에서 Istio를 사용하면 기본적으로 모든 네임스페이스의 정보를 모든 네임스페이스에 전달한다. 하지만 마이크로서비스 환경에서는 모든 서비스와 통신하는 서비스가 거의 없고, 대부분은 정해진 소수의 서비스와만 통신한다. 따라서 모든 구성을 알 필요 없이 실제로 통신할 네임스페이스 정보만 있으면 충분하다. 모든 정보를 전파하면 트래픽도 만만치 않고 시간이 오래 걸린다. 이 정보를 모두 유지하는 데도 비용이 든다.

그래서 원하는 구성만 선택해서 정보를 받아오는 것을 Configuration Scoping이라고 한다.

Istio에서 Configuration Scoping을 하는 방법은 세 가지가 있다.

  1. Sidecar 리소스를 사용해서 특정 워크로드가 Configuration 집합을 가져오도록 설정할 수 있다. 이는 워크로드 쪽에서 설정해서 자신이 받아올 Configuration을 설정하는 방법이다.
  2. exportTo 설정을 통해서 특정 워크로드로 Configuration을 내보내도록 설정할 수 있다. 이는 워크로드를 소유한 입장에서 자신의 Configuration을 어디로 보낼지 설정하는 방법으로 VirtualService, DestinationRule, ServiceEntry에서 exportTo를 사용할 수 있고 Service에서는 networking.istio.io/exportTo 어노테이션으로 설정할 수 있다.
  3. Istio의 전역 설정인 meshConfig에서 discoverySelectors를 사용하면 원하는 네임스페이스에만 Configuration이 자동으로 전파되도록 할 수 있다. discoverySelectors가 없으면 모든 네임스페이스에 전파한다. 다음은 discoverySelectors의 예시다.
1meshConfig: 
2  discoverySelectors: 
3    - matchLabels: 
4        # Allow any namespaces with `istio-discovery=enabled` 
5        istio-discovery: enabled
6    - matchLabels: 
7        # Allow "kube-system"; Kubernetes automatically adds this label to each namespace
8        kubernetes.io/metadata.name: kube-system

이 세 가지는 discoverySelectors -> exportTo -> Sidecar 순으로 적용되며, 모두 AND 조건으로 적용된다.

예시

다음과 같이 4개의 네임스페이스에 각각 서비스가 있는 상황을 생각해 보자.

 1frontend namespace 
 2    frontend service 
 3
 4catalog namespace 
 5    catalog service 
 6
 7payments namespace
 8    payments service 
 9
10observability namespace
11    tracing service

이때 frontend 서비스에는 다음과 같은 클러스터가 4개 생성된다.

1outbound|8080||frontend.frontend.svc.cluster.local
2outbound|8080||catalog.catalog.svc.cluster.local
3outbound|8080||payments.payments.svc.cluster.local
4outbound|4317||tracing.observability.svc.cluster.local

이때 frontend 네임스페이스의 frontend 서비스가 http://catalog.catalog.svc.cluster.local:8080/items 로 요청을 보낸다면 다음과 같이 처리된다.

  1. 애플리케이션이 catalog.catalog.svc.cluster.local의 DNS를 조회하고 Kubernetes의 ClusterIP인 10.96.20.30를 받는다.

  2. 애플리케이션이 10.96.20.30:8080으로 연결을 시도한다.

  3. Pod의 iptables 규칙이 outbound TCP 연결을 가로채서 Envoy virtualOutbound 15001 포트로 리다이렉션한다.

  4. Envoy virtualOutbound listener가 원래 목적지 주소를 확인한다.

  5. HTTP protocol임을 확인하고 RDS의 virtual host와 route를 조회한다.

  6. route가 outbound|8080||catalog.catalog.svc.cluster.local 클러스터를 선택한다.

  7. EDS에서 실제 catalog Pod의 endpoint인 10.244.2.17:8080를 선택한다.

  8. DestinationRule의 로드밸런싱, TLS, retry 관련 설정 적용한다.

  9. catalog Pod에 연결된다.

여기서 핵심은 DNS가 서비스 IP를 알려 주지만, Envoy는 Istiod에서 받은 listener, route, cluster, endpoint 구성으로 통신한다는 점이다.

그러면 frontend 네임스페이스에 Sidecar를 설정해 보자.

 1apiVersion: networking.istio.io/v1
 2kind: Sidecar 
 3metadata: 
 4  name: default
 5  namespace: frontend
 6spec: 
 7  egress: 
 8    - hosts: 
 9      - "./*"
10      - "catalog/*"

이렇게 구성하면 아까와 달리 아래 2개의 클러스터만 설정된다.

1outbound|8080||frontend.frontend.svc.cluster.local
2outbound|8080||catalog.catalog.svc.cluster.local

Sidecar는 꼭 1개만 있는 것은 아니므로 다음과 같은 순서로 적용된다.

  1. 같은 namespace에서 workloadSelector가 일치하는 Sidecar
  2. 같은 namespace에서 selector가 없는 기본 Sidecar
  3. root namespace에서 selector가 없는 기본 Sidecar
  4. 아무것도 없다면 mesh의 기본값

앞에서 설정한 spec.egress.hosts<configuration-namespace>/<service-host> 형식으로 정의한다. 다음 예시는 다음과 같다.

  • ./*: 현재 namespace의 모든 서비스와 관련 구성
  • catalog/* : catalog namespace의 모든 서비스 및 관련 구성
  • external-services/api.vendor.example : external-service namespace의 api.vendor.example 호스트의 서비스 및 관련 구성
  • */*: 현재 워크로드에서 볼 수 있도록 export된 전체 mesh 구성
  • ~/* : outbound 서비스 구성을 모두 제거

Sidecar의 핵심 필드

필드 역할 일반적인 사용 목적
workloadSelector 같은 namespace의 Pod/VM을 label로 선택 특정 애플리케이션만 별도 구성
ingress Envoy의 inbound listener 정의 포트 변경, UDS 전달, 명시적 TLS 종료
egress Outbound listener와 import할 서비스 구성 정의 proxy configuration 축소
inboundConnectionPool 서버 측 inbound connection pool 기본값 서버 과부하와 동시성 제어
outboundTrafficPolicy 알려지지 않은 outbound 목적지 처리 ALLOW_ANY 또는 REGISTRY_ONLY

참고로 egress에 설정하지 않았다고 바로 패킷을 차단하는 것은 아니고 outboundTrafficPolicy의 설정을 따르게 된다. 기본값인 ALLOW_ANY는 알려지지 않은(구성에 없어서 모르는) 목적지에 대한 요청도 그냥 통과시키고 REGISTRY_ONLY로 설정하면 알지 못하는 목적지에 대한 요청은 드랍한다.

주의할 점은 이 REGISTRY_ONLY를 보안 정책이나 아웃바운드 방화벽으로 사용하면 안된다. 이는 ServiceEntry 구성을 감지할 수 있는 방법을 제공하기 위해 오류를 발생시키도록 한 것이고 istio-proxy를 거치는 트래픽만 관여하기 때문에 istio-proxy를 거치지 않게 설정된 트래픽에는 전혀 적용되지 않는다.

ingress를 생략하는 경우 istiod가 워크로드와 Kubernetes Service의 정보를 보고 inbound listener를 자동으로 생성하므로 특별한 의도가 없다면 굳이 건드리지 않아도 된다.

아래는 앞에서 설명을 위해서 간단히만 다뤘기에 좀더 다양한 설정을 볼 수 있는 예시를 추가했다. 아래와 같이 연결에 대한 추가 설정을 할 수 있다.

 1apiVersion: networking.istio.io/v1 
 2kind: Sidecar 
 3metadata: 
 4  name: connection-pool-settings 
 5  namespace: prod-us1
 6spec: 
 7  workloadSelector: 
 8    labels: 
 9      app: productpage 
10  inboundConnectionPool: 
11    http: 
12      http1MaxPendingRequests: 1024
13      http2MaxRequests: 1024
14      maxRequestsPerConnection: 1024
15      maxRetries: 100
16  ingress: 
17    - port: 
18        number: 80 
19        protocol: HTTP 
20        name: somename 
21      connectionPool: 
22        http:
23          http1MaxPendingRequests: 1024 
24          http2MaxRequests: 1024 
25          maxRequestsPerConnection: 1024 
26          maxRetries: 100 
27        tcp: 
28          maxConnections: 100

보통은 연결에 대한 커넥션풀이나 재시도 설정을 DestinationRule에서만 다루어봤어서 Sidecar에 있는 설정과는 뭐가 다른지를 찾아봤다.

구분 DestinationRule.connectionPool Sidecar.inboundConnectionPool
주 적용 위치 호출자 측 Envoy의 outbound cluster 서버 측 Envoy의 inbound cluster
upstream 대상 원격 destination service 같은 Pod의 애플리케이션
선택 기준 DestinationRule.spec.host Sidecar.workloadSelector
주 목적 특정 호출자가 destination을 과도하게 호출하지 못하게 제한 서버 Pod 한 개가 전체 호출자로부터 받는 부하 제한
제한 단위 각 client Envoy의 outbound cluster별 각 server Envoy의 inbound port/cluster별
초과 시점 서버에 요청을 보내기 전 client Envoy에서 서버 Envoy까지 도착했지만 application으로 전달하기 전
Valid HTML5 Valid CSS WCAG 2.1 AA tested