ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이코에코(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-id JWT sub claim 8b8ec006-2d95-45aa-bdef-e08201f1bb82
    x-auth-provider JWT provider claim google, 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/security import 없음
    JWT 라이브러리 python-jose 필요 불필요
    코드 라인 3개 파일 참조 ~30줄 자체 구현
    도메인 독립성 ❌ 공유 모듈에 의존 ✅ 완전 독립
    빌드/배포 모듈 변경 시 전체 영향 도메인별 독립 배포

    삭제된 파일

    rm domains/_shared/security/__init__.py
    rm domains/_shared/security/jwt.py
    rm domains/_shared/security/dependencies.py

    ext-authz로 인증/인가 로직이 비즈니스 레이어에서 분리되면서

    1. JWT 파싱 불필요 → 헤더 추출만으로 충분
    2. 공통 모듈 불필요 → 각 도메인이 ~30줄로 자체 구현
    3. 도메인 완전 독립 → MSA 원칙 준수
    4. 의존성 감소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를 강화할 예정이다. 너무 늦지 않게 기록하겠다.

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango