Migre o Blobstore do App Engine para o Cloud Storage

Este guia aborda como migrar do Blobstore do App Engine para o Cloud Storage.

O Cloud Storage é semelhante ao Blobstore do App Engine, uma vez que pode usar o Cloud Storage para publicar objetos de dados grandes (blobs), como ficheiros de vídeo ou de imagem, e permitir que os utilizadores carreguem ficheiros de dados grandes. Embora o App Engine Blobstore seja acessível apenas através dos serviços agrupados legados do App Engine, o Cloud Storage é um produto autónomo Google Cloud que é acedido através das bibliotecas cliente da Google Cloud. O Cloud Storage oferece à sua app uma solução de armazenamento de objetos mais moderna e dá-lhe a flexibilidade de migrar para o Cloud Run ou outra plataforma de alojamento de Google Cloud apps Google Cloud mais tarde.

Para Google Cloud projetos criados após novembro de 2016, o Blobstore usa contentores do Cloud Storage nos bastidores. Isto significa que, quando migrar a sua app para o Cloud Storage, todos os objetos e autorizações existentes nesses contentores do Cloud Storage permanecem inalterados. Também pode começar a aceder a esses contentores existentes através das bibliotecas cliente do Google Cloud para o Cloud Storage.

Principais diferenças e semelhanças

O Cloud Storage exclui as seguintes dependências e limitações do Blobstore:

  • A API Blobstore para Python 2 tem uma dependência de webapp.
  • A Blobstore API para Python 3 usa classes de utilidade para usar controladores do Blobstore.
  • Para o Blobstore, o número máximo de ficheiros que podem ser carregados para o Blobstore é 500. Não existe limite para o número de objetos que pode criar num contentor do Cloud Storage.

O Cloud Storage não suporta:

  • Classes de processadores do Blobstore
  • Objetos do Blobstore

Semelhanças entre o Cloud Storage e o Blobstore do App Engine:

  • Capacidade de ler e escrever objetos de dados grandes num ambiente de tempo de execução, bem como armazenar e publicar objetos de dados grandes estáticos, como filmes, imagens ou outro conteúdo estático. O limite de tamanho do objeto para o Cloud Storage é de 5 TiB.
  • Permite-lhe armazenar objetos num contentor do Cloud Storage.
  • Ter um nível gratuito.

Antes de começar

  • Deve rever e compreender os preços e as quotas do Cloud Storage:
  • Ter uma app do App Engine Python 2 ou Python 3 existente que esteja a usar o Blobstore.
  • Os exemplos neste guia mostram uma app que migra para o Cloud Storage através da framework Flask. Tenha em atenção que pode usar qualquer framework Web, incluindo permanecer no webapp2, quando migrar para o Cloud Storage.

Vista geral

A um nível elevado, o processo de migração para o Cloud Storage a partir do Blobstore do App Engine consiste nos seguintes passos:

  1. Atualize os ficheiros de configuração
  2. Atualize a sua app Python:
    • Atualize a sua framework Web
    • Importe e inicialize o Cloud Storage
    • Atualize os controladores do Blobstore
    • Opcional: atualize o seu modelo de dados se usar o Cloud NDB ou o App Engine NDB
  3. Teste e implemente a sua app

Atualize os ficheiros de configuração

Antes de modificar o código da aplicação para mudar do Blobstore para o Cloud Storage, atualize os ficheiros de configuração para usar a biblioteca do Cloud Storage.

  1. Atualize o ficheiro app.yaml. Siga as instruções para a sua versão do Python:

    Python 2

    Para apps Python 2:

    1. Remova a secção handlers e todas as dependências da app Web desnecessárias na secção libraries.
    2. Se usar as bibliotecas cliente da Google Cloud, adicione as versões mais recentes das bibliotecas grpcio e setuptools.
    3. Adicione a biblioteca ssl, uma vez que é necessária para o Cloud Storage.

    Segue-se um exemplo de um ficheiro app.yaml com as alterações feitas:

    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 apps Python 3, elimine todas as linhas, exceto o elemento runtime. Por exemplo:

    runtime: python310 # or another support version
    

    O tempo de execução do Python 3 instala bibliotecas automaticamente, pelo que não tem de especificar bibliotecas incorporadas do tempo de execução do Python 2 anterior. Se a sua app Python 3 estiver a usar outros serviços agrupados antigos quando migrar para o Cloud Storage, deixe o ficheiro app.yaml tal como está.

  2. Atualize o ficheiro requirements.txt. Siga as instruções para a sua versão do Python:

    Python 2

    Adicione as bibliotecas de cliente da nuvem para o Cloud Storage à sua lista de dependências no ficheiro requirements.txt.

    google-cloud-storage
    

    Em seguida, execute pip install -t lib -r requirements.txt para atualizar a lista de bibliotecas disponíveis para a sua app.

    Python 3

    Adicione as bibliotecas de cliente do Google Cloud para o Cloud Storage à sua lista de dependências no ficheiro requirements.txt.

    google-cloud-storage
    

    O App Engine instala automaticamente estas dependências durante a implementação da app no runtime do Python 3. Por isso, elimine a pasta lib, se existir.

  3. Para apps Python 2, se a sua app estiver a usar bibliotecas incorporadas ou copiadas, tem de especificar esses caminhos no ficheiro 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)
    

Atualize a sua app Python

Depois de modificar os ficheiros de configuração, atualize a sua app Python.

Atualize a sua framework Web Python 2

Para apps Python 2 que usam a framework webapp2, é recomendável migrar da framework webapp2 desatualizada. Consulte o cronograma de compatibilidade do tempo de execução para saber a data de fim da compatibilidade do Python 2.

Pode migrar para outra framework Web, como o Flask, o Django ou o WSGI. Uma vez que o armazenamento na nuvem exclui dependências de webapp2 e os controladores do Blobstore não são suportados, pode eliminar ou substituir outras bibliotecas relacionadas com a app Web.

Se optar por continuar a usar webapp2, tenha em atenção que os exemplos ao longo deste guia usam o Cloud Storage com o Flask.

Se planeia usar os serviços do Google Cloud além do armazenamento na nuvem ou obter acesso às versões de tempo de execução mais recentes, deve considerar atualizar a sua app para o tempo de execução do Python 3. Para mais informações, consulte a vista geral da migração do Python 2 para o Python 3.

Importe e inicialize o Cloud Storage

Modifique os ficheiros da aplicação atualizando as linhas de importação e inicialização:

  1. Remova as declarações de importação do Blobstore, como as seguintes:

    import webapp2
    from google.appengine.ext import blobstore
    from google.appengine.ext.webapp import blobstore_handlers
    
  2. Adicione as declarações de importação para o Cloud Storage e as bibliotecas de autenticação da Google, como as seguintes:

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

    A biblioteca de autenticação Google é necessária para obter o mesmo ID do projeto que foi usado no Blobstore para o Cloud Storage. Importe outras bibliotecas, como a Cloud NBD, se aplicável à sua app.

  3. Crie um novo cliente para o Cloud Storage e especifique o contentor que é usado no Blobstore. Por exemplo:

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

    Para Google Cloud projetos posteriores a novembro de 2016, o Blobstore escreve num contentor do Cloud Storage com o nome do URL da sua app e segue o formato PROJECT_ID.appspot.com. Usa a autenticação Google para obter o ID do projeto para especificar o contentor do Cloud Storage que é usado para armazenar blobs no Blobstore.

Atualize os controladores do Blobstore

Uma vez que o Cloud Storage não suporta os controladores de carregamento e transferência do Blobstore, tem de usar uma combinação da funcionalidade do Cloud Storage, do módulo da biblioteca padrão, da sua framework Web e das utilidades do Python para carregar e transferir objetos (blobs) no Cloud Storage.io

O exemplo seguinte demonstra como atualizar os controladores da Blobstore usando o Flask como a framework Web de exemplo:

  1. Substitua as classes do controlador de carregamento do Blobstore por uma função de carregamento no Flask. Siga as instruções para a sua versão do Python:

    Python 2

    Os controladores Blobstore no Python 2 são webapp2 classes, conforme mostrado no seguinte exemplo do 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 o Cloud Storage:

    1. Substitua a classe de carregamento da app Web por uma função de carregamento do Flask.
    2. Substitua o controlador de carregamento e o encaminhamento por um método POST do Flask decorado com o encaminhamento.

    Exemplo de código atualizado:

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

    No exemplo de código do Cloud Storage atualizado, a app identifica agora os artefactos de objetos pelo respetivo nome do objeto (fname) em vez de blob_id. O encaminhamento também ocorre na parte inferior do ficheiro de aplicação.

    Para obter o objeto carregado, o método get_uploads() do Blobstore é substituído pelo método request.files.get() do Flask. No Flask, pode usar o método secure_filename() para obter um nome sem carateres de caminho, como /, para o ficheiro, e identificar o objeto através de gcs_client.bucket(BUCKET).blob(fname) para especificar o nome do contentor e o nome do objeto.

    A chamada Cloud Storage upload_from_file() executa o carregamento, conforme mostrado no exemplo atualizado.

    Python 3

    A classe do controlador de carregamento no Blobstore para Python 3 é uma classe de utilidade e requer a utilização do dicionário WSGIenviron como um parâmetro de entrada, conforme mostrado no exemplo do Blobstore seguinte:

    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 o Cloud Storage, substitua o método get_uploads(request.environ) do Blobstore pelo método request.files.get() do Flask.

    Exemplo de código atualizado:

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

    No exemplo de código do Cloud Storage atualizado, a app identifica agora os artefactos de objetos pelo respetivo nome do objeto (fname) em vez de blob_id. O encaminhamento também ocorre na parte inferior do ficheiro de aplicação.

    Para obter o objeto carregado, o método get_uploads() do Blobstore é substituído pelo método request.files.get() do Flask. No Flask, pode usar o método secure_filename() para obter um nome sem carateres de caminho, como /, para o ficheiro, e identificar o objeto através de gcs_client.bucket(BUCKET).blob(fname) para especificar o nome do contentor e o nome do objeto.

    O método Cloud Storage upload_from_file() executa o carregamento conforme mostrado no exemplo atualizado.

  2. Substitua as classes do controlador de transferência do Blobstore por uma função de transferência no Flask. Siga as instruções para a sua versão do Python:

    Python 2

    O exemplo de controlador de transferência seguinte mostra a utilização da classe BlobstoreDownloadHandler, que usa o 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 o Cloud Storage:

    1. Atualize o método send_blob() do Blobstore para usar o método download_as_bytes() do Cloud Storage.
    2. Altere o encaminhamento de webapp2 para Flask.

    Exemplo de código atualizado:

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

    No exemplo de código do Cloud Storage atualizado, o Flask decora a rota na função Flask e identifica o objeto através de '/view/<path:fname>'. O Cloud Storage identifica o objeto pelo nome do objeto e pelo nome do contentor, e usa o método download_as_bytes() para transferir o objeto como bytes, em vez de usar o método send_blob do Blobstore.blob Se o artefacto não for encontrado, a app devolve um erro HTTP 404.

    Python 3

    Tal como o controlador de carregamento, a classe do controlador de transferência no Blobstore para Python 3 é uma classe de utilidade e requer a utilização do dicionário WSGIenviron como um parâmetro de entrada, conforme mostrado no seguinte exemplo do 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 o Cloud Storage, substitua o método send_blob(request.environ, blob_key) do Blobstore pelo método blob.download_as_bytes() do Cloud Storage.

    Exemplo de código atualizado:

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

    No exemplo de código do Cloud Storage atualizado, blob_key é substituído por fname e o Flask identifica o objeto através do URL '/view/<path:fname>'. O método gcs_client.bucket(BUCKET).blob(fname) é usado para localizar o nome do ficheiro e o nome do contentor. O método download_as_bytes() do Cloud Storage transfere o objeto como bytes, em vez de usar o método send_blob() do Blobstore.

  3. Se a sua app usar um controlador principal, substitua a classe MainHandler pela função root() no Flask. Siga as instruções para a sua versão do Python:

    Python 2

    Segue-se um exemplo de utilização da classe MainHandler do 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 o Cloud Storage:

    1. Remova a classe MainHandler(BaseHandler), uma vez que o Flask processa o encaminhamento por si.
    2. Simplifique o código do Blobstore com o Flask.
    3. Remova o encaminhamento da app Web no final.

    Exemplo de código atualizado:

    @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 usou o Flask, não tem uma classe MainHandler, mas a função raiz do Flask tem de ser atualizada se o blobstore for usado. O exemplo seguinte usa a função 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 o Cloud Storage, substitua a função blobstore.create_upload_url('/upload') pelo método url_for() do Flask para obter o URL da função upload().

    Exemplo de código atualizado:

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

Teste e implemente a sua app

O servidor de desenvolvimento local permite-lhe testar a execução da sua app, mas não vai poder testar o Cloud Storage até implementar uma nova versão, porque todos os pedidos do Cloud Storage têm de ser enviados através da Internet para um contentor do Cloud Storage real. Consulte o artigo Testar e implementar a sua aplicação para saber como executar a sua aplicação localmente. Em seguida, implemente uma nova versão para confirmar que a app tem o mesmo aspeto que antes.

Apps que usam o App Engine NDB ou o Cloud NDB

Tem de atualizar o modelo de dados do Datastore se a sua app usar o NDB do App Engine ou o NDB do Google Cloud para incluir propriedades relacionadas com o Blobstore.

Atualize o modelo de dados

Uma vez que as propriedades BlobKey do NDB não são suportadas pelo Cloud Storage, tem de modificar as linhas relacionadas com o Blobstore para usar equivalentes incorporados do NDB, frameworks Web ou noutro local.

Para atualizar o modelo de dados:

  1. Encontre as linhas que usam BlobKey no modelo de dados, como as seguintes:

    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. Substituir 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. Se também estiver a atualizar do App Engine NDB para o Cloud NDB durante a migração, consulte o guia de migração do Cloud NDB para obter orientações sobre como refatorar o código NDB para usar gestores de contexto do Python.

Retrocompatibilidade para o modelo de dados do Datastore

Na secção anterior, a substituição de ndb.BlobKeyProperty por ndb.StringProperty tornou a app incompatível com versões anteriores, o que significa que a app não vai poder processar entradas mais antigas criadas pelo Blobstore. Se precisar de reter dados antigos, crie um campo adicional para novas entradas do Cloud Storage em vez de atualizar o campo ndb.BlobKeyProperty e crie uma função para normalizar os dados.

A partir dos exemplos nas secções anteriores, faça as seguintes alterações:

  1. Crie dois campos de propriedades separados quando definir o modelo de dados. Use a propriedade file_blob para identificar objetos criados pelo Blobstore e a propriedade file_gcs para identificar objetos criados pelo 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. Encontre as linhas que fazem referência a novas visitas, como as seguintes:

    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. Altere o código para que file_gcs seja usado para entradas recentes. Por exemplo:

    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. Crie uma nova função para normalizar os dados. O exemplo seguinte mostra a utilização de extração, transformação e carregamento (ETL) para percorrer todas as visitas e usa os dados de visitante e data/hora para verificar se existe file_gcs ou 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. Encontre a linha que faz referência à função 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. Una a função fetch_visits() à função etl_visits(), por exemplo:

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

Exemplos

O que se segue?