angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

Deux Flask applications, front-end et admin, sur un même domaine avec DispatcherMiddleware

9 octobre 2019 à côté de Peter

En utilisant le middleware dispatcher de Werkzeug, nous combinons deux applications dans une application plus grande avec un dispatching basé sur un préfixe dans l'url.

post main image
unsplash.com/@ytcount

L' Flask application que j'écris pour exécuter ce site Web a tout le code dans une seule'application'. J'ai déjà fait quelques réorganisations car je voulais une séparation complète du code du frontend et du code d'administration. Il est maintenant temps de procéder à une séparation totale, c'est-à-dire de faire du frontend une Flask application et de l'admin une autre Flask application tout en exécutant les deux dans le même domaine et résidant dans le même répertoire du projet. Parce que nous ne voulons pas dupliquer le code et les données partagées entre les deux applications, nous créons un'répertoire partagé' où les éléments statiques, le modèle de données, etc. vivent.

La solution middleware du répartiteur n'utilise qu'une seule instance de gunicorn. Il y a probablement d'autres façons de le faire, par exemple avoir plusieurs instances de gunicorn, chacune servant une application mais je n'ai pas enquêté là-dessus.

Deux applications

Nous avons deux Flask applications dans le même répertoire de projet. L'un s'appelle frontend et l'autre s'appelle admin. Les deux applications s'exécutent sur le même domaine et le préfixe'admin' est utilisé pour envoyer des requêtes soit à l'application frontend soit à l'application admin. Supposons que le port est 5000 puis demande :

http://127.0.0.1:5000/

est envoyé à l'application front-end et la demande :

http://127.0.0.1:5000/admin

Avant d'appliquer le dispatching de l'application à l'application réelle, nous voulons d'abord tester si cela fonctionne vraiment. Pour cela, j'ai créé un environnement virtuel et j'ai installé et Gunicorn:

pip3 install flask
pip3 install gunicorn

Dans l'environnement virtuel, j'ai créé la structure de répertoire suivante. Ceci montre déjà les fichiers que je vais utiliser :


│
├── 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

Il y a deux applications, frontend et admin. L'application frontend se trouve dans le répertoire app_frontend. Il se compose d'un seul fichier __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

L'application admin se trouve dans le répertoire app_admin. Il est presque identique à l'application frontend. Dans les deux applications, j'ai codé en dur le nom de l'application pour m'assurer que nous voyons vraiment la bonne application :

# 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

Exécutez les deux applications avec Flaskle serveur de développement de's

Pour vérifier qu'ils peuvent être exécutés, j'ai créé deux fichiers, run_frontend.py et 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')

Dans le répertoire du projet, tapez la commande suivante :

python3 run_frontend.py

Ceci démarrera Flaskle serveur de développement de l'entreprise :

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)

Puis pointez votre navigateur vers :

http://127.0.0.1:5000/

et vous devriez voir le texte suivant :

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

Pour vérifier l'administrateur, tapez le répertoire du projet :

python3 run_admin.py

et pointez votre navigateur vers :

http://127.0.0.1:5000/

et vous devriez voir :

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

Ce n'était pas vraiment spécial, nous avons maintenant deux applications qui fonctionnent.

Exécuter les deux applications avec le serveur Gunicorn WSGI

Pour pouvoir exécuter les deux avec le Gunicorn serveur, j'ai à nouveau créé deux fichiers :

# wsgi_frontend.py

from run_frontend import frontend
# wsgi_admin.py

from run_admin import admin

Maintenant, nous exécutons le Gunicorn serveur. Je n'entre pas dans tous les détails ici, vous voudrez peut-être lire à propos Gunicorndes options de configuration de's, y compris le répertoire de travail /Python chemin. La chose la plus importante ici est que nous devons commencer Gunicorn à utiliser la voie absolue.

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

Le terminal devrait s'afficher :

[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

Pointez votre navigateur sur :

http://127.0.0.1:5000/

et vous devriez voir :

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

Vous pouvez faire la même chose pour l'administrateur. Rien de spécial, nous avons maintenant deux applications qui peuvent toutes les deux être servies par Gunicorn.

Application Dispatching avec Flaskle serveur de développement de's

Avec DispatcherMiddleware de Werkzeug, il est très facile de combiner les deux applications en une seule qui peut être servie par le serveur HTTP WSGI WSGI gunicorn. Ceci est décrit dans le Flask document Application Dispatching, voir références ci-dessous. Notez que DispatcherMiddleware a été déplacé de werkzeug.wsgi vers werkzeug.middleware.dispatcher à partir de Werkzeug 0.15. Encore une fois, nous voulons tester ce premier en utilisant le serveur de Flaskdéveloppement de `s. Pour cela j'ai créé un fichier run_both.py :

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

L'objet DispatcherMiddleware n'a pas de méthode'run'. Au lieu de cela, nous pouvons utiliser'run_simple'.

Après avoir démarré le serveur de développement :

python3 run_both.py

tu devrais voir ça :

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

Pointer notre navigateur vers :

http://127.0.0.1:5000/

nous voyons :

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

Et quand on pointe notre navigateur vers :

http://127.0.0.1:5000/admin/

nous voyons :

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

Génial, nous avons les deux applications fonctionnant sur un seul domaine, 127.0.0.0.1, et le préfixe envoie la requête soit à l'application frontend, soit à l'application admin.

Répartition des applications avec le Gunicorn serveur

Pour exécuter les deux applications avec le Gunicorn serveur, j'ai créé le fichier wsgi_both.py :

# wsgi_both.py

from run_both import application

Après le démarrage du Gunicorn serveur :

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

que montre le terminal :

[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

Maintenant encore une fois, en pointant le navigateur vers :

http://127.0.0.1:5000/

spectacles :

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

et en pointant le navigateur vers :

http://127.0.0.1:5000/admin/

spectacles :

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

script_root et chemins

Il est important de comprendre que lorsque l'url admin est appelée par le dispatcher, le script_root (request.script_root) de l'application passe de vide à'/admin'. De plus, le chemin (request.path) n'inclut pas'/admin'.

(Voir lien ci-dessous : )'Le chemin d'accès est le chemin d'accès à l'intérieur de votre application, sur lequel le routage est effectué. Le script_root est en dehors de votre application, mais est géré par url_for.

Parce que d'habitude nous n'utilisons url_for() que pour la génération d'url, il n'y aura pas de problème. Cependant, si vous utilisez un chemin d' Flask url, comme request.path, current_app.static_url_path, dans votre application, vous devez le préfixer avec le script_root. Un exemple d'utilisation du chemin d'accès dans un modèle, avant :

    {{ request.path }}

après :

    {{ request.script_root + request.path }}

A moins que vous ne sachiez ce que vous faites, essayez d'éviter d'utiliser le chemin Flask url directement dans le code et utilisez url_for().

Partage d'éléments statiques

Les billets de blog peuvent avoir une ou plusieurs images. Le front-end sert les images à partir de son dossier statique. L'administrateur contient des fonctions permettant de télécharger une image et d'assigner une image à un billet de blog. Pour garder les choses simples, j'ai choisi de déplacer le dossier statique dans le dossier où se trouvent les dossiers app_frontend et app_admin afin qu'il ne soit pas seulement partagé mais qu'il ait aussi l'air partagé.

La seule chose que nous devons changer pour que cela fonctionne est de passer le dossier static_folder lorsque l' Flask objet est créé :

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

Ceci n'est fait que pour le développement.

Partage des constantes et du modèle de données

Vous ne devez jamais dupliquer le code. Les constantes et le modèle de données sont parmi les premières choses que nous partageons entre frontend et admin. Nous avons mis l'app_constants.py et models.py dans le répertoire partagé. Ensuite, nous remplaçons les références à celles-ci dans les dossiers de candidature :

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

Partager les plans d'action

Un certain nombre de Blueprints peuvent être partagés entre frontend et admin. L'un est le Blueprint utilisé pour se connecter et se déconnecter. Un autre est le Blueprint de pages qui montre des pages. Partager des Blueprints est facile, nous créons simplement un répertoire'shared/blueprints' et le mettons blueprints ici. Dans la fonction create_app() du frontend et de l'admin dans __init__.py nous changeons :

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

à :

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

La vue fonctionne dans l' blueprints appel render_template , ce qui signifie que nous devons nous assurer que les modèles existent à la fois dans le frontend et dans l'admin. Plus tard, nous pouvons aussi changer le répertoire des modèles pour ces fichiers partagés blueprints.

Problèmes d'enregistreur

J'utilise le logger à la fois dans l'application frontend et dans l'application admin, l'application frontend journaux à un fichier app_frontend.log et l'application admin journaux à un fichier app_admin.log.

Après avoir utilisé DispatcherMiddleware, il est apparu qu'un message de journal quelconque était toujours écrit dans les deux fichiers journaux, les messages de l'application frontale étaient écrits dans app_frontend.log et app_admin.log, et les messages de l'application admin étaient écrits dans app_frontend.log et app_admin.log.

Il semble que cela a à voir avec le fait que app.logger a toujours le nom flask.app. Bien qu'il existe des moyens de contourner ce problème, il est préférable de mettre à jour Flask vers 1.1 (ou 1.1.1) où app.logger prend maintenant le même nom que app.name. Après la mise à niveau, la journalisation était séparée pour le frontend et l'admin.

Répertoire statique staging et production

Je me sers Gunicorn d'un Nginx inverseur proxyde sens inverse. Pour les pages cela fonctionne bien mais le répertoire statique n'a pas été mappé correctement. Les images n'étaient pas affichées en mode administrateur, c'est-à-dire sur l'url'/admin'. Je ne connais pas directement un autre moyen que d'ajouter une autre directive de localisation pour Nginx compenser le /admin. Alors, avant :

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

Et après avoir utilisé DispatcherMiddleware :

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

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

Résumé

L'utilisation de DispatcherMiddleware de Werkzeug facilite l'exécution de deux applications sur un même domaine. Pour le développement, c'est probablement une bonne idée d'utiliser deux serveurs de Flask développement, un pour l'application frontend et un pour l'application admin.

Les premières étapes de la mise en œuvre de cette fonctionnalité dans mon application m'ont donné peu de choses à corriger. Ce que j'ai fait, c'est déplacer le dossier app vers app_frontend et copier app_frontend vers app_admin. En mode développement, en a dû changer hostname='localhost' en hostname='0.0.0.0.0', mais c'est un problème lorsque vous utilisez docker. Ensuite, j'ai dû changer les noms des cookies de session pour éviter les conflits, supprimer'/admin' de l'url_prefix dans lablueprint fonction register_function, renommer les fichiers log, et changer l'emplacement des modèles,'from app import models' en'from app_frontend import models'. De nombreux changements peuvent être effectués en utilisant le remplacement de regex récursif, voir'regexxer' pour Linux/Ubuntu.

Puis il courait. Deux applications fonctionnant sur le même domaine et écrites par vous-même, sont-elles toujours entièrement séparées ? Je ne crois pas, non. J'ai introduit un dossier partagé, où nous avons mis le dossier statique. Le dossier partagé est également utilisé pour partager le modèle de données et du code entre le frontend et l'admin.

Il est temps de supprimer le code admin du frontend et le code du frontend de l'admin et de partager ce qui doit être partagé !

Liens / crédits

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/

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.