이 글에서 다루는 것

Airflow Executor 세 종류를 비교하고, MLOps 파이프라인에서 KubernetesExecutor를 선택한 이유와 RBAC·pod_template·KPO 실전 구성을 다룹니다.

선수지식


이 단계에서 해결하려는 문제

Airflow를 Kubernetes 위에 올리면 Executor 선택이 필요하다. LocalExecutor, CeleryExecutor, KubernetesExecutor 중에서 MLOps 파이프라인에 가장 적합한 것을 선택하는 것은 단순한 설치 옵션이 아니라 운영 구조에 대한 결정이다. Task 격리, 동적 리소스 할당, Broker 관리 비용을 기준으로 판단한다.


🎯 핵심 요약

  • Executor 선택 기준: Task 격리 / 동적 리소스 / Worker 관리 비용
  • KubernetesExecutor 선택 이유
    • Task마다 독립 Pod → 환경 격리 보장
    • Broker(Redis/RabbitMQ) 불필요 → 운영 복잡도 감소
    • Idle Worker 없음 → 리소스 낭비 제거
    • Task 단위 리소스 정의 → ML 학습/서빙 작업에 적합
  • 실제 구성 요소
    • ServiceAccount + RBAC (Pod 생성 권한)
    • pod_template_file (Task Pod 표준 정의)
    • KubernetesPodOperator (커스텀 Task Pod)

1️⃣ Executor 종류 비교

Executor 비교
┌──────────────────┬──────────────────────────────────────────────────────────────────────┐
│ LocalExecutor    │ Scheduler가 Task를 직접 실행. 단일 머신. 병렬 제한.                  │
│                  │ 소규모 환경에서만 적합.                                              │
├──────────────────┼──────────────────────────────────────────────────────────────────────┤
│ CeleryExecutor   │ Broker(Redis/RabbitMQ) + 별도 Worker 프로세스.                       │
│                  │ Worker가 항상 실행 중 → 유휴 리소스 발생.                            │
│                  │ Broker 관리 비용 추가.                                               │
├──────────────────┼──────────────────────────────────────────────────────────────────────┤
│ KubernetesExecutor│ Task마다 Pod를 생성하고 완료 후 삭제.                               │
│                  │ Broker 불필요. Worker 관리 불필요.                                   │
│                  │ Task 격리, 동적 리소스 정의 가능.                                    │
└──────────────────┴──────────────────────────────────────────────────────────────────────┘

2️⃣ KubernetesExecutor를 선택한 이유

(1) Task 격리

CeleryExecutor의 Worker는 프로세스 단위로 격리된다. 같은 Worker Pod에서 다른 Task들이 동일한 Python 환경을 공유한다.

CeleryExecutor
Worker Pod → Task A, Task B, Task C (같은 환경 공유)

KubernetesExecutor는 Task마다 Pod가 분리된다.

KubernetesExecutor
Task A → Pod-A (독립 실행, 완료 후 삭제)
Task B → Pod-B (독립 실행, 완료 후 삭제)
Task C → Pod-C (독립 실행, 완료 후 삭제)

ML 파이프라인에서는 Task마다 패키지 의존성이 다를 수 있다. feature 생성 Task와 model 학습 Task는 서로 다른 라이브러리가 필요할 수 있다. KubernetesExecutor는 이 문제를 구조적으로 해결한다.

(2) Broker 불필요

CeleryExecutor는 반드시 Broker가 필요하다.

CeleryExecutor 구조
Scheduler → Redis/RabbitMQ(Broker) → Worker

KubernetesExecutor는 Broker 없이 Kubernetes API를 직접 사용한다.

KubernetesExecutor 구조
Scheduler → Kubernetes API → Task Pod

운영 컴포넌트가 줄어든다는 것은 장애 발생 지점이 줄어드는 것과 같다.

(3) Idle Worker 없음

CeleryExecutor는 Worker가 항상 실행 중이다. DAG가 실행되지 않는 시간에도 Worker Pod는 리소스를 점유한다. KubernetesExecutor는 Task가 실행될 때만 Pod가 생성된다.

(4) Task 단위 리소스 정의

from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
from kubernetes.client import models as k8s

train_task = KubernetesPodOperator(
    task_id="model_training",
    name="model-training-pod",
    namespace="airflow-dev",
    image="my-ml-image:latest",
    resources=k8s.V1ResourceRequirements(
        requests={"cpu": "2", "memory": "4Gi"},
        limits={"cpu": "4", "memory": "8Gi"},
    ),
    ...
)

3️⃣ 실제 RBAC + ServiceAccount 구성

(1) ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: airflow-dev
  namespace: airflow-dev

(2) Role (Pod 생성/삭제/조회 권한)

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: airflow-dev-role
  namespace: airflow-dev
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch", "create", "delete", "patch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "list"]

(3) RoleBinding

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: airflow-dev-rolebinding
  namespace: airflow-dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: airflow-dev-role
subjects:
  - kind: ServiceAccount
    name: airflow-dev
    namespace: airflow-dev

Scheduler와 Task Pod가 같은 Namespace라면 Role이 충분하다. 다른 Namespace에 Task Pod를 생성해야 한다면 ClusterRole과 ClusterRoleBinding이 필요하다.


4️⃣ pod_template_file: Task Pod 표준 정의

# pod_template.yaml
apiVersion: v1
kind: Pod
metadata:
  name: base-worker
  namespace: airflow-dev
spec:
  serviceAccountName: airflow-dev
  restartPolicy: Never
  containers:
    - name: base
      image: apache/airflow:2.9.0
      imagePullPolicy: IfNotPresent
      resources:
        requests:
          cpu: "500m"
          memory: "512Mi"
        limits:
          cpu: "1"
          memory: "1Gi"
      volumeMounts:
        - name: airflow-logs
          mountPath: /opt/airflow/logs
  volumes:
    - name: airflow-logs
      persistentVolumeClaim:
        claimName: airflow-logs-pvc-dev

Airflow Helm values에서 참조:

config:
  kubernetes:
    pod_template_file: /opt/airflow/pod_template.yaml
    worker_container_name: base
    namespace: airflow-dev

5️⃣ KubernetesPodOperator (KPO) 실전 예시

# KPO 예시 (핵심부)
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
from kubernetes.client import models as k8s

feature_task = KubernetesPodOperator(
    task_id="build_features",
    name="feature-builder",
    namespace="airflow-dev",
    image="my-feature-image:1.0.0",
    image_pull_policy="IfNotPresent",
    service_account_name="airflow-dev",
    resources=k8s.V1ResourceRequirements(
        requests={"cpu": "500m", "memory": "512Mi"},
        limits={"cpu": "1", "memory": "1Gi"},
    ),
    is_delete_operator_pod=True,
    get_logs=True,
)

train_task = KubernetesPodOperator(
    task_id="train_model",
    name="model-trainer",
    namespace="airflow-dev",
    image="my-train-image:1.0.0",
    resources=k8s.V1ResourceRequirements(
        requests={"cpu": "2", "memory": "4Gi"},
        limits={"cpu": "4", "memory": "8Gi"},
    ),
    is_delete_operator_pod=True,
    get_logs=True,
)

feature_task >> train_task

is_delete_operator_pod=True는 Task 완료 후 Pod를 정리한다. 운영 환경에서는 반드시 설정해서 Pod 누적을 방지해야 한다.


6️⃣ 운영 주의사항

(1) Cold Start 지연

Task 트리거 → Pod 생성 요청 → 스케줄링 → 이미지 Pull → 컨테이너 시작 → Task 실행. 이 과정은 10~30초 이상 소요될 수 있다.

완화 방법: imagePullPolicy: IfNotPresent, 경량 이미지 사용, Node에 이미지 사전 캐싱.

(2) 로그 지속성

Task Pod는 완료 후 삭제된다. Pod가 삭제되면 내부 로그도 사라진다.

  • 옵션 A: NFS PVC 마운트 → Task 로그를 공유 스토리지에 기록
  • 옵션 B: Remote Logging → S3, GCS 등 원격 저장소에 직접 기록 (권장)

이 프로젝트에서는 초기 구성으로 NFS PVC 마운트를 사용한다.

(3) RBAC 권한 범위

Task Pod 생성에 필요한 최소 권한만 부여해야 한다. 과도한 권한(ClusterAdmin 등)은 보안 위험이다.

(4) 동시 실행 Task 수 제한

config:
  core:
    parallelism: 16           # 전체 동시 Task 수
    max_active_tasks_per_dag: 8  # DAG당 동시 Task 수

7️⃣ 검증 체크리스트

# ServiceAccount 생성 확인
kubectl -n airflow-dev get serviceaccount airflow-dev

# Role/RoleBinding 확인
kubectl -n airflow-dev get role,rolebinding | grep airflow

# Task Pod 생성 테스트 (DAG 실행 후)
kubectl -n airflow-dev get pods --watch

# Task Pod 로그 확인
kubectl -n airflow-dev logs <task-pod-name> -c base

# NFS 로그 PVC Bound 확인
kubectl -n airflow-dev get pvc airflow-logs-pvc-dev

🧩 트러블슈팅

(1) Task Pod가 Pending 상태에서 멈춤

kubectl -n airflow-dev describe pod <task-pod-name>
# Events에서 원인 확인: Insufficient cpu/memory, No matching node 등

(2) Forbidden: pods is forbidden 에러

kubectl -n airflow-dev auth can-i create pods --as=system:serviceaccount:airflow-dev:airflow-dev
# yes → 권한 있음 / no → Role/RoleBinding 확인 필요

(3) Task 로그가 Airflow UI에 표시되지 않음

Remote Logging 미설정 상태에서 Pod가 삭제된 경우. NFS 마운트 확인 필요.

(4) 이미지 Pull이 느린 경우

imagePullPolicy: IfNotPresent로 변경. Private Registry의 경우 imagePullSecrets 설정.


🔧 MLOps 실전 연결

  • 환경 격리: feature 생성, 모델 학습, drift 검사 Task가 서로 다른 이미지/환경에서 실행
  • 리소스 효율: 학습 Task는 고사양, 검증 Task는 저사양으로 개별 정의
  • Broker 제거: CeleryExecutor의 Redis 브로커 운영 부담 없이 동일한 병렬 실행 가능
  • K8s 네이티브: PVC, Secret, ConfigMap을 Task Pod에 직접 마운트 가능

설계 판단 (Why This Way?)

ML 파이프라인은 Task별 환경 격리와 동적 리소스 할당이 필수이므로 CeleryExecutor 대신 KubernetesExecutor를 선택했습니다. pod_template_file을 명시적으로 정의하고, 최소 권한 원칙에 따라 ClusterRole 대신 Namespace 범위 Role을 사용합니다.


다음에 읽을 글

Airflow 7단계: DAG CI 구축 — test_dag_integrity + GitHub Actions 자동 검증