1. 概览
无服务器迁移站系列 Codelab(自定进度的动手教程)和相关视频旨在帮助 Google Cloud 无服务器开发者通过一次或多次迁移(主要是从旧版服务迁移)来指导他们的应用现代化。这样做可让您的应用更易于移植,并为您提供更多选择和灵活性,使您能够与更多 Cloud 产品集成并访问这些产品,还能更轻松地升级到新的语言版本。虽然最初侧重于最早的 Cloud 用户(主要是 App Engine [标准环境] 开发者),但本系列文章的范围足够广,可涵盖其他无服务器平台(例如 Cloud Functions 和 Cloud Run),或者其他平台(如果适用)。
此 Codelab 的目的是向 Python 2 App Engine 开发者展示如何从 App Engine Users API/服务迁移到 Cloud Identity Platform (GCIP)。此外,对于 Datastore 访问权限(主要在迁移模块 2 中介绍),还有从 App Engine NDB 到 Cloud NDB 的隐式迁移,以及升级到 Python 3。
模块 20 介绍了如何将 Users API 的使用添加到模块 1 示例应用。在本模块中,您将使用已完成的模块 20 应用,并将其使用迁移到 Cloud Identity Platform。
在接下来的实验中
- 将 App Engine Users 服务的使用替换为 Cloud Identity Platform
- 将 App Engine NDB 的使用替换为 Cloud NDB(另请参阅模块 2)
- 使用 Firebase Auth 设置不同的身份验证身份提供方
- 使用 Cloud Resource Manager API 获取项目 IAM 信息
- 使用 Firebase Admin SDK 获取用户信息
- 将示例应用移植到 Python 3
所需条件
- 具有有效的 GCP 结算账号的 Google Cloud Platform 项目
- 基本 Python 技能
- 常用 Linux 命令的实践知识
- 具备开发和部署 App Engine 应用的基础知识
- 有效的模块 20 App Engine 示例应用
调查问卷
您打算如何使用本教程?
您如何评价使用 Python 的体验?
您如何评价自己在使用 Google Cloud 服务方面的经验水平?
2. 背景
App Engine Users 服务是一个用户身份验证系统,供 App Engine 应用使用。它提供 Google 登录作为身份提供方,提供可在应用中使用的便捷登录和退出链接,并支持管理员用户和仅限管理员使用的功能。为了提高应用的可移植性,Google Cloud 建议从旧版 App Engine 捆绑服务迁移到 Cloud 独立服务,例如从 Users 服务迁移到 Cloud Identity Platform 等。
Identity Platform 基于 Firebase Authentication,并增加了多项企业功能,包括多重身份验证、OIDC 和 SAML 单点登录支持、多租户、99.95% 服务等级协议 (SLA) 等等。Identity Platform 与 Firebase Authentication 产品比较页面中也重点介绍了这些差异。这两种产品的功能都大大多于 Users 服务提供的功能。
此模块 21 Codelab 演示了如何将应用的用户身份验证从 Users 服务切换到 Identity Platform 功能,这些功能与模块 20 中演示的功能最为相似。模块 21 还介绍了如何从 App Engine NDB 迁移到 Cloud NDB 以进行 Datastore 访问,重复了模块 2 的迁移。
虽然模块 20 中的代码“宣传”为 Python 2 示例应用,但源代码本身与 Python 2 和 3 兼容,即使在模块 21 中迁移到 Identity Platform(和 Cloud NDB)后,源代码仍然与 Python 2 和 3 兼容。在升级到 Python 3 时,您可以继续使用 Users 服务,因为迁移到 Identity Platform 是可选的。请参阅模块 17 Codelab 和视频,了解如何在升级到第二代运行时(例如 Python 3)的同时继续使用捆绑服务。
本教程包含以下步骤:
- 设置/准备工作
- 更新配置
- 修改应用代码
3. 设置/准备工作
本节介绍如何执行以下操作:
- 设置 Cloud 项目
- 获取基准示例应用
- (重新)部署并验证基准应用
- 启用新的 Google Cloud 服务/API
这些步骤可确保您从可正常运行的代码开始,以便将代码迁移到独立 Cloud 服务。
1. 设置项目
如果您已完成模块 20 Codelab,请重复使用同一项目(和代码)。或者,您可以创建一个全新的项目或重复使用另一个现有项目。确保项目具有有效的结算账号,并且已启用 App Engine 应用。找到您的项目 ID,并在本 Codelab 中随时使用它,每当遇到 PROJ_ID 变量时,请使用您的项目 ID。
2. 获取基准示例应用
前提条件之一是拥有一个正常运行的模块 20 App Engine 应用,因此请完成其 Codelab(推荐;上面的链接),或从代码库中复制模块 20 代码。无论您是使用自己的代码还是我们的代码,我们都能在这里开始(“START”)。本 Codelab 将引导您完成迁移,最后获得类似于模块 21 代码库文件夹(“FINISH”)中的代码。
复制模块 20 代码库文件夹。它应类似于以下输出,如果您完成了模块 20 的 Codelab,则可能还会有一个 lib 文件夹:
$ ls README.md appengine_config.py templates app.yaml main.py requirements.txt
3. (重新)部署并验证基准应用
执行以下步骤来部署模块 20 应用:
- 删除
lib文件夹(如果有),然后运行pip install -t lib -r requirements.txt以重新填充该文件夹。如果您同时安装了 Python 2 和 3,可能需要使用pip2。 - 确保您已安装并初始化
gcloud命令行工具,并查看其用法。 - 如果您不想在每次发出
gcloud命令时都输入PROJ_ID,请先使用gcloud config set projectPROJ_ID设置云项目。 - 使用
gcloud app deploy部署示例应用 - 确认应用按预期运行,没有出现错误。如果您已完成模块 20 的 Codelab,应用会在顶部显示用户登录信息(用户电子邮件地址、可能的“管理员徽章”和登录/退出按钮),以及最近的访问记录(如下图所示)。

以常规用户身份登录会导致系统显示用户的电子邮件地址,“登录”按钮也会变为“退出”按钮:

以管理员用户身份登录会导致用户的电子邮件地址显示在旁边,并带有“(管理员)”字样:

4. 启用新的 Google Cloud API/服务
简介
模块 20 应用使用 App Engine NDB 和 Users API,这些捆绑服务不需要额外设置,但独立的 Cloud 服务需要额外设置,并且更新后的应用将同时使用 Cloud Identity Platform 和 Cloud Datastore(通过 Cloud NDB 客户端库)。此外,我们需要确定 App Engine 管理员用户,因此也需要使用 Cloud Resource Manager API。
费用
- App Engine 和 Cloud Datastore 具有“始终免费”层级配额,只要您不超过这些限制,完成本教程就不会产生费用。另请参阅 App Engine 价格页面和 Cloud Datastore 价格页面,了解更多详情。
- Cloud Identity Platform 的使用费用取决于月活跃用户数 (MAU) 或身份验证次数;每种使用模式都有“免费”版本。如需了解详情,请参阅其价格页面。此外,虽然 App Engine 和 Cloud Datastore 需要结算,但只要您不超出 其无需插桩的每日配额,单独使用 GCIP 就不需要启用结算功能,因此对于不涉及需要结算的 Cloud API/服务的 Cloud 项目,请考虑使用 GCIP。
- 根据 Cloud Resource Manager API 的价格页面,使用该 API 在大多数情况下都是免费的。
用户可以根据自己的偏好,通过 Cloud 控制台或命令行(通过 Cloud SDK 的一部分 gcloud 命令)启用 Cloud API。我们先从 Cloud Datastore 和 Cloud Resource Manager API 开始。
通过 Cloud 控制台
在 Cloud 控制台中,前往 API 管理器的“库”页面(确保选择正确的项目),然后使用搜索栏搜索 API。
启用以下 API:
分别找到并点击每个 API 的启用按钮 - 系统可能会提示您提供结算信息。例如,以下是 Resource Manager API 的页面:

启用后,该按钮会变为“管理”(通常在几秒钟后):

以相同的方式启用 Cloud Datastore:

从命令行
虽然从控制台中启用 API 在视觉上更直观,但有些人更喜欢使用命令行。此外,您还可以一次性启用任意数量的 API。运行此命令以启用 Cloud Datastore 和 Cloud Resource Manager API,并等待操作完成,如下所示:
$ gcloud services enable cloudresourcemanager.googleapis.com datastore.googleapis.com Operation "operations/acat.p2-aaa-bbb-ccc-ddd-eee-ffffff" finished successfully.
系统可能会提示您输入结算信息。
上述命令中使用的每个 API 的“网址”称为 API 服务名称,您可以在每个 API 的库页面底部找到这些名称。如果您希望为自己的应用启用其他 Cloud API,可以在相应的 API 页面上找到它们各自的服务名称。此命令会列出您可以启用的 API 的所有服务名称:
gcloud services list --available --filter="name:googleapis.com"。
无论是在 Cloud 控制台中还是在命令行中,完成上述步骤后,我们的示例现在都能够访问这些 API。接下来的步骤是启用 Cloud Identity Platform 并进行必要的代码更改。
启用并设置 Cloud Identity Platform(仅限 Cloud 控制台)
Cloud Identity Platform 是一项 Marketplace 服务,因为它会连接到 Google Cloud 之外的资源或依赖于这些资源,例如 Firebase Authentication。目前,您只能通过 Cloud 控制台启用 Marketplace 服务。请按以下步骤操作:
- 前往 Cloud Marketplace 中的 Cloud Identity Platform 页面,然后点击该页面上的启用按钮。如果系统提示,请从 Firebase Authentication 升级,这样可以解锁更多功能,例如背景部分中介绍的功能。以下是突出显示启用按钮的 Marketplace 页面:

- 启用 Identity Platform 后,系统可能会自动将您转到身份提供商页面。如果不是,请使用此便捷链接前往该页面。

- 启用 Google 身份验证提供程序。如果尚未设置任何提供商,请点击添加提供商,然后选择 Google。返回此界面时,Google 条目应处于启用状态。在本教程中,我们仅使用 Google 作为身份验证提供方,以将 App Engine Users 服务作为轻量级 Google 登录服务进行镜像。在您自己的应用中,您可以启用其他身份验证提供方。
- 选择并设置 Google 和其他所需的身份验证提供方后,点击应用设置详情,然后在随即显示的对话框窗口中,复制“Web”标签页上
config对象中的apiKey和authDomain,并将这两者都保存到安全的位置。为什么不全部复制?此对话框中的代码段是硬编码的,并且已过时,因此只需保存最重要的部分,并在我们的代码中使用这些部分,同时更频繁地使用 Firebase Auth。复制这些值并将其保存在安全的地方后,点击关闭按钮,完成所有必要的设置。
4. 更新配置
配置方面的更新包括更改各种配置文件,以及在 Cloud Identity Platform 生态系统中创建相当于 App Engine 的内容。
appengine_config.py
- 如果升级到 Python 3,请删除
appengine_config.py - 如果您计划改用 Identity Platform,但仍使用 Python 2,请勿删除该文件。我们将在稍后的 Python 2 向后移植期间更新它。
requirements.txt
模块 20 的 requirements.txt 文件仅列出了 Flask。对于模块 21,请添加以下软件包:
requirements.txt 的内容现在应如下所示:
flask
google-auth
google-cloud-ndb
google-cloud-resource-manager
firebase-admin
app.yaml
- 升级到 Python 3 意味着简化
app.yaml文件。移除除运行时指令之外的所有内容,并将运行时指令设置为当前受支持的 Python 3 版本。该示例目前使用的是 3.10 版。 - 如果您继续使用 Python 2,则暂时无需在此处采取任何行动。
之前:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
模块 20 示例应用没有静态文件处理程序。如果您的应用需要,请保持原样。您可以根据需要移除所有脚本处理程序,也可以保留它们以供参考,前提是将它们的句柄更改为 auto,如 app.yaml 迁移指南中所述。进行这些更改后,更新后的 Python 3 app.yaml 简化为:
升级后:
runtime: python310
其他配置更新
无论您是继续使用 Python 2 还是移植到 Python 3,如果您有 lib 文件夹,请将其删除。
5. 修改应用代码
此部分介绍了对主要应用文件 main.py 的更新,将 App Engine Users 服务的用法替换为 Cloud Identity Platform。更新主应用后,您将更新 Web 模板 templates/index.html。
更新导入和初始化
请按照以下步骤更新导入并初始化应用资源:
- 对于导入,请将 App Engine NDB 替换为 Cloud NDB。
- 除了 Cloud NDB 之外,还导入 Cloud Resource Manager。
- Identity Platform 基于 Firebase Auth,因此请导入 Firebase Admin SDK。
- Cloud API 需要使用 API 客户端,因此在初始化 Flask 后立即为 Cloud NDB 初始化 API 客户端。
虽然此处导入了 Cloud Resource Manager 软件包,但我们将在应用初始化的后期阶段使用它。以下是模块 20 中的导入和初始化,以及在实现上述更改后,相应部分应如下所示:
之前:
from flask import Flask, render_template, request
from google.appengine.api import users
from google.appengine.ext import ndb
app = Flask(__name__)
升级后:
from flask import Flask, render_template, request
from google.auth import default
from google.cloud import ndb, resourcemanager
from firebase_admin import auth, initialize_app
# initialize Flask and Cloud NDB API client
app = Flask(__name__)
ds_client = ndb.Client()
对 App Engine 管理员用户的支持
需要向应用添加两个组件,以支持识别管理员用户:
_get_gae_admins()- 整理管理员用户集;调用一次并保存is_admin()- 检查登录用户是否为管理员用户;在任何用户登录时调用
实用函数 _get_gae_admins() 调用 Resource Manager API 来获取当前的 Cloud IAM 允许政策。允许政策定义了向哪些主账号(自然人用户、服务账号等)授予何种角色并强制执行。设置包括:
- 正在获取云项目 ID (
PROJ_ID) - 创建 Resource Manager API 客户端 (
rm_client) - 创建一组(只读)App Engine Admin 角色 (
_TARGETS)
Resource Manager 需要 Cloud 项目 ID,因此请导入 google.auth.default() 并调用该函数以获取项目 ID。该调用包含一个看起来像网址但实际上是 OAuth2 权限范围的参数。在云端(例如在 Compute Engine 虚拟机或 App Engine 应用上)运行应用时,系统会提供具有广泛权限的默认服务账号。为遵循最小权限的最佳实践,我们建议您创建自己的用户管理的服务账号。
对于 API 调用,最好进一步将应用的范围缩小到正常运行所需的最低级别。我们将要进行的 Resource Manager API 调用是 get_iam_policy(),该调用需要以下某个范围才能运行:
https://www.googleapis.com/auth/cloud-platformhttps://www.googleapis.com/auth/cloud-platform.read-onlyhttps://www.googleapis.com/auth/cloudplatformprojectshttps://www.googleapis.com/auth/cloudplatformprojects.readonly
示例应用只需要对允许政策的只读权限。它不会修改政策,也不需要访问整个项目。这意味着,该应用不需要前三种所需的任何权限。最后一个是必需的,也是我们为示例应用实现的。
函数的正文部分会创建一个空的管理员用户集 (admins),通过 get_iam_policy() 获取 allow_policy,并遍历其所有绑定,专门查找 App Engine Admin 角色:
roles/viewerroles/editorroles/ownerroles/appengine.appAdmin
对于找到的每个目标角色,该脚本会整理属于该角色的用户,并将这些用户添加到管理员用户的总体集合中。最后,它会返回在此 App Engine 实例的生命周期内找到并缓存的所有管理员用户(以常量 [_ADMINS] 的形式)。我们很快就会看到该来电。
将以下 _get_gae_admins() 函数定义添加到 main.py 中,紧邻实例化 Cloud NDB API 客户端 (ds_client) 的代码下方:
def _get_gae_admins():
'return set of App Engine admins'
# setup constants for calling Cloud Resource Manager API
_, PROJ_ID = default( # Application Default Credentials and project ID
['https://www.googleapis.com/auth/cloudplatformprojects.readonly'])
rm_client = resourcemanager.ProjectsClient()
_TARGETS = frozenset(( # App Engine admin roles
'roles/viewer',
'roles/editor',
'roles/owner',
'roles/appengine.appAdmin',
))
# collate users who are members of at least one GAE admin role (_TARGETS)
admins = set() # set of all App Engine admins
allow_policy = rm_client.get_iam_policy(resource='projects/%s' % PROJ_ID)
for b in allow_policy.bindings: # bindings in IAM allow-policy
if b.role in _TARGETS: # only look at GAE admin roles
admins.update(user.split(':', 1).pop() for user in b.members)
return admins
当用户登录应用时,会发生以下情况:
- 用户登录 Firebase 后,系统会从 Web 模板中进行快速检查。
- 当模板中的身份验证状态发生变化时,系统会向
/is_admin发出 Ajax 样式的fetch()调用,该调用的处理程序是下一个函数is_admin()。 - Firebase ID 令牌在 POST 正文中传递给
is_admin(),后者会从标头中提取该令牌,并调用 Firebase Admin SDK 对其进行验证。如果用户有效,请提取其电子邮件地址并检查其是否为管理员用户。 - 然后,系统会将布尔值结果作为成功的 200 返回给模板。
将 is_admin() 添加到 main.py 中的 _get_gae_admins() 之后:
@app.route('/is_admin', methods=['POST'])
def is_admin():
'check if user (via their Firebase ID token) is GAE admin (POST) handler'
id_token = request.headers.get('Authorization')
email = auth.verify_id_token(id_token).get('email')
return {'admin': email in _ADMINS}, 200
这两个函数中的所有代码都需要复制 Users 服务(尤其是其 is_current_user_admin() 函数)提供的功能。模块 20 中的此函数调用完成了所有繁重的工作,这与模块 21 不同,在模块 21 中,我们实现了一个替代解决方案。好消息是,该应用不再依赖于仅限 App Engine 的服务,这意味着您可以将应用迁移到 Cloud Run 或其他服务。此外,您只需在 _TARGETS 中切换到所需的角色,即可更改自有应用的“管理员用户”定义,而 Users 服务则针对 App Engine 管理员角色进行了硬编码。
初始化 Firebase Auth 并缓存 App Engine 管理员用户
我们本可以在顶部(靠近初始化 Flask 应用和创建 Cloud NDB API 客户端的位置)初始化 Firebase Auth,但在定义所有管理员代码之前,没有必要这样做,而现在我们已经定义了所有管理员代码。同样,现在 _get_gae_admins() 已定义,调用它来缓存管理员用户列表。
在 is_admin() 的函数正下方添加以下代码行:
# initialize Firebase and fetch set of App Engine admins
initialize_app()
_ADMINS = _get_gae_admins()
访问数据模型更新
Visit 数据模型不会发生变化。Datastore 访问需要明确使用 Cloud NDB API 客户端上下文管理器 ds_client.context()。在代码中,这意味着您需要在 Python with 块内将 Datastore 调用封装在 store_visit() 和 fetch_visits() 中。此更新与模块 2 完全相同。进行以下更改:
之前:
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)
def store_visit(remote_addr, user_agent):
'create new Visit entity in Datastore'
with ds_client.context():
Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()
def fetch_visits(limit):
'get most recent visits'
with ds_client.context():
return Visit.query().order(-Visit.timestamp).fetch(limit)
将用户登录逻辑移至 Web 模板
App Engine Users 服务是服务器端服务,而 Firebase Auth 和 Cloud Identity Platform 主要是客户端服务。因此,模块 20 应用中的大部分用户管理代码都移到了模块 21 Web 模板中。
在 main.py 中,Web 上下文将五个基本数据片段传递给模板,其中前四个与用户管理相关,并且因用户是否已登录而异:
who- 如果用户已登录,则为用户的电子邮件地址;否则为 useradmin- 如果已登录的用户是管理员,则显示 (管理员)徽章sign- 显示登录或退出按钮link- 点击按钮时显示登录或退出链接visits- 最近访问的网页
之前:
@app.route('/')
def root():
'main application (GET) handler'
store_visit(request.remote_addr, request.user_agent)
visits = fetch_visits(10)
# put together users context for web template
user = users.get_current_user()
context = { # logged in
'who': user.nickname(),
'admin': '(admin)' if users.is_current_user_admin() else '',
'sign': 'Logout',
'link': '/_ah/logout?continue=%s://%s/' % (
request.environ['wsgi.url_scheme'],
request.environ['HTTP_HOST'],
), # alternative to users.create_logout_url()
} if user else { # not logged in
'who': 'user',
'admin': '',
'sign': 'Login',
'link': users.create_login_url('/'),
}
# add visits to context and render template
context['visits'] = visits # display whether logged in or not
return render_template('index.html', **context)
所有用户管理功能都将移至 Web 模板,因此我们只剩下访问次数,从而使主处理程序恢复到模块 1 应用中的状态:
升级后:
@app.route('/')
def root():
'main application (GET) handler'
store_visit(request.remote_addr, request.user_agent)
visits = fetch_visits(10)
return render_template('index.html', visits=visits)
更新网站模板
上一部分中的所有更新在模板中是什么样的?主要是将用户管理从应用移至模板中运行的 Firebase Auth,并将我们移入 JavaScript 的所有代码部分移植到 JavaScript。我们看到 main.py 大幅缩减,因此预计 templates/index.html 会有类似的增长。
之前:
<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
</head>
<body>
<p>
Welcome, {{ who }} <code>{{ admin }}</code>
<button id="logbtn">{{ sign }}</button>
</p><hr>
<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>
<script>
document.getElementById("logbtn").onclick = () => {
window.location.href = '{{ link }}';
};
</script>
</body>
</html>
将整个网页模板替换为以下内容:
升级后:
<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<script type="module">
// import Firebase module attributes
import {
initializeApp
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-app.js";
import {
GoogleAuthProvider,
getAuth,
onAuthStateChanged,
signInWithPopup,
signOut
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-auth.js";
// Firebase config:
// 1a. Go to: console.cloud.google.com/customer-identity/providers
// 1b. May be prompted to enable GCIP and upgrade from Firebase
// 2. Click: "Application Setup Details" button
// 3. Copy: 'apiKey' and 'authDomain' from 'config' variable
var firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
};
// initialize Firebase app & auth components
initializeApp(firebaseConfig);
var auth = getAuth();
var provider = new GoogleAuthProvider();
//provider.setCustomParameters({prompt: 'select_account'});
// define login and logout button functions
function login() {
signInWithPopup(auth, provider);
};
function logout() {
signOut(auth);
};
// check if admin & switch to logout button on login; reset everything on logout
onAuthStateChanged(auth, async (user) => {
if (user && user != null) {
var email = user.email;
who.innerHTML = email;
logbtn.onclick = logout;
logbtn.innerHTML = "Logout";
var idToken = await user.getIdToken();
var rsp = await fetch("/is_admin", {
method: "POST",
headers: {Authorization: idToken}
});
var data = await rsp.json();
if (data.admin) {
admin.style.display = "inline";
}
} else {
who.innerHTML = "user";
admin.style.display = "none";
logbtn.onclick = login;
logbtn.innerHTML = "Login";
}
});
</script>
</head>
<body>
<p>
Welcome, <span id="who"></span> <span id="admin"><code>(admin)</code></span>
<button id="logbtn"></button>
</p><hr>
<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>
<script>
var who = document.getElementById("who");
var admin = document.getElementById("admin");
var logbtn = document.getElementById("logbtn");
</script>
</body>
</html>
此 HTML 正文中包含许多组件,我们来逐一了解。
Firebase 导入
在 HTML 文档的标头中,在网页标题之后,导入所需的 Firebase 组件。Firebase 组件现在分为多个模块,以提高效率。用于初始化 Firebase 的代码是从主 Firebase 应用模块导入的,而用于管理 Firebase 身份验证、Google 作为身份验证提供方、登录和退出以及身份验证状态更改“回调”的函数都是从 Firebase Auth 模块导入的:
<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<script type="module">
// import Firebase module attributes
import {
initializeApp
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-app.js";
import {
GoogleAuthProvider,
getAuth,
onAuthStateChanged,
signInWithPopup,
signOut
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-auth.js";
Firebase 配置
在本教程的 Identity Platform 设置部分中,您之前已保存应用设置详情对话框中的 apiKey 和 authDomain。在下一部分中,将这些值添加到 firebaseConfig 变量。注释中提供了指向更详细说明的链接:
// Firebase config:
// 1a. Go to: console.cloud.google.com/customer-identity/providers
// 1b. May be prompted to enable GCIP and upgrade from Firebase
// 2. Click: "Application Setup Details" button
// 3. Copy: 'apiKey' and 'authDomain' from 'config' variable
var firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
};
Firebase 初始化
下一部分将使用此配置信息初始化 Firebase。
// initialize Firebase app & auth components
initializeApp(firebaseConfig);
var auth = getAuth();
var provider = new GoogleAuthProvider();
//provider.setCustomParameters({prompt: 'select_account'});
此设置用于启用将 Google 用作身份验证提供方的功能,并提供了一个已注释掉的选项,用于在浏览器会话中仅注册了一个 Google 账号时也显示账号选择器。换句话说,当您有多个账号时,系统会按预期显示此“账号选择器”:
不过,如果会话中只有一个用户,登录过程会自动完成,无需任何用户互动。(弹出式窗口随即显示,然后消失。)您可以取消注释自定义参数行,强制账号选择器对话框针对一位用户显示(而不是立即登录应用)。如果启用,即使是单用户登录也会显示账号选择器:
登录和退出函数
以下代码行构成了用于处理登录或退出按钮点击事件的函数:
// define login and logout button functions
function login() {
signInWithPopup(auth, provider);
};
function logout() {
signOut(auth);
};
登录和退出操作
此 <script> 代码块中的最后一个主要部分是针对每次身份验证更改(登录或退出)调用的函数。
// check if admin & switch to logout button on login; reset everything on logout
onAuthStateChanged(auth, async (user) => {
if (user && user != null) {
var email = user.email;
who.innerHTML = email;
logbtn.onclick = logout;
logbtn.innerHTML = "Logout";
var idToken = await user.getIdToken();
var rsp = await fetch("/is_admin", {
method: "POST",
headers: {Authorization: idToken}
});
var data = await rsp.json();
if (data.admin) {
admin.style.display = "inline";
}
} else {
who.innerHTML = "user";
admin.style.display = "none";
logbtn.onclick = login;
logbtn.innerHTML = "Login";
}
});
</script>
</head>
模块 20 中用于确定是发送“用户已登录”模板上下文还是“用户已退出”上下文的代码已迁移到此处。如果用户成功登录,顶部的条件会返回 true,从而触发以下操作:
- 系统会设置用户的电子邮件地址以供显示。
- 登录按钮会变为退出。
- 系统会以 Ajax 样式调用
/is_admin,以确定是否显示(admin)管理员用户徽章。
当用户退出登录时,系统会执行 else 子句来重置所有用户信息:
- 用户名设置为 user
- 任何管理员徽章均已移除
- 退出按钮已改回登录
模板变量
标题部分结束后,正文部分开始,其中包含的模板变量会被 HTML 元素替换,这些元素会根据需要进行更改:
- 显示的用户名称
(admin)管理员徽章(如适用)- 登录或退出按钮
<body>
<p>
Welcome, <span id="who"></span> <span id="admin"><code>(admin)</code></span>
<button id="logbtn"></button>
</p><hr>
最近访问和 HTML 元素变量
最近访问的代码不会更改,而最后的 <script> 块会为登录和退出时发生更改的 HTML 元素设置变量(如上文所述):
<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>
<script>
var who = document.getElementById("who");
var admin = document.getElementById("admin");
var logbtn = document.getElementById("logbtn");
</script>
</body>
</html>
至此,我们已完成从 App Engine NDB 和 Users API 切换到 Cloud NDB 和 Identity Platform 以及升级到 Python 3 所需的应用和 Web 模板更改。恭喜,您已成功创建新的模块 21 示例应用!您可以在 Module 21b repo 文件夹中查看我们的版本。
Codelab 的下一部分是可选的 (*),仅适用于必须继续使用 Python 2 的用户,其中将引导您完成必要的步骤,最终获得可正常运行的 Python 2 模块 21 应用。
6. *Python 2 向后移植
此可选部分适用于正在执行 Identity Platform 迁移但必须继续在 Python 2 运行时上运行的开发者。如果您不担心此问题,请跳过此部分。
如需创建可正常运行的 Python 2 版模块 21 应用,您需要:
- 运行时要求:支持 Python 2 的配置文件,以及为避免 Python 3 不兼容性而在主应用中做出的必要更改
- 次要库变更:在将一些必需的功能添加到 Resource Manager 客户端库之前,Python 2 已被弃用。因此,您需要寻找替代方法来访问缺失的功能。
我们现在就来执行这些步骤,首先是配置。
恢复 appengine_config.py
在本教程的前面部分,我们引导您删除了 appengine_config.py,因为 Python 3 App Engine 运行时不使用该文件。对于 Python 2,不仅必须保留它,而且还需要更新模块 20 appengine_config.py 以支持使用内置第三方库,即 grpcio 和 setuptools。只要您的 App Engine 应用使用 Cloud 客户端库(例如 Cloud NDB 和 Cloud Resource Manager 的客户端库),就必须使用这些软件包。
您稍后会将这些软件包添加到 app.yaml,但为了让应用能够访问它们,必须调用 setuptools 中的 pkg_resources.working_set.add_entry() 函数。这样,安装在 lib 文件夹中的已复制(自行捆绑或供应商提供的)第三方库便能够与内置库通信。
对 appengine_config.py 文件进行以下更新,以使这些更改生效:
之前:
from google.appengine.ext import vendor
# Set PATH to your libraries folder.
PATH = 'lib'
# Add libraries installed in the PATH folder.
vendor.add(PATH)
仅凭此代码不足以支持使用 setuptools 和 grpcio。还需要添加几行代码,因此请更新 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)
如需详细了解支持 Cloud 客户端库所需的更改,请参阅迁移捆绑服务文档。
app.yaml
与 appengine_config.py 类似,app.yaml 文件必须恢复为支持 Python 2 的文件。我们先从原始模块 20 app.yaml 开始:
之前:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
除了前面提到的 setuptools 和 grpcio 之外,还有一个依赖项(与 Identity Platform 迁移没有明确关系)需要使用 Cloud Storage 客户端库,该库需要另一个内置的第三方软件包 ssl。在新的 libraries 部分中添加所有这三个软件包,并选择这些软件包的“最新”可用版本,以添加到 app.yaml:
升级后:
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
requirements.txt
对于模块 21,我们向 Python 3 requirements.txt 添加了 Google Auth、Cloud NDB、Cloud Resource Manager 和 Firebase Admin SDK。Python 2 的情况更为复杂:
- Resource Manager API 提供了示例应用所需的允许政策功能。遗憾的是,Cloud Resource Manager 客户端库的最终 Python 2 版本中尚未提供此支持。(仅在 Python 3 版本中提供。)
- 因此,需要提供一种通过 API 访问此功能的替代方法。解决方案是使用较低级别的 Google API 客户端库与 API 进行通信。如需切换到此客户端库,请将
google-cloud-resource-manager替换为较低级别的google-api-python-client软件包。 - 由于 Python 2 已停用,因此支持模块 21 的依赖关系图需要将某些软件包锁定到特定版本。即使某些软件包未在 Python 3
app.yaml中指定,也必须调用它们。
之前:
flask
从模块 20 requirements.txt 开始,将其更新为以下内容,以获得有效的模块 21 应用:
升级后:
grpcio==1.0.0
protobuf<3.18.0
six>=1.13.0
flask
google-gax<0.13.0
google-api-core==1.31.1
google-api-python-client<=1.11.0
google-auth<2.0dev
google-cloud-datastore==1.15.3
google-cloud-firestore==1.9.0
google-cloud-ndb
google-cloud-pubsub==1.7.0
firebase-admin
随着依赖项的更改,软件包和版本号将在代码库中更新,但在撰写本文时,此 app.yaml 足以让应用正常运行。
其他配置更新
如果您尚未删除此 Codelab 前面部分中的 lib 文件夹,请立即删除。使用新更新的 requirements.txt,发出此熟悉的命令,将这些要求安装到 lib 中:
pip install -t lib -r requirements.txt # or pip2
如果您在开发系统上同时安装了 Python 2 和 3,则可能需要使用 pip2 而不是 pip。
修改应用代码
幸运的是,大部分必需的更改都在配置文件中。应用代码中唯一需要更改的地方是,进行小幅更新,以使用较低级别的 Google API 客户端库(而非 Resource Manager 客户端库)来访问 API。templates/index.html Web 模板无需更新。
更新导入和初始化
将 Resource Manager 客户端库 (google.cloud.resourcemanager) 替换为 Google API 客户端库 (googleapiclient.discovery),如下所示:
之前:
from flask import Flask, render_template, request
from google.auth import default
from google.cloud import ndb, resourcemanager
from firebase_admin import auth, initialize_app
升级后:
from flask import Flask, render_template, request
from google.auth import default
from google.cloud import ndb
from googleapiclient import discovery
from firebase_admin import auth, initialize_app
面向 App Engine 管理员用户的支持服务
需要在 _get_gae_admins() 中进行一些更改,以支持使用较低级别的客户端库。我们先讨论一下有哪些变化,然后再为您提供所有更新代码。
Python 2 代码需要同时使用从 google.auth.default() 返回的凭据和项目 ID。凭据在 Python 3 中未使用,因此已分配给通用下划线 ( _) 虚拟变量。由于 Python 2 版本需要它,因此请将下划线更改为 CREDS。此外,您将创建 API 服务端点(在概念上与 API 客户端类似),而不是创建 Resource Manager API 客户端,因此我们保留相同的变量名称 (rm_client)。不同之处在于,实例化服务端点需要凭据 (CREDS)。
以下代码反映了这些变化:
之前:
_, PROJ_ID = default( # Application Default Credentials and project ID
['https://www.googleapis.com/auth/cloudplatformprojects.readonly'])
rm_client = resourcemanager.ProjectsClient()
升级后:
CREDS, PROJ_ID = default( # Application Default Credentials and project ID
['https://www.googleapis.com/auth/cloud-platform'])
rm_client = discovery.build('cloudresourcemanager', 'v1', credentials=CREDS)
另一个区别是,Resource Manager 客户端库返回使用点状属性表示法的允许政策对象,而较低级别的客户端库返回使用方括号 ( [ ]) 的 Python 字典,例如,Resource Manager 客户端库使用 binding.role,而较低级别的库使用 binding['role']。前者还使用“下划线分隔”名称,而较低级别的库则偏好使用“驼峰式命名”名称,并且传递 API 参数的方式略有不同。
这些使用方面的差异如下所示:
之前:
allow_policy = rm_client.get_iam_policy(resource='projects/%s' % PROJ_ID)
for b in allow_policy.bindings: # bindings in IAM allow-policy
if b.role in _TARGETS: # only look at GAE admin roles
admins.update(user.split(':', 1).pop() for user in b.members)
升级后:
allow_policy = rm_client.projects().getIamPolicy(resource=PROJ_ID).execute()
for b in allow_policy['bindings']: # bindings in IAM allow-policy
if b['role'] in _TARGETS: # only look at GAE admin roles
admins.update(user.split(':', 1).pop() for user in b['members'])
将所有这些更改放在一起,将 Python 3 _get_gae_admins() 替换为等效的 Python 2 版本:
def _get_gae_admins():
'return set of App Engine admins'
# setup constants for calling Cloud Resource Manager API
CREDS, PROJ_ID = default( # Application Default Credentials and project ID
['https://www.googleapis.com/auth/cloud-platform'])
rm_client = discovery.build('cloudresourcemanager', 'v1', credentials=CREDS)
_TARGETS = frozenset(( # App Engine admin roles
'roles/viewer',
'roles/editor',
'roles/owner',
'roles/appengine.appAdmin',
))
# collate users who are members of at least one GAE admin role (_TARGETS)
admins = set() # set of all App Engine admins
allow_policy = rm_client.projects().getIamPolicy(resource=PROJ_ID).execute()
for b in allow_policy['bindings']: # bindings in IAM allow-policy
if b['role'] in _TARGETS: # only look at GAE admin roles
admins.update(user.split(':', 1).pop() for user in b['members'])
return admins
is_admin() 函数不需要任何更新,因为它依赖于已更新的 _get_gae_admins()。
至此,将 Python 3 模块 21 应用向后移植到 Python 2 所需的更改已全部完成。恭喜,您已成功更新到第 21 模块的示例应用!您可以在模块 21a 代码库文件夹中找到所有代码。
7. 总结/清理
此 Codelab 的最后几个步骤是确保运行此应用的主账号(用户或服务账号)拥有适当的权限,然后部署应用以确认其按预期运行,并且更改反映在输出中。
能够读取 IAM 允许政策
之前,我们介绍了要被识别为 App Engine 管理员用户所需的四个角色,但现在需要熟悉第五个角色:
roles/viewerroles/editorroles/ownerroles/appengine.appAdminroles/resourcemanager.projectIamAdmin(适用于访问 IAM 允许政策的主账号)
借助 roles/resourcemanager.projectIamAdmin 角色,正文可以确定最终用户是否是任何 App Engine 管理员角色的成员。如果未加入 roles/resourcemanager.projectIamAdmin,则对 Cloud Resource Manager API 的调用(用于获取允许政策)将会失败。
您无需在此处采取任何明确的操作,因为您的应用将在 App Engine 的默认服务账号下运行,该账号会自动获得此角色的成员资格。即使您在开发阶段使用默认服务账号,我们也强烈建议您创建并使用用户管理的服务账号,该账号具有应用正常运行所需的最低权限。如需向此类服务账号授予会员资格,请运行以下命令:
$ gcloud projects add-iam-policy-binding PROJ_ID --member="serviceAccount:USR_MGD_SVC_ACCT@PROJ_ID.iam.gserviceaccount.com" --role=roles/resourcemanager.projectIamAdmin
PROJ_ID 是云项目 ID,USR_MGD_SVC_ACCT@PROJ_ID.iam.gserviceaccount.com 是您为应用创建的用户管理的的服务账号。此命令会输出项目的更新后 IAM 政策,您可以在其中确认服务账号是否具有 roles/resourcemanager.projectIamAdmin 中的成员资格。如需了解详情,请参阅参考文档。再次强调,您无需在此 Codelab 中发出该命令,但请保存此命令,以便在更新您自己的应用时参考。
部署并验证应用
使用标准 gcloud app deploy 命令将应用上传到云端。部署后,您应该会看到与模块 20 应用几乎完全相同的功能,只是您已成功将 App Engine Users 服务替换为 Cloud Identity Platform(和 Firebase Auth)以进行用户管理:

与模块 20 相比,您会发现一个不同之处:点击“登录”后,系统会显示一个弹出式内容(窗口/广告/etc.),而不是重定向,如下面的一些屏幕截图所示。不过,与模块 20 类似,此行为会因已向浏览器注册的 Google 账号数量而略有不同。
如果浏览器中没有已注册的用户,或者只有一个尚未登录的用户,则会显示一个通用的 Google 登录弹出式窗口:

如果单个用户已在浏览器中注册,但又在其他位置登录,则不会显示对话框(或对话框会弹出并立即关闭),并且应用会进入已登录状态(显示用户电子邮件地址和退出按钮)。
有些开发者可能希望提供账号选择器,即使只有一个用户也是如此:

如需实现此功能,请按照前面的说明取消对网页模板中 provider.setCustomParameters({prompt: 'select_account'}); 行的注释。
如果有多个用户,系统会弹出账号选择器对话框(见下文)。如果用户尚未登录,系统会提示用户登录。如果已登录,弹出式窗口会消失,应用会进入已登录状态。

模块 21 的登录状态与模块 20 的界面相同:

管理员用户登录时也是如此:

与模块 21 不同,模块 20 始终从应用(服务器端代码)访问 Web 模板内容的逻辑。模块 20 的一个缺陷是,当最终用户首次访问应用时,系统会注册一次访问;当用户登录时,系统会注册另一次访问。
对于模块 21,登录逻辑仅在 Web 模板(客户端代码)中进行。无需进行任何必需的服务器端往返,即可确定要显示的内容。对服务器的唯一调用是在最终用户登录后检查管理员用户。这意味着,登录和退出不会注册额外的访问,因此“最近的访问”列表在用户管理操作期间会保持不变。请注意,上述屏幕截图显示的是同一组四次访问,但用户登录次数却不止一次。
模块 20 的屏幕截图展示了此 Codelab 开头的“双重访问 bug”。系统会针对每次登录或退出操作显示单独的访问日志。检查每张屏幕截图的最近访问时间戳,以了解时间顺序。
清理
常规
如果您暂时不想继续操作,建议您停用 App Engine 应用,以免产生结算费用。不过,如果您想进一步测试或实验,App Engine 平台有免费配额,因此只要您不超过该使用层级,就不会产生费用。这是计算费用,但相关 App Engine 服务也可能会产生费用,因此请查看其价格页面了解详情。如果此迁移涉及其他 Cloud 服务,则这些服务会单独计费。在任何一种情况下,如果适用,请参阅下文中的“本 Codelab 特有的问题”部分。
为了完全公开透明,我们在此说明,部署到 Google Cloud 无服务器计算平台(例如 App Engine)会产生少量 build 和存储费用。Cloud Build 和 Cloud Storage 都有各自的免费配额。存储该图片会占用部分配额。不过,您可能居住在没有此类免费层的地区,因此请注意存储空间用量,以尽可能减少潜在费用。您应查看的特定 Cloud Storage“文件夹”包括:
console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/imagesconsole.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com- 上述存储链接取决于您的
PROJECT_ID和LOCation,例如,如果您的应用托管在美国,则为“us”。
另一方面,如果您不打算继续学习此应用或其他相关迁移 Codelab,并且想要彻底删除所有内容,请关闭您的项目。
此 Codelab 特有的
以下列出的服务是此 Codelab 特有的。如需了解详情,请参阅各个产品的文档:
- App Engine Datastore 服务由 Cloud Datastore(Datastore 模式的 Cloud Firestore)提供,后者也提供免费层级;如需了解详情,请参阅其价格页面。
- Cloud Identity Platform 的使用在一定程度上是“免费”的,具体取决于您使用的服务。如需了解详情,请参阅其价格页面。
- 根据 Cloud Resource Manager API 的价格页面,使用该 API 在大多数情况下都是免费的。
后续步骤
除了本教程之外,还有其他迁移模块重点介绍如何从旧版捆绑服务迁出,您可以考虑使用这些模块,包括:
- 模块 2:从 App Engine
ndb迁移到 Cloud NDB - 模块 7-9:从 App Engine 任务队列(推送任务)迁移到 Cloud Tasks
- 模块 12-13:从 App Engine Memcache 迁移到 Cloud Memorystore
- 模块 15-16:从 App Engine Blobstore 迁移到 Cloud Storage
- 模块 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 还提供了有关应考虑哪些迁移以及任何相关的迁移模块“顺序”的指南。
8. 其他资源
下面列出了一些其他资源,供开发者进一步探索本模块或相关迁移模块。您可以在下方提供对此内容的反馈,找到代码链接以及您可能会觉得有用的各种文档。
Codelab 问题/反馈
如果您发现本 Codelab 存在任何问题,请先搜索您的问题,然后再提交。用于搜索和创建新问题的链接:
迁移时可参考的资源
下表中提供了模块 20(开始)和模块 21(完成)的代码库文件夹链接。
Codelab | Python 2 | Python 3 |
(不适用) | ||
模块 21(本 Codelab) |
在线参考
以下是与本教程相关的资源:
Cloud Identity Platform 和 Cloud Marketplace
- Identity Platform 产品页面
- Firebase Authentication
- Identity Platform 和 Firebase Auth 产品比较页面
- Identity Platform 价格信息
- Identity Platform 配额(以及无检测工具的用量)
- Identity Platform 提供商设置
- Cloud Marketplace 产品页面
- Marketplace 中的 Identity Platform 页面
Cloud Resource Manager、Cloud IAM、Firebase Admin SDK
- Resource Manager 产品页面
- Resource Manager 价格信息
- Resource Manager 客户端库
- Cloud IAM 概览(角色、允许政策等)
- Firebase Admin SDK (Python)
App Engine Users、App Engine NDB、Cloud NDB、Cloud Datastore
- App Engine Users 概览
- App Engine NDB 文档
- App Engine NDB 代码库
- Cloud NDB 客户端库
- Cloud NDB 代码库
- Cloud Datastore 产品页面
- Cloud Datastore 价格信息
其他迁移模块参考文档
- 迁移模块简介
- 所有“无服务器迁移站”资源
- 迁移到 Python 3 文档
- 迁移模块 17“在第二代运行时中使用捆绑服务”Codelab
- 迁移模块 20“向 Flask 应用添加 App Engine Users 服务”Codelab
App Engine 迁移
App Engine 平台
- App Engine 文档
- Python 2 App Engine(标准环境)运行时
- 在 Python 2 App Engine 上使用 App Engine 内置库
- Python 3 App Engine(标准环境)运行时
- Python 2 和 3 App Engine(标准环境)运行时之间的差异
- Python 2 到 3 App Engine(标准环境)迁移指南
- App Engine 价格和配额信息
- 第二代 App Engine 平台发布 (2018)
- 比较第一代和第二代平台
- 对旧版运行时的长期支持
- 文档迁移示例
- 社区提供的迁移示例
Cloud SDK
- Google Cloud SDK
- Cloud SDK
gcloud命令行工具 - 启用(和停用)Google API
- Cloud 控制台 API 管理器(启用/停用 API)
- 使用
gcloud启用 Google API - 使用
gcloud列出 Google API
其他云信息
- 在 Google Cloud 上使用 Python
- Python 客户端库文档
- Python 客户端库代码库
- “始终免费”层级
- Cloud SDK
- Cloud SDK
gcloud命令行工具 - 所有 Google Cloud 文档
视频
许可
此作品已获得 Creative Commons Attribution 2.0 通用许可授权。