Multi-tenancy allows multiple teams or organizations to share a single Airflow instance while maintaining logical isolation. Each tenant has their own DAGs, connections, variables, and resource quotas.
Key Insight: Without proper isolation, one team's resource-intensive DAGs can starve other teams' workflows.
Isolation Layers
Layer
Method
Granularity
Example
DAG Access
RBAC + Tags
Per-DAG
tags=['tenant:alpha']
Resources
Pools
Per-task
pool='alpha_pool'
Credentials
Connections
Per-connection
conn_id='alpha_db'
Configuration
Variables
Per-variable
Variable.get('alpha_config')
Logs
Log routing
Per-DAG
S3 prefix s3://logs/alpha/
Network
Namespaces
Per-tenant
K8s namespace airflow-alpha
Multi-Tenancy Architecture
RBAC Configuration for Multi-Tenancy
# webserver_config.py
from airflow.security import permissions
# Define tenant-specific roles
ROLES = {
'TenantAlphaAdmin': [
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG),
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_DAG),
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_DAG),
(permissions.ACTION_CAN_ACCESS_MENU, permissions.RESOURCE_ADMIN_MENU),
],
'TenantAlphaViewer': [
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG),
],
'TenantBetaAdmin': [
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG),
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_DAG),
],
}
# Map users to roles
AUTH_ROLES_MAP = {
'TenantAlphaAdmin': ['admin@alpha.com', 'lead@alpha.com'],
'TenantAlphaViewer': ['viewer@alpha.com'],
'TenantBetaAdmin': ['admin@beta.com'],
}
# Custom access control
from airflow.www.security import AirflowSecurityManager
class TenantSecurityManager(AirflowSecurityManager):
"""Custom security manager for tenant isolation."""
def can_access_dag(self, user, dag_id):
"""Check if user can access specific DAG."""
# Extract tenant from DAG tags
dag = self.get_dag(dag_id)
if not dag:
return False
tenant_tags = [tag for tag in dag.tags if tag.startswith('tenant:')]
# Check user's tenant
user_tenants = self.get_user_tenants(user)
return any(tag.replace('tenant:', '') in user_tenants for tag in tenant_tags)
def get_user_tenants(self, user):
"""Get tenants user has access to."""
# Implementation depends on your user model
return user.extra_dict.get('tenants', [])
Pool-Based Resource Quotas
# airflow.cfg - Pool configuration for tenants
[core]
# Default pool slots
default_pool_slots = 128
# Define pools for each tenant
# Use CLI: airflow pools set alpha_pool 32 "Pool for Tenant Alpha"
# Use CLI: airflow pools set beta_pool 32 "Pool for Tenant Beta"
# Usage in DAGs
from airflow.decorators import task, dag
from datetime import datetime
@dag(
schedule_interval="@daily",
start_date=datetime(2024, 1, 1),
catchup=False,
tags=['tenant:alpha'],
default_args={
'pool': 'alpha_pool', # Assign to tenant's pool
},
)
def tenant_alpha_dag():
@task(pool='alpha_pool', pool_slots=4)
def alpha_processing():
"""Task using Tenant Alpha's pool."""
return {"status": "processing"}
@task(pool='alpha_pool', pool_slots=2)
def alpha_reporting():
"""Task using Tenant Alpha's pool."""
return {"status": "reporting"}
alpha_processing() >> alpha_reporting()
tenant_alpha_dag()
Connection and Variable Isolation
# tenant_isolation.py
from airflow.models import Connection, Variable
from airflow import settings
class TenantIsolation:
"""Manage tenant-specific connections and variables."""
def __init__(self, tenant_id):
self.tenant_id = tenant_id
self.prefix = f"{tenant_id}_"
def get_connection(self, conn_id):
"""Get tenant-specific connection."""
tenant_conn_id = f"{self.prefix}{conn_id}"
session = settings.Session()
conn = session.query(Connection).filter(
Connection.conn_id == tenant_conn_id
).first()
if not conn:
# Fall back to shared connection
conn = session.query(Connection).filter(
Connection.conn_id == conn_id
).first()
return conn
def get_variable(self, key, default=None):
"""Get tenant-specific variable."""
tenant_key = f"{self.prefix}{key}"
try:
return Variable.get(tenant_key)
except KeyError:
if default is not None:
return default
raise
def set_variable(self, key, value):
"""Set tenant-specific variable."""
tenant_key = f"{self.prefix}{key}"
Variable.set(tenant_key, value)
def list_connections(self):
"""List all connections for this tenant."""
session = settings.Session()
return session.query(Connection).filter(
Connection.conn_id.like(f"{self.prefix}%")
).all()
# Usage in DAGs
from airflow.decorators import task, dag
from datetime import datetime
@dag(
schedule_interval="@daily",
start_date=datetime(2024, 1, 1),
tags=['tenant:alpha'],
)
def tenant_aware_dag():
@task
def use_tenant_resources():
"""Use tenant-specific connections and variables."""
isolation = TenantIsolation('alpha')
# Get tenant-specific connection
conn = isolation.get_connection('database')
print(f"Using connection: {conn.conn_id}")
# Get tenant-specific variable
config = isolation.get_variable('config', default='{}')
print(f"Config: {config}")
return {"tenant": "alpha"}
use_tenant_resources()
tenant_aware_dag()
Resource Quota Best Practices
Start with reasonable limits â monitor usage and adjust based on actual needs
Implement chargeback â track resource usage per tenant for cost allocation
Set up alerts â notify when tenants approach quota limits
Review quarterly â adjust quotas based on changing team needs
Document quotas â ensure tenants understand their resource limits
Key Concepts Table
Isolation Layer
Method
Implementation
Granularity
DAG Access
RBAC + Tags
tags=['tenant:alpha']
Per-DAG
Resources
Pools
pool='alpha_pool'
Per-task
Credentials
Connections
conn_id='alpha_db'
Per-connection
Configuration
Variables
Variable.get('alpha_config')
Per-variable
Logs
Log routing
S3 prefix s3://logs/alpha/
Per-DAG
Network
Namespaces
K8s namespace airflow-alpha
Per-tenant
Code Examples
Tenant Management API
# tenant_management.py
from airflow import settings
from airflow.models import Connection, Variable, Pool
from sqlalchemy import text
class TenantManager:
"""Manage tenant provisioning and deprovisioning."""
def __init__(self):
self.session = settings.Session()
def provision_tenant(self, tenant_id, config):
"""Provision a new tenant with resources."""
# Create pool
pool = Pool(
pool=f'{tenant_id}_pool',
slots=config.get('max_concurrent_tasks', 32),
description=f'Pool for tenant {tenant_id}',
)
self.session.add(pool)
# Create connections
for conn_config in config.get('connections', []):
conn = Connection(
conn_id=f"{tenant_id}_{conn_config['name']}",
conn_type=conn_config['type'],
host=conn_config.get('host'),
login=conn_config.get('login'),
password=conn_config.get('password'),
schema=conn_config.get('schema'),
)
self.session.add(conn)
# Create variables
for key, value in config.get('variables', {}).items():
var = Variable(
key=f"{tenant_id}_{key}",
val=str(value),
)
self.session.add(var)
self.session.commit()
print(f"Tenant {tenant_id} provisioned successfully")
def deprovision_tenant(self, tenant_id):
"""Deprovision a tenant and clean up resources."""
# Delete pool
pool = self.session.query(Pool).filter(
Pool.pool == f'{tenant_id}_pool'
).first()
if pool:
self.session.delete(pool)
# Delete connections
connections = self.session.query(Connection).filter(
Connection.conn_id.like(f'{tenant_id}_%')
).all()
for conn in connections:
self.session.delete(conn)
# Delete variables
variables = self.session.query(Variable).filter(
Variable.key.like(f'{tenant_id}_%')
).all()
for var in variables:
self.session.delete(var)
self.session.commit()
print(f"Tenant {tenant_id} deprovisioned")
def get_tenant_usage(self, tenant_id):
"""Get resource usage for a tenant."""
# Pool usage
pool = self.session.query(Pool).filter(
Pool.pool == f'{tenant_id}_pool'
).first()
pool_usage = {
'total_slots': pool.slots if pool else 0,
'occupied': pool.occupied_slots if pool else 0,
'utilization': pool.occupied_slots / pool.slots if pool and pool.slots > 0 else 0,
}
# Task count
from airflow.models import TaskInstance
from datetime import datetime, timedelta
task_count = self.session.query(TaskInstance).filter(
TaskInstance.pool == f'{tenant_id}_pool',
TaskInstance.execution_date >= datetime.now() - timedelta(hours=24),
).count()
return {
'pool': pool_usage,
'tasks_24h': task_count,
}
if __name__ == "__main__":
manager = TenantManager()
# Provision new tenant
manager.provision_tenant('gamma', {
'max_concurrent_tasks': 64,
'connections': [
{'name': 'database', 'type': 'postgres', 'host': 'db.gamma.com'},
],
'variables': {'config': '{}'},
})
# Check usage
usage = manager.get_tenant_usage('gamma')
print(f"Tenant usage: {usage}")