ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python项目CI/CD流水线搭建实战:从依赖锁定到自动化部署

Python项目CI/CD流水线搭建实战:从依赖锁定到自动化部署 1. 为什么Python项目的CI/CD必须自己动手搭一套先说个很直观的场景。你写了一个Python服务本地跑得好好的测试全绿于是你推上仓库等CI跑完结果挂了。挂了的原因不是代码逻辑而是CI环境里没有你的依赖、Python版本不对、某个系统包缺失或者更常见的——pip install -r requirements.txt在隔离环境里装出来的依赖和你本地完全不一样某一个传递依赖升级了小版本行为变了测试就红了。这不是个例。Python项目做CI/CD第一道坎往往不是工具的复杂度而是Python生态本身的碎片化。解释器版本多3.8到3.13同时存在、依赖解析方式分散pip、poetry、pipenv、uv各有人用、虚拟环境方案各有一套venv、virtualenv、conda、poetry shell——这些叠加在一起直接导致一个结论每一个Python项目要想稳定地跑CI/CD都必须把构造一个可复现的运行环境当成流水线的第一步来认真设计而不是随便找一个模板改改就完事。这篇文章我想讲的不是什么是CI/CD这种概念科普——这年头百度一搜一大堆。我想分享的是针对一个真实的Python项目从零到一搭一套能用的、稳定的、有质量门禁的CI/CD流水线具体怎么做每一段配置背后的理由是什么以及哪些地方是Python项目特有的大坑。无论你是用GitLab CI、GitHub Actions还是Jenkins核心逻辑都是通用的我会尽量把配置写出来。适合看这篇文章的人Python后端开发、数据工程师、或者任何在维护Python服务的同学。如果你已经跑通了基础流水线但觉得它不够稳、不够快、或者部署环节还在手动那这篇对你更有价值。2. 搭流水线之前先把Python项目的“环境可复现性”想清楚2.1 先回答一个问题你的依赖锁定了吗这是Python CI里最容易被忽略、却又最致命的一个问题。很多人直到某天CI莫名其妙挂掉、查了半天才发现是某个依赖的新版本发布了才意识到哦原来我从来没锁过版本。所谓依赖锁定指的是你不仅要记录直接依赖就是你在代码里import的那些还要记录它们的传递依赖这些包依赖的其它包的精确版本。标准做法是生成一份requirements.lock或poetry.lock之类的文件提交到Git仓库里让CI在安装时严格按锁文件的版本安装。这里有几种主流方案我按推荐程度排个序方案锁文件虚拟环境适用场景我的评价pip pip-toolsrequirements.txt requirements.lockvenv简单项目、对依赖树要求不高最通用够用poetrypoetry.lock自动创建应用型项目、发布到PyPI体验好但和某些CI缓存策略配合有点麻烦uvuv.lock自带追求速度和现代工具链新项目强烈推荐快得离谱pipenvPipfile.lock自动创建老项目迁移成本高现在不太推荐新项目用了我自己在实际项目里用得最多的是pip-tools原因很简单它不改变你的工作流程pip install的命令你还能继续用只是多了一个用编译工具生成锁文件的步骤。还有一个重要原因是它的pip-compile命令可以把直接依赖和传递依赖分开定义日常加包改包都特别直觉。# 在项目根目录 pip install pip-tools # 把直接依赖写在 requirements.in # 生成锁文件 pip-compile requirements.in --output-file requirements.txt注意这里有个细节requirements.txt不仅是锁文件它同时包含了直接依赖和传递依赖。你不需要再分requirements-prod.txt和requirements-dev.txt如果你用了pip-compile的两个输入文件requirements.in和requirements-dev.in生成两个输出文件就好。然后requirements-dev.txt里第一行写上-r requirements.txt这样开发环境会先装生产依赖再装开发工具。这个动作本身就是在为CI打基础——CI要装依赖它必须装的是和本地完全相同的版本组合否则测试结果就不可信。你越早接受锁定依赖是基本盘这件事后面流水线挂掉的概率就越低。2.2 虚拟环境在CI里的合理用法每次都从干净环境重建很多人问过我一个问题CI里到底要不要建虚拟环境答案是要而且每次都要从零建不要复用旧环境。原因很简单CI的作用之一是验证你从零开始能不能跑通。如果你在CI里复用了上一次跑的虚拟环境相当于默认了依赖不会发生变化这恰好掩盖了依赖声明不完整的问题。另一个原因是CI环境本身是不稳定的——跑在容器里的话每次都是新容器不存在保留上一次环境的说法。正确的做法是python -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install -r requirements-dev.txt然后后续所有步骤都在.venv环境下执行。有人觉得每次重建环境慢。是的慢但这就是为什么后面我要专门讲缓存策略——缓存pip的下载缓存而不是venv本身才是又稳又快的路径。先记住结论环境的可复现性优先于构建速度。速度可以通过缓存、并行、增量构建等其它方式弥补但如果环境不可复现速度再快也没有意义。2.3 Python版本的矩阵策略什么时候需要什么时候属于过度设计很多CI模板一上来就给你跑python-version: [3.8, 3.9, 3.10, 3.11, 3.12]的矩阵。看起来专业但如果你维护的是一个内部服务不是给外部用户用的开源库这种矩阵设计大概率是过度设计。我的建议分两种情况开源库 / SDK必须跑矩阵。你要对外承诺支持哪些Python版本就要在CI里测试这些版本。这是社区的基本信任。内部服务 / 单项目应用只需要跑一个或两个版本比如你在生产环境用的那个版本最多加一个开发主版本矩阵测试只是浪费时间和计算资源。以内部后端服务为例我会在CI里固定生产Python版本比如3.11最多再加一个3.12的小矩阵用来提前发现升级兼容性问题。不要贪多维护成本会随着矩阵数量线性上涨。3. 一套能直接落地的Python CI流水线骨架3.1 流水线的阶段划分和每一步的职责先看我给Python服务类项目设计的一套流水线结构这套结构我实际部署过稳定性很高lint format 检查代码风格、潜在bug的静态检查。test单元 集成跑pytest并把覆盖率报告存为CI制品。build构建Docker镜像或生成可分发产物tar包、wheel。publish把镜像或产物推送到私有仓库。deploy部署到测试/预发布/生产环境。这套结构和标准CI/CD长得像但每个阶段里对Python项目有很多特定的细节下面重点拆。3.2 Lint和格式化代码风格检查为什么也要自动化让CI帮你检查代码风格是一种无情的自动化。它的价值不在于挑你格式毛病而在于把代码风格的讨论从code review里彻底消灭掉——机器已经判断过的人不用再争了。Python生态最常用的组合是ruff black。以前我用的是flake8 black isort三件套但后来发现ruff一个工具就把 flake8、isort、pyupgrade、pydocstyle 这些全部替代了速度还快了不止一个量级。新项目直接上ruff老项目迁移也很简单。# 本地运行 ruff check . ruff format --check .我建议把这两条命令放到CI的lint阶段并在配置里把规则设定成你团队认可的强度。不要一开始就上全部规则都开的最大强度否则你会被一堆历史遗留问题淹没。务实做法是先开一组安全规则比如# pyproject.toml [tool.ruff] line-length 100 target-version py311 [tool.ruff.lint] select [E, F, W, I, UP, B, SIM] ignore [B008] # 视情况调整用Fpyflakes和Bbugbear就能抓住大部分真实bug了——未使用的导入、变量未定义、容易出错的模式。这些规则在CI里跑每一条报错都是一个真实的改进机会。3.3 测试阶段pytest的完整配置和覆盖率门禁测试阶段是Python CI里最有文章可写的部分。因为Python的测试生态特别丰富但同时也特别容易因为配置不当导致假阳性或假阴性——比如测试依赖外部服务、共享数据库状态、随机端口冲突等。我建议的pytest命令行是这样的pytest tests/ \ --disable-warnings \ --strict-markers \ --tbshort \ --capturesys \ -q \ --covsrc \ --cov-reportterm-missing \ --cov-reportxml:coverage.xml \ --junitxmlpytest.xml这里每个参数都有意义--strict-markers确保所有自定义marker都已注册防拼写错误。--tbshort失败时只输出精简回溯CI日志可读性更好。--covsrc只统计源码目录的覆盖率不把测试代码和虚拟环境算进去。--cov-reportxml:coverage.xml生成XML格式覆盖率报告后续可以上传到SonarQube或GitLab的测试报告界面。--junitxmlpytest.xml生成JUnit格式测试结果CI平台能直接展示哪些用例挂了、花多少时间。覆盖率门禁是我每次都要强调的点。光看测试用例数量没有意义覆盖率数字才是量化标准。但这里有个度的问题覆盖率设太高反而会逼团队写出无效测试。我见过很多团队为了把覆盖率从90%顶到95%写了一批只调用不断言的僵尸测试这样的覆盖率数字毫无意义。我的建议是设一个合理基线比如80%的总体行覆盖率 关键模块的100%关键路径覆盖并且用fail_under强制卡住# pyproject.toml [tool.coverage.run] branch true source [src] [tool.coverage.report] fail_under 80 skip_covered true show_missing true如果一段代码确实不需要测试比如外部SDK的封装、或者某些init逻辑用# pragma: no cover显式排除这比把全项目覆盖率硬顶上去要诚实得多。3.4 测试隔离如何让Python测试不依赖真实外部服务这一步是Python CI中最容易被低估的。当你的服务依赖数据库、Redis、Kafka或外部HTTP API时如果不做隔离CI里跑的测试常常出现本地过了、CI挂了的随机失败。解决这个问题有几种不同策略可以组合使用第一种用mocker/fake替换外部调用。这是最轻量、最快的方式适合单元测试。pytest-mock是标配。def test_order_creation(mocker): mock_client mocker.patch(src.payments.client.PaymentClient.charge) mock_client.return_value {status: success} ...但要注意mocker也不是万能的。它测的是你的代码在外部返回正常时行为正确一旦外部API的响应格式变了mocker完全发现不了。所以必须要有下一层验证。第二种用testcontainer起真实的依赖服务。在CI里用Docker启动一个真实的PostgreSQL或Redis容器测试全程直连真实服务。这比mock可靠得多能真正验证SQL语法、索引、连接池行为。Python生态里testcontainers库是首选。它可以在测试的fixture里拉起Docker容器测试结束自动销毁。import pytest from testcontainers.postgres import PostgresContainer pytest.fixture(scopesession) def db_engine(): with PostgresContainer(postgres:16-alpine) as postgres: engine create_engine(postgres.get_connection_url()) yield engine这里有个性能考量每次CI都重新拉镜像并启动数据库会增加不少时间。所以建议在测试阶段里只有集成测试marked asintegration才走testcontainer单元测试则保持纯本地运行用-m not integration排除。第三种用真实的外部服务副本/Sandbox。如果项目依赖某一个具体第三方API如支付网关在CI里用该服务商提供的sandbox环境跑回归测试。这个成本高适合核心业务链路使用。我个人的经验配置是单元测试不依赖任何外部服务→ 集成测试用testcontainer起真实中间件→ 核心链路的冒烟测试依赖sandbox。三层各司其职CI才稳得住。4. 依赖安装的速度与稳定性缓存的正确姿势4.1 缓存pip下载缓存而不是缓存虚拟环境前面说过CI里环境要每次重建但每次重建意味着每次都要重新下载一堆依赖包。网络好的时候还好网络差的时候一个Django项目装十五分钟很正常。所以缓存很关键但缓存什么是个讲究。我看很多人的CI配置是把.venv整个缓存起来然后每次进来直接复用。这样确实快但有一个很要命的问题它掩盖了依赖声明的错误。如果某个依赖其实没写进requirements里但在你本地环境碰巧装了一些包那么CI里会因为复用了旧venv而继续跑过但换一台新机器或者清掉缓存后就立刻暴露。而且venv缓存有一个跨平台问题Linux和macOS的venv不通用缓存缓存着反而容易因为哈希不匹配导致失效。正确做法是缓存pip的下载缓存而不是venv本身。pip默认会把下载的wheel包缓存在~/.cache/pip这个目录是平台无关的可以安全地跨构建复用。cache: key: $CI_COMMIT_REF_SLUG-${CI_PYTHON_VERSION} paths: - .pip-cache/安装时加上--cache-dirpip install --cache-dir.pip-cache -r requirements-dev.txt这样做的效果是第一次CI跑完所有依赖的wheel都躺在缓存目录里。第二次跑pip几乎秒装只要版本没变。而venv本身还是每次全新构建环境的正确性不被破坏。既快又稳这个组合才是对的。另外如果你用的Python版本很稳定、项目很大还可以考虑把pip的缓存路径直接用系统级的PIP_CACHE_DIR环境变量固定这样CI配置更简洁。4.2 依赖安装的告警处理什么时候必须fail我一直坚持在CI里把pip的warning当成error对待。因为很多warning其实暗示了未来的不兼容pip install --disable-pip-version-check --no-input \ --cache-dir.pip-cache \ -r requirements-dev.txt这里设了--disable-pip-version-check避免pip自己检查升级信息引入额外的网络请求和日志噪音。但有个更重要的点当某个依赖的Python版本即将不兼容时pip会给warning。如果你不在CI里fail等它真的坏了你才发现那时候临门一脚的紧迫感会逼着你去做rush修复效果一定不好。我建议加一个命令行环境变量PIP_DISABLE_PIP_VERSION_CHECK1以及如果你是用requirements.txt锁定版本的建议在CI里额外做一个检查锁文件是否过期的Job定期跑pip-compile --dry-run如果发现锁文件里有依赖和当前Python版本不兼容立刻报错提醒。这一步做在平时比踩坑时再修要划算太多。4.3 安装策略从源码构建的包怎么处理Python世界里有些包没有预编译的wheel只能在安装时从源码编译比如pydantic早期版本、某些使用C扩展的库。这类包安装很慢而且需要编译器。CI里如果没装编译器就会直接失败。两个解决办法用带编译器的镜像Docker基础镜像用python:3.11-slim的时候需要装build-essential但更省事的是直接用官方镜像python:3.11基于Debian自带编译工具链。缺点是镜像大一点。用--only-binary :all:强制只用wheel如果某个包只有源码版说明它还不太成熟或者环境兼容性差这样直接fail掉反而能提前暴露问题。我把这两条的结合方案写在这里# 先尝试只用wheel安装 pip install --only-binary :all: -r requirements.txt 2/dev/null \ || pip install -r requirements.txt这个写法保证只要所有包都有wheel就用纯wheel安装又快又稳如果有包没有wheel就退回去正常安装但构建日志里会有编译过程。不过这种fallback写法会掩盖谁在偷偷编译的问题所以我更推荐的是在一次CI里显式地列出哪些包用了源码安装方便维护者评估。可以在流水线里加一步pip install --reportinstall_report.json -r requirements-dev.txt python - EOF import json with open(install_report.json) as f: data json.load(f) for item in data[install]: if item[metadata].get(metadata_version) ... EOF如果不想搞这么复杂就用简单的pip freeze对比一下或者在CI日志里grepBuilding wheel出现就说明有源码编译标记为注意项。5. 构建产物从“一个Python脚本”到“一个可部署镜像”5.1 为什么要容器化Python项目的交付陷阱Python项目的交付一直是老大难问题。你写了一个脚本给同事运行结果同事机器上没有装依赖、Python版本不对、系统库缺失于是一个应该可以直接跑的东西变成了你的环境有问题吧的扯皮现场。容器化解决了这一点把所有运行环境打包在镜像里任何环境下跑出来的行为都一致。对于CI/CD来说这就是构建产物的标准形态。我建议无论你最终部署到哪里都要把构建Docker镜像这一步放到CI里来。好处有几个构建过程可复现每次CI用相同的Dockerfile、相同的依赖锁文件构建出可重复的镜像。构建环境干净不会因为你本机装了奇怪的东西而让镜像大小变大、行为变化。统一出口部署时只需要拉镜像 跑容器不再需要关心代码状态。5.2 多阶段Dockerfile把镜像从1.2GB压到200MBPython镜像做多阶段构建是标配。第一个阶段builder用来装依赖第二个阶段runtime只拷贝必要的文件和已安装的依赖这样最终镜像不包含编译工具链和中间缓存体积能小一个数量级。我常用的模板长这样# syntaxdocker/dockerfile:1.4 FROM python:3.11-slim AS builder WORKDIR /app ENV PIP_DEFAULT_TIMEOUT100 \ PIP_DISABLE_PIP_VERSION_CHECK1 \ PIP_NO_CACHE_DIR1 COPY requirements.txt . RUN pip install --prefix/install -r requirements.txt FROM python:3.11-slim AS runtime WORKDIR /app ENV PYTHONUNBUFFERED1 \ PYTHONDONTWRITEBYTECODE1 \ PATH/install/bin:$PATH # 如果项目需要系统级库在这里装 # RUN apt-get update apt-get install -y --no-install-recommends ... rm -rf /var/lib/apt/lists/* COPY --frombuilder /install /install COPY src/ /app/src/ COPY alembic/ /app/alembic/ # 如果有迁移脚本 COPY pyproject.toml /app/ EXPOSE 8000 CMD [uvicorn, src.main:app, --host, 0.0.0.0, --port, 8000]这里关键点用--prefix/install把依赖装到单独的目录这样最终阶段只需要拷贝/install目录。PYTHONDONTWRITEBYTECODE避免生成__pycache__镜像更干净。PYTHONUNBUFFERED确保日志实时输出不卡在缓冲区里容器日志才能及时采集到。很多人会问为什么不直接把所有文件COPY进去然后把整个venv也复制因为那样会把编译缓存、临时文件、甚至你的测试用例一起打进去体积大又不安全。多阶段构建的哲学是最终镜像只包含运行所必需的最小集。5.3 镜像Tag策略commit SHA 与语义化版本怎么配合镜像Tag策略是个看起来无所谓、实际影响部署回滚效率的关键设计。我见过两种极端只用latest回滚时完全不知道哪次部署对应哪个代码版本等于自断后路。不推荐。只用短commit SHA能定位代码但无法表达这是v1.2.0的release这种语义。正确姿势是开发/测试环境用commit SHA当Tag生产环境同时打上语义化版本Tag如v1.2.0和commit SHA Tag。- docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:$CI_COMMIT_TAG 2/dev/null || true - docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:latest - docker push $IMAGE_NAME:$CI_COMMIT_SHA - docker push $IMAGE_NAME:$CI_COMMIT_TAG 2/dev/null || true这样做的意义是commit SHA保证任何一次部署都能精确定位代码版本语义化版本Tag保证我在发版时能明确表达这是哪个重大release遇到问题可以直接回滚到上一个release Tag。另外一个重要习惯是每次部署前保存一份当前部署的镜像Tag commit SHA的映射关系比如存到JSON文件或部署记录里这样回滚时直接把镜像Tag指向上一个值即可不需要重新构建。5.4 数据库迁移Python项目CD中最容易被忽略的一环大部分Python Web项目Django、Flask SQLAlchemy、FastAPI Alembic都有数据库迁移脚本。部署时有一个经典的顺序问题先迁移还是先发布新代码答案是取决于迁移是否向后兼容。但现实是很多团队并不严格验证这件事。我建议在CD阶段把迁移拆成独立的一步并放到发布前执行同时给迁移步骤加一个明确的安全门禁如果迁移是纯向后兼容的加字段、加索引、加新表先迁移再发布代码。如果迁移涉及破坏性变更删字段、改约束需要采用扩展-迁移-收缩expand-migrate-contract模式先发一版兼容新旧代码的迁移等所有旧版本代码下线后再做清理迁移。在CI里数据库迁移步骤要做两件事在测试环境的数据库上跑一遍迁移确保迁移脚本本身没问题。生产部署前把迁移脚本作为发布Job的一部分执行并且做到幂等同类迁移重复执行不会报错。Alembic的迁移脚本天然支持upgrade head但你需要确保你的环境变量配置数据库连接串在CI和运行时之间一致这是最容易出问题的地方。alembic upgrade head我一般会把迁移Job做成一个手动触发的步骤放在生产部署之前给它一个审批关卡。不是所有项目都需要这个审批但如果是核心服务这一步能拦住很多低级事故。6. 从CI到CD自动化部署的落地与多环境管理6.1 部署方式选型Docker单机、Kubernetes还是Serverless部署目标决定你的CD配置复杂度。我这里主要基于最常见的Docker单机 Docker Compose的场景来展开因为这个场景对中小团队最真实。单机Docker部署的CD流程其实很简洁CI构建镜像 → 推到私有镜像仓库 → SSH到服务器 → 拉镜像 → 重启容器。这个流程用GitLab CI或GitHub Actions都能轻松实现。deploy-staging: stage: deploy environment: name: staging url: https://staging.example.com before_script: - command -v ssh-agent /dev/null || apt-get install openssh-client -y - eval $(ssh-agent -s) - echo $STAGING_SERVER_PRIVATE_KEY | tr -d \r | ssh-add - /dev/null script: - ssh -o StrictHostKeyCheckingno $STAGING_SERVER_USER$STAGING_SERVER_IP cd /srv/myapp docker pull $IMAGE_NAME:$CI_COMMIT_SHA docker compose -f docker-compose.prod.yml up -d --force-recreate --remove-orphans 这里有一个非常关键的细节不要在服务器上跑docker build而是只docker pull。因为生产服务器不应该有构建代码的能力它只会从镜像仓库拉取已经构建好的、经过测试的镜像。这保证了测试过的东西就是部署的东西而不是在服务器上重新构建然后期望结果一致。如果你有多个服务器或直接上Kubernetes那CD流程会变成更新Deployment镜像版本kubectl set image deployment/myapp myapp...:tag更复杂但也更标准化。这个不在本文展开但核心理念一致部署 指定镜像版本 触发更新 健康检查 回滚预案。6.2 多环境策略开发、测试、预发布、生产怎么串起来环境管理是CD里最容易被搞成一团浆糊的地方。很多团队的测试环境和开发环境混用预发布环境根本没人维护生产环境更是只敢手动部署。规范的做法是这样的环境触发方式数据用途回滚风险developmentpush到feature分支造数据开发者自测无stagingmerge到main分支模拟数据集成测试、评审低pre-production打语义化版本Tag生产脱敏副本发布前最终验证中production手动触发 审批生产数据真实用户高这四层不是所有项目都需要。如果项目规模小至少要有staging和production两层。staging环境要和production尽量一致依赖版本、配置、部署方式否则staging测出的结果没有参考价值。在GitLab CI里环境相关的配置要放到environment关键字里并且部署步骤要加上when: manual和allow_failure: false确保生产部署是显式手动触发的deploy-prod: stage: deploy when: manual environment: name: production url: https://example.com script: - ...这样做的好处是你永远清楚当前哪个commit正在生产运行而不会因为某个分支的自动部署把生产环境搞乱。6.3 配置与密钥管理绝不要把密钥写进镜像Python项目在部署时会遇到一个敏感话题数据库密码、API Key、JWT密钥怎么管理。很多教程里会把配置直接写进Dockerfile的ENV这是绝对错误做法——镜像是可以被拉取和检查的密钥一旦进去就等于泄露。推荐的方案是用CI/CD平台的变量功能在GitLab的Settings - CI/CD - Variables里配置密钥在部署时以环境变量注入。用密钥管理服务比如Vault、AWS Secrets Manager、阿里云KMS在应用启动时动态获取。适合安全要求较高的场景。用Docker secret如果用的是Docker Swarm或Docker Compose 3.1可以用secrets文件挂载。我个人的折衷做法是测试/预发布环境的密钥放CI变量生产环境的密钥放密钥管理服务应用启动时读取。好处是测试环境的密钥就算泄露了影响也有限生产环境密钥永不落盘、永不进镜像。以Docker Compose 密钥管理服务为例部署脚本里可以这样拉取密钥并写入一个临时环境文件export DATABASE_URL$(vault kv get -fieldurl secret/myapp/prod) export REDIS_URL$(vault kv get -fieldurl secret/myapp/prod-redis) docker compose -f docker-compose.prod.yml up -d --force-recreate如果你不想引入Vault这类重组件至少也要保证CI变量不在流水线日志中打印出来部署脚本不把密钥写入项目目录下的任何文件。这是底线。6.4 健康检查与自动回滚部署完不等于部署成功部署脚本执行完docker compose up -d并不代表部署成功。容器可能启动后立刻崩掉、可能健康检查失败、可能服务起不来但容器还在。所以CD的最后一环必须是健康检查而且是应用层的健康检查。在Docker Compose配置里加healthcheckservices: web: image: registry.example.com/myapp:${IMAGE_TAG} healthcheck: test: [CMD, curl, -f, http://localhost:8000/healthz] interval: 30s timeout: 5s retries: 3 start_period: 30s然后在部署脚本里等待健康for i in $(seq 1 10); do if curl -fsS http://localhost:8000/healthz /dev/null 21; then echo Health check passed. exit 0 fi sleep 5 done echo Health check failed, rolling back... docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d --force-recreate web exit 1这里我用了一个简单粗暴的回滚方式docker compose up -d --force-recreate会重新拉取之前正在运行的镜像Tag并重建容器这相当于回到上一次部署的状态。前提是你部署时明确记录了上一个Tag是什么——这就是前面提到的镜像Tag策略的重要性了。7. Python CI/CD实战中的坑与优化建议7.1 我踩过的三个典型坑第一个坑时间相关的测试在CI里不稳定。本地跑测试时时区是东八区CI容器默认是UTC。如果你的测试断言里用了本地时间相关逻辑本地绿、CI红的诡异现象就会频繁出现。解决方法是在所有测试里显式设置时区或者用freezegun这类库冻结时间不要依赖CI环境的默认时区。第二个坑依赖包在CI里装出不同版本。我遇到过一次requests从2.30.0升级到2.31.0内部的一个SSL默认行为变了导致集成测试全挂。那次之后我才意识到必须锁版本并且把依赖锁定是否一致放进了CI检查项。如果你现在还没有锁依赖版本读到这里就赶紧去把锁文件提交到仓库里。第三个坑CI日志太长导致关键信息被淹没。pytest输出几千行失败信息被冲掉。后来我把pytest的日志格式改成-q--tbline关键回溯只有一行一眼能看出问题。不要舍不得删日志CI的输出越精简定位问题的效率越高。7.2 流水线性能优化从15分钟压到5分钟我在一个中等规模的Django项目上做过一次CI优化把平均流水线耗时从15分钟压到5分钟核心动作是缓存pip下载缓存从每次3分钟降到20秒。这个最直接。并行化Joblint、单元测试、镜像构建三个不互相依赖的阶段并发跑。GitLab CI和GitHub Actions都支持needs关键字控制依赖关系。只用wheel安装避免源码编译省掉C扩展编译的时间。减少Docker镜像层数把依赖安装和源码拷贝分开这样源码变了只需要重建最后一层。跳过不需要的环境如果一个Job不测某个Python版本就不要在矩阵里包含它。每一条都会真实反映在流水线的总时长上。如果你正在被CI跑太慢困扰按这个顺序优化性价比极高。7.3 几条经验性的建议最后再啰嗦一下第一CI/CD是给团队用的基础设施不是炫技场。优先选择大家都会用的工具别为了新而新。第二所有自动化的规则都要有对应的打破规则的手段。比如覆盖率门禁卡住了应该允许管理员手动放行但要留下记录和理由。自动化的目的是提高效率不是制造流程障碍。第三持续改进比一步到位重要。先把一套最小可用的流水线跑通再逐步加质量门禁、部署步骤、安全扫描。流水线的复杂度应该跟项目的成熟度匹配没必要一上来就搭建一个庞大的体系那样只会让大家排斥用CI。第四我在实际使用中发现最能提升团队CI/CD体验的往往不是那些花哨的插件而是几个最基本的习惯写清楚每个Job的职责、控制流水线的并行度、保留关键日志。把这三件事做扎实再用任何CI平台都很顺手。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进