2025. 9. 14. 16:33ㆍ카테고리 없음
Introduction
Airflow 는 airbnb 에서 워크플로우를 관리하고 스케줄링하기 위해 개발한 파이썬 기반 오픈소스입니다. 본격적인 MLOps 환경을 구성하기 위해 새로 구축된 GPU 클러스터에 helm 차트를 이용해서 airflow 를 배포했습니다.
Airflow를 선정한 이유는 Dynamic, Extensible, Flexible이라는 장점 때문입니다. 운영 환경에서 모델 학습을 함께 실행해야 한다는 요구사항을 고려할 때 확장성(scalability)이 매우 중요했으며, GPU 리소스 관리도 필요했습니다. 개발 속도 측면에서 강점을 가진 Prefect도 대안으로 검토했으나, 최종적으로는 Airflow가 팀의 요구사항에 가장 적합하다고 판단하여 선정했습니다.
OS: Rocky Linux
Kubernetes v1.32.0
Helm chart: v1.18.0
Airflow: v3.0.2
클러스터 내 Airflow 구성

Airflow는 K8S 클러스터에서 배포하는 만큼, KubernetesExecutor 를 사용하도록 설정했습니다.
DAG가 시작되면 scheduler 가 동적으로 worker pod 을 생성하고, 작업완료 후 pod 을 제거합니다. 이 과정에서 리소스 제한 및 할당을 수행할 수 있어 효율적으로 구성할 수 있습니다.
웹서버는 NodePort 로 노출시켜서 사용자가 간단하게 접근할 수 있도록 구성했습니다.
각 컴포넌트의 역할을 간단히 소개하겠습니다.
- Webserver: UI 제공 및 API 서버 역할
- Scheduler: DAG 스케줄링 및 Worker Pod 생성
- Triggerer: 비동기 작업 트리거링
- DAG Processor: DAG 파일 파싱 및 처리
스토리지는 DB / DAG / log 으로 각각 역할이 분리되어 있습니다.
- airflow-dags-pv: DAG 파일 저장 (RWO)
- airflow-logs-pv: 로그 저장 (RWX)
- airflow-postgresql-pv: DB 데이터 (RWO)
Kubernetes의 스토리지
PV & PVC
PV(PersistentVolume) 은 클러스터 관리자가 프로비저닝한 스토리지 리소스입니다. 실제 물리적인 스토리지(NFS, iSCSI, 클라우드 스토리지, 로컬 디스크 등)를 추상화한 개념으로, 클러스터 레벨의 리소스입니다.
PVC(PersistentVolumeClaim) 는 사용자가 스토리지를 요청하는 방식입니다. Pod가 컴퓨팅 리소스를 요청하듯이, PVC는 스토리지 리소스를 요청합니다. PVC는 네임스페이스 레벨의 리소스로, 특정 크기와 접근 모드를 요구합니다.
이번 Airflow 배포에서는 hostPath 타입의 PV를 사용했습니다. hostPath는 노드의 파일시스템 경로를 직접 마운트하는 방식으로, 개발 및 테스트 환경에서 간단하게 사용할 수 있습니다. 다른 타입으로는 emptyDir, Network Volume 등이 있습니다만 emptyDir은 pod가 내려가면 내용이 삭제되므로 영구 스토리지로는 적합하지 않으며 Network Volume 은 상대적으로 복잡도가 높습니다. 클러스터 관리자 스토리지 정책을 변경할 예정이기 때문에 간단하게 hostPath 방식을 사용했고, 추후 storageClass로 마이그레이션 할 예정입니다.
StorageClass
StorageClass는 동적 프로비저닝을 위한 템플릿입니다. 클라우드 환경에서는 StorageClass를 통해 PVC가 생성될 때 자동으로 PV를 프로비저닝할 수 있습니다. 그러나 hostPath 를 사용한 수동 프로비저닝을 채택했기 때문에, "" 를 사용해서 비활성화 했습니다.
# PVC에서 특정 PV를 지정
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: airflow-dags
namespace: mlops
spec:
storageClassName: "" # 동적 프로비저닝 비활성화
selector:
matchLabels:
pv-name: airflow-dags-pv # 특정 PV 지정
🔥 트러블슈팅
DB 연결 이슈
배포하자마자 airflow-run-airflow-migrations 파드에서 크래시가 발생하고, 나머지 파드는 실행되지 않았습니다.
airflow-run-airflow-migrations 파드는 위 airflow 구성도에는 포함되지 않지만, airflow가 사용하는 데이터베이스의 초기화를 담당하는 job pod 입니다.
describe 로 확인했을 때는 특이 로그가 없었지만 logs로 직접 파드의 로그를 확인해보니 아래와 같이 DB연결에 문제가 있다 는 것을 알 수 있었습니다.
k describe airflow-run-airflow-migrations-abcde
# nothing special
k logs airflow-run-airflow-migrations-abcde
###############
psycopg2.OperationalError: connection to server at "airflow-postgresql.mlops" (${Node_IP}), port 5432 failed: Operation not permitted
Is the server running on that host and accepting TCP/IP connections?
따라서 DB 파드를 확인해보니 아래와 같이 PVC가 연결되지 않았다는 정보를 알 수 있었습니다. 제가 스토리지를 PV/PVC로 사용한다는 옵션을 줘놓고, 정작 Airflow 가 사용할 PV, PVC 를 생성하지 않았네요.
kubectl describe pod airflow-postgresql-0
#############
Conditions:
Type Status
PodScheduled False
Volumes:
config:
Type: ConfigMap (a volume populated by a ConfigMap)
Name: airflow-config
Optional: false
dags:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: airflow-dags
ReadOnly: false
logs:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: airflow-logs
ReadOnly: false
kube-api-access-q7s8w:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
Optional: false
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 60s (x10 over 46m) default-scheduler 0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.
helm chart 로 배포하는 airflow 는 DB 데이터 저장, DAG 저장, 실행 로그 저장 을 각각 수행하기 위해 3개의 스토리지를 요구하기 때문에, 총 3개의 PV와 PVC 를 생성해 주었습니다.
권한 문제
PV를 배포하고 나자 airflow-run-airflow-migrations job은 정상적으로 동작을 완료했지만, scheduler/triggerer/webserver 파드가 계속 재시작되었습니다. STATUS 에는 Running 이라고 되어 있지만 재시작 횟수를 보면 정상 동작하지 않고 있습니다.
NAME READY STATUS RESTARTS AGE
pod/airflow-postgresql-0 1/1 Running 0 8m17s
pod/airflow-scheduler-648859bbbf-m4ln4 1/2 Running 10 (75s ago) 8m17s
pod/airflow-statsd-69b9f7fc7c-xvpdh 1/1 Running 0 8m17s
pod/airflow-triggerer-0 2/2 Running 8 (74s ago) 8m17s
pod/airflow-webserver-79b7ff79fd-cntw8 0/1 Running 6 (2m6s ago) 8m17s
재시작 하고 있는 pod 들을 모두 describe 로 찔러보니, 경로를 생성할 수 있는 권한이 없다는 공통적인 문제가 있었습니다. 아래 로그메시지는 airflow-triggerer-0 pod 의 내용이지만, scheduler와 webserver에도 경로만 다른 동일한 에러가 발생하고 있었습니다.
k describe pod airflow-triggerer-0
#############
Unable to load the config, contains a configuration error.
Traceback (most recent call last):
File "/usr/local/lib/python3.12/pathlib.py", line 1311, in mkdir
os.mkdir(self, mode)
FileNotFoundError: [Errno 2] No such file or directory: '/opt/airflow/logs/scheduler/2025-08-13'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.12/logging/config.py", line 581, in configure
handler = self.configure_handler(handlers[name])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/logging/config.py", line 854, in configure_handler
result = factory(**kwargs)
^^^^^^^^^^^^^^^^^
File "/home/airflow/.local/lib/python3.12/site-packages/airflow/utils/log/file_processor_handler.py", line 53, in __init__
Path(self._get_log_directory()).mkdir(parents=True, exist_ok=True)
File "/usr/local/lib/python3.12/pathlib.py", line 1315, in mkdir
self.parent.mkdir(parents=True, exist_ok=True)
File "/usr/local/lib/python3.12/pathlib.py", line 1311, in mkdir
os.mkdir(self, mode)
PermissionError: [Errno 13] Permission denied: '/opt/airflow/logs/scheduler'
airflow user(default: 50000) 에게 PV가 베어메탈에서 물리적으로 접근하는 경로에 대해 접근 권한이 없기 때문에 발생하는 문제였습니다. user에게 sudo 권한을 주지 않고 해결할 방법을 고민하다가 helm install하기 전에 PV가 사용할 경로들의 디렉토리를 생성한 뒤, airflow user 로 소유자를 변경하고 775 권한을 부여하는 shell script를 클러스터의 모든 노드에서 실행했습니다.
echo "1. Creating Airflow directories..."
mkdir -p /data/airflow/logs/dag_processor
mkdir -p /data/airflow/logs/scheduler
mkdir -p /data/airflow/logs/worker
mkdir -p /data/airflow/dags
mkdir -p /data/airflow/postgresql
echo "2. Setting ownership (50000:0)..."
chown -R 50000:0 /data/airflow/logs
chown -R 50000:0 /data/airflow/dags
chown -R 50000:0 /data/airflow/postgresql
echo "3. Setting permissions (775)..."
chmod -R 775 /data/airflow/logs
chmod -R 775 /data/airflow/dags
chmod -R 775 /data/airflow/postgresql
클러스터가 대규모로 확장된다면 다른 방법을 찾아야겠지만, 우선은 클러스터 규모가 작고 당분간 확장 예정이 없어 간단하게 해결했습니다. 정석적인 방법은 airflow user 에게 해당 디렉토리에 대한 권한을 부여하면 될 것 같습니다.
스토리지 부족
0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) had untolerated taint {node.kubernetes.io/disk-pressure: }. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.
엥 진짜 다 해결했다고 생각했는데 위와 같은 에러가 발생했습니다.
에러메시지의 초반부에는 taint/toleration 관련 내용이 있어 감을 잡지 못했었는데, DiskPressure 를 키워드로 살펴보니 클러스터의 모든 노드가 스토리지를 허용량 이상 사용하고 있어서 클러스터가 더 이상의 작업을 거부하는 상황이었습니다.
도커 빌드 캐시, 안 쓰는 컨테이너 등등을 오랜만에 싹 지우니 문제가 해결되었고 마침내 정상적으로 실행 및 사용할 수 있었습니다.
공식 문서에 스토리지 용량 부족으로 인해 발생하는 DiskPressure 외에도 메모리 부족으로 인해 발생하는 MemoryPressure, 허용 프로세스 갯수를 초과해서 발생하는 PIDPressure도 소개되어 있으니, 참고해보셔도 좋을 듯 합니다.