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

1. 概览

无服务器迁移站系列 Codelab(自定进度的实操教程)和相关视频旨在指导 Google Cloud 无服务器开发者完成一次或多次迁移(主要是从旧服务迁移),从而实现应用的现代化改造。这样做可以提高应用的可移植性,为您提供更多选择和灵活性,让您能够集成并访问更广泛的 Cloud 产品,并更轻松地升级到较新版本。虽然本系列最初主要面向的是最早接触 Cloud 的用户(主要是 App Engine(标准环境)开发者),但涵盖的范围非常广泛,涵盖了 Cloud FunctionsCloud Run 等其他无服务器平台,或其他无服务器平台(如适用)。

在此 Codelab 中,您将学习如何从 App Engine Blob 存储区迁移到 Cloud Storage。此外,还有从以下来源进行的隐式迁移:

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

在接下来的实验中

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

所需条件

调查问卷

您将如何使用本教程?

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

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

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

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

<ph type="x-smartling-placeholder"></ph> 新手 中级 熟练

2. 背景

此 Codelab 将从第 15 单元中的示例应用开始,演示如何从 Blob 存储区(和 NDB)迁移到 Cloud Storage(和 Cloud NDB)。迁移过程涉及替换 App Engine 旧版捆绑服务的依赖项,以便您能够根据需要将应用移至其他 Cloud 无服务器平台或其他托管平台。

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

应用仍会记录最终用户“访问”并显示最近的十个,但上一个(模块 15)Codelab 添加了新功能来适应 Blob 存储区的使用:应用提示最终用户上传与其“访问”相对应的工件(文件)。用户可以执行此操作或选择“跳过”即可选择停用。无论用户决定如何,下一页所呈现的输出与此应用以前的版本是相同的,即显示最近的访问。还有一个转折点是,包含相应工件的访问即记录了一次“查看”用于显示访问工件的链接。此 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 存储分区。此文件在此 Codelab(模块 16)中不起任何作用,但如有需要,可以对该文件应用类似的迁移技术。

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

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

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

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

f5b5f9f19d8ae978.png

用户上传文件或跳过该文件后,该应用将呈现您所熟悉的“最近访问”页面:

f5ac6b98ee8a34cb.png

展示制品的访问将拥有一个“查看”链接,用于显示(或下载)相应工件。确认应用的功能后,您就可以从 App Engine 旧版服务(Web 应用 2、NDB、Blob 存储区)迁移到现代替代方案(Flask、Cloud NDB、Cloud Storage)。

4. 更新配置文件

更新后的应用版本中有三个配置文件会发挥作用。必需的任务包括:

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

app.yaml

通过更新 libraries 部分修改 app.yaml 文件。移除 jinja2,然后添加 grpciosetuptoolsssl。选择所有三个库的可用最新版本。同时添加 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 和 Python 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. 请使用 Flask 附带的 Jinja2,而不是使用 webapp2_extras 中的 Jinja2
  3. App Engine Blob 存储区和 NDB 已被 Cloud NDB 和 Cloud Storage 取代
  4. webapp 中的 Blob 存储区处理程序已被 io 标准库模块、Flask 和 werkzeug 实用程序的组合取代
  5. 默认情况下,Blob 存储区会写入以应用网址 (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 客户端。另一种方法是后者要求由 API 客户端的 Python 上下文管理器控制数据存储区访问权限。从本质上讲,这意味着使用 Cloud NDB 客户端库的所有 Datastore 访问调用都只能在 Python with 块中进行。

只需更改一次另一项是 Blob 存储区及其对象,例如Cloud Storage 不支持 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 动词方法,而是使用动词来装饰函数。Blob 存储区及其 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)

关于此次更新的一些说明:

  • 现在,文件制品通过文件名 (fname)(如果存在)标识,而不是通过 blob_id 标识,否则通过 None 标识(用户已选择停止上传文件)。
  • Blob 存储区处理程序将上传过程从其用户中抽离,但 Cloud Storage 并未这样做,因此您可以看到用于设置文件的 blob 对象和位置(存储分区)的新添加代码,以及执行实际上传的调用。(upload_from_file())。
  • webapp2 在应用文件底部使用路由表,而 Flask 路由在每个修饰的处理程序中都可以找到。
  • 这两个处理程序都会重定向到主屏幕 ( /),同时使用 HTTP 307 返回代码保留 POST 请求,从而封装各自的功能。

下载处理程序

更新下载处理程序遵循与上传处理程序类似的模式,只是需要查看的代码要少得多。将 Blob 存储区和 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>') 的匹配。
  • 与上传处理程序一样,对于 Blob 存储区处理程序提取的功能(即识别相关文件 (blob),以及明确下载二进制文件和 Blob 存储区处理程序的单个 send_blob() 方法调用),Cloud Storage 端需要做更多工作。
  • 在这两种情况下,如果未找到工件,系统会向用户返回 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)

它们本质上是 root() 中的 if-else 语句,而不是分离 get()post() 方法。此外,由于 root() 是单个函数,因此只需调用一次即可同时为 GETPOST 呈现模板,而在 webapp2 中则不可能实现。

下面是对 main.py 的第二组更改(也是最后一组更改)的示意图:

5ec38818c32fec2

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

因此,上面创建的解决方案非常适用于您,但前提是您是从头开始且没有 Blob 存储区创建的文件。由于我们更新了应用,使其按文件名(而不是 BlobKey)来识别文件,因此已完成的模块 16 应用将按原样无法查看 Blob 存储区文件。也就是说,我们针对此次迁移做出了向后不兼容的更改。现在,我们展示了名为 main-migrate.pymain.py 替代版本(可在代码库中找到),它尝试弥补这一差异。

第一条“扩展程序”支持 Blob 存储区创建的文件是一种具有 BlobKeyProperty(以及用于 Cloud Storage 创建文件的 StringPropertyStringProperty)的数据模型:

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 属性将用于标识 Blob 存储区创建的文件,而 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_blobfile_gcs 是否存在(或两者都没有)。如果有可用文件,请选择存在的文件并使用该标识符(如果是 Blob 存储区创建的文件,请使用 BlobKey;如果是 Cloud Storage 创建的文件,则使用文件名)。当我们说“Cloud Storage 创建的文件”时,指的是使用 Cloud Storage 客户端库创建的文件。Blob 存储区也会向 Cloud Storage 写入数据,但在此示例中,这些文件是由 Blob 存储区创建的文件。

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

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_gcsfile_blob,如果不存在,则选择其中一个(如果不存在,则选择 None)。

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

718b05b2adadb2e1

如果您是从头开始且没有 Blob 存储区创建的文件,请使用 main.py,但如果您要转换版本并希望支持 Blob 存储区 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 Blob 存储区替换为 Cloud Storage,将 App Engine NDB 替换为 Cloud NDB,将 webapp2 替换为 Flask。您的代码现在应与 FINISH (Module 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 BuildCloud 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*格式,例如“us”。

另一方面,如果您不打算继续学习此应用或其他相关的迁移 Codelab,而是想彻底删除所有内容,请关停项目

此 Codelab 的具体内容

下列服务是此 Codelab 独有的服务。有关详情,请参阅各个产品的文档:

请注意,如果从模块 15 迁移到模块 16,您在 Blob 存储区中仍会有数据,因此,我们在上面加入了其价格信息。

后续步骤

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

  • 第 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 的情况下实现容器化

您可以自行选择是否切换到其他无服务器平台,我们建议您先考虑最适合您的应用和用例的方案,然后再做任何更改。

无论您接下来考虑使用哪种迁移模块,都可以通过其开源代码库访问所有 Serverless Migration Station 内容(Codelab、视频、源代码 [如果有])。代码库的 README 还提供了有关应考虑哪些迁移以及任何相关“顺序”的指南。迁移模块

7. 其他资源

Codelab 问题/反馈

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

迁移时可参考的资源

下表列出了模块 15(START)和模块 16 (FINISH) 对应的代码库文件夹的链接。您还可以从所有 App Engine Codelab 迁移的代码库访问这些库,您可以克隆或下载 ZIP 文件。

Codelab

Python 2

Python 3

第 15 单元

代码

不适用

第 16 单元(此 Codelab)

代码

(与 Python 2 相同)

在线资源

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

App Engine Blob 存储区和 Cloud Storage

App Engine 平台

其他 Cloud 信息

Python

视频

许可

此作品已获得 Creative Commons Attribution 2.0 通用许可授权。