다중 팀

다중 팀 (Multi-Team)

Multi-Team은 하나의 Airflow 배포 안에서 여러 팀이 리소스 격리(resource isolation)와 팀 단위 접근 제어를 누리며 함께 사용할 수 있게 해주는 기능이에요. 중간 규모 이상의 조직에서 팀별로 분리된 실행 환경이나 UI 뷰가 필요할 때 특히 유용하죠. 이 페이지에서는 Multi-Team 모드의 핵심 개념, 활성화 방법, 팀별 리소스 설정, 실행자(executor) 구성, 그리고 일정 관리가 실제로 어떻게 동작하는지 차근차근 살펴볼게요.

출처: 문서

본문

경고

Multi-Team은 아직 실험적(experimental) 기능으로, 미리보기(preview) 단계에 있어요. Airflow 3.3이 이 기능의 상당 부분을 제공하지만 아직 완전하진 않아요. 일부 기능은 향후 릴리스(3.4+)에서 제공될 예정이고, 사용자 피드백에 따라 경고 없이 동작이 바뀔 수도 있어요. 자세한 내용은 아래의 작업 진행 중(Work in Progress) 섹션을 확인해 주세요.

Multi-Team Airflow는 조직이 하나의 Airflow 배포 안에서 여러 팀을 운영하면서도 리소스 격리와 팀 기반 접근 제어를 제공할 수 있게 해주는 기능이에요. 이 기능은 여러 팀이 Airflow 인프라를 공유하면서도 리소스의 논리적 분리를 유지해야 하는 중간 규모 이상의 조직을 위해 설계되었어요.

참고

Multi-Team Airflow는 멀티테넌시(multi-tenancy)와는 달라요. 하나의 배포 안에서의 격리를 제공하지만 완전한 테넌트 분리를 목표로 하진 않아요. 모든 팀은 동일한 Airflow 인프라와 스케줄러, 메타데이터 데이터베이스를 공유해요.

Multi-Team 모드를 언제 사용할까요

Multi-Team 모드는 일반적으로 Airflow 환경에 접근해야 하는 팀이 많은 중간 규모 이상의 조직을 위해 설계되었어요. 공유 Airflow 인프라를 관리하는 전담 플랫폼/DevOps 팀이 있는 경우가 많지만, 반드시 있어야 하는 건 아니에요.

다음과 같은 상황에서 Multi-Team 모드를 사용해 보세요:

  • 여러 팀이 Airflow 인프라를 공유해야 할 때
  • 팀 간에 UI와 API 수준에서 리소스 격리(Variables, Connections, Secrets 등)가 필요할 때 (태스크 수준 격리의 한계는 Airflow Security Model을 참고하세요)
  • 팀별로 별도의 실행 환경을 원할 때
  • Airflow UI에서 팀별로 별도의 뷰를 원할 때
  • 단일 Airflow 배포를 공유해 운영 오버헤드나 비용을 줄이고 싶을 때

핵심 개념

팀 (Teams)

팀(Team) 은 조직 내 사용자 그룹을 나타내는 논리적 묶음이에요. 팀은 일부 Airflow 메타데이터 데이터베이스에 저장되며 리소스 격리의 기반이 돼요.

Airflow 데이터베이스의 팀은 매우 단순한 구조로, 필드가 하나뿐이에요:

  • name: 팀의 고유 식별자 (3~50자, 영숫자와 하이픈, 밑줄 사용 가능)

팀은 별도의 연결(association) 테이블을 통해 Dag 번들( bundles)과 연결되며, 이 테이블이 팀 이름과 Dag 번들 이름을 이어줘요.

Dag 번들과 팀 소유권

팀은 Dag 번들(Dag Bundles) 을 통해 Dag와 연결돼요. 하나의 Dag 번들은 최대 한 개의 팀만 소유할 수 있어요. Dag 번들이 팀에 할당되면:

  • 해당 번들의 모든 Dag는 그 팀에 속해요
  • 해당 Dag의 태스크는 팀 연관성을 상속받아요
  • 해당 Dag와 연관된 모든 콜백(callbacks)도 팀 연관성을 상속받아요
  • 해당 Dag 태스크가 만든 trigger도 팀 연관성을 상속받아요
  • 스케줄러는 이 관계를 활용해 어떤 실행자를 사용할지 결정해요

참고

관계 체인(relationship chain)은 다음과 같아요: Task/Callback → Dag → Dag Bundle → Team

리소스 격리 (Resource Isolation)

Multi-Team 모드가 활성화되면 다음 리소스를 특정 팀에 범위를 한정(scope)할 수 있어요:

  • Variables: 팀 멤버는 자신의 팀이 소유한 변수 또는 전역 변수에만 접근할 수 있어요
  • Connections: 팀 멤버는 자신의 팀이 소유한 커넥션 또는 전역 커넥션에만 접근할 수 있어요
  • Pools: 풀을 팀에 할당할 수 있어요
  • XComs: 태스크는 자신의 팀에 속한 Dag의 XCom에만 접근할 수 있어요 (읽기의 경우 전역 Dag도 가능해요)

팀 할당이 없는 리소스는 전역(global) 으로 간주되며 모든 팀이 접근할 수 있어요.

팀 범위 XCom

Multi-Team 모드가 활성화되면 Task Execution API를 통한 XCom 접근은 요청하는 태스크의 팀으로 범위가 한정돼요. 이 팀은 태스크의 Dag에서 Dag -> bundle -> team 관계를 통해 파생돼요. 팀 간 XCom 공유는 없어요:

  • 태스크는 자신의 팀에 속한 Dag의 XCom과 전역(팀이 없는) Dag의 XCom을 읽을 수 있어요.
  • 태스크는 자신의 팀에 속한 Dag에 대해서만 XCom을 쓰거나 삭제할 수 있어요. 팀 태스크는 전역 Dag의 XCom을 변경할 수 없어요. 이는 팀 범위 Variables와 Connections가 동작하는 방식과 동일해요.

이 경계는 Execution API에서만 적용돼요. 다른 리소스와 마찬가지로, 데이터베이스에 직접 접근하는 구성 요소를 제한하진 않아요 (Airflow Security Model 참고).

시크릿 백엔드 (Secrets Backends)

Airflow의 시크릿 백엔드(환경 변수, metastore, 로컬 파일시스템 포함)는 팀 인식(team-aware) 방식으로 동작해요. 커스텀 Secrets Backend는 케이스 바이 케이스로 지원돼요.

태스크가 Variable이나 Connection을 요청하면 시크릿 백엔드는 팀별 값을 반환해요(있는 경우). 백엔드는 요청하는 태스크의 팀을 기반으로 올바른 값을 자동으로 결정해요.

인증 관리자 (Auth Manager)

멀티-팀 모드를 사용하려면 인증 관리자(auth manager)가 이와 호환되어야 해요. 호환되는 인증 관리자는 두 가지 메서드를 구현해야 해요:

  • is_authorized_team: 사용자가 팀에 대해 주어진 작업을 수행할 권한이 있는지 결정해요. 주로 사용자가 팀에 속해 있는지 확인하는 데 사용돼요.
  • _get_teams: 인증 관리자에 정의된 팀 집합을 반환해요.

초기화 중에 Airflow는 인증 관리자에 정의된 모든 팀이 Airflow 메타데이터 데이터베이스에도 존재하는지 검증해요. 팀이 하나라도 없으면 Airflow가 오류를 발생시켜요.

사용 중인 인증 관리자가 이 메서드들을 구현하지 않으면 런타임에 NotImplementedError가 발생해요.

멀티-팀과 호환되는 인증 관리자 예시:

Multi-Team 모드 활성화하기

Multi-Team 모드를 활성화하려면 airflow.cfg에 다음 설정을 추가하세요:

[core]
multi_team = True

환경 변수로 설정할 수도 있어요:

export AIRFLOW__CORE__MULTI_TEAM=True

경고

기존 배포에서 이 설정을 바꾸려면 신중한 계획이 필요해요.

팀 생성 및 관리

팀은 Airflow CLI로 관리해요. 사용 가능한 명령은 다음과 같아요.

팀 생성

airflow teams create <team_name>

팀 이름은 3~50자여야 하며 영숫자, 하이픈, 밑줄만 포함할 수 있어요.

팀 목록 보기

airflow teams list

이 명령은 배포의 모든 팀과 그 이름을 표시해요.

팀 삭제

airflow teams delete <team_name>

확인 프롬프트를 건너뛰려면:

airflow teams delete <team_name> --yes

경고

팀에 연관된 리소스(Dag 번들, Variables, Connections, Pools)가 있으면 삭제할 수 없어요. 먼저 이러한 연관을 제거해야 해요.

팀 리소스 설정

팀 범위 Variables

Variables는 생성 시 팀에 연관시킬 수 있어요. 팀에 속한 태스크는 다음에 접근할 수 있어요:

  1. 자신의 팀이 소유한 변수
  2. 전역 변수 (팀 연관 없음)

태스크가 변수를 요청하면 시스템은 먼저 팀별 변수가 있는지 확인해요.

팀 범위 변수는 Airflow UI 또는 환경 변수를 통해 생성하고 관리할 수 있어요.

환경 변수를 통해서는 다음 형식으로 팀 범위 변수를 설정할 수 있어요:

# Global variable
export AIRFLOW_VAR_MY_VARIABLE="global_value"

# Team-scoped variable for "team_a"
export AIRFLOW_VAR__TEAM_A___MY_VARIABLE="team_a_value"

형식은 다음과 같아요: AIRFLOW_VAR__{TEAM}___{KEY} (팀 앞에 밑줄 두 개, 팀과 키 사이에 밑줄 세 개를 사용한다는 점에 주의하세요)

팀 범위 Connections

Connections도 변수와 동일한 패턴을 따라요. 태스크는 자신의 팀이 소유한 커넥션 또는 전역 커넥션에 접근할 수 있어요.

팀 범위 커넥션은 Airflow UI 또는 환경 변수를 통해 생성하고 관리할 수 있어요.

환경 변수를 통해서는:

# Global connection
export AIRFLOW_CONN_MY_DATABASE="postgresql://..."

# Team-scoped connection for "team_a"
export AIRFLOW_CONN__TEAM_A___MY_DATABASE="postgresql://..."

형식은 다음과 같아요: AIRFLOW_CONN__{TEAM}___{CONN_ID} (팀 앞에 밑줄 두 개, 팀과 커넥션 ID 사이에 밑줄 세 개)

팀 범위 Pools

풀을 팀에 할당하면 태스크 실행 슬롯에 대한 리소스 격리를 제공할 수 있어요. 풀이 팀에 할당되면:

  • 그 팀의 태스크만 그 풀을 사용할 수 있어요
  • 스케줄러는 태스크를 스케줄링할 때 풀 접근을 검증해요
  • 다른 팀의 풀을 사용하려는 태스크는 오류와 함께 실패해요

팀 할당이 없는 풀은 모든 팀이 전역적으로 접근할 수 있어요.

CLI로 팀 범위 Pools 만들기

airflow pools set 명령에 --team-name 옵션을 사용해서 풀을 팀에 할당하세요:

# Create a pool assigned to team_a
airflow pools set team_a_pool 10 "Pool for team A" --team-name team_a

# Create a global pool (no team assignment)
airflow pools set shared_pool 20 "Shared pool for all teams"

# Update an existing pool to assign it to a team
airflow pools set existing_pool 5 "Now team-scoped" --team-name team_b

참고

core.multi_team이 비활성화되면 --team-name 옵션은 거부돼요. 지정한 팀은 데이터베이스에 존재해야 해요 (먼저 airflow teams create로 생성하세요).

REST API로 팀 범위 Pools 만들기

요청 본문에 team_name 필드를 넣어 POST /api/v2/pools 엔드포인트를 사용하세요:

curl -X POST "http://localhost:8080/api/v2/pools" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "team_a_pool",
    "slots": 10,
    "description": "Pool for team A",
    "include_deferred": false,
    "team_name": "team_a"
  }'

기존 풀의 팀 할당을 업데이트하려면 PATCH /api/v2/pools/{pool_name}을 사용하세요:

curl -X PATCH "http://localhost:8080/api/v2/pools/team_a_pool" \
  -H "Content-Type: application/json" \
  -d '{"team_name": "team_a"}'

team_name을 생략하거나 null로 설정하면 풀을 전역으로 만들 수 있어요.

UI로 팀 범위 Pools 만들기

Airflow UI에서 Admin > Pools로 이동해 풀을 생성하거나 편집하세요. Multi-Team 모드가 활성화되면 풀을 팀에 할당할 수 있는 Team 드롭다운이 표시돼요. 전역 풀로 만들려면 비워 두세요.

팀 기반 실행자(Executor) 구성

Multi-Team 모드의 가장 강력한 기능 중 하나는 팀마다 다른 실행자를 구성하거나, 같은 실행자를 다르게 구성할 수 있다는 점이에요. 이를 통해 팀별로 전용 컴퓨팅 리소스나 실행 흐름을 가질 수 있어요.

전역 실행자와 마찬가지로 팀 범위 실행자 구성도 여러 실행자를 지원해요 (예: LocalExecutorKubernetesExecutor를 함께 사용). 이를 통해 해당 팀의 태스크가 어떤 실행자를 사용할지 지정할 수 있어요. 여러 실행자 구성에 대한 자세한 내용은 Using Multiple Executors Concurrently를 참고하세요.

구성 형식

실행자 구성은 다음 형식을 사용해 팀 기반 할당을 지원해요:

[core]
executor = GlobalExecutor;team1=Team1Executor;team2=Team2Executor

예를 들어:

[core]
executor = LocalExecutor;team_a=CeleryExecutor;team_b=KubernetesExecutor

이 구성에서:

  • LocalExecutor는 전역 기본 실행자예요
  • team_a의 태스크는 CeleryExecutor 또는 LocalExecutor를 사용해요
  • team_b의 태스크는 KubernetesExecutor 또는 LocalExecutor를 사용해요
  • 전역 범위의 태스크는 LocalExecutor를 사용해요

중요한 규칙

  1. 전역 실행자가 먼저 와야 해요: 팀 접두사가 없는 전역 실행자가 최소 하나 구성되어야 하고, 팀별 실행자보다 먼저 나타나야 해요.
  2. 팀이 존재해야 해요: 실행자 구성의 모든 팀 이름은 Airflow가 시작되기 전에 데이터베이스에 존재해야 해요.
  3. 실행자가 멀티-팀을 지원해야 해요: 모든 실행자가 멀티-팀 모드를 지원하진 않아요. 실행자 클래스에 supports_multi_team = True가 있어야 해요.
  4. 중복 팀 없음: 각 팀은 실행자 구성에 한 번만 나타날 수 있어요.
  5. 팀 내 중복 실행자 없음: 팀은 같은 실행자를 여러 번 구성할 수 없어요.

구성 예시:

# Valid: Global executor with team-specific executors
executor = LocalExecutor;team_a=CeleryExecutor

# Valid: Multiple team-specific executors
executor = LocalExecutor;team_a=CeleryExecutor;team_b=KubernetesExecutor

# Valid: Multiple executors globally and per team
executor = LocalExecutor,KubernetesExecutor;team_a=CeleryExecutor,KubernetesExecutor;team_b=LocalExecutor

# Invalid: No global executor
executor = team_a=CeleryExecutor;team_b=LocalExecutor

# Invalid: Global executor after team executor
executor = team_a=CeleryExecutor;LocalExecutor

# Invalid: Duplicate Team
executor = LocalExecutor;team_a=CeleryExecutor;team_b=LocalExecutor;team_a=KubernetesExecutor

# Invalid: Duplicate Executor within a Team
executor = LocalExecutor;team_a=CeleryExecutor,CeleryExecutor;team_b=LocalExecutor

팀 간 실행자 별칭(Alias) 지정

같은 실행자 유형을 전역과 팀 수준 모두에서 사용할 때(예: 전역 LocalExecutor와 팀용 LocalExecutor), 태스크가 전역 실행자를 목표로 하려면 두 인스턴스를 구분할 방법이 필요해요. 이를 위해 Alias:ExecutorName 구문을 사용해 핵심 실행자에 별칭(aliases) 을 지정할 수 있어요:

[core]
executor = global_celery_exec:CeleryExecutor;team1=team_celery_exec:CeleryExecutor

이 구성에서:

  • 전역 CeleryExecutorglobal_celery_exec 별칭으로 사용할 수 있어요
  • team_a의 CeleryExecutorteam_celery_exec 별칭으로 사용할 수 있어요
  • team_a의 태스크가 executor="team_celery_exec", executor="CeleryExecutor", 또는 executor="airflow.providers.celery.executors.celery_executor.CeleryExecutor"로 설정하면 실행자에서 실행돼요
  • team_a의 태스크가 executor="global_celery_exec"로 설정하면 전역 실행자에서 실행돼요
# Runs on the global CeleryExecutor via alias
BashOperator(
    task_id="uses_global",
    executor="global_celery_exec",
    bash_command="echo 'running on global executor'",
)

# Runs on team_a's CeleryExecutor via alias
BashOperator(
    task_id="use_team_alias",
    executor="team_celery_exec",
    bash_command="echo 'running on team executor'",
)

# Runs on team_a's CeleryExecutor via class name
BashOperator(
    task_id="use_team_classname",
    executor="CeleryExecutor",
    bash_command="echo 'running on team executor'",
)

# Runs on team_a's CeleryExecutor via full module path
BashOperator(
    task_id="use_team_module_path",
    executor="airflow.providers.celery.executors.celery_executor.CeleryExecutor",
    bash_command="echo 'running on team executor'",
)

# Also runs on team_a's CeleryExecutor (implicit team default)
BashOperator(
    task_id="use_default",
    bash_command="echo 'running on default team executor'",
)

별칭은 모든 핵심 실행자(LocalExecutor, CeleryExecutor, KubernetesExecutor 등)와 커스텀 실행자 모듈 경로에서도 동작해요. 별칭과 다중 실행자 구성에 대한 자세한 내용은 Using Multiple Executors Concurrently를 참고하세요.

팀별 실행자 설정

여러 팀이 같은 실행자 유형을 사용할 때(예: team_ateam_b가 모두 CeleryExecutor를 사용), 각 팀은 해당 실행자에 대해 자신만의 설정을 제공할 수 있어요. 이를 통해 팀별로 다른 Celery 브로커를 지정하거나, 다른 Kubernetes 네임스페이스를 사용하거나, 실행자 설정을 독립적으로 커스터마이징할 수 있어요.

설정 해석 순서

팀 실행자가 설정 값(예: [celery] broker_url)을 읽을 때, 시스템은 다음 소스를 순서대로 확인해 처음 찾은 값을 반환해요:

  1. 팀별 환경 변수AIRFLOW__{TEAM}___{SECTION}__{KEY}
  2. 팀별 설정 파일 섹션[team_name=section]
  3. 기본 값 — 내장 기본값 또는 fallback

팀 실행자에서는 다음 소스는 건너뜁니다 (아직 팀 기반 구성을 지원하지 않아요):

  • 명령 실행 ({key}_cmd)
  • 시크릿 백엔드 ({key}_secret)

참고

팀별 구성은 전역 환경 변수나 전역 설정 파일 설정으로 폴백하지 않아요. 예를 들어 전역 CeleryExecutor와 팀 CeleryExecutor가 함께 사용 중일 때, 전역 CeleryExecutorcelery.worker_concurrency를 기본값 16에서 32로 올리고 싶을 수 있어요. 하지만 팀 CeleryExecutor32로 강제되지 않으며, 팀별 구성으로 명시적으로 재정의하지 않는 한 기본값 16을 계속 사용해요.

환경 변수로 설정

팀별 구성은 다음 형식의 환경 변수로 제공할 수 있어요:

AIRFLOW__{TEAM}___{SECTION}__{KEY}

구분자에 주의하세요: 팀 이름 앞에 밑줄 두 개(AIRFLOW__ 접두사의 일부), 팀 이름과 섹션 사이에 밑줄 세 개, 섹션과 키 사이에 밑줄 두 개를 사용해요. 팀 이름은 대문자예요.

# team_a's Celery broker URL
export AIRFLOW__TEAM_A___CELERY__BROKER_URL="redis://team-a-redis:6379/0"

# team_b's Celery broker URL
export AIRFLOW__TEAM_B___CELERY__BROKER_URL="redis://team-b-redis:6379/0"

# team_b's Celery result backend
export AIRFLOW__TEAM_B___CELERY__RESULT_BACKEND="db+postgresql://team-b-db/celery_results"
설정 파일로 설정

팀별 설정은 airflow.cfg 파일에 팀 이름과 등호로 시작하는 섹션을 사용해 넣을 수도 있어요:

# Global celery settings (used by the global executor, NOT as a fallback for teams)
[celery]
broker_url = redis://default-redis:6379/0
result_backend = db+postgresql://default-db/celery_results

# team_a overrides
[team_a=celery]
broker_url = redis://team-a-redis:6379/0
result_backend = db+postgresql://team-a-db/celery_results

# team_b overrides
[team_b=celery]
broker_url = redis://team-b-redis:6379/0
result_backend = db+postgresql://team-b-db/celery_results

Dag 번들과 팀 연관

Dag 번들은 Dag 번들 구성을 통해 팀과 연관돼요. Dag 번들을 구성할 때 각 번들에 대해 team_name을 지정해요:

[dag_processor]
dag_bundle_config_list = [
    {
        "name": "team_a_dags",
        "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
        "kwargs": {"path": "/opt/airflow/dags/team_a"},
        "team_name": "team_a"
    },
    {
        "name": "team_b_dags",
        "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
        "kwargs": {"path": "/opt/airflow/dags/team_b"},
        "team_name": "team_b"
    },
    {
        "name": "shared_dags",
        "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
        "kwargs": {"path": "/opt/airflow/dags/shared"}
    }
]

이 예시에서:

  • /opt/airflow/dags/team_a의 Dag는 team_a에 속해요
  • /opt/airflow/dags/team_b의 Dag는 team_b에 속해요
  • /opt/airflow/dags/shared의 Dag는 팀이 없어요 (전역)

참고

team_name에 지정한 팀은 Dag 번들을 동기화하기 전에 데이터베이스에 존재해야 해요. 먼저 airflow teams create로 팀을 생성하세요.

일정 관리가 어떻게 동작하나요

Multi-Team 모드가 활성화되면 스케줄러는 각 태스크에 올바른 실행자를 결정하기 위해 추가 로직을 수행해요:

  1. 태스크에서 팀으로 해석: 스케줄러는 다음 관계 체인을 따라 각 태스크의 팀을 결정해요:
    • Task → Dag (dag_id 경유)
    • Dag → Dag Bundle (bundle_name 경유)
    • Dag Bundle → Team (dag_bundle_team 연결 테이블 경유)
  2. 실행자 선택: 팀이 결정되면:
    • 팀별 실행자가 구성되어 있으면 그 실행자를 사용해요
    • 그렇지 않으면 전역 기본 실행자로 폴백해요

경고

Multi-Team Airflow는 팀 주변에 보안 경계를 만드는 논리적 격리를 제공하지만, 완전한 격리는 아니에요. 모든 팀은 같은 메타데이터 데이터베이스와 공통 Airflow 인프라를 공유해요. 절대적으로 엄격한 보안 요구사항이 있다면 별도의 Airflow 배포를 고려하세요.

팀 범위 Triggerer

버전 3.3.0에서 추가되었어요.

Multi-Team 모드가 활성화되면 triggerer는 --team-name CLI 인자로 각 특정 팀에 범위를 한정해야 해요. 팀 범위 triggerer는 해당 팀 Dag에 속한 지연된 태스크(trigger)를 처리해요. 이를 통해 팀별로 독립적인 용량과 장애 도메인을 가진 격리된 triggerer 인스턴스를 운영할 수 있어요.

구성

--team-name을 전달해 팀 범위 triggerer를 시작하세요:

# Triggerer for team_a only
airflow triggerer --team-name team_a

# Triggerer for team_b only
airflow triggerer --team-name team_b

# Global triggerer — processes triggers from Dags with no team association
airflow triggerer

시작 시 검증은 core.multi_team이 활성화되어 있고 지정된 팀이 데이터베이스에 존재하는지 확인해요.

동작

  • 팀 범위 triggerer (--team-name team_x): 원본 Dag가 team_x에 매핑된 번들에 속한 trigger만 선택해요.
  • 전역 triggerer (--team-name 없음): 원본 Dag가 팀 할당이 없는 번들에 속한 trigger만 선택해요.
  • Multi-Team 비활성화 (core.multi_team = False): --team-name이 거부돼요. 필터링이 발생하지 않으며 모든 triggerer가 모든 trigger를 처리해요 (기존 동작).

--queues와의 상호작용

팀 필터링과 큐 필터링은 직교(orthogonal)해요 — 두 조건은 AND로 결합돼요. 예를 들어 --team-name team_a --queues q1,q2로 시작한 triggerer는 team_a에 속하면서 큐 q1 또는 q2의 태스크에서 지연된 trigger만 처리해요.

참고

모든 팀에 대해 최소 하나의 triggerer가 실행 중인지 확인하세요. 그렇지 않으면 그 팀의 trigger는 triggerer가 시작될 때까지 미할당 상태로 남아요. --queues를 사용할 때도 모든 큐에 동일하게 적용돼요. --team-name--queues를 결합하면 이 요구사항은 각 팀-및-큐 조합으로 확장돼요.

팀 기반 에셋 이벤트 필터링

Multi-Team 모드가 활성화되면 에셋 이벤트는 다운스트림 Dag 실행을 트리거하기 전에 팀 구성원 자격으로 필터링돼요. 이는 한 팀의 Dag가 만든 에셋 이벤트가 다른 팀의 Dag 실행을 의도치 않게 트리거하는 것을 방지해요.

기본 동작

기본적으로 소비(consuming) Dag는 같은 팀 내의 프로듀서 또는 팀 연관이 없는 Dag(즉 전역 Dag)의 이벤트만 수신해요.

producer_teams로 크로스-팀 옵트인

버전 3.3.0에서 추가되었어요.

특정 팀이 다른 팀의 주어진 에셋에서 소비자를 트리거하는 이벤트를 생성하도록 허용하려면 Asset 정의에 AssetAccessControl 인스턴스와 함께 access_control 파라미터를 사용하세요:

from airflow.sdk import Asset, AssetAccessControl

shared_data = Asset(
    name="shared_data",
    uri="s3://bucket/shared/data.csv",
    access_control=AssetAccessControl(
        producer_teams=["team_analytics", "team_ml"],
    ),
)

이 구성으로 team_analytics 또는 team_ml의 에셋 이벤트는 소비자 자신의 팀 이벤트뿐만 아니라 shared_data를 스케줄로 사용하는 모든 소비 Dag에서도 수락돼요.

전역(팀이 없는) Dag 프로듀서가 소비자를 트리거하지 못하도록 하려면 allow_global=False로 설정하세요:

strict_data = Asset(
    name="strict_data",
    uri="s3://bucket/strict/data.csv",
    access_control=AssetAccessControl(
        producer_teams=["team_analytics"],
        allow_global=False,
    ),
)

consumer_teams로 크로스-팀 옵트인

버전 3.3.0에서 추가되었어요.

producer_teams소비자 쪽( Dag 스케줄에 사용된 에셋)에 지정되는 반면, consumer_teams프로듀서 쪽(태스크의 outlets에 사용된 에셋)에 지정돼요. 이는 그 특정 태스크가 생성한 이벤트를 수신할 수 있는 소비자 팀을 제어해요.

from airflow.sdk import DAG, Asset, AssetAccessControl, task

restricted_output = Asset(
    name="restricted_output",
    uri="s3://bucket/restricted/output.csv",
    access_control=AssetAccessControl(
        consumer_teams=["team_downstream", "team_reporting"],
    ),
)

with DAG(dag_id="producer_dag", schedule="@daily"):

    @task(outlets=[restricted_output])
    def produce_data():
        """Only team_downstream and team_reporting can consume events from this task."""

이 구성으로 team_downstream 또는 team_reporting에 속한 소비 Dag(팀이 없는 소비자 포함)만 produce_data 태스크가 생성한 에셋 이벤트를 수신해요.

참고

consumer_teams의 기본값은 None이며, 이는 빈 리스트와 같지 않아요:

  • None(기본값, 또는 필드 생략): 소비자-팀 제한이 적용되지 않아요. 크로스-팀 전달은 각 소비자 자신의 producer_teams 옵트인에 의해서만 결정돼요.
  • [](명시적 빈 리스트): 프로듀서의 자신의 팀으로만 전달을 제한해요 (팀이 없는 소비자 포함, allow_global에 따름). 이는 producer_teams에 이 프로듀서를 나열한 소비자라도 모든 크로스-팀 소비자를 차단해요.
  • ["team_x", ...]: 나열된 크로스-팀 소비자에게 추가로 전달해요.
프로듀서별 범위 지정

consumer_teams는 에셋 단위가 아니라 생성 태스크별로 범위가 지정돼요. 여러 태스크가 같은 에셋에 대해 이벤트를 생성하면 각 태스크의 consumer_teams가 생성한 이벤트에 독립적으로 적용돼요:

from airflow.sdk import DAG, Asset, AssetAccessControl, task

# This task restricts consumers to team_a only
restricted_asset = Asset(
    name="shared_asset",
    uri="s3://bucket/shared.csv",
    access_control=AssetAccessControl(
        consumer_teams=["team_a"],
    ),
)

# This task sets no access_control, so consumer_teams defaults to None (no consumer-team restriction)
unrestricted_asset = Asset(name="shared_asset", uri="s3://bucket/shared.csv")

with DAG(dag_id="dag_1", schedule="@daily"):

    @task(outlets=[restricted_asset])
    def task_restricted():
        """Events from this task only reach team_a consumers."""

with DAG(dag_id="dag_2", schedule="@daily"):

    @task(outlets=[unrestricted_asset])
    def task_unrestricted():
        """Events from this task reach all consumers (no restriction)."""
producer_teams와의 상호작용

producer_teamsconsumer_teams는 모두 논리적 AND로 적용돼요. 소비자 Dag는 두 검사를 모두 통과해야 큐에 들어가요:

  • producer_teams (소비자 쪽): "어떤 프로듀서 팀의 이벤트를 수락할까?"
  • consumer_teams (프로듀서 쪽): "어떤 소비자 팀에게 이벤트를 전달할까?"

예를 들어 소비자의 스케줄 참조에 producer_teams=["team_x"]가 있고 프로듀서의 outlet 참조에 consumer_teams=["team_y"]가 있으면, 프로듀서가 team_x에 속하고 그리고 소비자가 team_y에 속할 때만 소비자가 큐에 들어가요.

팀 없는 소비자 통과

팀이 없는 소비자(Dag 팀 연관 없음)는 그 내용과 관계없이 consumer_teams 목록 검사를 통과해요. 그러나 프로듀서 쪽 에셋에 allow_global=False를 설정하면 팀 없는 소비자가 이벤트를 받지 못하도록 차단할 수 있어요. 기본적으로(allow_global=True) 팀 없는 소비자는 모든 프로듀서의 이벤트를 수신해요.

동작 규칙

다음 표는 완전한 필터링 로직을 설명해요:

Producer Consumer producer_teams consumer_teams allow_global Result Reason
Team A (Dag) Team A (any) (any) (any) ✅ Allowed Same team
Team A (Dag) Team B [] (any) (any) ❌ Blocked Different team, no producer opt-in
Team A (Dag) Team B ["team_a"] None (any) ✅ Allowed Producer opt-in, consumer_teams unset (no consumer restriction)
Team A (Dag) Team B ["team_a"] [] (any) ❌ Blocked consumer_teams=[] allows only the producer’s own team, blocking all cross-team even with producer opt-in
Team A (Dag) Team B ["team_a"] ["team_b"] (any) ✅ Allowed Both opt-ins satisfied
Team A (Dag) Team B ["team_a"] ["team_c"] (any) ❌ Blocked Consumer team not in consumer_teams
Team A (Dag) Team B [] ["team_b"] (any) ❌ Blocked Producer opt-in not satisfied (AND logic)
(no team, Dag) Team B (any) None True ✅ Allowed Global producer, allow_global is True
(no team, Dag) Team B (any) (any) False ❌ Blocked Global producer blocked by allow_global=False
(no team, Dag) Team B (any) ["team_b"] True ✅ Allowed Global producer, allow_global is True, consumer in list
(no team, Dag) Team B (any) ["team_c"] (any) ❌ Blocked Global producer, but consumer not in consumer_teams
Team A (Dag) (no team) (any) (any) (any) ✅ Allowed Teamless consumer passes through (unless producer-side allow_global=False )
(no team, Dag) (no team) (any) (any) (any) ✅ Allowed Both global
Team A (API) Team A (any) (any) (any) ✅ Allowed Same team
Team A (API) Team B ["team_a"] None (any) ✅ Allowed Producer opt-in, consumer_teams unset (no consumer restriction)
Team A (API) Team B ["team_a"] ["team_b"] (any) ✅ Allowed Both opt-ins satisfied
Team A (API) (no team) (any) (any) (any) ✅ Allowed Teamless consumer always passes through
(no team, API) Team B (any) (any) (any) ❌ Blocked Teamless API user cannot trigger team-bound consumer
(no team, API) (no team) (any) (any) (any) ✅ Allowed Both global

주요 규칙:

  • 같은 팀: 항상 허용돼요.
  • 전역(팀 없는) Dag 프로듀서 + allow_global=True: consumer_teams가 제한하지 않는 한 팀과 관계없이 모든 소비자를 트리거해요.
  • 전역(팀 없는) Dag 프로듀서 + allow_global=False: 팀 바인딩된 소비자를 트리거하지 못하도록 차단돼요.
  • 팀 없는 API 사용자: 팀 없는 소비자만 트리거할 수 있어요. 플랫폼 운영자가 배포하고 의도적으로 공유하는 팀 없는 Dag와 달리, 팀이 없는 API 사용자는 검증된 팀 소속이 없으므로 팀 바인딩된 파이프라인에 대한 무제한 접근을 막기 위해 그들의 이벤트는 팀 없는 소비자로 제한돼요.
  • 팀 없는 소비자: 프로듀서 쪽 에셋에 allow_global=False가 설정되지 않는 한, 팀이나 consumer_teams와 관계없이 모든 소스(Dag 또는 API)의 이벤트를 수락해요.
  • producer_teams를 통한 크로스-팀: 프로듀서의 팀이 에셋의 producer_teams에 나열되면 허용돼요.
  • consumer_teams를 통한 크로스-팀: 소비자의 팀이 생성 태스크의 consumer_teams에 나열되면 허용돼요.
  • 두 필터 모두 (AND 로직): producer_teamsconsumer_teams가 모두 지정되면, 소비자는 큐에 들어가기 위해 두 검사를 모두 통과해야 해요.
  • Multi-Team 비활성화: 모든 필터링이 건너뛰어지고 기존 동작이 유지돼요.

API 트리거 이벤트

사용자가 REST API로 에셋 이벤트를 생성하면 사용자의 팀이 인증 관리자에서 해석돼요. 동일한 필터링 규칙이 적용되며 한 가지 차이가 있어요: 팀 없는 API 사용자는 팀 없는 소비자만 트리거할 수 있는 반면, 팀 없는 Dag 프로듀서는 전역으로 취급되어 모든 소비자를 트리거할 수 있어요.

REST API는 또한 요청 본문에 선택적 access_control 객체를 받아들이며 다음 필드를 가져요:

  • consumer_teams (list[str] | null): 어떤 소비자 팀이 이벤트를 받을 수 있는지 제한해요. 태스크 수준 consumer_teams와 동일한 규칙을 따라요. 생략하거나 null이면 소비자-팀 필터링이 적용되지 않아요.
  • allow_global (bool, 기본값 true): 팀 없는 소비자가 이벤트를 받을 수 있는지 여부를 결정해요.

Multi-Team 모드가 비활성화되면 access_control 파라미터는 수락되지만 무시돼요.

실제 사례: 크로스-팀 데이터 파이프라인

세 팀이 있는 이커머스 플랫폼을 생각해 볼게요:

  • team_ingestion: 매시간 Kafka에서 원시 클릭스트림 데이터를 S3로 수집해요.
  • team_analytics: 그 원시 데이터에서 집계 보고 테이블을 만들어요.
  • team_ml: 같은 원시 데이터를 사용해 추천 모델을 학습해요.

세 팀 모두 같은 에셋(s3://data-lake/clickstream/hourly.parquet)을 참조하지만 역할이 달라요:

# --- team_ingestion's Dag bundle ---
from airflow.sdk import DAG, Asset, AssetAccessControl, task

clickstream = Asset(
    name="clickstream_hourly",
    uri="s3://data-lake/clickstream/hourly.parquet",
    access_control=AssetAccessControl(
        # Allow team_analytics and team_ml to consume events produced by team_ingestion
        consumer_teams=["team_analytics", "team_ml"],
    ),
)

with DAG(dag_id="ingest_clickstream", schedule="@hourly"):

    @task(outlets=[clickstream])
    def ingest_from_kafka():
        """Pull clickstream events from Kafka and write to S3."""
# --- team_analytics's Dag bundle ---
from airflow.sdk import DAG, Asset, AssetAccessControl

clickstream = Asset(
    name="clickstream_hourly",
    uri="s3://data-lake/clickstream/hourly.parquet",
    access_control=AssetAccessControl(
        # Accept events from team_ingestion (in addition to own-team events)
        producer_teams=["team_ingestion"],
    ),
)

with DAG(dag_id="build_reporting_tables", schedule=clickstream):
    ...
# --- team_ml's Dag bundle ---
from airflow.sdk import DAG, Asset, AssetAccessControl

clickstream = Asset(
    name="clickstream_hourly",
    uri="s3://data-lake/clickstream/hourly.parquet",
    access_control=AssetAccessControl(
        # Accept events from team_ingestion (in addition to own-team events)
        producer_teams=["team_ingestion"],
    ),
)

with DAG(dag_id="train_recommendations", schedule=clickstream):
    ...

이 설정에서:

  • clickstream_hourly 에셋은 세 팀 모두에서 같은 전역 객체예요.
  • team_ingestioningest_from_kafka 태스크가 완료되면 에셋 이벤트를 방출해요.
  • team_analyticsbuild_reporting_tablesteam_mltrain_recommendations는 모두 이벤트를 받아요. 그 이유는:
    • 소비자 쪽 (producer_teams=["team_ingestion"])이 team_ingestion의 이벤트를 수락하도록 옵트인했기 때문이에요.
    • 프로듀서 쪽 (consumer_teams=["team_analytics", "team_ml"])이 그 소비자 팀에 이벤트를 전달하도록 옵트인했기 때문이에요.
  • 무관한 team_marketing의 Dag는 이벤트를 받지 않아요. 그 이유는 프로듀서 쪽 consumer_teams에 나열되지도 않았고, 자신의 producer_teamsteam_ingestion을 나열하지도 않기 때문이에요.

팀 기반 메트릭

버전 3.3.0에서 추가되었어요.

Multi-Team 모드가 활성화되면 Airflow는 많은 운영 메트릭에 team_name 태그를 추가해 각 메트릭이 관련된 리소스를 소유한 팀을 식별해요. 이를 통해 메트릭 백엔드에서 활동을 팀별로 나눠 볼 수 있어요. 태그는 팀 소유 리소스에만 존재해요. 전역 풀, 팀 없는 Dag, 전역 구성 요소는 team_name 태그 없이 동일한 메트릭을 방출해요.

참고

team_name 차원은 메트릭 태그로 방출되므로 태그를 지원하는 메트릭 백엔드가 필요해요: 태깅이 활성화된 StatsD(예: Datadog 또는 InfluxDB 방언) 또는 OpenTelemetry가 있어야 해요.

참고

Multi-Team 모드가 비활성화되면 메트릭은 team_name 태그 없이 방출돼요. 단일 팀 Airflow 환경에서 항상 그렇게 해왔던 것과 정확히 동일해요.

team_name 태그는 다음 구성 요소의 메트릭에 적용돼요:

  • Triggerer: heartbeat, capacity, trigger-outcome 메트릭 (예: triggerer_heartbeat, triggers.running, triggers.succeeded)
  • Executors: executor 슬롯 게이지 (예: executor.open_slots, executor.queued_tasks)
  • Scheduler: 팀 범위 풀의 풀 슬롯 게이지와 태스크/에셋 스케줄링 카운터 (예: pool.open_slots, scheduler.tasks.killed_externally, asset.triggered_dagruns)
  • Dag runs: dag run 타이밍 및 수명주기 메트릭 (예: dagrun.duration.<state>, dagrun.first_task_scheduling_delay, dag.callback_exceptions)
  • Task instances: 태스크 시작, 종료, 결과 카운터 (예: ti.start, ti.finish, ti_successes, ti_failures)
  • Dag processing: 파일별 파싱 및 콜백 메트릭 (예: dag_processing.processes, dag_processing.processor_timeouts, dag_processing.callback_only_count)
  • Callbacks: 콜백 실행 카운터 (callback_success / callback_failure, 선택적으로 접두사 포함)

참고

3.3에서 team_name 태그를 갖는 것은 Airflow 코어가 방출하는 메트릭뿐이에요. 프로바이더별 메트릭은 이 릴리스에서 업데이트되지 않았어요. 프로바이더 실행자는 예외인데, 코어 베이스 실행자에서 상속하기 때문에 executor.* 슬롯 게이지에 태그가 지정돼요.

중요한 고려사항

작업 진행 중 (Work in Progress)

Multi-Team 모드는 현재 미리보기 단계의 실험적 기능이에요. 아직 완전하지 않으며 사용자 피드백에 따라 경고 없이 변경될 수 있어요. 향후 릴리스(3.4+)에서 제공될 누락된 기능은 다음과 같아요:

  • 일부 UI 요소가 완전히 팀 인식이 아닐 수 있어요
  • 팀 기반 구성에 대한 명령 및 시크릿 조회
  • 플러그인 지원

식별자의 전역 고유성

Dag ID, Variable 키, Connection ID는 Airflow 배포 전체에서 고유해야 해요. 어떤 팀이 소유하는지와 관계없이 말이에요. 이는 S3 버킷 이름이 모든 AWS 계정에서 전역적으로 고유해야 하는 것과 유사해요. 네이밍 충돌을 피하기 위해 조직 내에서 네이밍 규칙을 세워야 해요 (예: 식별자에 팀 이름을 접두사로 붙이기).

아키텍처

다음 다이어그램은 팀 간 리소스 격리가 있는 Multi-Team Airflow 배포를 보여줘요.

파란색 구성 요소는 공유 구성 요소이고, 초록색 구성 요소는 팀 구성 요소예요 (초록색 그림자 상자는 아키텍처에 둘 이상의 팀이 있음을 나타내요).

더 알아보기 (Learn more)