Come sviluppatore Python potresti aver sentito parlare del termine microservizi e desideri creare da solo un microservizio Python. I microservice sono un’ottima architettura per la creazione di applicazioni altamente scalabili. Prima di iniziare a creare l’applicazione usando i microservice, è necessario conoscere i vantaggi e gli svantaggi dell’uso dei microservizi. In questo articolo imparerai i vantaggi e gli svantaggi dell’utilizzo dei microservice. Imparerai anche come creare il tuo microservice e distribuirlo utilizzando Docker Compose.
In questo tutorial vedremo:
- Quali sono i vantaggi e gli svantaggi dei microservizi.
- Perché dovresti creare microservice con Python.
- Come creare API REST utilizzando FastAPI e PostgreSQL.
- Come creare microservice utilizzando FastAPI.
- Come eseguire i microservice usando docker-compose.
- Come gestire i microservice utilizzando Nginx.
Prima creeremo una semplice API REST usando FastAPI e poi utilizzeremo PostgreSQL come nostro database. Estenderemo quindi la stessa applicazione a un microservizio.
Introduzione ai microservice
Il microservice è un approccio per suddividere grandi applicazioni monolitiche in singole applicazioni specializzate in uno specifico servizio/funzionalità. Questo approccio è spesso noto come architettura orientata ai servizi o SOA.
Nell’architettura monolitica, ogni logica di business risiede nella stessa applicazione. I servizi dell’applicazione come la gestione degli utenti, l’autenticazione e altre funzionalità utilizzano lo stesso database.
In un’architettura di microservice, l’applicazione è suddivisa in diversi servizi separati che vengono eseguiti in processi separati. Esiste un database diverso per le diverse funzionalità dell’applicazione e i servizi comunicano tra loro utilizzando HTTP, AMQP o un protocollo binario come TCP, a seconda della natura di ciascun servizio. La comunicazione tra servizi può essere eseguita anche utilizzando le code di messaggi come RabbitMQ, Kafka o Redis.
Vantaggi dei microservice
L’architettura a microservizi offre molti vantaggi. Alcuni di questi vantaggi sono:
- Un’applicazione debolmente accoppiata significa che i diversi servizi possono essere costruiti utilizzando le tecnologie più adatte alle singole funzioni e specificità. Quindi, il team di sviluppo non è vincolato alle scelte fatte durante l’avvio del progetto.
- Poiché i servizi sono responsabili di funzionalità specifiche, la comprensione e il controllo dell’applicazione è più semplice e facile.
- Anche il ridimensionamento dell’applicazione diventa più semplice perché se uno dei servizi richiede un utilizzo elevato della GPU, solo il server che contiene quel servizio deve avere una GPU elevata e gli altri possono essere eseguiti su un server normale.
Svantaggi dei microservice
L’architettura dei microservizi non è un proiettile d’argento che risolve tutti i tuoi problemi, ha anche i suoi svantaggi. Alcuni di questi inconvenienti sono:
- Poiché diversi servizi utilizzano un diverso database, le transazioni che coinvolgono più di un servizio devono gestire la consistenza dei dati.
- La suddivisione perfetta dei servizi è molto difficile da ottenere al primo tentativo e questo deve essere ripetuto prima di ottenere la migliore separazione possibile dei servizi.
- Poiché i servizi comunicano tra loro attraverso l’uso dell’interazione di rete, ciò rende l’applicazione più lenta a causa della latenza della rete e del servizio.
Perché Microservice in Python?
Python è uno strumento perfetto per la creazione di microservizi perché questo linguaggio ha una semplice curva di apprendimento, tonnellate di librerie e pacchetti già implementati e una grande community di utilizzatori. Grazie all’introduzione della programmazione asincrona in Python, sono emersi framework web con prestazioni alla pari con GO e Node.js.
Introduzione a FastAPI
FastAPI è un moderno framework Web ad alte prestazioni, dotato di tantissime funzioni interessanti come la documentazione automatica basata su OpenAPI e la libreria di convalida e serializzazione integrata. In questo link puoi leggere l’elenco di tutte le fantastiche funzionalità presenti FastAPI.
Perché FastAPI
Alcuni dei motivi per cui penso che FastAPI sia un’ottima scelta per la creazione di microservizi in Python sono:
- Documentazione automatica
Supporto Async/Await - Convalida e serializzazione integrate
- Tipo 100% annotato, quindi il completamento automatico funziona alla grande
Installazione FastAPI
Prima di installare FastAPI è necessario creare una nuova directory movie_service
e creare un nuovo ambiente virtuale all’interno della directory appena creata usando virtualenv.
Se non l’hai già installato virtualenv:
pip install virtualenv
virtualenv env
Nei sistemi Mac/Linux si può attivare l’ambiente virtuale usando il comando:
source ./env/bin/activate
.\env\Scripts\activate
pip install fastapi
Poiché FastAPI non viene fornito con un service integrato, è necessario installare uvicorn
per eseguire il service. uvicorn è un server ASGI che ci consente di utilizzare le funzionalità async/await.
Per installare uvicorn
si può usare il comando:
pip install uvicorn
Creazione di un semplice REST API utilizzando FastAPI
Prima di iniziare a creare un microservice utilizzando FastAPI, impariamo le basi di FastAPI. Creiamo una nuova directory zcode>app e un nuovo file main.py
all’interno della directory appena creata.
Aggiungiamo il seguente codice in main.py
.
#~/movie_service/app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def index():
return {"Real": "Python"}
E’ necessario importare e creare un’istanza di FastAPI e quindi registrare l’endpoint radice /
che restituisce un file JSON.
È possibile eseguire il server delle applicazioni utilizzando uvicorn app.main:app --reload
. Qui app.main
indica che si sta usando il file main.py
all’interno della directory app
e :app
indica a FastAPI il nome della nostra istanza.
Possiamo accedere all’app da http://127.0.0.1:8000
. Per accedere alla documentazione automatica bisogna andare su http://127.0.0.1:8000/docs
. Possiamo giocare e interagire con l’API dal browser stesso.
Aggiungiamo alcune funzionalità CRUD alla nostra applicazione. Aggiorniamo il main.py
in modo che assomigli a quanto segue:
#~/movie_service/app/main.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
fake_movie_db = [
{
'name': 'Star Wars: Episode IX - The Rise of Skywalker',
'plot': 'The surviving members of the resistance face the First Order once again.',
'genres': ['Action', 'Adventure', 'Fantasy'],
'casts': ['Daisy Ridley', 'Adam Driver']
}
]
class Movie(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
@app.get('/', response_model=List[Movie])
async def index():
return fake_movie_db
Come puoi vedere hai creato una nuova classe Movie
che estende la classe BaseModel
di Pydantic.
Il modello Movie
contiene il nome, la foto, il genere e il cast. Pydantic è integrato con FastAPI che rende la creazione di modelli e la convalida delle richieste un gioco da ragazzi.
Se andiamo alla pagina della documentazione possiamo vedere che sono descritti i campi del nostro modello nella sezione di risposta di esempio. Questo è possibile perché abbiamo definito il response_model
nella definizione del percorso nel decoratore @app.get
.
Ora aggiungiamo l’endpoint per aggiungere un film al nostro elenco di film.
Aggiungiamo una nuova definizione di endpoint per gestire la richiesta POST
.
@app.post('/', status_code=201)
async def add_movie(payload: Movie):
movie = payload.dict()
fake_movie_db.append(movie)
return {'id': len(fake_movie_db) - 1}
Ora apriamo il browser e testiamo la nuova API. Proviamo ad aggiungere un film con un campo non valido o senza i campi obbligatori e verifichiamo che la convalida venga gestita automaticamente da FastAPI.
Aggiungiamo un nuovo endpoint per aggiornare il film.
@app.put('/{id}')
async def update_movie(id: int, payload: Movie):
movie = payload.dict()
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
fake_movie_db[id] = movie
return None
raise HTTPException(status_code=404,
detail="Movie with given id not found")
Ecco l’indice id
della nostra lista fake_movie_db
.
Nota: ricorda di importare HTTPException
da Fastapi
Ora possiamo anche aggiungere l’endpoint per eliminare il film.
@app.delete('/{id}')
async def delete_movie(id: int):
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
del fake_movie_db[id]
return None
raise HTTPException(status_code=404,
detail="Movie with given id not found")
api
all’interno di app
e creiamo un nuovo file movies.py
all’interno della cartella creata di recente. Spostiamo tutti i codici relativi alle route da main.py
a movies.py
. Quindi, movies.py
dovrebbe essere simile al seguente:
#~/movie-service/app/api/movies.py
from typing import List
from fastapi import Header, APIRouter
from app.api.models import Movie
fake_movie_db = [
{
'name': 'Star Wars: Episode IX - The Rise of Skywalker',
'plot': 'The surviving members of the resistance face the First Order once again.',
'genres': ['Action', 'Adventure', 'Fantasy'],
'casts': ['Daisy Ridley', 'Adam Driver']
}
]
movies = APIRouter()
@movies.get('/', response_model=List[Movie])
async def index():
return fake_movie_db
@movies.post('/', status_code=201)
async def add_movie(payload: Movie):
movie = payload.dict()
fake_movie_db.append(movie)
return {'id': len(fake_movie_db) - 1}
@movies.put('/{id}')
async def update_movie(id: int, payload: Movie):
movie = payload.dict()
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
fake_movie_db[id] = movie
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
@movies.delete('/{id}')
async def delete_movie(id: int):
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
del fake_movie_db[id]
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
Qui abbiamo registrato un nuovo percorso API utilizzando APIRouter di FastAPI.
Inoltre, creiamo un nuovo file models.py
all’interno di api
in cui definiamo i nostri modelli Pydantic.
#~/movie-service/api/models.py
from typing import List
from pydantic import BaseModel
class Movie(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
main.py
il nuovo file con le “routes”.
#~/movie-service/app/main.py
from fastapi import FastAPI
from app.api.movies import movies
app = FastAPI()
app.include_router(movies)
movie-service
├── app
│ ├── api
│ │ ├── models.py
│ │ ├── movies.py
│ |── main.py
└── env
Prima di proseguire è bene assicurarsi che l’applicazione funzioni correttamente.
Utilizzo del database PostgreSQL con FastAPI
In precedenza abbiamo usato lista di stringhe Python per simulare un elenco di film, ma ora siamo pronti per utilizzare un database reale a tale questo scopo. In particolare, in questa applicazione useremo PostgreSQL. Installaremo PostgreSQL, se non l’hai già fatto. Dopo aver installato PostgreSQL creeremo un nuovo database che chiameremo movie_db
.
Utilizzeramo encode/database per connettersi al database utilizzando il supporto async
e await
.
Installa la libreria richiesta utilizzando:
pip install 'databases[postgresql]'
questo comando installerà anche sqlalchemy
e asyncpg
, che sono necessari per lavorare con PostgreSQL.
Creiamo un nuovo file all’interno di api
e lo chiamiamo db.py
. Questo file conterrà il modello del database reale per il REST API.
#~/movie-service/app/api/db.py
from sqlalchemy import (Column, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URL = 'postgresql://movie_user:movie_password@localhost/movie_db'
engine = create_engine(DATABASE_URL)
metadata = MetaData()
movies = Table(
'movies',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('plot', String(250)),
Column('genres', ARRAY(String)),
Column('casts', ARRAY(String))
)
database = Database(DATABASE_URL)
In particolare, DATABASE_URI
è l’URL utilizzato per connettersi al database PostgreSQL. movie_user
è il nome dell’utente del database, movie_password
è la password dell’utente del database ed movie_db
è il nome del database.
Proprio come in SQLAlchemy, abbiamo creato la tabella per il database dei film.
Aggiorniamo quindi main.py
per connettersi al database. Il codice di main.py
è il seguente:
#~/movie-service/app/main.py
from fastapi import FastAPI
from app.api.movies import movies
from app.api.db import metadata, database, engine
metadata.create_all(engine)
app = FastAPI()
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
app.include_router(movies)
movies.py
in modo che utilizzi un database invece di un falso elenco Python.
#~/movie-service/app/api/movies.py
from typing import List
from fastapi import Header, APIRouter
from app.api.models import MovieIn, MovieOut
from app.api import db_manager
movies = APIRouter()
@movies.get('/', response_model=List[MovieOut])
async def index():
return await db_manager.get_all_movies()
@movies.post('/', status_code=201)
async def add_movie(payload: MovieIn):
movie_id = await db_manager.add_movie(payload)
response = {
'id': movie_id,
**payload.dict()
}
return response
@movies.put('/{id}')
async def update_movie(id: int, payload: MovieIn):
movie = payload.dict()
fake_movie_db[id] = movie
return None
@movies.put('/{id}')
async def update_movie(id: int, payload: MovieIn):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
update_data = payload.dict(exclude_unset=True)
movie_in_db = MovieIn(**movie)
updated_movie = movie_in_db.copy(update=update_data)
return await db_manager.update_movie(id, updated_movie)
@movies.delete('/{id}')
async def delete_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return await db_manager.delete_movie(id)
db_manager.py
per manipolare il nostro database.
#~/movie-service/app/api/db_manager.py
from app.api.models import MovieIn, MovieOut, MovieUpdate
from app.api.db import movies, database
async def add_movie(payload: MovieIn):
query = movies.insert().values(**payload.dict())
return await database.execute(query=query)
async def get_all_movies():
query = movies.select()
return await database.fetch_all(query=query)
async def get_movie(id):
query = movies.select(movies.c.id==id)
return await database.fetch_one(query=query)
async def delete_movie(id: int):
query = movies.delete().where(movies.c.id==id)
return await database.execute(query=query)
async def update_movie(id: int, payload: MovieIn):
query = (
movies
.update()
.where(movies.c.id == id)
.values(**payload.dict())
)
return await database.execute(query=query)
models.py
in modo che possiamo utilizzare il modello Pydantic con la tabella sqlalchemy.
#~/movie-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class MovieIn(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
class MovieOut(MovieIn):
id: int
class MovieUpdate(MovieIn):
name: Optional[str] = None
plot: Optional[str] = None
genres: Optional[List[str]] = None
casts: Optional[List[str]] = None
La classe MovieIn
è il modello base da usare per aggiungere un film al database. Dobbiamo aggiungere id
a questo modello per ottenerlo dal database, creando il modello ModeulOut
. Il modello MovieUpdate
consente di impostare i valori nel modello come facoltativi in modo che durante l’aggiornamento del filmato possa essere inviato solo il campo che deve essere aggiornato.
Possiamo ora collegarci alla pagina “docs” della nostra applicazione ed inizia a giocare con l’API.
Modelli di gestione dei dati nei microservice
La gestione dei dati è uno degli aspetti più critici durante la creazione di un microservizio. Poiché le diverse funzioni dell’applicazione sono gestite da servizi diversi, l’utilizzo di un database può essere complicato.
Di seguito sono riportati alcuni modelli che è possibile utilizzare per gestire il flusso di dati nell’applicazione.
Database per servizio
L’utilizzo di un database per ogni servizio è ottimo se vuoi che i tuoi microservizi siano il più possibile accoppiati debolmente. Avere un database diverso per ogni servizio ci consente di scalare servizi diversi in modo indipendente. Una transazione che coinvolge più database viene eseguita tramite API ben definite. Ciò ha il suo svantaggio in quanto non è semplice implementare transazioni complesse che coinvolgono più servizi . Inoltre, l’aggiunta del sovraccarico di rete rende questo approccio meno efficiente da usare.
Database condiviso
Se ci sono molte transazioni che coinvolgono più servizi è meglio usare un database condiviso. Ciò comporta i vantaggi di un’applicazione altamente coerente, ma elimina la maggior parte dei vantaggi offerti dall’architettura dei microservizi. Gli sviluppatori che lavorano su un servizio devono coordinarsi con le modifiche allo schema in altri servizi.
Composizione API
Nelle transazioni che coinvolgono più database, il compositore API funge da gateway API ed esegue chiamate API ad altri microservizi nell’ordine richiesto. Infine, i risultati di ogni microservizio vengono restituiti al servizio client dopo aver eseguito un join in memoria. Lo svantaggio di questo approccio è l’inefficienza dei join in memoria di un set di dati di grandi dimensioni.
Creazione di un microservice Python in Docker
Il problema della distribuzione del microservice può essere notevolmente ridotto utilizzando Docker. Docker aiuta a incapsulare ogni servizio e a scalarlo in modo indipendente.
Installazione di Docker e Docker Compose
Se non hai già installato docker nel tuo sistema, verifichiamo se docker è installato eseguendo il comando docker
. Dopo aver completato l’installazione di Docker, installiamo Docker Compose . Docker Compose viene utilizzato per definire ed eseguire più container Docker. E’ utile anche per facilitare l’interazione tra i container.
Creazione del servizio Movies
Poiché gran parte del lavoro per la creazione di un servizio Movies è già stato svolto all’inizio con FastAPI, riutilizzeremo il codice che abbiamo già scritto. Creiamo una cartella nuova di zecca, che chiamerò python-microservices
e spostiamoci il codice che abbiamo scritto in precedenza.
Quindi, la struttura delle cartelle sarebbe simile a questa:
python-microservices/
└── movie-service/
├── app/
└── env/
Prima di tutto, creiamo un file requirements.txt
in cui conserveremo tutte le dipendenze che utilizzeremo in movie-service
.
Creiamo un nuovo file requirements.txt
all’interno di movie-service
e aggiungiamo quanto segue:
asyncpg==0.20.1
databases[postgresql]==0.2.6
fastapi==0.48.0
SQLAlchemy==1.3.13
uvicorn==0.11.2
httpx==0.11.1
Abbiamo usato tutte le librerie menzionate nel file tranne httpx che utilizzerai mentre effettui una chiamata API da servizio ad un altro.
Creiamo un Dockerfile
all’interno di movie-service
come segue:
FROM python:3.8-slim
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN apt-get update \
&& apt-get install gcc -y \
&& apt-get clean
RUN pip install -r /app/requirements.txt \
&& rm -rf /root/.cache/pip
COPY . /app/
Per prima cosa definiamo quale versione di Python usare. Quindi impostiamo la cartella app
come WORKDIR all’interno del contenitore Docker. Dopodiché viene installato gcc
, richiesto dalle librerie che utilizziamo nell’applicazione.
Infine, installiamo tutte le dipendenze in requirements.txt
e copiamo tutti i file all’interno di movie-service/app
.
Aggiorniamo db.py e sostituiamo:
DATABASE_URI = 'postgresql://movie_user:movie_password@localhost/movie_db'
con:
DATABASE_URI = os.getenv('DATABASE_URI')
NOTA: non dimentichiamo di importare os
nella parte superiore del file.
È necessario eseguire questa operazione in modo da poter fornire in seguito DATABASE_URI
come variabile di ambiente.
Inoltre, aggiorniamo main.py
e sostituiamo:
app.include_router(movies)
app.include_router(movies, prefix='/api/v1/movies', tags=['movies'])
Abbiamo aggiunto prefix=/api/v1/movies
in modo da gestire più facilmente le diverse versioni dell’API. Inoltre, i tag facilitano la ricerca delle API relative ai movies
nei docs di FastAPI.
Inoltre, dobbiamo aggiornare i nostri modelli in modo che casts
memorizzi l’ID del cast invece del nome effettivo. Quindi, aggiorniamo models.py
come segue:
#~/python-microservices/movie-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class MovieIn(BaseModel):
name: str
plot: str
genres: List[str]
casts_id: List[int]
class MovieOut(MovieIn):
id: int
class MovieUpdate(MovieIn):
name: Optional[str] = None
plot: Optional[str] = None
genres: Optional[List[str]] = None
casts_id: Optional[List[int]] = None
db.py
:
#~/python-microservices/movie-service/app/api/db.py
import os
from sqlalchemy import (Column, DateTime, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URL = os.getenv('DATABASE_URL')
engine = create_engine(DATABASE_URL)
metadata = MetaData()
movies = Table(
'movies',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('plot', String(250)),
Column('genres', ARRAY(String)),
Column('casts_id', ARRAY(Integer))
)
database = Database(DATABASE_URL)
Ora, aggiorniamo movies.py
per verificare se il cast con l’ID specificato si presenta nel cast-service prima di aggiungere un nuovo film o aggiornare un film.
#~/python-microservices/movie-service/app/api/movies.py
from typing import List
from fastapi import APIRouter, HTTPException
from app.api.models import MovieOut, MovieIn, MovieUpdate
from app.api import db_manager
from app.api.service import is_cast_present
movies = APIRouter()
@movies.post('/', response_model=MovieOut, status_code=201)
async def create_movie(payload: MovieIn):
for cast_id in payload.casts_id:
if not is_cast_present(cast_id):
raise HTTPException(status_code=404, detail=f"Cast with id:{cast_id} not found")
movie_id = await db_manager.add_movie(payload)
response = {
'id': movie_id,
**payload.dict()
}
return response
@movies.get('/', response_model=List[MovieOut])
async def get_movies():
return await db_manager.get_all_movies()
@movies.get('/{id}/', response_model=MovieOut)
async def get_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return movie
@movies.put('/{id}/', response_model=MovieOut)
async def update_movie(id: int, payload: MovieUpdate):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
update_data = payload.dict(exclude_unset=True)
if 'casts_id' in update_data:
for cast_id in payload.casts_id:
if not is_cast_present(cast_id):
raise HTTPException(status_code=404, detail=f"Cast with given id:{cast_id} not found")
movie_in_db = MovieIn(**movie)
updated_movie = movie_in_db.copy(update=update_data)
return await db_manager.update_movie(id, updated_movie)
@movies.delete('/{id}', response_model=None)
async def delete_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return await db_manager.delete_movie(id)
Aggiungiamo un servizio per effettuare una chiamata API al casts-service:
#~/python-microservices/movie-service/app/api/service.py
import os
import httpx
CAST_SERVICE_HOST_URL = 'http://localhost:8002/api/v1/casts/'
url = os.environ.get('CAST_SERVICE_HOST_URL') or CAST_SERVICE_HOST_URL
def is_cast_present(cast_id: int):
r = httpx.get(f'{url}{cast_id}')
return True if r.status_code == 200 else False
Nel precedente codice si effettua una chiamata API per ricavare il cast con uno specifico id e restituire true
se il cast esiste altrimenti false
.
Creazione del casts-service
Analogamente al movie-service, per creare un casts-service utilizzeremo FastAPI e un database PostgreSQL.
Creiamo una struttura di cartelle come la seguente:
python-microservices/
.
├── cast_service/
│ ├── app/
│ │ ├── api/
│ │ │ ├── casts.py
│ │ │ ├── db_manager.py
│ │ │ ├── db.py
│ │ │ ├── models.py
│ │ ├── main.py
│ ├── Dockerfile
│ └── requirements.txt
├── movie_service/
...
requirements.txt
:
asyncpg==0.20.1
databases[postgresql]==0.2.6
fastapi==0.48.0
SQLAlchemy==1.3.13
uvicorn==0.11.2
Dockerfile
:
FROM python:3.8-slim
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN apt-get update \
&& apt-get install gcc -y \
&& apt-get clean
RUN pip install -r /app/requirements.txt \
&& rm -rf /root/.cache/pip
COPY . /app/
main.py
:
#~/python-microservices/cast-service/app/main.py
from fastapi import FastAPI
from app.api.casts import casts
from app.api.db import metadata, database, engine
metadata.create_all(engine)
app = FastAPI()
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
app.include_router(casts, prefix='/api/v1/casts', tags=['casts'])
Abbiamo aggiunto il prefix /api/v1/casts
in modo che la gestione dell’API diventi più semplice. Inoltre, l’aggiunta dei tags
facilita la ricerca dei documenti relativi a casts nel docs di FastAPI.
casts.py
#~/python-microservices/cast-service/app/api/casts.py
from fastapi import APIRouter, HTTPException
from typing import List
from app.api.models import CastOut, CastIn, CastUpdate
from app.api import db_manager
casts = APIRouter()
@casts.post('/', response_model=CastOut, status_code=201)
async def create_cast(payload: CastIn):
cast_id = await db_manager.add_cast(payload)
response = {
'id': cast_id,
**payload.dict()
}
return response
@casts.get('/{id}/', response_model=CastOut)
async def get_cast(id: int):
cast = await db_manager.get_cast(id)
if not cast:
raise HTTPException(status_code=404, detail="Cast not found")
return cast
db_manager.py
#~/python-microservices/cast-service/app/api/db_manager.py
from app.api.models import CastIn, CastOut, CastUpdate
from app.api.db import casts, database
async def add_cast(payload: CastIn):
query = casts.insert().values(**payload.dict())
return await database.execute(query=query)
async def get_cast(id):
query = casts.select(casts.c.id==id)
return await database.fetch_one(query=query)
db.py
#~/python-microservices/cast-service/app/api/db.py
import os
from sqlalchemy import (Column, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URI = os.getenv('DATABASE_URI')
engine = create_engine(DATABASE_URI)
metadata = MetaData()
casts = Table(
'casts',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('nationality', String(20)),
)
database = Database(DATABASE_URI)
models.py
#~/python-microservices/cast-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class CastIn(BaseModel):
name: str
nationality: Optional[str] = None
class CastOut(CastIn):
id: int
class CastUpdate(CastIn):
name: Optional[str] = None
Esecuzione del microservizio utilizzando Docker Compose
Per eseguire i microservice, è necessario creare un filedocker-compose.yml e aggiungiamo quanto segue:
version: '3.7'
services:
movie_service:
build: ./movie-service
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
volumes:
- ./movie-service/:/app/
ports:
- 8001:8000
environment:
- DATABASE_URI=postgresql://movie_db_username:movie_db_password@movie_db/movie_db_dev
- CAST_SERVICE_HOST_URL=http://cast_service:8000/api/v1/casts/
movie_db:
image: postgres:12.1-alpine
volumes:
- postgres_data_movie:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=movie_db_username
- POSTGRES_PASSWORD=movie_db_password
- POSTGRES_DB=movie_db_dev
cast_service:
build: ./cast-service
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
volumes:
- ./cast-service/:/app/
ports:
- 8002:8000
environment:
- DATABASE_URI=postgresql://cast_db_username:cast_db_password@cast_db/cast_db_dev
cast_db:
image: postgres:12.1-alpine
volumes:
- postgres_data_cast:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=cast_db_username
- POSTGRES_PASSWORD=cast_db_password
- POSTGRES_DB=cast_db_dev
volumes:
postgres_data_movie:
postgres_data_cast:
Abbiamo 4 diversi servizi, movie_service, un database per movie_service, cast_service e un database per cast service. Abbiamo esposto il movie_service
alla porta 8001
e modo simile cast_service
alla porta 8002
.
Per il database, abbiamo utilizzato i volumi in modo che i dati non vengano distrutti quando il contenitore docker viene chiuso.
Eseguiamo il docker-compose usando il comando:
docker-compose up -d
Questo comando crea la dockar image, se non esiste già, e la esegue.
Andiamo su http://localhost:8002/docs per aggiungere un cast nel casts-service. Allo stesso modo, http://localhost:8001/docs per aggiungere il film nel movie-service.
Utilizziamo di Nginx per accedere a entrambi i servizi utilizzando un singolo indirizzo host
Abbiamo distribuito i microservizi utilizzando Docker compose, ma c’è un piccolo problema. È necessario accedere a ciascuno dei microservizi utilizzando una porta diversa. Si può risolvere questo problema utilizzando il reverse-proxy di Nginx , utilizzando Nginx puoi indirizzare la richiesta aggiungendo un middleware che indirizza le nostre richieste a diversi servizi in base all’URL dell’API.
Aggiungamo un nuovo file nginx_config.conf
all’interno python-microservices
con i seguenti contenuti.
server {
listen 8080;
location /api/v1/movies {
proxy_pass http://movie_service:8000/api/v1/movies;
}
location /api/v1/casts {
proxy_pass http://cast_service:8000/api/v1/casts;
}
}
Stiamo eseguendo Nginx alla porta 8080
e instradando le richieste al movie-service se l’endpoint inizia con /api/v1/movies
e in modo simile al casts-service se l’endpoint inizia con /api/v1/casts
.
Ora dobbiamo aggiungere il servizio nginx nel nostro file docker-compose-yml
. Aggiungiamo il seguente servizio dopo il servizio cast_db
:
...
nginx:
image: nginx:latest
ports:
- "8080:8080"
volumes:
- ./nginx_config.conf:/etc/nginx/conf.d/default.conf
depends_on:
- cast_service
- movie_service
...
docker-compose down
Ed eseguiamolo di nuovo con:
docker-compose up -d
Ora possiamo accedere sia al movie-service che al casts-service tramite la porta 8080
.
Collegandosi a http://localhost:8080/api/v1/movies/ otteniamo l’elenco dei film.
Ora, per accedere ai docs dei service è necessario modificare il main.py
del movie-service la seguente riga:
app = FastAPI()
con
app = FastAPI(openapi_url="/api/v1/movies/openapi.json", docs_url="/api/v1/movies/docs")
app = FastAPI(openapi_url="/api/v1/casts/openapi.json", docs_url="/api/v1/casts/docs")
Abbiamo cambiato l’endpoint in cui vengono serviti i docs e da dove viene servito il openapi.json
.
Ora possiamo accedere ai docs da http://localhost:8080/api/v1/movies/docs per il movie-service e da http://localhost:8080/api/v1/casts/docs per il casts-service.
Conclusione
L’architettura del microservice è ottima per suddividere una grande applicazione monolitica in logiche di business separate, ma anche questo comporta una complicazione. Python è ottimo per la creazione di microservizi grazie all’esperienza degli sviluppatori e a tonnellate di pacchetti e framework per rendere gli sviluppatori più produttivi.
Puoi trovare il codice completo presentato in questo tutorial su Github.