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는 그 자체만으로 주제를 따로 잡아도 긴 글이 나올 정도로 큰 주제이므로 여기서는 간단히만 살펴본다.

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을 하는 방법은 세 가지가 있다.
Sidecar리소스를 사용해서 특정 워크로드가 Configuration 집합을 가져오도록 설정할 수 있다. 이는 워크로드 쪽에서 설정해서 자신이 받아올 Configuration을 설정하는 방법이다.exportTo설정을 통해서 특정 워크로드로 Configuration을 내보내도록 설정할 수 있다. 이는 워크로드를 소유한 입장에서 자신의 Configuration을 어디로 보낼지 설정하는 방법으로VirtualService,DestinationRule,ServiceEntry에서exportTo를 사용할 수 있고Service에서는networking.istio.io/exportTo어노테이션으로 설정할 수 있다.- 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 로 요청을 보낸다면 다음과 같이 처리된다.
-
애플리케이션이
catalog.catalog.svc.cluster.local의 DNS를 조회하고 Kubernetes의 ClusterIP인10.96.20.30를 받는다. -
애플리케이션이
10.96.20.30:8080으로 연결을 시도한다. -
Pod의
iptables규칙이 outbound TCP 연결을 가로채서 Envoy virtualOutbound 15001 포트로 리다이렉션한다. -
Envoy virtualOutbound listener가 원래 목적지 주소를 확인한다.
-
HTTP protocol임을 확인하고 RDS의 virtual host와 route를 조회한다.
-
route가
outbound|8080||catalog.catalog.svc.cluster.local클러스터를 선택한다. -
EDS에서 실제
catalogPod의 endpoint인10.244.2.17:8080를 선택한다. -
DestinationRule의 로드밸런싱, TLS, retry 관련 설정 적용한다. -
catalogPod에 연결된다.
여기서 핵심은 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개만 있는 것은 아니므로 다음과 같은 순서로 적용된다.
- 같은 namespace에서
workloadSelector가 일치하는 Sidecar - 같은 namespace에서
selector가 없는 기본 Sidecar - root namespace에서 selector가 없는 기본 Sidecar
- 아무것도 없다면 mesh의 기본값
앞에서 설정한 spec.egress.hosts는 <configuration-namespace>/<service-host> 형식으로 정의한다. 다음 예시는 다음과 같다.
./*: 현재 namespace의 모든 서비스와 관련 구성catalog/*: catalog namespace의 모든 서비스 및 관련 구성external-services/api.vendor.example:external-servicenamespace의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으로 전달하기 전 |
Comments