容器镜像漏洞扫描拖垮 CI:我把构建时间从 20 分钟压回 3 分钟的排查实录
容器镜像漏洞扫描拖垮 CI我把构建时间从 20 分钟压回 3 分钟的排查实录上周三下午我正准备把一个新功能合并到主分支CI 突然跑了一个多小时还没结束。平时 3 分钟的构建硬生生拖到了 20 分钟。翻了一圈日志罪魁祸首是两周前接入的容器镜像漏洞扫描。说实话安全团队的初衷是好的。但把扫描直接塞进构建流程没做任何优化这跟在高速公路入口设检查站检查每辆车的底盘没啥区别——堵是必然的。问题是怎么发现的事情要从安全合规说起。公司安全团队要求所有上线镜像必须通过漏洞扫描Severity High 及以上必须清零。我花了一个下午把 Trivy 接进了 GitLab CI配置看起来挺简单scan_image:stage:testimage:aquasec/trivy:latestscript:-trivy image--exit-code 1--severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA第一遍跑通了我还挺得意。但接下来的几天每次推送代码都要等 20 分钟。更离谱的是开发人员开始抱怨本地分支的 CI 也慢了因为 merge request 触发的 pipeline 同样要跑完整扫描。我拉了一下构建耗时数据扫描阶段占了整个 pipeline 的 78%。Trivy 每次都要重新下载漏洞数据库然后对镜像逐层扫描。我们的镜像基于一个 Ubuntu 基础镜像一层叠一层Trivy 把每一层都拆开来分析。再加上没有缓存每次都是从零开始。划重点漏洞扫描不是不能慢但慢在构建流程里就是致命伤。排查时间线我按时间线记录了整个排查过程方便后续复盘。T0 分钟确认瓶颈先看了 GitLab CI 的 stage 耗时。build 阶段 2 分 30 秒scan 阶段 17 分钟。问题明确扫描是瓶颈。# 快速查看各 stage 耗时gl-ci-trace|grep-E(build|scan|test)|awk{print $1, $NF}T5 分钟分析 Trivy 扫描日志Trivy 的日志里反复出现Downloading DB...和Analyzing layer...。我注意到漏洞数据库下载占了 4 分钟镜像分析占了 13 分钟。# 本地复现加上时间戳timetrivy image--severityHIGH,CRITICAL my-app:latest本地跑了一遍结果一样数据库下载慢镜像分析慢。T15 分钟定位两个根因第一个根因Trivy 默认从 GitHub 下载漏洞数据库网络不稳定时经常超时重试。第二个根因我们镜像层数太多而且有很多重复文件Trivy 的逐层分析策略在这种情况下效率极低。T25 分钟制定三层修复方案我画了一个简单的决策树要么让扫描变快要么把扫描从构建流程里挪出去或者两者都做。最后决定三层一起上缓存漏洞数据库、并行扫描、分层扫描策略。解决方案三层修复第一层缓存漏洞数据库Trivy 的漏洞数据库可以缓存到本地或者 registry。我在 CI 里加了一个缓存 job每天只更新一次数据库其他 job 直接使用缓存。.trivy_cache:cache:key:trivy-db-${CI_COMMIT_REF_SLUG}paths:-.cache/trivybefore_script:-mkdir-p .cache/trivy-export TRIVY_CACHE_DIR.cache/trivy-|if [ ! -f .cache/trivy/db/trivy.db ]; then echo Cache miss, downloading DB... trivy image --download-db-only else echo DB cache hit, skipping download fi这一步把数据库下载时间从 4 分钟压到了 10 秒。第二层并行扫描 分层策略原来我们扫描的是最终构建完成的镜像。但实际上很多漏洞来自基础镜像业务代码层很少有高危漏洞。我改成了先扫描基础镜像再扫描业务层两者并行。scan_base_image:stage:testimage:aquasec/trivy:latestscript:-trivy image--severity HIGH,CRITICAL--skip-dirs /app ubuntu:22.04allow_failure:truescan_app_layer:stage:testimage:aquasec/trivy:latestscript:-trivy filesystem--severity HIGH,CRITICAL /apponly:-merge_requestsfilesystem模式扫描的是构建上下文而不是完整镜像。对于业务代码变更这通常就够了。只有基础镜像升级时才触发完整镜像扫描。第三层优化镜像本身扫描慢还有一个原因镜像太大了。我花了一个小时把镜像从 Ubuntu 换成了 distroless又砍掉了一些不必要的构建依赖。# 优化前Ubuntu 基础镜像层数多 FROM ubuntu:22.04 RUN apt-get update apt-get install -y python3 python3-pip COPY requirements.txt . RUN pip install -r requirements.txt COPY . /app # 优化后多阶段构建 distroless FROM python:3.11-slim as builder COPY requirements.txt . RUN pip install --user -r requirements.txt FROM gcr.io/distroless/python3 COPY --frombuilder /root/.local /home/nonroot/.local COPY --chownnonroot:nonroot . /app WORKDIR /app ENV PATH/home/nonroot/.local/bin:$PATH镜像从 1.2GB 压到了 180MB。镜像小了Trivy 扫描自然也少了。踩坑记录坑 1Trivy cache 目录权限GitLab runner 的 cache 目录默认权限是 root但 Trivy 容器里跑的是非 root 用户。结果缓存写进去了Trivy 读不出来。解决方式是统一用root用户跑 Trivy或者调整 cache 目录权限。scan_image:image:name:aquasec/trivy:latestentrypoint:[]script:-trivy image--cache-dir /tmp/trivy-cache...坑 2–skip-dirs 不生效我一开始用--skip-dirs /app想跳过业务代码目录结果发现 Trivy 的image模式里--skip-dirs只对filesystem模式有效。换成filesystem扫描构建上下文才真正跳过不需要的目录。坑 3并行扫描的依赖顺序scan_base_image和scan_app_layer放在同一个 stage但scan_app_layer其实依赖 build 阶段的产物。我一开始把它们放错了 stage导致scan_app_layer跑的时候构建产物还没生成。调整 stage 顺序后解决。坑 4distroless 的调试困难distrioless 镜像里没有 shell调试起来很费劲。我最后保留了一个debug版本的 Dockerfile本地调试用debug标签线上用distroless标签。# 本地调试dockerbuild-tmy-app:debug-fDockerfile.debug.# 线上部署dockerbuild-tmy-app:latest-fDockerfile.最终效果三层修复下来CI 构建时间从 20 分钟压回了 3 分钟甚至更短。优化项优化前优化后节省漏洞数据库下载4 分钟10 秒~3.8 分钟镜像扫描13 分钟45 秒~12 分钟镜像构建3 分钟2 分钟1 分钟总计20 分钟3 分钟17 分钟更重要的是开发人员不再抱怨 CI 慢了。安全合规和开发效率这两件事终于可以和平共处了。写在最后这件事给我最大的教训是安全工具接入不能简单粗暴。直接把 Trivy 塞进构建流程不考虑缓存、不考虑分层、不考虑镜像体积结果就是开发效率和安全合规两边都不讨好。如果你是安全团队建议把扫描放在独立的 stage并且给开发团队留足缓存策略。如果你是开发团队接入扫描工具的时候先本地跑一遍time trivy image看看瓶颈到底在哪。说到底DevSecOps 不是把安全工具堆进 CI 就完事了。安全左移没问题但左移不等于把负担全扔给开发。找到一个让两边都能接受的方案才是真正的工程实践。如果你也在 CI 里踩过扫描的坑欢迎评论区交流。踩坑的路上你并不孤单。