使用 Cloud Workstations 和 Python 进行 InnerLoop 开发

1. 概览

本实验演示了一些特性和功能,这些特性和功能旨在简化在容器化环境中开发 Python 应用的软件工程师的开发工作流。典型的容器开发要求用户了解容器和容器构建流程的详细信息。此外,开发者通常需要中断他们的流程,离开 IDE,以在远程环境中测试和调试其应用。借助本教程中提到的工具和技术,开发者无需离开 IDE 即可高效使用容器化应用。

学习内容

在本实验中,您将学习在 GCP 中使用容器进行开发的方法,包括:

  • 创建新的 Python 起始应用
  • 浏览开发过程
  • 开发简单的 CRUD 静态服务
  • 部署到 GKE
  • 调试错误状态
  • 利用断点 / 日志
  • 将更改热部署回 GKE

58a4cdd3ed7a123a

2. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时对其进行更新。
  • 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常您不在乎这是什么在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为 PROJECT_ID)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且该 ID 在项目期间会一直保留。
  • 此外,还有第三个值,即某些 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。如需关停资源,以免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

启动 Cloudshell 编辑器

本实验旨在与 Google Cloud Shell Editor 搭配使用,并经过测试。要访问该编辑器,请按以下步骤操作:

  1. 通过 https://console.cloud.google.com 访问您的 Google 项目。
  2. 点击右上角的 Cloud Shell 编辑器图标

8560cc8d45e8c112

  1. 窗口底部会打开一个新窗格
  2. 点击“打开编辑器”按钮

9e504cb98a6a8005

  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

创建工作站配置

  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. 验证“Configurations”(配置)部分下的结果。转变为“就绪”状态需要 2 分钟。

2e23c2e9983d1ccf.png

  1. 在控制台中打开 Cloud Workstations 并创建新实例。

a53adeeac81a78c8.png

  1. 将名称更改为“my-workstation”,然后选择现有配置:codeoss-python

f052cd47701ec774.png

  1. 验证“Workstations”部分下的结果。

启动工作站

  1. 启动和启动工作站。启动工作站需要几分钟的时间。

682f8a307032cba3

  1. 通过点击地址栏中的图标允许第三方 Cookie。1b8923e2943f9bc4

fcf9405b6957b7d7.png

  1. 点击“网站无法访问?”。

36a84c0e2e3b85b

  1. 点击“允许 Cookie”。

2259694328628fba

  1. 工作站启动后,您会看到 Code OSS IDE 启动。点击“标为已完成”在“使用入门”页面一是工作站 IDE

94874fba9b74cc22

3. 创建新的 Python 起始应用

在本部分中,您将创建一个新的 Python 应用。

  1. 打开一个新的终端。

c31d48f2e4938c38.png

  1. 创建一个新目录并将其作为工作区打开
mkdir music-service && cd music-service

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

如果您看到这条消息,请点击“允许”按钮,以便将其复制并粘贴到工作站中。

58149777e5cc350a

  1. 创建名为 requirements.txt 的文件,并将以下内容复制到其中

789e8389170bd900

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 部分将如下所示:

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”:

1769afd39be372ff.png

  1. 点击“继续登录”。

923bb1c8f63160f9

  1. 在终端中检查输出并打开链接:

517fdd579c34aa21

  1. 使用您的 Qwiklabs 学生凭据登录。

db99b345f7a8e72c.png

  1. 选择“允许”:

a5376553c430ac84.png

  1. 复制验证码并返回“工作站”标签页。

6719421277b92eac

  1. 粘贴验证码,然后按 Enter 键。

e9847cfe3fa8a2ce.png

添加 Kubernetes 集群

  1. 添加集群

62a3b97bdbb427e5

  1. 选择 Google Kubernetes Engine:

9577de423568bbaa

  1. 选择项目。

c5202fcbeebcd41c.png

  1. 选择“python-cluster”创建的新实例

719c2fc0a7f9e84f

  1. 该集群现在会显示在 Cloud Code 下的 Kubernetes 集群列表中。从此处导航和探索集群。

7e5f50662d4eea3c

使用 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 Editor 底部的窗格中,选择 Cloud Code 

d99a88992e15fea9.png

  1. 在顶部显示的面板中,选择 Run on Kubernetes。如果出现提示,请选择“Yes”以使用当前的 Kubernetes 上下文。

bfd65e9df6d4a6cb.png

此命令会启动源代码的构建,然后运行测试。构建和测试将需要几分钟时间才能运行完毕。这些测试包括单元测试和验证步骤,用于检查为部署环境设置的规则。此验证步骤已配置,确保即使您仍在开发环境中工作,也能收到部署问题的警告。

  1. 首次运行此命令时,屏幕顶部会显示一条提示,询问您是否需要当前的 Kubernetes 上下文,请选择“是”接受并使用当前上下文。
  2. 接下来,系统会显示一条提示,询问要使用哪个容器注册表。按 Enter 键可接受提供的默认值
  3. 选择“输出”标签,以查看进度和通知。使用下拉菜单选择“Kubernetes:运行/调试”

9c87ccbf5d06f50a

  1. 选择“Kubernetes:运行/调试 - 详细”查看右侧渠道下拉菜单中的 其他详细信息和实时从容器流式传输的日志

804abc8833ffd571

构建和测试完成后,“Kubernetes: Run/Debug”页面中将列出“Output”标签页日志的网址 http://localhost: 8080视图。

  1. 在 Cloud Code 终端中,将鼠标悬停在输出结果中的第一个网址 (http://localhost:8080) 上,然后在显示的工具提示中选择“打开网页预览”。
  2. 系统会打开一个新的浏览器标签页,并显示以下消息:Hello, World!

热重载

  1. 打开 app.py 文件
  2. 将问候语消息更改为 Hello from Python

请注意,在 Output 窗口的 Kubernetes: Run/Debug 视图中,Watcher 将更新后的文件与 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. 转到“调试”视图并停止当前线程 647213126d7a4c7b。如果系统询问,您可以选择每次运行后清理。
  2. 70d6bd947d04d1e6
  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 视图中,请注意启动了一个 Debuggable 容器
**************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
  6. 在浏览器中查看响应,浏览器现在会显示您刚刚输入的更新值。
  7. 停止“调试”进入模式,然后再次点击停止按钮 647213126d7a4c7b 以移除该断点。

5. 开发简单的 CRUD 静态服务

至此,您的应用已完全针对容器化开发进行了配置,并且您已完成 Cloud Code 的基本开发工作流。在以下部分中,您将通过添加连接到 Google Cloud 中的代管式数据库的 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 Identities。这样,您的应用就可以充当自己的服务账号,并在访问数据库时拥有单独的权限。

  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. 构建和测试完成后,“Output”(输出)标签页会显示 Resource deployment/python-app status completed successfully,并列出了一个网址:“Forwarded 网址 from service python-app: http://localhost:8080”
  3. 添加几个条目。

从 cloudshell 终端运行以下命令

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),请将这些变量添加到 Watch 窗口。
  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 中的红色方块,停止调试会话

6. 清理

恭喜!在本实验中,您从头开始创建了一个新的 Python 应用,并将其配置为与容器高效运行。然后,您按照传统应用堆栈中的相同开发者流程,将应用部署并调试到远程 GKE 集群。

在完成实验后进行清理:

  1. 删除实验中使用的文件
cd ~ && rm -rf ~/music-service
  1. 删除项目以移除所有相关基础架构和资源