-
이코에코(Eco²) Auth Offloading: 도메인 공통 모듈 제거이코에코(Eco²)/Auth Offloading (ext-authz) 2025. 12. 13. 17:17

Python, HCL, Go. 나름 다채로워진 Languages 분포다. 클러스터+Gitops 구축 당시엔 HCL+Shell이 가장 높았다.
지난 포스팅에 더해 Auth Offloading을 이어서 작성하겠다.
Auth Offloading이 필요했던 이유와 가능해진 배경, 의사결정 과정, 구현, 부하 테스트까지를 다뤘다면
이 글에선 Auth Offloading으로 얻을 수 있던 소소한 개선과 한계를 서술한다.
기존 구조의 문제점
domains/ ├── _shared/ │ └── security/ # 공통 모듈 │ ├── jwt.py # TokenPayload, extract_token_payload │ └── dependencies.py # build_access_token_dependency ├── scan/ │ └── api/dependencies.py → import from _shared/security ├── my/ │ └── security.py → import from _shared/security ├── location/ │ └── security.py → import from _shared/security ...
ext-authz 개발기에서 언급했듯 MSA에서 공통 모듈은 분산 모놀리스를 만든다.
모듈을 수정하면 모든 도메인이 영향을 받고, 도메인 간 독립 배포가 어려워진다.
그간 공통 모듈에 의존했던 이유는 각 도메인에서 JWT 파싱 로직으로 유저 정보를 검증, 추출했기 때문이다.# 기존: JWT를 직접 파싱해야 했음 def extract_token_payload(token: str) -> TokenPayload: payload = jwt.get_unverified_claims(token) # jose 라이브러리 return TokenPayload( sub=payload["sub"], jti=payload["jti"], type=TokenType(payload["type"]), exp=payload["exp"], iat=payload["iat"], provider=payload["provider"], )
ext-authz 도입 후
ext-authz가 JWT 검증을 완료하고, 필요한 정보를 HTTP 커스텀 헤더로 주입한다.

ext-authz가 주입하는 헤더:
헤더 출처 값 예시 x-user-idJWT subclaim8b8ec006-2d95-45aa-bdef-e08201f1bb82x-auth-providerJWT providerclaimgoogle,kakao
변경 후: 도메인 독립 구현
이제 각 도메인에서 단순 헤더 추출만 하면 된다. JWT 파싱 라이브러리(
python-jose)도 필요 없다.# domains/{도메인}/security.py (또는 dependencies.py) from uuid import UUID from typing import Optional from fastapi import Header, HTTPException, status from pydantic import BaseModel class UserInfo(BaseModel): """ext-authz가 주입한 헤더에서 추출된 사용자 정보""" user_id: UUID provider: str async def _extract_user_info( x_user_id: Optional[str] = Header(default=None, alias="x-user-id"), x_auth_provider: Optional[str] = Header(default=None, alias="x-auth-provider"), ) -> UserInfo: """ ext-authz가 주입한 헤더에서 사용자 정보 추출. Headers: x-user-id: JWT sub claim (사용자 UUID) x-auth-provider: JWT provider claim (google/kakao/naver) Note: - ext-authz가 JWT 검증과 블랙리스트 조회를 완료한 상태 - 이 헤더들은 클라이언트가 직접 설정할 수 없음 (ext-authz가 덮어씀) """ if not x_user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing x-user-id header", ) try: user_id = UUID(x_user_id) except ValueError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid x-user-id format", ) from exc return UserInfo( user_id=user_id, provider=x_auth_provider or "unknown", ) # 로컬 개발/테스트용 auth bypass if settings.auth_disabled: async def get_current_user() -> UserInfo: return UserInfo( user_id=UUID("00000000-0000-0000-0000-000000000000"), provider="disabled", ) else: get_current_user = _extract_user_info
엔드포인트에서 사용
from fastapi import APIRouter, Depends from domains.scan.api.dependencies import get_current_user, UserInfo router = APIRouter(prefix="/scan", tags=["scan"]) @router.post("/classify") async def classify( payload: ClassificationRequest, user: UserInfo = Depends(get_current_user), # 헤더에서 자동 추출 service: ScanService = Depends(), ): return await service.classify(payload, user.user_id)
헤더 신뢰성
// ext-authz server (Go) func allowResponse(userID, provider string) *authv3.CheckResponse { return &authv3.CheckResponse{ // ... Headers: []*corev3.HeaderValueOption{ { Header: &corev3.HeaderValue{ Key: "x-user-id", // 클라이언트가 설정해도 Value: userID, // ext-authz 값으로 덮어씀 }, }, { Header: &corev3.HeaderValue{ Key: "x-auth-provider", Value: provider, }, }, }, } }ext-authz의
allowResponse에서 헤더를 설정할 때, Envoy는 기존 헤더를 덮어쓴다.
따라서 백엔드 서비스에서 받는x-user-id,x-auth-provider는 항상 ext-authz가 검증 후 주입한 값이 된다.
의존성 분리 정리
항목 AS-IS TO-BE 의존성 _shared/securityimport없음 JWT 라이브러리 python-jose필요불필요 코드 라인 3개 파일 참조 ~30줄 자체 구현 도메인 독립성 ❌ 공유 모듈에 의존 ✅ 완전 독립 빌드/배포 모듈 변경 시 전체 영향 도메인별 독립 배포
삭제된 파일
rm domains/_shared/security/__init__.py rm domains/_shared/security/jwt.py rm domains/_shared/security/dependencies.pyext-authz로 인증/인가 로직이 비즈니스 레이어에서 분리되면서
- JWT 파싱 불필요 → 헤더 추출만으로 충분
- 공통 모듈 불필요 → 각 도메인이 ~30줄로 자체 구현
- 도메인 완전 독립 → MSA 원칙 준수
- 의존성 감소 →
python-jose제거 가능
한계: Global Choke Point

모든 인증 필요 트래픽이 ext-authz 서버를 경유하며, 검증 서버 장애 시 Fail-closed 정책에 따라 전 서비스 접근이 차단된다. 따라서 ext-authz의 처리량이 곧 시스템 전체의 상한이 된다. (merics, healths 등 비인증/인가 엔드포인트 제외)
FastAPI Auth 서버 중 JWT 검증(AuthN/AuthZ)만 분리되어 경량화된 go+gRPC 서버로 SOF가 옮겨간 것 뿐이다.
현재는 되려 서비스 API의 병목에 걸려, 유저수가 늘어나도 ext-authz 서버에 280 RPS 이상 부하를 주기 어렵지만..
유저 1000명의 환경에서 p99 20-80ms, avg 2ms가 유지되는 건 상당히 고무적이다.



Auth Offloading은 연산 분리를 넘어, 이코에코 도메인 간 코드 레벨의 결합도를 낮추는 개선으로 이어졌다.
클라우드 네이티브를 전제로 온전한 도메인 분리를 이루고자 K8s, GitOps, Istio, gRPC까지 긴 여정이었지만 차근차근 이뤄내니 기쁘다. 24년 6월부터 직접 구축하고 싶던 구조였으나, 역량 부족 + 취준과 업무에 바빠 개인 프로젝트는 docker-compose로 간단히 분리해 올리는 일이 많았다. (경험상 K8s - MSA 프로덕션에서 개발하는 일과 직접 구축하는 건 아무래도 다른 일 같긴 하다.)
아직 DB 레이어의 도메인 분리로 나아가기까지는 MQ가 과제로 남아있지만, 이제 그 기반이 갖춰졌으니 조만간 이뤄질 듯 싶다.
다음은 로깅 시스템, Jaegger, Kaili 등 MQ 도입 전 Observability를 강화할 예정이다. 너무 늦지 않게 기록하겠다.'이코에코(Eco²) > Auth Offloading (ext-authz)' 카테고리의 다른 글
이코에코(Eco) ext-authz 추가 테스트: Redis 병렬 연결 활성화 (vCPU 2, io-threads 2) (0) 2025.12.17 이코에코(Eco²) ext-authz 성능 튜닝: Redis PoolSize, HPA (0) 2025.12.15 이코에코(Eco²) ext-authz: AuthN/AuthZ 검증 엔진 Stress Test (0) 2025.12.14