Skip to content

Split View: Airflow 2 EOL 이후 — 2에서 3으로 가는 실제 작업 목록, 그리고 3.3까지 온 3.x의 현재

|

Airflow 2 EOL 이후 — 2에서 3으로 가는 실제 작업 목록, 그리고 3.3까지 온 3.x의 현재

들어가며 — 조용히 지나간 EOL

2026년 4월 22일, Apache Airflow 2가 EOL(수명 종료)을 지났습니다. 프로젝트 README의 버전 수명주기 표가 밝히는 공식 사실은 이렇습니다 — Airflow 2는 2025년 10월 22일부터 제한 유지보수(보안·치명 버그 픽스만)로 들어갔고, 2026년 4월 22일부로 상태가 EOL입니다. README의 문장을 그대로 옮기면, EOL 버전은 어떤 픽스도 지원도 받지 않습니다. endoflife.date의 데이터도 같은 날짜를 보여 줍니다.

이 글을 쓰는 오늘(2026-07-17) 기준으로 석 달이 지났습니다. 날짜를 한 줄로 세워 보면 이렇습니다(모두 GitHub 릴리스PyPI 릴리스 이력에서 확인한 날짜입니다).

2020-12-17  Airflow 2.0.0 첫 릴리스
2025-04-22  Airflow 3.0.0            (SLA, SubDAG, pickling, REST v1 제거)
2025-05-20  Airflow 2.11.0           (브리지 릴리스 — 이행 도구 백포트)
2025-09-25  Airflow 3.1.0            (Deadline Alerts가 experimental로 도착)
2025-10-22  Airflow 2.x 제한 유지보수 진입 (보안, 치명 버그 픽스만)
2026-03-14  Airflow 2.11.2           (지금까지 마지막 2.x 패치)
2026-04-07  Airflow 3.2.0            (asset partitioning, multi-team)
2026-04-22  Airflow 2.x EOL          (이후 어떤 픽스도 없음)
2026-07-06  Airflow 3.3.0            (state store, 플러그블 재시도, Java/Go SDK)

5년 4개월을 산 메이저 버전치고는 조용한 퇴장이었습니다. 그리고 지금 2.x에 남아 있는 팀에게 이 날짜들이 뜻하는 바는 단순합니다 — 다음 CVE가 나와도 2.x용 패치는 원칙적으로 없습니다. 문제는 2에서 3으로 가는 길이 짧지 않다는 것입니다. 이 글은 그 길에서 실제로 손이 가는 작업을, 과장 없이 공식 업그레이드 가이드릴리스 노트 원문 기준으로 정리합니다.

업그레이드가 아니라 아키텍처 전환이다

2와 3의 차이를 한 문장으로 줄이면 이렇습니다 — 2에서는 모든 컴포넌트가 메타데이터 DB에 직접 붙었고, 3에서는 태스크와 워커에 대해 API 서버가 유일한 접점입니다.

업그레이드 가이드가 스스로 나열하는 2.x의 문제는 정직합니다. 태스크 코드와 태스크 실행 코드가 같은 프로세스에서 돌았고, 워커가 DB에 직접 붙어 모든 사용자 코드를 실행했으며, 사용자 코드가 DB 세션을 임포트해 메타데이터 DB에 악의적인 조작을 할 수 있었고, DB 커넥션 수가 과도해 스케일링이 어려웠다는 것입니다. Airflow 3는 이걸 Task Execution API로 끊습니다 — 상태 전이, 하트비트, XCom, 리소스 조회가 전부 전용 API를 거치고, 태스크 코드는 DB 세션을 만질 수 없습니다.

이 전환이 마이그레이션 공수의 뿌리입니다. DAG 파일의 임포트 경로 수정은 도구가 절반쯤 해 주지만, "워커에서 메타데이터 DB를 직접 읽던 코드"는 설계를 바꿔야 합니다. 뒤에서 따로 다룹니다.

한 가지 뉘앙스도 문서 그대로 옮겨 둡니다 — DAG 작성자 코드는 Dag 파일 프로세서와 트리거러 안에서는 여전히 DB 직접 접근 상태로 실행될 수 있습니다. 격리는 태스크 실행 경로에 대한 것이지, 아직 모든 경로에 대한 것이 아닙니다.

DAG 코드 — ruff가 절반, 시맨틱 변화가 나머지 절반

기계가 잡아 주는 부분

공식 가이드가 안내하는 첫 도구는 Ruff의 AIR 규칙입니다. AIR301과 AIR302가 Airflow 3에서 깨지는 코드를, AIR311과 AIR312가 당장은 돌아가지만 갈아타야 할 코드를 가리킵니다. 문서는 ruff 0.13.1 이상을 요구합니다.

ruff check dags/ --select AIR301 --show-fixes    # 무엇이 어떻게 고쳐질지 미리보기
ruff check dags/ --select AIR301 --fix           # 안전한 자동 수정
ruff check dags/ --select AIR301 --fix --unsafe-fixes  # 임포트 경로 교체까지

여기서 unsafe라는 말에 겁먹을 필요는 없습니다 — AIR 규칙에서 unsafe 픽스란 대개 "임포트 멤버 이름은 그대로 두고 경로만 바꾸는" 수정을 뜻한다고 문서가 설명합니다.

임포트 이동의 방향은 하나입니다. DAG 작성에 쓰는 모든 것이 airflow.sdk 네임스페이스로 갑니다.

airflow.models.dag.DAG                    ->  airflow.sdk.DAG
airflow.models.baseoperator.BaseOperator  ->  airflow.sdk.BaseOperator
airflow.decorators.task                   ->  airflow.sdk.task
airflow.datasets.Dataset                  ->  airflow.sdk.Asset      (이름까지 바뀜)
airflow.models.variable.Variable          ->  airflow.sdk.Variable
airflow.models.connection.Connection      ->  airflow.sdk.Connection
airflow.hooks.base.BaseHook               ->  airflow.sdk.BaseHook

공식 타임라인은 명시적입니다 — 3.1에서는 레거시 임포트가 경고를 내며 동작하고, 미래 버전에서 제거됩니다. 지금 고치는 게 맞습니다.

또 하나, BashOperator, PythonOperator, ExternalTaskSensor, FileSensor처럼 코어에 번들되어 있던 흔한 오퍼레이터들이 apache-airflow-providers-standard라는 별도 패키지로 분리됐습니다. 이 패키지는 2.x에도 설치할 수 있어서, 업그레이드 전에 미리 임포트를 바꿔 두는 선행 작업이 가능합니다.

기계가 못 잡아 주는 부분 — 시맨틱이 바뀐다

임포트보다 위험한 건 돌아가는데 다르게 돌아가는 변화들입니다. 업그레이드 가이드의 Breaking Changes 절에서 실무에 크게 걸리는 것만 추립니다.

execution_date와 그 파생 키들이 컨텍스트에서 사라졌습니다. execution_date, prev_execution_date, next_execution_date, prev_ds, next_ds, tomorrow_ds, yesterday_ds 계열이 전부 제거 목록에 있습니다. 템플릿과 태스크 코드에서 이 키를 참조하면 DAG 에러가 납니다. 대체는 logical_date와 data interval 계열입니다.

크론 문자열의 기본 시맨틱이 바뀌었습니다. create_cron_data_intervals 설정의 기본값이 False가 되면서, schedule="0 0 * * *"처럼 크론 문자열을 그대로 넘긴 DAG는 이제 CronDataIntervalTimetable이 아니라 CronTriggerTimetable로 해석됩니다. 명시적으로 timetable 인스턴스를 넘긴 DAG는 영향이 없습니다. 문제는 data_interval_startds, ts 같은 템플릿 값이 logical_date에서 파생되는데, 두 timetable 사이에서 이 값들이 밀린다는 점입니다. 이 값들에 의존한다면 업그레이드 전에 create_cron_data_intervals=True를 명시하라는 게 문서의 지시이고, 3.x 런이 이미 생긴 뒤에 뒤늦게 플래그를 되돌리면 이전 런의 logical_date와 충돌을 피하려고 스케줄 런 하나를 건너뜁니다.

수동 트리거 런의 data interval을 가정하면 안 됩니다. 3에서는 수동으로 트리거한 런의 data_interval이 사용자가 넘긴 logical_date에서 파생된다고 가정하지 말라고 문서가 명시합니다. 사용자가 지정한 날짜가 필요하면 logical_date를 직접 읽어야 하고, 이는 TriggerDagRunOperator로 하위 DAG를 트리거하는 워크플로에 특히 걸립니다.

xcom_pull()의 기본 동작이 바뀌었습니다. 2에서는 task_ids 없이 부르면 DAG 런 전체에서 해당 키의 가장 최근 값을 찾아 줬지만, 3에서는 현재 태스크에서만 찾습니다. 다른 태스크의 XCom이 필요하면 이제 task_ids를 반드시 넘겨야 합니다. 이런 코드는 조용히 None을 받기 시작하므로, grep으로 찾아 두는 편이 안전합니다.

catchup_by_default가 False가 됐습니다. 스케줄 공백을 따라잡는 catchup에 의존했다면 DAG에 명시해야 합니다.

그리고 기능 제거 목록 — SubDAG(TaskGroup과 asset 스케줄링으로 대체), SequentialExecutor, CeleryKubernetesExecutor와 LocalKubernetesExecutor(다중 executor 구성으로 대체), DAG·XCom pickling, CLI의 --subdir 인자(Dag bundle로 대체), 그리고 REST API v1이 전부 사라졌습니다. API 클라이언트는 FastAPI 기반 v2로 옮겨야 합니다.

가장 아픈 부분 — 메타데이터 DB를 직접 읽던 커스텀 코드

커스텀 오퍼레이터나 태스크 코드가 Airflow DB 세션으로 메타데이터를 읽고 쓰던 팀이라면, 여기가 마이그레이션 공수의 최대 항목입니다. 3에서는 이 코드가 동작하지 않고, 공식 가이드는 두 가지 경로를 제시합니다.

권장 경로는 Airflow Python Client로 REST API를 쓰는 것입니다. DagRun, TaskInstance, Variable, Connection, XCom 등 대부분의 사용 사례가 API로 커버됩니다. 다만 문서가 단점도 그대로 적어 놓았습니다 — 토큰을 /auth/token 호출로 얻고 로테이션해야 하며, API 서버 가용성과 네트워크 경로에 의존하게 되고, 모든 DB 작업이 API 엔드포인트로 노출되어 있는 것은 아닙니다. 빠진 기능은 직접 DB로 돌아가지 말고 API 추가를 요청하라는 게 커뮤니티의 방침입니다.

우회로는 PostgresHook 같은 DbApiHook으로 메타데이터 DB에 일반 DB 커넥션을 만드는 것인데, 문서 스스로 권장하지 않는다고 못박습니다. 이유도 명시적입니다 — 이 방식은 Airflow 3.2 이후 버전에서 깨질 것이고, 메타데이터 DB 스키마는 공개 API가 아니어서 예고 없이 바뀌며, 태스크 격리라는 3의 핵심 설계와 충돌하고, 태스크마다 DB 커넥션을 여는 2 시절의 성능 특성으로 되돌아갑니다. 임시로 쓰더라도 읽기 전용 계정으로, 갈아탈 계획과 함께 써야 합니다. 실제 전환 예시는 issue #49187에 모여 있습니다.

공수 산정 관점에서 조언하자면, 마이그레이션 견적을 내기 전에 두 숫자부터 세는 게 좋습니다 — ruff AIR301 위반 개수, 그리고 DB 세션을 직접 만지는 커스텀 오퍼레이터 개수. 앞의 것은 대부분 기계적으로 풀리고, 뒤의 것은 하나하나가 설계 변경입니다.

배포자 체크리스트 — 순서가 있는 작업이다

운영 쪽 작업은 가이드의 단계를 따라가면 되지만, 몇 가지는 미리 알고 있어야 일정이 안 틀어집니다.

airflow config update           # 무엇이 바뀌어야 하는지 점검 (2.11에도 백포트됨)
airflow config update --fix     # 자동 수정
airflow db migrate              # 스키마 마이그레이션 — 가장 오래 걸리는 단계
airflow api-server              # webserver 명령은 없다
airflow dag-processor           # 이제 별도 기동이 필수 (로컬 개발도 포함)
  • 선행 조건. 최소 Airflow 2.7, 권장은 최신 2.x를 거쳐 3으로 가는 경로입니다. airflow dags reserialize가 에러 없이 돌아야 하고, DAG 처리 에러는 업그레이드 전에 구버전에서 해소해 둬야 합니다.
  • DB 청소와 백업. 스키마 변경 시간은 DB 크기에 비례합니다. 문서는 airflow db clean으로 오래된 XCom 등을 정리하고 시작하라고 권하며, 백업 없이 진행하다 마이그레이션이 중간에 끊기면 반쯤 마이그레이션된 상태에 놓일 수 있다고 경고합니다.
  • 컴포넌트 구성이 바뀝니다. 웹서버는 범용 API 서버가 됐고, Dag 프로세서는 항상 독립 프로세스로 띄워야 합니다. Helm 차트를 쓴다면 webserver 아래 값들을 전부 apiServer로 옮겨야 하고, 차트 1.16.0에서 1.18.0 사이에 키 이름이 여럿 바뀌었습니다.
  • 인증이 바뀝니다. 기본 auth manager가 Simple Auth로 바뀌었고, FAB 기반 인증을 유지하려면 FAB provider를 설치하고 auth_manager를 명시해야 합니다. OAuth 리다이렉트 URL에는 /auth 접두사가 붙습니다 — IdP 쪽에 등록된 redirect URL도 같이 고쳐야 한다는 뜻입니다.
  • 플러그인. Flask-AppBuilder 뷰와 블루프린트에 의존하던 플러그인은 FastAPI 앱으로 옮기거나 FAB provider의 호환 레이어에 얹어야 합니다.

SLA의 다섯 달 공백 — 제거가 대체재보다 먼저 온다

이번 메이저 전환에서 제일 교훈적인 사건은 SLA입니다. 3.0.0(2025-04-22)은 SLA 콜백과 메트릭을 제거하면서 릴리스 노트에 "더 유연한 대체 메커니즘인 DeadlineAlerts가 미래 버전에 계획되어 있다"고 적었습니다. 그 Deadline Alerts(AIP-86)는 3.1.0(2025-09-25)에서야 도착했고, 그마저 experimental 표기에 비동기 콜백만 지원했습니다. 동기 콜백(SyncCallback)은 3.2.0(2026-04-07)에 추가됐는데, 3.2.0 릴리스 노트 기준으로도 여전히 experimental 표기가 붙어 있습니다. 3.3.0은 Browse 메뉴에 Deadlines 페이지를 넣었습니다.

정리하면 — 3.0으로 일찍 넘어간 SLA 사용자에게는 본가 기능 기준으로 다섯 달의 공백이 있었고, 대체재는 도착한 지 열 달이 지난 지금도 experimental 딱지를 달고 성숙 중입니다. 3.0 릴리스 노트가 제시한 임시 방편은 태스크 수준 성공·실패 훅이나 외부 모니터링이었습니다. 특정 기능에 크게 의존하는 팀이라면, 메이저 업그레이드 계획에서 "그 기능의 대체재가 어느 버전에서 어떤 상태인지"를 릴리스 노트로 직접 확인하는 습관이 필요하다는 이야기이기도 합니다.

2.11은 다리로 설계됐다 — 아직 2.x라면 여기부터

2.11.0(2025-05-20)은 새 기능 릴리스라기보다 이행 장치입니다. 릴리스 노트의 "Ease migration to Airflow 3" 절이 밝히는 내용은 이렇습니다.

  • airflow config lintairflow config update가 2.11에 백포트되어, 3으로 가기 전에 설정을 미리 점검·수정할 수 있습니다.
  • execution_date를 쓰던 모든 모델에 logical_date 필드가 병행 추가됐습니다. 3은 execution_date를 완전히 버립니다.
  • create_delta_data_intervals 플래그(2.11 기본 True, 3.0 기본 False)를 미리 뒤집으면 timedelta 스케줄의 새 해석(DeltaTriggerTimetable)을 2.x에서 미리 겪어 볼 수 있습니다. 크론 쪽의 create_cron_data_intervals와 같은 취지의 리허설 장치입니다.
  • 타이밍 메트릭 단위를 통일하는 timer_unit_consistency도 같은 패턴입니다 — 2.11에서 켜 보고, 3.0에서는 항상 켜진 상태가 됩니다.

즉 공식 경로는 명확합니다. 최신 2.11.x로 올리고, 플래그를 3.0 기본값 방향으로 미리 뒤집어 보고, ruff와 config lint로 위반을 소진한 다음 3으로 넘어가는 것입니다. 2.11이 Python 3.9에서 3.12까지를 지원하므로 파이썬 버전 정렬도 이 단계에서 끝내 둘 수 있습니다.

3.x의 현재 — 2026년 7월, 3.3.0

넘어간 다음의 세계도 정직하게 봐야 합니다. 두 가지가 중요합니다.

첫째, 3.x 안에서도 최신 마이너 추적이 사실상 요구됩니다. 릴리스 이력을 보면 3.0.x의 마지막 패치는 3.0.6(2025-08-29)으로 3.1.0 직전이 끝이고, 3.1.x는 3.1.8(2026-03-11)로 3.2.0 직전, 3.2.x는 3.2.2(2026-05-29)로 3.3.0 직전이 마지막입니다. 이전 마이너를 위한 패치는 다음 마이너가 나오면 사실상 멈추는 패턴이고, README도 사용 중인 메이저의 최신 마이너를 쓰라고 권고합니다. LTS 같은 것은 없습니다. 대략 반년 주기의 마이너 업그레이드를 운영 캘린더에 넣어야 한다는 뜻입니다.

둘째, 코드 이동은 3.x 안에서도 계속되고 있습니다. 3.2.0은 태스크가 쓰는 예외들을 airflow.sdk.exceptions로 옮겼고(기존 airflow.exceptions 임포트는 경고를 내는 프록시로 남음), 직렬화(serde) 로직도 Task SDK로 옮겼습니다 — 두 호환 레이어 모두 Airflow 4에서 제거 예정이라고 못박습니다. 2에서 3으로 넘어왔다고 이사가 끝나는 게 아니라, airflow.sdk로의 이주가 여전히 진행형입니다. 마이그레이션 예산에 이 후속 작업도 넣어 두는 편이 현실적입니다.

기능 쪽 흐름은 짧게만 짚습니다. 3.1(2025-09-25)은 Deadline Alerts, Human-in-the-Loop, UI 다국어화를 들고 왔고, 3.2(2026-04-07)의 헤드라인은 asset partitioning — 자산 전체가 아니라 특정 파티션 변경만으로 하위 DAG를 트리거하는, 날짜 파티션된 데이터 레이크와 궁합이 좋은 기능 — 과 multi-team 배포(하나의 Airflow 안에서 팀별 DAG·커넥션·풀 격리, experimental 표기)였습니다. 3.3.0(2026-07-06)은 그 파티셔닝을 RollupMapper, FanOutMapper 같은 매퍼와 wait_policy로 확장했고, 재시도 정책을 플러그블하게 만들었으며(AIP-105), 태스크·자산 상태 저장소(AIP-103)를 넣었고, Python이 아닌 언어로 태스크를 쓰는 Coordinator 레이어(AIP-108)를 experimental로 실었습니다 — Java SDK는 2026-07-13에 1.0.0-beta1이 태그됐습니다. 새 기능 상당수에 experimental 표기가 붙어 있다는 점은 그대로 읽는 게 좋습니다. 이 기능들 때문에 서두를 이유는 약하고, 서둘러야 할 이유는 어디까지나 2.x의 EOL입니다.

그래서 지금 무엇을 해야 하나

상황별로 정리하면 이렇습니다.

아직 2.x 운영 중이라면. 보안 패치가 없는 소프트웨어를 스케줄러 권한으로 돌리고 있는 상태입니다. 첫 걸음은 3이 아니라 2.11.2입니다 — 거기서 airflow config lint, ruff AIR301/AIR302, 그리고 직접 DB 접근 코드 인벤토리라는 세 가지 측정을 먼저 하십시오. 측정 없이는 이 마이그레이션의 견적 자체가 안 나옵니다. 표준 오퍼레이터의 provider 패키지 전환처럼 2.x에서 미리 해 둘 수 있는 작업도 그 시점에 같이 하면 됩니다.

SLA, SubDAG, 직접 DB 접근에 깊이 의존한다면. 기능별 대체재의 상태를 릴리스 노트에서 직접 확인하고 이행 설계를 먼저 하십시오. SLA는 Deadline Alerts로 가는 재설계가 필요하고(아직 experimental 표기라는 점 포함), SubDAG는 TaskGroup·asset 스케줄링으로, DB 접근은 Python Client 기반으로 다시 짜야 합니다. 이 세 가지가 없다면 마이그레이션은 생각보다 기계적입니다.

이미 3.x라면. 최신 마이너를 따라가는 반년 주기 캘린더를 만들고, airflow.exceptions와 구 serde 경로에서 나오는 DeprecatedImportWarning을 지금 소진해 두십시오 — Airflow 4에서 제거 예정이라고 이미 공지된 항목들입니다.

마지막으로 레이어 구분 하나. Airflow는 배치 데이터 파이프라인의 제어면이고, 애플리케이션 코드의 내구 실행(durable execution)은 다른 레이어의 문제입니다 — 그쪽 이야기는 Temporal Worker Versioning GA 편에서 다뤘습니다. 오케스트레이터가 지휘하는 실행 엔진 쪽의 최근 변화는 PySpark 4.2의 Arrow UDF 기본화 편을, 오케스트레이션 도구 지형 전체의 비교는 데이터 오케스트레이션 가이드워크플로 엔진 2026 편을 참고하십시오.

마치며

정리하면 이렇습니다. Airflow 2는 2026년 4월 22일부로 끝났고, 마지막 패치는 2.11.2(2026-03-14)였습니다. 2에서 3으로 가는 길은 임포트 치환이 아니라 아키텍처 전환입니다 — 워커의 DB 직접 접근 제거가 핵심이고, 그 위에 execution_date 계열 제거, 크론 시맨틱 변화, xcom_pull 동작 변화 같은 시맨틱 지뢰가 얹혀 있습니다. 공식 도구(ruff AIR 규칙, config lint/update, 2.11 브리지)는 기계적인 부분을 꽤 줄여 주지만, 직접 DB 접근 코드와 SLA 의존은 설계를 다시 해야 풀립니다.

그리고 3.x는 도착지가 아니라 움직이는 기차입니다 — 마이너는 반년 주기로 나오고 이전 마이너 패치는 멈추며, airflow.sdk로의 이주는 4.0 제거 예고와 함께 계속되고 있습니다. 그러니 계획은 두 개가 필요합니다. 2에서 3으로 건너가는 일회성 프로젝트, 그리고 건너간 뒤 최신 마이너를 따라가는 운영 리듬. 전자를 미룰 수 있는 시간은, 이번 4월로 끝났습니다.

참고 자료

After Airflow 2 EOL — The Real Work List for Going 2 to 3, and Where 3.x Stands at 3.3

Introduction — The EOL That Passed Quietly

On April 22, 2026, Apache Airflow 2 passed EOL (end of life). The official fact, as stated in the project README's version life-cycle table, is this: Airflow 2 entered limited maintenance (security and critical bug fixes only) on October 22, 2025, and its status became EOL as of April 22, 2026. In the README's own words, an EOL version receives no fixes and no support of any kind. Data from endoflife.date shows the same date.

As of today, when this post is written (2026-07-17), three months have passed. Laid out on a single timeline (all dates verified against GitHub Releases and the PyPI release history):

2020-12-17  Airflow 2.0.0 first release
2025-04-22  Airflow 3.0.0            (SLA, SubDAG, pickling, REST v1 removed)
2025-05-20  Airflow 2.11.0           (bridge release — transition tooling backported)
2025-09-25  Airflow 3.1.0            (Deadline Alerts arrives as experimental)
2025-10-22  Airflow 2.x enters limited maintenance (security, critical fixes only)
2026-03-14  Airflow 2.11.2           (last 2.x patch to date)
2026-04-07  Airflow 3.2.0            (asset partitioning, multi-team)
2026-04-22  Airflow 2.x EOL          (no further fixes after this point)
2026-07-06  Airflow 3.3.0            (state store, pluggable retries, Java/Go SDK)

For a major version that lived 5 years and 4 months, it was a quiet exit. And for teams still on 2.x right now, what these dates mean is simple: when the next CVE lands, there will, in principle, be no patch for 2.x. The problem is that the road from 2 to 3 is not short. This post lays out the work that actually takes hands-on time on that road, without exaggeration, based on the original text of the official upgrade guide and the release notes.

This Is Not an Upgrade — It's an Architecture Change

The difference between 2 and 3 boils down to one sentence: in 2, every component attached directly to the metadata DB; in 3, the API server is the sole point of contact for tasks and workers.

The problems the upgrade guide itself lists for 2.x are candid. Task code and task-execution code ran in the same process, workers attached directly to the DB to execute all user code, user code could import a DB session and make malicious modifications to the metadata DB, and an excessive number of DB connections made scaling hard. Airflow 3 cuts this off with the Task Execution API — state transitions, heartbeats, XCom, and resource lookups all now go through a dedicated API, and task code can no longer touch a DB session.

This shift is the root of the migration effort. Tooling handles about half of the import-path fixes in DAG files automatically, but "code in a worker that used to read the metadata DB directly" requires a design change. We cover this separately below.

One nuance carried straight from the docs: DAG-author code can still run with direct DB access inside the Dag file processor and the triggerer. The isolation applies to the task-execution path, not yet to every path.

DAG Code — Ruff Handles Half, Semantic Changes the Other Half

What the machine catches

The first tool the official guide points to is Ruff's AIR rules. AIR301 and AIR302 flag code that breaks on Airflow 3; AIR311 and AIR312 flag code that still runs for now but should be migrated. The docs require ruff 0.13.1 or later.

ruff check dags/ --select AIR301 --show-fixes    # preview what would change and how
ruff check dags/ --select AIR301 --fix           # safe auto-fix
ruff check dags/ --select AIR301 --fix --unsafe-fixes  # including import-path replacement

No need to be scared off by the word "unsafe" here — the docs explain that in the AIR rules, an unsafe fix usually just means "keep the imported member name as-is and only change the path."

The import moves all point in one direction. Everything used to author a DAG moves into the airflow.sdk namespace.

airflow.models.dag.DAG                    ->  airflow.sdk.DAG
airflow.models.baseoperator.BaseOperator  ->  airflow.sdk.BaseOperator
airflow.decorators.task                   ->  airflow.sdk.task
airflow.datasets.Dataset                  ->  airflow.sdk.Asset      (even the name changes)
airflow.models.variable.Variable          ->  airflow.sdk.Variable
airflow.models.connection.Connection      ->  airflow.sdk.Connection
airflow.hooks.base.BaseHook               ->  airflow.sdk.BaseHook

The official timeline is explicit: in 3.1, legacy imports still work but emit a warning, and they will be removed in a future version. Fixing this now is the right call.

One more thing — common operators that used to be bundled into core, like BashOperator, PythonOperator, ExternalTaskSensor, and FileSensor, have been split out into a separate package called apache-airflow-providers-standard. This package can also be installed on 2.x, so you can do the import switch ahead of time, before the upgrade itself.

What the machine can't catch — semantics change

More dangerous than the imports are the changes where code still runs, just differently. Pulling out only what actually bites in practice from the upgrade guide's Breaking Changes section:

execution_date and its derived keys have vanished from the context. execution_date, prev_execution_date, next_execution_date, prev_ds, next_ds, tomorrow_ds, yesterday_ds — the whole family is on the removal list. Referencing these keys in templates or task code causes a DAG error. The replacements are logical_date and the data-interval family.

The default semantics of cron strings have changed. With the create_cron_data_intervals setting now defaulting to False, a DAG that passes a cron string as-is, like schedule="0 0 * * *", is now interpreted by CronTriggerTimetable instead of CronDataIntervalTimetable. DAGs that explicitly pass a timetable instance are unaffected. The problem is that template values like data_interval_start, ds, and ts are derived from logical_date, and these values shift between the two timetables. If you depend on these values, the docs instruct you to explicitly set create_cron_data_intervals=True before upgrading; if you flip the flag back after 3.x runs have already been created, it will skip one scheduled run to avoid conflicting with the previous run's logical_date.

Do not assume the data interval of a manually triggered run. The docs explicitly state that in 3, you should not assume a manually triggered run's data_interval is derived from the logical_date the user passed in. If you need the date the user specified, you must read logical_date directly — this bites workflows that trigger sub-DAGs with TriggerDagRunOperator in particular.

The default behavior of xcom_pull() has changed. In 2, calling it without task_ids found the most recent value for that key across the whole DAG run; in 3, it only searches within the current task. If you need another task's XCom, you must now pass task_ids. Code like this starts silently receiving None, so it's safer to grep for it ahead of time.

catchup_by_default is now False. If you relied on catchup to backfill schedule gaps, you now need to set it explicitly on the DAG.

And the removal list — SubDAG (replaced by TaskGroup and asset scheduling), SequentialExecutor, CeleryKubernetesExecutor and LocalKubernetesExecutor (replaced by multi-executor configuration), DAG/XCom pickling, the CLI's --subdir argument (replaced by Dag bundles), and REST API v1 — all of these are gone. API clients need to move to the FastAPI-based v2.

The Most Painful Part — Custom Code That Read the Metadata DB Directly

If your team had custom operators or task code that read and wrote metadata through an Airflow DB session, this is the single biggest line item in your migration effort. This code does not work in 3, and the official guide offers two paths.

The recommended path is to use the REST API via the Airflow Python Client. Most use cases — DagRun, TaskInstance, Variable, Connection, XCom, and more — are covered by the API. That said, the docs are candid about the downsides too: you have to obtain a token via a call to /auth/token and rotate it, you become dependent on API server availability and network paths, and not every DB operation is exposed as an API endpoint. The community's stance is that if a feature is missing, you should request that the API add it rather than fall back to the DB directly.

The workaround is to open a plain DB connection to the metadata DB with a DbApiHook like PostgresHook, but the docs themselves flatly say this is not recommended. The reasons are spelled out too — this approach will break on versions after Airflow 3.2, the metadata DB schema is not a public API and can change without notice, it conflicts with task isolation, which is 3's core design, and it reverts to the 2-era performance characteristics of opening a DB connection per task. Even if used temporarily, it should be with a read-only account and paired with a migration plan. Real-world conversion examples are collected in issue #49187.

From an effort-estimation standpoint, it's worth counting two numbers before you scope the migration: the number of ruff AIR301 violations, and the number of custom operators that touch a DB session directly. The former is mostly solved mechanically; the latter is a design change, one at a time.

The Deployer Checklist — This Work Has an Order

The operations side of the work is mostly a matter of following the guide's steps, but a few things are worth knowing ahead of time so the schedule doesn't slip.

airflow config update           # check what needs to change (also backported to 2.11)
airflow config update --fix     # auto-fix
airflow db migrate              # schema migration — the longest-running step
airflow api-server              # there is no webserver command anymore
airflow dag-processor           # now must be started separately (local dev too)
  • Prerequisites. The minimum is Airflow 2.7, and the recommended path is through the latest 2.x on the way to 3. airflow dags reserialize must run without errors, and DAG-processing errors need to be resolved on the old version before you upgrade.
  • DB cleanup and backup. Schema-change time scales with DB size. The docs recommend starting by clearing out old XComs and the like with airflow db clean, and warn that if you proceed without a backup and the migration is interrupted midway, you can end up in a half-migrated state.
  • Component configuration changes. The webserver has become a general-purpose API server, and the Dag processor must always be launched as an independent process. If you use the Helm chart, all values under webserver need to move to apiServer, and quite a few key names changed between chart 1.16.0 and 1.18.0.
  • Authentication changes. The default auth manager has changed to Simple Auth; to keep FAB-based auth, you need to install the FAB provider and explicitly set auth_manager. OAuth redirect URLs now get an /auth prefix — meaning you also need to update the redirect URL registered on the IdP side.
  • Plugins. Plugins that depended on Flask-AppBuilder views and blueprints need to move to a FastAPI app or sit on the FAB provider's compatibility layer.

SLA's Five-Month Gap — Removal Arrived Before the Replacement

The single most instructive event in this major transition is SLA. 3.0.0 (2025-04-22) removed SLA callbacks and metrics, and its release notes stated that "a more flexible replacement mechanism, DeadlineAlerts, is planned for a future release." That Deadline Alerts feature (AIP-86) didn't arrive until 3.1.0 (2025-09-25), and even then it was marked experimental and supported only async callbacks. Synchronous callbacks (SyncCallback) were added in 3.2.0 (2026-04-07), and as of the 3.2.0 release notes it is still marked experimental. 3.3.0 added a Deadlines page to the Browse menu.

To sum up — users who moved to 3.0 early faced a five-month gap with no first-party feature, and the replacement, ten months after it arrived, is still maturing under an experimental label. The stopgap 3.0's release notes suggested was task-level success/failure hooks or external monitoring. The lesson for teams that lean heavily on a specific feature: build the habit of checking the release notes yourself for exactly what version, and what state, a given feature's replacement is in before you plan a major upgrade.

2.11 Was Designed As a Bridge — Start Here If You're Still on 2.x

2.11.0 (2025-05-20) is less a new-feature release than a transition device. What the release notes' "Ease migration to Airflow 3" section lays out:

  • airflow config lint and airflow config update were backported to 2.11, so you can check and fix configuration ahead of moving to 3.
  • A logical_date field was added alongside every model that used to have execution_date. 3 drops execution_date entirely.
  • Flipping the create_delta_data_intervals flag ahead of time (default True in 2.11, default False in 3.0) lets you rehearse the new interpretation of timedelta schedules (DeltaTriggerTimetable) while still on 2.x. It's the same kind of rehearsal device as create_cron_data_intervals on the cron side.
  • timer_unit_consistency, which unifies timing-metric units, follows the same pattern — try it on in 2.11, and it's always on in 3.0.

In other words, the official path is clear: move up to the latest 2.11.x, flip the flags ahead of time toward 3.0's defaults, work through the violations with ruff and config lint, and then move to 3. Since 2.11 supports Python 3.9 through 3.12, you can also settle your Python version alignment at this stage.

Where 3.x Stands Now — July 2026, at 3.3.0

The world on the other side deserves an honest look too. Two things matter.

First, tracking the latest minor within 3.x is effectively required. Looking at the release history: 3.0.x's last patch was 3.0.6 (2025-08-29), right before 3.1.0; 3.1.x's was 3.1.8 (2026-03-11), right before 3.2.0; and 3.2.x's was 3.2.2 (2026-05-29), right before 3.3.0. Patches for the previous minor effectively stop once the next minor ships, and the README also recommends running the latest minor of whichever major you're on. There is no LTS. That means a roughly half-year cadence of minor upgrades needs to be on your operating calendar.

Second, code is still on the move even within 3.x. 3.2.0 moved the exceptions used by tasks into airflow.sdk.exceptions (the old airflow.exceptions import stays as a proxy that emits a warning), and also moved serialization (serde) logic to the Task SDK — the docs state flatly that both compatibility layers are slated for removal in Airflow 4. Getting from 2 to 3 doesn't mean the move is over; the migration to airflow.sdk is still ongoing. It's realistic to budget for this follow-up work as part of the migration.

Briefly on the feature side: 3.1 (2025-09-25) brought Deadline Alerts, Human-in-the-Loop, and UI localization; 3.2's (2026-04-07) headline was asset partitioning — triggering downstream DAGs off a specific partition change rather than the whole asset, a good fit for date-partitioned data lakes — and multi-team deployment (per-team DAG/connection/pool isolation within a single Airflow instance, marked experimental). 3.3.0 (2026-07-06) extended that partitioning with mappers like RollupMapper and FanOutMapper plus a wait_policy, made retry policy pluggable (AIP-105), added a task/asset state store (AIP-103), and shipped a Coordinator layer for writing tasks in non-Python languages (AIP-108) as experimental — the Java SDK was tagged 1.0.0-beta1 on 2026-07-13. It's worth reading plainly that a good number of the new features carry an experimental label. These features are a weak reason to hurry; the only strong reason to hurry is 2.x's EOL.

So What Should You Do Right Now

Broken down by situation:

If you're still running 2.x. You are running software with no security patches under scheduler privileges. The first step isn't 3 — it's 2.11.2. From there, take three measurements first: airflow config lint, ruff AIR301/AIR302, and an inventory of code with direct DB access. Without these measurements, you can't even scope this migration. Work you can front-load on 2.x, like switching standard operators to the provider package, can happen at this same point.

If you depend heavily on SLA, SubDAG, or direct DB access. Check the state of each feature's replacement directly in the release notes, and design the transition first. SLA needs a redesign toward Deadline Alerts (including the fact that it's still marked experimental), SubDAG needs to become TaskGroup/asset scheduling, and DB access needs to be rewritten on the Python Client. Without these three, the migration is more mechanical than it sounds.

If you're already on 3.x. Build a half-year cadence calendar for tracking the latest minor, and work through the DeprecatedImportWarnings coming from airflow.exceptions and the old serde path now — these are items already announced for removal in Airflow 4.

One last layer distinction. Airflow is the control plane for batch data pipelines; durable execution of application code is a problem for a different layer — that story is covered in the Temporal Worker Versioning GA post. For recent changes on the execution-engine side that the orchestrator directs, see PySpark 4.2 defaulting to the Arrow UDF; for a comparison across the whole orchestration-tool landscape, see the Data Orchestration Guide and Workflow Engines in 2026.

Closing

To sum up: Airflow 2 ended as of April 22, 2026, and its last patch was 2.11.2 (2026-03-14). The road from 2 to 3 is not an import substitution — it's an architecture change. Removing workers' direct DB access is the core of it, and on top of that sit semantic landmines like the removal of the execution_date family, the change in cron semantics, and the change in xcom_pull behavior. The official tooling (ruff's AIR rules, config lint/update, the 2.11 bridge) cuts down the mechanical part quite a bit, but direct DB-access code and SLA dependence can only be resolved by redesigning.

And 3.x is not a destination — it's a moving train. Minors ship on a roughly half-year cadence, patches for the previous minor stop, and the migration to airflow.sdk continues alongside an announced removal in 4.0. So you need two plans: the one-time project of crossing from 2 to 3, and the ongoing operating rhythm of tracking the latest minor after you've crossed. The time you had to put off the former ended this past April.

References