Migrar o Blobstore do App Engine para o Cloud Storage

Neste guia, explicamos como migrar do Blobstore do App Engine para o Cloud Storage.

O Cloud Storage é semelhante ao Blobstore do App Engine, porque você pode usá-lo para exibir objetos de dados grandes (blobs), como arquivos de vídeo ou imagem, e permitir que os usuários façam upload de arquivos de dados grandes. Enquanto o Blobstore do App Engine pode ser acessado apenas por meio dos serviços em pacote legados do App Engine, o Cloud Storage é um produto independente do Google Cloud acessado pelas Bibliotecas de cliente do Cloud. O Cloud Storage oferece ao app uma solução de armazenamento de objetos mais moderna e oferece a flexibilidade de migrar para o Cloud Run ou outra plataforma de hospedagem de apps do Google Cloud posteriormente.

Para projetos do Google Cloud criados após novembro de 2016, o Blobstore usa buckets do Cloud Storage nos bastidores. Isso significa que, quando você migra seu app para o Cloud Storage, todos os objetos e permissões atuais nesses buckets do Cloud Storage permanecem inalterados. Também é possível começar a acessar esses buckets usando as bibliotecas de cliente do Cloud para 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 depende do webapp.
  • A API Blobstore para Python 3 usa classes de utilitário para usar gerenciadores Blobstore.
  • Para o Blobstore, o número máximo de arquivos que podem ser enviados para o Blobstore é 500. Não há limite para o número de objetos que podem ser criados em um bucket do Cloud Storage.

O Cloud Storage não é compatível com:

  • Classes do gerenciador Blobstore
  • Objetos do Blobstore

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

  • Sabe ler e gravar objetos de dados grandes em um ambiente de execução, bem como armazenar e exibir objetos de dados grandes e estáticos, como filmes, imagens ou outros conteúdos estáticos. O limite de tamanho de objeto para o Cloud Storage é de 5 TiB.
  • Permite armazenar objetos em um bucket do Cloud Storage.
  • Ter um nível gratuito;

Antes de começar

  • Analise e entenda os preços e as cotas do Cloud Storage:
    • O Cloud Storage é um serviço de pagamento por utilização e tem os próprios preços de armazenamento de dados com base na classe de armazenamento dos dados e no local dos buckets.
    • As cotas do Cloud Storage têm algumas diferenças em relação às cotas e limites do Blobstore, que podem afetar as cotas de solicitações do App Engine.
  • Ter um aplicativo do Python 2 ou Python 3 no App Engine que esteja usando o Blobstore.
  • Os exemplos neste guia mostram um app que migra para o Cloud Storage usando o framework Flask. É possível usar qualquer framework da Web, incluindo a permanência em webapp2, ao migrar para o Cloud Storage.

Informações gerais

Em geral, o processo para migrar do Cloud Storage para o App Engine do repositório de blobs consiste nas seguintes etapas:

  1. Atualizar os arquivos de configuração
  2. Atualize seu aplicativo Python:
    • Atualizar seu framework da Web
    • Importar e inicializar o Cloud Storage
    • Atualizar gerenciadores do repositório de blobs
    • Opcional: atualizar seu modelo de dados se estiver usando o Cloud NDB ou o App Engine
  3. Testar e implantar seu aplicativo

Atualizar os arquivos de configuração

Antes de modificar o código do aplicativo para migrar do repositório de blobs para o Cloud Storage, atualize seus arquivos de configuração para usar a biblioteca do Cloud Storage.

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

    Python 2

    Para aplicativos Python 2:

    1. Remova a seção handlers e todas as dependências de webapp desnecessárias na seção libraries.
    2. Se você usa as bibliotecas de cliente do Cloud, adicione as versões mais recentes das bibliotecas grpcio e setuptools.
    3. Adicione a biblioteca ssl, já que isso é exigido pelo Cloud Storage.

    Veja a seguir um exemplo de arquivo app.yaml com as mudanças 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 do Python 3, exclua todas as linhas, exceto o elemento runtime. Exemplo:

    runtime: python310 # or another support version
    

    O ambiente de execução do Python 3 instala bibliotecas automaticamente. Portanto, não é necessário especificar bibliotecas integradas do ambiente de execução anterior do Python 2. Se o app Python 3 estiver usando outros serviços em pacote legados ao migrar para o Cloud Storage, deixe o arquivo app.yaml como está.

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

    Python 2

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

    google-cloud-storage
    

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

    Python 3

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

    google-cloud-storage
    

    O App Engine instala automaticamente essas dependências durante a implantação do app no ambiente de execução do Python 3. Portanto, exclua a pasta lib, se houver uma.

  3. Para apps do Python 2, se o app estiver usando bibliotecas integradas ou copiadas, você precisa especificar esses caminhos no arquivo 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 seu aplicativo Python

Depois de modificar os arquivos de configuração, atualize o app Python.

Atualizar seu framework da Web em Python 2

Para apps do Python 2 que usam o framework webapp2, é recomendável migrar do framework webapp2 desatualizado. Consulte o Cronograma de suporte do ambiente de execução para ver a data de término do suporte ao Python 2.

É possível migrar para outro framework da Web, como Flask, Django ou WSGI. Como o Cloud Storage exclui dependências em webapp2, e os gerenciadores do repositório de blobs não são compatíveis, é possível excluir ou substituir outras bibliotecas relacionadas ao app.

Se você quiser continuar usando webapp2, os exemplos neste guia usam o Cloud Storage com Flask.

Se você planeja usar os serviços do Google Cloud além do Cloud Storage ou tiver acesso às versões mais recentes do ambiente de execução, considere fazer upgrade do app para o ambiente de execução do Python 3. Para mais informações, consulte a visão geral de migração do Python 2 para o Python 3.

Importar e inicializar o Cloud Storage

Modifique os arquivos do aplicativo atualizando as linhas de importação e inicialização:

  1. Remova instruções de importação do repositório de blobs, como a seguinte:

    import webapp2
    from google.appengine.ext import blobstore
    from google.appengine.ext.webapp import blobstore_handlers
    
  2. Adicione as instruções de importação do Cloud Storage e das bibliotecas do Google Authentication, 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 do Google Authentication é necessária para acessar o mesmo ID do projeto usado no repositório de blobs para o Cloud Storage. Importe outras bibliotecas, como o Cloud NBD, se aplicável ao seu app.

  3. Criar um novo cliente para o Cloud Storage e especificar o bucket usado no repositório de blobs. Exemplo:

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

    Para projetos do Google Cloud após novembro de 2016, o repositório de blobs grava em um bucket do Cloud Storage nomeado com base no URL do aplicativo e segue o formato de PROJECT_ID.appspot.com. Use a autenticação do Google para receber o ID do projeto e especificar o bucket do Cloud Storage usado para armazenar blobs no repositório de blobs.

Atualizar gerenciadores do repositório de blobs

Como o Cloud Storage não é compatível com os gerenciadores de upload e download do repositório de blobs, é necessário usar uma combinação da funcionalidade do Cloud Storage, do módulo da biblioteca padrão io, do framework da Web e dos utilitários do Python para fazer upload e fazer o download de objetos (blobs) no Cloud Storage.

Veja a seguir como atualizar os gerenciadores do repositório de blobs usando o Flask como o framework da Web de exemplo:

  1. Substitua as classes do gerenciador de upload do repositório de blobs por uma função de upload no Flask. Siga as instruções para sua versão do Python:

    Python 2

    Os gerenciadores repositório de blobs no Python 2 são classes webapp2, conforme mostrado no exemplo a seguir do repositório de blobs:

    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 upload do app da Web por uma função de upload do Flask.
    2. Substitua o gerenciador de upload e o roteamento por um método POST do Flask decorado com roteamento.

    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 atualizado do Cloud Storage, o app agora identifica os artefatos de objeto pelo nome do objeto (fname) em vez de blob_id. O roteamento também ocorre na parte inferior do arquivo do aplicativo.

    Para receber o objeto enviado, o método get_uploads() do repositório de blobs é substituído pelo método request.files.get() do Flask. No Flask, é possível usar o método secure_filename() para conseguir um nome sem caracteres de caminho, como /, para o arquivo e identificar o objeto usando gcs_client.bucket(BUCKET).blob(fname) para especificar os nomes do bucket e do objeto.

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

    Python 3

    A classe de gerenciador de upload no repositório de blobs para Python 3 é uma classe de utilitário e requer o uso do WSGI environ como um parâmetro de entrada, como mostrado no repositório de blobs no seguinte exemplo:

    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 repositório de blobs 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 atualizado do Cloud Storage, o app agora identifica os artefatos de objeto pelo nome do objeto (fname) em vez de blob_id. O roteamento também ocorre na parte inferior do arquivo do aplicativo.

    Para receber o objeto enviado, o método get_uploads() do repositório de blobs é substituído pelo método request.files.get() do Flask. No Flask, é possível usar o método secure_filename() para conseguir um nome sem caracteres de caminho, como /, para o arquivo e identificar o objeto usando gcs_client.bucket(BUCKET).blob(fname) para especificar os nomes do bucket e do objeto.

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

  2. Substitua as classes do gerenciador de download do repositório de blobs por uma função de download no Flask. Siga as instruções para sua versão do Python:

    Python 2

    O exemplo de gerenciador de download a seguir mostra o uso da classe 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 o Cloud Storage:

    1. Atualize o método send_blob() do repositório de blobs para usar o método download_as_bytes() do Cloud Storage.
    2. Mude o roteamento 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 atualizado do Cloud Storage, o Flask decora a rota na função Flask e identifica o objeto usando '/view/<path:fname>'. O Cloud Storage identifica o objeto blob pelo nome do objeto e do bucket e usa o método download_as_bytes() para fazer o download do objeto como bytes, em vez de usar o método send_blob. no repositório de blobs. Se o artefato não for encontrado, o app retornará um erro HTTP 404.

    Python 3

    Assim como o gerenciador de upload, a classe do gerenciador de download no repositório de blobs para Python 3 é uma classe de utilitário e requer o uso do dicionário WSGI environ como um parâmetro de entrada, como mostrado no exemplo de repositório de blobs a seguir:

    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 send_blob(request.environ, blob_key) do repositório de blobs 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 atualizada do Cloud Storage, blob_key é substituído por fname, e o Flask identifica o objeto usando o URL '/view/<path:fname>'. O método gcs_client.bucket(BUCKET).blob(fname) é usado para localizar o nome do arquivo e do bucket. O método download_as_bytes() do Cloud Storage faz o download do objeto como bytes, em vez de usar o método send_blob() do repositório de blobs.

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

    Python 2

    Veja a seguir um exemplo de como usar a classe MainHandler do repositório de blobs:

    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), já que o Flask trata o roteamento para você.
    2. Simplificar o código do repositório de blobs com o Flask.
    3. Remova o roteamento de webapp 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 você usou o Flask, você não terá uma classe MainHandler, mas sua função raiz do Flask precisará ser atualizada se o repositório de blobs for usado. O exemplo a seguir 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 conseguir 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)
    

Testar e implantar seu aplicativo

Com o servidor de desenvolvimento local, é possível testar se o app é executado, mas não é possível testar o Cloud Storage até que uma nova versão seja implantada, já que todas as solicitações do Cloud Storage precisam ser enviadas pela Internet para um bucket do Cloud Storage. Consulte Como testar e implantar o aplicativo para saber como executá-lo localmente. Em seguida, implante uma nova versão para confirmar que o app aparece da mesma forma que antes.

Apps que usam o App Engine NDB ou o Cloud NDB

É preciso atualizar o modelo de dados do Datastore se o app usa o App Engine NDB ou o Cloud NDB para incluir propriedades relacionadas ao repositório de blobs.

Atualizar o modelo de dados

Como as propriedades BlobKey do NDB não são compatíveis com o Cloud Storage, é necessário modificar as linhas relacionadas ao repositório de blobs para usar equivalentes integrados do NDB, frameworks da Web ou outros.

Para atualizar o modelo de dados, faça o seguinte:

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

    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. Substitua 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 você também estiver fazendo upgrade do App Engine NDB para o Cloud NDB durante a migração, consulte o guia de migração do Cloud NDB para ver orientações sobre como refatorar o código do NDB para usar gerenciadores de contexto do Python.

Compatibilidade com versões anteriores do modelo de dados do Datastore

Na seção anterior, a substituição de ndb.BlobKeyProperty por ndb.StringProperty tornou o app incompatível com versões anteriores, o que significa que o app não poderá processar entradas mais antigas criadas pelo repositório de blobs. Se você precisar manter 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.

Nos exemplos das seções anteriores, faça as seguintes mudanças:

  1. Crie dois campos de propriedade separados ao definir seu modelo de dados. Use a propriedade file_blob para identificar objetos criados no repositório de blobs e a propriedade file_gcs para identificar objetos criados no 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. Mude o código para que o file_gcs seja usado para entradas recentes. 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 a seguir mostra o uso de extração, transformação e valor baixo (ETL) para repetir todas as visitas e usa os dados do visitante e do carimbo de data/hora para verificar se file_gcs ou 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. 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 o fetch_visits() com a 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)
    

Examples

  • Para ver um exemplo de como migrar um app do Python 2 para o Cloud Storage, compare o exemplo de código do repositório de blobs para Python 2 e o mesmo exemplo de código do Cloud Storage (links em inglês) no GitHub.
  • Para ver um exemplo de como migrar um app do Python 3 para o Cloud Storage, compare o mesmo exemplo de código do repositório de blobs para Python 3 e o mesmo exemplo de código do Cloud Storage no GitHub.

A seguir