Part 1에서 도입 배경과 선택 과정을 다뤘다면, 이번 글은 설계와 구축의 현실적인 이야기입니다.
엔터프라이즈 환경에서 안정적으로 운영 가능한 허브를 만들기 위해,
인증, 사용자 격리, 표준 베이스 이미지, 보안, 모니터링, 배포 자동화까지 전면 재설계를 진행했습니다.
1. JupyterHub 아키텍처 이해와 설계 원칙
JupyterHub는 구조가 단순해 보이지만,
Hub, Proxy, Authenticator, Spawner, Notebook Server의 다섯 가지 핵심 컴포넌트가 서로 맞물려 돌아갑니다.
각 요소가 분리되어 있어 유연하지만, 그만큼 설계 방향을 잘못 잡으면 관리 복잡도가 급격히 높아집니다.
우리가 가장 먼저 세운 설계 원칙은 다음 다섯 가지였습니다.
낮은 결합도 / 높은 응집도
각 기능은 독립적으로 동작하되 전체적으로 유기적으로 연결되도록 설계했습니다.유지보수 용이성
패키지 추가나 버전 교체를 운영자가 부담 없이 반영할 수 있어야 했습니다.확장성
사용자와 서비스가 늘어나도 구조 변경 없이 확장 가능해야 했습니다.보안성
인증, 접근제어, 권한 관리가 항상 최우선입니다.사용자 친화성
비개발자도 쉽게 사용할 수 있어야 했습니다.
이 다섯 가지 원칙은 이후 모든 기술적 결정을 이끌었습니다.
특히 그중에서도 ‘사용자 격리와 보안’은 가장 핵심적인 과제였습니다.
우리는 단순히 여러 사람이 동시에 노트북을 쓰는 환경이 아니라,
서로의 작업이 완전히 분리된 독립 실행 환경을 만들고자 했습니다.
그 출발점이 바로 Spawner 전략이었습니다.
2. Spawner 전략 — 사용자 격리와 자원 통제
처음에는 별도의 Spawner 없이 기본 구조로 운영을 시작했습니다.
즉, 모든 사용자가 같은 서버와 같은 파일 시스템을 공유했습니다.
겉보기엔 단순하고 빠르지만, 곧 치명적인 문제가 드러났습니다.
한 사용자가 작업 중인 노트북이 다른 사용자에게 그대로 노출되거나,
같은 경로의 파일을 수정하면서 서로의 실행 상태가 꼬이는 일이 발생했습니다.
공유 환경에서는 개인 작업이 서로 간섭될 수밖에 없었고,
결국 데이터 무결성과 보안 측면에서 치명적인 리스크가 되었습니다.
이 문제를 해결하기 위해 DockerSpawner를 채택했습니다.
이제 사용자가 로그인하면 JupyterHub는 즉시 새로운 컨테이너를 띄우고,
각 사용자에게 고유한 UID/GID와 전용 볼륨을 부여합니다.
CPU와 메모리 제한도 컨테이너 단위로 적용되어,
같은 서버에 수십 명이 접속해도 서로 간섭하지 않게 되었습니다.
3. 시행착오와 개선 과정
DockerSpawner 하나로 모든 문제가 해결된 것은 아니었습니다.
개발과 테스트 단계에서 다양한 이슈가 발생했고, 그 과정에서 구조가 다듬어졌습니다.
Athena 연결 불안정
컨테이너 네트워크 설정 미비로 간헐적인 연결 실패가 발생했습니다.
NAT 라우팅, 보안 그룹, DNS 설정을 재정비하여 안정화했습니다.Spark Livy 통신 문제
세션이 중복 생성되거나 타임아웃이 발생했습니다.
브리지 네트워크를 분리하고 포트를 고정해 해결했습니다.Zombie Container 누적
세션 종료 후 컨테이너가 정리되지 않아 Spawner already running 오류가 발생했습니다.
로그아웃 후처리 스크립트와 정기 청소 작업을 추가해 해소했습니다.볼륨 충돌 및 UID 권한 문제
모든 컨테이너가 동일 UID(1000)로 실행되며 캐시·설정 파일 충돌이 발생했습니다.
사용자별 볼륨 경로 분리 및 UID/GID 환경 변수 주입으로 해결했습니다.OAuth Redirect 충돌
컨테이너 IP가 매번 달라져 Redirect URI가 불일치했습니다.
Reverse Proxy를 고정하고 내부 통신만 허용하도록 수정했습니다.이미지 업데이트 시 세션 초기화
베이스 이미지 갱신 시 세션이 끊겼습니다.
Immutable Tag(:v1, :v2) 기반 버전 관리로 배포 주기를 고정했습니다.
이 일련의 과정을 통해 DockerSpawner는 단순 격리를 넘어,
운영 자동화 실험의 중심으로 발전했습니다.
4. 표준 베이스 이미지 설계
모든 컨테이너는 하나의 표준 베이스 이미지에서 파생됩니다.
이 이미지는 다음과 같은 구성으로 통일했습니다.
dbt-core, dbt-athena-community
PySpark, boto3, awswrangler
pandas, matplotlib, requests 등 공용 라이브러리
requirements.lock으로 패키지 버전을 고정하고, CI 파이프라인에서 자동 검증을 수행했습니다.
이를 통해 누가 로그인하든 동일한 환경을 보장하고 분석 재현성을 확보했습니다.
5. 보안 및 권한 관리
보안은 이번 프로젝트의 중심이었습니다.
우리는 개인 키와 수동 설정을 전면 제거했습니다.
Google OAuth 인증: 사내 계정 로그인 및 MFA 정책 상속
IAM Role 기반 접근 제어: 개인 AWS Key 없이 Role 매핑을 통해 S3·Athena·Glue 접근
Secrets Manager: DB 비밀번호·API Key 등 민감정보 안전 관리
IP Whitelist / Security Group: Hub ↔ Spawner 통신 고정, 외부 접근 차단
이 구조로 감사 추적성과 최소 권한 원칙(Least Privilege)을 동시에 달성했습니다.
사용자는 단순히 로그인만 하면 필요한 권한이 자동으로 주어집니다.
보안이 안정되니, 외부 서비스 연계는 그만큼 쉽게 풀렸습니다.
6. 외부 서비스 연계 구조
JupyterHub는 Athena, S3, Spark, Google Drive, GitHub Enterprise까지 하나의 체계로 엮였습니다.
Athena: PyAthena + 커스텀 매직 명령어(%sql, %sql.download, %sql.googledrive)
S3: awswrangler, boto3 기반 입출력 및 결과 저장
Google Drive: OAuth 기반 결과물 업로드
Spark Livy: API 기반 Spark Job 실행·모니터링
GitHub Enterprise: DBT사용을 위한 사용자 배치 등록 자동화
최종 아키텍처
최종 구조는 다음 네 가지 축으로 요약됩니다.
DockerSpawner 기반 사용자 격리
표준화된 베이스 이미지와 CI/CD
IAM Role 및 Secret Manager 중심의 보안 모델
매직 함수와 GitHub 자동화를 통한 통합 사용자 경험
Zeppelin 시절의 “쿼리 실행 공간”을 넘어,
지금의 JupyterHub는 인증·권한·격리·결과 관리가 완전히 통합된 엔터프라이즈 데이터 허브로 진화했습니다.
Part 2에서 우리는 JupyterHub의 핵심 아키텍처를 설계했습니다.
사용자 격리, 인증, 표준 베이스 이미지, 보안 모델까지 — 어떤 구조로 가져가야 할지 방향을 세운 단계였습니다.
이제 Part 3에서는 그 설계를 실제로 구현한 결과를 다룹니다.
Zeppelin 사용자의 익숙한 인터페이스를 계승하면서 기능을 확장한 %%sql, %%spark 매직,
지능형 SQL 작성 환경을 만든 LSP 자동완성과 Athena 스키마 패널,
그리고 협업을 자동화한 GitHub 연동 구조를 중심으로 살펴봅니다.
마지막으로, 이렇게 만들어진 시스템을 안정적으로 운영하기 위해
무중단 배포, Jenkins 기반 롤백, Portainer 모니터링, 셀 실행 로그 관리 등
실제 운영 방식까지 함께 정리합니다.