把一个二维码生成服务部署到 Cloud Run:
GET /qr?url=https://...返回 PNG 图片,带输入校验和缓存头,容器按请求自动扩缩,没有流量时缩到 0。
先看清成品
本机执行结果:3 个 pytest 测试通过;用 Docker 构建镜像并在
PORT=8080下运行,/healthz返回{"ok":true},/qr返回 200image/png,非法地址返回 400 中文错误。没有执行gcloud run deploy,线上地址、冷启动时间以你的项目为准。
注意:Google Cloud 按用量计费,价格以官方价格页和账单为准,本文不列金额。Google Cloud 不在中国大陆提供服务,请在服务可用的地区按条款注册使用,不要用代理或借用他人账号绕过地区限制,也不要共享账号。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
| Flask 服务 | /qr、/healthz 两个接口 |
| 容器镜像 | python:3.13-slim + gunicorn |
| Cloud Run 服务 | https://xxx.run.app |
| 测试 | 正常、非法协议、非法尺寸 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Google Cloud 项目 | 已关联结算账号 | 免费额度以官方 Free 页面为准 |
| gcloud CLI | 已 gcloud init 登录 |
按官方安装文档 |
| Docker | 可选 | 本地验证镜像 |
| Python | 3.13 | 本地测试 |
文件结构
21-google-cloud-A-src/
├── main.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
└── tests/test_main.py
第 1 步:服务代码
main.py:
import io, os
from urllib.parse import urlparse
from flask import Flask, request, send_file, jsonify
import qrcode
app = Flask(__name__)
app.json.ensure_ascii = False # 错误信息直接显示中文
MAX_LEN = 500
@app.get("/healthz")
def healthz():
return {"ok": True}
@app.get("/qr")
def qr():
url = request.args.get("url", "").strip()
p = urlparse(url)
if p.scheme not in ("http", "https") or not p.netloc or len(url) > MAX_LEN:
return jsonify(error=f"url 必须是 http(s) 地址,且不超过 {MAX_LEN} 字符"), 400
size = request.args.get("box", "8")
if not size.isdigit() or not 2 <= int(size) <= 20:
return jsonify(error="box 应为 2-20 的整数"), 400
img = qrcode.make(url, box_size=int(size), border=2)
buf = io.BytesIO(); img.save(buf, format="PNG"); buf.seek(0)
resp = send_file(buf, mimetype="image/png")
resp.headers["Cache-Control"] = "public, max-age=86400"
return resp
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
只接受 http/https,防止有人用它生成 之类的恶意二维码。
requirements.txt:
flask==3.1.3
gunicorn==26.2.0
qrcode[pil]==8.2
第 2 步:本地测试
tests/test_main.py:
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from main import app
c = app.test_client()
def test_ok():
r = c.get("/qr?url=https://example.com"); assert r.status_code == 200 and r.data[:8] == b"\x89PNG\r\n\x1a\n"
def test_bad_scheme():
r = c.get("/qr?url=alert(1)"); assert r.status_code == 400 and "必须" in r.get_data(as_text=True)
def test_bad_box(): assert c.get("/qr?url=https://a.com&box=99").status_code == 400
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt pytest
python -m pytest -q tests
检查结果:3 passed。
第 3 步:容器
Dockerfile:
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
# Cloud Run 通过 PORT 环境变量告诉容器监听端口(默认 8080)
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
docker build -t qr-svc .
docker run --rm -p 8080:8080 -e PORT=8080 qr-svc
curl -s localhost:8080/healthz
curl -s -o qr.png -w '%{http_code} %{content_type}\n' "localhost:8080/qr?url=https://example.com"
检查结果(本机实际输出):
{"ok":true}
200 image/png
日志里能看到 Starting gunicorn 26.2.0、Listening at: http://0.0.0.0:8080。
第 4 步:部署到 Cloud Run(本文未执行)
gcloud config set project 你的项目ID
gcloud run deploy qr-svc --source . --region asia-east1 --allow-unauthenticated
--source . 会用 Cloud Build 按 Dockerfile 构建镜像并推送到 Artifact Registry,首次运行会提示启用相关 API。完成后终端输出 Service URL。
URL=$(gcloud run services describe qr-svc --region asia-east1 --format 'value(status.url)')
curl -s -o qr.png -w '%{http_code}\n' "$URL/qr?url=https://example.com"
调试示例
第一版错误响应是这样的(本机实际输出):
{"error":"url \u5fc5\u987b\u662f http(s) \u5730\u5740..."}
Flask 默认把非 ASCII 转义。加一行 app.json.ensure_ascii = False 后返回:
{"error":"url 必须是 http(s) 地址,且不超过 500 字符"}
出错了怎么办?
| 现象 | 原因 | 解决 |
|---|---|---|
Container failed to start and listen on the port |
程序没监听 $PORT |
CMD 里用 --bind :$PORT |
| 部署时提示启用 API | 新项目没开 Cloud Build / Artifact Registry / Run | 按提示输入 y |
| 访问返回 403 | 没有允许未认证访问 | 加 --allow-unauthenticated,或用身份验证调用 |
| 第一次请求慢 | 缩到 0 后冷启动 | 可设 --min-instances 1(会产生常驻费用) |
| 本地中文错误是 \u 转义 | Flask 默认 ensure_ascii | 见调试示例 |
清理
gcloud run services delete qr-svc --region asia-east1
Artifact Registry 里的镜像也会占存储,不用时一起删除。
模板规则
- 容器只监听
$PORT,日志写标准输出。 - 外部输入全部校验,并有测试。
- 公开服务考虑滥用,必要时加 Cloud Armor 或身份验证。
项目资源
| 用途 | 地址 |
|---|---|
| Cloud Run 部署 Python 服务快速入门 | https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-python-service |
| Cloud Run 容器运行约定 | https://cloud.google.com/run/docs/container-contract |
| Cloud Run 价格 | https://cloud.google.com/run/pricing |
| 安装 gcloud CLI | https://cloud.google.com/sdk/docs/install |
| Google Cloud 免费计划 | https://cloud.google.com/free |
| python-qrcode | https://github.com/lincolnloop/python-qrcode |
Cloud Run 的门槛很低:能在本地 docker run 起来、监听 $PORT 的容器,基本都能直接部署。先在本地把镜像跑通,上线就只剩一条命令。