Post

서비스 메시와 istio

서비스 메시와 istio 살펴보기

*CloudNet에서 주관하는 istio(Istio Hands-on Study) 1기 스터디입니다. 아래의 글은 스터디의 내용을 기반으로 작성했습니다.

들어가며

CloudNet에서 주관하는 Istio 스터디에 참여하게 되었다. 사실 istio에 대해서 간단한 핸즈온은 해봤지만, 제대로 공부해본 적은 없는 것 같다. 하지만 최근 업무 파트가 istio 운영도 포함될 예정이며 특히 사내에서는 istio를 조금 색다르게 사용하고 있는 것 같아 학습에 대한 필요성을 느껴 신청하게 됐다.

Istio궁금한 점은 아래와 같은데, 이번 스터디를 통해 궁금증을 해소하고 싶다.

  • Cilium에서 제공하는 L7 기능과 비교되는 istio의 장단점 (Ingress, Egress 기능을 제외하면 리소스를 많이 차지하는 istio를 쓸만한 이유가 있을까?)
  • 특히 온프레미스에선 어떤 장단점이 있을까?

이번 포스팅에서는 서비스 메시의 필요성과 istio에 대해 간단하게 알아볼 예정이다.

Service Mesh

서비스 메시는 마이크로 서비스간의 통신을 도와주는 별도의 인프라 레이어다. 마이크로 서비스 아키텍처를 반영하면서 서비스가 많아짐에 따라 별도의 애플리케이션 수정없이 트래픽을 관리가 필요했다. 만약, 애플리케이션에서 이를 구현하고 관리한다면 언어별로 프레임워크 별로 이를 개별 구성해야한다. 오버헤드가 크기에, 별도의 인프라 레이어를 통해 해결하고자 했고, 이 레이어를 서비스 메시라고 부른다.

istio 공식 docs에서도 해당 부분을 언급한다. “Istio addresses the challenges developers and operators face with a distributed or microservices architecture.”

서비스 메시를 도입하면 전체적인 네트워크 모니터링이 가능하며, 네트워크를 제어할 수 있다. 하지만, 서비스 메시를 도입하면 네트워크 홉이 하나이상 더 추가되므로 무겁다. (아래에 사진만 봐도..) 그렇기에 서비스 메시를 도입하기전에 현재 환경에 정말 적합한지 충분한 고민이 필요해보인다.

image.png

출처: https://www.redhat.com/ko/topics/microservices/what-is-a-service-mesh

Istio

Istio는 서비스 메시 구현체 중 하나이다. Istio는 서비스 메시가 마땅히 갖춰야할 기능인 “마이크로서비스 전반의 트래픽 흐름을 관리, 정책 설정, 데이터 수집”이 가능하다.

“마이크로서비스 전반의 트래픽 흐름을 관리”에는 무엇이 있을까? 예를 들어, 서킷브레이커retry같은 기능이 있다. 이를 애플리케이션에 설정하지 않아도, istio에서 구현이 가능하다. 덕분에 개발자는 서비스 구현에만 집중할 수 있으며 istio는 각 통신을 mTLS방식으로 진행하기에 해커의 위협으로부터 안전하다.

구조

구조는 각 애플리케이션을 제어할 수 있는 전용 프록시를 두고, 프록시에 대한 설정을 중앙 데몬에서 제어하는 형태이다. 여기서 간략하게 언급하면 Proxy의 설정은 API 방식으로 제어 가능하다. 별도의 restart가 필요없다.

image.png

출처: https://istio.io/latest/docs/ops/deployment/architecture/

동작 방식

동작 방식은 프록시로 구분할 수 있다. 파드 당 하나의 프록시를 두는 sidecar 모드와 노드 당 전용 프록시를 두는 Ambient 모드가 있다.

image.png

출처: https://www.solo.io/blog/istio-more-for-less

구성

Istio는 Control Plane과 Data Plane으로 구성된다.

  • Control Plane: Istiod로 프록시 라우팅 규칙을 관리, 보안 및 암호화, Kubernetes와 상호 작용한다.
  • Data Plane: 실제 애플리케이션 바로 앞에 있는 프록시 집합을 의미하며, 사이드카 모드에서는 각 파드마다 sidecar proxy(envoy)가 존재한다.

Control Plane

Istiod는 Golang으로 작성되었고 Pilot, Galley, Citadel 3개의 요소가 존재한다. Pilot은 프록시 라우팅 설정을 관리하며, Galley는 Endpoint 갱신 등 K8s와 상호작용하며, Citadel은 보안 및 암호화 관련 기능을 담당한다.

아래의 그림을 통해 각 구성요소의 역할을 확인할 수 있다.

image.png

출처: https://istio.io/v1.4/docs/ops/deployment/architecture/arch.svg

Data Plane

Data Plane은 프록시 집합을 의미한다. Istio inject이 enables된 네임스페이스에서 생성된 모든 파드는 istio-proxy 컨테이너가 사이드카로 함께 생성된다. istio-proxy 내부에선 Envoy를 사용하고 있다.

*Envoy에 대한 설명은 조금 뒤에서 살펴볼 예정이다.

Istiod(control plane)에서 쿠버네티스 이벤트를 감지하고 이와 관련된 설정을 각 프록시에 푸시한다. 파드의 모든 네트워크는 프록시를 통해서 제어되기에 최종적으로 istio는 허용되는 모든 트래픽을 제어할 수 있다.

image.png

출처: https://www.solo.io/topics/istio/istio-architecture

이제 프록시로 사용하는 Envoy에 대해 살펴보자.

프록시

Istio는 프록시로 Envoy를 사용한다. Envoy는 L7 프록시로 아래와 같은 목적성을 가진다.

The network should be transparent to applications. When network and application problems do occur it should be easy to determine the source of the problem.”

image.png

출처: https://www.envoyproxy.io/docs/envoy/latest/intro/life_of_a_request

Envoy에 대해서는 2주차에 상세하게 배울 예정이라, 여기서는 간단하게 살펴본다.

방식

Envoy는 멀티 스레딩을 사용하며, 각 서비스에 대해 Listener를 통해 요청을 수신하며 이후 HTTP Router Filer를 통해 대상 클러스터로 옮기고 클러스터에서 실제 엔드포인트(Pod)로 라우팅한다. “Listeners > Routes > Clusters > Endpoints”

image.png

설정 제어

Envoy는 설정 방식으로 xDS를 사용한다. 덕분에 외부에서 Envoy의 설정을 동적으로 변경할 수 있다. 이 덕분에 중앙(istiod)에서 모든 프록시의 설정을 다운타임없이 제어할 수 있다. 관련하여 자세한 내용은 Envoy Docs에서 확인할 수 있다.

image.png

출처: (스터디원) https://kimdoky.github.io/devops/2025/04/10/study-istio-week1/

xDS Sync API는 아래와 같은 여러 방식이 있다.

  • LDS - Listener Discovery Service
  • RDS - Route Discovery Service
  • CDS - Cluseter Discovery Service
  • EDS - Endpoint Discovery Service

실습

실습 환경: docker (kind - k8s 1.23.17 ‘23.2.28 - Link) , istio 1.17.8(’23.10.11) - Link

환경은 최신버전이 아닌데, 그 이유는 Istio in Action의 번역판이 과거 개정판의 내용을 담고 있고 이를 맞추기 위함이다.

kind

실습 환경으로는 kind(kubernetes in docker)를 사용한다. kind를 통해 로컬에서 쿠버네티스와 가장 유사한 환경을 구축할 수 있다는 장점이 있다.

kind 설치(macOS)

윈도우 환경이라면, 문서를 참고

1
brew install kind

클러스터 구성

단일 노드 클러스터를 구성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kind create cluster --name myk8s --image kindest/node:v1.32.2 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000 # Sample Application
    hostPort: 30000
  - containerPort: 30001 # Prometheus
    hostPort: 30001
  - containerPort: 30002 # Grafana
    hostPort: 30002
  - containerPort: 30003 # Kiali
    hostPort: 30003
  - containerPort: 30004 # Tracing
    hostPort: 30004
  - containerPort: 30005 # kube-ops-view
    hostPort: 30005
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
EOF

노드 컨테이너에 실습에 필요한 유틸리티를 설치한다.

1
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bridge-utils net-tools dnsutils tcpdump ngrep iputils-ping git vim -y'

istio

istio를 설치하기 앞서, 우선 노드에 접속한다.

1
docker exec -it **myk8s****-control-plane bash**

istioctl 설치

1
2
3
4
export ISTIOV=1.17.8
echo 'export ISTIOV=1.17.8' >> /root/.bashrc
curl -s -L https://istio.io/downloadIstio | ISTIO_VERSION=$ISTIOV sh -
cp istio-$ISTIOV/bin/istioctl /usr/local/bin/istioctl

아래의 명령어로 정상 설치여부를 확인할 수 있다.

1
2
istioctl version --remote=false
1.17.8

istio 배포

1
istioctl install --set profile=default -y

배포 확인

핵심 파드(istiod, ingress)에는 pdb(pod disruption budget)까지 들어가있다는 것을 볼 수 있었다.

image.png

설정 진행

우리가 사용할 네임스페이스에, istio-injection(sidecar 주입) 레이블을 달아둔다.

1
2
3
kubectl get ns istioinaction --show-labels
NAME            STATUS   AGE   LABELS
istioinaction   Active   82s   istio-injection=enabled,kubernetes.io/metadata.name=istioinaction

아래와 같이 mutating config가 설정되는 것을 볼 수 있다. (mutating를 모른다면, Admission Controller Docs를 확인해보는 것을 추천한다.)

1
2
3
4
kubectl get mutatingwebhookconfiguration
NAME                         WEBHOOKS   AGE
istio-revision-tag-default   4          6m9s
istio-sidecar-injector       4          6m29s

서비스 배포

1
2
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction
1
2
3
NAME                     READY   STATUS    RESTARTS   AGE
catalog-6cf4b97d-mt6q6   2/2     Running   0          4m39s
webapp-7685bcb84-6vcvg   2/2     Running   0          4m35s

실제 yaml을 확인해보면 아래와 같이 하나의 컨테이너만 존재한다. 하지만 파드마다 컨테이너가 1개가 아닌 2개인 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
  "containers": [
    {
      "env": [
        {
          "name": "KUBERNETES_NAMESPACE",
          "valueFrom": {
            "fieldRef": {
              "apiVersion": "v1",
              "fieldPath": "metadata.namespace"
            }
          }
        }
      ],
      "image": "istioinaction/catalog:latest",
      "imagePullPolicy": "IfNotPresent",
      "name": "catalog",
      "ports": [
        {
          "containerPort": 3000,
          "name": "http",
          "protocol": "TCP"
        }
      ],
      "resources": {},
      "securityContext": {
        "privileged": false
      },
      "terminationMessagePath": "/dev/termination-log",
      "terminationMessagePolicy": "File"
    }
  ],
  "dnsPolicy": "ClusterFirst",
  "restartPolicy": "Always",
  "schedulerName": "default-scheduler",
  "securityContext": {},
  "serviceAccount": "catalog",
  "serviceAccountName": "catalog",
  "terminationGracePeriodSeconds": 30
}

istio proxy로 추가된 컨테이너를 확인해보자.

1
2
3
docker exec -it myk8s-control-plane bash
# 접속 후
crictl ps

아래의 사진처럼 istio-proxy(envoy)가 추가된 것을 확인할 수 있다.

image.png

Ingress gateway 배포

아래와 같이 webapp으로 라우팅되는 Ingress gateway 규칙을 설정한다. 그러면 istio-Ingress로 인입된 트래픽 중 HTTP(:80)으로 들어온 트래픽은 webapp으로 향하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
cat <<EOF | kubectl -n istioinaction apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: outfitters-gateway
  namespace: istioinaction
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: webapp-virtualservice
  namespace: istioinaction
spec:
  hosts:
  - "*"
  gateways:
  - outfitters-gateway
  http:
  - route:
    - destination:
        host: webapp
        port:
          number: 80
EOF

1
docker exec -it myk8s-control-plane istioctl proxy-status

명령어로 정보를 확인해보면 istio.ingressgateway의 RDS(routing 관련) 규칙이 NOT SENT → SYNCED로 바뀐 것을 확인할 수 있다.

image.png

이제 istio의 ingress gateway 서비스 nodeport를 30000으로 지정한다.

1
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort", "ports": [{"port": 80, "targetPort": 8080, "nodePort": 30000}]}}'

localhost:30000으로 접근하면 아래와 같이 webapp 서비스에 접속할 수 있다. (본인은 ec2로 두고 실습을 진행하였기에, ec2 public ip로 접근했다.)

image.png

retry 적용해보기

이제 catelog에게 3번까지 재시도를 하는 옵션을 넣어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat <<EOF | kubectl -n istioinaction apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog
spec:
  hosts:
  - catalog
  http:
  - route:
    - destination:
        host: catalog
    retries:
      attempts: 3
      retryOn: 5xx
      perTryTimeout: 2s
EOF

catelog 관련하여 확인을 진행한다.

1
2
3
4
kubectl get vs -n istioinaction
NAME                    GATEWAYS                 HOSTS         AGE
catalog                                          ["catalog"]   110s
webapp-virtualservice   ["outfitters-gateway"]   ["*"]         56m

retry 옵션을 넣고 나서는, 대부분 성공하는 것을 볼 수 있다.

image.png

다만, retry를 진행함에 따라 응답속도는 기존보다 늦어지는 것도 확인할 수 있다.

image.png

Traffic Routing

새로운 기능을 추가했을 때, 특정 유저집단만 새 배포버전으로 라우팅되도록 단계적으로 조절할 수 있다.

여기서는 새로운 버전의 앱을 배포한 후, istio 기능을 통해 트래픽을 기존 버전(v1)과 새로운 버전(v2)으로 조절해보는 실습을 진행한다.

실습에 들어가기 앞서, 스터디원분이 istio 리소스 관계도를 이쁘게 그려주셨다.

“Gateway → Virtual Service → DestinationRule → Pod”

image.png

출처: https://hackjsp.tistory.com/85

catalog 앱 v2 배포

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
cat <<EOF | kubectl -n istioinaction apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: catalog
    version: v2
  name: catalog-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: catalog
      version: v2
  template:
    metadata:
      labels:
        app: catalog
        version: v2
    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
EOF

라우팅 규칙 생성

아래와 같은 규칙을 생성하면, 우선 공평하게 2개의 서브셋으로 트래픽을 분배한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat <<EOF | kubectl -n istioinaction apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: catalog
spec:
  host: catalog
  subsets:
  - name: version-v1
    labels:
      version: v1
  - name: version-v2
    labels:
      version: v2
EOF
  • 트래픽 분배 확인
1
while true; do curl -s http://127.0.0.1:30000/api/catalog | jq; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done

Virtual Service 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat <<EOF | kubectl -n istioinaction apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog
spec:
  hosts:
  - catalog
  http:
  - route:
    - destination:
        host: catalog
        subset: version-v1
EOF

마치며

이번 주차는 서비스 메시와 istio에 대해 간략하면서도 넓게 살펴봤다. 실제 istio 환경을 구성해보고 retry, traffic routing과 같은 실습도 진행을 해봤다. envoy, istiod 동작방식, security, 트러블 슈팅 등에 앞으로 각 파트에 대해 자세하게 다룰 예정이다.

This post is licensed under CC BY 4.0 by the author.