====== Django Web Framework ====== Django és un //framework// per a aplicacions web en Python. És conegut com **//The framework for perfectionists with deadlines//** per la seva rapidesa de desenvolupament. En aquest article tractarem els temes de l'entorn d'execució d'un projecte Django, per tal que es pugui posar en producció de forma segura. Django té una documentació excel·lent, en particular el [[https://docs.djangoproject.com/en/stable/intro/tutorial01/|Tutorial Django]]. {{ django-logo.png?300 }} Referències: * Web oficial: https://www.djangoproject.com * El magnífic [[https://docs.djangoproject.com/en/stable/intro/tutorial01/|tutorial de la web oficial]]. * Un altre bon [[https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django|tutorial de MDN Mozilla Web Docs]]. {{tag> #FpInfor #Daw #DawMp07 #DawMp07Uf02 #DawMp07Uf2 #DawMp07Uf02 #Ciber #Ceti #CiberMp03 #CetiMp03 django framework python web DevOps }} \\ ===== Articles sobre Django en aquesta wiki ===== {{topic> django }} \\ ===== Introducció per a DevOps ===== Desenvolupar un projecte (en Django o en molts altres //frameworks//) és una tasca força diferent a la de construir la infraestructura correcta per a posar el projecte en producció, ja sigui directament en un servidor propi o de forma //dockeritzada// en contenidors. Això darrer és el què farem en aquest article, que és la feina més pròpia del DevOps. Prepar un projecte Django per a què sigui segur implica diverses coses: - Configurar un virtualenv amb les llibreries adequades (requirements.txt). - Crear el projecte amb les versions adequades i actualitzades. - Tenir el projecte sota un [[VCS]] (sistema de versionat) com [[Git]]. - Configurar les dades sensibles del projecte (contrasenyes, tokens, etc.) perquè no es publiquin al repositori de codi. - Habilitar configuració de l'app mitjançant variables d'entorn, enlloc de fitxers. - Fer un README per facilitar la clonació i posta en marxa del projecte. - Configurar la BD adequadament. - Configurar el servidor d'arxius estàtics. - Posar el projecte en producció amb un servidor d'aplicacions (uWSGI) + un servidor web (Nginx) per als arxius estàtics. \\ ===== Entorn Python Venv ===== Per treballar en Python ens convé una eina d'entorn que ens aïlli les diferents versions de llibreries que farem servir, i no tenir incompatibilitats entre les diferents aplicacions que puc tenir (ni tampoc amb el sistema operatiu, que té les seves pròpies versions de les llibreries). A l'article [[Python venv]] teniu detalls a fons de com utilitzar aquestes eines. Resumidament, necessitem crear l'entorn virtual, entrar-hi i instal·lar Django: $ python3 -m venv env $ source env/bin/activate (env) $ pip install django \\ ===== Quickstart: crear projecte mínim ===== Crearem un projecte Django i provarem si funciona simplement amb: (env) $ django-admin startproject myproject (env) $ cd myproject (env) $ ./manage.py runserver 0.0.0.0:8000 I per comprovar-ho accedeix amb el navegador a ''http://localhost:8000''. Si volem endinsar-nos a la programació amb Django es recomana seguir l'excel·lent [[https://docs.djangoproject.com/en/stable/intro/tutorial01/|tutorial oficial Django]]. Per entrar al admin panel de Django cal fer migració (crea BD) i crear un usuari: (env) $ ./manage.py migrate (env) $ ./manage.py createsuperuser Visita el //admin panel// a ''http://localhost:8000/admin/'' \\ ===== ORM, models i migracions ===== **Un //Object-Relational Mapper// és una eina que facilita l'accés a dades mapejant les taules i relacions en objectes (classes) del llenguatge de programació**. Això permet una abstracció de la base de dades concreta que s'està emprant i no haver d'utilitzar SQL, sinó crides a mètodes dels objectes instanciats. Un **model**, en el context d'un ORM, **és una //class// que està mapejada a una taula de la base de dades**. Així, per exemple, es pot crear la següent //class// a l'arxiu ''models.py'' de Django: from django.db import models class Llibre (models.Model): titol = models.CharField(max_length=100) autor = models.CharField(max_length=200) resum = models.TextField(null=True,blank=True) data_edicio = models.DateField() Per crear un llibre i guardar-lo a la taula, enlloc d'emprar SQL, amb el ORM faríem: llibre = Llibre() llibre.titol = "El mag del Kremlin" llibre.autor = "Giuliano da Empoli" import datetime llibre.data_edicio = datetime.datetime(2023, 3, 29) # per persistir l'objecte a la BD cal cridar el mètode save() llibre.save() En el context dels //frameworks// de desenvolupament, **una eina de migració gestiona les versions i evolució dels esquemes de les bases de dades**. Durant el desenvolupament convé planificar amb anterioritat l'esquema de la base de dades per tal que pateixi el mínim de modificacions. Cada cop que es modifica la base de dades hi ha un gran risc de que parts del codi deixin de funcionar o de que perdem dades en el canvi (migració) d'una versió de l'esquema a la següent. Malgrat tot, preveure totes les situacions es fa impossible i aplicar canvis a l'esquema de la base de dades es fa imprescindible, per molt que es vulgui minimitzar. L'ús d'eines de migració milloren aquesta gestió i permeten una transició ordenada i, sobretot, automatitzada, d'un esquema al següent. {{:django:migracions.png?600}} El mecanisme és simple: cal un arxiu de migració que realitzi els canvis a la BD a cada evolució. Cada migració pot implicar un canvi simple com afegir o esborrar una columna fins a grans canvis com afegir multitud de taules i relacions. Tenim dos exemples complementaris i paradigmàtics en la gestió de les migracions: els dels //frameworks// Django i Laravel: * En **Laravel** el desenvolupador ha d'escriure un script de migració per aplicar els canvis explícits a la base de dades, com eliminar una columna o crear noves taules i relacions. * En **Django** el desenvolupador modifica directament el model amb l'ORM. Amb la instrucció ''makemigrations'' l'arxiu de migració es genera automàticament. El //framework// analitza el codi del model actual i l'anterior, i genera les ordres adequades dins un script de migració. En el projecte que estavem desenvolupant, podem crear models i després realitzar les migracions executant: (env1) $ ./manage.py makemigrations (env1) $ ./manage.py migrate Veurem que ara **apareix un arxiu ''db.sqlite3'' a l'arrel del projecte** on s'hauran creat algunes taules, en particular ''users'' i ''groups'', que son objectes //builtin// de Django (venen predefinits tot i que es poden modificar). SQLite és un SGBD senzill per BD locals en un sol arxiu amb propòsits de desenvolupament (no és apta per a desplegament). Per visualitzar les taules podem crear un superusuari i entrar a l'//admin panel//: (env1) $ ./manage.py createsuperuser (env1) $ ./manage.py runserver Entrem a http://localhost:8000/admin/ amb l'usuari i contrasenya creats i podrem veure, crear i modificar usuaris i grups i les diferents característiques d'aquests. {{:django:django-admin-panel-1.png?500}} \\ ===== Crear repositori Git ===== Anem a Github (o a Gitlab, Bitbucket, etc.) i fem [[Git#crear un nou repositori]]. Si ho feu a la web, ella mateix ens donarà la "xuleta" de com inicialitzar el nostre projecte i connectar-lo al repo remot. No pugeu res al repositori remot fins que hagueu configurat correctament el projecte amb el ''.gitignore'' com es descriu més avall. En particular, prengueu cura de no fer cap ''git add .'' (afegeix tots els arxius) fins no tenir el ''.gitignore'' a l'arrel. Teniu articles amb informació extensa sobre el [[VCS|sistema de control de versions]] i de [[Git]] en particular. ==== Configurar .gitignore ==== Abans d'entrar tots els arxius ens convé crear un arxiu ''.gitignore'' a l'arrel del nostre repositori (si esteu seguint el tutorial Django oficial, seria la carpeta ''mysite''). Aquest li dirà a la instrucció ''git add'' que ignori els arxius que compleixi amb determinats patrons. En particular, NO volem que entrin al repo: * Arxius que continguin contrasenyes i altra informació sensible que no volem que es publiqui. * El fitxer de la BD de SQLite (db.sqlite) * Els arxius transitoris d'execució accelerada de Python (els arxius .pyc) * Les carpetes de [[virtualenv]]. El ''virtualenv'' es crearà de nou cada cop que clonem un repositori. Una versió mínima dels arxius i carpetes que NO volem que es pugin al repo seria aquest: *.pyc /env/ /venv/ /static/* /media/* .env db.sqlite3 .coverage __pycache__ ==== Pujar els arxius ==== Si tens correctament ajustat el ''.gitignore'', ja pots afegir tots els arxius al repositori local i pujar-los al repo remot: (env) $ git init (env) $ git branch -m main (env) $ git add . (env) $ git commit -am "primer commit" Aquí hauries de enllaçar el teu repositori local amb el remot (per ex. de Github): (env) $ git remote add origin https://github.com/... (env) $ git push -u origin main \\ ===== requirements.txt ===== Podem afegir llibreries que necessitem al nostre entorn. Ens serà convenient, de cara a clonar i/o portar a producció el projecte, que sapiguem exactament les versions que necessitem. Podem saber les versions que tenim a l'actual venv amb: (env) $ pip freeze Hi ha un consens en utilitzar l'arxiu ''requirements.txt'' per a aquest objectiu. Per tant, cada cop que afegim noves dependències, caldrà afegir-les a aquest arxiu, de forma manual, o bé amb l'anterior instrucció. Afegim una llibreria (necessitarem ''django-environ'' més endavant): (env) $ pip install django-environ Veiem que ara tenim la llibreria afegida amb: (env) $ pip freeze I ara volquem tota aquesta info a ''requirements.txt'': (env) $ pip freeze > requirements.txt Quan necessitis carregar les llibreries a l'instal·lar un nou repo, ho pots fer així: (env) $ pip install -r requirements.txt No oblidis afegir l'arxiu ''requirements.txt'' a git amb ''git add'' i pujar-ho al teu repo remot. \\ ===== Versions ===== L'elecció de versions del projecte és molt important. Si examinem el contingut de ''requirements.txt'' (generat amb ''pip freeze'') veurem que ens mostra les versions de tots els paquets que tenim instal·lats en el //virtualenv//. En el meu cas (Desembre 2021) em surten aquestes: asgiref==3.4.1 Django==3.2.9 django-environ==0.8.1 mysqlclient==2.1.0 pytz==2021.3 sqlparse==0.4.2 En algun cas es poden trobar problemes per fer funcionar un projecte, si el sistema operatiu és diferent. En aquests casos potser és millor simplificar el ''requirements.txt'' posant els //packages// imprescindibles i deixar que el propi gestor ''pip'' trobi les dependències. En aquest cas no ho faríem amb el ''pip freeze'', sinó que crearíem el fitxe a mà: django==3.2 django-environ mysqlclient Hem de tenir en compte el cicle de vida de les versions de Django que fem servir. Les versions LTS són les //Long Term Service// que ens donaran cobertura durant més temps. Podem veure la [[https://www.djangoproject.com/download/#supported-versions|política de versions a la web de descàrrega de Django]]: {{https://static.djangoproject.com/img/release-roadmap.4cf783b31fbe.png}} Ara utilitzarem la 4.2 (LTS) però caldrà tenir cura d'actualitzar a la següent LTS (5.2) abans de que el suport de l'actual finalitzi \\ ===== Variables d'entorn amb django-environ ===== És important que les dades sensibles com //passwords//, //keys//, //tokens//, etc. no es pugin al [[VCS]]. Si heu pujat el vostre repositori a un Github públic és fàcil que hagueu rebut un email de **Gitguardian** o del mateix **Github (dependabot)** dient-vos que la vostra clau de seguretat del projecte Django no és bo que estigui a l'arxiu ''settings.py'', que és on apareix inicialment. Quan després configurem una base de dades MySQL haurem de configurar usuari i contrasenya al mateix arxiu ''settings.py'', amb el què encara rebrem més emails. Instal·lem [[https://django-environ.readthedocs.io|django-environ]] per facilitar que més tard puguem passar aquestes dades sensibles a l'aplicació mitjançant les variables d'entorn, i així no posar-les en arxius. En producció es passaran aquests valors com a variables d'entorn (per exemple, en utilitzar [[Docker]]). En development, per facilitar el treball diari, posarem les dades sensibles a l'arxiu ''.env''. Aquest arxiu carregarà els continguts com a variables d'entorn just abans d'engegar l'aplicació (quan fem ''./manage.py runserver''). Si no ho heu fet abans: (env) $ pip install django-environ Retirarem les dades sensibles de ''settings.py'' i les situarem a l'arxiu ''.env'' (que ja em marcat com a exclòs al ''.gitignore'') i que simula la càrrega de variables d'entorn com quan estiguem en mode de producció. Mireu la [[https://django-environ.readthedocs.io/en/latest/quickstart.html|documentació "quickstart" de Django-environ]] per ajustar el ''settings.py'', traient la configuració de les variables: * ''SECRET_KEY'' * ''DEBUG'' Més endavant ajustarem la info de la base de dades. Crea l'arxiu ''.env'' amb les variables d'entorn, en aquest cas ''SECRET_KEY'' i també ''DEBUG''. Per comprovar que funciona bé, mira de canviar el valor de ''DEBUG'' de ''True'' a ''False''. Si visitem una URL errònia el missatge que dona hauria de ser ben diferent (amb ''DEBUG=True'' hauria de donar més detalls de l'error). Un parell de bones pràctiques: * Afegir ''.env'' al ''.gitignore'' per tal de no pujar per error l'arxiu de variables d'entorn a producció. * Crear un arxiu d'exemple amb les variables d'entorn necessàries per facilitar la posada en marxa, però enlloc d'anomenar-lo ''.env'', que es diugui ''.env.example'' Per fer una llista com ''ALLOWED_HOSTS'' dins el ''.env'' podem seguir [[https://stackoverflow.com/questions/60410625/get-django-allowed-hosts-env-variable-formated-right-in-settings-py|aquest fil amb possibles solucions]]. Potser la més elegant és fer-ho com a list: ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') ALLOWED_HOSTS=localhost,127.0.0.1,mysite.com \\ ===== Utilitzant MySQL ===== Per defecte, Django utilitza la BD SQLite, però ens cal una BD apta per a producció com MySQL o PostgreSQL. A la [[https://docs.djangoproject.com/en/stable/ref/databases/|doc oficial de Django tenim com utilitzar els diversos SGBD més habituals]]. Per utilitzar MySQL caldrà instal·lar els binaris necessaris al sistema operatiu. Per a Ubuntu: $ sudo apt install libmysqlclient-dev python3-dev python3-mysqldb gcc pkgconf I també els connectors del virtualenv de Python: (env) $ pip install mysqlclient Com que estem utilitzant ''django-environ'' (veure secció anterior), caldrà que ajustem ''settings.py'' per a que la info de la BD la prenguem del ''.env''. La pàgina [[https://django-environ.readthedocs.io/en/latest/quickstart.html|quickstart de django-environ]] explica com configurar la BD amb una ''DATABASE_URL'', enlloc de configurar paràmetre per paràmetre. Ajusta la variable de la BD d'aquesta manera: DATABASE_URL='mysql://usuari:contrasenya@host:port/nom_db' Per veure si la configuració de la BD funciona, pots fer (si la BD està buida crearà les taules): (env) $ ./manage.py migrate Si vols utilitzar SQLite enlloc de MySQL, la variable a utilitzar seria: DATABASE_URL=sqlite:///db.sqlite3 \\ ===== Django en producció ===== Ja hem configurat el nostre projecte adientment, ara ens queda portar-lo a producció amb un servidor d'aplicacions. Molts tutorials posen en marxa Django amb el propi servidor de desenvolupament (el ./manage.py runserver) però això no és una bona pràctica. El servidor de desenvolupament no està pensat per gestionar adequadament el //multithreading// i altres implicacions en producció. Necessitem un servidor d'aplicacions específic, com cal, per posar Django en producció de forma segura. **WSGI significa "Web Server Gateway Interface"** i és una especificació standard per al desplegament d'aplicacions web sobre servidors web com Apache o Nginx. Veurem a continuació alguns servidors que segueixen aquest patró. ==== Servidor d'aplicacions uWSGI ==== [[https://uwsgi-docs.readthedocs.io|uWSGI (doc oficial)]] funciona en pur Python. Podem seguir el [[https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html|tutorial per Django amb uWSGI i Nginx]] de la documentació oficial. Assegura't que tens instal·lades les dependències per compilar packages: (env) $ sudo apt install gcc build-essential python3-dev En realitat la seva instal·lació i posta en marxa és molt senzilla: (env) $ pip install uwsgi (env) $ uwsgi --http :8000 --module mysite.wsgi Substitueix **mysite** pel nom del teu projecte (potser és **myproject**?). Veuràs que a l'executar Django amb el servidor d'aplicacions de producció uWSGI (o amb qualsevol altre) la ruta ''/admin'' no es veurà correctament. Falten els **arxius estàtics: CSS, JavaScript i imatges**. Això és normal. Segueix llegint per saber com s'ha d'instal·lar correctament. \\ ===== Arxius estàtics ===== Si has posat en marxa el projecte amb un servidor de producció (no amb el servidor de development) veuràs que si intentes accedir al panell d'administració /admin no es veu correctament. Falten els **arxius estàtics, imatges i CSS, que cal servir amb un servidor web com Apache o Nginx**, que son molt més eficients que forçar a Django a fer-ho. Per recopilar tots els arxius estàtics caldrà ajustar la variable ''STATIC_ROOT'' i ''STATIC_URL'' a ''settings.py'': STATIC_ROOT = 'static/' STATIC_URL = '/static/' I recopilar els arxius estàtics amb la instrucció: (env) $ ./manage.py collectstatic Veurem que a l'arrel del projecte s'haurà creat la carpeta ''static'' amb els arxius estàtics com imatges i fulles d'estil CSS. Afegeix ''static/'' al ''.gitignore''. ==== Configurar Nginx per a servir arxius estàtics ==== Un cop recopilats els arxius estàtics a la carpeta ''static/'' ens caldrà servir aquests arxius amb un Nginx, que és força més lleuger que l'Apache. Django no ha de servir mai arxius estàtics, ja que és menys eficient que el Web Server, que està pensat per a això específicament. El següent diagrama mostra com es durà a terme la posada en producció final: {{:django:diagrama_django_produccio.png?direct}} Segueix [[https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html#configure-nginx-for-your-site|la secció de Nginx del tutorial oficial de Django amb uWSGI i Nginx]] per finalitzar la configuració del servidor web amb connexió a uWSGI. Fixa't que en aquesta configuració final Nginx servirà el nostre projecte al port 8000 i que uWSGI posarà en marxa la site a través de //socket// i al port 8001: (env) $ uwsgi --socket :8001 --module mysite.wsgi **Canvia la configuració per tal que escolti al port 80 i que respongui al domini mysite.polls**. Recorda modificar l'arxiu ''/etc/polls'' per tal que el navegador miri ''localhost'' quan anem a mysite.polls. Comprova que accedint al panell d'administració ''/admin'' es visualitzen els CSS correctament. **Troubleshooting (possibles errades):** * Revisa que has parat (stop) i inhabilitat Apache (possible col·lisió de ports). * Revisa la configuració de ''mysite.conf'' de Nginx. Millor que deixis només activada el //mysite// i desactivis la //default// (links simbòlics). * Revisa els errors de Nginx amb ''sudo journalctl -xeu nginx.service'' * Si no veus el CSS fes CTRL+u , mira el codi font i clica un arxiu CSS aviam quin error et dona. * Comprova que has afegit ''STATIC_ROOT = BASE_DIR / 'static/''' al ''settings.css'' * Comprova que has executat ''./manage.py collectstatic'' i que no dona errors. * Si tens un ''Internal Server Error'' probablement hagis fet malament la comanda ''uwsgi'' (hauràs posat mysite.wsgi quan en realitat la teva carpeta es diu diferent de mysite). Si no veus els arxius estàtics perquè et dona un error FORBIDDEN, revisa que els permisos de tot el //path// fins a les carpetes ''static'' i ''media'' hi pot accedir l'usuari de NGINX (www-data). Ja saps que està prohibidíssim posar permisos 777, busca una solució raonable i segura. ==== Engegant Django app amb Supervisor ==== **Supervisord** Si has fet l'exercici anterior veuràs que uWSGI només està actiu si l'executes. El què voldrem ara és que la nostra webapp s'executi amb l'arrencada del computador. Una bona eina per aconseguir això és [[http://supervisord.org]] Instal·la i configura [[http://supervisord.org]] per tal que a l'arrencar la màquina també funcioni la nostra webapp ''mysite''. [program:polls] directory=/home/user/dev/mysite command=/home/user/dev/djenv/bin/uwsgi --socket :8001 --module mysite.wsgi Cal recarregar supervisord: $ sudo supervisorctl reload Comprovem si està en marxa: $ sudo supervisorctl status \\ ===== Més Django ===== Pots continuar a l'article [[Django Docker]] per dockeritzar la nostra webapp. Altres parts de Django interessants: * [[https://docs.djangoproject.com/en/4.1/howto/custom-management-commands/|Custom commands]] per afegir funcionalitats des de la //shell//. * [[https://faker.readthedocs.io/|Python Faker]] per generar dades de prova. \\