홈랩의 애플리케이션들은 이미 Ingress로 외부 접근 경로를 관리하고 있었습니다. Gateway API를 사용해 보려니 각 서비스의 HTTPRoute를 따로 작성하고, 기존 Ingress와 함께 유지해야 하는 부분이 늘어났습니다. 서비스 경로가 바뀔 때 어느 쪽을 수정해야 하는지도 신경 써야 했습니다.
그래서 애플리케이션에는 Ingress를 남기고, 선택한 Ingress만 기존 Gateway에 연결되는 HTTPRoute로 변환하는 작은 컨트롤러를 만들었습니다. 이름은 ingress-gateway-bridge입니다. WordPress와 DBtools부터 검증한 뒤 서비스를 늘렸고, 2026년 10월 8일에 남아 있던 수동 HTTPRoute 전환을 마쳤습니다.
기존 Ingress를 남기고 Tunnel을 선택한 이유
기존 Helm chart와 Rancher의 관리 흐름은 Ingress 중심이었습니다. Gateway API를 도입하기 위해 애플리케이션마다 manifest를 바꾸는 것보다는, 서비스가 익숙한 방식으로 경로를 선언하고 인프라 쪽에서 연결 대상을 바꾸는 편이 맞았습니다.
Cloudflare Tunnel의 실행 위치와 계정도 하나가 아니었습니다. 집과 Oracle Cloud의 Tunnel이 같은 도메인 아래 서비스를 노출할 수 있으므로 hostname만 보고 목적지를 정하기는 어렵습니다. 그래서 애플리케이션은 ingressClassName으로 연결 방식을 선택하고, 운영자는 그 클래스가 어느 Gateway를 사용할지 명시적으로 지정하도록 했습니다.
이 구분 덕분에 애플리케이션에는 호스트·경로·Service 정보가 남고, 인프라에는 Gateway·Tunnel·TLS와 접근 정책이 모입니다. 브리지는 두 설정 사이에서 HTTPRoute를 만들고 원본과 동기화하는 역할을 맡습니다.
애플리케이션의 경로와 인프라의 Gateway 연결하기
Gateway API에서는 HTTPRoute의 parentRefs로 Gateway를 지정합니다. 하나의 Gateway에 여러 Route를 연결할 수 있습니다. 기본 구조는 Gateway API의 HTTP routing 안내에서 볼 수 있습니다.
이번 컨트롤러는 명시적으로 등록한 IngressClass를 기존 Gateway에 연결합니다. 인프라 GitOps가 Gateway와 TLS를 준비하고, 애플리케이션 GitOps는 Ingress를 관리합니다. 컨트롤러는 그 Ingress를 읽어 애플리케이션 namespace에 HTTPRoute를 생성합니다.

여기서 Ingress → 브리지 → HTTPRoute는 설정을 만드는 흐름입니다. 사용자 요청이 브리지 컨트롤러를 통과한다는 뜻은 아닙니다. Cloudflare 쪽의 실제 요청은 Edge와 Tunnel, Connector 및 프록시를 거쳐 애플리케이션으로 전달됩니다. Cilium을 사용하는 경로는 해당 Gateway의 데이터 경로를 따릅니다.
클래스 이름의 접미사나 호스트 이름으로 대상을 추측하지 않게 했습니다. 선택한 클래스와 Gateway의 관계는 설정으로 명시합니다. 이 방식으로 기존 Cilium Ingress와 Cloudflare Tunnel Ingress도 필요한 곳에서는 계속 사용할 수 있었습니다.
매핑 대상은 GatewayClass 이름이 아니라 실제 Gateway 객체입니다. 브리지 전용 IngressClass를 다른 활성 컨트롤러와 공유하지 않고, 서로 다른 namespace의 Route를 연결할 때는 Gateway의 allowedRoutes도 확인합니다. 같은 hostname의 DNS나 Tunnel 설정을 여러 컨트롤러가 동시에 관리하지 않도록 책임 범위도 나눠야 합니다.
변환할 수 있는 범위를 먼저 정하기
기존 Ingress 컨트롤러의 모든 기능을 재현하려고 하면 작은 브리지로 끝나지 않습니다. 우선 표준 경로와 Service 백엔드를 중심으로 지원 범위를 정했습니다.
| Ingress 항목 | 브리지의 처리 |
|---|---|
Exact | HTTPRoute의 Exact로 변환 |
Prefix | 경로 요소 기준 PathPrefix로 변환 |
| 숫자로 지정한 Service port | 같은 port로 backendRef 생성 |
| 이름으로 지정한 Service port | Service의 port로 해석 |
| Ingress TLS | 바인딩에서 외부 TLS 처리를 명시한 경우만 허용 |
| ImplementationSpecific·wildcard host 등 | 초기 버전에서는 변환 거부 |
이름으로 지정한 포트를 해석할 때는 Service의 targetPort가 아니라 port를 사용해야 합니다. HTTPRoute의 backendRef가 가리키는 대상은 Service이기 때문입니다. Service의 포트가 변경될 때도 다시 반영하도록 watch를 연결했습니다.
NGINX나 Traefik용 annotation은 원본에 남겨 두되 브리지에서 해석하지 않았습니다. 지원하지 않는 옵션을 다른 의미로 변환해 버리는 것보다, 브리지가 책임지는 설정 범위를 분명하게 두는 편이 관리하기 쉬웠습니다.
TLS는 기존 Gateway와 백엔드 정책에서 처리하기
바인딩의 tlsPolicy: External은 운영자가 Gateway나 터널 edge에 TLS를 이미 구성했다는 뜻으로 사용했습니다. 브리지가 인증서를 발급하거나 Secret을 복사하지는 않습니다.
외부 HTTPS와 백엔드 HTTPS도 나눠 봐야 합니다. Service port가 443이라는 것만으로 Gateway가 그 백엔드에 HTTPS로 연결하는 것은 아닙니다. WordPress에는 기존 BackendTLSPolicy를 유지해 origin HTTPS와 CA 검증이 계속 적용되도록 했습니다.
Cilium 쪽은 기존 HTTPS listener에 전달 Route를 연결했습니다. HTTP에서 HTTPS로 보내는 리다이렉트는 별도의 HTTP listener 바인딩으로 처리했습니다. 애플리케이션마다 Gateway를 새로 만드는 대신 기존 공유 Gateway를 사용했습니다.
브리지는 DNS나 Tunnel을 직접 생성하지 않습니다. 또한 Tunnel을 거친다는 사실만으로 로그인이나 접근 통제가 적용되는 것은 아닙니다. Cloudflare Access 또는 애플리케이션 인증을 어디에서 맡을지, 기존의 직접 접속 경로가 남아 있는지는 별도로 확인해야 합니다.
리다이렉트와 헤더를 보존하기
남아 있던 서비스를 옮길 때는 경로와 백엔드만 같아서는 부족했습니다. Grafana와 Nextcloud의 HTTP→HTTPS 리다이렉트, Nextcloud의 요청·응답 헤더도 유지해야 했습니다. 이를 위해 브리지 전용 annotation을 작게 추가했습니다.
아래는 annotation 부분만 발췌한 예시입니다. 리다이렉트에는 기존 TLS 설정과 HTTP listener 바인딩이 함께 필요합니다.
metadata:
annotations:
ingress-gateway-bridge.colah16.github.io/https-redirect: "true"
ingress-gateway-bridge.colah16.github.io/https-redirect-status-code: "302"
ingress-gateway-bridge.colah16.github.io/request-headers: '{"set":[{"name":"X-Forwarded-Proto","value":"https"}]}'헤더 옵션은 set, add, remove를 구분했습니다. JSON의 알 수 없는 키, 중복 키, 잘못된 헤더 형식도 검사했습니다. 헤더 이름은 대소문자를 구분하지 않으므로, 대소문자만 다른 중복도 걸러야 했습니다.
기존 Cilium 리다이렉트의 302, HTTPS port 443, host·path·query를 보존했습니다. Nextcloud의 X-Forwarded-Proto와 HSTS 등의 응답 헤더도 기존 Route와 대조했습니다. Envoy가 추가하는 server: envoy처럼 기존에도 보이던 응답까지 새 문제로 오해하지 않도록 전환 전 값을 먼저 저장했습니다.
생성된 HTTPRoute의 소유자를 하나로 유지하기
생성한 Route에는 원본 Ingress의 UID를 가리키는 ownerReference를 붙였습니다. 클래스가 바뀌거나 원본에서 경로가 사라지면 브리지 소유의 Route를 정리하도록 했습니다. API 서버가 기본값을 채운 뒤에도 매번 불필요한 update가 반복되지 않는지 확인했습니다.
Git에는 원본 Ingress와 브리지 설정을 저장합니다. 생성된 HTTPRoute까지 Git에 넣으면 Fleet와 컨트롤러가 같은 결과물을 관리하게 됩니다. 이번 전환에서는 새 Route를 확인한 뒤 기존 수동 Route manifest를 제거했습니다.
권한도 분리했습니다. 읽기는 필요한 리소스를 watch할 수 있도록 주고, Route 쓰기는 승인한 애플리케이션 namespace별 Role로 부여했습니다. update·delete는 생성되는 Route 이름으로 제한했습니다. Kubernetes RBAC의 create 권한은 resourceNames로 제한할 수 없으므로 namespace 범위와 컨트롤러의 클래스 선택·소유권 검사를 함께 사용했습니다.
한 서비스씩 옮기고 기존 응답과 비교하기
각 서비스에서 원본 Ingress 소유권, 현재 generation의 Accepted·ResolvedRefs, 기존 Route와 새 Route의 경로·백엔드·필터를 대조했습니다. 기존 Route를 제거한 뒤에는 새 Route만 있는 상태에서 다시 요청했습니다.
| 이번에 추가 전환한 서비스 | 생성된 Route |
|---|---|
| Portfolio | 호스트별 전달 2개 |
| Grafana | HTTPS 전달 1개 + HTTP 리다이렉트 1개 |
| Nextcloud 직접 접속 호스트 | HTTPS 전달 4개 + HTTP 리다이렉트 4개 |
| Immich 직접 접속 별칭 | HTTPS 전달 1개 + HTTP 리다이렉트 1개 |
전환 전후 GET 검사 35개에서 상태 코드, 리다이렉트 대상, 보안 헤더와 health 응답을 비교했습니다. Grafana의 DB health, Nextcloud의 설치·maintenance 상태, Immich ping, WordPress 관리자·공개 경로도 확인했습니다. WordPress의 /wp-admin-tools나 /foo/wp-admin/가 기존처럼 404인 것도 경로 경계 검증에 포함했습니다.
최종적으로 수동 관리하던 HTTPRoute 일곱 개를 제거했고, 전환 완료 시점의 HTTPRoute 18개는 모두 원본 Ingress를 소유자로 가리켰습니다. 기존 인증서와 공유 Gateway도 유지했습니다. 이 검증은 GET 요청과 라우팅 비교 중심이며, 인증 후 파일 업로드나 WebDAV 쓰기까지 검증한 결과는 아닙니다.
코드는 ingress-gateway-bridge 저장소에 있습니다. 초기 개발 버전이지만, 제 홈랩에서는 애플리케이션의 경로를 Ingress 한 곳에서 수정하고 기존 Gateway로 전달하는 흐름을 만들 수 있었습니다.

답글 남기기