HTTPRoute에서의 RegularExpression Path Matching
Kubernetes Gateway API와 HTTPRoute
Kubernetes Gateway API는 Kubernetes에서 서비스 네트워킹을 모델링하는 리소스 모음입니다. 그 중 HTTPRoute는 HTTP 요청을 매칭하고 백엔드 서비스로 전달하는 라우팅 규칙을 정의하는 리소스입니다.
HTTPRoute를 다루면서 가장 중요한 것 중 하나는 바로 여러 개의 HTTPRoute를 정의했을 때 어떻게 Rules들이 Merge 되는가입니다.
여러 개의 HTTPRoute가 하나의 Gateway에 attach 되었을 때, Proxy 혹은 LoadBalancer는 무조건 다음과 같은 순서로 우선순위를 부여해야합니다.
- Exact path match
- Prefix path match (높은 character count 수)
PathPrefix: /col이라고 해서/color로 라우팅되지 않음
- RegularExpression path match (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
Istio 코드 기반: Path Match 우선순위의 동작방식
Istio는 Gateway API 기반 HTTPRoute를 처리할 때, Path Matching Rule의 우선순위를 아래와 같이 소스코드에서 처리합니다.
createURIMatch()
Gateway API의 Path Match 타입을 Istio 내부 StringMatch로 변환하는 함수입니다:
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:
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:
return nil, &ConfigError{...}
}
}
getURIRank()
각 match type에 숫자 rank를 부여하는 함수입니다. 리턴값이 클수록 우선순위가 높습니다:
// 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
}
return -1
}
sortHTTPRoutes()
getURIRank를 사용해서 route를 우선순위별로 정렬하는 함수입니다:
func sortHTTPRoutes(routes []*istio.HTTPRoute) {
sort.SliceStable(routes, func(i, j int) bool {
// ...
r1, r2 := getURIRank(m1), getURIRank(m2)
len1, len2 := getURILength(m1), getURILength(m2)
switch {
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)
}
})
}
핵심은 Exact(3) > Prefix(2) > Regex(1) 입니다. Regex를 만족하는 PathPrefix가 선언되어 있으면 RegularExpression으로는 절대 매칭되지 않습니다.
Kubernetes Gateway API 스펙 자체에서는 “RegularExpression” path matches의 우선순위를 정의하지 않고 각 구현체에게 맡깁니다. 정해진 스펙이 없기 때문에, 사용자는 자신이 사용하는 구현체가 어떻게 구현했는지를 따를 수 밖에 없습니다. Istio의 경우
getURIRank를 통해서 URI match type을 Exact > Prefix > Regex 순서로 우선순위를 가져갑니다. 그러므로, Regex를 아무리 복잡하게 설정하더라도, Route에 부합하는 PathPrefix가 존재한다면, PathPrefix match를 우선적으로 적용합니다.
예를 들어 다음과 같이 설정된 경우:
PathPrefix: /api/v1/-> backend-serviceRegularExpression: /api/v1/hooks/.*/callback-> webhook-handler
/api/v1/hooks/provider/callback 요청은 무조건 PathPrefix가 먼저 적용되어 RegularExpression이 적용되지 않습니다.
만약 RegularExpression을 써야한다면, unexpected behavior를 방지하기 위해서 gateway에서 같은 hostnames 그룹 내에서는 PathPrefix 대신 모두 RegularExpression을 사용해야 합니다 (e.g. RegularExpression: /.*).
실전 예시: 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.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: "/"
backendRefs:
- name: backend-svc
port: 8080
(2) Waypoint에서 RegularExpression으로 세밀 분기
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: backend-svc-waypoint
spec:
parentRefs:
- kind: Service
name: backend-svc
rules:
- matches:
- path:
type: RegularExpression
value: "/.*"
backendRefs:
- name: backend-svc
port: 8080
- matches:
- path:
type: RegularExpression
value: "/api/v1/hooks/.*/callback"
backendRefs:
- name: webhook-handler
port: 8080
Waypoint Rules -> Envoy Config 변환 관련 Tip
waypoint.rules가 아예 blank일 때, default로 backend-svc Envoy 클러스터로 forwarding하는 rules가 채워져 있습니다.- 만약
waypoint.rules에/api/v1/hooks/.*/callbackRegularExpression만 존재하면, 해당 조건을 만족하는 요청만 routing되고, 나머지 모든 request는 404 NOT FOUND가 발생합니다. "/.*"와"/api/v1/hooks/.*/callback"가 둘 다 존재해야 expected behavior가 재현됩니다. 그리고 Waypoint와 Gateway에 각각 별도의 Envoy에 bound 되는 rules이기 때문에, Gateway의 PathPrefix rules의 영향을 받지 않습니다.
결론
- Istio는 소스코드 레벨에서 PathPrefix > RegularExpression 우선순위를 강제합니다.
- 실전에서는 Gateway와 Waypoint를 명확하게 분리해서 사용하는 것을 추천합니다.
- 이 구조를 모르고 HTTPRoute에서 (같은 hostname 안에서) PathPrefix/Regex를 혼합하면, 실제로는 PathPrefix가 다 먹어버리는 함정이 생깁니다.