Airflow 3 클러스터로 구성하기 — Docker Compose에서 Kubernetes까지
단일 노드 Airflow 3를 Docker Compose 멀티 컴포넌트, 그리고 Kubernetes + 공식 Helm 차트 기반 프로덕션 클러스터로 단계적으로 확장하는 실전 가이드.
들어가며
이 글은 Airflow 3 실전 연재의 2편입니다. 1편 아키텍처 해부에서 Scheduler, API server, DAG processor, Triggerer, Worker, Metadata DB가 각각 어떤 일을 하는지 살펴봤다면, 이번 편은 그 컴포넌트들을 실제로 여러 프로세스·여러 노드에 흩뿌려서 클러스터로 묶는 방법을 다룹니다. 노드 한 대로 시작해서 Docker Compose로 컴포넌트를 분리하고, 마지막엔 Kubernetes 위에 공식 Helm 차트로 프로덕션 클러스터를 올리는 순서로 따라가겠습니다.
다 읽고 나면 다음 편 환경설정 & 최적화에서 다룰 동시성·풀·executor 튜닝을 적용할 "그릇"이 준비됩니다.
클러스터링의 핵심은 한 문장입니다: 상태(state)는 밖으로 빼고, 컴포넌트는 늘릴 수 있게 만든다.
왜 단일 노드를 벗어나야 하는가
처음 Airflow를 배울 때는 한 머신에서 LocalExecutor로 모든 컴포넌트를 한 프로세스 안에 띄우는 게 가장 편합니다. 하지만 운영에 올리는 순간 세 가지 벽에 부딪힙니다.
- 단일 장애점(SPOF) — 그 한 대가 죽으면 파이프라인 전체가 멈춥니다.
- 확장 한계 — 태스크가 늘어나면 한 머신의 CPU/메모리로는 감당이 안 됩니다.
- 상태의 휘발성 — Metadata DB가 같은 컨테이너 안에 있으면 재시작 한 번에 실행 이력이 날아갈 수 있습니다.
해결의 방향은 단계적입니다. 아래 흐름처럼 "상태 분리 → 컴포넌트 분리 → 노드 분산 → 다중화"의 순서로 올라갑니다.
각 단계는 앞 단계를 버리는 게 아니라 쌓아 올리는 구조입니다. Docker Compose로 배운 컴포넌트 분리 개념이 Kubernetes에서도 그대로 이어집니다.
1단계: Docker Compose로 컴포넌트 분리하기
가장 먼저 할 일은 모든 컴포넌트를 한 프로세스에 욱여넣던 구조를 컴포넌트별 컨테이너로 쪼개는 것입니다. Airflow 3는 컴포넌트가 명확히 분리되어 있어 이 작업이 자연스럽습니다.
공식
docker-compose.yaml을 그대로 쓰되, 개발/소규모 검증용임을 잊지 마세요. 진짜 프로덕션은 Kubernetes로 갑니다.
단일 호스트 토폴로지
CeleryExecutor를 쓰면 Scheduler가 작업을 브로커(Redis)에 넣고, Worker가 그걸 꺼내 실행합니다. 한 호스트 안이지만 각 컴포넌트는 독립 컨테이너로 뜹니다.
화살표에서 한 가지만 짚고 갑시다. Airflow 3에서는 Worker가 Metadata DB에 직접 붙지 않고 API server의 Task Execution API를 통해 통신합니다(점선). 이 변화 덕분에 워커를 다른 노드, 심지어 다른 네트워크로 빼는 게 훨씬 안전해집니다. 자세한 배경은 1편 아키텍처 해부를 참고하세요.
docker-compose.yaml 핵심 발췌
전체 파일은 공식 Running Airflow in Docker 문서에서 받을 수 있습니다. 여기서는 컴포넌트가 어떻게 나뉘는지가 보이도록 핵심만 추렸습니다.
x-airflow-common: &airflow-common
image: apache/airflow:3.0.0
environment: &airflow-common-env
AIRFLOW__CORE__EXECUTOR: CeleryExecutor
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
AIRFLOW__CELERY__BROKER_URL: redis://:@redis:6379/0
AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://airflow:airflow@postgres/airflow
# 원격 로그를 객체 스토리지로 (예시 — S3)
AIRFLOW__LOGGING__REMOTE_LOGGING: "true"
AIRFLOW__LOGGING__REMOTE_BASE_LOG_FOLDER: s3://my-airflow-logs/logs
AIRFLOW__LOGGING__REMOTE_LOG_CONN_ID: aws_logs
volumes:
- ./dags:/opt/airflow/dags
- ./logs:/opt/airflow/logs
- ./config:/opt/airflow/config
depends_on: &airflow-common-depends-on
redis: { condition: service_healthy }
postgres: { condition: service_healthy }
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: airflow
POSTGRES_PASSWORD: airflow
POSTGRES_DB: airflow
volumes:
- postgres-db-volume:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "airflow"]
interval: 10s
retries: 5
redis:
image: redis:7.2
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
retries: 5
airflow-api-server:
<<: *airflow-common
command: api-server
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080/api/v2/version"]
interval: 30s
airflow-scheduler:
<<: *airflow-common
command: scheduler
airflow-dag-processor:
<<: *airflow-common
command: dag-processor
airflow-triggerer:
<<: *airflow-common
command: triggerer
airflow-worker:
<<: *airflow-common
command: celery worker
volumes:
postgres-db-volume:command: 한 줄만 다르고 나머지 설정(x-airflow-common)은 공유한다는 점에 주목하세요. 하나의 이미지, 컴포넌트별 다른 명령어 — 이게 Airflow 3 컨테이너 운영의 기본 패턴입니다. docker compose up -d 한 번이면 6개 컴포넌트가 한 번에 뜹니다.
명령어 이름이 버전에 따라 바뀔 수 있습니다(예:
api-server는 3.x에서 기존webserver를 대체). 사용하는 이미지 태그의 공식 compose 파일을 기준으로 맞추세요.
2단계: Kubernetes + 공식 Helm 차트로 프로덕션 클러스터
Docker Compose는 한 호스트에 묶입니다. 진짜 프로덕션은 노드를 가로질러 분산되고, 장애가 나면 스스로 복구되며, 부하에 따라 워커가 늘었다 줄었다 해야 합니다. 이걸 가장 표준적으로 푸는 방법이 Kubernetes 위에 공식 Apache Airflow Helm 차트를 올리는 것입니다.
Kubernetes 분산 토폴로지
Compose에서 컨테이너였던 것들이 여기서는 Deployment / Pod가 되고, 상태 저장소(Postgres, Redis, 오브젝트 스토리지)는 클러스터 밖의 관리형 서비스로 빠집니다. 이게 핵심입니다 — 상태는 클러스터 밖, 컴퓨트는 클러스터 안.
이 그림이 1단계 다이어그램과 다른 점은 두 가지입니다. (1) 상태 저장소가 클러스터 밖으로 나갔고, (2) 컴포넌트가 여러 replica로 다중화됐습니다. 이 두 가지가 고가용성과 확장성의 토대입니다.
values.yaml 핵심 발췌
공식 차트는 helm repo add apache-airflow https://airflow.apache.org로 추가한 뒤 설치합니다. values.yaml에서 손대는 핵심만 보면 다음과 같습니다.
# 어떤 executor로 태스크를 돌릴지 — CeleryExecutor 또는 KubernetesExecutor
executor: "CeleryExecutor"
# 이미지 태그는 항상 명시적으로 고정
images:
airflow:
repository: apache/airflow
tag: "3.0.0"
# Scheduler 다중화 (active-active HA)
scheduler:
replicas: 2
# API server (UI + REST API) 다중화
apiServer:
replicas: 2
# DAG processor (3.x에서 독립 컴포넌트)
dagProcessor:
enabled: true
# Triggerer — deferrable operator의 async 대기 처리
triggerer:
replicas: 2
# Celery worker 기본 수 (오토스케일은 아래 KEDA로)
workers:
replicas: 2
keda:
enabled: true # 큐 깊이 기반 오토스케일
minReplicaCount: 1
maxReplicaCount: 10
# 메타데이터 DB는 차트 내장 Postgres 대신 외부 관리형 DB 사용
postgresql:
enabled: false
data:
metadataConnection:
user: airflow
pass: <secret 참조>
host: postgres-ha.db.svc.cluster.local
port: 5432
db: airflow
# Celery 브로커도 외부 사용 시 redis 내장 비활성화
redis:
enabled: false
# 원격 로그 — 객체 스토리지로 (예시)
config:
logging:
remote_logging: "True"
remote_base_log_folder: "s3://my-airflow-logs/logs"
remote_log_conn_id: "aws_logs"
# DAG는 git-sync로 가져오기 (DAG bundle 메커니즘)
dags:
gitSync:
enabled: true
repo: https://github.com/your-org/airflow-dags.git
branch: main
subPath: "dags"위 키 이름은 차트 버전에 따라 달라질 수 있습니다. 특히
apiServer·dagProcessor처럼 Airflow 3에서 새로 생긴 컴포넌트의 정확한 키는 반드시 설치하려는 차트 버전의values.yaml을 기준으로 확인하세요. 공식 Airflow Helm Chart 문서가 진실의 원천입니다.
KubernetesExecutor를 쓸까, CeleryExecutor를 쓸까
| 항목 | CeleryExecutor | KubernetesExecutor |
|---|---|---|
| 워커 형태 | 상시 떠 있는 워커 풀 | 태스크마다 Pod 생성·종료 |
| 브로커 필요 | 필요 (Redis/RabbitMQ) | 불필요 |
| 시작 지연 | 짧음 (워커 대기 중) | 있음 (Pod 스케줄링) |
| 자원 격리 | 워커 단위 | 태스크 단위 (강함) |
| 적합한 경우 | 짧고 빈번한 태스크가 많음 | 태스크별 자원 편차가 큼 |
Airflow 3는 여러 executor를 동시에 구성(hybrid) 할 수 있어, 짧은 태스크는 Celery로, 무거운 배치는 Kubernetes로 나눠 보낼 수도 있습니다. 단순하게 시작하려면 CeleryExecutor + KEDA 조합을 권합니다.
상태 저장소 분리: 클러스터의 진짜 핵심
컴포넌트를 아무리 다중화해도 상태 저장소가 단일 장애점이면 의미가 없습니다. 세 가지 상태를 클러스터 밖으로 빼고 각각 견고하게 만듭니다.
- Metadata DB (Postgres HA) — 모든 실행 이력·DAG 버전·연결 정보가 여기 있습니다. primary + replica 구성 또는 관리형 서비스(AWS RDS, Cloud SQL 등)를 쓰고, 자동 백업을 반드시 켜세요. 여기가 날아가면 Airflow의 기억이 통째로 사라집니다.
- 브로커 (Celery용 Redis/RabbitMQ) — Scheduler가 워커에게 작업을 전달하는 큐입니다. CeleryExecutor를 쓸 때만 필요하며, KubernetesExecutor에선 불필요합니다. 관리형 Redis나 클러스터형 RabbitMQ로 가용성을 확보합니다.
- 원격 로그 (S3/GCS) — Pod는 언제든 사라지므로, 워커가 만든 태스크 로그를 객체 스토리지로 즉시 올려야 로그가 보존됩니다. 위 설정 발췌의
remote_logging항목이 이 역할을 합니다.
기억하세요: Pod와 컨테이너는 가축(cattle)처럼 다루고, 상태 저장소는 반려동물(pet)처럼 다룬다. 컴퓨트는 언제든 죽고 살아나도 되지만, 상태는 그래선 안 됩니다.
고가용성: 다중화와 오토스케일
마지막으로 "한 대 죽어도 멈추지 않고, 부하에 따라 늘었다 줄었다" 하는 단계입니다.
- Scheduler 다중화 (active-active) — Airflow 3의 Scheduler는 여러 개를 동시에 돌릴 수 있습니다. 메타데이터 DB의 row-level lock으로 같은 태스크를 두 번 잡지 않도록 조율하므로,
scheduler.replicas: 2이상이면 한 인스턴스가 죽어도 다른 인스턴스가 스케줄링을 이어받습니다. 마스터 선출 같은 별도 설정이 필요 없습니다. - API server / Triggerer 다중화 — UI·REST API 트래픽과 deferrable operator의 async 대기도 replica를 늘려 분산·이중화합니다.
- Worker 오토스케일 (KEDA) — CeleryExecutor에서는 브로커 큐의 대기 작업 수를 KEDA가 관찰해 워커 Pod를 자동으로 늘리고, 한가하면 줄입니다. KubernetesExecutor라면 태스크마다 Pod가 생기므로 클러스터의 노드 오토스케일러(Cluster Autoscaler 등)에 위임합니다.
이 모든 다중화가 안전하게 동작하는 전제는 상태가 이미 밖으로 빠져 있다는 것입니다. 그래서 상태 분리를 클러스터링의 "진짜 핵심"이라고 부른 것이고요.
마무리
이번 편에서는 단일 노드에서 출발해 Docker Compose로 컴포넌트를 분리하고, Kubernetes + 공식 Helm 차트로 분산 클러스터를 올린 뒤, 상태 저장소 분리와 고가용성까지 단계적으로 쌓아 올렸습니다. 핵심 원칙은 처음 약속한 그대로입니다 — 상태는 밖으로, 컴퓨트는 늘릴 수 있게.
클러스터라는 그릇은 준비됐습니다. 다음 편 환경설정 & 최적화에서는 이 위에서 parallelism·max_active_tasks_per_dag·max_active_runs_per_dag 같은 동시성 3계층과 Pool, executor 튜닝으로 실제 성능을 끌어내는 방법을 다룹니다.
클러스터를 처음 올릴 땐 항상 작게 시작하세요. Compose로 컴포넌트 분리를 몸에 익히고, 그다음 Kubernetes로 넘어가면 같은 개념이 그대로 이어집니다.