Skip to main content
  1. Archives/

HTTPRoute에서의 RegularExpression Path Matching

·693 words

HTTPRoute에서의 RegularExpression Path Matching #

HTTPRoute를 다루면서 가장 중요한 것 중 하나는 바로 여러 개의 HTTPRoute를 정의했을 때의 어떻게 Rules들이 Merge 되는가입니다. pilot/pkg/config/kube/gateway/route_collections.go#L662func mergeHTTPRoutes()

여러 개의 HTTPRoute가 하나의 Gateway에 attach 되었을 때, Proxy 혹은 LoadBalancer는 무조건 다음과 같은 순서로 우선순위를 부여해야합니다.

  1. “Exact” path match
  2. “Prefix” path match (높은 character count 수)
    1. PathPrefix: /col 이라고 해서 /color로 라우팅되지 않음
  3. “RegularExpression” path match **(only in case of Istio)
    1. Istio에서는 RE2 syntax 사용 (e.g. .* matches any character)
  4. “Method” match
  5. Largest number of “header” matches
  6. Largest number of “query param” matches

1. Istio 코드 기반: Path Match 우선순위의 동작방식 #

Istio는 Gateway API 기반 HTTPRoute를 처리할 때, Path Matching Rule의 우선순위를 아래와 같이 소스코드에서 살펴봅니다.

핵심 코드 1: createURIMatch() #

func createURIMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
    tp := k8s.PathMatchPathPrefix
    if match.Path.Type != nil {
        tp = *match.Path.Type
    }
    dest := "/"
    if match.Path.Value != nil {
        dest = *match.Path.Value
    }
    switch tp {
    case k8s.PathMatchPathPrefix:
        // "When specified, a trailing `/` is ignored."
        if dest != "/" {
            dest = strings.TrimSuffix(dest, "/")
        }
        return &istio.StringMatch{
            MatchType: &istio.StringMatch_Prefix{Prefix: dest},
        }, nil
    case k8s.PathMatchExact:
        return &istio.StringMatch{
            MatchType: &istio.StringMatch_Exact{Exact: dest},
        }, nil
    case k8s.PathMatchRegularExpression:
        return &istio.StringMatch{
            MatchType: &istio.StringMatch_Regex{Regex: dest},
        }, nil
    default:
        // Should never happen, unless a new field is added
        return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)}
    }
}

핵심 코드 2: getURIRank() #

// getURIRank ranks a URI match type. Exact > Prefix > Regex
func getURIRank(match *istio.HTTPMatchRequest) int {
    if match.Uri == nil {
        return -1
    }
    switch match.Uri.MatchType.(type) {
    case *istio.StringMatch_Exact:
        return 3
    case *istio.StringMatch_Prefix:
        return 2
    case *istio.StringMatch_Regex:
        return 1
    }
    // should not happen
    return -1
}

핵심 코드 3: sortHTTPRoutes() #

// sortHTTPRoutes sorts generated vs routes to meet gateway-api requirements
func sortHTTPRoutes(routes []*istio.HTTPRoute) {
    sort.SliceStable(routes, func(i, j int) bool {
        if len(routes[i].Match) == 0 {
            return false
        } else if len(routes[j].Match) == 0 {
            return true
        }
        m1, m2 := routes[i].Match[0], routes[j].Match[0]
        r1, r2 := getURIRank(m1), getURIRank(m2)
        len1, len2 := getURILength(m1), getURILength(m2)
        switch {
        // 1: Exact/Prefix/Regex
        case r1 != r2:
            return r1 > r2 // 우선순위 높은 쪽이 먼저
        case len1 != len2:
            return len1 > len2
        case (m1.Method == nil) != (m2.Method == nil):
            return m1.Method != nil
        case len(m1.Headers) != len(m2.Headers):
            return len(m1.Headers) > len(m2.Headers)
        default:
            return len(m1.QueryParams) > len(m2.QueryParams)
        }
    })
}

getURIRank의 리턴값이 클수록 우선순위가 높음

→ Exact(3) > Prefix(2) > Regex(1)

→ Regex를 만족하는 PathPrefix가 선언되어 있으면 RegularExpression으로는 절대 매칭되지 않음!

소스코드 전체 확인은 conversion.go, route_collections.go 참고.


“RegularExpression” path matches의 우선순위에 대해서는 Gateway API 구현체마다 상이할 수 있습니다.

  • pilot/pkg/config/kube/gateway/conversion.go#L456
  • Istio에서는 getURIRank 를 통해서 URI match type을. Exact > Prefix > Regex 순서로 우선순위를 가져간다. 그러므로, Regex를 아무리 복잡하게 설정하더라도, Route에 부합하는 PathPrefix가 존재한다면, PathPrefix match를 우선적으로 적용한다.
    • e.g
      1. PathPrefix: /desk/app/ → ch-dropwizard
      2. RegularExpression: /desk/app/naver-talks/.*/webhook → lambda 인 경우

그리하여, /desk/app/naver-talks/some/webhook 요청은 무조건 PathPrefix가 먼저 적용되어 RegularExpression이 적용되지 않습니다.

만약에, RegularExpression을 써야한다면, unexpected behavior를 방지하기 위해서 gateway에서 같은 hostnames 그룹 내에서는 모두 PathPrefix 대신 모두 RegularExpression을 사용하도록 해야합니다. (e.g. RegularExpression: /.* )

2. 실전 예시: Gateway/Waypoint 분리 구조 #

(1) Gateway에서 PathPrefix로 대분류 #

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: public-ingress-route
spec:
  parentRefs:
    - name: istio-public-ingress
      namespace: istio-gateway
  hostnames:
    - "api.exp.channel.io"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: "/" # 모든 트래픽
      backendRefs:
        - name: ch-dropwizard-public
          port: 8080

(2) Waypoint에서 RegularExpression으로 세밀 분기 #

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: ch-dropwizard-waypoint
spec:
  parentRefs:
    - kind: Service
      name: ch-dropwizard-public
  rules:
    - matches:
        - path:
            type: RegularExpression
            value: "/.*"
      backendRefs:
        - name: ch-dropwizard-public
          port: 8080
    - matches:
        - path:
            type: RegularExpression
            value: "/desk/naver-talk/.*/webhook"
      backendRefs:
        - name: ch-dropwizard-lambda
          port: 8080

Waypoint rules → Envoy Config 변환 관련 Tip

  1. waypoint.rules: [] 가 아예 blank일 때 → default로 ch-dropwizard-public Envoy 클러스터로 forwarding 하는 rules가 채워져있음.
  2. 만약 waypoint.rules: /desk/naver-talk/.*/webhook RegularExpression만 존재하면, 해당 조건을 만족하는 req.만 routing되고, 나머지 모든 request는 404 NOT FOUND 가 발생함.
  3. 위 사진처럼 1) /.* 와 2) /desk/naver-talk/.*/webhook 가 둘 다 존재해야 expected behavior가 재현됨. 그리고 Waypoint와 Gateway에 각각 별도의 Envoy에 bound 되는 rules이기 때문에, Gateway의 PathPrefix rules의 영향을 받지도 않음.

결론/정리 #

  • Istio는 소스코드 레벨에서 PathPrefix > RegularExpression 우선순위를 강제한다.
  • 실전에서는 Gateway와 Waypoint를 명확하게 분리해서 사용해야하는 방법 제시
  • 이 구조를 모르고 HTTPRoute에서 (같은 hostname 안에서) PathPrefix/Regex를 혼합하면, 실제로는 PathPrefix가 다 먹어버리는 함정이 생긴다.