Slide 1

Slide 1 text

PyCon Korea 2019 하나의 Django 코드로 여러 사이트 운영하기 박종현

Slide 2

Slide 2 text

• 래블업 주식회사 • Python, Django, Polymer/LitElement • 오픈소스: https://github.com/lablup/backend.ai 발표자

Slide 3

Slide 3 text

• 하나의 Django 소스를 이용해서 여러 개의 사이트 운영하기 • 데이터 분리하기 - sites 프레임워크 • 설정 및 URL 분리하기 - middleware • 템플릿 불러오기 - custom template loader • 리소스 불러오기 - custom static finder 내용

Slide 4

Slide 4 text

• 기본 뼈대는 동일하지만 기관 별로 다른 내용의 서비스가 필요한 경우 • 래블업의 사례: 대학별 교육용 서비스를 별도 도메인으로 공급 • site1.service.com 사이트 사용자가 site2.service.com 에 올라간 학습 자료를 볼 수 있으면 안 됨 • 코드 베이스를 최대한 공유하여 빠르게 여러 서비스 제공 • 코드 개발의 편의성을 유지하면서 다양성을 가지는 사이트 운영 여러 사이트 운영이 필요한 경우

Slide 5

Slide 5 text

• Overload 하위에 기본 사이트 이외 설정 및 자원 저장 장난감 앱 구조 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ ├── settings.py │ └── urls.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ └── urls.py └── templates └── base.html 데모 사이트: https://github.com/adrysn/multisite

Slide 6

Slide 6 text

sites 프레임워크 데이터 분리하기

Slide 7

Slide 7 text

• DB 수준에서 사이트 지정 • 웹사이트의 domain과 name을 지정 sites 프레임워크 class Site(models.Model): domain = models.CharField( _('domain name'), max_length=100, validators=[_simple_domain_name_validator], unique=True, ) name = models.CharField(_('display name'), max_length=50) https://github.com/django/django/blob/master/django/contrib/sites/models.py

Slide 8

Slide 8 text

DB에서 site 지정 Site 1 domain name Entry id title body … sites Site 2 domain name Site 3 domain name 연결된 site에 관한 정보를 가지는 모델

Slide 9

Slide 9 text

DB에서 site 지정 class Entry(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) title = models.CharField(max_length=50) body = models.TextField() created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) sites = models.ManyToManyField(Site, related_name='entries', blank=True) objects = models.Manager() on_site = CurrentSiteManager() Entry.on_site.all() 같이 사이트별 쿼리 가능

Slide 10

Slide 10 text

View에서 사용 예 from django.contrib.sites.shortcuts import get_current_site def my_view(request): entry = Entry.on_site.get(title='hello world') current_site = get_current_site(request) if current_site.domain == 'foo.com': # Do something for site foo.com pass else: # Do something else for other sites pass

Slide 11

Slide 11 text

• django.contrib.sites → INSTALLED_APPS • SITE_ID = 1 in settings (default site) •migrate sites 프레임워크 활성화 $ python manage.py migrate multisite_django | Operations to perform: multisite_django | Apply all migrations: admin, auth, contenttypes, sessions, sites multisite_django | Running migrations: multisite_django | Applying sites.0001_initial... OK multisite_django | Applying sites.0002_alter_domain_unique... OK . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ ├── settings.py │ └── urls.py └── templates └── base.html

Slide 12

Slide 12 text

• site1: site1.local -> site1.mysite.com • site2: site2.local -> site2.mysite.com sites 프레임워크 활성화 $ ./manage.py shell >>> from django.contrib.sites.models import Site >>> site1 = Site.objects.first() >>> site1.domain = 'site1.mysite.com' >>> site1.name = 'Site 1' >>> site1.save() >>> >>> Site.objects.create(domain='site2.mysite.com', name='Site 2') /etc/hosts 127.0.0.1 site1.local 127.0.0.1 site2.local

Slide 13

Slide 13 text

Middleware SITE_ID 동적으로 설정하기

Slide 14

Slide 14 text

• Django의 미들웨어 지원 • 요청(request)/응답(response)을 가로채 특정 작업을 수행 User View do work make response 미들웨어 (middleware) Request Response Middlewares Session Authentication CsrfView …

Slide 15

Slide 15 text

미들웨어 (middleware) MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware’, ... 'multisite.middlewares.MultiSiteMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware', ] request.site == 현재 사이트 def my_view(request): current_site = get_current_site(request) if current_site.domain == 'foo.com': ... def my_view(request): if request.site.domain == 'foo.com': ... 커스텀 미들웨어

Slide 16

Slide 16 text

사이트 구분을 위한 미들웨어 작성 class MultiSiteMiddleware: def __init__(self, get_response): self.get_response = get_response # One-time configuration and initialization. # Called only when web server starts def __call__(self, request): # Code to be executed for each request before # the view (and later middleware) are called. response = self.get_response(request) # Code to be executed for each request/response after # the view is called. return response . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ └── middlewares.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ └── urls.py └── templates └── base.html

Slide 17

Slide 17 text

사이트 구분을 위한 미들웨어 작성 def __call__(self, request): domain, _ = split_domain_port(request.get_host()) # Get the production domain corresponding to the current site. if settings.DEBUG: try: domain = MultiSiteMiddleware.OVERLOADING_MATCHING_TABLE[domain] except KeyError: raise ImproperlyConfigured( f'No matching overloaded domain for {domain}' ) # Get current site object and set SITE_ID. try: current_site = Site.objects.get(domain=domain) except Site.DoesNotExist: current_site = Site.objects.get(id=settings.DEFAULT_SITE_ID) request.current_site = current_site settings.SITE_ID = current_site.id settings.site_domain = domain 요청이 들어온 도메인으로 사이트를 구분 OVERLOADING_MATCHING_TABLE = { 'site1.local': 'site1.mysite.com', 'site2.local': 'site2.mysite.com', } 개발 도메인일 경우 프로덕션 도메인으로 이름 변경

Slide 18

Slide 18 text

설정 및 URL 불러오기 Middleware continue...

Slide 19

Slide 19 text

• overload 폴더 내에 사이트 도메인과 일치하는 이름의 폴더가 있을 경우 그 하위 설정 파일을 찾아 적용. 없으면 기본 사이트 설정 적용. 사이트별 설정 적용 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ └── middlewares.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ └── urls.py └── templates └── base.html def __call__(self, request): ... # Determine the module path which contains site-wise assets. site_path = settings.site_domain.replace('.', '-') # Override settings. try: new_settings = import_module(f'overload.{site_path}.settings') except (ModuleNotFoundError, ImportError): new_settings = import_module(f'overload.default_settings') finally: for field in self.OVERRIDING_FIELDS: # Override fields only in OVERRIDING_FIELDS. if hasattr(new_settings, field): new_value = getattr(new_settings, field) setattr(settings, field, new_value)

Slide 20

Slide 20 text

• overload 폴더 내에 사이트 도메인과 일치하는 이름의 폴더가 있을 경우 그 하위 URL 파일을 찾아 적용. 없으면 기본 사이트 URL 사용. URL 구분하기 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ └── middlewares.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ └── urls.py └── templates └── base.html def __call__(self, request): ... # Override URL patterns. try: override_urls = import_module(f'overload.{site_path}.urls') if hasattr(override_urls, 'urlpatterns'): request.urlconf = f'overload.{site_path}.urls' except ImportError: # fallback to the default URL pass response = self.get_response(request) return response

Slide 21

Slide 21 text

Custom Template Loader 템플릿 불러오기

Slide 22

Slide 22 text

Template Loader 설정 추가 TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': False, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'multisite.context_processors.common_settings', ], 'loaders': [ 'multisite.template_loaders.MultiSiteLoader', 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ], }, }, ]

Slide 23

Slide 23 text

사이트별 템플릿 구분하기 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ │ └── entry_list.html │ ├── urls.py │ └── views.py ├── multisite │ ├── middlewares.py │ └── template_loaders.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ ├── templates │ │ └── entry │ │ └── entry_list.html │ └── urls.py └── templates └── base.html class MultiSiteLoader(Loader): def get_template_sources(self, template_name): domain = settings.site_domain template_dir = (Path(settings.BASE_DIR) / 'overload' / domain.replace('.', '-') / 'templates').resolve() template_dir = str(template_dir) name = safe_join(template_dir, template_name) yield Origin( name=name, template_name=template_name, loader=self, ) def get_contents(self, origin): try: with open(origin.name, encoding=self.engine.file_charset) as fp: return fp.read() except FileNotFoundError: raise TemplateDoesNotExist(origin) Overload 하위에 위치하는 template 파일을 로드 template_name -> Origin Origin -> actual contents

Slide 24

Slide 24 text

Custom Static Finder 리소스 불러오기

Slide 25

Slide 25 text

Static Finder 설정 추가 STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'multisite.static_finders.MultiSiteFinder', ]

Slide 26

Slide 26 text

사이트별 리소스 불러오기 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ ├── middlewares.py │ ├── static_finders.py │ └── template_loaders.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ ├── static │ │ └── logo2.png │ ├── templates │ │ └── entry │ └── urls.py └── templates └── base.html Overload 하위에 위치하는 리소스 파일을 로드 class MultiSiteFinder(BaseFinder): def __init__(self, app_names=None, *args, **kwargs): # List of locations with static files self.locations = [] # Maps dir paths to an appropriate storage instance self.storages = OrderedDict() self.overload_base_directory = os.path.join(BASE_DIR, 'overload') if not os.path.exists(self.overload_base_directory): raise ImproperlyConfigured('There is no overload static directory.’) for overload_dir_info in next(os.walk(self.overlaoad_base_directory))[1]: static_dir = os.path.join(BASE_DIR, 'overload', overload_dir_info, 'static') if os.path.isdir(static_dir): root = static_dir prefix = '' if (prefix, root) not in self.locations: self.locations.append((prefix, root)) for prefix, root in self.locations: filesystem_storage = FileSystemStorage(location=root) filesystem_storage.prefix = prefix self.storages[root] = filesystem_storage super().__init__(*args, **kwargs)

Slide 27

Slide 27 text

사이트별 리소스 불러오기 def find(self, path, all=False): """Looks for files in the overload directories.""" matches = [] for prefix, root in self.locations: if root not in searched_locations: searched_locations.append(root) matched_path = self.find_location(root, path, prefix) if matched_path: if not all: return matched_path matches.append(matched_path) return matches

Slide 28

Slide 28 text

요약 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry/entries.html │ ├── urls.py │ └── views.py ├── multisite │ ├── middlewares.py │ ├── static_finders.py │ ├── settings.py │ ├── template_loaders.py │ └── urls.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ ├── static │ │ └── site2-mysite-com │ ├── templates │ │ └── entry/entries.html │ └── urls.py └── templates └── base.html Site 프레임워크 = + 하나의 소스 여러 사이트 서빙

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

데모앱 데모 사이트: https://github.com/adrysn/multisite

Slide 31

Slide 31 text

감사합니다