본문 바로가기
프로젝트&&스터디/KANS2기

[KANS-6주차] Envoy Gateway 사용하기

by james_janghun 2024. 10. 12.

Envoy Gateway

https://gateway.envoyproxy.io/docs/

 

Welcome to Envoy Gateway

Envoy Gateway Documents

gateway.envoyproxy.io

 

대부분의 Gateway API는 envoy를 통해서 트래픽을 전송 및 추적하고 있습니다. 그런데 Envoy에서도 자체적으로 gateway를 제공하고 있어 오히려 최강자가 아닐까 생각됩니다.

아키텍처

 

Envoy 또한 GatewayClass를 통해서 설정값을 관리하게 됩니다. 해당 설정에 따라 Gateway를 구성하며 HTTP/GRPC/TLS/TCP/UDP 등 대표적인 프로토콜의 라우팅을 모두 지원하고 있습니다. 해당 라우팅 요청에 따라 Target인 backend 혹은 service에 요청을 전달합니다.

 

또한 Envoy에서 지원하는 강력한 기능들을 적용할 수 있습니다. 보안설정(SecurityPolicy), 확장기능(EnvoyExtensionPolicy)등이 있습니다.

 

공식문서에 보면 이게 전부다 기능인데 너무 많아서 일부분만 블로그에서 소개하겠습니다. (실제로는 이것보다 훨씬 많으며 공식문서를 참조하시기 바랍니다.

 

 

 

Envoy Proxy 설치

공식문서 QuickStart의 내용을 작성하였습니다.

 

먼저 쿠버네티스 클러스터와 MetalLb를 설치합니다. (설치과정은 제 블로그 글을 링크로 첨부하였으니 거기서도 확인이 가능합니다)

 

kubectl apply --server-side -f https://github.com/envoyproxy/gateway/releases/download/v1.1.2/install.yaml

 

customresourcedefinition.apiextensions.k8s.io/backendlbpolicies.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/backendtlspolicies.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/grpcroutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/referencegrants.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/tcproutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/tlsroutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/udproutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/backends.gateway.envoyproxy.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/backendtrafficpolicies.gateway.envoyproxy.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/clienttrafficpolicies.gateway.envoyproxy.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/envoyextensionpolicies.gateway.envoyproxy.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/envoypatchpolicies.gateway.envoyproxy.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/envoyproxies.gateway.envoyproxy.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/securitypolicies.gateway.envoyproxy.io serverside-applied
namespace/envoy-gateway-system serverside-applied
serviceaccount/envoy-gateway serverside-applied
configmap/envoy-gateway-config serverside-applied
clusterrole.rbac.authorization.k8s.io/eg-gateway-helm-envoy-gateway-role serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/eg-gateway-helm-envoy-gateway-rolebinding serverside-applied
role.rbac.authorization.k8s.io/eg-gateway-helm-infra-manager serverside-applied
role.rbac.authorization.k8s.io/eg-gateway-helm-leader-election-role serverside-applied
rolebinding.rbac.authorization.k8s.io/eg-gateway-helm-infra-manager serverside-applied
rolebinding.rbac.authorization.k8s.io/eg-gateway-helm-leader-election-rolebinding serverside-applied
service/envoy-gateway serverside-applied
deployment.apps/envoy-gateway serverside-applied
serviceaccount/eg-gateway-helm-certgen serverside-applied
role.rbac.authorization.k8s.io/eg-gateway-helm-certgen serverside-applied
rolebinding.rbac.authorization.k8s.io/eg-gateway-helm-certgen serverside-applied
job.batch/eg-gateway-helm-certgen serverside-applied

 

설치되느 내용을 간단하게 살펴보면 gatewayclass, gateway 등의 CRD와 envoy-gateway-system namespace, service-account, role과 rolebinding, 그리고 실질적인 envoy-gateway service 그리고 설정이 있는 configmap 등을 확인할 수 있었습니다.

 

우리는 모든 요청을 envoy-gateway를 통해 보내게 될 것이므로 외부 client에서 호출시 envoy-gateway의 external IP를 MetalLB를 통해 얻으셔야 합니다.

 

저의 경우 172.18.255.200을 gateway로 사용하고 있기 때문에 해당 IP로 호출하겠습니다.

 

이렇게 환경변수로 넣어놓을 수도 있습니다.

export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}')

 

Request Mirroring

미러링 기능은 요청을 다른 서비스에도 미러링 할 수 있는 기능입니다. 이러한 미러링은 Gateway API의 HTTPRequestMirrorFilter를 사용해 수행합니다. 이 때 filter에 등록된 BackendRef에서는 응답을 할 수 없고, 하더라도 무시됩니다. 즉 HTTPRoute의 BackendRef에서 응답이 오게됩니다.

 

테스트를 위해서 backend-2라는 service, deployment, service account를 생성합니다.

cat <<EOF | kubectl apply -f -
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: backend-2
---
apiVersion: v1
kind: Service
metadata:
  name: backend-2
  labels:
    app: backend-2
    service: backend-2
spec:
  ports:
    - name: http
      port: 3000
      targetPort: 3000
  selector:
    app: backend-2
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend-2
      version: v1
  template:
    metadata:
      labels:
        app: backend-2
        version: v1
    spec:
      serviceAccountName: backend-2
      containers:
        - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          imagePullPolicy: IfNotPresent
          name: backend-2
          ports:
            - containerPort: 3000
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
EOF

 

그리고 HTTPRoute를 설정하면 됩니다. 여기서 중요한 것은 HTTPRoute의 RequestMirror Filters입니다. 해당 부분에 미러링할 서비스 정보를 작성하고, backendRefs에 백앤드로 라우팅할 곳을 지정합니다.

cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: http-mirror
spec:
  parentRefs:
  - name: eg
  hostnames:
  - backends.example
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /get
    filters:
    - type: RequestMirror
      requestMirror:
        backendRef:
          kind: Service
          name: backend-2
          port: 3000
    backendRefs:
    - group: ""
      kind: Service
      name: backend
      port: 3000
EOF

 

이렇게 라우트 미러링 설정을 마치고 gateway host 주소를 복사한 뒤에 요청을 날려보겠습니다.

export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}')

 

curl -v --header "Host: backends.example" "http://${GATEWAY_HOST}/get"

 

 

요청 결과 응답한 백앤드의 정보를 확인할 수 있는데 backend-v2가 아닌 backend에서 응답을 주었습니다. 또한 로그 기록을 살펴보면 미러링이 된것을 확인할 수 있습니다.

kubectl logs deploy/backend && kubectl logs deploy/backend-2

 

 

Multiple BackendRefs

다음과 같이 HTTPRoute에서 여러 BackendRefs를 둘 수 있습니다. 이 때 HTTPRequestMirrorFilter에서는 이 Backend의 요청을 모두 미러링하게 됩니다. 동일하게 backend-3라는 service, serviceAccount, deployment를 생성하겠습니다.

cat <<EOF | kubectl apply -f -
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: backend-2
---
apiVersion: v1
kind: Service
metadata:
  name: backend-3
  labels:
    app: backend-3
    service: backend-3
spec:
  ports:
    - name: http
      port: 3000
      targetPort: 3000
  selector:
    app: backend-3
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-3
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend-3
      version: v1
  template:
    metadata:
      labels:
        app: backend-3
        version: v1
    spec:
      serviceAccountName: backend-3
      containers:
        - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          imagePullPolicy: IfNotPresent
          name: backend-3
          ports:
            - containerPort: 3000
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
EOF

 

그리고 BackendRefs에 backend-3를 추가합니다.

cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: http-mirror
spec:
  parentRefs:
  - name: eg
  hostnames:
  - backends.example
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /get
    filters:
    - type: RequestMirror
      requestMirror:
        backendRef:
          kind: Service
          name: backend-2
          port: 3000
    backendRefs:
    - group: ""
      kind: Service
      name: backend
      port: 3000
    - group: ""
      kind: Service
      name: backend-3
      port: 3000
EOF

 

이 상태로 curl로 gateway_host를 호출해 보겠습니다.

curl -v --header "Host: backends.example" "http://${GATEWAY_HOST}/get"

 

자연스럽게 backend와 backend-3의 부하분산이 되는 것을 확인할 수 있습니다. 

 

그리고 backend 들의 로그를 분석해보면 backend-2에서는 backend와 backend-3의 트래픽을 모두 미러링한것을 확인했습니다.

kubectl logs deploy/backend && kubectl logs deploy/backend-2 && kubectl logs deploy/backend-3

 

Multiple HTTPRequestMirrorFilters

그럼 미러링하는 Filter를 여러개 둘 수 있을까요? 아쉽게도 해당 기능은 지원하지 않습니다. HTTPRoute설정에서는 하나의 MirrorFilter만 지정할 수 있습니다. 여러개를 지정할 경우 admission Webhook에서 거절합니다.

 

Envoy Gateway의 부하분산 방식

  • RoundRobin: 사용 가능한 각 업스트림 호스트가 라운드 로빈 순서로 선택되는 간단한 정책입니다.
  • Random : 로드 밸런서는 사용 가능한 호스트를 무작위로 선택합니다.
  • Least Request : 로드 밸런서는 호스트의 가중치가 같거나 다른지 여부에 따라 다른 알고리즘을 사용합니다.
  • Consistent Hash : 로드 밸런서는 업스트림 호스트에 대한 일관된 해싱을 구현합니다.

Envoy Gateway는 BackendTrafficPolicy라는 CRD를 사용해서 LoadBalancer의 부하분산 방식을 규정합니다. 기본 설정은 Least Request 방식입니다.

 

모든 실습은 따라서 BackendTrafficPolicy를 규정하고 해당 policy에서 설정한 target(주로 HTTPRoute)으로 트래픽을 분산하는 방식입니다.

 

실습을 위해서 backend 애플리케이션 pod의 갯수를 4개로 올립니다.

kubectl patch deployment backend -n default -p '{"spec": {"replicas": 4}}'

 

실습에서는 hey라는 부하분산 도구를 이용하는데, 간단하게 요청에 대한 응답시간을 그림/그래프로 제공해주는 도구입니다.

관심있으신 분들은 이거 설치해서 사용해보시면 좋을 것 같네요. (brew로 간단하게 설치가능)

brew install hey

 

RoundRobin

BackendTrafficPolicy에 loadbalancer Type을 RoundRobin으로 설정하고, HTTPRoute 백앤드에 /round라는 prefix 설정을 하였습니다.

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: round-robin-policy
  namespace: default
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: round-robin-route
  loadBalancer:
    type: RoundRobin
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: round-robin-route
  namespace: default
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "www.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /round
      backendRefs:
        - name: backend
          port: 3000
EOF

 

 

hey를 통해서 100번의 호출을 라운드로빈으로 실행해보겠습니다.

hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/round

 

 

실제로 호출 수를 확인해보면 다음과 같이 고르게 분산된 것을 확인할 수 있습니다.

kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done

 

Random

이번에는 LoadBalancerType을 Random으로 규정하고 HTTPRoute옵션을 주겠습니다.

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: random-policy
  namespace: default
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: random-route
  loadBalancer:
    type: Random
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: random-route
  namespace: default
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "www.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /random
      backendRefs:
        - name: backend
          port: 3000
EOF

 

hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/random

kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done

 

 

확실히 Random이기 때문에 pod마다 고르게 분배되지는 못하는 것을 확인할 수 있습니다.

 

Least Request

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: least-request-policy
  namespace: default
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: least-request-route
  loadBalancer:
    type: LeastRequest
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: least-request-route
  namespace: default
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "www.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /least
      backendRefs:
        - name: backend
          port: 3000
EOF

 

역시 100번의 호출을 진행해 보겠습니다.

hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/least

kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done

 

 

Least Request의 특징으로는 요청이 적은 pod에 트래픽을 우선 배치하기 때문에 초기 100개를 한꺼번에 보낼때는 랜덤하게 분산되고 있었지만 다음요청을 1개 더 보낼때는 아마 최소 요청을 받은 rv88w 파드가 우선배치 될 가능성이 높습니다. 1개의 요청만 더 보내보면 rv88w의 요청값이 올라간 것을 확인할 수 있습니다.

 

hey -n 1 -c 1 -host "www.example.com" http://${GATEWAY_HOST}/least

 

Consistent Hash

Envoy Gateway는 기본적으로 해시 알고리즘로 Maglev를 사용하고 있습니다. 해시를 만들때 쓰이는 것은 SourceIP, Header, Cookie가 있으며 우리는 SourceIP부터 실습을 해보겠습니다. 코드를 보면 Consistent Hash의 경우 type을 별도로 지정하도록 되어있습니다. 이곳에 sourceip, header, cookie 중 선택해 넣으면 되겠습니다.

 

sourceIP

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: source-ip-policy
  namespace: default
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: source-ip-route
  loadBalancer:
    type: ConsistentHash
    consistentHash:
      type: SourceIP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: source-ip-route
  namespace: default
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "www.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /source
      backendRefs:
        - name: backend
          port: 3000
EOF

 

100 번의 부하분산을 해보겠습니다.

hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/source

 

sourceIP의 경우 동일한 sourceIP에서 보내는 요청은 모두 한 곳으로만 트래픽을 보내는 것을 확인할 수 있습니다. 추가적으로 200번을 더 호출하였는데도 계속해서 동일한 pod가 요청을 받았습니다.

 

Header

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: header-policy
  namespace: default
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: header-route
  loadBalancer:
    type: ConsistentHash
    consistentHash:
      type: Header
      header:
        name: FooBar
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: header-route
  namespace: default
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "www.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /header
      backendRefs:
        - name: backend
          port: 3000
EOF

 

 

hey -n 100 -c 100 -host "www.example.com" -H "FooBar: 1.2.3.4" http://${GATEWAY_HOST}/header

 

kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done

 

최초 1.2.3.4라는 헤더에는 thnbc라는 pod가 100번을 호출 받았습니다.

 

hey -n 100 -c 100 -host "www.example.com" -H "FooBar: 5.6.7.8" http://${GATEWAY_HOST}/header

 

 

그 뒤에 5.6.7.8의 경우 bvb8m이 받았습니다. 그렇다면 다시 1.2.3.4를 호출하게 되면 thnbc가 요청을 받지 않을까 예측해볼 수 있습니다.

hey -n 100 -c 100 -host "www.example.com" -H "FooBar: 1.2.3.4" http://${GATEWAY_HOST}/header

 

 

실제로도 그런것을 확인할 수 있습니다.

 

Cookie

cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: cookie-policy
  namespace: default
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: cookie-route
  loadBalancer:
    type: ConsistentHash
    consistentHash:
      type: Cookie
      cookie:
        name: FooBar
        ttl: 60s
        attributes:
          SameSite: Strict
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: cookie-route
  namespace: default
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "www.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /cookie
      backendRefs:
        - name: backend
          port: 3000
EOF

 

 

10개의 요청을 /cookie 백앤드로 보내보겠습니다. 일단 동일한 쿠키정보를 가진 요청의 경우 같은 pod로 요청을 보낼 것입니다.

for i in {1..10}; do curl -I --header "Host: www.example.com" --cookie "FooBar=1.2.3.4" http://${GATEWAY_HOST}/cookie ; sleep 1; done
kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done

 

 

먼저 cskld라는 pod가 요청을 전부 받았습니다.

for i in {1..10}; do curl -I --header "Host: www.example.com" --cookie "FooBar=5.6.7.8" http://${GATEWAY_HOST}/cookie ; sleep 1; done

이제 쿠키정보가 다른 요청을 보내게 되면 다른 pod가 요청을 받게됩니다.

 

 

만약 쿠키가 없는 요청의 경우 envoy gateway가 별도로 ttl, atrrivutes 필드에 따라 쿠키를 자동으로 생성합니다. 한번 쿠키가 없이 요청을 보내보겠습니다.

curl -v --header "Host: www.example.com" http://${GATEWAY_HOST}/cookie

 

이처럼 자동으로 쿠키 정보가 설정되는 것을 확인할 수 있습니다.