에어플로 소개 (Introduction)

에어플로 소개 (Introduction)

원문 출처: Apache Airflow 공식 문서 – Architecture Overview

에어플로(Airflow)는 워크플로(workflow)를 만들고 실행하는 플랫폼이에요. 여기서 워크플로가 정확히 뭘 의미하는지부터 볼게요. 워크플로는 보통 DAG(Directed Acyclic Graph, 방향성 비순환 그래프)로 표현돼요. DAG 안에는 **태스크(Task)**라고 부르는 개별 작업 조각들이 들어 있고, 이 조각들이 서로 의존 관계와 데이터 흐름을 고려해서 배치돼요.

DAG는 태스크 사이의 의존 관계를 명시하는데, 이게 곧 태스크가 어떤 순서로 실행될지를 정해줘요. 태스크는 "무엇을 할지"를 나타내요. 데이터를 가져오거나, 분석을 돌리거나, 다른 시스템을 트리거하거나, 그 이상의 일을 할 수도 있어요. 지금 이 순간 내가 어떤 일을 실행하든 Airflow는 그 일이 뭔지에 대해 **무관심(agnostic)**해요. 제공자(provider) 중 하나가 만든 고수준 지원을 통해 실행해 주거나, 셸이나 파이썬 **오퍼레이터(Operator)**를 이용해 명령으로 직접 실행해 줄 뿐이에요. 어떤 걸 실행하든 기꺼이 오케스트레이션하고 돌려주는 구조죠.

에어플로 구성 요소

에어플로의 아키텍처는 여러 구성 요소로 이루어져 있어요. 아래 절들에서 각 요소가 하는 일과, 최소 설치(bar-minimum)에 꼭 필요한지 아니면 확장성·성능·스케일링을 높이기 위한 선택 요소인지를 설명할게요.

필수 구성 요소

최소한의 에어플로 설치에는 다음 요소들이 들어가요.

  • 스케줄러(scheduler): 예약된 워크플로를 트리거하고, 태스크를 실행기(executor)에 제출해서 실행하도록 하는 역할을 해요. 여기서 **실행기(executor)**는 별도의 구성 요소가 아니라 스케줄러의 설정 속성이에요. 스케줄러 프로세스 안에서 실행되죠. 기본 제공되는 실행기가 여러 개 있고, 직접 만들어 쓸 수도 있어요.
  • DAG 프로세서(Dag processor): *DAG 번들(Dag bundle)*에서 DAG 파일을 파싱해서 **메타데이터 데이터베이스(metadata database)**로 직렬화해요. DAG 파일 처리에 대한 자세한 내용은 DAG 파일 처리 문서에서 확인할 수 있어요.
  • DAG 번들(Dag bundle): DAG 프로세서가 파싱할 DAG 파일을 가져올 위치로, *워커(worker)*가 올바른 버전의 DAG 파일에 접근할 수 있게 해줘요. 기본값은 디스크의 로컬 폴더예요. DAG 번들에 대한 자세한 내용은 DAG 번들 문서를 참고하세요.
  • API 서버(API Server): REST API를 제공하고, DAG와 태스크의 동작을 검사·트리거·디버깅할 수 있는 사용자 인터페이스(UI)를 제공해요. 또 태스크가 *태스크 SDK(task SDK)*를 통해 메타데이터 데이터베이스에 직접 접근하지 않고도 상태를 에어플로에 알려줄 때 이 API 서버를 이용해요.
  • 메타데이터 데이터베이스(metadata database): 보통 PostgreSQL이나 MySQL을 쓰며, 태스크·DAG·변수(variable)의 상태를 저장해요.

메타데이터 데이터베이스 세팅 방법은 데이터베이스 백엔드 설정 문서에 설명돼 있고, 에어플로가 동작하려면 반드시 필요해요.

선택 구성 요소

에어플로를 더 잘 확장하고, 스케일링하고, 성능을 높이기 위한 선택적 요소들도 있어요.

  • 워커(worker, 선택): 스케줄러가 준 태스크를 실제로 실행해요. 기본 설치에서는 워커가 스케줄러의 일부로 들어가 별도 요소가 아닐 수 있어요. CeleryExecutor에서는 오래 실행되는 프로세스로, KubernetesExecutor에서는 POD로 실행될 수 있어요.
  • 트리거러(triggerer, 선택): 지연(deferred) 태스크를 asyncio 이벤트 루프에서 실행해요. 지연 태스크를 쓰지 않는 기본 설치에서는 트리거러가 필요 없어요. 지연 태스크에 대한 자세한 내용은 Deferrable Operators & Triggers 문서를 참고하세요. 참고로 Human-in-the-loop 태스크는 스케줄러가 관리하는 awaiting_input 상태에서 대기하며 트리거러를 사용하지 않아요.
  • 플러그인(plugins, 선택) 폴더: 플러그인은 (설치된 패키지와 비슷하게) 에어플로의 기능을 확장하는 방법이에요. 플러그인은 스케줄러, DAG 프로세서, 트리거러, API 서버가 읽어요. 플러그인에 대한 자세한 내용은 플러그인 문서를 참고하세요.

에어플로 구성 요소 배포

모든 구성 요소는 파이썬 애플리케이션이라 다양한 배포 방식으로 올릴 수 있어요. 각 요소는 자기 파이썬 환경에 추가 *설치 패키지(installed package)*를 깔 수 있어요. 예를 들어 커스텀 오퍼레이터나 센서를 설치하거나, 커스텀 플러그인으로 에어플로 기능을 확장할 때 유용해요.

에어플로는 스케줄러, DAG 프로세서, API 서버만 배포하는 단일 머신의 간단한 설치로도 동작해요. 하지만 에어플로는 스케일링과 보안까지 고려해 설계됐어요. 다양한 구성 요소를 서로 다른 머신에서, 서로 다른 보안 경계(security perimeter)로, 여러 인스턴스를 띄워 스케일링하는 분산 환경에서도 동작할 수 있죠.

구성 요소를 분리하면 보안도 높아져요. 각 요소를 서로 격리하고 역할을 나눌 수 있기 때문이에요. 예를 들어 DAG 프로세서스케줄러를 분리하면, 스케줄러DAG 번들에 접근할 수 없고 *DAG 작성자(Dag author)*가 제공한 코드를 실행할 수도 없어요.

한 사람이 에어플로를 설치하고 관리할 수도 있지만, 더 복잡한 설정에서는 시스템의 다른 부분과 상호작용하는 여러 사용자 역할이 등장해요. 이는 안전한 에어플로 배포에서 중요한 측면이에요. 역할에 대한 자세한 설명은 에어플로 보안 모델 문서에 있으며, 대체로 다음과 같아요.

  • 배포 관리자(Deployment Manager): 에어플로를 설치·설정하고 배포를 관리하는 사람
  • DAG 작성자(Dag author): DAG를 작성해서 에어플로에 제출하는 사람
  • 운영 사용자(Operations User): DAG와 태스크를 트리거하고 실행을 모니터링하는 사람

아키텍처 다이어그램

아래 다이어그램들은 에어플로를 배포하는 여러 방식을 보여줘요. 단일 머신·단일 사용자의 단순한 배포에서 시작해, 구성 요소를 분리하고 사용자 역할을 나누고, 마지막으로 보안 경계를 더 강하게 격리하는 복잡한 배포로 점진적으로 옮아가요.

다이어그램에서 연결선의 색과 모양이 뜻하는 바는 이래요.

  • 갈색 실선: DAG 번들 제출과 동기화
  • 파란색 실선: 설치 패키지플러그인의 배포 및 접근
  • 검은색 점선: 스케줄러가 (실행기를 통해) 워커를 제어하는 흐름
  • 검은색 실선: 워크플로 실행을 관리하기 위해 UI에 접근
  • 빨간색 점선: 메타데이터 데이터베이스에 접근

기본 에어플로 배포

가장 단순한 배포로, 보통 단일 머신에서 운영·관리해요. 이런 배포는 대개 LocalExecutor를 쓰는데, 이 경우 스케줄러워커가 같은 파이썬 프로세스에 있어요. DAG 프로세서도 같은 머신에서 돌며 DAG 번들에서 DAG 파일을 읽어 스케줄러가 읽을 수 있게 메타데이터 데이터베이스로 직렬화해요. API 서버스케줄러와 같은 머신에서 돌아요.

이런 설치에서는 대개 사용자 역할을 나누지 않아요. 배포, 설정, 운영, 작성, 유지보수를 모두 한 사람이 하고, 구성 요소 사이에 보안 경계도 없어요. 단일 머신에서 단순하게 실행하고 싶다면, 아래의 더 복잡한 다이어그램은 건너뛰고 Workloads 섹션으로 바로 가면 돼요.

분산 에어플로 아키텍처

에어플로 구성 요소를 여러 머신에 분산하고, 배포 관리자, DAG 작성자, 운영 사용자 같은 다양한 사용자 역할을 도입한 아키텍처예요. 이 역할들에 대해 더 알고 싶다면 에어플로 보안 모델 문서를 읽어보세요.

분산 배포에서는 구성 요소의 보안 측면을 잘 고려해야 해요. API 서버DAG 번들에 직접 접근하지 못해요. UI의 Code 탭에 있는 코드는 메타데이터 데이터베이스에서 읽어와요. API 서버DAG 작성자가 제출한 코드를 실행할 수 없고, 배포 관리자설치 패키지플러그인으로 설치한 코드만 실행할 수 있어요. 운영 사용자는 UI에만 접근할 수 있고 DAG와 태스크만 트리거할 수 있으며 DAG를 작성할 수는 없어요.

DAG 프로세서워커는 모두 DAG 번들에 접근해야 해요. 반면 스케줄러트리거러메타데이터 데이터베이스를 읽으며 DAG 번들에 직접 접근할 필요가 없어요. 분산 배포에서 워커는 태스크를 실행할 때 스케줄러가 정한 특정 DAG 번들 버전을 받아요. DAG 번들 백엔드를 구성하는 일반적인 방법은 DAG 번들 문서에 설명돼 있어요. 헬름 차트(Helm chart)는 K8S 클러스터에 에어플로를 배포하는 방법 중 하나예요.

별도 DAG 처리 아키텍처

DAG 프로세서는 모든 에어플로 3 배포에서 필수 구성 요소예요. 항상 독립 프로세스로 실행되며, 스케줄러DAG 번들에 직접 접근하지 못하고 DAG 작성자가 제공한 코드를 실행할 수 없도록 보장해요. 에어플로는 아직 완전한 멀티테넌트 기능을 지원하지 않지만, 이런 분리를 통해 DAG 작성자가 제공한 코드가 절대 스케줄러의 맥락에서 실행되지 않도록 해요.

참고: 버전 관리를 지원하지 않는 기본 로컬 디스크 DAG 번들 백엔드를 쓸 때는, DAG 프로세서워커가 최신 파일을 따라잡기 전까지 서로 다른 버전의 DAG를 보게 되는 경우가 있을 수 있어요. Git 같은 버전 관리되는 DAG 번들 백엔드는, 스케줄러가 각 태스크를 디스패치할 때 특정 번들 버전을 고정(pin)할 수 있게 해서 이 문제를 해결해요. 필요하면 DAG 번들의 동기화·스캔 주기도 구성할 수 있어요.

태스크 실행 아키텍처

위 다이어그램들은 에어플로 구성 요소가 어떻게 배포되는지를 보여줬어요. 아래 다이어그램은 반대로 태스크가 실제로 실행될 때 워커 내부에서 무슨 일이 일어나는지 보여줘요. 태스크 SDK, 슈퍼바이저(Supervisor)와 코디네이터(Coordinator) 프로세스, 언어 런타임들이 어떻게 함께 동작하는지, 어떤 프로세스가 관여하고 어떤 클래스와 프로토콜로 통신하는지를 다뤄요.

파이썬 태스크 SDK 실행

워커가 태스크를 실제로 실행할 때, 사용자 코드를 직접 돌리지 않아요. 대신 가벼운 **슈퍼바이저(Supervisor)**를 시작하는데, 이 슈퍼바이저는 고유한 네이티브 운영체제 프로세스에서 실행되고, 태스크 SDK 런타임(task_runner)이 사용자 코드를 실행하는 두 번째 네이티브 프로세스를 *포크(fork)*해요. 두 프로세스는 소켓으로 통신하고, 짧은 수명의 태스크 JWT를 쥐고 있거나 *실행 API(Execution API)*와 대화하는 쪽은 슈퍼바이저뿐이에요. 사용자 코드는 토큰을 절대 보지 못하고 데이터베이스에도 닿지 않아요.

같은 런타임은 dag.test()와 로컬 실행을 위해 *인프로세스(in-process)*로도 돌 수 있어요. 이는 하나의 파이썬 프로세스로, 포크도 소켓도 HTTP도 없어요. 아래 다이어그램은 두 경로를 대조하면서 각 파이썬 프로세스가 어디에 있는지 표시해요.

슈퍼바이저 실행의 메시지 흐름을 순서 다이어그램으로 보면 — 시작, 사용자 코드 실행, 프록시된 Connection/Variable/XCom 조회, 하트비트, 최종 상태 보고 — 각 프로세스가 각자 자신의 생명선(lifeline)에 놓여 있어요. 슈퍼바이저가 가운데에 앉아서, Task ↔ Supervisor 요청/응답 왕복(태스크가 Connection/Variable/XCom을 요청하고 답을 받는 것)이 이웃한 생명선 사이를 오가는 화살표로 읽혀요. 각 화살표는 번호가 매겨져 있고, 보낸 쪽 색으로 구분되며, 사용된 메시지 클래스나 프로토콜로 라벨링돼 있어요.

비파이썬 언어 SDK (Go와 Java)

AIP-72에서 도입된 태스크 실행 인터페이스(TEI, Task Execution Interface)는 언어에 구애받지 않아요. 그래서 태스크를 컴파일되는 비파이썬 언어로도 작성할 수 있어요. 파이썬 DAG는 여전히 @task.stub(queue=...)으로 태스크를 선언해요(그래서 파이썬 태스크와 비파이썬 태스크를 한 DAG에 섞을 수 있어요). 하지만 실제 작업은 그에 맞는 런타임에 위임돼요. 현재 두 가지 다른 통합 스타일이 있는데, Go SDK는 독립 워커를 실행하고 Java SDK는 기존 파이썬 슈퍼바이저에 끼워들어가요.

Go SDK — 독립 엣지 워커. Go 태스크 SDK파이썬 슈퍼바이저도, msgpack stdin 소켓도 없어요. 오래 실행되는 컴파일된 엣지 워커(airflow-go-edge-worker)가 **엣지 실행기 API(Edge Executor API)**에서 작업을 가져와서(pull), 사용자의 컴파일된 DAG 번들을 go-plugin (gRPC) 서브프로세스로 띄우고, gRPC로 태스크를 호출해요. 태스크는 네이티브 TEI 클라이언트실행 API에 HTTPS로 직접 닿아요. 그래서 파이썬 태스크와 달리 태스크 JWT를 직접 쥐고 있어요.

Java (JVM) SDK — 슈퍼바이저에 끼워지는 코디네이터. Java 태스크 SDK는 반대 방향으로 가요. 새 코디네이터(Coordinator) 계층을 통해 기존 파이썬 슈퍼바이저를 재사용해요. CoordinatorManager는 태스크의 queueBaseCoordinator로 해석해서, java 큐면 JavaCoordinator를, 그 외에는 내장 _PythonCoordinator를 사용해요. JavaCoordinator는 루프백 TCP 서버 두 개를 열고 subprocess.Popen으로 JVM 번들 프로세스를 띄우며, 공유 ActivitySubprocess의 하위 클래스인 _JavaActivitySubprocess로 이를 구동해요. JVM은 TCP로 돌아와서 연결하고 파이썬 태스크와 같은 msgpack 프로토콜로 이야기해요. 그래서 파이썬 쪽이 하트비트를 보내고 상태를 관리하며 실행 API 호출을 모두 프록시해요. 그 결과 JVM 태스크는 파이썬 태스크처럼 태스크 JWT를 절대 직접 쥐지 않아요.

Java 태스크의 종단 간 워크플로 — @task.stub부터 코디네이터, JVM 서브프로세스, 프록시된 Connection/Variable/XCom 조회, 최종 상태 보고까지 — 는 아래 순서 다이어그램으로 보여줘요. 위와 마찬가지로 슈퍼바이저가 중앙 생명선이라, 루프백 TCP 위의 JVM ↔ Supervisor 왕복이 이웃 생명선 사이를 오가는 화살표로 그려져요.

워크로드

DAG는 일련의 태스크를 거쳐 실행돼요. 흔히 마주치는 태스크 유형은 세 가지예요.

  • 오퍼레이터(Operators): 미리 정의된 태스크로, 빠르게 이어 붙여서 DAG의 대부분을 만들 수 있어요.
  • 센서(Sensors): 오퍼레이터의 특수 하위 클래스로, 외부 이벤트가 발생하기를 기다리는 일에만 집중해요.
  • TaskFlow 데코레이터가 붙은 @task: 사용자 정의 파이썬 함수를 태스크로 감싼 것이에요.

실제로 이들은 모두 에어플로의 BaseOperator 하위 클래스예요. 그래서 태스크(Task)와 오퍼레이터(Operator) 개념이 어느 정도 호환되기도 하는데, 별개의 개념으로 생각하는 게 이해하기 쉬워요. 본질적으로 오퍼레이터와 센서는 템플릿이고, DAG 파일에서 하나를 호출하면 그때 태스크가 만들어져요.

제어 흐름 (Control Flow)

DAG는 여러 번 실행되도록 설계됐고, 여러 번 실행이 병렬로 일어날 수 있어요. DAG는 매개변수화되는데, 항상 "실행되는 구간"인 데이터 간격(data interval)을 포함하고, 그 외에도 선택적인 매개변수를 가질 수 있어요.

태스크는 서로 의존 관계를 선언해요. DAG에서 이건 >><< 연산자로 볼 수 있어요.

first_task >> [second_task, third_task]
fourth_task << third_task

또는 set_upstreamset_downstream 메서드로도 선언할 수 있어요.

first_task.set_downstream([second_task, third_task])
fourth_task.set_upstream(third_task)

이런 의존 관계가 그래프의 "간선(edge)"을 이루고, 에어플로가 태스크를 어떤 순서로 실행할지 계산하는 근거가 돼요. 기본적으로 태스크는 자신의 모든 업스트림 태스크가 성공할 때까지 기다렸다가 실행돼요. 이 기본 동작은 브랜칭(Branching), LatestOnly, 트리거 규칙(Trigger Rules) 같은 기능으로 커스터마이즈할 수 있어요.

태스크 사이에서 데이터를 주고받는 방법은 세 가지가 있어요.

  • XComs("Cross-communications"): 태스크가 작은 메타데이터 조각을 push·pull할 수 있는 시스템이에요.
  • 큰 파일을 스토리지 서비스(직접 운영하거나 퍼블릭 클라우드의 일부)로 업로드·다운로드하는 방법
  • TaskFlow API가 암시적 XComs로 태스크 사이의 데이터를 자동으로 전달하는 방법

에어플로는 공간이 생기면 워커에 태스크를 보내서 실행해요. 그래서 DAG 안의 모든 태스크가 반드시 같은 워커나 같은 머신에서 실행되리라는 보장은 없어요.

DAG를 만들다 보면 아주 복잡해지기 쉬워요. 에어플로는 이를 좀 더 지속 가능하게 만드는 여러 장치를 제공해요. 예를 들어 TaskGroups는 UI에서 태스크를 시각적으로 묶어 보여줘요.

데이터스토어 같은 중앙 리소스에 쉽게 접근을 미리 설정해 두는 기능도 있어요. Connections & Hooks가 그것이고, 풀(Pools)로 동시성을 제한할 수도 있어요.

사용자 인터페이스 (User Interface)

에어플로에는 사용자 인터페이스가 함께 와요. DAG와 태스크가 뭘 하고 있는지 보고, DAG 실행을 트리거하고, 로그를 보고, DAG의 문제를 제한적으로 디버깅하고 해결할 수 있어요.

일반적으로 UI는 에어플로 설치 전체의 상태를 보는 가장 좋은 방법이에요. 개별 DAG로 들어가 레이아웃과 각 태스크의 상태, 각 태스크의 로그까지 확인할 수 있죠.