從 App Engine 使用者服務遷移至 Cloud Identity Platform (單元 21)

1. 總覽

無伺服器遷移站系列的程式碼研究室系列 (自助式實作教學課程) 和相關影片,旨在引導 Google Cloud 無伺服器開發人員透過一或多種遷移作業 (主要用於遷移舊版服務) 逐步翻新應用程式。這麼一來,您的應用程式就能更具可攜性,並提供更多選擇和使用彈性,進而整合及使用更多 Cloud 產品,也更容易升級至較新的語言版本。本系列課程一開始將著重在最早的 Cloud 使用者 (主要是 App Engine (標準環境) 開發人員),但涵蓋其他無伺服器平台,包括 Cloud FunctionsCloud Run 或其他無伺服器平台 (如適用)。

本程式碼研究室旨在向 Python 2 App Engine 開發人員說明如何從 App Engine Users API/服務遷移至 Cloud Identity Platform (GCIP)。也提供從 App Engine NDB 間接遷移Cloud NDB 的做法,讓可存取 Datastore (主要涵蓋於遷移單元 2),以及升級至 Python 3。

單元 20 說明如何將 Users API 的使用方式新增至模組 1 範例應用程式中。在本單元中,您將會一一完成單元 20 的應用程式,並將該模組的用法遷移至 Cloud Identity Platform。

在接下來的研究室中

軟硬體需求

問卷調查

您會如何使用這個教學課程?

只能閱讀 閱讀並完成練習

您對 Python 的使用體驗有何評價?

新手 中級 還算容易

針對使用 Google Cloud 服務的經驗,您會給予什麼評價?

新手 中級 還算容易

2. 背景

App Engine Users 服務是供 App Engine 應用程式使用的使用者驗證系統。這項工具提供 Google 登入做為識別資訊提供者,提供可在應用程式中使用的登入和登出連結,同時支援管理員使用者和管理員專屬功能的概念。為提升應用程式可攜性,Google Cloud 建議您從舊版 App Engine 套裝組合服務遷移至 Cloud 獨立服務,例如從使用者服務遷移至 Cloud Identity Platform 等。

Identity Platform 是以 Firebase 驗證為基礎,並新增了多項企業功能,包括多重驗證、OIDC 與支援 SAML 單一登入 (SSO)、多用戶群架構、99.95% 的服務水準協議等。此外,Identity Platform 和 Firebase 驗證產品的比較頁面也會醒目顯示這些差異。與「使用者」服務所提供的功能相比,這兩項產品提供的功能還多。

本單元單元 21 程式碼研究室示範如何將應用程式的使用者驗證從使用者服務切換至 Identity Platform 功能,其中最能反映單元 20 中的功能。單元 21 也包含從 App Engine NDB 遷移至 Cloud NDB 來存取 Datastore 的功能,並重複執行模組 2 遷移作業。

當模組 20 程式碼為「已宣傳」時如同 Python 2 範例應用程式,原始碼本身與 Python 2 和 Python 3 相容,而且在本單元 21 中遷移至 Identity Platform (和 Cloud NDB) 後,來源本身仍保持不變。您可以在升級至 Python 3 時繼續使用 Users 服務,因為不一定要遷移至 Identity Platform。請參閱單元 17 程式碼研究室和影片,瞭解如何在升級至 Python 3 等第 2 代執行階段時繼續使用套裝組合服務。

本教學課程包含下列步驟:

  1. 設定/事前作業
  2. 更新設定
  3. 修改應用程式程式碼

3. 設定/事前作業

本節說明如何:

  1. 設定 Cloud 專案
  2. 取得基準範例應用程式
  3. (重新) 部署及驗證基準應用程式
  4. 啟用新的 Google Cloud 服務/API

這些步驟可以確保您從可遷移至獨立 Cloud 服務的可用程式碼開始著手。

1. 設定專案

如果您已完成單元 20 程式碼研究室,請重複使用相同的專案和程式碼。或者,您可以建立新的專案,或是重複使用其他現有專案。請確認專案具備有效的帳單帳戶和已啟用的 App Engine 應用程式。在本程式碼研究室中,請找出並備妥專案 ID,每次遇到 PROJ_ID 變數時都能使用這個 ID。

2. 取得基準範例應用程式

其中一項必要條件是正常運作的 Module 20 App Engine 應用程式,因此請完成其程式碼研究室 (建議;上方連結),或從存放區複製模組 20 程式碼。無論您使用自有或我們的資料,我們都會從這裡開始 (「START」)。本程式碼研究室將引導您逐步完成遷移作業,最後程式碼會與單元 21 存放區資料夾 (「FINISH」) 中顯示的程式碼類似。

複製模組 20 存放區資料夾。輸出內容應如下所示。如果您已完成單元 20 程式碼研究室,可能就會看到 lib 資料夾:

$ ls
README.md               appengine_config.py     templates
app.yaml                main.py                 requirements.txt

3. (重新) 部署及驗證基準應用程式

執行下列步驟來部署模組 20 應用程式:

  1. 如有 lib 資料夾,請刪除該資料夾,然後執行 pip install -t lib -r requirements.txt 來重新填入資料夾。如果您同時安裝 Python 2 和 3,可能需要使用 pip2
  2. 確認您已安裝初始化 gcloud 指令列工具,並檢查其使用情況
  3. 如果您不想在核發的每個 gcloud 指令中輸入 PROJ_ID,請先使用 gcloud config set project PROJ_ID 設定 Cloud 專案。
  4. 使用 gcloud app deploy 部署範例應用程式
  5. 確認應用程式可正常運作,且不會出現錯誤。如果您已完成單元 20 程式碼研究室,應用程式會在頂端顯示使用者登入資訊 (使用者電子郵件、可能的「管理員徽章」和登入/登出按鈕),以及最近的造訪記錄 (如下圖所示)。

907e64c19ef964f8.png

以一般使用者的身分登入會導致系統顯示使用者的電子郵件地址,並且顯示「登入」按鈕會變成「登出」按鈕:

ad7b59916b69a035.png

以管理員使用者身分登入後,使用者的電子郵件地址會和「(admin)」一起顯示:

867bcb3334149e4.png

4. 啟用新的 Google Cloud API/服務

簡介

模組 20 應用程式採用 App Engine NDB 和使用者 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 專案不需要計費的 Cloud API/服務,不妨考慮採用這種做法。
  • 根據定價頁面,Cloud Resource Manager API 大部分都可免費使用。

使用者可以依據自己的偏好,透過 Cloud 控制台或指令列 (透過 Cloud SDK 中的 gcloud 指令) 啟用 Cloud API。首先介紹 Cloud Datastore 和 Cloud Resource Manager API。

使用 Cloud 控制台

前往 Cloud 控制台中的 API 管理員的程式庫頁面 (需找到正確的專案),然後使用搜尋列搜尋 API。c7a740304e9d35b.png

啟用下列 API:

分別找出並點選各個 API 的「啟用」按鈕。系統可能會要求您提供帳單資訊。舉例來說,以下是 Resource Manager API 的網頁:

fc7bd8f4c49d12e5.png

啟用後 (通常會在幾秒鐘後),按鈕會變成 [管理]:

8eca12d6cc7b45b0.png

以下列方式啟用 Cloud Datastore:

83811599b110e46b.png

透過指令列

雖然從控制台啟用 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 以外的資源,或依賴於 Google Cloud 以外的資源,例如 Firebase 驗證。目前您只能透過 Cloud 控制台啟用 Marketplace 服務。步驟如下:

  1. 前往 Cloud Marketplace 中的「Cloud Identity Platform」頁面,然後按一下其中的 [啟用] 按鈕。當系統提示時,從 Firebase 驗證升級。這樣一來,您就能使用額外功能,例如先前「背景」部分所述的功能。以下是醒目顯示「Enable」按鈕的 Marketplace 頁面:28475f1c9b29de69.png
  2. 啟用 Identity Platform 之後,系統會將您自動導向「Identity Providers」(識別資訊提供者) 頁面。如果不是,請透過這個方便的連結前往「網頁」。fc2d92d42a5d1dd7.png
  3. 啟用 Google 驗證提供者。如果尚未設定提供者,請按一下「Add a Provider」(新增提供者),然後選取「Google」。當您返回這個畫面時,Google 項目應已啟用。Google 是本教學課程中唯一使用的驗證供應商,目的是將「App Engine 使用者」服務鏡像為輕量的 Google 登入服務。您可在自己的應用程式中啟用其他驗證服務供應商。
  4. 選取並設定 Google 和其他想要的驗證服務供應商後,請按一下「應用程式設定詳細資料」,接著從確認對話方塊視窗中,複製「網頁」分頁中 config 物件中的 apiKeyauthDomain,並將兩者儲存在安全的地方。為什麼不複製全部?這個對話方塊中的程式碼片段採用硬式編碼且註明日期,因此只要儲存最重要的位元,即可用於程式碼,同時保有更多 Firebase 驗證的用量。將值複製並儲存在安全的地方後,請按一下「關閉」按鈕,完成所有必要設定。bbb09dcdd9be538e.png

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 範例應用程式沒有靜態檔案處理常式。應用程式如有必要,請保持原樣。您可以按照 app.yaml 遷移指南的說明,移除所有指令碼處理常式,也可以保留這些處理常式做為參考,但只要將其控制代碼變更為 auto 即可。完成這些變更後,更新 Python 3 的 app.yaml 會簡化為:

變更後:

runtime: python310

其他設定更新

無論是要保留 Python 2,還是移植到 Python 3,如有 lib 資料夾,請刪除該資料夾。

5. 修改應用程式程式碼

本節將更新主要應用程式檔案 main.py,並將 App Engine 使用者服務替換為 Cloud Identity Platform。更新主要應用程式後,您將更新網路範本 templates/index.html

更新匯入和初始化作業

請按照下列步驟更新匯入作業並初始化應用程式資源:

  1. 匯入時,請將 App Engine NDB 替換為 Cloud NDB。
  2. 除了 Cloud NDB 以外,也匯入 Cloud Resource Manager。
  3. Identity Platform 是以 Firebase Auth 為基礎,因此請匯入 Firebase Admin SDK。
  4. Cloud API 需要使用 API 用戶端,因此請在初始化 Flask 後,為 Cloud NDB 啟動用戶端。

當這裡匯入 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 allow-policy。allow-policy 會定義並強制執行要授予哪些「主體」 (人類使用者、服務帳戶等) 的角色。設定內容包括:

  • 正在擷取 Cloud 專案 ID (PROJ_ID)
  • 建立 Resource Manager API 用戶端 (rm_client)
  • 建立一組 App Engine 管理員角色 (唯讀) (_TARGETS)

Resource Manager 需要 Cloud 專案 ID,因此請匯入 google.auth.default() 並呼叫該函式,以取得專案 ID。該呼叫具有看起來類似網址的參數,但為 OAuth2 權限範圍。以 Compute Engine VM 或 App Engine 應用程式等方式在雲端執行應用程式時,系統會提供具備大範圍權限的預設服務帳戶。為遵循最低權限的最佳做法,建議您自行建立使用者自行管理的服務帳戶

針對 API 呼叫,建議您「進一步」縮減應用程式的範圍,使其達到正常運作所需的最低級別。我們將要進行的 Resource Manager API 呼叫是 get_iam_policy()需要下列其中一個範圍才能運作:

  • https://www.googleapis.com/auth/cloud-platform
  • https://www.googleapis.com/auth/cloud-platform.read-only
  • https://www.googleapis.com/auth/cloudplatformprojects
  • https://www.googleapis.com/auth/cloudplatformprojects.readonly

範例應用程式只需具備 allow-policy 的唯讀存取權。而不會修改政策,也不需要存取整個專案。這表示應用程式不需要前三項所需的任何權限。最後一個是必要資訊,這就是我們要為範例應用程式實作的內容。

函式的主體會建立一組空白的管理員使用者 (admins),透過 get_iam_policy() 擷取 allow_policy,並迴圈所有專門尋找 App Engine 管理員角色的繫結:

  • roles/viewer
  • roles/editor
  • roles/owner
  • roles/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

使用者登入應用程式時,會發生以下情況:

  1. 使用者登入 Firebase 後,系統會使用網頁範本快速檢查。
  2. 當範本中的驗證狀態變更時,會向 /is_admin 發出 Ajax 樣式的 fetch() 呼叫,其處理常式為下一個函式 is_admin()
  3. 系統會將 Firebase ID 權杖傳遞至 POST 主體至 is_admin(),後者會從標頭中取出該權杖,並呼叫 Firebase Admin SDK 進行驗證。如果是有效的使用者,請擷取對方的電子郵件地址,並確認對方是否為管理員使用者。
  4. 接著,布林結果會傳回範本,做為成功的 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

這兩個函式的所有程式碼都必須複製「使用者」服務提供的功能,具體來說是函式的 is_current_user_admin() 函式。與實作替代解決方案的單元 21 不同,單元 20 中的這個函式呼叫會執行所有繁重的工作。好消息是,應用程式不再依賴僅限 App Engine 的服務,也就是說,您可以將應用程式遷移至 Cloud Run 或其他服務。此外,您也可以變更「admin user」的定義對於自己的應用程式,只需在 _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()。在程式碼中,這表示您將 Datastore 呼叫納入 Python with 區塊內的 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)

將使用者登入邏輯移至網路範本

App Engine Users 服務是在伺服器端進行,Firebase 驗證和 Cloud Identity Platform 則主要屬於用戶端。因此,模組 20 應用程式中的大部分使用者管理程式碼會移至模組 21 網路範本。

main.py 中,網路環境會將五項重要資料傳遞至範本,前四則與使用者管理相關聯,且會因使用者登入狀態而有所不同:

  • who:使用者的電子郵件地址 (如果已登入),或為 <使用者>
  • admin — 如果登入的使用者是管理員,則為 (管理員) 徽章
  • 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)

所有使用者管理都將移至網路範本,因此我們只保留造訪,將主要處理常式移回單元 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 驗證,以及所有遷移至 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 驗證模組匯入:

<!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 設定部分,您儲存了「Application Setup Details」對話方塊的 apiKeyauthDomain。請在下一節中將這些值新增至 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 帳戶)。也就是說,如果您有多個帳戶,系統會顯示「帳戶挑選者」如預期:a38369389b7c4c7e.png;不過,如果工作階段中只有一位使用者,使用者不需與使用者互動,系統就會自動完成登入程序。(彈出式視窗隨後會消失)。您可以取消註解自訂參數行,強制讓一位使用者顯示帳戶挑選器對話方塊 (而非立即登入應用程式)。若啟用後,即使是單一使用者登入,還會顯示帳戶挑選器:b75624cb68d94557.png

登入和登出函式

下幾行程式碼構成登入或登出按鈕點擊的功能:

// 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

  1. 使用者的電子郵件地址會設定為要顯示的。
  2. 「Login」按鈕會變更為「Logout」
  3. 系統會為 /is_admin 的 Ajax 樣式呼叫,以決定是否要顯示 (admin) 管理員使用者徽章。

使用者登出時,系統會執行 else 子句來重設所有使用者資訊:

  1. 使用者名稱已設為 user
  2. 所有管理員徽章已移除
  3. 「Logout」按鈕已改回「Login」(登入)

範本變數

標題區段結束後,主要內文會以範本變數取代範本變數,這些變數會視需要變更:

  1. 顯示的使用者名稱
  2. (admin) 管理員徽章 (如適用)
  3. 「Login」或「Logout」按鈕
<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 所需的變更。恭喜您進入新的單元 21 範例應用程式!您可以在模組 21b 存放區資料夾中查看我們的版本。

本程式碼研究室的下一部分是選用 (*),僅適用於應用程式必須維持在 Python 2 的使用者,系統會引導你完成必要的 Python 2 單元 21 應用程式。

6. *Python 2 向後移植

這個選用章節適用於執行 Identity Platform 遷移作業的開發人員,但使用者必須繼續在 Python 2 執行階段上執行。如果你沒有顧慮,請略過這一節。

如要建立可正常運作的 Python 2 版本模組 21 應用程式,您需要下列項目:

  1. 執行階段需求:支援 Python 2 的設定檔,以及主要應用程式中的必要變更,以免 Python 3 不相容
  2. 變更程式庫的次要變更:在 Resource Manager 用戶端程式庫加入某些必要功能之前,Python 2 已淘汰。因此,您需要透過其他方式存取缺少的功能。

讓我們從設定開始,即採取這些步驟。

還原 appengine_config.py

在本教學課程的先前版本中,由於 Python 3 App Engine 執行階段並未使用 appengine_config.py,系統已引導您刪除該物件。如果是 Python 2,您不僅必須保留該模組,而且需要更新模組 20 appengine_config.py,才能支援使用內建的第三方程式庫 (即 grpciosetuptools)。每當 App Engine 應用程式使用類似 Cloud NDB 和 Cloud Resource Manager 的 Cloud 用戶端程式庫時,都必須用到這些套件。

您會暫時將這些套件新增至 app.yaml,但為了讓應用程式存取,必須呼叫 setuptoolspkg_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)

僅此程式碼無法支援使用 setuptoolsgrpcio。還需要幾行,因此請更新 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

除了上述的 setuptoolsgrpcio 之外,還有依附元件 (與 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 AuthCloud NDBCloud Resource ManagerFirebase 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 已足以應付運作中的應用程式。

其他設定更新

如果您尚未刪除本程式碼研究室中的 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 網路範本。

更新匯入和初始化作業

將 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。此外,您無須建立 Resource Manager API 用戶端,而是會建立 API 服務端點 (與 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 用戶端程式庫會傳回使用虛線屬性標記法的 allow-policy 物件,而較低層級的用戶端程式庫會傳回使用方括號 ( [ ]) 的 Python 字典,例如對 Resource Manager 用戶端程式庫使用 binding.role,而較低層級程式庫則使用 binding['role']。前者也會使用「底線分隔」以及偏好使用「CamelCased」的低階程式庫還有一種略為不同的方式傳入 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. 摘要/清除

本程式碼研究室的最後一個步驟是確保執行這個應用程式的主體 (使用者或服務帳戶) 具備適當權限,然後部署應用程式以確認其正常運作,並將變更反映在輸出內容中。

可讀取 IAM 允許政策

先前提到,您必須瞭解取得 App Engine 管理員使用者的 4 個角色,而現在還有第五個角色很熟悉:

  • roles/viewer
  • roles/editor
  • roles/owner
  • roles/appengine.appAdmin
  • roles/resourcemanager.projectIamAdmin (適用於存取 IAM 允許政策的主體)

roles/resourcemanager.projectIamAdmin 角色可讓主體判斷使用者是否為任何 App Engine 管理員角色。如果沒有 roles/resourcemanager.projectIamAdmin 的成員資格,呼叫 Cloud Resource Manager API 以取得 allow-policy 時會失敗。

不需要在這裡採取任何明確行動,因為應用程式會在 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 是 Cloud 專案 ID,USR_MGD_SVC_ACCT@PROJ_ID.iam.gserviceaccount.com 是您為應用程式建立的使用者管理服務帳戶。這個指令會輸出專案更新後的 IAM 政策,方便您確認服務帳戶在 roles/resourcemanager.projectIamAdmin 中具有成員資格。詳情請參閱參考說明文件。如要重複說明,您不需要在本程式碼研究室中發出該指令,但可以儲存為此參考,以翻新自己的應用程式。

部署及驗證應用程式

使用標準 gcloud app deploy 指令將應用程式上傳至雲端。部署完成後,功能應會顯示「幾乎」與模組 20 應用程式相同。不過,您已成功透過 Cloud Identity Platform 和 Firebase Auth 取代 App Engine 使用者服務,以便管理使用者:

(3a83ae745121d70.png)

與單元 20 相比,點選 [登入] 結果會開啟彈出式視窗 (而非重新導向),如以下螢幕截圖所示。不過,就像單元 20 一樣,視瀏覽器上已註冊的 Google 帳戶數量而定,運作方式會稍有不同。

如果瀏覽器沒有註冊任何使用者,或是單一使用者尚未登入,系統會顯示一般的 Google 登入彈出式視窗:

8437f5f3d489a942.png

如果某位使用者已透過瀏覽器註冊,卻在別處登入,系統就不會顯示對話方塊 (或彈出並立即關閉),且應用程式會進入登入狀態 (顯示使用者電子郵件和「登出」按鈕)。

有些開發人員可能想提供帳戶挑選器,即使是單一使用者也沒問題:

b75624cb68d94557.png

如要實作這項功能,請按照上述步驟取消註解網站範本中的 provider.setCustomParameters({prompt: 'select_account'}); 行。

如有多位使用者,系統會彈出帳戶挑選器對話方塊 (請見下文)。如果尚未登入,系統會提示使用者。如果您已登入,彈出式視窗會消失,且應用程式會進入登入狀態。

c454455b6020d5e4.png

模組 21 的登入狀態看起來與模組 20 的使用者介面相同:

49ebe4dcc1eff11f.png

管理員使用者登入時也是如此:

44302f35b39856eb.png

與模組 21 不同,模組 20 一律會從應用程式存取網路範本內容 (伺服器端程式碼)。單元 20 的缺點是使用者首次按下應用程式時會登錄一次造訪,在使用者登入時登錄了一次。

單元 21 的登入邏輯只需在網路範本 (用戶端程式碼) 中進行。過程中不必經過伺服器端行程,也能判斷要顯示哪些內容。在使用者登入後,系統只會對伺服器發出的呼叫會檢查管理員使用者。這表示登入和登出並不會登錄其他造訪記錄,因此,最近的造訪清單會在使用者管理動作上保持不變。請注意,以上螢幕截圖顯示跨多使用者登入作業的四次造訪相同。

單元 20 螢幕截圖示範「重複造訪錯誤」。每次登入或登出動作都會以個別的造訪記錄顯示。查看每張螢幕截圖中最近造訪的時間戳記,並按時間順序顯示。

清除所用資源

一般

如果您現階段已完成設定,建議您停用 App Engine 應用程式,以免產生帳單費用。不過,如果您想測試或進行其他測試,App Engine 平台提供免費配額,而且只要不超出用量限制,就不需支付任何費用。這適用於運算,但相關 App Engine 服務可能也會產生費用,詳情請參閱定價頁面。如果這項遷移作業涉及其他 Cloud 服務,我們會另外計費。無論採用哪種情況,請參閱「本程式碼研究室的專屬」以下章節。

如要完整揭露,部署至 Google Cloud 無伺服器運算平台 (如 App Engine) 會產生少許建構和儲存空間費用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_IDLOC,例如「us」如果應用程式是由美國代管

另一方面,如果您不打算繼續使用這個應用程式或其他相關的遷移程式碼研究室,且想要徹底刪除所有項目,請關閉專案

本程式碼研究室的專屬功能

下列服務專屬於本程式碼研究室。詳情請參閱每項產品的說明文件:

  • App Engine Datastore 服務是由 Cloud Datastore (Datastore 模式的 Cloud Firestore) 提供,這項服務也提供免費方案;詳情請參閱價格頁面
  • Cloud Identity Platform 有些「免費」級別的使用情形取決於您使用的服務。詳情請參閱價格頁面
  • 根據定價頁面,Cloud Resource Manager API 大部分都可免費使用。

後續步驟

除了本教學課程之外,其他著重於淘汰舊版套裝組合服務的其他遷移模組,建議提供以下建議:

  • 模組 2:從 App Engine ndb 遷移至 Cloud NDB
  • 模組 7-9:從 App Engine 工作佇列 (推送工作) 遷移至 Cloud Tasks
  • 模組 12-13:從 App Engine Memcache 遷移至 Cloud Memorystore
  • 模組 15 至 16:從 App Engine Blob 遷移至 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 的情況下執行應用程式

您可以選擇是否要改用其他無伺服器平台,在進行任何變更前,建議您考量應用程式和用途的最佳選擇。

無論接下來選擇使用哪個遷移模組,所有無伺服器遷移站內容 (程式碼研究室、影片、原始碼 [如有]) 都可以透過其開放原始碼存放區存取。存放區的 README 還針對應考慮遷移的項目和任何相關的「訂單」提供指引接下來介紹遷移模組

8. 其他資源

以下列出開發人員可進一步探索這個遷移模組或相關遷移模組的其他資源。請在下方提供有關此內容的意見回饋、程式碼連結,以及各式各樣實用的說明文件。

程式碼研究室問題/意見回饋

如果您在本程式碼研究室中發現任何問題,請先搜尋您的問題再提出申請。搜尋及建立新問題的連結:

遷移資源

下表提供模組 20 (START) 和單元 21 (FINISH) 的存放區資料夾連結。

Codelab

Python 2

Python 3

單元 20

程式碼

(不適用)

單元 21 (本程式碼研究室)

程式碼

程式碼

線上參考資料

以下是與本教學課程相關的資源:

Cloud Identity Platform 和 Cloud Marketplace

Cloud Resource Manager、Cloud IAM、Firebase Admin SDK

App Engine 使用者、App Engine NDB、Cloud NDB、Cloud Datastore

其他遷移模組參考資料

App Engine 遷移

App Engine 平台

Cloud SDK

其他 Cloud 資訊

影片

授權

這項內容採用的是創用 CC 姓名標示 2.0 通用授權。