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 값을 오버로딩 하는 법도 이야기할 것입니다. 이를 통해, 코드 개발의 편의성을 유지하면서도 다양성을 가지는 여러 사이트를 운영하는 경험을 공유하려고 합니다.

Jonghyun Park

August 17, 2019
Tweet

More Decks by Jonghyun Park

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  8. DB에서 site 지정
    Site 1
    domain
    name
    Entry
    id
    title
    body

    sites Site 2
    domain
    name
    Site 3
    domain
    name
    연결된 site에 관한 정보를 가지는 모델

    View full-size slide

  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() 같이 사이트별 쿼리 가능

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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')

    /etc/hosts
    127.0.0.1 site1.local
    127.0.0.1 site2.local

    View full-size slide

  13. Middleware
    SITE_ID 동적으로 설정하기

    View full-size slide

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

    View full-size slide

  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':
    ...
    커스텀 미들웨어

    View full-size slide

  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

    View full-size slide

  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',
    }
    개발 도메인일
    경우 프로덕션
    도메인으로 이름
    변경

    View full-size slide

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

    View full-size slide

  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)

    View full-size slide

  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

    View full-size slide

  21. Custom Template Loader
    템플릿 불러오기

    View full-size slide

  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',
    ],
    },
    },
    ]

    View full-size slide

  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

    View full-size slide

  24. Custom Static Finder
    리소스 불러오기

    View full-size slide

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

    View full-size slide

  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)

    View full-size slide

  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

    View full-size slide

  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 프레임워크
    =
    + 하나의 소스
    여러 사이트 서빙

    View full-size slide

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

    View full-size slide

  30. 감사합니다

    View full-size slide