Nello sviluppo software moderno, i microservizi sono emersi come un’architettura fondamentale, che consente scalabilità, flessibilità e gestione efficiente di sistemi complessi.

I microservizi sono applicazioni piccole e indipendenti che svolgono compiti specifici, consentendo una distribuzione e una scalabilità flessibili. Questo approccio modulare alla progettazione del software allenta l’accoppiamento (coupling) tra i componenti, migliorando la flessibilità e la gestibilità durante lo sviluppo.

Questo articolo fornisce una panoramica sui microservizi, sulle loro funzionalità e sulla loro creazione con Python. Dimostra inoltre come distribuire i microservizi su Kinsta utilizzando un Dockerfile.

Cosa sono i microservizi?

I microservizi sono servizi indipendenti e autonomi all’interno di un’applicazione, ognuno dei quali risponde a specifiche esigenze aziendali. Comunicano attraverso API leggere o message broker, formando un sistema completo.

A differenza dei sistemi monolitici che scalano interamente in base alla domanda, i microservizi permettono di scalare singoli componenti ad alto traffico. Questa architettura facilita la gestione dei guasti e gli aggiornamenti delle funzionalità, contrastando le limitazioni dei sistemi monolitici.

I vantaggi dell’uso dei microservizi sono molteplici, come ad esempio:

  • Flessibilità e scalabilità: il disaccoppiamento dei singoli servizi permette di aumentare il numero di nodi che eseguono un’istanza di un particolare servizio ad alto traffico.
  • Modularità del codice: ciascun servizio può utilizzare uno stack tecnologico distinto, il che significa che si possono scegliere gli strumenti di sviluppo migliori per ciascuno di essi.

Tuttavia, le architetture a microservizi presentano alcune sfide:

  • Monitoraggio di più servizi: il monitoraggio dei singoli servizi di un sistema diventa difficile quando le istanze di un particolare servizio sono distribuite su più nodi. Questa difficoltà è particolarmente evidente in caso di guasti alla rete o altri problemi di sistema.
  • Costi: lo sviluppo di applicazioni a microservizi può essere molto più costoso rispetto alla costruzione di sistemi monolitici a causa dei costi associati alla gestione di più servizi. Ogni servizio richiede la propria infrastruttura e le proprie risorse, il che può diventare costoso, soprattutto in caso di espansione del sistema.

Come progettare un microservizio con Python

Ora che conosciamo i vantaggi dell’utilizzo di un’architettura a microservizi, è il momento di costruirne una con Python.

Per questo esempio, supponiamo di voler costruire un’applicazione web di e-commerce. Il sito web ha diversi componenti, tra cui il catalogo dei prodotti, l’elenco degli ordini, il sistema di elaborazione dei pagamenti e i log, ognuno dei quali deve essere implementato come servizio indipendente. Inoltre, è necessario stabilire un metodo di comunicazione da servizio a servizio per trasferire i dati tra questi servizi, come ad esempio HTTP, in modo efficiente.

Costruiamo un microservizio in Python per gestire un catalogo di prodotti. Il microservizio recupererà i dati dei prodotti da una fonte specificata e li restituirà in formato JSON.

Prerequisiti

Per seguire questo tutorial, è necessario avere:

1. Creare il progetto

  1. Per iniziare, creiamo una cartella per il progetto chiamata flask-microservice e la directory corrente nella directory del progetto.
  2. Eseguiamo quindi python3 --version per verificare che Python sia installato correttamente sul computer.
  3. Installiamo virtualenv per creare un ambiente di sviluppo isolato per il microservizio Flask eseguendo questo comando:
    pip3 install virtualenv
  4. Creiamo un ambiente virtuale eseguendo questo comando:
    virtualenv venv
  5. Infine, attiviamo l’ambiente virtuale utilizzando uno dei seguenti comandi in base al sistema operativo del computer:
    # Windows: 
    .\venv\Scripts\activate
    # Unix or macOS:
    source venv/bin/activate

2. Configurare un server Flask

Nella directory principale, creiamo un file requirements.txt e aggiungiamo queste dipendenze.

flask
requests

Eseguiamo il file pip3 command on your terminal to install the dependencies.

pip install -r requirements.txt

Successivamente, creiamo una nuova cartella nella directory principale e chiamiamola services. All’interno di questa cartella, creiamo un nuovo file, products.py, e aggiungiamo il codice sottostante per configurare un server Flask.

import requests
import os
from flask import Flask, jsonify
app = Flask(__name__)
port = int(os.environ.get('PORT', 5000))

@app.route("/")
def home():
    return "Hello, this is a Flask Microservice"
if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=port)

Nel codice qui sopra, viene configurato un server Flask di base. Inizializza un’applicazione Flask, definisce una singola route per l’URL principale ("/") e, quando vi si accede, visualizza il messaggio "Hello, this is a Flask Microservice". Il server viene eseguito su una porta specificata, ottenuta da una variabile d’ambiente o predefinita alla porta 5000, e si avvia in modalità di debug per essere pronto a gestire le richieste in arrivo.

3. Definire gli endpoint dell’API

Una volta configurato il server, creiamo un endpoint API per un microservizio che recupera i dati dei prodotti da un’API disponibile pubblicamente. Aggiungiamo questo codice al file products.py:

BASE_URL = "https://dummyjson.com"
@app.route('/products', methods=['GET'])
def get_products():
    response = requests.get(f"{BASE_URL}/products")
    if response.status_code != 200:
        return jsonify({'error': response.json()['message']}), response.status_code
    products = []
    for product in response.json()['products']:
        product_data = {
            'id': product['id'],
            'title': product['title'],
            'brand': product['brand'],
            'price': product['price'],
            'description': product['description']
        }
        products.append(product_data)
    return jsonify({'data': products}), 200 if products else 204

Il codice precedente crea un endpoint /products nel server Flask. Quando vi si accede tramite una richiesta GET, recupera i dati dei prodotti da un’API fittizia. In caso di successo, elabora i dati recuperati, estrae i dettagli del prodotto e restituisce le informazioni in formato JSON. In caso di errori o di assenza di dati disponibili, risponde con un messaggio di errore e un codice di stato appropriati.

4. Testare il microservizio

A questo punto, abbiamo configurato con successo un semplice microservizio. Per avviare il servizio, avviamo il server di sviluppo, che inizierà a funzionare all’indirizzo http://localhost:5000.

flask --app services/products run

Poi possiamo fare una richiesta GET all’endpoint /products usando il client Postman; dovremmo vedere una risposta simile a quella dello screenshot qui sotto.

Richiesta GET riuscita all'endpoint del prodotto API fittizio in Postman
Test della richiesta HTTP API GET in Postman.

Come implementare l’autenticazione e l’autorizzazione in un microservizio in Python

Quando si costruiscono microservizi, è importante implementare misure di sicurezza solide come l’autenticazione e l’autorizzazione. La sicurezza del microservizio assicura che solo gli utenti autorizzati possano accedere e utilizzare il servizio, proteggendo i dati sensibili e prevenendo gli attacchi malevoli.

Un metodo efficace per implementare l’autenticazione e l’autorizzazione sicura nei microservizi è rappresentato dai JSON Web Tokens (JWT).

I JWT sono uno standard aperto molto diffuso che offre un modo sicuro ed efficiente di trasmettere informazioni di autenticazione tra client e server. Si tratta di token compatti, crittografati e firmati digitalmente che vengono trasmessi insieme alle richieste HTTP. Includendo un JWT in ogni richiesta, il server può verificare rapidamente l’identità e i permessi di un utente.

Per implementare l’autenticazione JWT nel microservizio, procediamo come segue:

  1. Aggiungiamo il pacchetto pyjwt di Python al file requirements.txt e reinstalliamo le dipendenze usando pip install -r requirements.txt.
  2. Poiché il servizio non ha un database dedicato, creiamo un file users.json nella directory principale del progetto per memorizzare un elenco di utenti autorizzati. Incolliamo il codice sottostante nel file:
    [
        {   
            "id": 1,
            "username": "admin",
            "password": "admin"
        
        }
    ]

  3. Quindi, nel file services/products.py, sostituiamo le dichiarazioni di importazione con le seguenti:
    import requests 
    from flask import Flask, jsonify, request, make_response
    import jwt
    from functools import wraps
    import json
    import os
    from jwt.exceptions import DecodeError

    Stiamo importando questi moduli per gestire le richieste HTTP, creare un’app Flask, gestire i dati JSON, implementare l’autenticazione basata su JWT e gestire le eccezioni, abilitando un’ampia gamma di funzionalità all’interno del server Flask.

  4. Aggiungiamo il seguente codice sotto la creazione dell’istanza dell’app Flask per generare una chiave segreta che verrà utilizzata per firmare i token JWT.
    app.config['SECRET_KEY'] = os.urandom(24)
  5. Per verificare i JWT, creiamo una funzione decoratore e aggiungiamo il seguente codice sopra le route API nel codice del server Flask. Questa funzione decoratore autenticherà e convaliderà gli utenti prima che accedano alle route protetti.
    def token_required(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            token = request.cookies.get('token')
            if not token:
                return jsonify({'error': 'Authorization token is missing'}), 401
            try:
                data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
                current_user_id = data['user_id']
            except DecodeError:
                return jsonify({'error': 'Authorization token is invalid'}), 401
            return f(current_user_id, *args, **kwargs)
        return decorated

    Questa funzione decoratore controlla le richieste HTTP in arrivo alla ricerca di un token di autorizzazione JWT, che dovrebbe essere presente nelle intestazioni della richiesta o nei cookie. Se il token manca o non è valido, il decoratore invia un messaggio unauthorized status code come risposta.

    Al contrario, se è presente un token valido, il decoratore estrae l’ID utente dopo averlo decodificato. Questo processo salvaguarda gli endpoint API protetti consentendo l’accesso solo agli utenti autorizzati.

  6. Definiamo un endpoint API per l’autenticazione dell’utente utilizzando il codice seguente.
    with open('users.json', 'r') as f:
        users = json.load(f)
    @app.route('/auth', methods=['POST'])
    def authenticate_user():
        if request.headers['Content-Type'] != 'application/json':
            return jsonify({'error': 'Unsupported Media Type'}), 415
        username = request.json.get('username')
        password = request.json.get('password')
        for user in users:
            if user['username'] == username and user['password'] == password:
                token = jwt.encode({'user_id': user['id']}, app.config['SECRET_KEY'],algorithm="HS256")
                response = make_response(jsonify({'message': 'Authentication successful'}))
                response.set_cookie('token', token)
                return response, 200
        return jsonify({'error': 'Invalid username or password'}), 401

    Per autenticare e autorizzare gli utenti, l’endpoint API /auth controlla le credenziali contenute nel payload JSON della richiesta POST rispetto all’elenco degli utenti autorizzati. Se le credenziali sono valide, genera un token JWT utilizzando l’ID dell’utente e la chiave segreta dell’applicazione e imposta il token come cookie nella risposta. Gli utenti possono ora utilizzare questo token per effettuare le successive richieste API.

    Dopo aver creato l’endpoint /auth, usiamo Postman per inviare una richiesta HTTP POST a http://localhost:5000/auth. Nel corpo della richiesta, includiamo le credenziali del finto utente amministratore che abbiamo creato.

    La richiesta di Postman mostra il corpo della richiesta
    La richiesta di Postman mostra il corpo della richiesta.

    Se la richiesta va a buon fine, l’API genererà un token JWT, lo imposterà nei cookie di Postman e invierà una risposta di successo autenticata.

  7. Infine, aggiorniamo l’endpoint dell’API GET per controllare e verificare il token JWT utilizzando il codice seguente:
    @app.route('/products', methods=['GET'])
    @token_required
    def get_products(current_user_id):
        headers = {'Authorization': f'Bearer {request.cookies.get("token")}'}    
        response = requests.get(f"{BASE_URL}/products", headers=headers)
        if response.status_code != 200:
            return jsonify({'error': response.json()['message']}), response.status_code
        products = []
        for product in response.json()['products']:
            product_data = {
                'id': product['id'],
                'title': product['title'],
                'brand': product['brand'],
                'price': product['price'],
                'description': product['description']
            }
            products.append(product_data)
        return jsonify({'data': products}), 200 if products else 204

Come containerizzare i microservizi Python con Docker

Docker è una piattaforma che impacchetta le applicazioni e le loro dipendenze in un ambiente di sviluppo isolato. L’impacchettamento dei microservizi in container ottimizza i processi di distribuzione e gestione nei server, poiché ogni servizio viene eseguito in modo indipendente nel proprio container.

Per containerizzare il microservizio, dobbiamo creare un’immagine Docker da un Dockerfile che specifichi le dipendenze necessarie per eseguire l’applicazione in un container. Creiamo un Dockerfile nella directory principale del progetto e aggiungiamo queste istruzioni:

FROM python:3.9-alpine
WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "./services/products.py"]

Prima di creare l’immagine, diamo un’occhiata ai seguenti comandi:

  • FROM: indica a Docker quale immagine di base utilizzare. Un’immagine di base è un’istanza precostituita contenente il software e le dipendenze per eseguire l’applicazione Flask in un container.
  • WORKDIR: imposta la directory specificata all’interno del container come directory di lavoro.
  • COPY requirements.txt ./: copia le dipendenze nel file requirements.txt nel file requirements.txt del container.
  • RUN – Esegue il comando specificato per installare le dipendenze richieste dall’immagine.
  • COPY . .: copia tutti i file dalla directory principale del progetto alla directory di lavoro all’interno del container.
  • EXPOSE: specifica la porta su cui il container ascolterà le richieste. Tuttavia, Docker non pubblica la porta sul computer host.
  • CMD: specifica il comando predefinito da eseguire all’avvio del container.

Successivamente, aggiungiamo un file .dockerignore nella directory principale del progetto per specificare i file che l’immagine Docker deve escludere. Limitare il contenuto dell’immagine ridurrà le dimensioni finali e il tempo di build associato.

/venv
/services/__pycache__/
.gitignore

Ora, eseguiamo il comando seguente per creare l’immagine Docker:

docker build -t flask-microservice .

Infine, una volta creata l’immagine, possiamo eseguire il microservizio in un container Docker utilizzando il seguente comando:

docker run -p 5000:5000 flask-microservice

Questo comando avvierà un container Docker che esegue il microservizio ed esporrà la porta 5000 del container alla porta 5000 del computer host, consentendo di effettuare richieste HTTP dal browser web o da Postman utilizzando l’URL http://localhost:5000.

Distribuire microservizi Python con Kinsta

Kinsta offre soluzioni di hosting gestito per applicazioni web e database: si possono distribuire e gestire senza problemi i propri microservizi Python e le API di backend in un ambiente di produzione.

Seguiamo questi passaggi per preparare il microservizio Flask alla distribuzione con MyKinsta:

  1. Per prima cosa, creiamo un nuovo Procfile nella directory principale e aggiungiamo il codice qui sotto. Questo specifica il comando per eseguire il microservizio Flask sul server HTTP Gunicorn WSGI di Kinsta per le applicazioni Python.
    web: gunicorn services.wsgi
  2. Nel file requirements.txt, aggiungiamo la dipendenza Gunicorn:
    gunicorn==20.1.*
  3. Quindi, creiamo un nuovo file services/wsgi.py e aggiungiamo il codice seguente.
    from services.products import app as application
    if __name__ == "__main__":
                    application.run()
  4. Creiamo un file .gitignore nella cartella principale del progetto e aggiungiamo quanto segue:
    services/__pycache__
    venv
  5. Infine, creiamo un nuovo repository su GitHub e inviamo i file del progetto.

Quando il repository sarà pronto, basta seguire questi passaggi per distribuire il microservizio Flask su Kinsta:

    1. Accedere o creare un account per visualizzare la dashboard MyKinsta.
    2. Autorizzare Kinsta con il provider Git (Bitbucket, GitHub o GitLab).
    3. Cliccare su Applicazioni nella barra laterale di sinistra e poi su Aggiungi applicazione.
    4. Nel cruscotto, cliccare su Aggiungi servizio e selezionare Applicazione.
    5. Selezionare il repository e il branch da cui si desidera effettuare il deploy.
    6. Assegnare un nome unico all’applicazione e scegliere la posizione del data center.
    1. Per configurare l’ambiente di build, selezionare l’opzione per utilizzare un Dockerfile per creare l’immagine del container.
  1. Fornire il percorso del file Docker e il contesto.
  2. Infine, controllare le altre informazioni e cliccare su Crea applicazione.

Testare il microservizio

Una volta che il processo di distribuzione sarà andato a buon fine, clicchiamo sull’URL fornito per testare il microservizio effettuando delle richieste HTTP in Postman. Effettuiamo una richiesta GET all’endpoint principale.

Richiesta GET riuscita all'URL del microservizio distribuito per la route Home
Eseguire una richiesta HTTP API GET all’endpoint product del microservizio.

Per autenticarci e generare un token JWT, inviamo una richiesta POST all’endpoint API /auth, passando le credenziali dell’amministratore nel corpo della richiesta.

Una richiesta POST a un endpoint Auth di un microservizio per l'autenticazione in Postman
Richiesta HTTP API POST all’endpoint auth del microservizio.

Infine, dopo esserci autenticati, eseguiramo una richiesta GET all’endpoint /products per recuperare i dati.

Richiesta HTTP API GET riuscita all'endpoint dei prodotti in Postman
Richiesta HTTP API GET all’endpoint del microservizio products.

Riepilogo

Man mano che le applicazioni crescono in dimensioni e complessità, è fondamentale adottare modelli di architettura che permettano ai sistemi software di scalare senza affaticare le risorse disponibili.

L’architettura a microservizi offre scalabilità, flessibilità di sviluppo e manutenibilità, rendendo più facile la gestione di applicazioni complesse.

Kinsta semplifica il processo di hosting dei vostri microservizi. Vi permette di utilizzare senza problemi il vostro database preferito e di ospitare comodamente sia l’applicazione che il database attraverso una dashboard unificata.

Jeremy Holcombe Kinsta

Content & Marketing Editor presso Kinsta, web developer di WordPress e content writer. Al di fuori di tutto ciò che riguarda WordPress, mi piacciono la spiaggia, il golf e il cinema. Ho anche i problemi di tutte le persone più alte della media ;).