从"能跑"到"能上线":用华为云码道把 Flask 应用做成生产级容器与 CI/CD
从"能跑"到"能上线":用华为云码道把 Flask 应用做成生产级容器与 CI/CD
本地
python app.py能跑,不等于能上线。生产级容器化要解决的是另一组问题:镜像怎么又小又安全、探针怎么配才不误杀、滚动更新怎么零中断、发布失败怎么自动回滚、指标日志怎么采。我让华为云码道(CodeArts)的容器与部署配置专家,把一个 Flask 应用从零做成生产级 Docker + K8s + CI/CD 全套配置。本文复盘这套配置的每个关键决策。
一、示例应用:一个带探针端点的订单服务
先准备一个真实可运行的 Flask 应用 app.py,刻意提供 /health 探针端点(K8s 探针要用):
@app.route("/health")
def health():
return jsonify(status="ok", uptime=round(time.time() - START_TIME, 2))
@app.route("/api/orders")
def list_orders():
status = request.args.get("status")
items = ORDERS if not status else [o for o in ORDERS if o["status"] == status]
return jsonify(count=len(items), orders=items)
@app.route("/api/orders/<int:order_id>")
def get_order(order_id):
...
先真实跑通,确认它是个"活"的应用(Flask test_client 实测):
/health -> 200 {'status': 'ok', 'uptime': 0.01}
/api/orders -> 200 count=3
/api/orders?status=paid -> 200 count=1
/api/orders/1 -> 200
/api/orders/999 -> 404
应用没问题,接下来是真正的挑战:把它变成能在 K8s 上稳定跑的生产镜像。
二、Dockerfile:多阶段 + 非 root + 层缓存
直接 FROM python:3.12 + pip install 的"教科书 Dockerfile",会带来三个生产问题:镜像臃肿、以 root 运行、每次改代码都重装依赖。CodeArts 部署专家给的是生产版:
# 阶段 1:builder —— 只负责构建自包含 venv
FROM ${BASE_IMAGE} AS builder
WORKDIR /build
COPY requirements.txt ./
RUN python -m venv /opt/venv \
&& /opt/venv/bin/python -m pip install --no-cache-dir -r requirements.txt
# 阶段 2:runtime —— 最小运行时
FROM ${BASE_IMAGE} AS runtime
RUN groupadd --gid 10001 app \
&& useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin app
COPY --from=builder --chown=10001:10001 /opt/venv /opt/venv
WORKDIR /app
COPY --chown=10001:10001 app.py ./
USER 10001:10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD python -c "..."
STOPSIGNAL SIGTERM
CMD ["sh", "-c", "... exec gunicorn --bind 0.0.0.0:${PORT} ... ${APP_MODULE}"]
四个关键点:
- 多阶段构建:builder 里装依赖,runtime 只拷贝 venv,镜像不含 pip 缓存和构建残留。生产建议把基础镜像固定到 digest(
python@sha256:...)而非 tag,防供应链漂移。 - 非 root(UID 10001):与 K8s
securityContext.runAsUser严格一致,避免 volume 权限冲突。 - 层缓存友好:先
COPY requirements.txt再装依赖,最后才COPY app.py——改业务代码不会触发依赖重装层失效。 exec gunicorn作为 PID 1:exec让 gunicorn 直接接收SIGTERM,配合STOPSIGNAL SIGTERM才能优雅下线(后面详说)。
三、一个巧妙的补丁:给没有 /metrics 的应用补上指标端点
这里有个真实坑:requirements.txt 装了 prometheus-client,但 app.py 并没有暴露 /metrics。如果 K8s 注解写 prometheus.io/path: /metrics,抓取会 404。
专家的解法很干净——不改业务代码,在镜像里生成一个 wsgi.py 包装层:
from app import app
from prometheus_client import Counter, Histogram, generate_latest
@app.before_request
def _start_timer():
request.environ["_prom_start"] = time.perf_counter()
@app.after_request
def _record_metrics(response):
if request.path != "/metrics":
REQUEST_COUNT.labels(request.method, endpoint, str(response.status_code)).inc()
...
@app.route("/metrics")
def _metrics():
return Response(generate_latest(_registry()), mimetype=CONTENT_TYPE_LATEST)
然后 APP_MODULE=wsgi:app 启 gunicorn。业务代码零改动,指标端点补齐,多 worker 下用 PROMETHEUS_MULTIPROC_DIR 聚合。这种"用包装层解耦"的思路,比硬改业务代码干净得多。
四、K8s 部署:探针三件套与时间预算
Deployment 最容易配错的是探针。专家的配置是"三件套 + 明确的时间预算":
startupProbe: # 启动期接管,最长 60s(2s×30)
httpGet: { path: /health, port: http }
periodSeconds: 2
failureThreshold: 30
livenessProbe: # 只判僵死,阈值放宽(30s 才失败)
httpGet: { path: /health, port: http }
periodSeconds: 10
failureThreshold: 3
readinessProbe: # 决定是否进 Endpoints,比 liveness 敏感
httpGet: { path: /health, port: http }
periodSeconds: 5
failureThreshold: 2
三者的分工是:
- startupProbe:应用慢启动时,先由它接管失败判定,避免被 liveness 误杀;
- livenessProbe:只判断"进程是否僵死",阈值故意放宽到 30s,防止 GC/抖动触发无谓重启;
- readinessProbe:决定是否把流量切进来,最敏感,抖动时先摘流而不是重启。
优雅下线的预算同样关键:
terminationGracePeriodSeconds: 45 ≥ preStop(5s) + gunicorn graceful-timeout(30s) + 余量
preStop sleep 5 等 kube-proxy 摘除 Endpoint 后再退出,避免 502;gunicorn 有 30s 优雅超时处理在途请求。两个时间加起来必须落在 45s 宽限期内,否则会被 SIGKILL 截断正在处理的请求。
安全上下文也一步到位:
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true # 只读根文件系统
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
volumes:
- name: tmp
emptyDir: { sizeLimit: 64Mi } # 只读根下唯一的可写点
只读根文件系统下,gunicorn 的心跳文件和 Prometheus 多进程 mmap 需要一个可写目录,就挂一个 64Mi 的 emptyDir 到 /tmp。"最小权限"不是口号,是每一条 securityContext 落到位。
五、HPA:扩缩容要"快上慢下"
自动扩缩容最怕"抖动"。专家的 HPA 策略是"扩容快、缩容慢":
minReplicas: 3
maxReplicas: 12
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 突发流量立即扩
policies: [{ type: Percent, value: 100, periodSeconds: 30 }]
scaleDown:
stabilizationWindowSeconds: 300 # 流量回落等 5 分钟再缩
policies: [{ type: Percent, value: 20, periodSeconds: 60 }]
两个细节:minReplicas 与 replicas 保持都是 3,避免 apply 顺序导致互相覆盖;缩容用 300s 稳定窗口 + 每 60s 最多减 1 个或 20%,防止流量回落时副本反复抖动。
六、CI/CD:带扫描门禁和自动回滚的流水线
一份好的流水线不是"能 build 就行",而是有门禁、能回滚。专家给的 GitHub Actions 流水线四段式:
lint → test → build/scan → deploy
- lint:ruff 静态检查 + yamllint + hadolint(Dockerfile)+ kubeconform(K8s 清单 schema 校验);
- test:pytest + 覆盖率,测试报告归档;
- build/scan:buildx 多阶段构建、推送 SWR、Trivy 扫描 HIGH/CRITICAL 漏洞
exit-code: 1直接阻断发布; - deploy:kubectl apply → set image →
rollout status等待。
部署段最关键的是失败自动回滚:
- name: 等待滚动更新(失败自动回滚)
run: |
if ! kubectl -n "${NAMESPACE}" rollout status deployment/"${DEPLOYMENT}" --timeout=300s; then
kubectl -n "${NAMESPACE}" describe deployment/"${DEPLOYMENT}" || true
kubectl -n "${NAMESPACE}" get pods -o wide || true
kubectl -n "${NAMESPACE}" rollout undo deployment/"${DEPLOYMENT}"
kubectl -n "${NAMESPACE}" rollout status deployment/"${DEPLOYMENT}" --timeout=300s
exit 1
fi
滚动更新超时 → 自动导出 describe/pods → rollout undo 回滚 → 再等回滚完成 → 标记失败。发布不是"按下就跑",是"失败能退回来"。
镜像标签同时保留 sha-xxxxxxx(可追溯)和语义化版本(可回滚定位),environment: production 可挂人工审批。
七、可观测性:指标、日志、告警三件套
配置里可观测性分三层:
- 指标:Deployment 打 Prometheus 注解(
scrape: "true"、port: "8000"、path: /metrics),配合前面 wsgi 包装层的/metrics端点,Prometheus 自动抓取;若用 prometheus-operator,则删掉注解改用 ServiceMonitor。 - 日志:应用用结构化日志(
%(asctime)s %(levelname)s %(message)s),gunicorn--access-logfile -输出到 stdout,由容器日志采集器统一收集。 - 告警:关键规则如 Pod 反复重启(
restart_total突增)、5xx 比例超阈值、就绪副本不足(deployment unavailable replicas)触发告警。
指标看趋势、日志查原因、告警做兜底,三层缺一不可。
八、配置校验:不只写,还要验
配置写完不算完。我对 5 个 YAML 做了真实语法校验:
OK .github/workflows/ci-cd.yml -> workflow:lint,test,build,deploy
OK k8s/deployment.yaml -> Deployment
OK k8s/hpa.yaml -> HorizontalPodAutoscaler
OK k8s/namespace.yaml -> Namespace
OK k8s/service.yaml -> Service
--- YAML 校验: 5/5 通过 ---
中间还踩了一个真实坑:CI 里 yamllint -d "{extends: relaxed, ...}" 这行内联的 {...} 被 YAML 误当成流式映射解析,报 mapping values are not allowed here。改成块标量(run: |)后通过。配置文件的语法错误在 CI 里才暴露,代价很高,本地先校验能省一轮。
九、避坑清单
这套配置沉淀出几条经验:
- 探针要三件套,别只配 liveness——否则慢启动被误杀、抖动时重启而非摘流。
- 优雅下线要算时间预算:
terminationGracePeriod ≥ preStop + graceful-timeout,否则在途请求被截断。 - 只读根文件系统要留可写点:
/tmp挂 emptyDir,否则 gunicorn 心跳和 Prometheus mmap 写不进去。 - 缺
/metrics用包装层补,别硬改业务代码。 - HPA 要"快上慢下",且
minReplicas与replicas对齐。 - CI 必须有扫描门禁和自动回滚,否则"发布"只是"碰运气"。
- 配置本地先校验(yamllint/kubeconform),别等 CI 才发现语法错。
十、写在最后
容器化部署的门槛从来不是"会不会写 Dockerfile",而是那些让服务在线上稳定的细节:镜像安不安全、探针配没配对、更新会不会中断、发布失败能不能退、指标日志采不采得到。这些细节散落在无数篇博客和经验里,拼起来才是一套生产级配置。
这次让 CodeArts 容器与部署配置专家从零产出一整套 Dockerfile + K8s + CI/CD + 可观测性配置,最直接的价值是:这些"老工程师才知道的坑",被一次性组织成了可落地、可校验、带注释的配置。你拿到的不是一段示例,是一份能改造就上线的工程骨架。
从"能跑"到"能上线"的那道鸿沟,靠的就是这一份份被认真对待的配置。
本文基于 /workspace/container-demo 的真实配置撰写:应用端点实测通过(/health 200、/api/orders count=3、404 正常),Dockerfile/K8s/CI-CD 全套配置落盘,5 个 YAML 校验 5/5 通过。配置设计由华为云码道(CodeArts)容器与部署配置专家产出。
- 点赞
- 收藏
- 关注作者
评论(0)