HTTPRoute에서의 RegularExpression Path Matching
Table of Contents
HTTPRoute에서의 RegularExpression Path Matching #
HTTPRoute를 다루면서 가장 중요한 것 중 하나는 바로 여러 개의 HTTPRoute를
정의했을 때의 어떻게 Rules들이 Merge 되는가입니다.
pilot/pkg/config/kube/gateway/route_collections.go#L662
의 func mergeHTTPRoutes()
여러 개의 HTTPRoute가 하나의 Gateway에 attach 되었을 때, Proxy 혹은 LoadBalancer는 무조건 다음과 같은 순서로 우선순위를 부여해야합니다.
- “Exact” path match
- “Prefix” path match (높은 character count 수)
- PathPrefix: /col 이라고 해서 /color로 라우팅되지 않음
- “RegularExpression” path match **(only in case of Istio)
- Istio에서는 RE2 syntax 사용 (e.g.
.*
matches any character)
- Istio에서는 RE2 syntax 사용 (e.g.
- “Method” match
- Largest number of “header” matches
- 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
- PathPrefix: /desk/app/ → ch-dropwizard
- RegularExpression: /desk/app/naver-talks/.*/webhook → lambda 인 경우
- e.g
그리하여, /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
- waypoint.rules:
[]
가 아예 blank일 때 → default로 ch-dropwizard-public Envoy 클러스터로 forwarding 하는 rules가 채워져있음. - 만약 waypoint.rules:
/desk/naver-talk/.*/webhook
RegularExpression만 존재하면, 해당 조건을 만족하는 req.만 routing되고, 나머지 모든 request는404 NOT FOUND
가 발생함. - 위 사진처럼 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가 다 먹어버리는 함정이 생긴다.