从 App Engine Blob 存储区迁移到 Cloud Storage(模块 16)

1. 概览

无服务器迁移站系列 Codelab(自学进度、动手教程)和相关视频旨在帮助 Google Cloud 无服务器开发者实现应用现代化,引导他们完成一项或多项迁移,主要是从旧版服务迁移。这样做可以提高应用的可移植性,为您提供更多选择和灵活性,使您能够集成和访问更广泛的 Cloud 产品,并更轻松地升级到较新的语言版本。虽然本系列最初侧重于最早的 Cloud 用户(主要是 App Engine [标准环境] 开发者),但其内容足够广泛,也涵盖了其他无服务器平台(例如 Cloud Functions 和 Cloud Run)或其他平台(如适用)。

此 Codelab 将介绍如何从 App Engine Blobstore 迁移到 Cloud Storage。此外,还存在以下隐式迁移:

如需了解更多分步信息,请参阅任何相关的迁移模块。

在接下来的实验中

  • 添加对 App Engine Blobstore API/库的使用
  • 将用户上传的内容存储到 Blobstore 服务
  • 为下一步迁移到 Cloud Storage 做好准备

所需条件

调查问卷

您将如何使用本教程?

仅阅读教程内容 阅读并完成练习

您如何评价使用 Python 的体验?

新手水平 中等水平 熟练水平

您如何评价自己在使用 Google Cloud 服务方面的经验水平?

新手 中等 熟练

2. 背景

此 Codelab 从模块 15 的示例应用开始,演示如何从 Blobstore(和 NDB)迁移到 Cloud Storage(和 Cloud NDB)。迁移过程涉及替换对 App Engine 旧版捆绑服务的依赖项,以便您根据需要将应用迁移到其他 Cloud 无服务器平台或其他托管平台。

与本系列中的其他迁移相比,此迁移需要更多工作。Blobstore 依赖于原始 webapp 框架,因此示例应用使用的是 webapp2 框架,而不是 Flask。本教程重点介绍如何迁移到 Cloud Storage、Cloud NDB、Flask 和 Python 3。

该应用仍会注册最终用户的“访问”并显示最近的十次访问,但之前的(第 15 单元)Codelab 添加了新功能以适应 Blobstore 的使用:该应用会提示最终用户上传与其“访问”相对应的工件(文件)。用户可以执行此操作,也可以选择“跳过”来选择退出。无论用户做出何种决定,下一页都会呈现与此应用之前版本相同的输出,显示最近的访问记录。还有一个变化是,具有相应工件的访问记录会显示一个“查看”链接,用于显示访问记录的工件。此 Codelab 会实现前面提到的迁移,同时保留所述功能。

3. 设置/准备工作

在进入本教程的主要部分之前,我们先设置项目、获取代码,然后部署基准应用,以确保我们从可正常运行的代码开始。

1. 设置项目

如果您已部署模块 15 应用,我们建议您重复使用同一项目(和代码)。或者,您也可以创建全新项目或重复使用其他现有项目。确保项目具有有效的结算账号,并且 App Engine 已启用。

2. 获取基准示例应用

此 Codelab 的前提条件之一是拥有一个可正常运行的模块 15 示例应用。如果您没有,可以从模块 15 的“START”文件夹(链接如下)中获取。本 Codelab 将引导您完成每个步骤,最后您将获得与模块 16“FINISH”文件夹中的代码类似的代码。

模块 15 起始文件的目录应如下所示:

$ ls
README.md       app.yaml        main-gcs.py     main.py         templates

main-gcs.py 文件是模块 15 中 main.py 的替代版本,允许选择与应用的分配网址(基于项目 ID:PROJECT_ID.appspot.com)的默认值不同的 Cloud Storage 存储桶。此文件在本(模块 16)Codelab 中不起任何作用,但如果需要,您可以对该文件应用类似的迁移技术。

3. (重新)部署基准应用

您现在需要执行的剩余准备工作步骤:

  1. 重新熟悉 gcloud 命令行工具
  2. 使用 gcloud app deploy 重新部署示例应用
  3. 确认应用在 App Engine 上运行没有任何问题

成功执行这些步骤并确认模块 15 应用正常运行后,初始页面会向用户显示一个表单,提示用户上传访问工件文件,并提供一个“跳过”按钮,供用户选择不上传:

f5b5f9f19d8ae978.png

用户上传文件或跳过此步骤后,应用会呈现熟悉的“最近访问”页面:

f5ac6b98ee8a34cb.png

如果访问记录中包含工件,则访问时间戳右侧会显示一个“查看”链接,用于显示(或下载)该工件。确认应用的功能后,您就可以从 App Engine 旧版服务(webapp2、NDB、Blobstore)迁移到现代替代方案(Flask、Cloud NDB、Cloud Storage)。

4. 更新配置文件

更新后的应用版本会用到三个配置文件。需要执行的任务如下:

  1. 更新 app.yaml 中必需的内置第三方库,并为迁移到 Python 3 做好准备
  2. 添加一个 requirements.txt,用于指定所有非内置的必需库
  3. 添加 appengine_config.py,以便应用同时支持内置和非内置第三方库

app.yaml

通过更新 libraries 部分来修改 app.yaml 文件。移除了 jinja2,并添加了 grpcio、setuptools 和 ssl。为这三个库选择可用的最新版本。此外,还添加了 Python 3 runtime 指令,但已将其注释掉。完成后,它应如下所示(如果您选择了 Python 3.9):

之前:

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

libraries:
- name: jinja2
  version: latest

升级后:

#runtime: python39
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

这些更改主要涉及 App Engine 服务器上提供的 Python 2 内置库(因此您无需自行捆绑这些库)。我们移除了 Jinja2,因为它随附于 Flask,而我们将把 Flask 添加到 reqs.txt。每当使用 Google Cloud 客户端库(例如 Cloud NDB 和 Cloud Storage 的客户端库)时,都需要 grpcio 和 setuptools。最后,Cloud Storage 本身需要 ssl 库。顶部注释掉的运行时指令用于在您准备好将此应用移植到 Python 3 时使用。我们将在本教程结束时介绍此主题。

requirements.txt

添加一个 requirements.txt 文件,其中需要 Flask 框架以及 Cloud NDB 和 Cloud Storage 客户端库,这些都不是内置的。使用以下内容创建文件:

flask
google-cloud-ndb
google-cloud-storage

Python 2 App Engine 运行时要求自行打包非内置第三方库,因此请执行以下命令,将这些库安装到 lib 文件夹中:

pip install -t lib -r requirements.txt

如果您的开发机器上同时安装了 Python 2 和 3,您可能需要使用 pip2 命令,以确保获取这些库的 Python 2 版本。升级到 Python 3 后,您无需再自行捆绑。

appengine_config.py

添加一个支持内置和非内置第三方库的 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)

刚刚完成的步骤应该与 App Engine 文档的为 Python 2 应用安装库部分中列出的步骤类似或相同,更具体地说,appengine_config.py 的内容应该与该部分第 5 步中的内容一致。

配置文件的工作已完成,接下来我们继续处理应用。

5. 修改应用文件

导入

main.py 的第一组更改包括换出所有要替换的内容。具体变化如下:

  1. webapp2 已替换为 Flask
  2. 不使用 webapp2_extras 中的 Jinja2,而是使用 Flask 附带的 Jinja2
  3. App Engine Blobstore 和 NDB 已被 Cloud NDB 和 Cloud Storage 取代
  4. webapp 中的 Blobstore 处理程序被替换为 io 标准库模块、Flask 和 werkzeug 实用程序的组合
  5. 默认情况下,Blobstore 会将数据写入以应用网址 (PROJECT_ID.appspot.com) 命名的 Cloud Storage 存储桶。由于我们要移植到 Cloud Storage 客户端库,因此需要使用 google.auth 来获取项目 ID,以指定完全相同的存储桶名称。(您可以更改存储桶名称,因为它不再是硬编码的。)

之前:

import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers

将 main.py 中的当前导入部分替换为以下代码段,以实现上述列表中的更改。

升级后:

import io

from flask import (Flask, abort, redirect, render_template,
        request, send_file, url_for)
from werkzeug.utils import secure_filename

import google.auth
from google.cloud import exceptions, ndb, storage

初始化和不必要的 Jinja2 支持

接下来要替换的代码块是 BaseHandler,它指定使用 webapp2_extras 中的 Jinja2。这是不必要的,因为 Jinja2 随 Flask 一起提供,并且是其默认模板引擎,因此请将其移除。

在模块 16 方面,实例化旧版应用中没有的对象。这包括初始化 Flask 应用,以及为 Cloud NDB 和 Cloud Storage 创建 API 客户端。最后,我们按照上述导入部分中的说明,将 Cloud Storage 存储桶名称组合在一起。以下是实施这些更新前后的对比:

之前:

class BaseHandler(webapp2.RequestHandler):
    'Derived request handler mixing-in Jinja2 support'
    @webapp2.cached_property
    def jinja2(self):
        return jinja2.get_jinja2(app=self.app)

    def render_response(self, _template, **context):
        self.response.write(self.jinja2.render_template(_template, **context))

升级后:

app = Flask(__name__)
ds_client = ndb.Client()
gcs_client = storage.Client()
_, PROJECT_ID = google.auth.default()
BUCKET = '%s.appspot.com' % PROJECT_ID

更新 Datastore 访问权限

Cloud NDB 与 App Engine NDB 基本兼容。我们已经介绍过的一个区别是,需要使用 API 客户端。另一个区别是,后者要求 Datastore 访问权限由 API 客户端的 Python 上下文管理器控制。从本质上讲,这意味着使用 Cloud NDB 客户端库的所有 Datastore 访问调用只能在 Python with 块中进行。

这是一个变化;另一个变化是,Cloud Storage 不支持 Blobstore 及其对象(例如 BlobKey),因此请将 file_blob 更改为 ndb.StringProperty。以下是数据模型类以及反映这些更改的更新后的 store_visit() 和 fetch_visits() 函数:

之前:

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

def store_visit(remote_addr, user_agent, upload_key):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent),
            file_blob=upload_key).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

升级后:

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

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

def fetch_visits(limit):
    'get most recent visits'
    with ds_client.context():
        return Visit.query().order(-Visit.timestamp).fetch(limit)

下图展示了到目前为止所做的更改:

a8f74ca392275822.png

更新处理程序

上传处理程序

webapp2 中的处理程序是类,而 Flask 中的处理程序是函数。Flask 不使用 HTTP 动词方法,而是使用动词来修饰函数。Blobstore 及其 webapp 处理程序已被 Cloud Storage 以及 Flask 及其实用程序的功能所取代:

之前:

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

关于此更新的一些说明:

  • 现在,文件工件不再使用 blob_id,而是通过文件名 (fname) 进行标识(如果存在),否则使用 None(用户选择不上传文件)。
  • Blobstore 处理程序对用户隐藏了上传过程,但 Cloud Storage 没有,因此您可以看到新添加的代码,这些代码设置了文件的 blob 对象和位置(存储桶),以及执行实际上传的调用。(upload_from_file())。
  • webapp2 在应用文件底部使用路由表,而 Flask 路由则位于每个修饰的处理程序中。
  • 两个处理程序都会通过重定向到主页 ( /) 来完成其功能,同时使用 HTTP 307 返回代码保留 POST 请求。

下载处理程序

更新下载处理程序遵循与上传处理程序类似的模式,只是需要查看的代码要少得多。将 Blobstore 和 webapp 功能替换为等效的 Cloud Storage 和 Flask 功能:

之前:

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

关于此更新的说明:

  • 同样,Flask 使用其路由修饰处理程序函数,而 webapp 在底部的路由表中执行此操作,因此请识别后者的模式匹配语法 ('/view/([^/]+)?')与 Flask 的 ('/view/<path:fname>')。
  • 与上传处理程序一样,对于 Blobstore 处理程序抽象出的功能,Cloud Storage 端需要执行更多操作,即识别相关文件(Blob)并显式下载二进制文件,而不是调用 Blobstore 处理程序的单个 send_blob() 方法。
  • 在这两种情况下,如果未找到工件,系统都会向用户返回 HTTP 404 错误。

主处理程序

对主应用的最终更改在主处理程序中进行。webapp2 HTTP 动词方法已替换为结合了其功能的单个函数。将 MainHandler 类替换为 root() 函数,并移除 webapp2 路由表,如下所示:

之前:

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)

升级后:

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

它们不是单独的 get() 和 post() 方法,本质上是 root() 中的 if-else 语句。此外,由于 root() 是一个函数,因此只需调用一次即可为 GET 和 POST 渲染模板,而在 webapp2 中,这实际上是不可能的。

下图展示了对 main.py 的第二组也是最后一组更改:

5ec38818c32fec2.png

(可选)向后兼容性“增强”

因此,上述解决方案非常有效,但前提是您从头开始,并且没有 Blobstore 创建的文件。由于我们更新了应用,使其通过文件名而不是 BlobKey 来标识文件,因此完成的模块 16 应用将无法查看 Blobstore 文件。换句话说,我们在执行此迁移时做出了向后不兼容的更改。现在,我们提供 main.py 的替代版本,称为 main-migrate.py(可在代码库中找到),该版本试图弥合这一差距。

支持 Blobstore 创建的文件的第一个“扩展程序”是一个数据模型,该模型具有 BlobKeyProperty(以及 Cloud Storage 创建的文件的 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.BlobKeyProperty()  # backwards-compatibility
    file_gcs  = ndb.StringProperty()

file_blob 属性将用于标识 Blobstore 创建的文件,而 file_gcs 用于标识 Cloud Storage 文件。现在,在创建新访问时,请在 file_gcs(而不是 file_blob)中明确存储一个值,因此 store_visit 看起来略有不同:

之前:

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

升级后:

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

提取最近的访问记录时,先“标准化”数据,然后再将其发送到模板:

之前:

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

升级后:

@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))
    return render_template('index.html', **context)

接下来,确认 file_blob 或 file_gcs 是否存在(或两者都不存在)。如果有可用的文件,请选择存在的文件并使用该标识符(对于 Blobstore 创建的文件,使用 BlobKey;对于 Cloud Storage 创建的文件,使用文件名)。“Cloud Storage 创建的文件”是指使用 Cloud Storage 客户端库创建的文件。Blobstore 也会写入 Cloud Storage,但在这种情况下,这些文件将是 Blobstore 创建的文件。

现在,更重要的是,这个 etl_visits() 函数是什么?它用于为最终用户对数据进行标准化或 ETL(提取、转换和加载)。如下所示:

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]

这可能与您预期的结果一致:代码会循环遍历所有访问,并针对每次访问,逐字逐句地获取访问者和时间戳数据,然后检查是否存在 file_gcs 或 file_blob,如果存在,则选择其中一个(如果都不存在,则选择 None)。

下图展示了 main.py 和 main-migrate.py 之间的区别:

718b05b2adadb2e1.png

如果您是从头开始,没有 Blobstore 创建的文件,请使用 main.py;但如果您正在过渡,并且希望支持由 Blobstore 和 Cloud Storage 创建的文件,请查看 main-migrate.py,了解如何处理此类场景,以帮助您规划自己的应用迁移。在进行复杂的迁移时,可能会出现特殊情况,因此本示例旨在展示如何更好地使用真实数据对真实应用进行现代化改造。

6. 总结/清理

本部分将通过部署应用、验证应用是否按预期运行以及检查任何反映的输出来结束此 Codelab。应用验证完成后,执行所有清理步骤并考虑后续步骤。

部署并验证应用

在重新部署应用之前,请务必运行 pip install -t lib -r requirements.txt,以在 lib 文件夹中获取这些自行捆绑的第三方库。如果您想运行向后兼容的解决方案,请先将 main-migrate.py 重命名为 main.py。现在运行 gcloud app deploy,并确认该应用与第 15 模块的应用运行方式完全相同。表单界面如下所示:

f5b5f9f19d8ae978.png

“最近访问”页面如下所示:

f5ac6b98ee8a34cb.png

恭喜!您已完成本 Codelab,成功将 App Engine Blobstore 替换为 Cloud Storage,将 App Engine NDB 替换为 Cloud NDB,并将 webapp2 替换为 Flask。您的代码现在应该与 FINISH(模块 16)文件夹中的代码一致。该文件夹中还存在备用 main-migrate.py。

Python 3“迁移”

只需将 app.yaml 顶部的 Python 3 runtime 指令取消注释,即可将此应用移植到 Python 3。源代码本身已与 Python 3 兼容,因此无需进行任何更改。如需将其部署为 Python 3 应用,请执行以下步骤:

  1. 取消注释 app.yaml 顶部的 Python 3 runtime 指令。
  2. 删除 app.yaml 中的所有其他行。
  3. 删除 appengine_config.py 文件。(在 Python 3 运行时中未使用)
  4. 删除 lib 文件夹(如果存在)。(Python 3 运行时不需要)

清理

常规

如果您目前已完成操作,建议您停用 App Engine 应用,以免产生费用。不过,如果您想进行更多测试或实验,App Engine 平台提供了免费配额,只要您不超过该用量层级,就不会产生费用。这是计算费用,但可能还会产生相关 App Engine 服务的费用,因此请查看其价格页面了解详情。如果此迁移涉及其他 Cloud 服务,则这些服务会单独计费。在任何一种情况下,如果适用,请参阅下文中的“本 Codelab 专属”部分。

为充分披露信息,特此说明:部署到 App Engine 等 Google Cloud 无服务器计算平台会产生少量构建和存储费用。Cloud Build 和 Cloud Storage 都有自己的免费配额。存储该映像会占用部分配额。不过,您所在的地区可能没有此类免费层级,因此请注意您的存储空间用量,以尽可能降低潜在费用。您应查看的特定 Cloud Storage“文件夹”包括:

  • console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/images
  • console.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com
  • 上述存储空间链接取决于您的 PROJECT_ID 和 *LOC*ation,例如,如果您的应用托管在美国,则为“us”。

另一方面,如果您不打算继续使用此应用或其他相关的迁移 Codelab,并希望彻底删除所有内容,请关停项目。

此 Codelab 专属

下面列出的服务是此 Codelab 独有的。如需了解详情,请参阅各产品的文档:

请注意,如果您从模块 15 迁移到模块 16,Blobstore 中仍会有数据,因此我们在上文中提供了其价格信息。

后续步骤

除了本教程之外,其他侧重于从旧版捆绑服务迁移的迁移模块包括:

  • 模块 2:从 App Engine ndb 迁移到 Cloud NDB
  • 模块 7-9:从 App Engine 任务队列推送任务迁移到 Cloud Tasks
  • 模块 12-13:从 App Engine Memcache 迁移到 Cloud Memorystore
  • 模块 18-19:从 App Engine 任务队列(拉取任务)迁移到 Cloud Pub/Sub

App Engine 不再是 Google Cloud 中唯一的无服务器平台。如果您有一个小型 App Engine 应用或功能有限的应用,并且希望将其转换为独立的微服务,或者您希望将单体式应用拆分为多个可重复使用的组件,那么这些都是考虑迁移到 Cloud Functions 的充分理由。如果容器化已成为应用开发工作流的一部分,尤其是当工作流包含 CI/CD(持续集成/持续交付或持续部署)流水线时,请考虑迁移到 Cloud Run。以下模块涵盖了这些场景:

  • 从 App Engine 迁移到 Cloud Functions:请参阅模块 11
  • 从 App Engine 迁移到 Cloud Run:请参阅模块 4,了解如何使用 Docker 将应用容器化;或参阅模块 5,了解如何在不使用容器、不具备 Docker 知识或不使用 Dockerfile 的情况下将应用容器化

您可以选择改用其他无服务器平台,我们建议您在做出任何更改之前,先考虑最适合您的应用和应用场景的选项。

无论您接下来考虑哪个迁移模块,都可以在其开源代码库中访问所有无服务器迁移工作站内容(Codelab、视频、源代码 [如有])。该代码库的 README 还提供了有关应考虑哪些迁移以及迁移模块的任何相关“顺序”的指导。

7. 其他资源

Codelab 问题/反馈

如果您发现此 Codelab 存在任何问题,请先搜索您的问题,然后再提交。用于搜索和创建新问题的链接:

迁移时可参考的资源

下表提供了指向模块 15(开始)和模块 16(完成)的代码库文件夹的链接。您也可以从所有 App Engine Codelab 迁移的代码库中访问这些代码,您可以克隆该代码库或下载 ZIP 文件。

Codelab

Python 2

Python 3

第 15 单元

代码

不适用

模块 16(此 Codelab)

代码

(与 Python 2 相同)

在线资源

以下是可能与本教程相关的在线资源:

App Engine Blobstore 和 Cloud Storage

App Engine 平台

其他云信息

Python

视频

许可

本作品已获知识共享署名 2.0 通用许可。