Migrazione dell'archivio BLOB di App Engine a Cloud Storage

Questa guida illustra come eseguire la migrazione da App Engine Blobstore a Cloud Storage.

Cloud Storage è simile al Blobstore di App Engine perché consente di utilizzare Cloud Storage per gestire oggetti di dati (blob) di grandi dimensioni, come file video o immagine, e consentire agli utenti di caricare file di dati di grandi dimensioni. Sebbene l'archivio BLOB di App Engine sia accessibile solo tramite i servizi in bundle legacy di App Engine, Cloud Storage è un prodotto autonomo di Google Cloud a cui si accede tramite le librerie client di Cloud. Cloud Storage offre alla tua app una soluzione di archiviazione di oggetti più moderna e ti offre la flessibilità necessaria per eseguire in un secondo momento la migrazione a Cloud Run o a un'altra piattaforma di hosting di app Google Cloud.

Per i progetti Google Cloud creati dopo novembre 2016, Blobstore utilizza i bucket Cloud Storage dietro le quinte. Ciò significa che quando esegui la migrazione dell'app a Cloud Storage, tutti gli oggetti e le autorizzazioni esistenti nei bucket Cloud Storage esistenti rimangono invariati. Puoi inoltre iniziare ad accedere a questi bucket esistenti utilizzando le librerie client di Cloud per Cloud Storage.

Differenze e somiglianze principali

Cloud Storage esclude le seguenti dipendenze e limitazioni dell'archivio BLOB:

  • L'API Blobstore per Python 2 ha una dipendenza dall'app web.
  • L'API Blobstore per Python 3 utilizza classi di utilità per usare i gestori Blobstore.
  • Per Blobstore, il numero massimo di file che possono essere caricati in Blobstore è 500. Non esiste alcun limite al numero di oggetti che puoi creare in un bucket Cloud Storage.

Cloud Storage non supporta:

  • Classi di gestori dell'archivio BLOB
  • Oggetti dell'archivio BLOB

Somiglianze in Cloud Storage e nell'archivio BLOB di App Engine:

  • È in grado di leggere e scrivere oggetti di dati di grandi dimensioni in un ambiente di runtime, nonché di archiviare e gestire oggetti di dati statici di grandi dimensioni, come filmati, immagini o altri contenuti statici. Il limite di dimensione degli oggetti per Cloud Storage è 5 TiB.
  • Consente di archiviare oggetti in un bucket Cloud Storage.
  • Hanno un livello gratuito.

Prima di iniziare

  • Devi esaminare e comprendere i prezzi e le quote di Cloud Storage:
  • Avere un'app Python 2 o Python 3 App Engine esistente che utilizza Blobstore.
  • Gli esempi in questa guida mostrano un'app di cui viene eseguita la migrazione in Cloud Storage utilizzando il framework Flask. Tieni presente che puoi utilizzare qualsiasi framework web, incluso rimanere su webapp2, durante la migrazione a Cloud Storage.

Panoramica

A livello generale, il processo di migrazione dall'archivio BLOB di App Engine a Cloud Storage prevede i seguenti passaggi:

  1. Aggiornare i file di configurazione
  2. Aggiorna l'app Python:
    • Aggiorna il framework web
    • Importa e inizializza Cloud Storage
    • Aggiornamento gestori Blobstore
    • Facoltativo: aggiorna il tuo modello dei dati se utilizzi Cloud NDB o App Engine NDB
  3. Testare l'app ed eseguirne il deployment

Aggiornare i file di configurazione

Prima di modificare il codice dell'applicazione per passare da Blobstore a Cloud Storage, aggiorna i file di configurazione in modo da utilizzare la libreria di Cloud Storage.

  1. Aggiorna il file app.yaml. Segui le istruzioni relative alla tua versione di Python:

    Python 2

    Per le app Python 2:

    1. Rimuovi la sezione handlers ed eventuali dipendenze webapp non necessarie nella sezione libraries.
    2. Se utilizzi le librerie client di Cloud, aggiungi le versioni più recenti delle librerie grpcio e setuptools.
    3. Aggiungi la libreria ssl poiché è richiesta da Cloud Storage.

    Di seguito è riportato un file app.yaml di esempio con le modifiche apportate:

    runtime: python27
    threadsafe: yes
    api_version: 1
    
    handlers:
    - url: /.*
      script: main.app
    
    libraries:
    - name: grpcio
      version: latest
    - name: setuptools
      version: latest
    - name: ssl
      version: latest
    

    Python 3

    Per le app Python 3, elimina tutte le righe tranne l'elemento runtime. Ad esempio:

    runtime: python310 # or another support version
    

    Il runtime Python 3 installa le librerie automaticamente, quindi non è necessario specificare le librerie integrate del runtime Python 2 precedente. Se l'app Python 3 utilizza altri servizi in bundle legacy durante la migrazione a Cloud Storage, non modificare il file app.yaml.

  2. Aggiorna il file requirements.txt. Segui le istruzioni per la tua versione di Python:

    Python 2

    Aggiungi le librerie client di Cloud per Cloud Storage al tuo elenco di dipendenze nel file requirements.txt.

    google-cloud-storage
    

    Esegui quindi pip install -t lib -r requirements.txt per aggiornare l'elenco delle librerie disponibili per la tua app.

    Python 3

    Aggiungi le librerie client di Cloud per Cloud Storage al tuo elenco di dipendenze nel file requirements.txt.

    google-cloud-storage
    

    App Engine installa automaticamente queste dipendenze durante il deployment dell'app nel runtime Python 3, quindi elimina la cartella lib, se esistente.

  3. Per le app Python 2, se la tua app utilizza librerie integrate o copiate, devi specificare questi percorsi nel file appengine_config.py:

    import pkg_resources
    from google.appengine.ext import vendor
    
    # Set PATH to your libraries folder.
    PATH = 'lib'
    # Add libraries installed in the PATH folder.
    vendor.add(PATH)
    # Add libraries to pkg_resources working set to find the distribution.
    pkg_resources.working_set.add_entry(PATH)
    

Aggiorna l'app Python

Dopo aver modificato i file di configurazione, aggiorna l'app Python.

Aggiorna il framework web Python 2

Per le app Python 2 che utilizzano il framework webapp2, ti consigliamo di eseguire la migrazione dal framework webapp2 obsoleto. Consulta la pianificazione del supporto dell'esecuzione per la data di fine del supporto per Python 2.

Puoi eseguire la migrazione a un altro framework web come Flask, Django o WSGI. Poiché Cloud Storage esclude le dipendenze da webapp2 e i gestori dell'archivio BLOB non sono supportati, puoi eliminare o sostituire altre librerie relative alle app web.

Se scegli di continuare a utilizzare webapp2, tieni presente che gli esempi in questa guida utilizzano Cloud Storage con Flask.

Se prevedi di utilizzare i servizi Google Cloud oltre a Cloud Storage o di ottenere l'accesso alle versioni di runtime più recenti, ti consigliamo di eseguire l'upgrade della tua app al runtime Python 3. Per ulteriori informazioni, consulta la panoramica della migrazione da Python 2 a Python 3.

Importa e inizializza Cloud Storage

Modifica i file dell'applicazione aggiornando le righe di importazione e inizializzazione:

  1. Rimuovi le istruzioni di importazione dell'archivio BLOB, come le seguenti:

    import webapp2
    from google.appengine.ext import blobstore
    from google.appengine.ext.webapp import blobstore_handlers
    
  2. Aggiungi le istruzioni di importazione per Cloud Storage e le librerie di autenticazione di Google, come riportato di seguito:

    import io
    from flask import (Flask, abort, redirect, render_template,
    request, send_file, url_for)
    from google.cloud import storage
    import google.auth
    

    La libreria di autenticazione di Google è necessaria per ottenere lo stesso ID progetto utilizzato in Blobstore per Cloud Storage. Importa altre librerie come Cloud NBD, se applicabili alla tua app.

  3. Creare un nuovo client per Cloud Storage e specificare il bucket che viene utilizzato in Blobstore. Ad esempio:

    gcs_client = storage.Client()
    _, PROJECT_ID = google.auth.default()
    BUCKET = '%s.appspot.com' % PROJECT_ID
    

    Per i progetti Google Cloud dopo novembre 2016, Blobstore scrive in un bucket Cloud Storage chiamato in base all'URL dell'app e segue il formato PROJECT_ID.appspot.com. Utilizza l'autenticazione Google per ottenere l'ID progetto per specificare il bucket Cloud Storage utilizzato per archiviare i blob in Blobstore.

Aggiornamento gestori Blobstore

Poiché Cloud Storage non supporta i gestori di caricamento e download di Blobstore, per caricare e scaricare oggetti (blob) in Cloud Storage devi utilizzare una combinazione di funzionalità di Cloud Storage, modulo libreria standard io, framework web e utilità Python.

Quanto segue mostra come aggiornare i gestori Blobstore utilizzando Flask come framework web di esempio:

  1. Sostituisci le classi del gestore dei caricamenti Blobstore con una funzione di caricamento in Flask. Segui le istruzioni per la tua versione di Python:

    Python 2

    I gestori del Blobstore in Python 2 sono classi webapp2, come mostrato nel seguente esempio di Blobstore:

    class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads()
            blob_id = uploads[0].key() if uploads else None
            store_visit(self.request.remote_addr, self.request.user_agent, blob_id)
            self.redirect('/', code=307)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Per utilizzare Cloud Storage:

    1. Sostituisci la classe di caricamento Webapp con una funzione di caricamento Flask.
    2. Sostituisci il gestore di caricamento e il routing con un metodo Flask POST decorato con routing.

    Esempio di codice aggiornato:

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    Nell'esempio di codice aggiornato di Cloud Storage, l'app ora identifica gli artefatti degli oggetti in base al nome dell'oggetto (fname) anziché blob_id. Il routing avviene anche nella parte inferiore del file dell'applicazione.

    Per ottenere l'oggetto caricato, il metodo get_uploads() di Blobstore viene sostituito con il metodo request.files.get() di Flask. In Flask, puoi utilizzare il metodo secure_filename() per ottenere un nome senza caratteri del percorso, come /, per il file e identificare l'oggetto utilizzando gcs_client.bucket(BUCKET).blob(fname) per specificare il nome del bucket e il nome dell'oggetto.

    La chiamata Cloud Storage upload_from_file() esegue il caricamento come mostrato nell'esempio aggiornato.

    Python 3

    La classe del gestore dei caricamenti in Blobstore per Python 3 è una classe di utilità e richiede l'utilizzo del dizionario environ WSGI come parametro di input, come mostrato nel seguente esempio di Blobstore:

    class UploadHandler(blobstore.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads(request.environ)
            if uploads:
                blob_id = uploads[0].key()
                store_visit(request.remote_addr, request.user_agent, blob_id)
            return redirect('/', code=307)
    ...
    @app.route('/upload', methods=['POST'])
    def upload():
        """Upload handler called by blobstore when a blob is uploaded in the test."""
        return UploadHandler().post()
    

    Per utilizzare Cloud Storage, sostituisci il metodo get_uploads(request.environ) di Blobstore con il metodo request.files.get() di Flask.

    Esempio di codice aggiornato:

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    Nell'esempio di codice aggiornato di Cloud Storage, l'app ora identifica gli artefatti degli oggetti in base al nome dell'oggetto (fname) anziché blob_id. Il routing avviene anche nella parte inferiore del file dell'applicazione.

    Per ottenere l'oggetto caricato, il metodo get_uploads() di Blobstore viene sostituito con il metodo request.files.get() di Flask. In Flask, puoi utilizzare il metodo secure_filename() per ottenere un nome senza caratteri del percorso, come /, per il file e identificare l'oggetto utilizzando gcs_client.bucket(BUCKET).blob(fname) per specificare il nome del bucket e il nome dell'oggetto.

    Il metodo Cloud Storage upload_from_file() esegue il caricamento come mostrato nell'esempio aggiornato.

  2. Sostituisci le classi del gestore dei download di Blobstore con una funzione di download in Flask. Segui le istruzioni per la tua versione di Python:

    Python 2

    Il seguente esempio di gestore dei download mostra l'utilizzo della classe BlobstoreDownloadHandler, che utilizza webapp2:

    class ViewBlobHandler(blobstore_handlers.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            self.send_blob(blob_key) if blobstore.get(blob_key) else self.error(404)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Per utilizzare Cloud Storage:

    1. Aggiorna il metodo send_blob() dell'archivio BLOB per utilizzare il metodo download_as_bytes() di Cloud Storage.
    2. Modifica il routing da webapp2 a Flask.

    Esempio di codice aggiornato:

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    Nell'esempio di codice Cloud Storage aggiornato, Flask decora la route nella funzione Flask e identifica l'oggetto utilizzando '/view/<path:fname>'. Cloud Storage identifica l'oggetto blob in base al nome dell'oggetto e al nome del bucket e utilizza il metodo download_as_bytes() per scaricare l'oggetto come byte, invece di utilizzare il metodo send_blob da Blobstore. Se l'elemento non viene trovato, l'app restituisce un errore HTTP 404.

    Python 3

    Come il gestore dei caricamenti, la classe del gestore dei download in Blobstore per Python 3 è una classe di utilità e richiede l'utilizzo del dizionario environ WSGI come parametro di input, come mostrato nel seguente esempio di Blobstore:

    class ViewBlobHandler(blobstore.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            if not blobstore.get(blob_key):
                return "Photo key not found", 404
            else:
                headers = self.send_blob(request.environ, blob_key)
    
            # Prevent Flask from setting a default content-type.
            # GAE sets it to a guessed type if the header is not set.
            headers['Content-Type'] = None
            return '', headers
    ...
    @app.route('/view/<blob_key>')
    def view_photo(blob_key):
        """View photo given a key."""
        return ViewBlobHandler().get(blob_key)
    

    Per utilizzare Cloud Storage, sostituisci send_blob(request.environ, blob_key) di Blobstore con il metodo blob.download_as_bytes() di Cloud Storage.

    Esempio di codice aggiornato:

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    Nell'esempio di codice aggiornato di Cloud Storage, blob_key viene sostituito con fname e Flask identifica l'oggetto utilizzando l'URL '/view/<path:fname>'. Il metodo gcs_client.bucket(BUCKET).blob(fname) viene utilizzato per individuare il nome file e il nome del bucket. Il metodo download_as_bytes() di Cloud Storage scarica l'oggetto come byte, invece di utilizzare il metodo send_blob() da Blobstore.

  3. Se la tua app utilizza un gestore principale, sostituisci la classe MainHandler con la funzione root() in Flask. Segui le istruzioni relative alla tua versione di Python:

    Python 2

    Di seguito è riportato un esempio di utilizzo della classe MainHandler di Blobstore:

    class MainHandler(BaseHandler):
        'main application (GET/POST) handler'
        def get(self):
            self.render_response('index.html',
                    upload_url=blobstore.create_upload_url('/upload'))
    
        def post(self):
            visits = fetch_visits(10)
            self.render_response('index.html', visits=visits)
    
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Per utilizzare Cloud Storage:

    1. Rimuovi la classe MainHandler(BaseHandler), poiché Flask gestisce il routing per te.
    2. Semplifica il codice Blobstore con Flask.
    3. Rimuovi il routing dell'app web alla fine.

    Esempio di codice aggiornato:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    Python 3

    Se hai utilizzato Flask, non avrai una classe MainHandler, ma la funzione radice Flask deve essere aggiornata se viene utilizzato l'archivio BLOB. Nell'esempio seguente viene utilizzata la funzione blobstore.create_upload_url('/upload'):

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = blobstore.create_upload_url('/upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    Per utilizzare Cloud Storage, sostituisci la funzione blobstore.create_upload_url('/upload') con il metodo url_for() di Flask per ottenere l'URL della funzione upload().

    Esempio di codice aggiornato:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload') # Updated to use url_for
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

Test e deployment dell'app

Il server di sviluppo locale ti consente di testare l'esecuzione dell'app, ma non potrai testare Cloud Storage finché non esegui il deployment di una nuova versione, perché tutte le richieste di Cloud Storage devono essere inviate tramite Internet a un bucket Cloud Storage effettivo. Consulta Test e deployment dell'applicazione per sapere come eseguire l'applicazione localmente. quindi esegui il deployment di una nuova versione per verificare che l'app abbia lo stesso aspetto di prima.

App che utilizzano App Engine NDB o Cloud NDB

Devi aggiornare il modello dei dati Datastore se l'app utilizza App Engine NDB o Cloud NDB per includere le proprietà relative al Blobstore.

Aggiorna il modello dei dati

Poiché le proprietà BlobKey di NDB non sono supportate da Cloud Storage, devi modificare le righe relative al Blobstore per utilizzare gli equivalenti integrati di NDB, framework web o altrove.

Per aggiornare il modello dei dati:

  1. Trova le righe che utilizzano BlobKey nel modello dei dati, ad esempio:

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.BlobKeyProperty()
    
  2. Sostituisci ndb.BlobKeyProperty() con ndb.StringProperty():

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.StringProperty() # Modified from ndb.BlobKeyProperty()
    
  3. Se esegui anche l'upgrade da App Engine NDB a Cloud NDB durante la migrazione, consulta la guida alla migrazione di Cloud NDB per ottenere indicazioni su come eseguire il refactoring del codice NDB per utilizzare i gestori di contesto Python.

Compatibilità con le versioni precedenti del modello dei dati Datastore

Nella sezione precedente, la sostituzione di ndb.BlobKeyProperty con ndb.StringProperty ha reso l'app incompatibile con le versioni precedenti, il che significa che l'app non sarà in grado di elaborare voci precedenti create da Blobstore. Se devi conservare i dati precedenti, crea un campo aggiuntivo per le nuove voci di Cloud Storage anziché aggiornare il campo ndb.BlobKeyProperty e crea una funzione per normalizzare i dati.

In base agli esempi nelle sezioni precedenti, apporta le seguenti modifiche:

  1. Crea due campi di proprietà separati quando definisci il modello dei dati. Utilizza la proprietà file_blob per identificare gli oggetti creati da Blobstore e la proprietà file_gcs per identificare gli oggetti creati da Cloud Storage:

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.BlobKeyProperty()  # backwards-compatibility
        file_gcs  = ndb.StringProperty()
    
  2. Individua le righe che fanno riferimento a nuove visite, come riportato di seguito:

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_blob=upload_key).put()
    
  3. Modifica il tuo codice in modo che file_gcs venga utilizzato per le voci recenti. Ad esempio:

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_gcs=upload_key).put() # change file_blob to file_gcs for new requests
    
  4. Crea una nuova funzione per normalizzare i dati. L'esempio seguente mostra l'utilizzo di estrazione, trasformazione e basso (ETL) per eseguire il loop di tutte le visite e porta i dati relativi a visitatori e timestamp a verificare se esistono file_gcs o file_gcs:

    def etl_visits(visits):
        return [{
                'visitor': v.visitor,
                'timestamp': v.timestamp,
                'file_blob': v.file_gcs if hasattr(v, 'file_gcs') \
                        and v.file_gcs else v.file_blob
                } for v in visits]
    
  5. Trova la riga che fa riferimento alla funzione fetch_visits():

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    
  6. Inserisci fetch_visits() all'interno della funzione etl_visits(), ad esempio:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = etl_visits(fetch_visits(10)) # etl_visits wraps around fetch_visits
        return render_template('index.html', **context)
    

Esempi

Passaggi successivi