다들 아시다시피 저는 작은 규모로 Airflow를 운영하고 있습니다.
사용자는 고작 3명 정도지만 그래도 여러 이유로 인증은 저에게 좀 큰 문제 중에 하나 였습니다.
로그인 없이 바로 모든 것이 오픈되어 있으면 좀 곤란했어요.
그래서 Airflow 3.x 에서 기본적으로 제공하는 인증 방식들을 하나씩 살펴봤습니다.
| 인증 방식 | 문제점 |
|---|---|
| SimpleAuth | 비밀번호가 평문으로 저장된다. 프로덕션에서 쓰라고 만든 게 아니다. |
| FABAuth | PostgreSQL이나 MySQL 같은 외부 DB가 필요하다. |
| LDAP Auth | Active Directory 서버가 필요하다. 엔터프라이즈용이다. |
보다 보니 이런 생각이 듭니다.
“3명이 쓰는 Airflow인데, 왜 이렇게 복잡해야 하지?”
그래서 딱 필요한 수준의 인증만 제공하는 인증 관리자 를 직접 만들어보기로 했습니다. 물론 Claude와 함께요 ㅎㅎ
구현 목표: “딱 인증 정도만.”
처음부터 명확한 원칙을 세웠습니다.
욕심 부리지 않고 딱 필요한 정도만.
만들 것:
- YAML 파일 하나로 사용자 관리
- bcrypt로 안전하게 비밀번호 해싱
- Admin/Editor/Viewer 3단계 역할
- CLI로 사용자 추가/삭제
만들지 않을 것:
- 외부 DB 의존성
- 복잡한 그룹/퍼미션 매트릭스
- OAuth, SSO 연동
이게 필요하면 이미 FAB나 LDAP를 써야 하는 환경이라고 생각했습니다.
아키텍처 설계
Airflow 3.x의 인증 시스템을 살펴보니, 핵심은 BaseAuthManager 를 상속받아 구현하는 구조였습니다.
AIP-56에서 인증 확장을 염두에 두고 설계된 덕분에 비교적 깔끔하게 접근할 수 있었죠.
┌─────────────────────────────────────────────────────────────┐
│ Apache Airflow 3.x │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ FileAuthManager │◄───│ BaseAuthManager │ │
│ └────────┬────────┘ └─────────────────┘ │
│ │ │
│ ┌────────▼────────┐ ┌─────────────────┐ │
│ │ UserStore │◄───│ users.yaml │ │
│ └────────┬────────┘ └─────────────────┘ │
│ │ │
│ ┌────────▼────────┐ ┌─────────────────┐ │
│ │ FileAuthPolicy │ │ FastAPI App │ │
│ │ (RBAC rules) │ │ (login/logout) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
그래서 컴포넌트를 이렇게 분리했습니다:
- FileAuthManager: Airflow와 인터페이스하는 메인 클래스
- UserStore: YAML 파일 읽기/쓰기, 사용자 CRUD
- FileAuthPolicy: 역할별 권한 정의(RBAC)
- FastAPI App: 로그인/로그아웃 엔드포인트
구현하면서 마주친 문제들
바이브 코딩(?)을 하다 보니 예상하지 못한 문제들을 꽤 많이 만났고, 그럴 때마다 해결하기 위해서 인간이 뇌를 좀 써서 온몸 비틀기를 해야 했습니다..ㅠ
1. 파일 동시 접근 문제
YAML 파일을 여러 프로세스가 동시에 읽고 쓸 수 있습니다. 그래서 경쟁 조건(race condition)이 발생하면 파일이 깨질 수 있다는 문제가 있습니다.
해결책: 파일 잠금 + 원자적 쓰기
def save(self) -> None:
"""Save users to YAML file atomically with file locking."""
# 임시 파일에 먼저 쓴다
fd, temp_path = tempfile.mkstemp(
suffix=".tmp",
prefix=".users_",
dir=self._file_path.parent,
)
with os.fdopen(fd, "w", encoding="utf-8") as f:
# 배타적 잠금 획득
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
yaml.safe_dump(data, f, default_flow_style=False)
f.flush()
os.fsync(f.fileno()) # 디스크에 확실히 쓰기
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
# 원자적 교체 - 중간에 죽어도 파일이 안 깨진다
os.replace(temp_path, self._file_path)
os.replace()는 POSIX 시스템에서 원자적(atomic) 연산입니다.
임시 파일에 먼저 쓰고, 마지막에 한 번에 교체하면 중간에 프로세스가 죽어도 원본 파일이 손상되지 않습니다.
2. Hot Reload 지원
실제 프로덕션 환경에 올라가 있는 상황에선, Airflow가 실행 중일 때 인증 유저를 관리 가능해야합니다.
그런데 Airflow가 실행 중일 때 users.yaml을 직접 수정하면 어떻게 될까?
재시작 없이 변경사항이 반영되는 기능도 제공해야합니다.
HOT_RELOAD_CHECK_INTERVAL = 5.0 # 5초마다 체크
def _check_hot_reload(self) -> None:
"""Check if file has changed and reload if necessary."""
current_time = time.time()
# 너무 자주 체크하면 성능에 영향
if current_time - self._last_check_time < HOT_RELOAD_CHECK_INTERVAL:
return
self._last_check_time = current_time
current_mtime = self._file_path.stat().st_mtime
if current_mtime > self._last_mtime:
logger.info("Users file changed, reloading")
self.reload()
파일의 수정 시간(mtime)을 주기적으로 확인해서, 변경되었으면 자동으로 다시 로드하는 방식으로 구현했습니다. 과도한 I/O를 피하면서도 실용적인 수준의 실시간성을 확보할 수 있었습니다.
3. JWT 토큰 보안
JWT 토큰에는 사용자 정보가 담기게 됩니다. 만약 공격자가 토큰을 조작해서 role: viewer를 role: admin으로 바꾸면? 아찔해지겠죠…
해결책: 토큰 역직렬화 시 항상 원본 데이터와 대조
def deserialize_user(self, token: dict[str, Any]) -> FileUser | None:
"""Deserialize user from JWT token payload.
Security: Always validates against current user store to prevent
privilege escalation via token manipulation.
"""
username = token.get("username")
if not username:
return None
# 토큰의 정보를 믿지 않고, 저장소에서 다시 가져온다
user = self.user_store.get_user(username)
if not user or not user.is_active:
return None
return user # 저장소의 역할을 사용
그래서 토큰에서 username만 추출하고, 실제 역할은 users.yaml에서 다시 읽는다. 토큰을 조작해도 권한 상승이 불가능합니다.
RBAC 역할 기반 접근 제어
제 환경에서는 세밀한 권한 분리가 필요하지 않았기 때문에, 단순한 3단계 역할 계층을 설계했습니다.
class Role(str, Enum):
ADMIN = "admin" # Level 3: 모든 권한
EDITOR = "editor" # Level 2: Dag 실행/관리
VIEWER = "viewer" # Level 1: 읽기 전용
ROLE_HIERARCHY = {
Role.ADMIN: 3,
Role.EDITOR: 2,
Role.VIEWER: 1,
}
권한 체크 로직은 간단하게 구현할 수 있습니다.
@classmethod
def has_minimum_role(cls, user_role: str, required_role: Role) -> bool:
"""Check if user has at least the required role level."""
return cls.get_role_level(user_role) >= ROLE_HIERARCHY[required_role]
리소스별 권한 매트릭스:
| 리소스 | Viewer | Editor | Admin |
|---|---|---|---|
| Dag 조회 | |||
| Dag 실행 | |||
| Connection 수정 | |||
| Variable 수정 | |||
| Pool 수정 |
Admin만 인프라 설정(Connection, Variable, Pool)을 건드릴 수 있게 했습니다. Editor는 Dag 실행까지만. Viewer는 모니터링용으로.
인증 관리자를 위한 CLI 도구
사용자 관리를 위해서 CLI 도구가 필요했습니다.
# 초기 설정
python -m airflow_file_auth_manager.cli init \
-f users.yaml \
-e admin@example.com
# 사용자 추가
python -m airflow_file_auth_manager.cli add-user \
-f users.yaml \
-u developer \
-r editor \
-e dev@example.com
# 사용자 목록
python -m airflow_file_auth_manager.cli list-users -f users.yaml
이런식으로 추가해 볼 수 있습니다.
Airflow에 세팅하는 법
pip install airflow-file-auth-manager
Airflow 설정은 간단합니다:
# airflow.cfg
[core]
auth_manager = airflow_file_auth_manager.FileAuthManager
[file_auth_manager]
users_file = /path/to/users.yaml
환경 변수로도 가능:
export AIRFLOW__CORE__AUTH_MANAGER=airflow_file_auth_manager.FileAuthManager
export AIRFLOW_FILE_AUTH_USERS_FILE=/path/to/users.yaml
마치며
이 모든 구현이 가능했던 건 AIP-56 Extensible User Management 덕분이었습니다.
Airflow 3.x에서 인증 시스템을 확장 가능하게 열어둔 덕분에, 이렇게 가벼운 파일 기반 인증도 자연스럽게 녹여낼 수 있었습니다.
물론 이 방식은 멀티 노드 환경이나 사용자 수가 많은 조직에는 적합하지 않습니다. 파일 동기화 문제가 있기 때문이죠.
하지만 저처럼 소규모 Airflow 프로덕션을 운영하고 있다면,
“FAB도 LDAP도 너무 무겁다”
라고 느끼는 분들에겐 충분히 실용적인 선택지라고 생각합니다.

