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

1. 總覽

本研究室介紹了相關功能和功能,方便軟體工程師在容器化環境中開發 Python 應用程式,簡化開發工作流程。一般的容器開發作業會要求使用者瞭解容器和容器建構程序的詳細資料。此外,開發人員通常必須中斷流程,從 IDE 中移出 IDE,才能在遠端環境中對應用程式進行測試及偵錯。有了本教學課程中提及的工具和技術,開發人員不必離開 IDE,就能有效地使用容器化應用程式。

學習目標

在本研究室中,您將瞭解在 GCP 中使用容器進行開發的方法,包括:

  • 建立新的 Python 範例應用程式
  • 逐步完成開發程序
  • 開發簡易的 CRUD 休息服務
  • 部署至 GKE
  • 偵錯錯誤狀態
  • 使用中斷點 / 記錄
  • 熱部署變更回 GKE

58a4cdd3ed7a123a.png

2. 設定和需求

自修環境設定

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

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 「專案名稱」是這項專案參與者的顯示名稱。這是 Google API 未使用的字元字串。您可以隨時更新。
  • 所有 Google Cloud 專案的專案 ID 均不得重複,而且設定後即無法變更。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 工作站做為 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 cli 指令設定專案 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. 在「設定」部分下方確認結果。系統需要 2 分鐘的時間才能轉換為「就緒」狀態。

2e23c2e9983d1ccf.png

  1. 開啟控制台中的 Cloud Workstations 並建立新的執行個體。

a53adeeac81a78c8.png

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

f052cd47701ec774.png

  1. 在「Workstations」區段下方驗證結果。

啟動工作站

  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
  • 同步處理設定,將 *.py 檔案的任何變更從 IDE 載入執行中的容器

編輯之後,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」圖示,然後選取「登入 Google Cloud」:

1769afd39be372ff.png

  1. 按一下「繼續登入」。

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」在初始設定中建立的 ID

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 結構定義。

bfd65e9df6d4a6cb.png

這個指令會啟動原始碼建構作業,然後執行測試。建構與測試會在幾分鐘內執行。這些測試包括單元測試和驗證步驟,可檢查針對部署環境設定的規則。這個驗證步驟已經過設定,可確保您即使仍在開發環境中工作,也會收到部署問題的警告。

  1. 首次執行指令時,畫面頂端會顯示提示,詢問您是否要使用目前的 Kubernetes 環境,請選取「是」。可接受並使用目前的內容。
  2. 接著系統會顯示提示,詢問要使用哪個 Container Registry。按下 Enter 鍵即可接受提供的預設值
  3. 選取「Output」(輸出)位於下方窗格中的分頁,即可查看進度和通知。選取「Kubernetes:執行/偵錯」

9c87ccbf5d06f50a.png

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

804abc8833ffd571.png

建構與測試完成後,「輸出」分頁記錄中的網址 http://localhost:8080 就會列在「Kubernetes: Run/Debug」中。檢視畫面。

  1. 在 Cloud Code 終端機中,將滑鼠遊標懸停在輸出的第一個網址 (http://localhost:8080) 上,然後在顯示的工具提示中選取「Open Web Preview」。
  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. 程序完成後。您會發現偵錯工具已附加在「Output」(輸出) 分頁中,並顯示 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. 按兩下變數名稱「訊息」在彈出式視窗中,將值變更為其他值,例如 "Greetings from Python"
  5. 按一下偵錯控制台中的「繼續」按鈕 607c33934f8d6b39.png
  6. 請在瀏覽器中查看回應,以便顯示剛才輸入的更新值。
  7. 停止「Debug」模式,方法是按下停止按鈕 647213126d7a4c7b.png,然後再次按一下中斷點來移除中斷點。

5. 開發簡易 CRUD 靜息服務

此時您的應用程式已全面完成容器化開發作業,您也完成了 Cloud Code 的基本開發工作流程。在接下來的各節中,您將學到以下內容,新增連結至 Google Cloud 代管資料庫的其餘服務端點。

為其餘服務編寫程式碼

以下程式碼會建立簡易的靜態服務,以 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,請將應用程式設為使用工作負載身分。這可讓應用程式在存取資料庫時,以本身的服務帳戶的形式運作,並擁有個別的權限。

  1. 更新「deployment.yaml」。在檔案結尾加入下列程式碼 (請確保下例中定位點縮排)
      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」
  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. 刪除專案,移除所有相關的基礎架構和資源