透過 Python 使用 Cloud Workstations 進行內部迴圈開發

1. 總覽

本實驗室將示範各項功能和工具,協助軟體工程師在容器化環境中開發 Python 應用程式,簡化開發工作流程。一般容器開發作業需要使用者瞭解容器的詳細資料和容器建構程序。此外,開發人員通常必須中斷流程,離開 IDE 在遠端環境中測試及偵錯應用程式。有了本教學課程中提及的工具和技術,開發人員就能在 IDE 中有效處理容器化應用程式。

學習目標

在本實驗室中,您將瞭解如何在 GCP 中使用容器進行開發,包括:

  • 建立新的 Python 啟動條件應用程式
  • 逐步完成開發程序
  • 開發簡易的 CRUD REST 服務
  • 部署至 GKE
  • 偵錯錯誤狀態
  • 運用中斷點 / 記錄
  • 將變更熱部署回 GKE

58a4cdd3ed7a123a.png

2. 設定和需求

自修實驗室環境設定

  1. 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果沒有 Gmail 或 Google Workspace 帳戶,請先建立帳戶

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 專案名稱是這個專案參與者的顯示名稱。這是 Google API 未使用的字元字串。你隨時可以更新該位置資訊。
  • 專案 ID 在所有 Google Cloud 專案中都是不重複的,而且設定後即無法變更。Cloud 控制台會自動產生不重複的字串,通常您不需要在意這個字串。在大多數程式碼研究室中,您需要參照專案 ID (通常會標示為 PROJECT_ID)。如果您不喜歡產生的 ID,可以產生另一個隨機 ID。你也可以嘗試使用自己的名稱,看看是否可用。完成這個步驟後就無法變更,且專案期間都會維持這個設定。
  • 請注意,部分 API 會使用第三個值,也就是「專案編號」。如要進一步瞭解這三種值,請參閱說明文件
  1. 接著,您需要在 Cloud 控制台中啟用帳單,才能使用 Cloud 資源/API。完成本程式碼研究室的費用應該不高,甚至完全免費。如要關閉資源,避免產生本教學課程以外的費用,您可以刪除自己建立的資源,或刪除整個專案。Google Cloud 新使用者可參加價值$300 美元的免費試用計畫。

啟動 Cloud Shell 編輯器

本實驗室專為 Google Cloud Shell 編輯器設計及測試,如要存取編輯器,請按照下列步驟操作:

  1. 前往 https://console.cloud.google.com 存取 Google 專案。
  2. 按一下右上角的 Cloud Shell 編輯器圖示

8560cc8d45e8c112.png

  1. 視窗底部會開啟新窗格
  2. 按一下「開啟編輯器」按鈕

9e504cb98a6a8005.png

  1. 編輯器會開啟,右側顯示檔案總管,中央區域則顯示編輯器
  2. 畫面底部也應顯示終端機窗格
  3. 如果終端機尚未開啟,請使用 `ctrl+`` 鍵組合開啟新的終端機視窗

環境設定

在 Cloud Shell 設定專案的專案 ID 和專案編號,分別儲存為 PROJECT_IDPROJECT_ID 變數。

export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID \
    --format='value(projectNumber)')

佈建本實驗室使用的基礎架構

在本實驗室中,您會將程式碼部署至 GKE,並存取儲存在 Spanner 資料庫中的資料。您也會使用 Cloud Workstations 做為 IDE。下方的設定指令碼會為您準備好基礎架構。

  1. 下載設定指令碼並設為可執行。
wget https://raw.githubusercontent.com/GoogleCloudPlatform/container-developer-workshop/main/labs/python/setup_with_cw.sh
chmod +x setup_with_cw.sh
  1. 開啟 setup_with_cw.sh 檔案,並編輯目前設為 CHANGEME 的密碼值
  2. 執行設定指令碼,建立本實驗室中使用的 GKE 叢集和 Spanner 資料庫
./setup_with_cw.sh &

Cloud Workstations 叢集

  1. 在 Cloud 控制台中開啟 Cloud Workstations。等待叢集處於 READY 狀態。

305e1a3d63ac7ff6.png

建立工作站設定

  1. 如果 Cloud Shell 工作階段已中斷連線,請按一下「重新連線」,然後執行 gcloud 指令列指令來設定專案 ID。執行指令前,請將下方的範例專案 ID 換成您的 Qwiklabs 專案 ID。
gcloud config set project qwiklabs-gcp-project-id
  1. 在終端機下載並執行下列指令碼,建立 Cloud Workstations 設定。
wget https://raw.githubusercontent.com/GoogleCloudPlatform/container-developer-workshop/main/labs/python/workstation_config_setup.sh
chmod +x workstation_config_setup.sh
./workstation_config_setup.sh
  1. 在「設定」部分下方驗證結果。轉換為「READY」狀態需要 2 分鐘。

2e23c2e9983d1ccf.png

  1. 在控制台中開啟 Cloud Workstations,然後建立新執行個體。

a53adeeac81a78c8.png

  1. 將名稱變更為 my-workstation,然後選取現有設定:codeoss-python

f052cd47701ec774.png

  1. 驗證「工作站」部分下的結果。

啟動工作站

  1. 啟動工作站。工作站會在幾分鐘內啟動。

682f8a307032cba3.png

  1. 按一下網址列中的圖示,允許第三方 Cookie。1b8923e2943f9bc4.png

fcf9405b6957b7d7.png

  1. 按一下「網站無法正常運作嗎?」。

36a84c0e2e3b85b.png

  1. 按一下「允許 Cookie」。

2259694328628fba.png

  1. 工作站啟動後,您會看到 Code OSS IDE。在工作站 IDE 的「開始使用」頁面中,按一下「標示為完成」

94874fba9b74cc22.png

3. 建立新的 Python 啟動條件應用程式

在本節中,您將建立新的 Python 應用程式。

  1. 開啟新的終端機。

c31d48f2e4938c38.png

  1. 建立新目錄並開啟做為工作區
mkdir music-service && cd music-service

code-oss-cloud-workstations -r --folder-uri="$PWD"

如果看到這則訊息,請按一下「允許」按鈕,這樣就能複製並貼到工作站。

58149777e5cc350a.png

  1. 建立名為 requirements.txt 的檔案,並將下列內容複製到檔案中

789e8389170bd900.png

Flask
gunicorn
google-cloud-spanner
ptvsd==4.3.2
  1. 建立名為 app.py 的檔案,然後將下列程式碼貼入該檔案:
import os
from flask import Flask, request, jsonify
from google.cloud import spanner

app = Flask(__name__)

@app.route("/")
def hello_world():
    message="Hello, World!"
    return message

if __name__ == '__main__':
    server_port = os.environ.get('PORT', '8080')
    app.run(debug=False, port=server_port, host='0.0.0.0')

  1. 建立名為 Dockerfile 的檔案,然後將下列內容貼入其中:
FROM python:3.8
ARG FLASK_DEBUG=0
ENV FLASK_DEBUG=$FLASK_DEBUG
ENV FLASK_APP=app.py
WORKDIR /app
COPY requirements.txt .
RUN pip install --trusted-host pypi.python.org -r requirements.txt
COPY . .
ENTRYPOINT ["python3", "-m", "flask", "run", "--port=8080", "--host=0.0.0.0"]

附註:FLASK_DEBUG=1 可讓您自動將程式碼變更重新載入 Python Flask 應用程式。這個 Dockerfile 可讓您將這個值做為建構引數傳遞。

產生資訊清單

在終端機中執行下列指令,產生預設的 skaffold.yaml 和 deployment.yaml

  1. 使用下列指令初始化 Skaffold
skaffold init --generate-manifests

系統提示時,請使用箭頭鍵移動游標,並按空格鍵選取選項。

您可以選擇:

  • 8080,適用於連接埠
  • y 儲存設定

更新 Skaffold 設定

  • 變更預設應用程式名稱
  • 開啟「skaffold.yaml
  • 選取目前設為 dockerfile-image 的圖片名稱
  • 按一下滑鼠右鍵,然後選擇「變更所有出現位置」
  • 輸入新名稱,如python-app
  • 進一步編輯建構部分,以
  • docker.buildArgs 新增至票證 FLASK_DEBUG=1
  • 同步設定,將 IDE 中對 *.py 檔案所做的任何變更載入至執行中的容器

編輯後,skaffold.yaml 檔案中的建構部分會如下所示:

build:
 artifacts:
 - image: python-app
   docker:
     buildArgs:
       FLASK_DEBUG: "1"
     dockerfile: Dockerfile
   sync:
     infer:
     - '**/*.py'

修改 Kubernetes 設定檔

  1. 變更預設名稱
  • 開啟 deployment.yaml 檔案
  • 選取目前設為 dockerfile-image 的圖片名稱
  • 按一下滑鼠右鍵,然後選擇「變更所有出現位置」
  • 輸入新名稱,如python-app

4. 逐步完成開發程序

新增商業邏輯後,您現在可以部署及測試應用程式。下一節將展示如何使用 Cloud Code 外掛程式。這個外掛程式會與 skaffold 整合,簡化開發程序。在後續步驟中部署至 GKE 時,Cloud Code 和 Skaffold 會自動建構容器映像檔、將其推送至 Container Registry,然後將 your 應用程式部署至 GKE。這項作業會在幕後進行,將詳細資料從開發人員流程中抽象化。

登入 Google Cloud

  1. 按一下 Cloud Code 圖示,然後選取「Sign in to Google Cloud」(登入 Google Cloud):

1769afd39be372ff.png

  1. 按一下「Proceed to sign in」(繼續登入)。

923bb1c8f63160f9.png

  1. 查看終端機的輸出內容並開啟連結:

517fdd579c34aa21.png

  1. 使用 Qwiklabs 學生憑證登入。

db99b345f7a8e72c.png

  1. 選取「允許」:

a5376553c430ac84.png

  1. 複製驗證碼,然後返回「工作站」分頁。

6719421277b92eac.png

  1. 貼上驗證碼,然後按下 Enter 鍵。

e9847cfe3fa8a2ce.png

新增 Kubernetes 叢集

  1. 新增叢集

62a3b97bdbb427e5.png

  1. 選取 Google Kubernetes Engine:

9577de423568bbaa.png

  1. 選取專案。

c5202fcbeebcd41c.png

  1. 選取在初始設定中建立的「python-cluster」。

719c2fc0a7f9e84f.png

  1. 現在 Cloud Code 下方的 Kubernetes 叢集清單中會顯示該叢集。從這裡瀏覽及探索叢集。

7e5f50662d4eea3c.png

使用 gcloud CLI 設定目前的專案 ID

  1. 從 Qwiklabs 頁面複製這個實驗室的專案 ID。

fcff2d10007ec5bc.png

  1. 在終端機中執行 gcloud CLI 指令,設定專案 ID。請先將範例專案 ID 改為對應的值,再執行指令。執行下列指令前,請先「取代」專案 ID。
gcloud config set project qwiklabs-gcp-project-id

部署到 Kubernetes

  1. 在 Cloud Shell 編輯器底部的窗格中,選取 Cloud Code 

d99a88992e15fea9.png

  1. 在頂端顯示的面板中,選取「Run on Kubernetes」(在 Kubernetes 中執行)。如果系統顯示提示,請選取「Yes」使用目前的 Kubernetes 環境。

bfd65e9df6d4a6cb.png

這個指令會啟動原始碼的建構作業,然後執行測試。建構和測試程序需要幾分鐘才能完成。這些測試包括單元測試,以及檢查部署環境所設規則的驗證步驟。這個驗證步驟已設定完成,可確保您在開發環境中作業時,也能收到部署問題的警告。

  1. 首次執行指令時,畫面頂端會顯示提示,詢問您是否要使用目前的 Kubernetes 內容,請選取「Yes」接受並使用目前的內容。
  2. 接著系統會顯示提示,詢問要使用哪個容器登錄服務。按下 Enter 鍵接受預設值
  3. 選取下方窗格中的「輸出」分頁,即可查看進度和通知。使用下拉式選單選取「Kubernetes: Run/Debug」

9c87ccbf5d06f50a.png

  1. 在右側的管道下拉式選單中選取「Kubernetes: Run/Debug - Detailed」,即可查看其他詳細資料和容器的即時記錄檔串流

804abc8833ffd571.png

建構和測試完成後,「Kubernetes: Run/Debug」檢視畫面會顯示 http://localhost: 8080 網址,並記錄在「Output」分頁中。

  1. 在 Cloud Code 終端機中,將游標懸停在輸出內容中的第一個網址 (http://localhost:8080) 上,然後在顯示的工具提示中選取「開啟網頁預覽」。
  2. 系統會開啟新的瀏覽器分頁,並顯示「Hello, World!」訊息

熱重載

  1. 開啟 app.py 檔案。
  2. 將問候語訊息變更為「Hello from Python

請注意,在 Output 視窗的 Kubernetes: Run/Debug 檢視畫面中,監控程式會將更新後的檔案與 Kubernetes 中的容器同步

Update initiated
Build started for artifact python-app
Build completed for artifact python-app

Deploy started
Deploy completed

Status check started
Resource pod/python-app-6f646ffcbb-tn7qd status updated to In Progress
Resource deployment/python-app status updated to In Progress
Resource deployment/python-app status completed successfully
Status check succeeded
...
  1. 切換至 Kubernetes: Run/Debug - Detailed 檢視畫面後,您會發現系統會辨識檔案變更,然後建構並重新部署應用程式
files modified: [app.py]
Syncing 1 files for gcr.io/veer-pylab-01/python-app:3c04f58-dirty@sha256:a42ca7250851c2f2570ff05209f108c5491d13d2b453bb9608c7b4af511109bd
Copying files:map[app.py:[/app/app.py]]togcr.io/veer-pylab-01/python-app:3c04f58-dirty@sha256:a42ca7250851c2f2570ff05209f108c5491d13d2b453bb9608c7b4af511109bd
Watching for changes...
[python-app] * Detected change in '/app/app.py', reloading
[python-app] * Restarting with stat
[python-app] * Debugger is active!
[python-app] * Debugger PIN: 744-729-662
  1. 重新整理先前顯示結果的瀏覽器分頁,即可查看更新後的結果。

偵錯

  1. 前往「Debug」檢視畫面,然後停止目前的執行緒 647213126d7a4c7b.png。如果系統詢問,您可以選擇在每次執行後清理。
  2. 70d6bd947d04d1e6.png
  3. 按一下底部選單中的 Cloud Code,然後選取 Debug on Kubernetes,即可在 debug 模式下執行應用程式。
  • Output 視窗的 Kubernetes Run/Debug - Detailed 檢視畫面中,請注意 skaffold 會以偵錯模式部署這個應用程式。
  1. 程序完成時。您會發現附加的偵錯工具,以及顯示「Attached debugger to container "python-app-8476f4bbc-h6dsl" successfully.」的「輸出」分頁,並列出網址 http://localhost:8080。
Port forwarding pod/python-app-8bd64cf8b-cskfl in namespace default, remote port 5678 -> http://127.0.0.1:5678
  1. 底部的狀態列會從藍色變成橘色,表示目前處於偵錯模式。
  2. Kubernetes Run/Debug 檢視畫面中,請注意已啟動可偵錯的容器
**************URLs*****************
Forwarded URL from service python-app: http://localhost:8080
Debuggable container started pod/python-app-8bd64cf8b-cskfl:python-app (default)
Update succeeded
***********************************

運用中斷點

  1. 開啟 app.py 檔案。
  2. 找出顯示 return message 的陳述式
  3. 點選行號左側的空白處,在該行新增中斷點。系統會顯示紅色指標,表示已設定中斷點
  4. 首次執行時,系統會提示您容器內的來源位置。這個值與 Dockerfile 中的目錄有關。

按下 Enter 鍵接受預設值

fccc866f32b5ed86.png

應用程式會在幾分鐘內建構及部署完畢。

  1. 重新載入瀏覽器,請注意偵錯工具會在該中斷點停止程序,並允許您調查在 GKE 中遠端執行的應用程式變數和狀態
  2. 按一下「變數」部分
  3. 按一下「Locals」,即可找到 "message" 變數。
  4. 按兩下變數名稱「message」,然後在彈出式視窗中將值變更為其他內容,例如 "Greetings from Python"
  5. 按一下偵錯控制面板中的「繼續」按鈕 607c33934f8d6b39.png
  6. 在瀏覽器中查看回應,現在應該會顯示您剛輸入的更新值。
  7. 按下停止按鈕 647213126d7a4c7b.png 即可停止「偵錯」模式,再次點選中斷點即可移除。

5. 開發簡易 CRUD REST 服務

此時,您的應用程式已完全設定為容器化開發,且您已透過 Cloud Code 逐步瞭解基本開發工作流程。在接下來的章節中,您將新增 REST 服務端點,連線至 Google Cloud 中的代管資料庫,藉此練習所學內容。

編寫 REST 服務的程式碼

下方程式碼會建立簡單的 REST 服務,並使用 Spanner 做為應用程式的資料庫。將下列程式碼複製到應用程式中,即可建立應用程式。

  1. app.py 替換為下列內容,建立主要應用程式
import os
from flask import Flask, request, jsonify
from google.cloud import spanner


app = Flask(__name__)


instance_id = "music-catalog"

database_id = "musicians"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)


@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

@app.route('/singer', methods=['POST'])
def create():
    try:
        request_json = request.get_json()
        singer_id = request_json['singer_id']
        first_name = request_json['first_name']
        last_name = request_json['last_name']
        def insert_singers(transaction):
            row_ct = transaction.execute_update(
                f"INSERT Singers (SingerId, FirstName, LastName) VALUES" \
                f"({singer_id}, '{first_name}', '{last_name}')"
            )
            print("{} record(s) inserted.".format(row_ct))

        database.run_in_transaction(insert_singers)

        return {"Success": True}, 200
    except Exception as e:
        return e



@app.route('/singer', methods=['GET'])
def get_singer():

    try:
        singer_id = request.args.get('singer_id')
        def get_singer():
            first_name = ''
            last_name = ''
            with database.snapshot() as snapshot:
                results = snapshot.execute_sql(
                    f"SELECT SingerId, FirstName, LastName FROM Singers " \
                    f"where SingerId = {singer_id}",
                    )
                for row in results:
                    first_name = row[1]
                    last_name = row[2]
                return (first_name,last_name )
        first_name, last_name = get_singer()  
        return {"first_name": first_name, "last_name": last_name }, 200
    except Exception as e:
        return e


@app.route('/singer', methods=['PUT'])
def update_singer_first_name():
    try:
        singer_id = request.args.get('singer_id')
        request_json = request.get_json()
        first_name = request_json['first_name']
        
        def update_singer(transaction):
            row_ct = transaction.execute_update(
                f"UPDATE Singers SET FirstName = '{first_name}' WHERE SingerId = {singer_id}"
            )

            print("{} record(s) updated.".format(row_ct))

        database.run_in_transaction(update_singer)
        return {"Success": True}, 200
    except Exception as e:
        return e


@app.route('/singer', methods=['DELETE'])
def delete_singer():
    try:
        singer_id = request.args.get('singer')
    
        def delete_singer(transaction):
            row_ct = transaction.execute_update(
                f"DELETE FROM Singers WHERE SingerId = {singer_id}"
            )
            print("{} record(s) deleted.".format(row_ct))

        database.run_in_transaction(delete_singer)
        return {"Success": True}, 200
    except Exception as e:
        return e

port = int(os.environ.get('PORT', 8080))
if __name__ == '__main__':
    app.run(threaded=True, host='0.0.0.0', port=port)

新增資料庫設定

如要安全地連線至 Spanner,請設定應用程式以使用 Workload Identity。這樣一來,應用程式就能以自己的服務帳戶身分運作,並在存取資料庫時擁有個別權限。

  1. 更新「deployment.yaml」。在檔案結尾新增下列程式碼 (請務必保留以下範例中的 Tab 縮排)
      serviceAccountName: python-ksa
      nodeSelector:
        iam.gke.io/gke-metadata-server-enabled: "true" 

變更後,規格部分應如下所示

   spec:
     containers:
     - name: python-app
       image: python-app
     serviceAccountName: python-ksa
     nodeSelector:
       iam.gke.io/gke-metadata-server-enabled: "true"

部署及驗證應用程式

  1. 在 Cloud Shell 編輯器底部的窗格中,選取 Cloud Code,然後選取畫面頂端的 Debug on Kubernetes
  2. 建構和測試完成後,「輸出」分頁會顯示 Resource deployment/python-app status completed successfully,並列出網址:「Forwarded URL from service python-app: http://localhost: 8080」(從服務 python-app 轉送的網址:http://localhost:8080)。
  3. 新增幾個項目。

在 Cloud Shell 終端機執行下列指令

curl -X POST http://localhost:8080/singer -H 'Content-Type: application/json' -d '{"first_name":"Cat","last_name":"Meow", "singer_id": 6}'
  1. 在終端機中執行下列指令,測試 GET
curl -X GET http://localhost:8080/singer?singer_id=6
  1. 測試刪除:現在請執行下列指令,嘗試刪除項目。視需要變更 item-id 的值。
curl -X DELETE http://localhost:8080/singer?singer_id=6
    This throws an error message
500 Internal Server Error

找出並修正問題

  1. 偵錯模式並找出問題。以下提供幾項訣竅:
  • 我們發現 DELETE 有問題,因為它未傳回預期結果。因此您會在 delete_singer 方法的 app.py 中設定中斷點。
  • 逐步執行並觀察每個步驟的變數,即可在左側視窗中查看本機變數的值。
  • 如要觀察特定值 (例如 singer_idrequest.args),請將這些變數新增至「監看」視窗。
  1. 請注意,指派給 singer_id 的值是 None。變更程式碼以修正問題。

修正後的程式碼片段如下所示。

@app.route('/delete-singer', methods=['DELETE', 'GET'])
def delete_singer():
    try:
        singer_id = request.args.get('singer_id')
  1. 重新啟動應用程式後,請再次嘗試刪除,確認問題是否解決。
  2. 按一下偵錯工具列中的紅色方塊 647213126d7a4c7b.png,停止偵錯工作階段。

6. 清除

恭喜!在本實驗室中,您從頭開始建立新的 Python 應用程式,並設定該應用程式,使其能有效與容器搭配運作。然後,您按照傳統應用程式堆疊中的相同開發人員流程,將應用程式部署至遠端 GKE 叢集並進行偵錯。

完成實驗室後,請執行下列清理作業:

  1. 刪除實驗室中使用的檔案
cd ~ && rm -rf ~/music-service
  1. 刪除專案,移除所有相關基礎架構和資源