关于此 Codelab
1. 概览
本实验演示了一些特性和功能,这些特性和功能旨在简化在容器化环境中开发 Python 应用的软件工程师的开发工作流。典型的容器开发要求用户了解容器和容器构建流程的详细信息。此外,开发者通常需要中断他们的流程,离开 IDE,以在远程环境中测试和调试其应用。借助本教程中提到的工具和技术,开发者无需离开 IDE 即可高效使用容器化应用。
学习内容
在本实验中,您将学习在 GCP 中使用容器进行开发的方法,包括:
- 创建新的 Python 起始应用
- 浏览开发过程
- 开发简单的 CRUD 静态服务
- 部署到 GKE
- 调试错误状态
- 利用断点 / 日志
- 将更改热部署回 GKE
2. 设置和要求
自定进度的环境设置
- 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个。
- 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时对其进行更新。
- 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常您不在乎这是什么在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为
PROJECT_ID
)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且该 ID 在项目期间会一直保留。 - 此外,还有第三个值,即某些 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档。
- 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。如需关停资源,以免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
启动 Cloudshell 编辑器
本实验旨在与 Google Cloud Shell Editor 搭配使用,并经过测试。要访问该编辑器,请按以下步骤操作:
- 通过 https://console.cloud.google.com 访问您的 Google 项目。
- 点击右上角的 Cloud Shell 编辑器图标
- 窗口底部会打开一个新窗格
- 点击“打开编辑器”按钮
- 编辑器将打开,右侧为探索器,中心区域为编辑器
- 屏幕底部还应提供一个终端窗格
- 如果终端未打开,请使用 `ctrl+` 的组合键打开新的终端窗口
环境设置
在 Cloud Shell 中,设置项目 ID 和项目编号。将它们保存为 PROJECT_ID
和 PROJECT_ID
变量。
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID \
--format='value(projectNumber)')
预配本实验中使用的基础架构
在本实验中,您会将代码部署到 GKE,并访问存储在 Spanner 数据库中的数据。您还将使用 Cloud 工作站作为 IDE。下面的设置脚本会为您准备此基础架构。
- 下载设置脚本并使其可执行。
wget https://raw.githubusercontent.com/GoogleCloudPlatform/container-developer-workshop/main/labs/python/setup_with_cw.sh
chmod +x setup_with_cw.sh
- 打开
setup_with_cw.sh
文件,然后修改当前设为“CHANGEME”的密码的值 - 运行设置脚本,以建立您将在本实验中使用的 GKE 集群和 Spanner 数据库
./setup_with_cw.sh &
Cloud Workstations 集群
- 在 Cloud 控制台中打开 Cloud Workstations。等待集群处于
READY
状态。
创建工作站配置
- 如果您的 Cloud Shell 会话已断开连接,请点击“重新连接”然后运行 gcloud cli 命令来设置项目 ID。运行命令之前,请将以下示例项目 ID 替换为您的 qwiklabs 项目 ID。
gcloud config set project qwiklabs-gcp-project-id
- 在终端中下载并运行以下脚本以创建 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
- 验证“Configurations”(配置)部分下的结果。转变为“就绪”状态需要 2 分钟。
- 在控制台中打开 Cloud Workstations 并创建新实例。
- 将名称更改为“
my-workstation
”,然后选择现有配置:codeoss-python
。
- 验证“Workstations”部分下的结果。
启动工作站
- 启动和启动工作站。启动工作站需要几分钟的时间。
- 通过点击地址栏中的图标允许第三方 Cookie。
- 点击“网站无法访问?”。
- 点击“允许 Cookie”。
- 工作站启动后,您会看到 Code OSS IDE 启动。点击“标为已完成”在“使用入门”页面一是工作站 IDE
3. 创建新的 Python 起始应用
在本部分中,您将创建一个新的 Python 应用。
- 打开一个新的终端。
- 创建一个新目录并将其作为工作区打开
mkdir music-service && cd music-service
code-oss-cloud-workstations -r --folder-uri="$PWD"
如果您看到这条消息,请点击“允许”按钮,以便将其复制并粘贴到工作站中。
- 创建名为
requirements.txt
的文件,并将以下内容复制到其中
Flask
gunicorn
google-cloud-spanner
ptvsd==4.3.2
- 创建名为
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')
- 创建名为
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。
- 使用以下命令初始化 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 配置文件
- 更改默认名称
- 打开
deployment.yaml
文件 - 选择当前设为“
dockerfile-image
”的映像名称 - 右键点击并选择“更改所有出现次数”
- 输入新名称
python-app
4. 开发过程介绍
添加业务逻辑后,您现在可以部署和测试应用了。以下部分将介绍如何使用 Cloud Code 插件。除此之外,此插件还可与 Skaffold 集成,以简化您的开发流程。当您按以下步骤部署到 GKE 时,Cloud Code 和 Skaffold 会自动构建容器映像,将其推送到 Container Registry,然后将 your
应用部署到 GKE。这是在后台将细节从开发者流程中提取出来的。
登录 Google Cloud
- 点击 Cloud Code 图标,然后选择“Sign in to Google Cloud”:
- 点击“继续登录”。
- 在终端中检查输出并打开链接:
- 使用您的 Qwiklabs 学生凭据登录。
- 选择“允许”:
- 复制验证码并返回“工作站”标签页。
- 粘贴验证码,然后按 Enter 键。
添加 Kubernetes 集群
- 添加集群
- 选择 Google Kubernetes Engine:
- 选择项目。
- 选择“python-cluster”创建的新实例
- 该集群现在会显示在 Cloud Code 下的 Kubernetes 集群列表中。从此处导航和探索集群。
使用 gcloud cli 设置当前项目 ID
- 从 Qwiklabs 页面复制此实验的项目 ID。
- 从终端运行 gcloud cli 命令,以设置项目 ID。运行命令之前,替换示例项目 ID。先替换项目 ID,然后再运行以下命令。
gcloud config set project qwiklabs-gcp-project-id
将容器部署到 Kubernetes
- 在 Cloud Shell Editor 底部的窗格中,选择 Cloud Code 
- 在顶部显示的面板中,选择 Run on Kubernetes。如果出现提示,请选择“Yes”以使用当前的 Kubernetes 上下文。
此命令会启动源代码的构建,然后运行测试。构建和测试将需要几分钟时间才能运行完毕。这些测试包括单元测试和验证步骤,用于检查为部署环境设置的规则。此验证步骤已配置,确保即使您仍在开发环境中工作,也能收到部署问题的警告。
- 首次运行此命令时,屏幕顶部会显示一条提示,询问您是否需要当前的 Kubernetes 上下文,请选择“是”接受并使用当前上下文。
- 接下来,系统会显示一条提示,询问要使用哪个容器注册表。按 Enter 键可接受提供的默认值
- 选择“输出”标签,以查看进度和通知。使用下拉菜单选择“Kubernetes:运行/调试”
- 选择“Kubernetes:运行/调试 - 详细”查看右侧渠道下拉菜单中的 其他详细信息和实时从容器流式传输的日志
构建和测试完成后,“Kubernetes: Run/Debug”页面中将列出“Output”标签页日志的网址 http://localhost: 8080视图。
- 在 Cloud Code 终端中,将鼠标悬停在输出结果中的第一个网址 (http://localhost:8080) 上,然后在显示的工具提示中选择“打开网页预览”。
- 系统会打开一个新的浏览器标签页,并显示以下消息:
Hello, World!
热重载
- 打开
app.py
文件 - 将问候语消息更改为
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 ...
- 如果切换到
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
- 刷新查看之前结果的浏览器标签页以查看更新后的结果。
调试
- 转到“调试”视图并停止当前线程
。如果系统询问,您可以选择每次运行后清理。
- 点击底部菜单中的
Cloud Code
,然后选择Debug on Kubernetes
以在debug
模式下运行应用。
- 在
Output
窗口的Kubernetes Run/Debug - Detailed
视图中,请注意 Skaffold 将在调试模式下部署此应用。
- 该过程完成时。您会发现连接了调试程序,“输出”标签页显示
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
- 底部状态栏的颜色从蓝色变为橙色,表示它处于调试模式。
- 在
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 ***********************************
利用断点
- 打开
app.py
文件 - 找到显示
return message
的语句 - 点击行号左侧的空白处,为该行添加断点。系统会显示一个红色指示器,指明断点已设置
- 首次执行此操作时,系统会显示一条提示,询问数据源在容器中的什么位置。此值与 Dockerfile 中的目录相关。
按 Enter 键接受默认值
构建和部署应用需要几分钟的时间。
- 重新加载浏览器,并注意调试程序会在断点停止进程,并允许您调查在 GKE 中远程运行的应用变量和状态
- 点击向下进入“变量”部分
- 点击“Locals”,即可找到
"message"
变量。 - 双击变量名称“message”在弹出式窗口中,将值更改为其他值,例如
"Greetings from Python"
- 点击调试控制台中的“继续”按钮
- 在浏览器中查看响应,浏览器现在会显示您刚刚输入的更新值。
- 停止“调试”进入模式,然后再次点击停止按钮
以移除该断点。
5. 开发简单的 CRUD 静态服务
至此,您的应用已完全针对容器化开发进行了配置,并且您已完成 Cloud Code 的基本开发工作流。在以下部分中,您将通过添加连接到 Google Cloud 中的代管式数据库的 REST 服务端点,练习所学知识。
对其余服务进行编码
以下代码创建了一个简单的静态服务,该服务使用 Spanner 作为为应用提供支持的数据库。如需创建应用,请将以下代码复制到您的应用中。
- 通过将
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。这样,您的应用就可以充当自己的服务账号,并在访问数据库时拥有单独的权限。
- 更新
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"
部署并验证应用
- 在 Cloud Shell 编辑器底部的窗格中,选择
Cloud Code
,然后选择屏幕顶部的Debug on Kubernetes
。 - 构建和测试完成后,“Output”(输出)标签页会显示
Resource deployment/python-app status completed successfully
,并列出了一个网址:“Forwarded 网址 from service python-app: http://localhost:8080” - 添加几个条目。
从 cloudshell 终端运行以下命令
curl -X POST http://localhost:8080/singer -H 'Content-Type: application/json' -d '{"first_name":"Cat","last_name":"Meow", "singer_id": 6}'
- 在终端中运行以下命令来测试 GET
curl -X GET http://localhost:8080/singer?singer_id=6
- 测试删除:现在,尝试运行以下命令来删除条目。根据需要更改 item-id 的值。
curl -X DELETE http://localhost:8080/singer?singer_id=6
This throws an error message
500 Internal Server Error
找出并解决问题
- 调试模式并找出问题。请参考以下提示:
- 我们知道 DELETE 出错了,因为它没有返回所需的结果。因此,您需要在
delete_singer
方法的app.py
中设置断点。 - 逐步执行执行,并观察每一步的变量,观察左侧窗口中局部变量的值。
- 如需观察特定值(如
singer_id
和request.args
),请将这些变量添加到 Watch 窗口。
- 请注意,分配给
singer_id
的值为None
。更改代码以解决问题。
修复后的代码段将如下所示。
@app.route('/delete-singer', methods=['DELETE', 'GET']) def delete_singer(): try: singer_id = request.args.get('singer_id')
- 重启应用后,通过尝试删除应用再次测试。
- 点击调试工具栏
中的红色方块,停止调试会话
6. 清理
恭喜!在本实验中,您从头开始创建了一个新的 Python 应用,并将其配置为与容器高效运行。然后,您按照传统应用堆栈中的相同开发者流程,将应用部署并调试到远程 GKE 集群。
在完成实验后进行清理:
- 删除实验中使用的文件
cd ~ && rm -rf ~/music-service
- 删除项目以移除所有相关基础架构和资源