Purumir's Blog

Machine Learning, SW architect, Management, favorites

Flask Multitenancy Class Routing

Flask를 통해서 Multitenancy를 구현하는 방법에 대해서 이야기 해보고자 합니다. 제가 사용한 방법보다 더 효과적인 방법이 있을수 있으며, 이 방식은 그 중 하나의 방법 정도로 살펴봐 주시면 좋을듯 합니다. 이 문서는 몇개의 데이터 소스를 사전 등록하고 이 안에서 class routing을 tenant에 따라서 수행하는 방법을 다룹니다.

Multitenancy Pattern

https://learn.microsoft.com/en-us/azure/azure-sql/database/saas-tenancy-app-design-patterns?view=azuresql

위 글에서 제시하는 패턴 중 “D. Multitenant app with database-per-tenant”에 대해서 다루고자 합니다.

Flask-SQLAlchemy에서 Multi-datasource 설정

https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/binds/

위 Flask-SQLAlchemy의 가이드 문서를 보면 Flask에 다중 데이터 소스를 등록할 수 있습니다. SQLALCHEMY_DATABASE_URI는 서비스에서 사용하는 공통 database로 사용하고, SQLALCHEMY_BINDS는 각 tenant별 데이터 소스를 등록합니다.

config.py에서의 데이터 소스

1
2
3
4
5
6
7
# MAIN Datasource
SQLALCHEMY_DATABASE_URI = "postgresql:///{db_username}:{db_password}@{db_host}:5432/{db_name}"
# Per Tenant Datasource
SQLALCHEMY_BINDS = {
"tenant1_ds": "postgresql:///{db_username}:{db_password}@{db_host}:5432/{db_name}",
"tenant2_ds": "postgresql:///{db_username}:{db_password}@{db_host}:5432/{db_name}",
"tenant3_ds": "postgresql:///{db_username}:{db_password}@{db_host}:5432/{db_name}",

Multitenancy Model Routing

Flask App기동시 Model Initialize 최초 수행하기

1
2
3
# Flask App 기동시 최초 1회 수행합니다.
with app.app_context():
initialize_multi_tenant_model()
1
2
3
4
5
6
7
class Tenants(Enum):
"""
# tenant 목록을 Enum으로 선언합니다.
"""
TENANT1 = 'tenant1'
TENANT2 = 'tenant2'
TENANT3 = 'tenant3'
1
2
3
4
5
# Flask가 기동하면서 로딩할 base model class를 지정합니다.
base_models = [
# base model을 지정합니다.
InBoundClassInfo
]
1
2
3
4
5
6
7
8
9
10
11
12
class InBoundClassInfo(db.Model):
"""
__abstract__ 를 넣어야 tenant별로 class를 동적으로 생성가능합니다.
"""
__abstract__ = True
__tablename__ = 'in_bound_class_info'

field1 = db.Column(db.String(50), primary_key=True)
field2 = db.Column(db.String(50), nullable=True)

def __repr__(self):
return '<%r %r>' % (self.__tablename__, self.field1)
1
2
3
4
5
6
7
8
9
10
def initialize_multi_tenant_model():
"""
Flask App Context에서 이 메소드를 실행하여 tenant별로 class를 생성하여 등록합니다.
"""
tenants= [tenant.value for tenant in tenants]

for tenant in tenants:
for base_model in base_models:
# camel_to_snake는 base_model의 CamelCase를 Snake Case로 만드는 함수를 만드시면 됩니다.
initialize_model(tenant, base_model, camel_to_snake(base_model.__name__))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def initialize_model(tenant_nm, base_cls, tbl_nm):
"""
파라미터로 전달된 정보를 기반으로 Model을 생성하여 dict에 등록합니다.
"""
# tenant1_svc_in_bound_class_info -> Tenant1SvcInBoundClassInfo로 리턴하는 함수(string_transform)를 사용합니다.
cls_nm = string_transform(tenant_nm + '_svc_' + tbl_nm )
Model = type(cls_nm, (base_cls,),{
# SQLALCHEMY_BINDS 에서 선언한 datasource와 일치하게 됩니다.
'__bind_key__': tenant_nm + '_ds',
# InBoundClassInfo의 경우 in_bound_class_info로 테이블이 생성되었다고 가정합니다.
'__tablename__': tbl_nm,
# 해당 datasource에서 schema 정보를 기술합니다.
'__table_args__': {'schema': tenant_nm + '_ds'}
})
# Tenant1SvcInBoundClassInfo, Tenant2SvcInBoundClassInfo, Tenant3SvcInBoundClassInfo라는 이름으로 모델을 dict에 등록합니다.
flask_mdl_dict[cls_nm] = Model
1
2
3
4
5
6
7
8
def get_model_class_per_tenant(tenant, base_cls_nm):
"""
해당 tenant + base model 정보를 기반으로 해당 tenant의 클래스를 리턴합니다.
"""
cls_nm = string_transform(tenant_nm + '_svc_' + tbl_nm )
model_cls = flask_mdl_dict.get(cls_nm)

return model_cls

위와 같이 설정하고 나서 Flask의 서비스 클래스 등에서 다음과 같이 호출하여 db model을 사용하면 됩니다.

1
2
model_cls = get_model_class_per_tenant(tenant, "in_bound_class_info")
in_bound_class_info = db.session.query(model_cls).filter(model_cls.field1 == 'ONE').one()

database-per-tenant에 해당하는 Flask에서 구성가능한 Multienancy 패턴을 기술해 보았습니다. 위와 같이 구성할 경우 테넌트별로 각각 db Model을 별도로 선언하고 코딩해야 하는 코드 중복을 줄일 수 있습니다.