1. 概览
无服务器迁移站系列 Codelab(自定进度的实操教程)和相关视频旨在指导 Google Cloud 无服务器开发者完成一次或多次迁移(主要是从旧服务迁移),从而实现应用的现代化改造。这样做可以提高应用的可移植性,为您提供更多选择和灵活性,让您能够集成并访问更广泛的 Cloud 产品,并更轻松地升级到较新版本。虽然本系列最初主要面向的是最早接触 Cloud 的用户(主要是 App Engine(标准环境)开发者),但涵盖的范围非常广泛,涵盖了 Cloud Functions 和 Cloud Run 等其他无服务器平台,或其他无服务器平台(如适用)。
第 15 单元的 Codelab 说明了如何在单元 0 中向示例应用添加 App Engine blobstore
用法。接下来,您就可以在第 16 单元中将该用量迁移到 Cloud Storage。
在接下来的实验中
- 添加对 App Engine Blob 存储区 API/库的使用
- 将用户上传内容存储到
blobstore
服务 - 为下一步迁移到 Cloud Storage 做好准备
所需条件
- 具有有效的 GCP 结算账号的 Google Cloud Platform 项目
- 基本 Python 技能
- 常用 Linux 命令的实践知识
- 具备开发和部署 App Engine 应用的基础知识
- 可正常运行的 Module 0 App Engine 应用(从代码库中获取)
调查问卷
您将如何使用本教程?
您如何评价使用 Python 的体验?
您如何评价自己在使用 Google Cloud 服务方面的经验水平?
<ph type="x-smartling-placeholder">2. 背景
如需从 App Engine Blob 存储区 API 迁移,请从模块 0 将其用法添加到现有基准 App Engine ndb
应用中。示例应用向用户显示最近 10 次访问。我们正在修改该应用,以提示最终用户上传与其“访问”对应的工件(文件)。如果用户不想这样做,则可以“跳过”选项。无论用户决定如何决定,下一页都会呈现与模块 0(以及本系列中的许多其他模块)中的应用相同的输出。实现此 App Engine blobstore
集成后,我们可以在下一个(模块 16)Codelab 中将其迁移到 Cloud Storage。
App Engine 提供了对 Django 和 Jinja2 模板系统的访问权限,使得此示例与众不同的一个因素(除了添加 Blob 存储区访问权限之外)是它在模块 0 中使用 Django 切换到在模块 15 中使用 Jinja2。对 App Engine 应用进行现代化改造的关键步骤是将 Web 框架从 webapp2
迁移到 Flask。后者使用 Jinja2 作为其默认模板系统,因此我们开始朝着这个方向发展,即实现 Jinja2,同时继续使用 webapp2
访问 Blob 存储区。由于 Flask 默认使用 Jinja2,这意味着在模块 16 中不需要对模板进行任何更改。
3. 设置/准备工作
在进入本教程的主要部分之前,请先设置您的项目、获取代码并部署基准应用,以从可正常工作的代码入手。
1. 设置项目
如果您已部署模块 0 应用,我们建议您重复使用同一项目(和代码)。或者,您也可以创建一个全新的项目或重复使用其他现有项目。确保项目具有有效的结算账号并且已启用 App Engine。
2. 获取基准示例应用
此 Codelab 的前提条件之一是拥有有效的模块 0 示例应用。如果没有,可以从模块 0“START”中获取(下方链接即可找到)。此 Codelab 将逐步引导您完成每个步骤,最后使用与第 15 单元“FINISH”中的代码类似的代码文件夹中。
- START:Module 0 folder (Python 2)
- 完成:Module 15 folder (Python 2)
- 整个代码库(用于克隆或下载 ZIP 文件)
模块 0 起始文件的目录应如下所示:
$ ls README.md index.html app.yaml main.py
3. (重新)部署基准应用
现在需要执行的其余准备工作步骤:
- 重新熟悉
gcloud
命令行工具 - 使用
gcloud app deploy
重新部署示例应用 - 确认应用在 App Engine 上运行没有任何问题
成功执行这些步骤并看到您的 Web 应用正常运行(输出类似于下文)后,您就可以在应用中使用缓存了。
4. 更新配置文件
app.yaml
应用配置没有实质性更改,但是如前所述,我们正在从 Django 模板(默认)移至 Jinja2,因此为了进行切换,用户应指定 App Engine 服务器上可用的最新版 Jinja2,并且您需要将其添加到 app.yaml
的内置第三方库部分。
之前:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
您可以通过添加新的 libraries
部分来修改 app.yaml
文件,如下所示:
之后:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
libraries:
- name: jinja2
version: latest
其他配置文件不需要更新,所以我们继续看应用文件。
5. 修改应用文件
导入和 Jinja2 支持
main.py
的第一组更改包括使用 Blob 存储区 API,以及用 Jinja2 替换 Django 模板。具体变化如下:
os
模块的用途是创建 Django 模板的文件路径名。由于我们要切换到处理此操作的 Jinja2,因此不再需要使用os
和 Django 模板渲染程序google.appengine.ext.webapp.template
,因此将将其移除。- 导入 Blob 存储区 API:
google.appengine.ext.blobstore
- 导入在原始
webapp
框架中找到的 Blob 存储区处理程序 - 这些处理程序在webapp2
中不可用:google.appengine.ext.webapp.blobstore_handlers
- 从
webapp2_extras
软件包导入 Jinja2 支持
之前:
import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template
将 main.py
中的当前导入部分替换为以下代码段,从而实现上述列表中的更改。
之后:
import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers
导入后,添加一些样板代码以支持使用 Jinja2,如 webapp2_extras
文档中所定义。以下代码段使用 Jinja2 功能封装标准的 webapp2 请求处理程序类,因此请在导入后立即将此代码块添加到 main.py
:
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))
添加 Blob 存储区支持
本系列中的其他迁移与保持示例应用的功能或输出相同(或几乎相同)而不对用户体验做出(很多更改)的迁移不同,此示例与常态有很大的不同。我们将更新应用以要求用户提供用于注册其访问的文件工件,而不是立即记录新的访问,然后显示最近的十次访问。然后,最终用户可以上传相应文件,或选择“跳过”不上传任何内容完成此步骤后,“最近访问”列页面。
此更改允许我们的应用程序使用 Blob 存储区服务在最近的访问页面上存储(并可能稍后呈现)该图片或其他文件类型。
更新数据模型并实现其用途
我们将存储更多数据,具体而言是更新数据模型以存储上传到 Blob 存储区的文件的 ID(称为“BlobKey
”),并添加引用以将其保存在 store_visit()
中。由于查询时会随所有其他数据一起返回这些额外数据,因此 fetch_visits()
会保持不变。
以下是包含ndb.BlobKeyProperty
file_blob
的这些更新前后的对比:
之前:
class Visit(ndb.Model):
'Visit entity registers visitor IP address & timestamp'
visitor = ndb.StringProperty()
timestamp = ndb.DateTimeProperty(auto_now_add=True)
def store_visit(remote_addr, user_agent):
'create new Visit entity in Datastore'
Visit(visitor='{}: {}'.format(remote_addr, user_agent)).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.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)
以下是目前所作更改的示意图:
支持文件上传
功能上最显著的变化是支持文件上传,无论是提示用户文件,还是支持“跳过”功能,或呈现与访问对应的文件。所有这一切都是图片的一部分。以下是支持文件上传所需的更改:
- 主处理程序
GET
请求不再提取最近的访问以显示。而是会提示用户进行上传。 - 当最终用户提交要上传的文件或跳过该过程时,表单中的
POST
会将控制权传递给派生自google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler
的新UploadHandler
。 UploadHandler
的POST
方法会执行上传操作,调用store_visit()
来注册访问,然后触发 HTTP 307 重定向以将用户发送回“/”,其中...- 主处理程序的
POST
方法会查询(通过fetch_visits()
)并显示最近的访问。如果用户选择“跳过”未上传任何文件,但访问仍会被记录,且经过同一重定向。 - “最近访问”显示内容包含向用户显示的新字段,可能是超链接“查看”如果有上传文件或“无”否则。这些更改是通过 HTML 模板实现的,并且增加了上传表单(即将介绍更多内容)。
- 如果最终用户点击“查看”链接,它会向派生自
google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler
的新ViewBlobHandler
发出GET
请求,如果图片是图片,则会在浏览器中呈现文件(如果支持,则在浏览器中显示);如果找不到,则提示下载;如果找不到,则返回 HTTP 404 错误。 - 除了新的一对处理程序类以及向它们发送流量的新路由对之外,主处理程序还需要新的
POST
方法来接收上述 307 重定向。
在这些更新之前,模块 0 应用仅包含具有 GET
方法和单个路由的主处理程序:
之前:
class MainHandler(webapp2.RequestHandler):
'main application (GET) handler'
def get(self):
store_visit(self.request.remote_addr, self.request.user_agent)
visits = fetch_visits(10)
tmpl = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(tmpl, {'visits': visits}))
app = webapp2.WSGIApplication([
('/', MainHandler),
], debug=True)
实现这些更新后,现在有三个处理程序:1) 使用 POST
方法的上传处理程序,2)“查看 blob”使用 GET
方法下载处理程序;以及 3) 使用 GET
和 POST
方法下载主处理程序。进行这些更改,使应用的其余部分如下所示。
之后:
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)
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)
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)
我们刚刚在这段代码中添加了几个关键调用:
- 在
MainHandler.get
中,有对blobstore.create_upload_url
的调用。此调用会生成POST
形式的网址,并调用上传处理程序将文件发送到 Blob 存储区。 - 在
UploadHandler.post
中,有对blobstore_handlers.BlobstoreUploadHandler.get_uploads
的调用。这是一个真正的神奇功能,它会将文件放入 Blob 存储区,并返回该文件的唯一永久性 ID,即BlobKey
。 - 在
ViewBlobHandler.get
中,使用文件的BlobKey
调用blobstore_handlers.BlobstoreDownloadHandler.send
会提取文件并将其转发给最终用户的浏览器
这些调用代表了对添加到应用中的功能的大部分访问。下面是对 main.py
的第二组更改(也是最后一组更改)的示意图:
更新 HTML 模板
对主应用所做的一些更新会影响应用的界面 (UI),因此需要在 Web 模板中进行相应的更改,实际上有两个更改:
- 需要一个文件上传表单,其中包含 3 个输入元素:一个文件,以及一对用于上传文件和跳过文件的提交按钮。
- 通过添加“视图”来更新最近的访问输出结果用于上传相应文件或“无”的访问的链接否则。
之前:
<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>
<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>
</body>
</html>
实施上述列表中的更改,以构成更新后的模板:
之后:
<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>
<h1>VisitMe example</h1>
{% if upload_url %}
<h3>Welcome... upload a file? (optional)</h3>
<form action="{{ upload_url }}" method="POST" enctype="multipart/form-data">
<input type="file" name="file"><p></p>
<input type="submit"> <input type="submit" value="Skip">
</form>
{% else %}
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }}
<i><code>
{% if visit.file_blob %}
(<a href="/view/{{ visit.file_blob }}" target="_blank">view</a>)
{% else %}
(none)
{% endif %}
</code></i>
from {{ visit.visitor }}
</li>
{% endfor %}
</ul>
{% endif %}
</body>
</html>
下图展示了对 index.html
进行必要的更新:
最后一项更改是 Jinja2 倾向于将其模板放在 templates
文件夹中,因此请创建该文件夹并将 index.html
移入其中。在最后这个步骤中,您现在已经完成了将 Blob 存储区添加到模块 0 示例应用的所有必要更改。
(可选)Cloud Storage“增强功能”
Blob 存储区存储最终演化为 Cloud Storage 本身。这意味着,您可以在 Cloud 控制台(尤其是 Cloud Storage 浏览器)中看到 Blob 存储区上传操作。问题出在哪里。答案是 App Engine 应用的默认 Cloud Storage 存储分区。该名称是 App Engine 应用的完整域名 PROJECT_ID
.appspot.com
的名称。这非常方便,因为所有项目 ID 都是唯一的,对吧?
对示例应用进行的更新会将上传的文件拖放到该存储分区中,但开发者可以选择更具体的位置。默认存储分区可通过 google.appengine.api.app_identity.get_default_gcs_bucket_name()
以编程方式访问,如果您要访问此值(例如,将其用作整理上传文件的前缀),则需要执行新的导入操作。例如,按文件类型排序:
例如,要实现与图片类似的代码,您需要编写如下代码,以及一些用于检查文件类型以选择所需的存储分区名称的代码:
ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')
您还将使用 Python 标准库 imghdr
模块等工具验证上传的图片,以确认图片类型。最后,您可能需要限制上传内容的大小,以应对不良行为者。
就这么简单。我们如何更新应用以支持指定上传文件的存储位置?关键在于通过添加如下所示的 gs_bucket_name
参数,调整 MainHandler.get
中对 blobstore.create_upload_url
的调用,以指定 Cloud Storage 中所需的上传位置:
blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))
如果您想指定上传应在何处进行,这是一项可选更新,因此它不是代码库中 main.py
文件的一部分。不过,代码库中有一个名为 main-gcs.py
的备用资源供您查看。我们不再使用单独的存储分区“文件夹”main-gcs.py
中的代码将上传内容存储在“根目录”中存储分区 (PROJECT_ID
.appspot.com
) 与 main.py
类似,但提供您所需的基架,便于您将样本派生到本部分中提示的内容中。下图显示了“差异”部分介于 main.py
和 main-gcs.py
之间。
6. 摘要/清理
在此 Codelab 的最后,本部分将部署应用,验证应用是否按预期以及任何反映的输出中正常运行。应用验证后,执行所有清理步骤并考虑后续步骤。
部署并验证应用
使用 gcloud app deploy
重新部署您的应用,并确认应用按预期运行,这与模块 0 应用的用户体验 (UX) 有所不同。现在,您的应用中有两个不同的屏幕,第一个是访问文件上传表单提示:
最终用户可以在其中上传文件,然后点击“提交”或者点击“跳过”不上传任何内容不管是哪种情况,结果都是最近的访问屏幕,现在通过“view”得到增强链接或“无”访问时间戳和访问者信息之间的具体差异:
恭喜您完成此 Codelab,并已将对 App Engine Blob 存储区的使用添加到模块 0 的示例应用中。您的代码现在应与 FINISH (Module 15) 文件夹中的代码一致。该文件夹中还存在备用 main-gcs.py
。
清理
常规
如果您目前已完成,我们建议您停用 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
*格式,例如“us
”。
另一方面,如果您不打算继续学习此应用或其他相关的迁移 Codelab,而是想彻底删除所有内容,请关停项目。
此 Codelab 的具体内容
下列服务是此 Codelab 独有的服务。有关详情,请参阅各个产品的文档:
- App Engine Blob 存储区服务受存储的数据配额和限制的约束,因此请查看相关信息以及旧版捆绑服务的价格页面。
- App Engine Datastore 服务由 Cloud Datastore(Datastore 模式的 Cloud Firestore)提供,该服务也有一个免费层级;如需了解详情,请参阅其价格页面。
后续步骤
单元 16 介绍了需要考虑的下一个逻辑迁移,向开发者展示如何从 App Engine Blob 存储区服务迁移到使用 Cloud Storage 客户端库。升级的好处包括能够访问更多 Cloud Storage 功能,熟悉适用于 App Engine 以外的应用的客户端库,无论是在 Google Cloud、其他云,还是本地应用。如果您觉得不需要 Cloud Storage 提供的所有功能,或担心它对费用的影响,则可以继续使用 App Engine Blob 存储区。
第 16 单元之外还有许多其他可能的迁移,例如 Cloud NDB 和 Cloud Datastore、Cloud Tasks 或 Cloud Memorystore。还可以跨产品迁移到 Cloud Run 和 Cloud Functions。迁移代码库包含所有代码示例,提供了所有 Codelab 和视频的链接,还提供了关于应考虑哪些迁移以及任何相关“订单”的指导。迁移。
7. 其他资源
Codelab 问题/反馈
如果您在此 Codelab 中发现任何问题,请先搜索您的问题,然后再提交。用于搜索和创建新问题的链接:
迁移时可参考的资源
下表列出了模块 0(开始阶段)和模块 15 (FINISH) 对应的代码库文件夹的链接。您还可以从所有 App Engine Codelab 迁移的代码库访问这些库,您可以克隆或下载 ZIP 文件。
Codelab | Python 2 | Python 3 |
模块 0 | 不适用 | |
第 15 单元(此 Codelab) | 不适用 |
在线资源
以下是可能与本教程相关的在线资源:
App Engine
- App Engine Blob 存储区服务
- App Engine 存储的数据配额和限制
- App Engine 文档
- Python 2 App Engine(标准环境)运行时
- 在 Python 2 App Engine 上使用 App Engine 内置库
- App Engine 价格和配额信息
- 第二代 App Engine 平台发布(2018 年)
- 比较第一个与第二代平台
- 对旧版运行时的长期支持
- 文档迁移示例代码库
- 社区提供的迁移示例代码库
Google Cloud
- 在 Google Cloud Platform 上使用 Python 应用
- Google Cloud Python 客户端库
- Google Cloud“始终免费”计划层级
- Google Cloud SDK(gcloud 命令行工具)
- 所有 Google Cloud 文档
Python
视频
许可
此作品已获得 Creative Commons Attribution 2.0 通用许可授权。