Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Run multiple sites from one Django source

Run multiple sites from one Django source

Django는 하나의 코드로 여러 사이트를 운영할 수 있는 Site 프레임워크를 제공합니다. 이를 이용하면 같은 레이아웃을 가지지만 서로 다른 콘텐츠를 제공하는 두 개 이상의 사이트를 쉽게 만들 수 있습니다. 하지만 내용뿐만 아니라 레이아웃까지 다르게 가져가려면 Site 프레임워크를 확장할 필요가 있습니다. 이 발표에서는 Django의 Site 프레임워크에 관해 먼저 알아보고, Site 프레임워크를 미들웨어 수준에서 확장하여 하나의 코드 기반으로 여러 개의 사용자 정의 가능한 사이트를 운영한 경험과 방법을 이야기하고자 합니다. 각 사이트 별로 서로 다른 템플릿과 리소스를 사용하되, 특정 사이트에 템플릿이 없을 경우 기본(fallback) 템플릿을 사용하는 방법에 관해 알아볼 것입니다. 사이트 별로 urls.py 파일을 별도로 구성하는 방법 및 사이트 URL에 따라 settings.py 값을 오버로딩 하는 법도 이야기할 것입니다. 이를 통해, 코드 개발의 편의성을 유지하면서도 다양성을 가지는 여러 사이트를 운영하는 경험을 공유하려고 합니다.

51df149d8dc13916f924acf398e72234?s=128

Jonghyun Park

August 17, 2019
Tweet

Transcript

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

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

    발표자
  3. • 하나의 Django 소스를 이용해서 여러 개의 사이트 운영하기 •

    데이터 분리하기 - sites 프레임워크 • 설정 및 URL 분리하기 - middleware • 템플릿 불러오기 - custom template loader • 리소스 불러오기 - custom static finder 내용
  4. • 기본 뼈대는 동일하지만 기관 별로 다른 내용의 서비스가 필요한

    경우 • 래블업의 사례: 대학별 교육용 서비스를 별도 도메인으로 공급 • site1.service.com 사이트 사용자가 site2.service.com 에 올라간 학습 자료를 볼 수 있으면 안 됨 • 코드 베이스를 최대한 공유하여 빠르게 여러 서비스 제공 • 코드 개발의 편의성을 유지하면서 다양성을 가지는 사이트 운영 여러 사이트 운영이 필요한 경우
  5. • 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
  6. sites 프레임워크 데이터 분리하기

  7. • 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
  8. DB에서 site 지정 Site 1 domain name Entry id title

    body … sites Site 2 domain name Site 3 domain name 연결된 site에 관한 정보를 가지는 모델
  9. 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() 같이 사이트별 쿼리 가능
  10. 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
  11. • 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
  12. • 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') <Site: site2.mysite.com> /etc/hosts 127.0.0.1 site1.local 127.0.0.1 site2.local
  13. Middleware SITE_ID 동적으로 설정하기

  14. • Django의 미들웨어 지원 • 요청(request)/응답(response)을 가로채 특정 작업을 수행

    User View do work make response 미들웨어 (middleware) Request Response Middlewares Session Authentication CsrfView …
  15. 미들웨어 (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': ... 커스텀 미들웨어
  16. 사이트 구분을 위한 미들웨어 작성 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
  17. 사이트 구분을 위한 미들웨어 작성 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', } 개발 도메인일 경우 프로덕션 도메인으로 이름 변경
  18. 설정 및 URL 불러오기 Middleware continue...

  19. • 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)
  20. • 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
  21. Custom Template Loader 템플릿 불러오기

  22. 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', ], }, }, ]
  23. 사이트별 템플릿 구분하기 . ├── 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
  24. Custom Static Finder 리소스 불러오기

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

    ]
  26. 사이트별 리소스 불러오기 . ├── 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)
  27. 사이트별 리소스 불러오기 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
  28. 요약 . ├── 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 프레임워크 = + 하나의 소스 여러 사이트 서빙
  29. None
  30. 데모앱 데모 사이트: https://github.com/adrysn/multisite

  31. 감사합니다