Envoy Gateway 멀티 네임스페이스와 팀 소유권: allowedRoutes, ReferenceGrant, 그리고 팀 간 거버넌스

9 min read

Gateway API의 가장 큰 가치는 YAML이 더 예뻐진 것만이 아닙니다. 오랫동안 흐릿했던 질문에 마침내 명확한 답을 준다는 점입니다:

누가 ingress 계층을 소유하는가? 누가 route를 바꿀 수 있는가? 네임스페이스끼리 서로를 참조할 수 있는가?

많은 Ingress의 고통은 모든 것이 뒤엉켜 있었다는 데서 왔습니다. 플랫폼 팀은 승인되지 않은 노출 변경을 두려워했고, 앱 팀은 path 하나 바꾸려 해도 티켓을 열어야 한다고 느꼈습니다. 모두가 YAML 속에서 서로를 괴롭히게 된 셈이죠.

Gateway API는 이를 아주 분명하게 나눕니다:

  • 플랫폼 ingress는 독립적으로 거버넌스 가능
  • route는 앱 팀에 위임 가능
  • 네임스페이스 간 동작은 명시적 승인 필요

이 글에서는 이 소유 모델을 위에서 아래까지 설명합니다.

먼저 여기부터: Attachment와 Reference는 다른 문제다

이건 가장 자주 헷갈리는 구분입니다.

Route가 Gateway / Listener에 Attach되는 문제

이것은 다음에 의해 결정됩니다:

  • route의 parentRefs
  • listener의 allowedRoutes

즉 route가 listener에 attach할 수 있는지는 ReferenceGrant가 아니라, 그 listener가 받아줄 의사가 있는지로 결정됩니다.

네임스페이스 간 리소스 참조 문제

이때 주로 등장하는 것이 ReferenceGrant입니다. 예를 들면:

  • Gateway가 다른 네임스페이스의 Secret을 참조하려는 경우
  • HTTPRoute가 다른 네임스페이스의 Service를 참조하려는 경우

이건 attach의 문제가 아닙니다. "대상 네임스페이스가 내 리소스를 써도 된다고 정식으로 승인했는가"의 문제입니다.

많은 사람이 이 두 가지를 하나로 뭉뚱그려 생각하다가 스스로 제정신을 의심하게 됩니다. 역할만 분리해 보면 규칙은 생각보다 신비롭지 않습니다.

allowedRoutes: 먼저 누가 Attach할 수 있는지 결정하자

가장 보수적인 listener는 이렇게 생깁니다:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shared-gateway
  namespace: infra
spec:
  gatewayClassName: eg
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      allowedRoutes:
        namespaces:
          from: Same

이 뜻은 infra 네임스페이스에 있는 route만 이 listener에 attach할 수 있다는 것입니다.

특정 팀 네임스페이스만 attach하도록 허용하려면 Selector를 사용합니다:

allowedRoutes:
  namespaces:
    from: Selector
    selector:
      matchLabels:
        shared-gateway-access: "true"

이 패턴은 플랫폼 팀에 특히 잘 맞습니다. 모든 걸 완전히 잠글 필요도 없지만, 그렇다고 from: All로 활짝 열어 아무나 붙게 둘 필요도 없습니다. 특정 label을 가진 네임스페이스만 attach 권한을 얻도록 만들 수 있습니다.

💡 allowedRoutes는 Gateway 단위가 아니라 listener 단위입니다. 서로 다른 listener마다 다른 접근 정책을 가질 수 있습니다.

ReferenceGrant: 대상 네임스페이스가 결정한다

route나 gateway가 다른 네임스페이스의 리소스를 참조하려면, 대상 네임스페이스가 권한을 줘야 합니다.

예를 들어 앱 팀의 HTTPRoute가 공유 네임스페이스에 있는 백엔드로 트래픽을 보내려는 경우:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
  namespace: team-a
spec:
  parentRefs:
    - name: shared-gateway
      namespace: infra
      sectionName: https
  rules:
    - backendRefs:
        - name: shared-api
          namespace: shared-services
          port: 8080

그러면 shared-services 네임스페이스에는 이런 ReferenceGrant가 필요합니다:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-team-a-route
  namespace: shared-services
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      namespace: team-a
  to:
    - group: ""
      kind: Service

쉽게 풀면:

  • team-aHTTPRoute 리소스가
  • shared-servicesService 리소스를 참조하도록 승인받았다

중요한 점은 이 승인이 소스가 아니라 대상 네임스페이스에서 만들어진다는 것입니다. 소스 쪽이 "접근하고 싶다"고 선언한다고 되는 게 아니라, 리소스 소유자가 결정합니다. 사실 꽤 원칙적인 설계입니다. 자기 리소스에 누가 접근할지 소유자가 통제해야 하니까요.

흔한 플랫폼 팀 / 앱 팀 분리 모델

가장 일반적인 소유 모델은 이렇습니다:

플랫폼 팀이 소유

  • GatewayClass
  • Gateway
  • Listener
  • TLS 인증서 관리
  • allowedRoutes 정책

앱 팀이 소유

  • 각자 네임스페이스의 HTTPRoute / GRPCRoute
  • 백엔드 서비스
  • 라우팅 규칙 조정

이 분리의 장점은 분명합니다:

  • 플랫폼 팀은 ingress 노출과 보안 경계를 지킨다
  • 앱 팀은 자신의 트래픽 규칙을 빠르게 바꿀 수 있다
  • path 하나 바꾸려고 공유 Gateway를 건드릴 필요가 없다

물론 모든 권한을 한 그룹에 몰아주는 것도 틀린 건 아닙니다. 하지만 팀이 커질수록 그 모델은 보통 "누구나 바꿀 수 있으니, 깨졌을 때 누가 뭘 했는지 아무도 모른다"로 진화합니다.

실전 조언: 멀티 테넌트 재난 피하는 법

정말 유용한 습관들:

  • 공유 Gateway는 기본값을 allowedRoutes.namespaces.from: Selector로 시작하기
  • 네임스페이스 label 규칙은 안정적으로 유지하기. 다음 달에 team-aallow-gw로 바꾸지 않기
  • 네임스페이스 간 backend/Secret 참조는 모두 ReferenceGrant를 통과시키기
  • Route 이름에 팀이나 서비스를 드러내기. 예: team-a-api-route
  • 공유 listener에서는 route가 잘못된 입구에 붙지 않도록 항상 sectionName 사용하기

붙잡아 둘 만한 원칙 하나:

ingress 계층에서 경계를 강제할 수 있다면, 문서와 자율 통제에만 기대지 마세요.

엔지니어링에서는 말로 한 약속보다 시스템화된 절차가 더 강합니다. 좀 연륜 있는 조언처럼 들릴 수 있지만, 실제로 꽤 유용합니다.

한 줄 요약

Gateway API에서 allowedRoutes는 "누가 들어올 수 있는지"를 제어하고, ReferenceGrant는 "들어온 뒤 무엇에 접근할 수 있는지"를 제어합니다. 이 둘만 명확히 구분해도 멀티 네임스페이스와 멀티 팀 시나리오는 훨씬 다루기 쉬워집니다.

다음 단계

소유권과 권한을 정리했으니, 마지막 퍼즐은 이것입니다: 설정이 반영되지 않을 때, 어떤 status와 어떤 condition, 어떤 명령을 먼저 봐야 할까?

다음 글에서는 디버깅과 status 해석을 다룹니다: 👉 상태와 디버깅 핸드북