Migrar el almacén de blobs de App Engine a Cloud Storage

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

Cloud Storage es similar a Blobstore de App Engine en el sentido de que puedes usar Cloud Storage para servir objetos de datos grandes (blobs), como archivos de vídeo o de imagen, y permitir que tus usuarios suban archivos de datos grandes. Aunque solo se puede acceder a Blobstore de App Engine a través de los servicios empaquetados antiguos de App Engine, Cloud Storage es un producto independiente al que se accede a través de las bibliotecas de cliente de Cloud. Google CloudCloud Storage ofrece a tu aplicación una solución de almacenamiento de objetos más moderna y te da la flexibilidad de migrar a Cloud Run u otra plataforma de alojamiento de aplicaciones más adelante. Google Cloud

En los Google Cloud proyectos creados después de noviembre del 2016, Blobstore usa segmentos de Cloud Storage en segundo plano. Esto significa que, cuando migres tu aplicación a Cloud Storage, todos los objetos y permisos que tengas en los segmentos de Cloud Storage no cambiarán. También puedes empezar a acceder a esos segmentos con las bibliotecas de cliente de Cloud para Cloud Storage.

Diferencias y similitudes clave

Cloud Storage excluye las siguientes dependencias y limitaciones de Blobstore:

  • La API Blobstore para Python 2 depende de webapp.
  • La API Blobstore para Python 3 usa clases de utilidad para usar controladores de Blobstore.
  • En Blobstore, el número máximo de archivos que se pueden subir es 500. No hay límite en el número de objetos que puedes crear en un segmento de Cloud Storage.

Cloud Storage no admite lo siguiente:

  • Clases de controlador de Blobstore
  • Objetos de Blobstore

Similitudes entre Cloud Storage y Blobstore de App Engine:

  • Puede leer y escribir objetos de datos de gran tamaño en un entorno de tiempo de ejecución, así como almacenar y servir objetos de datos estáticos de gran tamaño, como películas, imágenes u otro contenido estático. El límite de tamaño de los objetos de Cloud Storage es de 5 TiB.
  • Permite almacenar objetos en un segmento de Cloud Storage.
  • Tener un nivel gratuito.

Antes de empezar

  • Debes revisar y comprender los precios y las cuotas de Cloud Storage:
  • Tener una aplicación de App Engine de Python 2 o Python 3 que use Blobstore.
  • En los ejemplos de esta guía se muestra una aplicación que migra a Cloud Storage con el framework Flask. Ten en cuenta que puedes usar cualquier framework web, incluido webapp2, al migrar a Cloud Storage.

Información general

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

  1. Actualizar archivos de configuración
  2. Actualiza tu aplicación Python:
    • Actualizar el framework web
    • Importar e inicializar Cloud Storage
    • Actualizar controladores de Blobstore
    • Opcional: Actualiza tu modelo de datos si usas Cloud NDB o App Engine NDB
  3. Probar y desplegar una aplicación

Actualizar los archivos de configuración

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

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

    Python 2

    En el caso de las aplicaciones Python 2:

    1. Elimina la sección handlers y las dependencias de aplicaciones web innecesarias de la sección libraries.
    2. Si usas bibliotecas de cliente de Cloud, añade las versiones más recientes de las bibliotecas grpcio y setuptools.
    3. Añade la biblioteca ssl, ya que Cloud Storage la necesita.

    A continuación, se muestra un ejemplo de archivo app.yaml 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

    En las aplicaciones de Python 3, elimina todas las líneas excepto el elemento runtime. Por ejemplo:

    runtime: python310 # or another support version
    

    El tiempo de ejecución de Python 3 instala las bibliotecas automáticamente, por lo que no es necesario especificar las bibliotecas integradas del tiempo de ejecución de Python 2 anterior. Si tu aplicación Python 3 usa otros servicios empaquetados antiguos al migrar a Cloud Storage, deja el archivo app.yaml tal cual.

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

    Python 2

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

    google-cloud-storage
    

    A continuación, ejecuta pip install -t lib -r requirements.txt para actualizar la lista de bibliotecas disponibles para tu aplicación.

    Python 3

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

    google-cloud-storage
    

    App Engine instala automáticamente estas dependencias durante la implementación de la aplicación en el tiempo de ejecución de Python 3, así que elimina la carpeta lib si existe.

  3. En el caso de las aplicaciones de Python 2, si tu aplicación 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)
    

Actualizar una aplicación Python

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

Actualizar tu framework web de Python 2

En el caso de las aplicaciones de Python 2 que usan el framework webapp2, se recomienda migrar del framework webapp2 obsoleto. Consulta el calendario de asistencia del entorno de ejecución para ver la fecha de finalización de la asistencia de Python 2.

Puedes migrar a otro framework web, como Flask, Django o WSGI. Como Cloud Storage excluye las dependencias de webapp2 y los controladores de Blobstore no son compatibles, puedes eliminar o sustituir otras bibliotecas relacionadas con aplicaciones web.

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

Si tienes previsto usar los servicios de Google Cloud además de Cloud Storage o acceder a las versiones de tiempo de ejecución más recientes, te recomendamos que actualices tu aplicación al tiempo de ejecución de Python 3. Para obtener más información, consulta la información general sobre la migración de Python 2 a Python 3.

Importar e inicializar Cloud Storage

Modifica los archivos de tu aplicación actualizando las líneas de importación e inicialización:

  1. Elimina las instrucciones 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. Añade las instrucciones de importación de Cloud Storage y las bibliotecas de autenticación de Google, como las siguientes:

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

    Se necesita la biblioteca de autenticación de Google para obtener el mismo ID de proyecto que se usó en Blobstore para Cloud Storage. Importa otras bibliotecas, como Cloud NBD, si procede en tu aplicación.

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

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

    En los proyectos posteriores a noviembre del 2016, Blobstore escribe en un segmento de Cloud Storage con el nombre de la URL de tu aplicación y sigue el formato PROJECT_ID.appspot.com. Google Cloud Usas la autenticación de Google para obtener el ID de proyecto y especificar el segmento de Cloud Storage que se usa para almacenar blobs en Blobstore.

Actualizar controladores de Blobstore

Como Cloud Storage no admite los controladores de subida y descarga de Blobstore, debes usar una combinación de funciones de Cloud Storage, el módulo de biblioteca estándar io, tu framework web y las utilidades de Python para subir y descargar objetos (blobs) en Cloud Storage.

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

  1. Sustituye las clases de controlador de subida de Blobstore por una función de subida en Flask. Sigue las instrucciones correspondientes a 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, sigue estos pasos:

    1. Sustituye la clase de subida de la aplicación web por una función de subida de Flask.
    2. Sustituye el controlador de subidas y el enrutamiento por un método POST de Flask decorado con el enrutamiento.

    Ejemplo de código actualizado:

    @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 el ejemplo de código de Cloud Storage actualizado, la aplicación ahora identifica los artefactos de objeto por su nombre de 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 sustituye por el método request.files.get() de Flask. En Flask, puedes usar el método secure_filename() para obtener el nombre del archivo sin caracteres de ruta, como /, e identificar el objeto mediante gcs_client.bucket(BUCKET).blob(fname) para especificar el nombre del contenedor y el nombre del objeto.

    La llamada a Cloud Storage upload_from_file() realiza la subida como se muestra en el ejemplo actualizado.

    Python 3

    La clase de controlador de subida de Blobstore para Python 3 es una clase de utilidad y requiere el uso del diccionario WSGI environ como parámetro de entrada, tal como se muestra en el 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, sustituye el método get_uploads(request.environ) de Blobstore por el método request.files.get() de Flask.

    Ejemplo de código actualizado:

    @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 el ejemplo de código de Cloud Storage actualizado, la aplicación ahora identifica los artefactos de objeto por su nombre de 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 sustituye por el método request.files.get() de Flask. En Flask, puedes usar el método secure_filename() para obtener el nombre del archivo sin caracteres de ruta, como /, e identificar el objeto mediante gcs_client.bucket(BUCKET).blob(fname) para especificar el nombre del contenedor y el nombre del objeto.

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

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

    Python 2

    En el siguiente ejemplo de controlador de descargas 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, sigue estos pasos:

    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.

    Ejemplo de código actualizado:

    @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 el ejemplo de código de Cloud Storage actualizado, Flask decora la ruta en la función de Flask e identifica el objeto mediante '/view/<path:fname>'. Cloud Storage identifica el blobobjeto por su nombre y el nombre del segmento, 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 devuelve un error HTTP 404.

    Python 3

    Al igual que el controlador de subida, la clase de controlador de descarga de Blobstore para Python 3 es una clase de utilidad y requiere el uso del diccionario WSGI environ como parámetro de entrada, tal 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, sustituye send_blob(request.environ, blob_key) de Blobstore por el método blob.download_as_bytes() de Cloud Storage.

    Ejemplo de código actualizado:

    @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 el ejemplo de código de Cloud Storage actualizado, blob_key se sustituye 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 localizar el nombre del archivo y el nombre del contenedor. 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 aplicación usa un controlador principal, sustituye la clase MainHandler por la función root() en Flask. Sigue las instrucciones correspondientes a tu versión de Python:

    Python 2

    A continuación, se muestra un ejemplo de 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, sigue estos pasos:

    1. Elimina la clase MainHandler(BaseHandler), ya que Flask se encarga de las rutas.
    2. Simplifica el código de Blobstore con Flask.
    3. Elimina el enrutamiento de la aplicación web al final.

    Ejemplo de código actualizado:

    @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 has usado Flask, no tendrás una clase MainHandler, pero tendrás que actualizar tu función raíz de Flask si usas 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, sustituye la función blobstore.create_upload_url('/upload') por el método url_for() de Flask para obtener la URL de la función upload().

    Ejemplo de código actualizado:

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

Probar y desplegar una aplicación

El servidor de desarrollo local te permite comprobar que tu aplicación se ejecuta, pero no podrás probar Cloud Storage hasta que implementes una nueva versión, ya que todas las solicitudes de Cloud Storage deben enviarse a través de Internet a un segmento de Cloud Storage real. Consulta el artículo Probar y desplegar una aplicación para saber cómo ejecutar tu aplicación de forma local. A continuación, despliega una nueva versión para confirmar que la aplicación se muestra igual que antes.

Aplicaciones que usan App Engine NDB o Cloud NDB

Si tu aplicación usa App Engine NDB o Cloud NDB, debes actualizar el modelo de datos de Datastore para incluir propiedades relacionadas con Blobstore.

Actualizar el modelo de datos

Como Cloud Storage no admite las propiedades BlobKey de NDB, debes modificar las líneas relacionadas con Blobstore para usar los equivalentes integrados de NDB, los frameworks web u otros.

Para actualizar tu modelo de datos, sigue estos pasos:

  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 vas a actualizar de App Engine NDB a Cloud NDB durante la migración, consulta la guía de migración de Cloud NDB para obtener información sobre cómo refactorizar el código NDB para usar gestores de contexto de Python.

Retrocompatibilidad del modelo de datos de Datastore

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

A partir de los ejemplos de las secciones anteriores, haz los siguientes cambios:

  1. Crea dos campos de propiedad independientes al definir tu modelo de datos. Utilice la propiedad file_blob para identificar los objetos creados en Blobstore y la propiedad file_gcs para identificar los objetos creados en 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 hagan referencia a las nuevas visitas, 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 se use file_gcs en las 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 para normalizar los datos. En el siguiente ejemplo se muestra el uso de la extracción, la transformación y la carga (ETL) para recorrer todas las visitas y se toman los datos del visitante y de la marca de tiempo para comprobar si existen 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. 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. Encierra la función fetch_visits() entre 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

Siguientes pasos