云端部署

Google Cloud:把一个二维码生成服务部署到 Cloud Run

把一个二维码生成服务部署到 Cloud Run:GET /qr?url=https://... 返回 PNG 图片,带输入校验和缓存头,容器按请求自动扩缩,没有流量时缩到 0。

BINBHO · 图文教程

把一个二维码生成服务部署到 Cloud Run:GET /qr?url=https://... 返回 PNG 图片,带输入校验和缓存头,容器按请求自动扩缩,没有流量时缩到 0。

先看清成品

本机执行结果:3 个 pytest 测试通过;用 Docker 构建镜像并在 PORT=8080 下运行,/healthz 返回 {"ok":true},/qr 返回 200 image/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 的容器,基本都能直接部署。先在本地把镜像跑通,上线就只剩一条命令。