Migra de Blobstore de App Engine a Cloud Storage

En esta guía, se explica cómo migrar de Blobstore de App Engine a Cloud Storage.

Cloud Storage es similar a Blobstore de App Engine en que puedes usar Cloud Storage para entregar grandes objetos de datos (BLOB), como archivos de imagen o video, y permitir que tus usuarios suban archivos grandes de datos. Si bien solo es posible acceder a Blobstore de App Engine a través de los servicios agrupados en paquetes heredados de App Engine, Cloud Storage es un producto independiente de Google Cloud al que se accede a través de las bibliotecas cliente de Cloud. Cloud Storage ofrece a tu app una solución de almacenamiento de objetos más moderna y te brinda la flexibilidad de migrar a Cloud Run o a otra plataforma de hosting de apps de Google Cloud más adelante.

Para los proyectos de Google Cloud creados después de noviembre de 2016, Blobstore usa buckets de Cloud Storage en segundo plano. Esto significa que cuando migras tu app a Cloud Storage, todos tus objetos y permisos existentes en esos buckets de Cloud Storage existentes no se modifican. También puedes comenzar a acceder a esos buckets existentes con las bibliotecas cliente de Cloud para Cloud Storage.

Similitudes y diferencias clave

Cloud Storage excluye las siguientes dependencias y limitaciones de Blobstore:

  • La API de Blobstore para Python 2 tiene una dependencia en webapp.
  • La API de Blobstore para Python 3 usa clases de utilidad a fin de utilizar controladores de Blobstore.
  • Para Blobstore, la cantidad máxima de archivos que se pueden subir a Blobstore es de 500. No hay límite para la cantidad de objetos que puedes crear en un bucket de Cloud Storage.

Cloud Storage no admite lo siguiente:

  • Clases de controladores de Blobstore
  • Objetos de Blobstore

Similitudes de Cloud Storage y Blobstore de App Engine:

  • Puede leer y escribir grandes objetos de datos grandes un entorno de ejecución, así como almacenar y entregar grandes objetos de datos estáticos, como películas, imágenes o cualquier otro contenido estático. El límite de tamaño de los objetos para Cloud Storage es de 5 TiB.
  • Te permite almacenar objetos en un bucket de Cloud Storage.
  • Tiene un nivel gratuito.

Antes de comenzar

  • Debes revisar y comprender los precios y las cuotas de Cloud Storage:
  • Debes tener una app de App Engine de Python 2 o Python 3 existente que use Blobstore.
  • En los ejemplos de esta guía, se muestra una app que migra a Cloud Storage mediante el framework de Flask. Ten en cuenta que puedes usar cualquier framework web, incluso para permanecer en webapp2, cuando migres a Cloud Storage.

Descripción general

A mayor escala, el proceso para migrar a Cloud Storage desde Blobstore de App Engine consta de los siguientes pasos:

  1. Actualiza archivos de configuración
  2. Actualiza la app de Python:
    • Actualiza el framework web
    • Importa y, luego, inicializa Cloud Storage
    • Actualiza los controladores de Blobstore
    • Opcional: Actualiza tu modelo de datos si usas Cloud NDB o App Engine NDB
  3. Implementar y probar tu app

Actualiza archivos de configuración

Antes de modificar el código de la aplicación para pasar de Blobstore a Cloud Storage, actualiza tus archivos de configuración a fin de usar la biblioteca de Cloud Storage.

  1. Actualiza el archivo app.yaml: Sigue las instrucciones para tu versión de Python:

    Python 2

    Para las apps de Python 2:

    1. Quita la sección handlers y cualquier dependencia de webapp innecesaria en la sección libraries.
    2. Si usas las bibliotecas cliente de Cloud, agrega las versiones más recientes de las bibliotecas grpcio y setuptools.
    3. Agrega la biblioteca ssl, ya que Cloud Storage lo requiere.

    El siguiente es un archivo app.yaml de ejemplo con los cambios realizados:

    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

    Para las apps de Python 3, borra todas las líneas, excepto el elemento runtime. Por ejemplo:

    runtime: python310 # or another support version
    

    El entorno de ejecución de Python 3 instala bibliotecas de forma automática, por lo que no es necesario especificar bibliotecas integradas del entorno de ejecución de Python 2 anterior. Si tu app de Python 3 usa otros servicios agrupados en paquetes heredados cuando migras a Cloud Storage, deja el archivo app.yaml tal como está.

  2. Actualiza el archivo requirements.txt: Sigue las instrucciones para tu versión de Python:

    Python 2

    Agrega las bibliotecas cliente de Cloud para Cloud Storage a tu lista de dependencias en el archivo requirements.txt.

    google-cloud-storage
    

    Luego, ejecuta pip install -t lib -r requirements.txt a fin de actualizar la lista de bibliotecas disponibles para la app.

    Python 3

    Agrega las bibliotecas cliente de Cloud para Cloud Storage a tu lista de dependencias en el archivo requirements.txt.

    google-cloud-storage
    

    App Engine instala de forma automática estas dependencias durante la implementación de la app en el entorno de ejecución de Python 3, así que borra la carpeta lib, si existe una.

  3. En el caso de las apps de Python 2, si tu app usa bibliotecas integradas o copiadas, debes especificar esas rutas en el archivo 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)
    

Actualiza la app de Python

Después de modificar los archivos de configuración, actualiza tu app de Python.

Actualiza tu framework web de Python 2

Para las apps de Python 2 que usan el framework webapp2, se recomienda migrar fuera del framework webapp2 desactualizado. Consulta el programa de asistencia para el entorno de ejecución a fin de conocer la fecha de finalización de la asistencia de Python 2.

Puedes migrar a otro framework web, como Flask, Django o WSGI. Dado que Cloud Storage excluye las dependencias en webapp2 y los controladores de Blobstore no son compatibles, puedes borrar o reemplazar otras bibliotecas relacionadas con webapp.

Si decides seguir usando webapp2, ten en cuenta que los ejemplos de esta guía usan Cloud Storage con Flask.

Si planeas usar los servicios de Google Cloud además de Cloud Storage, o si deseas obtener acceso a las versiones más recientes del entorno de ejecución, considera actualizar tu app al entorno de ejecución de Python 3. Para obtener más información, consulta Descripción general de la migración de Python 2 a Python 3.

Importa y, luego, inicializa Cloud Storage

Para modificar los archivos de la aplicación, actualiza las líneas de importación y de inicialización:

  1. Quita las declaraciones de importación de Blobstore, como las siguientes:

    import webapp2
    from google.appengine.ext import blobstore
    from google.appengine.ext.webapp import blobstore_handlers
    
  2. Agrega las declaraciones de importación para Cloud Storage y las bibliotecas de autenticación de Google, como se muestra a continuación:

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

    La biblioteca de autenticación de Google se necesita para obtener el mismo ID del proyecto que se usó en Blobstore para Cloud Storage. Importa otras bibliotecas como Cloud NBD si corresponde a tu app.

  3. Crea un cliente nuevo para Cloud Storage y especifica el bucket que se usa en Blobstore. Por ejemplo:

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

    Para los proyectos de Google Cloud posteriores a noviembre de 2016, Blobstore escribe en un bucket de Cloud Storage con el nombre de la URL de tu app y sigue el formato de PROJECT_ID.appspot.com. Usa la autenticación de Google a fin de obtener el ID del proyecto para especificar el bucket de Cloud Storage que se usa para almacenar BLOB en Blobstore.

Actualiza los controladores de Blobstore

Debido a que Cloud Storage no admite los controladores de carga y descarga de Blobstore, debes usar una combinación de la funcionalidad de Cloud Storage, el módulo de biblioteca estándar io, tu framework web y las utilidades de Python para subir y descargar objetos (BLOB) en Cloud Storage.

A continuación, se muestra cómo actualizar los controladores de Blobstore con Flask como framework web de ejemplo:

  1. Reemplaza las clases de controladores de carga de Blobstore por una función de carga en Flask. Sigue las instrucciones para tu versión de Python:

    Python 2

    Los controladores de Blobstore en Python 2 son clases webapp2, como se muestra en el siguiente ejemplo de 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)
    

    Para usar Cloud Storage, haz lo siguiente:

    1. Reemplaza la clase de carga de webapp por la función de carga de Flask.
    2. Reemplaza el controlador de carga y el enrutamiento por un método POST de Flask decorado con enrutamiento.

    Muestra de código actualizada:

    @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)
    

    En la muestra de código de Cloud Storage actualizada, la app ahora identifica artefactos de objetos por el nombre del objeto (fname) en lugar de blob_id. El enrutamiento también se produce en la parte inferior del archivo de la aplicación.

    Para obtener el objeto subido, el método get_uploads() de Blobstore se reemplaza por el método request.files.get() de Flask. En Flask, puedes usar el método secure_filename() para obtener un nombre sin caracteres de ruta, como /, para el archivo y, también, identificar el objeto mediante gcs_client.bucket(BUCKET).blob(fname) para especificar el nombre del bucket y el nombre del objeto.

    La llamada upload_from_file() de Cloud Storage realiza la carga, como se muestra en el ejemplo actualizado.

    Python 3

    La clase de controlador de carga en Blobstore para Python 3 es una clase de utilidad y requiere usar el diccionario environ de WSGI como parámetro de entrada, como se muestra en siguiente ejemplo de 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()
    

    Para usar Cloud Storage, reemplaza el método get_uploads(request.environ) de Blobstore por el método request.files.get() de Flask.

    Muestra de código actualizada:

    @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)
    

    En la muestra de código de Cloud Storage actualizada, la app ahora identifica artefactos de objetos por el nombre del objeto (fname) en lugar de blob_id. El enrutamiento también se produce en la parte inferior del archivo de la aplicación.

    Para obtener el objeto subido, el método get_uploads() de Blobstore se reemplaza por el método request.files.get() de Flask. En Flask, puedes usar el método secure_filename() para obtener un nombre sin caracteres de ruta, como /, para el archivo y, también, identificar el objeto mediante gcs_client.bucket(BUCKET).blob(fname) para especificar el nombre del bucket y el nombre del objeto.

    El método upload_from_file() de Cloud Storage realiza la carga, como se muestra en el ejemplo actualizado.

  2. Reemplaza las clases de controlador de descarga de Blobstore por una función de descarga en Flask. Sigue las instrucciones para tu versión de Python:

    Python 2

    En el siguiente ejemplo del controlador de descarga, se muestra el uso de la clase BlobstoreDownloadHandler, que usa 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)
    

    Para usar Cloud Storage, haz lo siguiente:

    1. Actualiza el método send_blob() de Blobstore para usar el método download_as_bytes() de Cloud Storage.
    2. Cambia el enrutamiento de webapp2 a Flask.

    Muestra de código actualizada:

    @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)
    

    En la muestra de código de Cloud Storage actualizada, Flask decora la ruta en la función Flask y, también, identifica el objeto con '/view/<path:fname>'. Cloud Storage identifica el objeto blob por el nombre del objeto y el nombre del bucket, y usa el método download_as_bytes() para descargar el objeto como bytes, en lugar de usar el método send_blob de Blobstore. Si no se encuentra el artefacto, la aplicación muestra un error de 404 de HTTP.

    Python 3

    Al igual que el controlador de carga, la clase de controlador de descarga en Blobstore para Python 3 es una clase de utilidad y requiere el uso del diccionario environ de WSGI como parámetro de entrada, como se muestra en el siguiente ejemplo de 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)
    

    Para usar Cloud Storage, reemplaza send_blob(request.environ, blob_key) de Blobstore por el método blob.download_as_bytes() de Cloud Storage.

    Muestra de código actualizada:

    @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)
    

    En la muestra de código de Cloud Storage actualizada,blob_key se reemplaza por fname, y Flask identifica el objeto mediante la URL '/view/<path:fname>'. El método gcs_client.bucket(BUCKET).blob(fname) se usa para ubicar el nombre del archivo y el nombre del bucket. El método download_as_bytes() de Cloud Storage descarga el objeto como bytes, en lugar de usar el método send_blob() de Blobstore.

  3. Si tu app usa un controlador principal, reemplaza la clase MainHandler por la función root() en Flask. Sigue las instrucciones para tu versión de Python:

    Python 2

    El siguiente es un ejemplo del uso de la clase MainHandler de 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)
    

    Para usar Cloud Storage, haz lo siguiente:

    1. Quita la clase MainHandler(BaseHandler), ya que Flask se encarga del enrutamiento.
    2. Simplifica el código de Blobstore con Flask.
    3. Quita el enrutamiento de webapp al final.

    Muestra de código actualizada:

    @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

    Si usaste Flask, no tendrás una clase MainHandler, pero la función raíz de Flask debe actualizarse si se usa blobstore. En el siguiente ejemplo, se usa la función 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)
    

    Para usar Cloud Storage, reemplaza la función blobstore.create_upload_url('/upload') por el método url_for() de Flask a fin de obtener la URL para la función upload().

    Muestra de código actualizada:

    @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)
    

Implementar y probar tu app

El servidor de desarrollo local te permite probar que la app se ejecute, pero no podrá probar Cloud Storage hasta que implementes una versión nueva, ya que todas las solicitudes de Cloud Storage deben enviarse a través de Internet a un bucket de Cloud Storage real. Consulta Implementa y prueba tu aplicación para saber cómo ejecutarla de forma local. Luego, implementa una versión nueva para confirmar que la app aparezca igual que antes.

Aplicaciones que usan App Engine NDB o Cloud NDB

Debes actualizar tu modelo de datos de Datastore si tu app usa App Engine NDB o Cloud NDB para incluir las propiedades relacionadas con Blobstore.

Actualiza tu modelo de datos

Debido a que las propiedades BlobKey de NDB no son compatibles con Cloud Storage, debes modificar las líneas relacionadas con Blobstore para usar equivalentes integrados de NDB, frameworks web o en otro lugar.

Para actualizar tu modelo de datos, haz lo siguiente:

  1. Busca las líneas que usan BlobKey en el modelo de datos, como las siguientes:

    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. Reemplaza ndb.BlobKeyProperty() por 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. Si también actualizas de App Engine NDB a Cloud NDB durante la migración, consulta la guía de migración de Cloud NDB a fin de obtener orientación sobre cómo refactorizar el código NDB para usar administradores de contexto de Python.

Retrocompatibilidad para el modelo de datos de Datastore

En la sección anterior, el reemplazo de ndb.BlobKeyProperty por ndb.StringProperty hizo que la app sea incompatible con versiones anteriores, lo que significa que la app no podrá procesar las entradas anteriores que creó Blobstore. Si necesitas conservar datos antiguos, crea un campo adicional para las entradas nuevas de Cloud Storage en lugar de actualizar el campo ndb.BlobKeyProperty, y crea una función a fin de normalizar los datos.

En los ejemplos de las secciones anteriores, realiza los siguientes cambios:

  1. Crea dos campos de propiedad independientes cuando definas tu modelo de datos. Usa la propiedad file_blob para identificar los objetos creados por Blobstore y la propiedad file_gcs para identificar los objetos creados por 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. Busca las líneas que hacen referencia a visitas nuevas, como las siguientes:

    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. Cambia tu código para que file_gcs se use en entradas recientes. Por ejemplo:

    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 función nueva para normalizar los datos. En el siguiente ejemplo, se muestra el uso de extracción, transformación y carga (ETL) para recorrer en bucle todas las visitas, y se toman los datos de visitantes y marcas de tiempo a fin de verificar si file_gcs o file_gcs. existe:

    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. Busca la línea que hace referencia a la función 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. Une fetch_visits() dentro de la función etl_visits(), por ejemplo:

    @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)
    

Ejemplos

¿Qué sigue?