使用 Python 進行 InnerLoop 開發

1. 總覽

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

學習目標

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

  • 建立新的 Python 範例應用程式
  • 逐步完成開發程序
  • 開發簡易的 CRUD 休息服務

2. 設定和需求

自修環境設定

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

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 「專案名稱」是這項專案參與者的顯示名稱。這是 Google API 不使用的字元字串,您可以隨時更新。
  • 所有 Google Cloud 專案的專案 ID 均不得重複,且設定後即無法變更。Cloud 控制台會自動產生一個不重複的字串。但通常是在乎它何在在大部分的程式碼研究室中,您必須參照專案 ID (通常稱為 PROJECT_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)')

取得原始碼

  1. 本研究室的原始碼位於 GitHub 的 GoogleCloudPlatform 中的 container-developer-workshop。請使用下列指令複製資料夾,然後變更至目錄中。
git clone https://github.com/GoogleCloudPlatform/container-developer-workshop.git &&
cd container-developer-workshop/labs/python
mkdir music-service && cd music-service 
cloudshell workspace .

如果終端機「未」開啟,使用 `ctrl+` 的按鍵組合開啟新的終端機視窗

佈建本研究室中使用的基礎架構

在本研究室中,您會將程式碼部署至 GKE,並存取 Spanner 資料庫中儲存的資料。下列設定指令碼可協助您完成這個基礎架構的設定。佈建程序需要 10 分鐘以上。設定處理期間,您可以繼續進行後續步驟。

../setup.sh

3. 建立新的 Python 範例應用程式

  1. 建立名為 requirements.txt 的檔案,並將下列內容複製到檔案中
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,然後將應用程式部署至 GKE。這會在背景執行,將詳細資料從開發人員流程中抽離出來。

部署到 Kubernetes

  1. 在 Cloud Shell 編輯器底部的窗格中,選取「Cloud Code」 圖示 。

fdc797a769040839.png

  1. 在頂端的面板中,選取「Run on Kubernetes」。如果出現提示,請選取「是」以使用目前的 Kubernetes 結構定義。

cfce0d11ef307087.png

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

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

f95b620569ba96c5.png

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

94acdcdda6d2108.png

建構和測試完成後,「Output」(輸出) 分頁標籤會顯示 Attached debugger to container "python-app-8476f4bbc-h6dsl" successfully.,以及網址 http://localhost:8080。

  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. 按一下底部選單中的 Cloud Code 並選取 Debug on Kubernetes,即可透過 debug 模式執行應用程式。
  • 請注意,在 Output 視窗的 Kubernetes Run/Debug - Detailed 檢視畫面中,Skaffold 會在偵錯模式部署這個應用程式。
  1. 這是第一次執行提示時,系統會詢問來源在容器內的位置。這個值與 Dockerfile 中的目錄相關。

按下 Enter 鍵即可接受預設值

583436647752e410.png

建構及部署應用程式需要幾分鐘的時間。

  1. 程序完成後。畫面上會顯示偵錯工具。
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. 重新載入瀏覽器並留意偵錯工具,會在中斷點停止程序,然後針對在 GKE 中從遠端執行的應用程式調查變數和狀態
  5. 按一下「變數」部分。
  6. 按一下「Locals」,即可找到 "message" 變數。
  7. 按兩下變數名稱「訊息」在彈出式視窗中,將值變更為其他值,例如 "Greetings from Python"
  8. 按一下偵錯控制台中的「繼續」按鈕 607c33934f8d6b39.png
  9. 請在瀏覽器中查看回應,以便顯示剛才輸入的更新值。
  10. 停止「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" 

部署及驗證應用程式

  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 container-developer-workshop
  1. 刪除專案,移除所有相關的基礎架構和資源