Istio 란 무엇이고, 무엇을 할 수 있을까?

김세환·2023년 12월 11일
0
post-thumbnail

코드 예제는 https://github.com/kimsehwan96/istio-example 에서 볼 수 있습니다.
이글에서는 엠비언트 메시에 대해서는 다루고 있지 않습니다.

TL;DR

애플리케이션 코드의 변경 없이 마이크로 서비스의 가시성 확보, 트래픽 매니징(L7 라우팅), 보안등의 기능을 추가, 확장 할 수 있는 솔루션입니다. 이것은 Sidecar 패턴에 의해 구현되고 최근에는 이것으로 인한 overhead 를 줄이기위한 istio ambient mesh 등이 소개되고있지만, 여기서는 Envoy Proxy를 사이드카형태로 파드에 배치하여 사용하는 방식에 대해 설명합니다.

즉, 애플리케이션 레이어 에서 구현되었던 기능들을 인프라 레이어 로 디커플링하는 방법이라고 생각하시면 편합니다.

Service Mesh 란?

서비스메시는 애플리케이션 코드의 변경 없이 observability(가시성 → log / metrics / trace)와 traffic management(트래픽 관리) 그리고 보안을 담당할 인프라 계층으로 정의됩니다.

그렇다면 MSA 구조에서는 DNS(+ 서비스 디스커버리)와 로드밸런서로 관리가 충분 할탠데 왜 서비스메시를 워크로드에 적용하고 있을까요? 여기에 대한 답은 가시성, 트래픽관리, 보안 그리고 계층이라는 4가지 키워드에 담겨있습니다.

가시성(Observability)

가시성을 구성하는 3대 요소에는 로그, 메트릭, 트레이스가 있습니다. 이 3가지는 각각 다른 방법으로 수집되고 다른 용도로 사용되지만, 각각 또는 함께 보았을 때 인프라 및 애플리케이션에 대한 중요한 인사이트를 제공하고 디버깅 포인트를 제공해줍니다.

매우 중요한 기능이지만 기존에는 직접 로깅, 메트릭수집 그리고 트레이스 관련 코드를 개발자가 코드로 작성해야 사용 가능한 기능이였습니다. 애플리케이션 로그의 경우는 Elasticsearch 를 사용하는 ELK 스택이라든지, AWS Cloudwatch , 데이터독 과 같은 솔루션을 도입해서 활용하면 되지만 마이크로서비스 환경에서 마이크로서비스들끼리 어떻게 트래픽을 주고받는지에 대한 로그는 따로 확보하기가 쉽지 않았습니다. 메트릭과 트레이싱도 마찬가지구요.

서비스메시는 애플리케이션 “바깥” 에서, 즉 애플리케이션의 코드를 수정하지 않고도 “인프라”레이어에서 가시성을 확보 할 수 있습니다. Istio는 내부적으로 Envoy의 강력한 Observability 기능 덕분에 매우 쉬벡 로그, 메트릭, 트레이싱을 구현 할 수 있었고 이와 같은 이유로 AWS App Mesh, Hashicorp Consul 과 같은 다른 서비스메시 솔루션들 또한 내부적으로 Enovy를 사용하고 있습니다.

트래픽 관리

서비스메시는 서비스간의 통신을 컨트롤 합니다. 여러팀에서 독립적으로 마이크로서비스를 개발, 배포하는 동적인 환경에서 트래픽을 어떤 서비스로 보내야 하는지를 애플리케이션 내부에서 관리한다면 경로가 바뀔 때 마다 애플리케이션을 매번 새로 배포해줘야 할 수도 있습니다. Istio는 Sidecar Pattern 을 통해서 애플리케이션 코드의 변경 없이 작업자가 VirtualService 와 DestionationRule 이라는 Istio 의 Custom Resource를 통해서 원하는 서비스로 트래픽을 보낼 수 있게 만들어줍니다. 또한 VirtualSerice 의 weight, subset 항목을 통해 Canary 배포등의 작업을 수행할 수 있습니다. (유의 : 여기서의 카나리배포는, 카나리배포를 적용하고자하는 기존 파드, 신규파드등이 이미 존재하고 있을때 트래픽 레벨에서 유입량을 조절해주는 것이므로, 파드 자체를 카나리배포하는것은 작업자, 혹은 ArgoCD 같은 별도 Operator 등을 사용해야 할 필요가 있습니다.)

물론 순수 K8s 만을 이용해 Canary 구현은 replica 수를 조절하면서 가능하겠지만, 이 과정이 매우 간단해지는것이고 Envoy의 기능을 활용해서 Header / Path 기반의 L7 라우팅 또한 매우 쉽게 설정 할 수 있습니다.

보안

서비스메시라는 개념은 마이크로서비스로의 전환이라는 업계 트렌드 속에서 등장했습니다. 하나의 모놀로식 구조로 구현되고, 내부 컴포넌트간의 Function Call 이였던 것들이 독립적인 마이크로서비스 컴포넌트로 분리되고, 각 컴포넌트(서비스)간 통신으로 동작하게 되자 Man in the middle 등의 공격의 가능성에 대해서도 대비할 필요가 생겼습니다.

따라서 많은 서비스 메시들은 서비스를 인증(authn), 인가(authz)하는 기능들을 제공하고있습니다. 대표적인 기능이 istio 에서 매우 쉽게 설정 할 수 있는 mTLS(Mutual TLS) 설정입니다. 이를 통해 Isitio Control Plane이 알지 못하는 서비스와는 아예 통신을 할 수 없게 막을 수 있으며, Policy를 통해 어떤 서비스가 어떤 서비스와 통신 할수 있는지 없는지 여부 또한 코드화 해서 관리 할 수 있습니다. 이 모든것이 “애플리케이션 코드의 변경 없이” 클러스터 보안을 강화 할 수 있습니다.

계층(layer)

“애플리케이션 코드의 변경 없이”가 서비스메시가 주는 특별한 가치라고 볼 수 있습니다. 서비스 메시 없이도 트래픽관리, 보안, 가시성 모두 애플리케이션 코드, 혹은 쿠버네티스 설정으로 구현 해 낼 수 있습니다. 하지만 애플리케이션 레이어와 인프라(istio)레이어를 분리하는 “디커플링” 과정을 통해 관심사의 분리를 정확하게 해낼 수 있습니다. 이에 따라 각 개발자가 집중해야 할 대상이 명확해지고, 생산성을 높일 수 있습니다.

또한 애플리케이션 코드와 서비스 메시 영역은 “애플리케이션 계층”, “인프라 계층” 으로 디커플링 되어있기 때문에 동일한 워크로드를 Istio 가 아닌 App Mesh, Consul, Linkerd 와 연동한다고 해도 애플리케이션 코드는 변경이 필요 없습니다.

Istio 는 어떻게 동작하는가?

서비스 메시를 이야기 할 때에는 Envoy 이야기를 하지 않을 수 없습니다. Linkerd를 제외한 Istio, Consul, App Mesh, Kong Mesh 등 대부분의 서비스메시는 모두 Envoy를 기반으로 만들어졌습니다.

Envoy는 대사, 또는 사절 즉 메시지를 전달해주는 사람이라는 영단에서 나왔는데, 대리인이라는 뜻의 Proxy와 의미상 유사해서 채택하였다고 합니다. 구글에서는 Istio 프로젝트를 준비중일 때 Envoy의 존재를 몰랐고, Nginx 를 sidecar proxy로 쓰는 방향으로 진행하다가 Envoy의 존재를 알게되고 메인테이너/개발자의 동의를 얻고 Istio 에 Envoy를 사용하기로 했습니다.

Envoy의 가장 큰 특징은 가시성이 뛰어나고 성능이 우수하며, API를 통해 실시간 설정 업데이트가 가능하다는 점입니다. 이 점이 Nginx 와 가장 큰 차이점입니다. Nginx의 경우 설정이 변경되었을 때 conf 파일을 수정하고 reload를 수행하거나, 유료 서비스인 Nginx Instance Manager 를 사용해서 동일한 작업을 해줘야 하지만. Envoy는 API를 통해 동적으로 신규 설정을 적용 할 수 있습니다.

Istio 의 경우 VirtualService , DestinationRule 과 같은 리소스가 생성되면 이를 Envoy 설정으로 변경해서 Envoy Proxy 에 전파해주는 방식으로 동작합니다.

Sidecar

우리는 앞서 Istio가 Sidecar Pattern 형태로 Istio(Enboy) Proxy를 사용해서 서비스메시를 구현했다고 했습니다.

파드는 쿠버네티스의 애플리케이션/워크로드의 기본 구성 요소입니다. 쿠버네티스는 컨테이너 대신 파드를 관리하고, 파드는 컨테이너를 캡슐화 합니다. 파드에는 하나 이상의 컨테이너, 스토리지, IP주소 및 컨테이너가 실행되는 방식을 제어하는 옵션이 포함 될 수 있습니다.

일반적으로 하나의 컨테이너를 포함하는 파드는 가장 일반적인 쿠버네티스 사용 사례이며, 여러 컨테이너를 포함하는 파드또한 사용됩니다. 다중 컨테이너 파드는 몇가지 패턴이 있는데 이 중 하나가 사이드카 컨테이너 패턴입니다.

사이드카 컨테이너는 파드의 기본 컨테이너와 함께 실행되는 컨테이너입니다. 사이드카 패턴은 현재 컨테이너의 기능을 변경하지 않고 확장합니다. 즉 애플리케이션의 코드 수정 없이 애플리케이션 컨테이너의 로깅을 붙이거나, 프록시를 붙이는 등 기능을 확장하기 위해서 사용됩니다.

위는 실제 애플리케이션 파드에 애플리케이션 컨테이너와, isito-proxy 컨테이너가 사이드카 패턴으로 동시에 떠있는 모습입니다.

그럼 여기서 파드에 어떻게 여러 컨테이너가 붙고, 같이 상호작용이 가능한지 궁금 할 수 있습니다. 무언가가 파드라는 추상화된 그룹으로 컨테이너들을 묶어주고있다고 추측이 가실겁니다.

쿠버네티스에서 파드를 생성하게되면, 우리가 생성하라고 이야기 하지도 않았지만 pause 컨테이너라는 것도 같이 생성됩니다.

이건 여러개의 컨테이너에 대해서 동일한 자원 환경, 다시말하면 동일한 network namespace, volume mount 등을 공유해서 사용하기 위해서 필요합니다. pause 컨테이너는 Pod 내부의 컨테이너들을 위한 일종의 부모 컨테이너 역할을 수행합니다. 이 pause 컨테이너는 크게 두가지 역할을 하는데. 첫 번째, pod의 컨테이너들이 리눅스 namespace를 공유할 수 있도록 합니다. (리눅스 네임스페이스에는 PID, 네트워크, UTS, Mount, IPC, cgroup 등이 있습니다.).

두 번째로는 PID 1 (init process)로서의 역할을 하고, 좀비 프로세스를 거둬들이기도 합니다. 그냥 이 Pod라는 그룹의 부모 프로세스/컨테이너 역할을 하는거라고 간단하게 생각하면 되겠습니다.

이렇게 pause 컨테이너가 파드 내의 여러 컨테이너들의 network, PID, IPC, UTS 등 네임스페이스를 공유해서 사용 할 수 있게 해주기 때문에, 해당 파드 내의 컨테이너는 해당 파드 안에서 동일한 네트워크 설정을 공유하게되고, localhost 로 컨테이너간 통신도 가능하게 됩니다.

우리가 특정 애플리케이션 파드에 sidecar 형태로 istio-proxy를 붙이도록 설정하면 istio의 webhook을 통해 istiod 의 API를 호출하게 되고, 우리 애플리케이션 파드에 init container 와 istio-proxy 컨테이너를 주입하게 됩니다. init container 는 다른 컨테이너들이 생기기 전에 먼저 수행되며, 트래픽을 제어하기 위한 iptable 작업을 수행합니다.

https://github.com/istio/cni/blob/master/tools/packaging/common/istio-iptables.sh

여기서 istio-iptables 명령어를 통해 해당 파드로 들어는 트래픽을 모두 istio-proxy 로 프록싱하도록 설정합니다.

(자세한 내용은 : istio proxy의 작동원리 )

이렇게 istio-proxy 가 설정되어있는 파드로 들어오는 트래픽은 애플리케이션 컨테이너로 인입되기 전 무조건 istio-proxy(envoy)를 거치게 되고, envoy proxy 의 설정에 따라서 애플리케이션의 특정 경로로 라우팅 한다든지, 인증처리를 한다든지, mTLS 검증을 한다든지 등의 기능 확장을 애플리케이션 코드 없이 할 수 있게 됩니다.

Istio 의 구성요소

Istio 서비스 메시는 논리적으로 데이터 플레인과 컨트롤 플레인으로 나뉩니다.

데이터 플레인은 사이드카로 배포된 프록시(Envoy)집합으로 구성됩니다. 이 프록시들은 마이크로 서비스간의 모든 네트워크 통신을 중개하고 제어하며, 트래픽에 대한 telemetry를 수집합니다. (가시성)

컨트롤 플레인은 트래픽을 라우팅하도록 프록시를 구성하고 관리합니다. 실제 트래픽을 처리하는 부분이 아닌 프록시들을 관리하기위한 것 이라고 보면 됩니다.

Envoy

Istio는 확장된 버전의 Envoy 프록시를 사용하며, Envoy는 서비스메시의 모든 서비스에 대한 인바운드, 아웃바운드 트래픽을 중개 합니다. Envoy 프록시는 데이터 플레인 트래픽과 상호작용하는 유일한 Istio 의 컴포넌트입니다. Sidecar 형태로 배포되고 서비스 디스커버리, 로드밸런싱, TLS Termination, 서킷브레이커, Health Check, 가시성 확보 등 다양한 기능들을 애플리케이션 코드 변경 없이 확장 할 수 있습니다.

Istiod

Istiod는 서비스 디스커버리, 프록시 구성 및 관리, 인증서 관리등을 수행합니다. Istiod는 트래픽을 제어하는 라우팅 규칙을 Envoy 전용 설정으로 변환하고, 런타임 사이드카(Envoy들) 에게 전파합니다. 또한 CA 역할을 해서 데이터플레인에서 mTLS 통신을 하기 위한 인증서를 생성합니다. Istio 에서 mTLS 로 마이크로서비스간 서버/클라이언트 상호 TLS 인증을 할 때 모두 Istiod CA 가 서명한 인증서를 갖고 인증합니다.

Istio 가 할 수 있는 것

Traffic Management

Blue-Green 배포, 카나리 배포, A/B 테스트, 서킷 브레이커, Fault injection 등 다양한 제어 기능을 사용 할 수 있습니다. 또한 보통 API Gateway 라고 부르는, 혹은 쿠버네티스 Ingress 와 비슷한 기능또한 제공 할 수 있습니다.

기존 쿠버네티스 클러스터에서 서비스를 노출하기위해서는 NodePort, LoadBalancer 와 같은 쿠버네티스 서비스 오브젝트를 생성하여 노출하거나, Kubernetes Ingress 를 사용하여 노출하였습니다.

우선 쿠버네티스의 서비스 오브젝트로 생성하는 NodePort 의 경우는 워커노드의 머신의 특정 포트를 노출하여서 외부에서 서비스 접근을 할 수 있게 해주지만, 서비스 오브젝트 뒷단의 파드가 어떻게 있건 단순한 로드밸런싱 정도밖에 해줄 수 없습니다.

LoadBalancer 오브젝트의 경우 생성 할 때 마다(클라우드 기준) 클라우드의 로드밸런서 리소스와 1:1 매칭이 되는데, 만약 노출해야하는 서비스가 100개라면 어떻게 해야 할까요? 클라우드에 존재하는 로드밸런서를 100개를 만드는것은 비용면에서도, 관리 측면에서도 맞지 않아 보입니다.

위와 같이 로드밸런서 를 여러개 만들지 않고서 단일 로드밸런서 를 통해 트래픽을 라우팅하고자 도입된 개념이 Kubernetes Ingress 입니다. Ingress Controller 에는 대표적으로 Nginx 가 있으며, Istio 도 이렇게 사용이 가능합니다.

위와 같이 ingress controller 를 통해 생성된 단일 로드밸런서를 통해 서비스를 노출 할 수 있습니다.

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: ingress
spec:
  rules:
  - host: httpbin.example.com
    http:
      paths:
      - path: /status/*
        backend:
          serviceName: httpbin
          servicePort: 8000
  - host: app.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: app
          servicePort: 8080
  - host: grafana.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: grafana
          servicePort: 20001
...
...

위와 같은 ingress 를 통해 단일 로드밸런서에서 host 및 path 기반의 트래픽 라우팅(L7)이 가능합니다. istio 또한 위와 같은 방식으로 사용 가능합니다.

Istio 는 위와 같은 방식, 즉 Ingress Controller 로 사용 가능하지만 더 나아가 Istio Gateway 라는 개념으로 확장시켰습니다. Istio Gateway 리소스는 클러스터와의 North-South 트래픽을 담당한다는 점에서 Kubernbetes Ingress 와 비슷하게 동작하지만, 기타 Istio 에서 지원하는 더 다양한 기능 (Canary / Blue-Green 배포등을 지원하기 위한 subset, weight 개념등)을 지원 할 수 있게 됩니다.

Istio Gateway 리소스는 L4 ~ L6 에서 동작하며, TLS Termination, 포트 노출등의 작업을 하고 VirtualSerivce 라는 리소스를 통해 L7 에서 구성 할 수 있는 버전 기반의 트래픽 라우팅, 오류 주입, 리다이렉션, rewrite 기타 다양한 라우팅 규칙을 사용 할 수 있습니다.

apiVersion: v1
kind: Namespace
metadata:
  name: httpbin
---
apiVersion: v1
kind: Pod
metadata:
  namespace: httpbin
  name: httpbin
  labels:
    app: httpbin
    sidecar.istio.io/inject: 'true'
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: httpbin
    image: kennethreitz/httpbin
---
apiVersion: v1
kind: Service
metadata:
  namespace: httpbin
  name: httpbin-service
spec:
  type: ClusterIP
  selector:
    app: httpbin
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - hosts:
        - 'app.hayden.com'
      port:
        name: http
        number: 80
        protocol: HTTP
      tls:
        httpsRedirect: false
    - hosts:
        - httpbin.local.dev
      port:
        name: https
        number: 443
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: httpbin-cert
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin-virtualservice
spec:
  gateways:
    - httpbin-gateway
  hosts:
    - 'app.hayden.com'
  http:
    - match:
        - uri:
            prefix: /
      route:
        - destination:
            host: httpbin-service.httpbin.svc.cluster.local
            port:
              number: 80

위와 같이 gateway 리소스는 istio : ingressgateway 를 select하게 되는데, 해당 레이블이 있는 파드가 ingress controller 역할을 하게 됩니다.

mTLS

mTLS 는 기본적으로 활성화 되어있습니다. 따라서 istio-proxy 가 사이드카로 배포된 마이크로서비스 끼리는 추가적인 인증/인가 정책을 붙이지 않는 한 보호된 상태로 서비스간 트래픽을 주고 받게 됩니다.

위는 제가 테스트중인 환경에서, kiali 라는 istio용 대시보드를 통해 서비스간 연결을 보는 부분입니다. 각 마이크로 서비스간 통신에 자물쇠가 채워져 있는 것이 보이는데, 디폴트로 mTLS 가 enabled 된 상태이기 때문입니다. 다만, mTLS 가 enabled 되어있지만 기본옵션으로 mTLS 트래픽 및 plain text 트래픽 모두 받도록 설정이 되어있기 때문에, 이 상태에서 제가 istio-proxy 사이드카가 배포되지 않은 서비스에서 node-test-server 서비스를 한번 호출해보겠습니다.

apiVersion: v1
kind: Namespace
metadata:
  name: test
---
apiVersion: v1
kind: Pod
metadata:
  namespace: test
  name: httpbin-test
  labels:
    app: httpbin-test
    sidecar.istio.io/inject: 'false'
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: httpbin
    image: kennethreitz/httpbin

위는 트래픽을 보내볼 테스트용 파드(httpbin-test)를 서술하였습니다. 사이드카 인젝션을 명시적으로 거부하도록 하고 위 파드를 배포해보겠습니다.

위에서 보는 것 처럼 이 파드에는 istio-proxy 사이드카 컨테이너가 붙어있지 않습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-test-server-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: node-test-server
  template:
    metadata:
      labels:
        app: node-test-server
        version: v1
        sidecar.istio.io/inject: 'true'
    spec:
      containers:
        - name: node-test-server
          image: kimsehwan96/node-server
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
              name: http

위는 트래픽의 대상이 될 node-test-server 라는 이름의 Deployment 오브젝트를 구성한 내용입니다.

sidecar.istio.io/inject: 'true' 을 통해 istio-proxy 를 사이드카로 주입합니다. 그러면 이 애플리케이션 파드는 서비스메시의 데이터플레인에 포함되게 됩니다.

위 상태에서 별다른 설정 없이 한번 httpbin-test 파드에서 위 애플리케이션을 호출해보겠습니다.

위와 같이 내부 서비스 오브젝트의 도메인으로 호출 하였을 때 mTLS 가 활성화 되어있음에도 불구하고 plain text 형태로 들어올 이 요청에도 응답을 처리하게 됩니다.

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: node-test-server-mtls-strict
spec:
  mtls:
    mode: STRICT

위와같이 PeerAuthentication 정책을 STRICT 로 하여 node-server-test 네임스페이스에 반영해보면

# curl -XGET http://node-test-server-service.node-test-server.svc.cluster.local
curl: (56) Recv failure: Connection reset by peer

위와 같이 요청이 거부됩니다.

위는 istio-proxy 가 없는 파드에서 요청한 것이고, 아래는 istio-proxy 가 있는 파드에서 요청한 것입니다. 이렇게 mTLS 를 강제하여서 서비스를 보호 할 수 있습니다.

https://istio.io/latest/docs/reference/config/security/peer_authentication/#PeerAuthentication-MutualTLS-Mode 이 문서에서 각각 어떤 옵션이 있는지 확인 할 수 있는데

PeerAuthentication.MutualTLS.Mode 의 값이 PREMISSIVE 면 plain text 및 mTLS 터널 트래픽을 모두 허용하는데, 디폴트 옵션이 이것입니다. 이 옵션은 서비스메시 전체 / 네임스페이스 별 분리 적용이 가능하기 때문에 필요에 의해 적절히 설정하면 되겠습니다.

Istio 예제/예시

설치 방법

설치 방법에는 크게 istioctl 을 통해 설치하는 방법과 helm 을 이용하는 방법이 있습니다.

Operator 를 이용해 설치하는 방법도 존재하지만, 공식적으로 권장하지 않는 방식입니다. https://istio.io/latest/docs/setup/install/operator/

istioctl을 통해 설치하기 위해서는 먼저 로컬 머신에 istioctl을 설치합니다.

$ curl -L https://istio.io/downloadIstio | sh -

이후 $ istioctl install --set profile=demo -y 와 같은 명령어로 istio 를 현재 k8s context 클러스터에 생성하게 됩니다. 다만 위 방법은 gitops 에 맞지 않는 방식이기 때문에 아래와 같은 방법으로 하는것이 더 나을것으로 판단됩니다. istioctl manifest 라는 커맨드를 통해 istioctl 및 profile에 맞는 k8s manifests 를 생성 할 수 있습니다. (참고 : https://istio.io/latest/docs/reference/commands/istioctl/#istioctl-manifest)

두번째로는 helm 을 이용해서 설치하는 방법입니다. 이건 profile 과 다르게 우리가 설치해야하는 요소를 직접 하나하나 설치해야하는 단점이 있지만, 조금 더 관리가 편하고 manifests 코드 자체를 git 에 올리지 않아도 되는 장점(?)이 있습니다. istioctl 을 통해 생성하는 manifests 파일은 크기가 어마어마합니다.

다만, profile 은 프로파일에 아래와 같이 설치되는 컴포넌트들이 지정되는데, helm 으로 할 경우는 각 요소를 직접 설치해줘야하는 단점은 있습니다만. 어려운건 아닙니다.

helm 으로 istiod, ingress-gateway 를 설치한 방법은

https://github.com/kimsehwan96/istio-example/tree/master/istiod

https://github.com/kimsehwan96/istio-example/tree/master/istio-ingress-gateway

위에서 코드로 확인 가능합니다.

Ingress Gateway

Ingress Gateway 는 기존 K8s Ingress 와 유사하게, 단일 로드밸런서의 엔드포인트를 통해 클러스터 외부의 요청을 받아서, 내부 서비스들로 로드밸런싱을 해주는 역할을 하는 게이트웨이라고 볼 수 있습니다. 이 오브젝트를 생성하면, 클라우드환경의 경우 클라우드 공급자의 로드밸런서를 통해서, On-premise 의 경우 케이스에 따라 다르겠지만, Metallb 등을 사용해서 IP Address Pool 안에서의 IP 등으로 type: LoadBalancer 인 서비스 오브젝트가 생성되고, External IP 와 매핑됩니다.

예를들어, AWS 기준으로 이렇게 생성하였을 때 Application Load Balancer 가 생성되고, 그것에 해당하는 도메인 foo.aws-alb.com 이 생성되었다고 해봅시다. 이 경우에 Route53 에서 *.hayden.com 이라는 도메인에 대한 A 레코드(with alias) foo.aws-alb.com 으로 지정하면, 우리는 *.hayden.com 으로 들어오는 트래픽을 모두 Istio Ingress Gateway로 전달 해줄 수 있습니다.

이후 Gateway 오브젝트 및 VirtualService 오브젝트를 사용하여 호스트헤더, HTTP 헤더, 경로 기반의 라우팅을 할 수 있게 됩니다. 즉 North-South 트래픽을 컨트롤 하기위해 생성하는 Istio 의 오브젝트라고 생각하면 됩니다.

다만 순수한 K8s Ingress 오브젝트와는 다르기 때문에 Ingress 로 검색 할수는 없고, Ingress Gateway는 디플로이먼트(파드) 및 서비스(로드밸런서)로 구성되게 됩니다.

위에서 보는것과 같이 내부 서비스메시의 트래픽 시작점이 istio-ingressgateway 이고 (단, 외부에서 클러스터로 호출 한 경우에 한정) 이후에는 각 마이크로서비스간 사전 정의된 방식대로 트래픽이 흘러가게 됩니다.

위는 다양한 방식의 K8s 에서의 North-South 트래픽을 처리하는 방법을 도식화 한 것입니다. 방금 소개한 방식은 Istio Gateway 에 해당합니다.

자세한 내용은 : https://jimmysong.io/en/blog/istio-servicemesh-api-gateway/#using-istio-gateway-to-expose-services 블로그에 잘 정리되어있습니다.

namespace: istio-system

resources:
  - ./istio-ingress-namespace.yaml
helmCharts:
  - name: gateway
    includeCRDs: true
    repo: https://istio-release.storage.googleapis.com/charts
    releaseName: istio-ingressgateway
    version: 1.19.0
    namespace: istio-system

위는 istio ingress gateway 를 설치하는 helm 예시입니다. 위를 통해 생성하면 deployment + pod , service 오브젝트가 생기고, 해당 서비스 오브젝트는 LoadBalancer 타입이므로 External IPs 와 매핑되게 됩니다. 저는 로컬에서 테스트하고 있어 localhost 로 연결되었습니다.

위는 실제 ingress 로 부터 들어온 트래픽을 실제 서비스로 라우팅 해주기위한 Gateway , VirtualService 오브젝트입니다. Gateway 오브젝트에서 어떤 ingress gateway 에서 들어온 트래픽인지 지정하고, 어떤 호스트로 들어온것을 처리할것인지 명시합니다. 이후에 VirtualService 에서 서비스별로 만들어놓은 게이트웨이를 지정하여서 트래픽을 받아 처리하게 됩니다.

Virtual Service + Gateway

Istio 에서 Gateway (커스텀 리소스)는 들어오거나 나가는 HTTP/TCP 연결을 수신하는 서비스메시의 가장자리에서 동작하는 로드밸런서를 의미합니다. 여기서는 노출 되어야하는 포트, 사용할 프로토콜, 로드밸런서에 대한 SNI 구성, TLS Termination 등을 설정합니다.

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: my-gateway
  namespace: some-config-namespace
spec:
  selector:
    app: my-gateway-controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - uk.bookinfo.com
    - eu.bookinfo.com
    tls:
      httpsRedirect: true # sends 301 redirect for http requests
  - port:
      number: 443
      name: https-443
      protocol: HTTPS
    hosts:
    - uk.bookinfo.com
    - eu.bookinfo.com
    tls:
      mode: SIMPLE # enables HTTPS on this port
      serverCertificate: /etc/certs/servercert.pem
      privateKey: /etc/certs/privatekey.pem
  - port:
      number: 9443
      name: https-9443
      protocol: HTTPS
    hosts:
    - "bookinfo-namespace/*.bookinfo.com"
    tls:
      mode: SIMPLE # enables HTTPS on this port
      credentialName: bookinfo-secret # fetches certs from Kubernetes secret
  - port:
      number: 9080
      name: http-wildcard
      protocol: HTTP
    hosts:
    - "*"
  - port:
      number: 2379 # to expose internal service via external port 2379
      name: mongo
      protocol: MONGO
    hosts:
    - "*"

위는 L4-L6 속성을 작성하고있습니다. 이후에 이 Gateway 를 VirtualService 에 바인딩하여서 게이트웨이로 들어온 트래픽을 제어 할 수 있습니다.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo-rule
  namespace: bookinfo-namespace
spec:
  hosts:
  - reviews.prod.svc.cluster.local
  - uk.bookinfo.com
  - eu.bookinfo.com
  gateways:
  - some-config-namespace/my-gateway
  - mesh # applies to all the sidecars in the mesh
  http:
  - match:
    - headers:
        cookie:
          exact: "user=dev-123"
    route:
    - destination:
        port:
          number: 7777
        host: reviews.qa.svc.cluster.local
  - match:
    - uri:
        prefix: /reviews/
    route:
    - destination:
        port:
          number: 9080 # can be omitted if it's the only port for reviews
        host: reviews.prod.svc.cluster.local
      weight: 80
    - destination:
        host: reviews.qa.svc.cluster.local
      weight: 20

위와 같이 VirtualService 는 게이트웨이와 바인딩하여서, 해당 게이트웨이로 도착하는 트래픽에 대한 라우팅을 적용하게 됩니다. 여기에서는 호스트 헤더, HTTP 헤더 기반의 라우팅, 경로 기반 라우팅, rewrite, 가중치 기반 트래픽 전달 등을 반영 할 수 있습니다.

mTLS

mTLS 는 mutual TLS 의 약자입니다.

기존의 TLS 핸드쉐이크의 경우 서버의 인증서만을 검증하였었는데, mTLS 의 경우 클라이언트의 인증서를 서버에서 검증하는 과정까지 추가된 것이라고 볼 수 있습니다. 별도로 반영하지 않아도 현재 Istio는 디폴트로 적용되어있고, 다만 기본 옵션이 Istio Proxy 에서 mTLS 로 보호된 터널과, plain text 로 들어오는 트래픽 둘다 허용하는 모드로 되어있기 때문에, 정말 검증되고 인증된, 즉 Service Mesh 내 트래픽만 받고 싶다면 mTLS 옵션을 STRICT 로 반영하면 됩니다. 기본은 Permissive mode 이기 때문입니다.

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: python-test-server-mtls-strict
  namespace: foo
spec:
  mtls:
    mode: STRICT

위와 같이 특정 네임스페이스에 대해서 mTLS 트래픽만 받겠다고 잠글 수 있고, 서비스메시 전체에 대해서도 잠글 수 있습니다.

Kiali 라는 Istio 용 대시보드를 사용해보면, 이렇게 서비스간 mTLS 가 적용되어있는 모습을 확인 할 수 있습니다. 실제로 서비스메시 외 서비스(즉 Istio Proxy 가 주입되지 않은 서비스)에서 호출하는 경우 요청은 실패합니다.

Canary

Istio 의 가중치 기반 라우팅 기능을 통해 특정 버전의 서비스에 대한 트래픽 비율을 조절 할 수 있습니다. DestionationRuleVirtualService 리소스를 통해 구현 가능합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-test-server-v1
spec:
  selector:
    matchLabels:
      app: node-test-server
      version: v1
  template:
    metadata:
      labels:
        app: node-test-server
        version: v1
        sidecar.istio.io/inject: 'true'
    spec:
      containers:
        - name: node-test-server
          image: kimsehwan96/node-server
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
              name: http
          resources:
            requests:
              cpu: 100m
            limits:
              cpu: 100m

위는 우리가 테스트로 사용할 서버를 version: v1 레이블을 붙인 배포입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-test-server-v2
spec:
  selector:
    matchLabels:
      app: node-test-server
      version: v2
  template:
    metadata:
      labels:
        app: node-test-server
        version: v2
        sidecar.istio.io/inject: 'true'
    spec:
      containers:
        - name: node-test-server
          image: kimsehwan96/node-server
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
              name: http
          resources:
            requests:
              cpu: 100m
            limits:
              cpu: 100m

위는 방금 봤던 구성과 동일하지만, 버전만 다른 구성입니다.

apiVersion: v1
kind: Service
metadata:
  name: node-test-server-service
  labels:
    app: node-test-server
spec:
  type: ClusterIP
  selector:
    app: node-test-server
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 3000

서비스 오브젝트 또한 생성합니다. selector 가 app: node-test-server 이므로 이 서비스 오브젝트를 통해서 마이크로서비스에 대한 트래픽이 직접 들어가면, 파드개수에 맞게 라운드로빈 형태로 로드밸런싱 되었을 것입니다. 우리는 VirtualService 및 DestionationRule 을통해 카나리 배포를 테스트해볼겁니다.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: node-test-server-virtualservice-internal
spec:
  hosts:
    - node-test-server-service.node-test-server.svc.cluster.local
  http:
  - route:
    - destination:
        host: node-test-server-service.node-test-server.svc.cluster.local
        port:
          number: 80
        subset: v1
      weight: 90
    - destination:
        host: node-test-server-service.node-test-server.svc.cluster.local
        port:
          number: 80
        subset: v2
      weight: 10

위와 같이 VirtualService 를 지정하고, destination 은 아까 만든 서비스 오브젝트를 향하게 합니다. 이렇게하면 내부 서비스가 node-test-server-service.node-test-server.svc.cluster.local 를 호출하더라도 이 VirtualService 에 등록된 정보를 토대로 istio(envoy) proxy 가 트래픽을 한번 처리하게 됩니다.

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: node-test-server-destination-internal
spec:
  host: node-test-server-service.node-test-server.svc.cluster.local
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

DestinationRule 을통해 VirtualService 에 지정한 subset과 서비스 오브젝트에 등록된 엔드포인트(파드)를 매칭 시킬 수 있습니다. 이 조건을 통해 실제 파드별 버전에 따라 트래픽을 라우팅하게 됩니다.

이렇게만 해도 충분히 트래픽의 90%는 버전1로, 10%는 버전2로 가는 설정이 되었지만 실제 환경과 유사하게, HPA(Horizontal Pod Auto scaling 을 활성화해서 트래픽이 많이 들어가는 버전의 파드에 대해 유연하게 확장하도록 설정도 해봅니다.

$ kubectl autoscale deployments node-test-server-v2 -n node-test-server --cpu-percent=10 --min=1 --max=10
horizontalpodautoscaler.autoscaling/node-test-server-v2 autoscaled
$ kubectl autoscale deployments node-test-server-v1 -n node-test-server --cpu-percent=10 --min=1 --max=10
horizontalpodautoscaler.autoscaling/node-test-server-v1 autoscaled

이후 테스트용으로 트래픽을 흘려보고 Kiali 및 HPA를 지켜봅니다.

우리가 설정한대로 버전1에 90% 트래픽이, 버전2에 10% 트래픽이 흘러갑니다.

버전1 파드에 대해서 Replica 가 9개, 버전2 파드에 대해 Replica 가 2개가 생성되었습니다. 버전1 앱에 트래픽이 많이 흘러들어가고 있기 때문이라고 납득 할 수 있습니다.

실제 파드 자체를 Canary 배포, 예를들어 기존 버전 앱, 신규 버전 앱을 1:9 비율로 배포를 하고 조절한다, 이것 자체는 Deployment 로도 충분히 가능하고, ArgoCD를 통해서도 충분히 가능한 작업입니다.

하지만 파드 자체의 개수외에 트래픽 자체에 대한 컨트롤은 Istio 로 할 수 있습니다. 이것이 더 유연한 작업들을 할 수 있게 도와주고, 여기서는 모든 트래픽에 대해서 랜덤하게 1:9 비율로 틀어줬지만, 헤더기반의 라우팅 또한 적용 가능합니다.

즉 특정 유저나 내부 개발자들만 신규 버전 마이크로서비스로 트래픽이 흐르고, 그 외에는 과거 버전 마이크로서비스로 트래픽이 흐르게 하는등 조건에 맞게 유연하게 트래픽 관리가 가능합니다.

Authz/Authn

JWT 토큰에 대해서 검증하고, 검증된 트래픽에 대해서만 뒷단의 특정 경로, 특정 HTTP Method 만 허용하게 하는등 인증을 애플리케이션 레이어가 아닌 인프라 레이어에서 할 수 있도록 작업 할 수 있습니다.

apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: kibana-request-authn
spec:
  selector:
    matchLabels:
      app: kibana
  jwtRules:
    - issuer: 'https://keycloak.foo.bar/realms/mater'
      jwksUri: 'https://keycloak.foo.bar/realms/master/protocol/openid-connect/certs'
      forwardOriginalToken: true
      fromHeaders:
        - name: Authorization
          prefix: 'Bearer '

위와 같이 요청으로 들어온 트래픽에서 Authorization: Bearer {JWT Token} 부분을 떼어내고, 유효한 토큰인지 ReqeustAuthentication 리소스로 검증 하도록 할 수 있습니다.

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: kibana-authz-policy-oauth2
spec:
  selector:
    matchLabels:
      app: kibana
  action: CUSTOM
  provider:
    name: oauth2-proxy
  rules:
    - to:
        - operation:
            paths:
              - '/*'
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: kibana-authz-policy-allow
spec:
  selector:
    matchLabels:
      app: kibana
  action: ALLOW
  rules:
    - from:
        - source:
            requestPrincipals:
              ['https://keycloak.foo.bar/realms/k8s/*']
      to:
        - operation:
            methods: ['*']
            paths: ['/*']
      when:
        - key: request.auth.claims[oauth2-proxy_roles]
          values: ['kibana']

또한 위와 같이 Authn (인증) 이후에 해당 JWT 토큰의 role / claim 등을 까보고, 그것을 기반으로 어떤 경로, 어떤 HTTP method 를 허용할지 지정해줄 수 있습니다. 이것을 통해 서비스간 JWT 토큰 인증을 애플리케이션이 아닌, Envoy Proxy 에서 처리하도록 할 수 있습니다.

예를들어 외부에서 요청하는 1개의 API 요청이, 내부적으로는 20개의 마이크로 서비스를 거친다고 가정할 때, 각 20개의 마이크로 서비스 애플리케이션 내부에서 JWT 토큰 검증 로직이 일일이 들어가있다면 추후에 기능을 변경/확장 할 때 20개의 애플리케이션에 대해서 작업해야하지만 Istio 레이어로 분리하면 애플리케이션 코드를 일일이 수정하지 않고 yaml 기반의 istio 설정을 조금만 변경해주면 구현이 가능하게 됩니다.

profile
DevOps 엔지니어로 핀테크 회사에서 일하고있습니다. 아직 많이 부족합니다.

0개의 댓글