Cloudflare Tunnel의 라우팅을 Kubernetes Ingress로 관리하는 과정에서 WordPress 관리자 경로를 분리하려고 했습니다. /wp-admin 아래는 관리자용 Service로 보내고, 나머지는 공개용 Service로 보내는 구성입니다.

그런데 pathType: Prefix로 적은 경로가 Kubernetes에서 기대한 방식과 다르게 동작했습니다. 경로 중간에 등장한 문자열이 매칭되기도 하고, 점 하나가 정규식으로 해석되기도 했습니다. 이를 재현해 STRRL의 Cloudflare Tunnel Ingress Controller에 수정 PR을 제안했고, 2026년 9월 29일에 실제 터널 트래픽 검증과 수정 이미지 적용을 마쳤습니다.

Prefix는 경로 요소를 기준으로 매칭합니다

Kubernetes의 Ingress Prefix는 /로 나뉜 경로 요소를 기준으로 매칭합니다. /admin은 /admin, /admin/, /admin/settings에 매칭되지만 /admin-tools에는 매칭되지 않습니다. 앞에 다른 경로가 붙은 /prefix/admin도 대상이 아닙니다. 이 규칙은 Kubernetes의 path type 설명에 명시돼 있습니다.

Cloudflare Tunnel의 path 조건은 정규식입니다. 따라서 Ingress의 경로 문자열을 이 조건에 그대로 넘기면, Kubernetes Prefix의 의미가 유지되지 않습니다. /admin이라는 문자열만 전달하면 시작 위치와 경로 요소의 끝을 제한하지 못하고, /v1.0의 점은 임의의 문자 하나를 뜻할 수 있습니다.

WordPress 경로에서 문제를 재현하기

운영 WordPress의 DNS를 바꾸기 전에 별도 테스트 터널과 호스트를 만들었습니다. 백엔드는 응답 본문에 PUBLIC, ADMIN, LITERAL을 돌려주는 세 개의 echo 서비스였습니다. HTTP 상태 코드뿐 아니라 어느 백엔드로 갔는지 확인하기 위해서입니다.

하나의 Ingress를 유지한 채 컨트롤러 이미지만 바꿔 수정 전후를 비교했습니다. 대표적인 결과는 다음과 같습니다. 아래 응답은 모두 HTTP 200이며, 표의 값은 응답 본문으로 확인한 백엔드입니다.

요청 경로수정 전수정 후
/wp-adminPUBLICADMIN
/wp-admin/ADMINADMIN
/wp-admin/edit.phpADMINADMIN
/foo/wp-admin/ADMINPUBLIC
/admin-toolsADMINPUBLIC
/v1X0LITERALPUBLIC

테스트 Ingress에는 /wp-admin/을 Prefix로 넣었습니다. Kubernetes 규칙에서는 끝의 슬래시 유무에 따라 /wp-admin의 매칭 결과가 달라지지 않아야 합니다. 이 경계도 실제 요청으로 확인했습니다.

Prefix를 정규식으로 변환할 때 필요한 세 가지

변환에 필요한 것은 경로 시작을 고정하는 ^, 정규식 특수문자를 문자 그대로 처리하는 escaping, 그리고 경로 요소의 끝을 확인하는 (/|$)입니다.

/wp-admin/ → ^/wp-admin(/|$)
/admin     → ^/admin(/|$)
/v1.0      → ^/v1\.0(/|$)
/          → ^/

루트 경로는 따로 처리했습니다. 끝의 슬래시를 정리한 결과가 빈 문자열이 되는 /은 모든 절대 경로를 매칭하도록 ^/을 사용합니다.

구현의 핵심은 아래 함수입니다. Prefix에만 이 변환을 적용하고, 다른 지원 path type의 기존 동작은 유지합니다.

func ingressPathRegex(path string, pathType networkingv1.PathType) string {
    if pathType != networkingv1.PathTypePrefix {
        return path
    }

    prefix := strings.TrimRight(path, "/")
    if prefix == "" {
        return "^/"
    }
    return "^" + regexp.QuoteMeta(prefix) + "(/|$)"
}

특히 regexp.QuoteMeta가 필요합니다. 시작과 끝만 제한해도 점이나 괄호가 정규식으로 해석된다면, 문자 그대로 쓴 Ingress 경로의 의미는 여전히 달라집니다.

ImplementationSpecific의 정규식은 유지하기

기존 정규식 사용자를 고려해 ImplementationSpecific은 그대로 두었습니다. 테스트에 넣은 /legacy/(admin|login)$는 수정 전후에 동일하게 전달됩니다. 시작 위치가 고정되지 않은 정규식이므로 /foo/legacy/admin도 계속 매칭됩니다.

이 변경은 기존에 Prefix 경로에 정규식을 넣어 사용하던 구성에 영향을 줍니다. 그런 구성은 업그레이드 전에 경로를 확인하고, 컨트롤러의 정규식 동작을 의도했다면 ImplementationSpecific으로 옮겨야 합니다. PR에도 이 호환성 변화를 안내했습니다.

생성된 설정과 실제 응답을 함께 검증하기

Cloudflare API에서 생성된 터널 규칙을 다시 읽어 변환 결과를 확인했습니다. 같은 Ingress의 UID와 spec을 유지하고 컨트롤러 이미지만 바꾼 최종 비교에서는 20개 요청이 모두 예상한 백엔드에 도달했습니다.

테스트용 호스트를 처음 연결하는 동안에는 HTTP 530이 한 번 있었고, 이미지 변경 직후에는 이전 규칙이 잠시 보이기도 했습니다. 그래서 설정 전파를 기다린 뒤 완전한 비교를 다시 수행했습니다. 이미지가 Ready가 되는 시점과 외부 터널에 새 설정이 적용되는 시점은 같지 않았습니다.

회귀 테스트 8개, 전체 패키지의 race test, envtest 통합 테스트 17개도 통과했습니다. 수정 코드의 lint 결과는 문제가 없었지만, 전체 lint에는 기존 upstream에서도 발생하는 두 개의 deprecation 경고가 남았습니다. 수정 이미지의 AMD64·ARM64 빌드와 실행도 확인했습니다.

수정 제안과 변경 내용은 PR #368에서 볼 수 있습니다. 이 글의 검증 대상은 해당 수정 이미지입니다. upstream 릴리스에 반영된 범위는 사용하는 버전의 변경 내역을 확인해야 합니다.

이번 작업에서는 200 응답만 확인했다면 문제를 놓쳤을 것입니다. 경로 변환을 검증할 때는 실제로 선택된 백엔드까지 보는 편이 훨씬 확실했습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다