Zoptymalizuj swoje środowisko pod kątem rozwoju i produkcji: samouczek pidantyczny, część 2
Opublikowany: 2022-07-22Deweloperzy mogą być swoimi najgorszymi wrogami. Widziałem niezliczone przykłady inżynierów rozwijających system, który nie pasuje do ich środowiska produkcyjnego. Ten dysonans prowadzi do dodatkowej pracy i nie wyłapywania błędów systemowych na później w procesie rozwoju. Dopasowanie tych konfiguracji ostatecznie ułatwi ciągłe wdrożenia. Mając to na uwadze, stworzymy przykładową aplikację na naszym środowisku programistycznym Django, uproszczoną przez Docker, pydantic i conda.
Typowe środowisko programistyczne wykorzystuje:
- Repozytorium lokalne;
- Baza danych PostgreSQL oparta na platformie Docker; oraz
- Środowisko conda (do zarządzania zależnościami Pythona).
Pydantic i Django nadają się do projektów zarówno prostych, jak i złożonych. Poniższe kroki pokazują proste rozwiązanie, które pokazuje, jak odzwierciedlić nasze środowiska.
Konfiguracja repozytorium Git
Zanim zaczniemy pisać kod lub instalować systemy deweloperskie, stwórzmy lokalne repozytorium Git:
mkdir hello-visitor cd hello-visitor git init
Zaczniemy od podstawowego pliku .gitignore
Pythona w katalogu głównym repozytorium. W tym samouczku dodamy do tego pliku przed dodaniem plików, których nie chcemy śledzić Git.
Konfiguracja PostgreSQL Django przy użyciu Docker
Django wymaga relacyjnej bazy danych i domyślnie używa SQLite. Zwykle unikamy SQLite do przechowywania danych o znaczeniu krytycznym, ponieważ nie obsługuje on dobrze równoczesnego dostępu użytkowników. Większość programistów wybiera bardziej typową produkcyjną bazę danych, taką jak PostgreSQL. Niezależnie od tego powinniśmy używać tej samej bazy danych do rozwoju i produkcji. Ten mandat architektoniczny jest częścią aplikacji The Twelve-factor.
Na szczęście obsługa lokalnej instancji PostgreSQL z Docker i Docker Compose to pestka.
Aby uniknąć zanieczyszczenia naszego katalogu głównego, umieścimy pliki związane z Dockerem w osobnych podkatalogach. Zaczniemy od utworzenia pliku Docker Compose do wdrożenia PostgreSQL:
# docker-services/docker-compose.yml version: "3.9" services: db: image: "postgres:13.4" env_file: .env volumes: - hello-visitor-postgres:/var/lib/postgresql/data ports: - ${POSTGRES_PORT}:5432 volumes: hello-visitor-postgres:
Następnie utworzymy plik środowiska docker-compose
, aby skonfigurować nasz kontener PostgreSQL:
# docker-services/.env POSTGRES_USER=postgres POSTGRES_PASSWORD=MyDBPassword123 # The 'maintenance' database POSTGRES_DB=postgres # The port exposed to localhost POSTGRES_PORT=5432
Serwer bazy danych jest teraz zdefiniowany i skonfigurowany. Zacznijmy nasz kontener w tle:
sudo docker compose --project-directory docker-services/ up -d
Ważne jest, aby zwrócić uwagę na użycie sudo w poprzednim poleceniu. Będzie to wymagane, chyba że w naszym środowisku programistycznym zostaną wykonane określone kroki.
Tworzenie bazy danych
Połączmy się i skonfigurujmy PostgreSQL za pomocą standardowego zestawu narzędzi pgAdmin4. Użyjemy tych samych danych logowania, co wcześniej skonfigurowane w zmiennych środowiskowych.
Teraz stwórzmy nową bazę danych o nazwie hello_visitor
:
Z naszą bazą danych jesteśmy gotowi do zainstalowania naszego środowiska programistycznego.
Zarządzanie środowiskiem Python przez Miniconda
Musimy teraz skonfigurować izolowane środowisko Pythona i wymagane zależności. Dla uproszczenia konfiguracji i konserwacji wybraliśmy Minicondę.
Stwórzmy i aktywujmy nasze środowisko conda:
conda create --name hello-visitor python=3.9 conda activate hello-visitor
Teraz utworzymy plik hello-visitor/requirements.txt
, wyliczając nasze zależności w Pythonie:
django # PostgreSQL database adapter: psycopg2 # Pushes .env key-value pairs into environment variables: python-dotenv pydantic # Utility library to read database connection information: dj-database-url # Static file caching: whitenoise # Python WSGI HTTP Server: gunicorn
Następnie poprosimy Pythona o zainstalowanie tych zależności:
cd hello-visitor pip install -r requirements.txt
Nasze zależności powinny być teraz zainstalowane w ramach przygotowań do prac nad rozwojem aplikacji.
Rusztowanie Django
Zbudujemy nasz projekt i aplikację, najpierw uruchamiając django-admin
, a następnie uruchamiając wygenerowany przez niego plik manage.py
:
# From the `hello-visitor` directory mkdir src cd src # Generate starter code for our Django project. django-admin startproject hello_visitor . # Generate starter code for our Django app. python manage.py startapp homepage
Następnie musimy skonfigurować Django, aby załadować nasz projekt. Plik settings.py
wymaga dostosowania tablicy INSTALLED_APPS
, aby zarejestrować naszą nowo utworzoną aplikację homepage
:
# src/hello_visitor/settings.py # ... INSTALLED_APPS = [ "homepage.apps.HomepageConfig", "django.contrib.admin", # ... ] # ...
Konfiguracja ustawień aplikacji
Korzystając z podejścia pydantic i ustawień Django pokazanego w pierwszej części, musimy stworzyć plik zmiennych środowiskowych dla naszego systemu programistycznego. Przeniesiemy nasze obecne ustawienia do tego pliku w następujący sposób:
- Utwórz plik
src/.env
, aby przechowywać ustawienia naszego środowiska programistycznego. - Skopiuj ustawienia z
src/hello_visitor/settings.py
i dodaj je dosrc/.env
. - Usuń skopiowane linie z pliku
settings.py
. - Upewnij się, że parametry połączenia z bazą danych używają tych samych poświadczeń, które wcześniej skonfigurowaliśmy.
Nasz plik środowiska, src/.env
, powinien wyglądać tak:
DATABASE_URL=postgres://postgres:MyDBPassword123@localhost:5432/hello_visitor DATABASE_SSL=False SECRET_KEY="django-insecure-sackl&7(1hc3+%#*4e=)^q3qiw!hnnui*-^($o8t@2^^qqs=%i" DEBUG=True DEBUG_TEMPLATES=True USE_SSL=False ALLOWED_HOSTS='[ "localhost", "127.0.0.1", "0.0.0.0" ]'
Skonfigurujemy Django do odczytywania ustawień z naszych zmiennych środowiskowych za pomocą pydantic, za pomocą tego fragmentu kodu:
# src/hello_visitor/settings.py import os from pathlib import Path from pydantic import ( BaseSettings, PostgresDsn, EmailStr, HttpUrl, ) import dj_database_url # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent class SettingsFromEnvironment(BaseSettings): """Defines environment variables with their types and optional defaults""" # PostgreSQL DATABASE_URL: PostgresDsn DATABASE_SSL: bool = True # Django SECRET_KEY: str DEBUG: bool = False DEBUG_TEMPLATES: bool = False USE_SSL: bool = False ALLOWED_HOSTS: list class Config: """Defines configuration for pydantic environment loading""" env_file = str(BASE_DIR / ".env") case_sensitive = True config = SettingsFromEnvironment() os.environ["DATABASE_URL"] = config.DATABASE_URL DATABASES = { "default": dj_database_url.config(conn_max_age=600, ssl_require=config.DATABASE_SSL) } SECRET_KEY = config.SECRET_KEY DEBUG = config.DEBUG DEBUG_TEMPLATES = config.DEBUG_TEMPLATES USE_SSL = config.USE_SSL ALLOWED_HOSTS = config.ALLOWED_HOSTS # ...
Jeśli napotkasz jakiekolwiek problemy po zakończeniu poprzednich edycji, porównaj nasz spreparowany plik settings.py
z wersją w naszym repozytorium kodu źródłowego.
Tworzenie modelu
Nasza aplikacja śledzi i wyświetla liczbę odwiedzających stronę główną. Potrzebujemy modelu, który utrzyma tę liczbę, a następnie użyjemy mapera obiektowo-relacyjnego (ORM) Django, aby zainicjować pojedynczy wiersz bazy danych poprzez migrację danych.
Najpierw stworzymy nasz model VisitCounter
:
# hello-visitor/src/homepage/models.py """Defines the models""" from django.db import models class VisitCounter(models.Model): """ORM for VisitCounter""" count = models.IntegerField() @staticmethod def insert_visit_counter(): """Populates database with one visit counter. Call from a data migration.""" visit_counter = VisitCounter(count=0) visit_counter.save() def __str__(self): return f"VisitCounter - number of visits: {self.count}"
Następnie uruchomimy migrację, aby utworzyć nasze tabele bazy danych:
# in the `src` folder python manage.py makemigrations python manage.py migrate
Aby sprawdzić, czy istnieje tabela homepage_visitcounter
, możemy wyświetlić bazę danych w pgAdmin4.
Następnie musimy umieścić początkową wartość w naszej tabeli homepage_visitcounter
. Utwórzmy osobny plik migracji, aby to osiągnąć za pomocą rusztowania Django:
# from the 'src' directory python manage.py makemigrations --empty homepage
Dostosujemy utworzony plik migracji tak, aby korzystał z metody VisitCounter.insert_visit_counter
, którą zdefiniowaliśmy na początku tej sekcji:
# src/homepage/migrations/0002_auto_-------_----.py # Note: The dashes are dependent on execution time. from django.db import migrations from ..models import VisitCounter def insert_default_items(apps, _schema_editor): """Populates database with one visit counter.""" # To learn about apps, see: # https://docs.djangoproject.com/en/3.2/topics/migrations/#data-migrations VisitCounter.insert_visit_counter() class Migration(migrations.Migration): """Runs a data migration.""" dependencies = [ ("homepage", "0001_initial"), ] operations = [ migrations.RunPython(insert_default_items), ]
Teraz jesteśmy gotowi do wykonania tej zmodyfikowanej migracji dla aplikacji homepage
:
# from the 'src' directory python manage.py migrate homepage
Sprawdźmy, czy migracja została wykonana poprawnie, patrząc na zawartość naszej tabeli:
Widzimy, że nasza tabela homepage_visitcounter
istnieje i została wypełniona początkową liczbą odwiedzin równą 0. Gdy nasza baza danych jest pusta, skoncentrujemy się na tworzeniu naszego interfejsu użytkownika.
Twórz i konfiguruj nasze poglądy
Musimy zaimplementować dwie główne części naszego UI: widok i szablon.
Tworzymy widok homepage
, aby zwiększyć liczbę odwiedzających, zapisać go w bazie danych i przekazać do szablonu w celu wyświetlenia:
# src/homepage/views.py from django.shortcuts import get_object_or_404, render from .models import VisitCounter def index(request): """View for the main page of the app.""" visit_counter = get_object_or_404(VisitCounter, pk=1) visit_counter.count += 1 visit_counter.save() context = {"visit_counter": visit_counter} return render(request, "homepage/index.html", context)
Nasza aplikacja Django musi słuchać próśb skierowanych do homepage
. Aby skonfigurować to ustawienie, dodamy ten plik:
# src/homepage/urls.py """Defines urls""" from django.urls import path from . import views # The namespace of the apps' URLconf app_name = "homepage" # pylint: disable=invalid-name urlpatterns = [ path("", views.index, name="index"), ]
Aby nasza aplikacja homepage
była obsługiwana, musimy ją zarejestrować w innym pliku urls.py
:
# src/hello_visitor/urls.py from django.contrib import admin from django.urls import include, path urlpatterns = [ path("", include("homepage.urls")), path("admin/", admin.site.urls), ]
Bazowy szablon HTML naszego projektu będzie znajdował się w nowym pliku src/templates/layouts/base.html
:
<!DOCTYPE html> {% load static %} <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"> <title>Hello, visitor!</title> <link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/> </head> <body> {% block main %}{% endblock %} <!-- Option 1: Bootstrap Bundle with Popper --> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"></script> </body> </html>
Rozszerzymy podstawowy szablon dla naszej aplikacji homepage
w nowym pliku, src/templates/homepage/index.html
:
{% extends "layouts/base.html" %} {% block main %} <main> <div class="container py-4"> <div class="p-5 mb-4 bg-dark text-white text-center rounded-3"> <div class="container-fluid py-5"> <h1 class="display-5 fw-bold">Hello, visitor {{ visit_counter.count }}!</h1> </div> </div> </div> </main> {% endblock %}
Ostatnim krokiem w tworzeniu naszego interfejsu użytkownika jest poinformowanie Django, gdzie znaleźć te szablony. Dodajmy element słownika TEMPLATES['DIRS']
do naszego pliku settings.py
:
# src/hello_visitor/settings.py TEMPLATES = [ { ... 'DIRS': [BASE_DIR / 'templates'], ... }, ]
Nasz interfejs użytkownika jest już zaimplementowany i jesteśmy prawie gotowi do testowania funkcjonalności naszej aplikacji. Zanim przeprowadzimy nasze testy, musimy wprowadzić ostatni element naszego środowiska: statyczne buforowanie treści.
Nasza konfiguracja zawartości statycznej
Aby uniknąć korzystania ze skrótów architektonicznych w naszym systemie programistycznym, skonfigurujemy buforowanie zawartości statycznej w celu odzwierciedlenia naszego środowiska produkcyjnego.
Będziemy przechowywać wszystkie pliki statyczne naszego projektu w jednym katalogu, src/static
, i poinstruujemy Django, aby zebrał te pliki przed wdrożeniem.
Użyjemy logo Toptal jako favicon
naszej aplikacji i będziemy przechowywać je jako src/static/favicon.ico
:
# from `src` folder mkdir static cd static wget https://frontier-assets.toptal.com/83b2f6e0d02cdb3d951a75bd07ee4058.png mv 83b2f6e0d02cdb3d951a75bd07ee4058.png favicon.ico
Następnie skonfigurujemy Django do zbierania plików statycznych:
# src/hello_visitor/settings.py # Static files (CSS, JavaScript, images) # a la https://docs.djangoproject.com/en/3.2/howto/static-files/ # # Source location where we'll store our static files STATICFILES_DIRS = [BASE_DIR / "static"] # Build output location where Django collects all static files STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_ROOT.mkdir(exist_ok=True) # URL to use when referring to static files located in STATIC_ROOT. STATIC_URL = "/static/" STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
Chcemy tylko przechowywać nasze oryginalne pliki statyczne w repozytorium kodu źródłowego; nie chcemy przechowywać wersji zoptymalizowanych pod kątem produkcji. Dodajmy to drugie do naszego .gitignore
za pomocą tej prostej linii:
staticfiles
Ponieważ nasze repozytorium kodu źródłowego prawidłowo przechowuje wymagane pliki, musimy teraz skonfigurować nasz system buforowania do pracy z tymi plikami statycznymi.
Buforowanie plików statycznych
W produkcji — a zatem również w naszym środowisku programistycznym — użyjemy WhiteNoise, aby wydajniej obsługiwać pliki statyczne naszej aplikacji Django.
Rejestrujemy WhiteNoise jako oprogramowanie pośredniczące, dodając następujący fragment kodu do naszego pliku src/hello_visitor/settings.py
. Kolejność rejestracji jest ściśle określona, a WhiteNoiseMiddleware
musi pojawić się natychmiast po SecurityMiddleware
:
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # ... ] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Buforowanie plików statycznych powinno być teraz skonfigurowane w naszym środowisku programistycznym, co umożliwi nam uruchomienie naszej aplikacji.
Uruchamianie naszego serwera programistycznego
Mamy w pełni zakodowaną aplikację i możemy teraz uruchomić wbudowany serwer WWW naszego Django za pomocą tego polecenia:
# in the `src` folder python manage.py runserver
Gdy przejdziemy do http://localhost:8000
, licznik zwiększy się za każdym razem, gdy odświeżymy stronę:
Mamy teraz działającą aplikację, która zwiększa liczbę odwiedzin podczas odświeżania strony.
Gotowy do wdrożenia
Ten samouczek omówił wszystkie kroki potrzebne do stworzenia działającej aplikacji w pięknym środowisku programistycznym Django, które pasuje do produkcji. W części 3 omówimy wdrażanie naszej aplikacji w jej środowisku produkcyjnym. Warto również zapoznać się z naszymi dodatkowymi ćwiczeniami podkreślającymi zalety Django i pydantic: są one zawarte w repozytorium z kompletnym kodem dla tego samouczka pydantic.
Blog Toptal Engineering wyraża wdzięczność Stephenowi Davidsonowi za przeglądanie i testowanie wersji beta przykładów kodu przedstawionych w tym artykule.