angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Twee Flask apps, frontend en admin, op één domein met behulp van DispatcherMiddleware

Met behulp van Werkzeug's dispatcher middleware combineren we twee apps tot een grotere met dispatching op basis van een prefix in de url.

9 oktober 2019
post main image
unsplash.com/@ytcount

De Flask applicatie die ik schrijf om deze website te beheren heeft alle code in één 'app'. Ik heb al wat gereorganiseerd omdat ik een volledige scheiding van frontend code en de administratie code wilde. Nu is het tijd voor een totale scheiding, wat betekent dat de frontend een Flask app wordt en de admin een andere Flask app terwijl beide in hetzelfde domein draaien en beide in dezelfde projectdirectory staan. Omdat we geen code en data willen dupliceren tussen beide apps creëren we een 'shared directory' waar de statische items, het datamodel etc. zich bevinden.

De dispatcher middleware-oplossing maakt slechts gebruik van één exemplaar van gunicorn. Waarschijnlijk zijn er andere manieren om dit te doen, bijvoorbeeld door meerdere gevallen van gunicorn te hebben, elk met een app, maar ik heb dit niet onderzocht.

Twee apps

We hebben twee Flask apps in dezelfde projectdirectory. De ene heet frontend en de andere heet admin. Beide apps draaien op hetzelfde domein en de prefix 'admin' wordt gebruikt om verzoeken naar de frontend app of de admin app te sturen. Veronderstel dat de haven 5000 is dan verzoek:

http://127.0.0.1:5000/

wordt naar de frontend app en aanvraag gestuurd:

http://127.0.0.1:5000/admin

wordt naar de admin app gestuurd. Alvorens de applicatie te verzenden naar de eigenlijke applicatie willen we eerst testen of deze echt werkt. Hiervoor heb ik een virtuele omgeving gecreëerd en geïnstalleerd Flask en Gunicorn:

pip3 install flask
pip3 install gunicorn

In de virtuele omgeving heb ik de volgende mappenstructuur gemaakt. Dit toont al de bestanden die ik zal gebruiken:


│
├── flask_dispatch
│   ├── bin
│   ├── include
│   ├── lib
│   ├── lib64
│   ├── share
│   ├── pyvenv.cfg
│   │
│   ├── project
│   │   ├── app_admin
│   │   │   └── __init__.py
│   │   ├── app_frontend
│   │   │   └── __init__.py
│   │   ├── __init__.py
│   │   ├── run_admin.py
│   │   ├── run_both.py
│   │   ├── run_frontend.py
│   │   ├── wsgi_admin.py
│   │   ├── wsgi_both.py
│   │   └── wsgi_frontend.py

Er zijn twee apps, frontend en admin. De frontend app staat in de directory app_frontend. Het bestaat uit slechts één bestand __init__.py:

# app_frontend/__init__.py

from flask import Flask, request

def create_app():
    app_name = 'frontend'
    print('app_name = {}'.format(app_name))

    # create app
    app = Flask(__name__, instance_relative_config=True)

    @app.route("/")
    def hello():
        return 'Hello ' + app_name + '! request.url = ' + request.url
    
    # return app
    return app

De admin app staat in de directory app_admin. Het is bijna identiek aan de frontend app. In beide apps heb ik de naam van de applicatie gecodeerd om er zeker van te zijn dat we echt de juiste app zien:

# app_admin/__init__.py

from flask import Flask, request

def create_app():
    app_name = 'admin'
    print('app_name = {}'.format(app_name))

    # create app
    app = Flask(__name__, instance_relative_config=True)

    @app.route("/")
    def hello():
        return 'Hello ' + app_name + '! request.url = ' + request.url
    
    # return app
    return app

Voer de twee apps uit met Flask's ontwikkelingsserver

Om te controleren of ze kunnen draaien heb ik twee bestanden gemaakt, run_frontend.py en run_admin.py:

# run_frontend.py

# import frontend
from app_frontend import create_app as app_frontend_create_app
frontend = app_frontend_create_app()

if __name__ == '__main__':
    frontend.run(host='0.0.0.0')
# run_admin.py

# import admin
from app_admin import create_app as app_admin_create_app
admin = app_admin_create_app()

if __name__ == '__main__':
    admin.run(host='0.0.0.0')

Typ in de projectdirectory het volgende commando in:

python3 run_frontend.py

Dit zal 's ontwikkelingsserver starten Flask:

app_name = frontend
 * Serving Flask app "app_frontend" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Richt uw browser dan op:

http://127.0.0.1:5000/

en je zou de volgende tekst moeten zien:

Hello frontend! request.url = http://127.0.0.1:5000/

Om de admin te controleren, typt u in de projectdirectory:

python3 run_admin.py

en wijs je browser aan:

http://127.0.0.1:5000/

en je zou het moeten zien:

Hello admin! request.url = http://127.0.0.1:5000/

Dit was niet echt bijzonder, we hebben nu twee werkende apps.

Voer de twee apps uit met de Gunicorn WSGI-server

Om de twee te kunnen draaien met de Gunicorn server heb ik weer twee bestanden gemaakt:

# wsgi_frontend.py

from run_frontend import frontend
# wsgi_admin.py

from run_admin import admin

Nu draaien we de Gunicorn server. Ik ga hier niet op alle details in, u wilt misschien lezen over Gunicorn's configuratie opties, inclusief de werkmap /Python pad. Het belangrijkste hier is dat we de absolute weg moeten gaan Gunicorn gebruiken.

/var/www/.../flask_dispatch/bin/gunicorn -b :5000 wsgi_frontend:frontend

De terminal zou het moeten laten zien:

[2019-10-09 11:07:31 +0200] [28073] [INFO] Starting gunicorn 19.9.0
[2019-10-09 11:07:31 +0200] [28073] [INFO] Listening at: http://0.0.0.0:5000 (28073)
[2019-10-09 11:07:31 +0200] [28073] [INFO] Using worker: sync
[2019-10-09 11:07:31 +0200] [28076] [INFO] Booting worker with pid: 28076
app_name = frontend

Richt uw browser op:

http://127.0.0.1:5000/

en je zou het moeten zien:

Hello frontend! request.url = http://127.0.0.1:5000/

U kunt hetzelfde doen voor de admin. Niets bijzonders, we hebben nu twee apps die beide kunnen worden bediend door Gunicorn.

Applicatie Dispatching met Flask's ontwikkelingsserver

Met behulp van Werkzeug's DispatcherMiddleware is het zeer eenvoudig om beide apps te combineren tot één die bediend kan worden door de gunicorn WSGI HTTP server. Dit wordt beschreven in het Flask document Application Dispatching, zie onderstaande referenties. Merk op dat DispatcherMiddleware vanaf Werkzeug.wsgi is verhuisd van werkzeug.wsgi naar werkzeug.middleware.dispatcher met ingang van Werkzeug 0.15. Ook dit willen we eerst testen met behulp van 's Flaskdevelopment server. Hiervoor heb ik een bestand run_both.py gemaakt:

# run_both.py

from werkzeug.middleware.dispatcher import DispatcherMiddleware
from werkzeug.serving import run_simple

# import frontend
from app_frontend import create_app as app_frontend_create_app
frontend = app_frontend_create_app()

# import admin
from app_admin import create_app as app_admin_create_app
admin = app_admin_create_app()

# merge
application = DispatcherMiddleware(
    frontend, {
    '/admin': admin
})

if __name__ == '__main__':
    run_simple(
        hostname='localhost',
        port=5000,
        application=application,
        use_reloader=True,
        use_debugger=True,
        use_evalex=True)

Het DispatcherMiddleware-object heeft geen methode 'run'. In plaats daarvan kunnen we 'run_simple' gebruiken.

Na het starten Flaskvan 's ontwikkelingsserver:

python3 run_both.py

Je zou dit moeten zien:

app_name = frontend
app_name = admin
 * Running on http://localhost:5000/ (Press CTRL+C to quit)
 * Restarting with stat
app_name = frontend
app_name = admin
 * Debugger is active!
 * Debugger PIN: 136-162-082

Onze browser aanwijzen:

http://127.0.0.1:5000/

zien we:

Hello frontend! request.url = http://127.0.0.1:5000/

En als we onze browser op onze browser richten...:

http://127.0.0.1:5000/admin/

zien we:

Hello admin! request.url = http://127.0.0.1:5000/admin/

Geweldig, we hebben beide apps draaien op een enkel domein, 127.0.0.0.1, en de prefix stuurt het verzoek naar de frontend app of de admin app.

Applicatie Verzending met de Gunicorn server

Om beide applicaties te draaien met de Gunicorn server heb ik het bestand wsgi_both.py aangemaakt:

# wsgi_both.py

from run_both import application

Na het starten van de Gunicorn server:

/var/www/.../flask_dispatch/bin/gunicorn -b :5000 wsgi_both:application

de terminal laat het zien:

[2019-10-09 11:17:25 +0200] [28508] [INFO] Starting gunicorn 19.9.0
[2019-10-09 11:17:25 +0200] [28508] [INFO] Listening at: http://0.0.0.0:5000 (28508)
[2019-10-09 11:17:25 +0200] [28508] [INFO] Using worker: sync
[2019-10-09 11:17:25 +0200] [28511] [INFO] Booting worker with pid: 28511
app_name = frontend
app_name = admin

En nu weer, de browser aanwijzen:

http://127.0.0.1:5000/

shows:

Hello frontend! request.url = http://127.0.0.1:5000/

en de browser aan te wijzen:

http://127.0.0.1:5000/admin/

shows:

Hello admin! request.url = http://127.0.0.1:5000/admin/

script_root en paden

Het is belangrijk om te begrijpen dat wanneer de beheerder url wordt aangeroepen door de dispatcher, de script_root (request.script_root) van de applicatie verandert van leeg naar '/admin'. Ook het pad (request.path) bevat geen '/admin'.

(Zie de link hieronder: ) "Het pad is het pad binnen uw toepassing, waarop de routering wordt uitgevoerd. De script_root staat buiten uw toepassing, maar wordt behandeld door url_for.'.

Omdat we meestal alleen url_for() gebruiken voor het genereren van url zal er geen probleem zijn. Als u echter een Flask urlpad, zoals request.path, current_app.static_url_path, in uw toepassing gebruikt, moet u dit vooraf met de script_root instellen. Een voorbeeld van het gebruik van het pad in een sjabloon, eerder:

    {{ request.path }}

daarna:

    {{ request.script_root + request.path }}

Tenzij je weet wat je aan het doen bent, probeer het Flask url-pad niet direct in code te gebruiken en gebruik url_for().

Statische items delen

Blogberichten kunnen één of meer afbeeldingen bevatten. De voorkant dient voor de beelden uit de statische map. De admin bevat functies voor het uploaden van een afbeelding en het toewijzen van een afbeelding aan een blogbericht. Om het eenvoudig te houden heb ik ervoor gekozen om de statische map te verplaatsen naar de map waar de app_frontend en app_admin mappen staan zodat het niet alleen gedeeld wordt maar er ook gedeeld uitziet.

Het enige wat we moeten veranderen om dit te laten werken is de static_folder passeren wanneer het Flask object wordt gemaakt:

    app = Flask(__name__, 
        instance_relative_config=True,
        static_folder='/home/flask/project/shared/static')

Dit wordt alleen gedaan voor ontwikkeling.

Het delen van constanten en datamodellen

Je moet nooit de code dupliceren. De constanten en het datamodel zijn een van de eerste dingen die we delen tussen frontend en admin. We hebben de app_constants.py en modellen.py in de gedeelde directory gezet. Daarna vervangen we de verwijzingen naar hen in de aanvraagdossiers:

from shared.app_constants import *
from shared.models import <classes to import>

Blueprints delen

Een aantal Blueprints kan gedeeld worden tussen frontend en admin. Een daarvan is de auth Blueprint die gebruikt wordt om in en uit te loggen. Een andere is de Blueprint die de pagina's toont. Blueprints delen is eenvoudig, we maken gewoon een 'shared/blueprints' directory aan en zetten deze blueprints hier. In de frontend en admin's create_app() functie in __init__.py veranderen we:

    from .blueprints.auth.views import auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>')

aan:

    from shared.blueprints.auth.views import auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>')

De weergave functioneert in de blueprints oproep render_template , wat betekent dat we ervoor moeten zorgen dat de sjablonen zowel in de frontend als in de admin aanwezig zijn. Later kunnen we voor deze gedeelde blueprints.

Problemen met het houtbedrijf

Ik gebruik de logger zowel in de frontend app als in de admin app, de frontend app logt naar een bestand app_frontend.log en de admin app logt naar een bestand app_admin.log.

Na het gebruik van DispatcherMiddleware bleek dat er altijd een logbericht naar beide logbestanden werd geschreven, berichten uit de frontend app werden geschreven naar app_frontend.log en app_admin.log, en berichten uit de admin app werden geschreven naar app_frontend.log en app_admin.log.

Het blijkt dat dit te maken heeft met het feit dat app.logger altijd de naam flask.app heeft. Hoewel er manieren zijn om dit te omzeilen, is het beter om te upgraden Flask naar 1.1 (of 1.1.1.1) waar app.logger nu dezelfde naam heeft als app.name. Na de upgrade werd de logging gescheiden voor de frontend en de admin.

Statische directory voor staging en productie

Ik gebruik Gunicorn met een Nginx omgekeerde proxy. Voor de pagina's werkt dit prima, maar de statische directory is niet goed in kaart gebracht. Afbeeldingen werden niet getoond in de admin mode, d.w.z. op de url '/admin'. Ik weet niet direct een andere manier om een andere locatie richtlijn toe te voegen om de /admin te Nginx compenseren. Dus daarvoor:

  location /static/ {
    alias /var/www/.../static/;
  }

En na het gebruik van DispatcherMiddleware...:

  location /admin/static/ {
    alias /var/www/.../static/;
  }

  location /static/ {
    alias /var/www/.../static/;
  }

Samenvatting

Door gebruik te maken van Werkzeug's DispatcherMiddleware is het eenvoudig om twee apps op één domein te draaien. Voor ontwikkeling is het waarschijnlijk een goed idee om twee Flask ontwikkelingsservers te gebruiken, één voor de frontend app en één voor de admin app.

De eerste stappen die ik in mijn aanvraag heb gezet om dit in praktijk te brengen, hebben weinig dingen opgeleverd die moesten worden opgelost. Wat ik deed was de app-map verplaatsen naar app_frontend en app_frontend kopiëren naar app_admin. In ontwikkelingsmodus moest hostname='localhost' in hostname='0.0.0.0.0.0' veranderen, maar dit is een probleem bij het draaien met docker. Vervolgens moest ik de sessiecookienamen wijzigen om conflicten te voorkomen, '/admin' uit de url_prefix in deblueprint register_functie verwijderen, de logbestanden een andere naam geven en de locatie van de modellen wijzigen, 'van app-importmodellen' in 'van app-importmodellen'. Veel wijzigingen kunnen worden aangebracht door gebruik te maken van recursieve regexvervanging, zie 'regexxer' voor Linux/Ubuntu.

Toen was het aan het rennen. Zijn twee apps die op hetzelfde domein draaien en door u zelf geschreven zijn, altijd volledig gescheiden? Ik denk het niet. Ik heb een gedeelde map geïntroduceerd, waar we de statische map hebben geplaatst. De gedeelde map wordt ook gebruikt om het datamodel en wat code te delen tussen frontend en admin.

Tijd om de admin code uit de frontend en frontend code van de admin te verwijderen en te delen wat er gedeeld moet worden!

Links / credits

Add a prefix to all Flask routes
https://stackoverflow.com/questions/18967441/add-a-prefix-to-all-flask-routes

Application Dispatching
https://flask.palletsprojects.com/en/1.1.x/patterns/appdispatch/

DispatcherMiddleware with different loggers per app in flask 1.0 #2866
https://github.com/pallets/flask/issues/2866

Flask 1.1 Released
https://palletsprojects.com/blog/flask-1-1-released/

How do I run multiple python apps in 1 command line under 1 WSGI app?
https://www.slideshare.net/onceuponatimeforever/how-do-i-run-multiple-python-apps-in-1-command-line-under-1-wsgi-app

How to implement Flask Application Dispatching by Path with WSGI?
https://stackoverflow.com/questions/30906489/how-to-implement-flask-application-dispatching-by-path-with-wsgi

request.path doesn't include request.script_root when running under a subdirectory #3032
https://github.com/pallets/flask/issues/3032

Serving WSGI Applications
https://werkzeug.palletsprojects.com/en/0.15.x/serving/

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen (3)

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.

avatar

Very helpful. Thanks.
One thing:
http://127.0.0.1:8000/en/blog/two-flask-apps-frontend-and-admin-on-one-domain-using-dispatchermiddleware
should be
https://www.peterspython.com/en/blog/two-flask-apps-frontend-and-admin-on-one-domain-using-dispatchermiddleware

avatar
user59176594 3 jaar geleden

I would like to integrate several DashApps into a website running under Flask (the html frame comes from Flask, the DashApp should be embedded in the html frame). I want to avoid the iframe method.
The DashApps should not create their own Flask instance, as they normally do, but should be transferred to the Flask instance of the running website.
Can that be done with the Dispatcher Middleware?

avatar
peter 3 jaar geleden user59176594

I do not think this is possible (without iframes) because DispatcherMiddleware is used for a different purpose. Did you check this information on the plotly site: https://dash.plotly.com/integrating-dash